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()