diff --git a/lib/MemberBuilder.js b/lib/MemberBuilder.js new file mode 100644 index 0000000..60dbf1f --- /dev/null +++ b/lib/MemberBuilder.js @@ -0,0 +1,663 @@ +/** + * Handles building members (properties, methods) + * + * Copyright (C) 2010 Mike Gerwitz + * + * This file is part of ease.js. + * + * ease.js is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @author Mike Gerwitz + * @package core + */ + +var util = require( __dirname + '/util' ), + + Warning = require( __dirname + '/warn' ).Warning, + + fallback = util.definePropertyFallback(), + visibility = [ 'public', 'protected', 'private' ] +; + + +module.exports = function MemberBuilder() +{ + // permit omitting 'new' keyword + if ( !( this instanceof module.exports ) ) + { + return new module.exports(); + } +}; + + +// we're throwing everything into the prototype +exports = module.exports.prototype; + + +/** + * Initializes member object + * + * The member object contains members for each level of visibility (public, + * protected and private). + * + * @param {Object} mpublic default public members + * @param {Object} mprotected default protected members + * @param {Object} mprivate default private members + * + * @return {{public: Object, protected: Object, private: Object}} + */ +exports.initMembers = function( mpublic, mprotected, mprivate ) +{ + return { + 'public': mpublic || {}, + 'protected': mprotected || {}, + 'private': mprivate || {}, + }; +}; + + +/** + * Copies a method to the appropriate member prototype, depending on + * visibility, and assigns necessary metadata from keywords + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object} meta metadata container + * @param {string} name property name + * @param {*} value property value + * + * @param {Object.} keywords parsed keywords + + * @param {Object=} instCallback function to call in order to retrieve + * object to bind 'this' keyword to + * @param {number} cid class id + * + * @return {undefined} + */ +exports.buildMethod = function( + members, meta, name, value, keywords, instCallback, cid, base +) +{ + // TODO: We can improve performance by not scanning each one individually + // every time this method is called + var prev_data = scanMembers( members, name, base ), + prev = ( prev_data ) ? prev_data.member : null, + prev_keywords = ( prev && prev.___$$keywords$$ ), + dest = getMemberVisibility( members, keywords ); + ; + + // ensure that the declaration is valid (keywords make sense, argument + // length, etc) + validateMethod( keywords, prev_data, prev_keywords, value, name ); + + // we might be overriding an existing method + if ( prev ) + { + // by default, perform method hiding, even if the keyword was not + // provided (the keyword simply suppresses the warning) + var operation = hideMethod; + + // TODO: warning if no super method when override keyword provided + if ( keywords[ 'override' ] || prev_keywords[ 'abstract' ] ) + { + // override the method + operation = overrideMethod; + } + + dest[ name ] = operation( prev, value, instCallback, cid ); + } + else if ( keywords[ 'abstract' ] ) + { + // we do not want to wrap abstract methods, since they are not callable + dest[ name ] = value; + } + else + { + // we are not overriding the method, so simply copy it over, wrapping it + // to ensure privileged calls will work properly + dest[ name ] = overrideMethod( value, null, instCallback, cid ); + } + + // store keywords for later reference (needed for pre-ES5 fallback) + dest[ name ].___$$keywords$$ = keywords; +}; + + +/** + * Validates a method declaration, ensuring that keywords are valid, overrides + * make sense, etc. + * + * @param {Object.} keywords parsed keywords + * + * @param {Object} prev_data data of member being overridden + * @param {Object} prev_keywords keywords of member being overridden + * @param {*} value property value + * @param {string} name property name + */ +function validateMethod( keywords, prev_data, prev_keywords, value, name ) +{ + var prev = ( prev_data ) ? prev_data.member : null; + + if ( keywords[ 'abstract' ] ) + { + // do not permit private abstract methods (doesn't make sense, since + // they cannot be inherited/overridden) + if ( keywords[ 'private' ] ) + { + throw TypeError( + "Method '" + name + "' cannot be both private and abstract" + ); + } + } + + // const doesn't make sense for methods; they're always immutable + if ( keywords[ 'const' ] ) + { + throw TypeError( + "Cannot declare method '" + name + "' as constant; keyword is " + + "redundant" + ); + } + + // virtual static does not make sense, as static methods cannot be + // overridden + if ( keywords[ 'virtual' ] && ( keywords[ 'static' ] ) ) + { + throw TypeError( + "Cannot declare static method '" + name + "' as virtual" + ); + } + + // do not allow overriding getters/setters + if ( prev_data && ( prev_data.get || prev_data.set ) ) + { + throw TypeError( + "Cannot override getter/setter '" + name + "' with method" + ); + } + + // search for any previous instances of this member + if ( prev ) + { + // disallow overriding properties with methods + if ( !( prev instanceof Function ) ) + { + throw TypeError( + "Cannot override property '" + name + "' with method" + ); + } + + // disallow overriding non-virtual methods + if ( keywords[ 'override' ] && !( prev_keywords[ 'virtual' ] ) ) + { + throw TypeError( + "Cannot override non-virtual method '" + name + "'" + ); + } + + // do not allow overriding concrete methods with abstract + if ( keywords[ 'abstract' ] && !( util.isAbstractMethod( prev ) ) ) + { + throw TypeError( + "Cannot override concrete method '" + name + "' with " + + "abstract method" + ); + } + + // ensure parameter list is at least the length of its supertype + if ( ( value.__length || value.length ) + < ( prev.__length || prev.length ) + ) + { + throw TypeError( + "Declaration of method '" + name + "' must be compatiable " + + "with that of its supertype" + ); + } + + // do not permit visibility deescalation + if ( prev_data.visibility < getVisibilityValue( keywords ) ) + { + throw TypeError( + "Cannot de-escalate visibility of method '" + name + "'" + ); + } + + // if redefining a method that has already been implemented in the + // supertype, the default behavior is to "hide" the method of the + // supertype, unless otherwise specified + // + // IMPORTANT: do this last, to ensure we throw errors before warnings + if ( !( keywords[ 'new' ] || keywords[ 'override' ] ) ) + { + if ( !( prev_keywords[ 'abstract' ] ) ) + { + throw Warning( Error( + "Hiding method '" + name + "'; " + + "use 'new' if intended, or 'override' to override instead" + ) ); + } + } + } +} + + +/** + * Copies a property to the appropriate member prototype, depending on + * visibility, and assigns necessary metadata from keywords + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object} meta metadata container + * @param {string} name property name + * @param {*} value property value + * + * @param {Object.} keywords parsed keywords + + * @param {Object=} base optional base object to scan + * + * @return {undefined} + */ +exports.buildProp = function( members, meta, name, value, keywords, base ) +{ + // TODO: We can improve performance by not scanning each one individually + // every time this method is called + var prev_data = scanMembers( members, name, base ), + prev = ( prev_data ) ? prev_data.member : null; + + // disallow overriding methods with properties + if ( prev instanceof Function ) + { + throw new TypeError( + "Cannot override method '" + name + "' with property" + ); + } + + // do not allow overriding getters/setters + if ( prev_data && ( prev_data.get || prev_data.set ) ) + { + throw TypeError( + "Cannot override getter/setter '" + name + "' with property" + ); + } + + // do not permit visibility de-escalation + if ( prev && ( prev_data.visibility < getVisibilityValue( keywords ) ) ) + { + throw TypeError( + "Cannot de-escalate visibility of property '" + name + "'" + ); + } + + // abstract properties do not make sense + if ( keywords[ 'abstract' ] ) + { + throw TypeError( + "Property '" + name + "' cannot be declared as abstract" + ); + } + + if ( keywords[ 'static' ] && keywords[ 'const' ] ) + { + throw TypeError( + "Static keyword cannot be used with const for property '" + + name + "'" + ); + } + + // properties are inherently virtual + if ( keywords['virtual'] ) + { + throw TypeError( "Cannot declare property '" + name + "' as virtual" ); + } + + getMemberVisibility( members, keywords )[ name ] = [ value, keywords ]; +}; + + +/** + * Copies a getter to the appropriate member prototype, depending on + * visibility, and assigns necessary metadata from keywords + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object} meta metadata container + * @param {string} name getter name + * @param {*} value getter value + * + * @param {Object.} keywords parsed keywords + * + * @param {Object=} base optional base object to scan + * + * @return {undefined} + */ +exports.buildGetter = function( members, meta, name, value, keywords, base ) +{ + validateGetterSetter( members, keywords, name, base ); + + Object.defineProperty( + getMemberVisibility( members, keywords ), + name, + { + get: value, + enumerable: true, + + // otherwise we can't add a setter to this + configurable: true, + } + ); +}; + + +/** + * Copies a setter to the appropriate member prototype, depending on + * visibility, and assigns necessary metadata from keywords + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object} meta metadata container + * @param {string} name setter name + * @param {*} value setter value + * + * @param {Object.} keywords parsed keywords + * + * @param {Object=} base optional base object to scan + * + * @return {undefined} + */ +exports.buildSetter = function( members, meta, name, value, keywords, base ) +{ + validateGetterSetter( members, keywords, name, base ); + + Object.defineProperty( + getMemberVisibility( members, keywords ), + name, + { + set: value, + enumerable: true, + + // otherwise we can't add a getter to this + configurable: true, + } + ); +}; + + +/** + * Performs common validations on getters/setters + * + * If a problem is found, an exception will be thrown. + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object.} keywords parsed keywords + * @param {string} name getter/setter name + * @param {Object} base optional base to parse + */ +function validateGetterSetter( members, keywords, name, base ) +{ + var prev_data = scanMembers( members, name, base ), + prev = ( prev_data ) ? prev_data.member : null, + + prev_keywords = ( prev && prev.___$$keywords$$ ) + ? prev.___$$keywords$$ + : {} + ; + + if ( prev ) + { + // To speed up the system we'll simply check for a getter/setter, rather + // than checking separately for methods/properties. This is at the + // expense of more detailed error messages. They'll live. + if ( !( prev_data.get || prev_data.set ) ) + { + throw TypeError( + "Cannot override method or property '" + name + + "' with getter/setter" + ); + } + } +} + + +/** + * Returns member prototype to use for the requested visibility + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {Object.} keywords parsed keywords + * + * @return {Object} reference to visibility of members argument to use + */ +function getMemberVisibility( members, keywords ) +{ + var viserr = function() + { + throw TypeError( + "Only one of public, protected or private may be used" + ); + } + + // there's cleaner ways of doing this, but consider it loop unrolling for + // performance + if ( keywords[ 'private' ] ) + { + ( keywords[ 'public' ] || keywords[ 'protected' ] ) && viserr(); + return members[ 'private' ]; + } + else if ( keywords[ 'protected' ] ) + { + ( keywords[ 'public' ] || keywords[ 'private' ] ) && viserr(); + return members[ 'protected' ]; + } + else + { + // public keyword is the default, so explicitly specifying it is only + // for clarity + ( keywords[ 'private' ] || keywords[ 'protected' ] ) && viserr(); + return members[ 'public' ]; + } +} + + +/** + * Scan each level of visibility for the requested member + * + * @param {{public: Object, protected: Object, private: Object}} members + * + * @param {string} name member to locate + * @param {Object=} base optional base object to scan + * + * @return {Object} Array of member and number corresponding to visibility, + * level if located, otherwise an empty object + */ +function scanMembers( members, name, base ) +{ + var i = visibility.length, + member = null; + + // locate requested member by scanning each level of visibility + while ( i-- ) + { + var visobj = members[ visibility[ i ] ]; + + // In order to support getters/setters, we must go off of the + // descriptor. We must also ignore base properties (last argument), such + // as Object.prototype.toString(). However, we must still traverse the + // prototype chain. + if ( member = util.getPropertyDescriptor( visobj, name, true ) ) + { + return { + get: member.get, + set: member.set, + member: member.value, + visibility: ( ( fallback ) ? 0 : i ), + }; + } + } + + // if a second comparison object was given, try again using it instead of + // the original members object + if ( base !== undefined ) + { + var base_methods = base.___$$methods$$, + base_props = base.___$$props$$; + + // scan the base's methods and properties, if they are available + return ( base_methods && scanMembers( base_methods, name ) ) + || ( base_props && scanMembers( base_props, name ) ) + || null + ; + } + + // nothing was found + return null; +} + + +/** + * Hide a method with a "new" method + */ +function hideMethod( super_method, new_method, instCallback, cid ) +{ + // TODO: This function is currently unimplemented. It exists at present to + // provide a placeholder and ensure that the override keyword is required to + // override a parent method. +} + + +/** + * Generates a method override function + * + * The override function simply wraps the method so that its invocation will + * pass a __super property. This property may be used to invoke the overridden + * method. + * + * @param {function()} super_method method to override + * @param {function()} new_method method to override with + * + * @param {Object=} instCallback function to call in order to retrieve + * object to bind 'this' keyword to + * @param {number} cid class id + * + * @return {function()} override method + */ +function overrideMethod( super_method, new_method, instCallback, cid ) +{ + instCallback = instCallback || function() {}; + + // return a function that permits referencing the super method via the + // __super property + var override = null; + + // are we overriding? + if ( new_method ) + { + override = function() + { + var context = instCallback( this, cid ) || this, + retval = undefined + ; + + // the _super property will contain the parent method (we don't + // store the previous value for performance reasons and because, + // during conventional use, it's completely unnecessary) + context.__super = super_method; + + retval = new_method.apply( context, arguments ); + + // prevent sneaky bastards from breaking encapsulation by stealing + // method references (we set to undefined rather than deleting it + // because deletion causes performance degradation within V8) + context.__super = undefined; + + // if the value returned from the method was the context that we + // passed in, return the actual instance (to ensure we do not break + // encapsulation) + if ( retval === context ) + { + return this; + } + + return retval; + }; + } + else + { + // we are defining a new method + override = function() + { + var context = instCallback( this, cid ) || this, + retval = undefined + ; + + // invoke the method + retval = super_method.apply( context, arguments ); + + // if the value returned from the method was the context that we + // passed in, return the actual instance (to ensure we do not break + // encapsulation) + if ( retval === context ) + { + return this; + } + + return retval; + }; + } + + // This is a trick to work around the fact that we cannot set the length + // property of a function. Instead, we define our own property - __length. + // This will store the expected number of arguments from the super method. + // This way, when a method is being overridden, we can check to ensure its + // compatibility with its super method. + util.defineSecureProp( override, + '__length', + ( super_method.__length || super_method.length ) + ); + + return override; +} + + +/** + * Return the visibility level as a numeric value, where 0 is public and 2 is + * private + * + * @param {Object} keywords keywords to scan for visibility level + * + * @return {number} visibility level as a numeric value + */ +function getVisibilityValue( keywords ) +{ + if ( fallback ) + { + // if we have to fall back, we don't support levels of visibility + return 0; + } + else if ( keywords[ 'protected' ] ) + { + return 1; + } + else if ( keywords[ 'private' ] ) + { + return 2; + } + else + { + // default is public + return 0; + } +} + diff --git a/lib/class.js b/lib/class.js index 0c1a118..6e1585f 100644 --- a/lib/class.js +++ b/lib/class.js @@ -26,7 +26,7 @@ var util = require( __dirname + '/util' ), ClassBuilder = require( __dirname + '/ClassBuilder' ), class_builder = ClassBuilder( - require( __dirname + '/member_builder' ), + require( __dirname + '/MemberBuilder' )(), require( __dirname + '/VisibilityObjectFactoryFactory' ) .fromEnvironment() ) diff --git a/lib/interface.js b/lib/interface.js index 4f419de..b1de6ac 100644 --- a/lib/interface.js +++ b/lib/interface.js @@ -23,7 +23,7 @@ */ var util = require( __dirname + '/util' ), - member_builder = require( __dirname + '/member_builder' ), + member_builder = require( __dirname + '/MemberBuilder' )(), Class = require( __dirname + '/class' ); diff --git a/test/test-class_builder-const.js b/test/test-class_builder-const.js index c61ad15..0e88666 100644 --- a/test/test-class_builder-const.js +++ b/test/test-class_builder-const.js @@ -25,7 +25,7 @@ var common = require( './common' ), assert = require( 'assert' ), builder = common.require( 'ClassBuilder' )( - common.require( 'member_builder' ), + common.require( 'MemberBuilder' )(), common.require( 'VisibilityObjectFactoryFactory' ).fromEnvironment() ) ; diff --git a/test/test-class_builder-final.js b/test/test-class_builder-final.js index b504074..6693270 100644 --- a/test/test-class_builder-final.js +++ b/test/test-class_builder-final.js @@ -25,7 +25,7 @@ var common = require( './common' ), assert = require( 'assert' ), builder = common.require( 'ClassBuilder' )( - common.require( 'member_builder' ), + common.require( 'MemberBuilder' )(), common.require( 'VisibilityObjectFactoryFactory' ).fromEnvironment() ), diff --git a/test/test-class_builder-member-restrictions.js b/test/test-class_builder-member-restrictions.js index 1b501be..6019865 100644 --- a/test/test-class_builder-member-restrictions.js +++ b/test/test-class_builder-member-restrictions.js @@ -27,7 +27,7 @@ var common = require( './common' ), ClassBuilder = common.require( 'ClassBuilder' ), builder = ClassBuilder( - common.require( 'member_builder' ), + common.require( 'MemberBuilder' )(), common.require( 'VisibilityObjectFactoryFactory' ).fromEnvironment() ) ; diff --git a/test/test-class_builder-static.js b/test/test-class_builder-static.js index 13e2bd9..e67bb4b 100644 --- a/test/test-class_builder-static.js +++ b/test/test-class_builder-static.js @@ -26,7 +26,7 @@ var common = require( './common' ), assert = require( 'assert' ), fallback = common.require( 'util' ).definePropertyFallback() builder = common.require( 'ClassBuilder' )( - common.require( 'member_builder' ), + common.require( 'MemberBuilder' )(), common.require( 'VisibilityObjectFactoryFactory' ).fromEnvironment() ) ; diff --git a/test/test-class_builder-visibility.js b/test/test-class_builder-visibility.js index c3dd3da..ba5e1f9 100644 --- a/test/test-class_builder-visibility.js +++ b/test/test-class_builder-visibility.js @@ -28,7 +28,7 @@ var common = require( './common' ), assert = require( 'assert' ), util = common.require( 'util' ), builder = common.require( 'ClassBuilder' )( - common.require( 'member_builder' ), + common.require( 'MemberBuilder' )(), common.require( 'VisibilityObjectFactoryFactory' ).fromEnvironment() ) ; diff --git a/test/test-member_builder-gettersetter.js b/test/test-member_builder-gettersetter.js index d17d461..4e400a4 100644 --- a/test/test-member_builder-gettersetter.js +++ b/test/test-member_builder-gettersetter.js @@ -26,8 +26,10 @@ var common = require( './common' ), assert = require( 'assert' ), util = common.require( 'util' ), - buildGetter = common.require( 'member_builder' ).buildGetter, - buildSetter = common.require( 'member_builder' ).buildSetter, + builder = common.require( 'MemberBuilder' )(), + + buildGetter = builder.buildGetter, + buildSetter = builder.buildSetter, // member visibility types are quoted because they are reserved keywords members = {}, diff --git a/test/test-member_builder-method-hiding.js b/test/test-member_builder-method-hiding.js index 5871099..914cc91 100644 --- a/test/test-member_builder-method-hiding.js +++ b/test/test-member_builder-method-hiding.js @@ -26,7 +26,7 @@ var common = require( './common' ), assert = require( 'assert' ), warn = common.require( 'warn' ) builder = common.require( 'ClassBuilder' )( - common.require( 'member_builder' ), + common.require( 'MemberBuilder' )(), common.require( 'VisibilityObjectFactoryFactory' ).fromEnvironment() ) ; diff --git a/test/test-member_builder-method.js b/test/test-member_builder-method.js index 339fb1f..551228c 100644 --- a/test/test-member_builder-method.js +++ b/test/test-member_builder-method.js @@ -25,7 +25,7 @@ var common = require( './common' ), assert = require( 'assert' ), mb_common = require( __dirname + '/inc-member_builder-common' ), - builder = common.require( 'member_builder' ), + builder = common.require( 'MemberBuilder' )(), util = common.require( 'util' ), warn = common.require( 'warn' ), diff --git a/test/test-member_builder-prop.js b/test/test-member_builder-prop.js index a2aaf66..baffd13 100644 --- a/test/test-member_builder-prop.js +++ b/test/test-member_builder-prop.js @@ -25,7 +25,7 @@ var common = require( './common' ), assert = require( 'assert' ), mb_common = require( __dirname + '/inc-member_builder-common' ), - builder = common.require( 'member_builder' ), + builder = common.require( 'MemberBuilder' )(), util = common.require( 'util' ) ; diff --git a/test/test-member_builder.js b/test/test-member_builder.js index 58c89a3..bb84669 100644 --- a/test/test-member_builder.js +++ b/test/test-member_builder.js @@ -24,7 +24,8 @@ var common = require( './common' ), assert = require( 'assert' ), - builder = common.require( 'member_builder' ); + builder = common.require( 'MemberBuilder' )() +; ( function testCanEmptyMemberObject()