diff --git a/README.md b/README.md
index 2e4f12a..d900169 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ Current support includes:
* Classical inheritance
* Abstract classes and methods
* Interfaces
+* Traits as mixins
* Visibility (public, protected, and private members)
* Static and constant members
diff --git a/README.traits b/README.traits
new file mode 100644
index 0000000..437da55
--- /dev/null
+++ b/README.traits
@@ -0,0 +1,98 @@
+GNU ease.js Traits
+==================
+The trait implementation is not yet complete; this is the list of known
+issues/TODOs. If you discover any problems, please send an e-mail to
+bug-easejs@gnu.org.
+
+Aside from the issues below, traits are stable and ready to be used in
+production. See the test cases and performance tests for more information
+and a plethora of examples until the documentation is complete.
+
+
+TODO: Trait Extending
+---------------------
+Currently, the only way for a trait to override methods of a class it is
+being mixed into is to implement a common interface. Traits should
+alternatively be able to "extend" classes, which will have effects similar
+to Scala in that the trait can only be mixed into that class. Further,
+traits should be able to extend and mix in other traits (though such should
+be done conservatively).
+
+
+TODO: Documentation
+-------------------
+Due to the trait implementation taking longer than expected to complete, and
+the importance of the first GNU release, trait documentation is not yet
+complete. Instead, traits have been released as a development preview, with
+the test cases and performance tests serving as interim documentation.
+
+Comprehensive documentation, including implementation details and rationale,
+will be available shortly.
+
+
+TODO: Static members
+--------------------
+Static members are currently unsupported. There is no particular difficulty
+in implementing them---the author didn't want it to hold up an initial
+release (the first GNU release) even further.
+
+
+TODO: Getters/setters
+---------------------
+Getters and setters, although they act like properties, should be treated as
+though they are methods. Further, they do not suffer from the same
+complications as properties, because they are only available in an ES5
+environment (as an ECMAScript language feature).
+
+
+TODO: Mixin Caching
+-------------------
+The pattern Type.use(...)(...)---that is, mix a trait into a class and
+immediate instantiate the result---is a common idiom that can often be
+better for self-documentation than storing the resulting class in another
+variable before instantiation. Currently, it's also a terrible thing to do
+in any sort of loop, as it re-mixes each and every time.
+
+We should introduce a caching system to avoid that cost and make it fairly
+cheap to use such an idiom. Further, this would permit the Scala-like
+ability to use Type.use in Class.isA checks.
+
+
+TODO: Public/Protected Property Support
+---------------------------------------
+Private properties are currently supported on traits because they do not
+affect the API of the type they are mixed into. However, due to limitations
+of pre-ES5 environments, implementing public and protected member epoxying
+becomes ugly in the event of a fallback, amounting essentially to
+re-assignment before/after trait method proxying. It is possible, though.
+
+This is not a necessary, or recommended, feature---one should aim to
+encapsulate all data, not expose it---but it does have its legitimate uses.
+As such, this is not a high-priority item.
+
+
+TODO: Trait-specific error messages
+-----------------------------------
+All error messages resulting from traits should refer to the trait by name
+and any problem members by name, and should offer context-specific
+suggestions for resolution. Currently, the errors may be more general and
+may reflect the internal construction of traits, which will be rather
+confusing to users.
+
+
+TODO: Performance enhancements
+------------------------------
+The current trait implementation works well, but is relatively slow
+(compared to how performant it could be). While this is sufficient for most
+users' uses, there is plenty of room for improvement. Until that time, be
+mindful of the performance test cases in the `test/perf' directory.
+
+
+TODO: Intermediate object as class
+----------------------------------
+The immediate syntax---Foo.use(T)()---is a short-hand equivalent of
+Foo.use(T).extend({})(). As such, for consistency, Class.isA should consider
+the intermediate object returned by a call to `use' to be a class.
+
+If we are to do so, though, we must make sure that the entire class API is
+supported.
diff --git a/doc/about.texi b/doc/about.texi
index 15d4660..1f98080 100644
--- a/doc/about.texi
+++ b/doc/about.texi
@@ -20,6 +20,7 @@ Current support includes:
@item Classical inheritance
@item Abstract classes and methods
@item Interfaces
+@item Traits as mixins
@item Visibility (public, protected, and private members)
@item Static, constant, and final members
@end itemize
diff --git a/index.js b/index.js
index 3edd1c3..ad7069e 100644
--- a/index.js
+++ b/index.js
@@ -23,5 +23,6 @@ exports.Class = require( __dirname + '/lib/class' );
exports.AbstractClass = require( __dirname + '/lib/class_abstract' );
exports.FinalClass = require( __dirname + '/lib/class_final' );
exports.Interface = require( __dirname + '/lib/interface' );
+exports.Trait = require( __dirname + '/lib/Trait' );
exports.version = require( __dirname + '/lib/version' );
diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js
index 8b4db46..91a072b 100644
--- a/lib/ClassBuilder.js
+++ b/lib/ClassBuilder.js
@@ -28,6 +28,9 @@ var util = require( __dirname + '/util' ),
warn = require( __dirname + '/warn' ),
Warning = warn.Warning,
+ hasOwn = Object.prototype.hasOwnProperty,
+
+
/**
* IE contains a nasty enumeration "bug" (poor implementation) that makes
* toString unenumerable. This means that, if you do obj.toString = foo,
@@ -148,6 +151,9 @@ function ClassBuilder( member_builder, visibility_factory )
*/
exports.ClassBase = function Class() {};
+// the base class has the class identifier 0
+util.defineSecureProp( exports.ClassBase, '__cid', 0 );
+
/**
* Default static property method
@@ -259,7 +265,8 @@ exports.isInstanceOf = function( type, instance )
implemented = meta.implemented;
i = implemented.length;
- // check implemented interfaces
+ // check implemented interfaces et. al. (other systems may make use of
+ // this meta-attribute to provide references to types)
while ( i-- )
{
if ( implemented[ i ] === type )
@@ -297,6 +304,7 @@ exports.prototype.build = function extend( _, __ )
base = args.pop() || exports.ClassBase,
prototype = this._getBase( base ),
cname = '',
+ autoa = false,
prop_init = this._memberBuilder.initMembers(),
members = this._memberBuilder.initMembers( prototype ),
@@ -308,6 +316,10 @@ exports.prototype.build = function extend( _, __ )
abstract_methods =
util.clone( exports.getMeta( base ).abstractMethods )
|| { __length: 0 }
+
+ virtual_members =
+ util.clone( exports.getMeta( base ).virtualMembers )
+ || {}
;
// prevent extending final classes
@@ -326,6 +338,12 @@ exports.prototype.build = function extend( _, __ )
delete props.__name;
}
+ // gobble up auto-abstract flag if present
+ if ( ( autoa = props.___$$auto$abstract$$ ) !== undefined )
+ {
+ delete props.___$$auto$abstract$$;
+ }
+
// IE has problems with toString()
if ( enum_bug )
{
@@ -338,7 +356,7 @@ exports.prototype.build = function extend( _, __ )
// increment class identifier
this._classId++;
- // build the various class components (xxx: this is temporary; needs
+ // build the various class components (XXX: this is temporary; needs
// refactoring)
try
{
@@ -346,9 +364,12 @@ exports.prototype.build = function extend( _, __ )
this._classId,
base,
prop_init,
- abstract_methods,
- members,
- static_members,
+ {
+ all: members,
+ 'abstract': abstract_methods,
+ 'static': static_members,
+ 'virtual': virtual_members,
+ },
function( inst )
{
return new_class.___$$svis$$;
@@ -393,8 +414,7 @@ exports.prototype.build = function extend( _, __ )
new_class.___$$sinit$$ = staticInit;
attachFlags( new_class, props );
-
- validateAbstract( new_class, cname, abstract_methods );
+ validateAbstract( new_class, cname, abstract_methods, autoa );
// We reduce the overall cost of this definition by defining it on the
// prototype rather than during instantiation. While this does increase the
@@ -407,6 +427,7 @@ exports.prototype.build = function extend( _, __ )
// create internal metadata for the new class
var meta = createMeta( new_class, base );
meta.abstractMethods = abstract_methods;
+ meta.virtualMembers = virtual_members;
meta.name = cname;
attachAbstract( new_class, abstract_methods );
@@ -441,125 +462,217 @@ exports.prototype._getBase = function( base )
exports.prototype.buildMembers = function buildMembers(
- props, class_id, base, prop_init, abstract_methods, members,
- static_members, staticInstLookup
+ props, class_id, base, prop_init, memberdest, staticInstLookup
)
{
- var hasOwn = Array.prototype.hasOwnProperty,
- defs = {},
+ var context = {
+ _cb: this,
- smethods = static_members.methods,
- sprops = static_members.props,
+ // arguments
+ prop_init: prop_init,
+ class_id: class_id,
+ base: base,
+ staticInstLookup: staticInstLookup,
- _self = this
+ defs: {},
+
+ // holds member builder state
+ state: {},
+
+ // TODO: there does not seem to be tests for these guys; perhaps
+ // this can be rectified with the reflection implementation
+ members: memberdest.all,
+ abstract_methods: memberdest['abstract'],
+ static_members: memberdest['static'],
+ virtual_members: memberdest['virtual'],
+ };
+
+ // default member handlers for parser
+ var handlers = {
+ each: _parseEach,
+ property: _parseProp,
+ getset: _parseGetSet,
+ method: _parseMethod,
+ };
+
+ // a custom parser may be provided to hook the below property parser;
+ // this can be done to save time on post-processing, or alter the
+ // default behavior of the parser
+ if ( props.___$$parser$$ )
+ {
+ // this isn't something that we actually want to parse
+ var parser = props.___$$parser$$;
+ delete props.___$$parser$$;
+
+ function hjoin( name, orig )
+ {
+ handlers[ name ] = function()
+ {
+ var args = Array.prototype.slice.call( arguments );
+
+ // invoke the custom handler with the original handler as
+ // its last argument (which the custom handler may choose
+ // not to invoke at all)
+ args.push( orig );
+ parser[ name ].apply( context, args );
+ };
+ }
+
+ // this avoids a performance penalty unless the above property is
+ // set
+ parser.each && hjoin( 'each', handlers.each );
+ parser.property && hjoin( 'property', handlers.property );
+ parser.getset && hjoin( 'getset', handlers.getset );
+ parser.method && hjoin( 'method', handlers.method );
+ }
+
+ // parse members and process accumulated member state
+ util.propParse( props, handlers, context );
+ this._memberBuilder.end( context.state );
+}
+
+
+function _parseEach( name, value, keywords )
+{
+ var defs = this.defs;
+
+ // disallow use of our internal __initProps() method
+ if ( reserved_members[ name ] === true )
+ {
+ throw Error( name + " is reserved" );
+ }
+
+ // if a member was defined multiple times in the same class
+ // declaration, throw an error (unless the `weak' keyword is
+ // provided, which exists to accomodate this situation)
+ if ( hasOwn.call( defs, name )
+ && !( keywords['weak'] || defs[ name ].weak )
+ )
+ {
+ throw Error(
+ "Cannot redefine method '" + name + "' in same declaration"
+ );
+ }
+
+ // keep track of the definitions (only during class declaration)
+ // to catch duplicates
+ defs[ name ] = keywords;
+}
+
+
+function _parseProp( name, value, keywords )
+{
+ var dest = ( keywordStatic( keywords ) )
+ ? this.static_members.props
+ : this.prop_init;
+
+ // build a new property, passing in the other members to compare
+ // against for preventing nonsensical overrides
+ this._cb._memberBuilder.buildProp(
+ dest, null, name, value, keywords, this.base
+ );
+}
+
+
+function _parseGetSet( name, get, set, keywords )
+{
+ var dest = ( keywordStatic( keywords ) )
+ ? this.static_members.methods
+ : this.members,
+
+ is_static = keywordStatic( keywords ),
+ instLookup = ( ( is_static )
+ ? this.staticInstLookup
+ : exports.getMethodInstance
+ );
+
+ this._cb._memberBuilder.buildGetterSetter(
+ dest, null, name, get, set, keywords, instLookup,
+ this.class_id, this.base
+ );
+}
+
+
+function _parseMethod( name, func, is_abstract, keywords )
+{
+ var is_static = keywordStatic( keywords ),
+ dest = ( is_static )
+ ? this.static_members.methods
+ : this.members,
+ instLookup = ( is_static )
+ ? this.staticInstLookup
+ : exports.getMethodInstance
;
- util.propParse( props, {
- each: function( name, value, keywords )
+ // constructor check
+ if ( public_methods[ name ] === true )
+ {
+ if ( keywords[ 'protected' ] || keywords[ 'private' ] )
{
- // disallow use of our internal __initProps() method
- if ( reserved_members[ name ] === true )
- {
- throw Error( name + " is reserved" );
- }
-
- // if a member was defined multiple times in the same class
- // declaration, throw an error
- if ( hasOwn.call( defs, name ) )
- {
- throw Error(
- "Cannot redefine method '" + name + "' in same declaration"
- );
- }
-
- // keep track of the definitions (only during class declaration)
- // to catch duplicates
- defs[ name ] = 1;
- },
-
- property: function( name, value, keywords )
- {
- var dest = ( keywordStatic( keywords ) ) ? sprops : prop_init;
-
- // build a new property, passing in the other members to compare
- // against for preventing nonsensical overrides
- _self._memberBuilder.buildProp(
- dest, null, name, value, keywords, base
+ throw TypeError(
+ name + " must be public"
);
- },
+ }
+ }
- getset: function( name, get, set, keywords )
- {
- var dest = ( keywordStatic( keywords ) ) ? smethods : members,
- is_static = keywordStatic( keywords ),
- instLookup = ( ( is_static )
- ? staticInstLookup
- : exports.getMethodInstance
- );
+ var used = this._cb._memberBuilder.buildMethod(
+ dest, null, name, func, keywords, instLookup,
+ this.class_id, this.base, this.state
+ );
- _self._memberBuilder.buildGetterSetter(
- dest, null, name, get, set, keywords, instLookup, class_id, base
- );
- },
+ // do nothing more if we didn't end up using this definition
+ // (this may be the case, for example, with weak members)
+ if ( !used )
+ {
+ return;
+ }
- method: function( name, func, is_abstract, keywords )
- {
- var is_static = keywordStatic( keywords ),
- dest = ( is_static ) ? smethods : members,
- instLookup = ( is_static )
- ? staticInstLookup
- : exports.getMethodInstance
- ;
+ // note the concrete method check; this ensures that weak
+ // abstract methods will not count if a concrete method of the
+ // smae name has already been seen
+ if ( is_abstract )
+ {
+ this.abstract_methods[ name ] = true;
+ this.abstract_methods.__length++;
+ }
+ else if ( ( hasOwn.call( this.abstract_methods, name ) )
+ && ( is_abstract === false )
+ )
+ {
+ // if this was a concrete method, then it should no longer
+ // be marked as abstract
+ delete this.abstract_methods[ name ];
+ this.abstract_methods.__length--;
+ }
- // constructor check
- if ( public_methods[ name ] === true )
- {
- if ( keywords[ 'protected' ] || keywords[ 'private' ] )
- {
- throw TypeError(
- name + " must be public"
- );
- }
- }
-
- _self._memberBuilder.buildMethod(
- dest, null, name, func, keywords, instLookup,
- class_id, base
- );
-
- if ( is_abstract )
- {
- abstract_methods[ name ] = true;
- abstract_methods.__length++;
- }
- else if ( ( hasOwn.call( abstract_methods, name ) )
- && ( is_abstract === false )
- )
- {
- // if this was a concrete method, then it should no longer
- // be marked as abstract
- delete abstract_methods[ name ];
- abstract_methods.__length--;
- }
- },
- } );
+ if ( keywords['virtual'] )
+ {
+ this.virtual_members[ name ] = true;
+ }
}
/**
* Validates abstract class requirements
*
+ * We permit an `auto' flag for internal use only that will cause the
+ * abstract flag to be automatically set if the class should be marked as
+ * abstract, instead of throwing an error; this should be used sparingly and
+ * never exposed via a public API (for explicit use), as it goes against the
+ * self-documentation philosophy.
+ *
* @param {function()} ctor class
* @param {string} cname class name
* @param {{__length}} abstract_methods object containing abstract methods
+ * @param {boolean} auto automatically flag as abstract
*
* @return {undefined}
*/
-function validateAbstract( ctor, cname, abstract_methods )
+function validateAbstract( ctor, cname, abstract_methods, auto )
{
if ( ctor.___$$abstract$$ )
{
- if ( abstract_methods.__length === 0 )
+ if ( !auto && ( abstract_methods.__length === 0 ) )
{
throw TypeError(
"Class " + ( cname || "(anonymous)" ) + " was declared as " +
@@ -567,15 +680,18 @@ function validateAbstract( ctor, cname, abstract_methods )
);
}
}
- else
+ else if ( abstract_methods.__length > 0 )
{
- if ( abstract_methods.__length > 0 )
+ if ( auto )
{
- throw TypeError(
- "Class " + ( cname || "(anonymous)" ) + " contains abstract " +
- "members and must therefore be declared abstract"
- );
+ ctor.___$$abstract$$ = true;
+ return;
}
+
+ throw TypeError(
+ "Class " + ( cname || "(anonymous)" ) + " contains abstract " +
+ "members and must therefore be declared abstract"
+ );
}
}
@@ -659,6 +775,12 @@ exports.prototype.createConcreteCtor = function( cname, members )
// generate and store unique instance id
attachInstanceId( this, ++_self._instanceId );
+ // handle internal trait initialization logic, if provided
+ if ( typeof this.___$$tctor$$ === 'function' )
+ {
+ this.___$$tctor$$.call( this );
+ }
+
// call the constructor, if one was provided
if ( typeof this.__construct === 'function' )
{
@@ -666,9 +788,10 @@ exports.prototype.createConcreteCtor = function( cname, members )
// subtypes), and since we're using apply with 'this', the
// constructor will be applied to subtypes without a problem
this.__construct.apply( this, ( args || arguments ) );
- args = null;
}
+ args = null;
+
// attach any instance properties/methods (done after
// constructor to ensure they are not overridden)
attachInstanceOf( this );
@@ -676,9 +799,7 @@ exports.prototype.createConcreteCtor = function( cname, members )
// Provide a more intuitive string representation of the class
// instance. If a toString() method was already supplied for us,
// use that one instead.
- if ( !( Object.prototype.hasOwnProperty.call(
- members[ 'public' ], 'toString'
- ) ) )
+ if ( !( hasOwn.call( members[ 'public' ], 'toString' ) ) )
{
// use __toString if available (see enum_bug), otherwise use
// our own defaults
@@ -926,8 +1047,7 @@ function attachStatic( ctor, members, base, inheriting )
// we use hasOwnProperty to ensure that undefined values will not
// cause us to continue checking the parent, thereby potentially
// failing to set perfectly legal values
- var has = Object.prototype.hasOwnProperty,
- found = false,
+ var found = false,
// Determine if we were invoked in the context of a class. If
// so, use that. Otherwise, use ourself.
@@ -946,16 +1066,16 @@ function attachStatic( ctor, members, base, inheriting )
// available and we are internal (within a method), we can move on
// to check other levels of visibility. `found` will contain the
// visibility level the property was found in, or false.
- found = has.call( props[ 'public' ], prop ) && 'public';
+ found = hasOwn.call( props[ 'public' ], prop ) && 'public';
if ( !found && _self._spropInternal )
{
// Check for protected/private. We only check for private
// properties if we are not currently checking the properties of
// a subtype. This works because the context is passed to each
// recursive call.
- found = has.call( props[ 'protected' ], prop ) && 'protected'
+ found = hasOwn.call( props[ 'protected' ], prop ) && 'protected'
|| !in_subtype
- && has.call( props[ 'private' ], prop ) && 'private'
+ && hasOwn.call( props[ 'private' ], prop ) && 'private'
;
}
diff --git a/lib/MemberBuilder.js b/lib/MemberBuilder.js
index 51f8bb3..a184d64 100644
--- a/lib/MemberBuilder.js
+++ b/lib/MemberBuilder.js
@@ -95,6 +95,10 @@ exports.initMembers = function( mpublic, mprotected, mprivate )
* Copies a method to the appropriate member prototype, depending on
* visibility, and assigns necessary metadata from keywords
*
+ * The provided ``member run'' state object is required and will be
+ * initialized automatically if it has not been already. For the first
+ * member of a run, the object should be empty.
+ *
* @param {__visobj} members
* @param {!Object} meta metadata container
* @param {string} name property name
@@ -108,10 +112,12 @@ exports.initMembers = function( mpublic, mprotected, mprivate )
* @param {number} cid class id
* @param {Object=} base optional base object to scan
*
+ * @param {Object} state member run state object
+ *
* @return {undefined}
*/
exports.buildMethod = function(
- members, meta, name, value, keywords, instCallback, cid, base
+ members, meta, name, value, keywords, instCallback, cid, base, state
)
{
// TODO: We can improve performance by not scanning each one individually
@@ -125,11 +131,11 @@ exports.buildMethod = function(
// ensure that the declaration is valid (keywords make sense, argument
// length, etc)
this._validate.validateMethod(
- name, value, keywords, prev_data, prev_keywords
+ name, value, keywords, prev_data, prev_keywords, state
);
// we might be overriding an existing method
- if ( keywords[ 'proxy' ] )
+ if ( keywords[ 'proxy' ] && !( prev && keywords.weak ) )
{
// TODO: Note that this is not compatible with method hiding, due to its
// positioning (see hideMethod() below); address once method hiding is
@@ -140,12 +146,23 @@ exports.buildMethod = function(
}
else if ( prev )
{
-
- if ( keywords[ 'override' ] || prev_keywords[ 'abstract' ] )
+ if ( keywords.weak && !( prev_keywords[ 'abstract' ] ) )
{
+ // another member of the same name has been found; discard the
+ // weak declaration
+ return false;
+ }
+ else if ( keywords[ 'override' ] || prev_keywords[ 'abstract' ] )
+ {
+ // if we have the `abstract' keyword at this point, then we are
+ // an abstract override
+ var override = ( keywords[ 'abstract' ] )
+ ? aoverride( name )
+ : prev;
+
// override the method
dest[ name ] = this._overrideMethod(
- prev, value, instCallback, cid
+ override, value, instCallback, cid
);
}
else
@@ -170,9 +187,51 @@ exports.buildMethod = function(
// store keywords for later reference (needed for pre-ES5 fallback)
dest[ name ].___$$keywords$$ = keywords;
+ return true;
};
+/**
+ * Creates an abstract override super method proxy to NAME
+ *
+ * This is a fairly abstract concept that is disastrously confusing without
+ * having been put into the proper context: This function is intended to be
+ * used as a super method for a method override in the case of abstract
+ * overrides. It only makes sense to be used, at least at this time, with
+ * mixins.
+ *
+ * When called, the bound context (`this') will be the private member object
+ * of the caller, which should contain a reference to the protected member
+ * object of the supertype to proxy to. It is further assumed that the
+ * protected member object (pmo) defines NAME such that it proxies to a
+ * mixin; this means that invoking it could result in an infinite loop. We
+ * therefore skip directly to the super-super method, which will be the
+ * method we are interested in proxying to.
+ *
+ * There is one additional consideration: If this super method is proxying
+ * from a mixin instance into a class, then it is important that we bind the
+ * calling context to the pmo instaed of our own context; otherwise, we'll
+ * be executing within the context of the trait, without access to the
+ * members of the supertype that we are proxying to! The pmo will be used by
+ * the ease.js method wrapper to look up the proper private member object,
+ * so it is not a problem that the pmo is being passed in.
+ *
+ * That's a lot of text for such a small amount of code.
+ *
+ * @param {string} name name of method to proxy to
+ *
+ * @return {Function} abstract override super method proxy
+ */
+function aoverride( name )
+{
+ return function()
+ {
+ return this.___$$super$$.prototype[ name ]
+ .apply( this.___$$pmo$$, arguments );
+ };
+}
+
+
/**
* Copies a property to the appropriate member prototype, depending on
* visibility, and assigns necessary metadata from keywords
@@ -285,35 +344,39 @@ exports.buildGetterSetter = function(
*/
function getMemberVisibility( members, keywords, name )
{
- var viserr = function()
- {
- throw TypeError(
- "Only one access modifier may be used for definition of '" +
- name + "'"
- );
- }
-
// there's cleaner ways of doing this, but consider it loop unrolling for
// performance
if ( keywords[ 'private' ] )
{
- ( keywords[ 'public' ] || keywords[ 'protected' ] ) && viserr();
+ ( keywords[ 'public' ] || keywords[ 'protected' ] )
+ && viserr( name );
return members[ 'private' ];
}
else if ( keywords[ 'protected' ] )
{
- ( keywords[ 'public' ] || keywords[ 'private' ] ) && viserr();
+ ( keywords[ 'public' ] || keywords[ 'private' ] )
+ && viserr( name );
return members[ 'protected' ];
}
else
{
// public keyword is the default, so explicitly specifying it is only
// for clarity
- ( keywords[ 'private' ] || keywords[ 'protected' ] ) && viserr();
+ ( keywords[ 'private' ] || keywords[ 'protected' ] )
+ && viserr( name );
return members[ 'public' ];
}
}
+function viserr( name )
+{
+ throw TypeError(
+ "Only one access modifier may be used for definition of '" +
+ name + "'"
+ );
+}
+
+
/**
* Scan each level of visibility for the requested member
@@ -486,3 +549,19 @@ exports._getVisibilityValue = function( keywords )
}
}
+
+/**
+ * End member run and perform post-processing on state data
+ *
+ * A ``member run'' should consist of the members required for a particular
+ * object (class/interface/etc). This action will perform validation
+ * post-processing if a validator is available.
+ *
+ * @param {Object} state member run state
+ *
+ * @return {undefined}
+ */
+exports.end = function( state )
+{
+ this._validate && this._validate.end( state );
+};
diff --git a/lib/MemberBuilderValidator.js b/lib/MemberBuilderValidator.js
index 0730043..e6e60cf 100644
--- a/lib/MemberBuilderValidator.js
+++ b/lib/MemberBuilderValidator.js
@@ -32,10 +32,86 @@ module.exports = exports = function MemberBuilderValidator( warn_handler )
/**
- * Validates a method declaration, ensuring that keywords are valid, overrides
- * make sense, etc.
+ * Initialize validation state if not already done
*
- * Throws exception on validation failure
+ * @param {Object} state validation state
+ *
+ * @return {Object} provided state object STATE
+ */
+exports.prototype._initState = function( state )
+{
+ if ( state.__vready ) return state;
+
+ state.warn = {};
+ state.__vready = true;
+ return state;
+};
+
+
+/**
+ * Perform post-processing on and invalidate validation state
+ *
+ * All queued warnings will be triggered.
+ *
+ * @param {Object} state validation state
+ *
+ * @return {undefined}
+ */
+exports.prototype.end = function( state )
+{
+ // trigger warnings
+ for ( var f in state.warn )
+ {
+ var warns = state.warn[ f ];
+ for ( var id in warns )
+ {
+ this._warningHandler( warns[ id ] );
+ }
+ }
+
+ state.__vready = false;
+};
+
+
+/**
+ * Enqueue warning within validation state
+ *
+ * @param {Object} state validation state
+ * @param {string} member member name
+ * @param {string} id warning identifier
+ * @param {Warning} warn warning
+ *
+ * @return {undefined}
+ */
+function _addWarn( state, member, id, warn )
+{
+ ( state.warn[ member ] = state.warn[ member ] || {} )[ id ] = warn;
+}
+
+
+/**
+ * Remove warning from validation state
+ *
+ * @param {Object} state validation state
+ * @param {string} member member name
+ * @param {string} id warning identifier
+ *
+ * @return {undefined}
+ */
+function _clearWarn( state, member, id, warn )
+{
+ delete ( state.warn[ member ] || {} )[ id ];
+}
+
+
+/**
+ * Validates a method declaration, ensuring that keywords are valid,
+ * overrides make sense, etc.
+ *
+ * Throws exception on validation failure. Warnings are stored in the state
+ * object for later processing. The state object will be initialized if it
+ * has not been already; for the initial validation, the state object should
+ * be empty.
*
* @param {string} name method name
* @param {*} value method value
@@ -45,12 +121,16 @@ module.exports = exports = function MemberBuilderValidator( warn_handler )
* @param {Object} prev_data data of member being overridden
* @param {Object} prev_keywords keywords of member being overridden
*
+ * @param {Object} state pre-initialized state object
+ *
* @return {undefined}
*/
exports.prototype.validateMethod = function(
- name, value, keywords, prev_data, prev_keywords
+ name, value, keywords, prev_data, prev_keywords, state
)
{
+ this._initState( state );
+
var prev = ( prev_data ) ? prev_data.member : null;
if ( keywords[ 'abstract' ] )
@@ -133,13 +213,30 @@ exports.prototype.validateMethod = function(
// disallow overriding non-virtual methods
if ( keywords[ 'override' ] && !( prev_keywords[ 'virtual' ] ) )
{
- throw TypeError(
- "Cannot override non-virtual method '" + name + "'"
- );
+ if ( !( keywords[ 'abstract' ] ) )
+ {
+ throw TypeError(
+ "Cannot override non-virtual method '" + name + "'"
+ );
+ }
+
+ // at this point, we have `abstract override'
+ if ( !( prev_keywords[ 'abstract' ] ) )
+ {
+ // TODO: test me
+ throw TypeError(
+ "Cannot perform abstract override on non-abstract " +
+ "method '" + name + "'"
+ );
+ }
}
- // do not allow overriding concrete methods with abstract
- if ( keywords[ 'abstract' ] && !( prev_keywords[ 'abstract' ] ) )
+ // do not allow overriding concrete methods with abstract unless the
+ // abstract method is weak
+ if ( keywords[ 'abstract' ]
+ && !( keywords.weak )
+ && !( prev_keywords[ 'abstract' ] )
+ )
{
throw TypeError(
"Cannot override concrete method '" + name + "' with " +
@@ -147,10 +244,32 @@ exports.prototype.validateMethod = function(
);
}
+
+ var lenprev = ( prev.__length === undefined )
+ ? prev.length
+ : prev.__length;
+
+ var lennow = ( value.__length === undefined )
+ ? value.length
+ : value.__length;
+
+ if ( keywords[ 'proxy' ] )
+ {
+ // otherwise we'd be checking against the length of a string.
+ lennow = NaN;
+ }
+
+ if ( keywords.weak && !( prev_keywords[ 'abstract' ] ) )
+ {
+ // weak abstract declaration found after its concrete
+ // definition; check in reverse order
+ var tmp = lenprev;
+ lenprev = lennow;
+ lennow = tmp;
+ }
+
// ensure parameter list is at least the length of its supertype
- if ( ( value.__length || value.length )
- < ( prev.__length || prev.length )
- )
+ if ( lennow < lenprev )
{
throw TypeError(
"Declaration of method '" + name + "' must be compatible " +
@@ -168,16 +287,25 @@ exports.prototype.validateMethod = function(
);
}
- // Disallow overriding method without override keyword (unless parent
- // method is abstract). In the future, this will provide a warning to
- // default to method hiding.
- if ( !( keywords[ 'override' ] || prev_keywords[ 'abstract' ] ) )
+ // Disallow overriding method without override keyword (unless
+ // parent method is abstract). In the future, this will provide a
+ // warning to default to method hiding. Note the check for a
+ if ( !( keywords[ 'override' ]
+ || prev_keywords[ 'abstract' ]
+ || keywords.weak
+ ) )
{
throw TypeError(
"Attempting to override method '" + name +
"' without 'override' keyword"
);
}
+
+ // prevent non-override warning
+ if ( keywords.weak && prev_keywords[ 'override' ] )
+ {
+ _clearWarn( state, name, 'no' );
+ }
}
else if ( keywords[ 'override' ] )
{
@@ -185,7 +313,7 @@ exports.prototype.validateMethod = function(
// but it shouldn't stop the class definition (it doesn't adversely
// affect the functionality of the class, unless of course the method
// attempts to reference a supertype)
- this._warningHandler( Error(
+ _addWarn( state, name, 'no', Error(
"Method '" + name +
"' using 'override' keyword without super method"
) );
diff --git a/lib/MethodWrappers.js b/lib/MethodWrappers.js
index c568b43..086d2ad 100644
--- a/lib/MethodWrappers.js
+++ b/lib/MethodWrappers.js
@@ -89,7 +89,7 @@ exports.standard = {
// not to keep an unnecessary reference to the keywords object
var is_static = keywords && keywords[ 'static' ];
- return function()
+ var ret = function()
{
var context = getInst( this, cid ) || this,
retval = undefined,
@@ -122,6 +122,13 @@ exports.standard = {
? this
: retval;
};
+
+ // ensures that proxies can be used to provide concrete
+ // implementations of abstract methods with param requirements (we
+ // have no idea what we'll be proxying to at runtime, so we need to
+ // just power through it; see test case for more info)
+ ret.__length = NaN;
+ return ret;
},
};
diff --git a/lib/Trait.js b/lib/Trait.js
new file mode 100644
index 0000000..e1c18ff
--- /dev/null
+++ b/lib/Trait.js
@@ -0,0 +1,680 @@
+/**
+ * Provides system for code reuse via traits
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var AbstractClass = require( __dirname + '/class_abstract' ),
+ ClassBuilder = require( __dirname + '/ClassBuilder' );
+
+
+/**
+ * Trait constructor / base object
+ *
+ * The interpretation of the argument list varies by number. Further,
+ * various trait methods may be used as an alternative to invoking this
+ * constructor.
+ *
+ * @return {Function} trait
+ */
+function Trait()
+{
+ switch ( arguments.length )
+ {
+ case 1:
+ return Trait.extend.apply( this, arguments );
+ break;
+
+ case 2:
+ return createNamedTrait.apply( this, arguments );
+ break;
+
+ default:
+ throw Error( "Missing trait name or definition" );
+ }
+};
+
+
+/**
+ * Create a named trait
+ *
+ * @param {string} name trait name
+ * @param {Object} def trait definition
+ *
+ * @return {Function} named trait
+ */
+function createNamedTrait( name, dfn )
+{
+ if ( arguments.length > 2 )
+ {
+ throw Error(
+ "Expecting at most two arguments for definition of named " +
+ "Trait " + name + "'; " + arguments.length + " given"
+ );
+ }
+
+ if ( typeof name !== 'string' )
+ {
+ throw Error(
+ "First argument of named class definition must be a string"
+ );
+ }
+
+ dfn.__name = name;
+
+ return Trait.extend( dfn );
+}
+
+
+Trait.extend = function( dfn )
+{
+ // we may have been passed some additional metadata
+ var meta = this.__$$meta || {};
+
+ // store any provided name, since we'll be clobbering it (the definition
+ // object will be used to define the hidden abstract class)
+ var name = dfn.__name || '(Trait)';
+
+ // augment the parser to handle our own oddities
+ dfn.___$$parser$$ = {
+ each: _parseMember,
+ property: _parseProps,
+ getset: _parseGetSet,
+ };
+
+ // automatically mark ourselves as abstract if an abstract method is
+ // provided
+ dfn.___$$auto$abstract$$ = true;
+
+ // give the abstract trait class a distinctive name for debugging
+ dfn.__name = '#AbstractTrait#';
+
+ function TraitType()
+ {
+ throw Error( "Cannot instantiate trait" );
+ };
+
+ // implement interfaces if indicated
+ var base = AbstractClass;
+ if ( meta.ifaces )
+ {
+ base = base.implement.apply( null, meta.ifaces );
+ }
+
+ // and here we can see that traits are quite literally abstract classes
+ var tclass = base.extend( dfn );
+
+ TraitType.__trait = true;
+ TraitType.__acls = tclass;
+ TraitType.__ccls = null;
+ TraitType.toString = function()
+ {
+ return ''+name;
+ };
+
+ // invoked to trigger mixin
+ TraitType.__mixin = function( dfn, tc, base )
+ {
+ mixin( TraitType, dfn, tc, base );
+ };
+
+ // mixes in implemented types
+ TraitType.__mixinImpl = function( dest_meta )
+ {
+ mixinImpl( tclass, dest_meta );
+ };
+
+ return TraitType;
+};
+
+
+/**
+ * Verifies trait member restrictions
+ *
+ * @param {string} name property name
+ * @param {*} value property value
+ * @param {Object} keywords property keywords
+ * @param {Function} h original handler that we replaced
+ *
+ * @return {undefined}
+ */
+function _parseMember( name, value, keywords, h )
+{
+ // traits are not permitted to define constructors
+ if ( name === '__construct' )
+ {
+ throw Error( "Traits may not define __construct" );
+ }
+
+ // will be supported in future versions
+ if ( keywords['static'] )
+ {
+ throw Error(
+ "Cannot define member `" + name + "'; static trait " +
+ "members are currently unsupported"
+ );
+ }
+
+ // apply original handler
+ h.apply( this, arguments );
+}
+
+
+/**
+ * Throws error if non-internal property is found within PROPS
+ *
+ * For details and rationale, see the Trait/PropertyTest case.
+ *
+ * @param {string} name property name
+ * @param {*} value property value
+ * @param {Object} keywords property keywords
+ * @param {Function} h original handler that we replaced
+ *
+ * @return {undefined}
+ */
+function _parseProps( name, value, keywords, h )
+{
+ // ignore internal properties
+ if ( name.substr( 0, 3 ) === '___' )
+ {
+ return;
+ }
+
+ if ( !( keywords['private'] ) )
+ {
+ throw Error(
+ "Cannot define property `" + name + "'; only private " +
+ "properties are permitted within Trait definitions"
+ );
+ }
+
+ // apply original handler
+ h.apply( this, arguments );
+}
+
+
+/**
+ * Immediately throws an exception, as getters/setters are unsupported
+ *
+ * This is a temporary restriction; they will be supported in future
+ * releases.
+ *
+ * @param {string} name property name
+ * @param {*} value property value
+ * @param {Object} keywords property keywords
+ * @param {Function} h original handler that we replaced
+ *
+ * @return {undefined}
+ */
+function _parseGetSet( name, value, keywords, h )
+{
+ throw Error(
+ "Cannot define property `" + name + "'; getters/setters are " +
+ "currently unsupported"
+ );
+}
+
+
+/**
+ * Implement one or more interfaces
+ *
+ * Implementing an interface into a trait has the same effect as it does
+ * within classes in that it will automatically define abstract methods
+ * unless a concrete method is provided. Further, the class that the trait
+ * is mixed into will act as though it implemented the interfaces.
+ *
+ * @param {...Function} interfaces interfaces to implement
+ *
+ * @return {Object} staged trait object
+ */
+Trait.implement = function()
+{
+ var ifaces = arguments;
+
+ return {
+ extend: function()
+ {
+ // pass our interface metadata as the invocation context
+ return Trait.extend.apply(
+ { __$$meta: { ifaces: ifaces } },
+ arguments
+ );
+ },
+ };
+};
+
+
+/**
+ * Determines if the provided value references a trait
+ *
+ * @param {*} trait value to check
+ *
+ * @return {boolean} whether the provided value references a trait
+ */
+Trait.isTrait = function( trait )
+{
+ return !!( trait || {} ).__trait;
+};
+
+
+/**
+ * Create a concrete class from the abstract trait class
+ *
+ * This class is the one that will be instantiated by classes that mix in
+ * the trait.
+ *
+ * @param {AbstractClass} acls abstract trait class
+ *
+ * @return {Class} concrete trait class for instantiation
+ */
+function createConcrete( acls )
+{
+ // start by providing a concrete implementation for our dummy method and
+ // a constructor that accepts the protected member object of the
+ // containing class
+ var dfn = {
+ // protected member object (we define this as protected so that the
+ // parent ACLS has access to it (!), which is not prohibited since
+ // JS does not provide a strict typing mechanism...this is a kluge)
+ // and target supertype---that is, what __super calls should
+ // referene
+ 'protected ___$$pmo$$': null,
+ 'protected ___$$super$$': null,
+ __construct: function( base, pmo )
+ {
+ this.___$$super$$ = base;
+ this.___$$pmo$$ = pmo;
+ },
+
+ // mainly for debugging; should really never see this.
+ __name: '#ConcreteTrait#',
+ };
+
+ // every abstract method should be overridden with a proxy to the
+ // protected member object that will be passed in via the ctor
+ var amethods = ClassBuilder.getMeta( acls ).abstractMethods;
+ for ( var f in amethods )
+ {
+ // TODO: would be nice if this check could be for '___'; need to
+ // replace amethods.__length with something else, then
+ if ( !( Object.hasOwnProperty.call( amethods, f ) )
+ || ( f.substr( 0, 2 ) === '__' )
+ )
+ {
+ continue;
+ }
+
+ // we know that if it's not public, then it must be protected
+ var vis = ( acls.___$$methods$$['public'][ f ] !== undefined )
+ ? 'public'
+ : 'protected';
+
+ // setting the correct visibility modified is important to prevent
+ // visibility de-escalation errors if a protected concrete method is
+ // provided
+ dfn[ vis + ' proxy ' + f ] = '___$$pmo$$';
+ }
+
+ // virtual methods need to be handled with care to ensure that we invoke
+ // any overrides
+ createVirtProxy( acls, dfn );
+
+ return acls.extend( dfn );
+}
+
+
+/**
+ * Create virtual method proxies for all virtual members
+ *
+ * Virtual methods are a bit of hassle with traits: we are in a situation
+ * where we do not know at the time that the trait is created whether or not
+ * the virtual method has been overridden, since the class that the trait is
+ * mixed into may do the overriding. Therefore, we must check if an override
+ * has occured *when the method is invoked*; there is room for optimization
+ * there (by making such a determination at the time of mixin), but we'll
+ * leave that for later.
+ *
+ * @param {AbstractClass} acls abstract trait class
+ * @param {Object} dfn destination definition object
+ *
+ * @return {undefined}
+ */
+function createVirtProxy( acls, dfn )
+{
+ var vmembers = ClassBuilder.getMeta( acls ).virtualMembers;
+
+ // f = `field'
+ for ( var f in vmembers )
+ {
+ var vis = ( acls.___$$methods$$['public'][ f ] !== undefined )
+ ? 'public'
+ : 'protected';
+
+ // this is the aforementioned proxy method; see the docblock for
+ // more information
+ dfn[ vis + ' virtual override ' + f ] = ( function()
+ {
+ return function()
+ {
+ var pmo = this.___$$pmo$$,
+ o = pmo[ f ];
+
+ // proxy to virtual override from the class we are mixed
+ // into, if found; otherwise, proxy to our supertype
+ return ( o )
+ ? o.apply( pmo, arguments )
+ : this.__super.apply( this, arguments );
+ };
+ } )( f );
+
+ // this guy bypasses the above virtual override check, which is
+ // necessary in certain cases to prevent infinte recursion
+ dfn[ vis + ' virtual __$$' + f ] = ( function( f )
+ {
+ return function()
+ {
+ return this.___$$parent$$[ f ].apply( this, arguments );
+ };
+ } )( f );
+ }
+}
+
+
+/**
+ * Mix trait into the given definition
+ *
+ * The original object DFN is modified; it is not cloned. TC should be
+ * initialized to an empty array; it is used to store context data for
+ * mixing in traits and will be encapsulated within a ctor closure (and thus
+ * will remain in memory).
+ *
+ * @param {Trait} trait trait to mix in
+ * @param {Object} dfn definition object to merge into
+ * @param {Array} tc trait class context
+ * @param {Class} base target supertyep
+ *
+ * @return {Object} dfn
+ */
+function mixin( trait, dfn, tc, base )
+{
+ // the abstract class hidden within the trait
+ var acls = trait.__acls;
+
+ // retrieve the private member name that will contain this trait object
+ var iname = addTraitInst( trait, dfn, tc, base );
+
+ // recursively mix in trait's underlying abstract class (ensuring that
+ // anything that the trait inherits from is also properly mixed in)
+ mixinCls( acls, dfn, iname );
+ return dfn;
+}
+
+
+/**
+ * Recursively mix in class methods
+ *
+ * If CLS extends another class, its methods will be recursively processed
+ * to ensure that the entire prototype chain is properly proxied.
+ *
+ * For an explanation of the iname parameter, see the mixin function.
+ *
+ * @param {Class} cls class to mix in
+ * @param {Object} dfn definition object to merge into
+ * @param {string} iname trait object private member instance name
+ *
+ * @return {undefined}
+ */
+function mixinCls( cls, dfn, iname )
+{
+ var methods = cls.___$$methods$$;
+
+ mixMethods( methods['public'], dfn, 'public', iname );
+ mixMethods( methods['protected'], dfn, 'protected', iname );
+
+ // if this class inherits from another class that is *not* the base
+ // class, recursively process its methods; otherwise, we will have
+ // incompletely proxied the prototype chain
+ var parent = methods['public'].___$$parent$$;
+ if ( parent && ( parent.constructor !== ClassBuilder.ClassBase ) )
+ {
+ mixinCls( parent.constructor, dfn, iname );
+ }
+}
+
+
+/**
+ * Mix implemented types into destination object
+ *
+ * The provided destination object will ideally be the `implemented' array
+ * of the destination class's meta object.
+ *
+ * @param {Class} cls source class
+ * @param {Object} dest_meta destination object to copy into
+ *
+ * @return {undefined}
+ */
+function mixinImpl( cls, dest_meta )
+{
+ var impl = ClassBuilder.getMeta( cls ).implemented || [],
+ i = impl.length;
+
+ while ( i-- )
+ {
+ // TODO: this could potentially result in duplicates
+ dest_meta.push( impl[ i ] );
+ }
+}
+
+
+/**
+ * Mix methods from SRC into DEST using proxies
+ *
+ * @param {Object} src visibility object to scavenge from
+ * @param {Object} dest destination definition object
+ * @param {string} vis visibility modifier
+ * @param {string} iname proxy destination (trait instance)
+ *
+ * @return {undefined}
+ */
+function mixMethods( src, dest, vis, iname )
+{
+ for ( var f in src )
+ {
+ if ( !( Object.hasOwnProperty.call( src, f ) ) )
+ {
+ continue;
+ }
+
+ // TODO: this is a kluge; we'll use proper reflection eventually,
+ // but for now, this is how we determine if this is an actual method
+ // vs. something that just happens to be on the visibility object
+ if ( !( src[ f ].___$$keywords$$ ) )
+ {
+ continue;
+ }
+
+ var keywords = src[ f ].___$$keywords$$,
+ vis = keywords['protected'] ? 'protected' : 'public';
+
+ // if abstract, then we are expected to provide the implementation;
+ // otherwise, we proxy to the trait's implementation
+ if ( keywords[ 'abstract' ] && !( keywords[ 'override' ] ) )
+ {
+ // copy the abstract definition (N.B. this does not copy the
+ // param names, since that is not [yet] important); the
+ // visibility modified is important to prevent de-escalation
+ // errors on override
+ dest[ vis + ' weak abstract ' + f ] = src[ f ].definition;
+ }
+ else
+ {
+ var vk = keywords['virtual'],
+ virt = vk ? 'virtual ' : '',
+ ovr = ( keywords['override'] ) ? 'override ' : '',
+ pname = ( vk ? '' : 'proxy ' ) + virt + ovr + vis + ' ' + f;
+
+ // if we have already set up a proxy for a field of this name,
+ // then multiple traits have defined the same concrete member
+ if ( dest[ pname ] !== undefined )
+ {
+ // TODO: between what traits?
+ throw Error( "Trait member conflict: `" + f + "'" );
+ }
+
+ // if non-virtual, a normal proxy should do
+ if ( !( keywords[ 'virtual' ] ) )
+ {
+ dest[ pname ] = iname;
+ continue;
+ }
+
+ // proxy this method to what will be the encapsulated trait
+ // object (note that we do not use the proxy keyword here
+ // beacuse we are not proxying to a method of the same name)
+ dest[ pname ] = ( function( f )
+ {
+ return function()
+ {
+ var pdest = this[ iname ];
+
+ // invoke the direct method on the trait instance; this
+ // bypasses the virtual override check on the trait
+ // method to ensure that it is invoked without
+ // additional overhead or confusion
+ var ret = pdest[ '__$$' + f ].apply( pdest, arguments );
+
+ // if the trait returns itself, return us instead
+ return ( ret === pdest )
+ ? this
+ : ret;
+ };
+ } )( f );
+ }
+ }
+}
+
+
+/**
+ * Add concrete trait class to a class instantion list
+ *
+ * This list---which will be created if it does not already exist---will be
+ * used upon instantiation of the class consuming DFN to instantiate the
+ * concrete trait classes.
+ *
+ * Here, `tc' and `to' are understood to be, respectively, ``trait class''
+ * and ``trait object''.
+ *
+ * @param {Class} T trait
+ * @param {Object} dfn definition object of class being mixed into
+ * @param {Array} tc trait class object
+ * @param {Class} base target supertyep
+ *
+ * @return {string} private member into which C instance shall be stored
+ */
+function addTraitInst( T, dfn, tc, base )
+{
+ var base_cid = base.__cid;
+
+ // creates a property of the form ___$to$N$M to hold the trait object
+ // reference; M is required because of the private member restrictions
+ // imposed to be consistent with pre-ES5 fallback
+ var iname = '___$to$' + T.__acls.__cid + '$' + base_cid;
+
+ // the trait object array will contain two values: the destination field
+ // and the trait to instantiate
+ tc.push( [ iname, T ] );
+
+ // we must also add the private field to the definition object to
+ // support the object assignment indicated by TC
+ dfn[ 'private ' + iname ] = null;
+
+ // create internal trait ctor if not available
+ if ( dfn.___$$tctor$$ === undefined )
+ {
+ // TODO: let's check for inheritance or something to avoid this weak
+ // definition (this prevents warnings if there is not a supertype
+ // that defines the trait ctor)
+ dfn[ 'weak virtual ___$$tctor$$' ] = function() {};
+ dfn[ 'virtual override ___$$tctor$$' ] = createTctor( tc, base );
+ }
+
+ return iname;
+}
+
+
+/**
+ * Trait initialization constructor
+ *
+ * May be used to initialize all traits mixed into the class that invokes
+ * this function. All concrete trait classes are instantiated and their
+ * resulting objects assigned to their rsepective pre-determined field
+ * names.
+ *
+ * This will lazily create the concrete trait class if it does not already
+ * exist, which saves work if the trait is never used.
+ *
+ * @param {Object} tc trait class list
+ * @param {Class} base target supertype
+ *
+ * @return {undefined}
+ */
+function tctor( tc, base )
+{
+ // instantiate all traits and assign the object to their
+ // respective fields
+ for ( var t in tc )
+ {
+ var f = tc[ t ][ 0 ],
+ T = tc[ t ][ 1 ],
+ C = T.__ccls || ( T.__ccls = createConcrete( T.__acls ) );
+
+ // instantiate the trait, providing it with our protected visibility
+ // object so that it has access to our public and protected members
+ // (but not private); in return, we will use its own protected
+ // visibility object to gain access to its protected members...quite
+ // the intimate relationship
+ this[ f ] = C( base, this.___$$vis$$ ).___$$vis$$;
+ }
+
+ // if we are a subtype, be sure to initialize our parent's traits
+ this.__super && this.__super();
+};
+
+
+/**
+ * Create trait constructor
+ *
+ * This binds the generic trait constructor to a reference to the provided
+ * trait class list.
+ *
+ * @param {Object} tc trait class list
+ * @param {Class} base target supertype
+ *
+ * @return {function()} trait constructor
+ */
+function createTctor( tc, base )
+{
+ return function()
+ {
+ return tctor.call( this, tc, base );
+ };
+}
+
+
+module.exports = Trait;
+
diff --git a/lib/VisibilityObjectFactory.js b/lib/VisibilityObjectFactory.js
index cf2ab5c..dab7feb 100644
--- a/lib/VisibilityObjectFactory.js
+++ b/lib/VisibilityObjectFactory.js
@@ -80,7 +80,7 @@ exports.prototype.setup = function setup( dest, properties, methods )
this._doSetup( dest,
properties[ 'protected' ],
methods[ 'protected' ],
- 'public'
+ true
);
// then add the private parts
@@ -127,20 +127,21 @@ exports.prototype._createPrivateLayer = function( atop_of, properties )
/**
* Set up destination object by copying over properties and methods
*
- * @param {Object} dest destination object
- * @param {Object} properties properties to copy
- * @param {Object} methods methods to copy
- * @param {boolean} unless_keyword do not set if keyword is set on existing
- * method
+ * The prot_priv parameter can be used to ignore both explicitly and
+ * implicitly public methods.
+ *
+ * @param {Object} dest destination object
+ * @param {Object} properties properties to copy
+ * @param {Object} methods methods to copy
+ * @param {boolean} prot_priv do not set unless protected or private
*
* @return {undefined}
*/
exports.prototype._doSetup = function(
- dest, properties, methods, unless_keyword
+ dest, properties, methods, prot_priv
)
{
- var hasOwn = Array.prototype.hasOwnProperty,
- pre = null;
+ var hasOwn = Array.prototype.hasOwnProperty;
// copy over the methods
if ( methods !== undefined )
@@ -149,7 +150,8 @@ exports.prototype._doSetup = function(
{
if ( hasOwn.call( methods, method_name ) )
{
- pre = dest[ method_name ];
+ var pre = dest[ method_name ],
+ kw = pre && pre.___$$keywords$$;
// If requested, do not copy the method over if it already
// exists in the destination object. Don't use hasOwn here;
@@ -163,9 +165,9 @@ exports.prototype._doSetup = function(
// protected). This is the *last* check to ensure a performance
// hit is incured *only* if we're overriding protected with
// protected.
- if ( !unless_keyword
+ if ( !prot_priv
|| ( pre === undefined )
- || !( pre.___$$keywords$$[ unless_keyword ] )
+ || ( kw[ 'private' ] || kw[ 'protected' ] )
)
{
dest[ method_name ] = methods[ method_name ];
diff --git a/lib/class.js b/lib/class.js
index 8e1bca4..ec6d6b8 100644
--- a/lib/class.js
+++ b/lib/class.js
@@ -45,6 +45,8 @@ var util = require( __dirname + '/util' ),
)
;
+var _nullf = function() { return null; }
+
/**
* This module may be invoked in order to provide a more natural looking class
@@ -120,6 +122,30 @@ module.exports.implement = function( interfaces )
};
+/**
+ * Mix a trait into a class
+ *
+ * The ultimate intent of this depends on the ultimate `extend' call---if it
+ * extends another class, then the traits will be mixed into that class;
+ * otherwise, the traits will be mixed into the base class. In either case,
+ * a final `extend' call is necessary to complete the definition. An attempt
+ * to instantiate the return value before invoking `extend' will result in
+ * an exception.
+ *
+ * @param {Array.} traits traits to mix in
+ *
+ * @return {Function} staging object for class definition
+ */
+module.exports.use = function( traits )
+{
+ // consume traits onto an empty base
+ return createUse(
+ _nullf,
+ Array.prototype.slice.call( arguments )
+ );
+};
+
+
/**
* Determines whether the provided object is a class created through ease.js
*
@@ -290,6 +316,14 @@ function createStaging( cname )
cname
);
},
+
+ use: function()
+ {
+ return createUse(
+ _nullf,
+ Array.prototype.slice.call( arguments )
+ );
+ },
};
}
@@ -312,7 +346,7 @@ function createImplement( base, ifaces, cname )
{
// Defer processing until after extend(). This also ensures that implement()
// returns nothing usable.
- return {
+ var partial = {
extend: function()
{
var args = Array.prototype.slice.call( arguments ),
@@ -355,7 +389,154 @@ function createImplement( base, ifaces, cname )
def
);
},
+
+ // TODO: this is a naive implementation that works, but could be
+ // much more performant (it creates a subtype before mixing in)
+ use: function()
+ {
+ var traits = Array.prototype.slice.call( arguments );
+ return createUse(
+ function() { return partial.__createBase(); },
+ traits
+ );
+ },
+
+ // allows overriding default behavior
+ __createBase: function()
+ {
+ return partial.extend( {} );
+ },
};
+
+ return partial;
+}
+
+
+/**
+ * Create a staging object representing an eventual mixin
+ *
+ * This staging objects prepares a class definition for trait mixin. In
+ * particular, the returned staging object has the following features:
+ * - invoking it will, if mixing into an existing (non-base) class without
+ * subclassing, immediately complete the mixin and instantiate the
+ * generated class;
+ * - calling `use' has the effect of chaining mixins, stacking them atop
+ * of one-another; and
+ * - invoking `extend' will immediately complete the mixin, resulting in a
+ * subtype of the base.
+ *
+ * Mixins are performed lazily---the actual mixin will not take place until
+ * the final `extend' call, which may be implicit by invoking the staging
+ * object (performing instantiation).
+ *
+ * The third argument determines whether or not a final `extend' call must
+ * be explicit: in this case, any instantiation attempts will result in an
+ * exception being thrown.
+ *
+ * @param {function()} basef returns base from which to lazily
+ * extend
+ * @param {Array.} traits traits to mix in
+ * @param {boolean} nonbase extending from a non-base class
+ * (setting will permit instantiation
+ * with implicit extend)
+ *
+ * @return {Function} staging object for mixin
+ */
+function createUse( basef, traits, nonbase )
+{
+ // invoking the partially applied class will immediately complete its
+ // definition and instantiate it with the provided constructor arguments
+ var partial = function()
+ {
+ // this argument will be set only in the case where an existing
+ // (non-base) class is extended, meaning that an explict Class or
+ // AbstractClass was not provided
+ if ( !( nonbase ) )
+ {
+ throw TypeError(
+ "Cannot instantiate incomplete class definition; did " +
+ "you forget to call `extend'?"
+ );
+ }
+
+ return createMixedClass( basef(), traits )
+ .apply( null, arguments );
+ };
+
+
+ // otherwise, its definition is deferred until additional context is
+ // given during the extend operation
+ partial.extend = function()
+ {
+ var args = Array.prototype.slice.call( arguments ),
+ dfn = args.pop(),
+ ext_base = args.pop(),
+ base = basef();
+
+ // extend the mixed class, which ensures that all super references
+ // are properly resolved
+ return extend.call( null,
+ createMixedClass( ( base || ext_base ), traits ),
+ dfn
+ );
+ };
+
+ // syntatic sugar to avoid the aruduous and seemingly pointless `extend'
+ // call simply to mix in another trait
+ partial.use = function()
+ {
+ return createUse(
+ function()
+ {
+ return partial.__createBase();
+ },
+ Array.prototype.slice.call( arguments ),
+ nonbase
+ );
+ };
+
+ // allows overriding default behavior
+ partial.__createBase = function()
+ {
+ return partial.extend( {} );
+ };
+
+ return partial;
+}
+
+
+function createMixedClass( base, traits )
+{
+ // generated definition for our [abstract] class that will mix in each
+ // of the provided traits; it will automatically be marked as abstract
+ // if needed
+ var dfn = { ___$$auto$abstract$$: true };
+
+ // this object is used as a class-specific context for storing trait
+ // data; it will be encapsulated within a ctor closure and will not be
+ // attached to any class
+ var tc = [];
+
+ // "mix" each trait into the class definition object
+ for ( var i = 0, n = traits.length; i < n; i++ )
+ {
+ traits[ i ].__mixin( dfn, tc, ( base || ClassBuilder.ClassBase ) );
+ }
+
+ // create the mixed class from the above generated definition
+ var C = extend.call( null, base, dfn ),
+ meta = ClassBuilder.getMeta( C );
+
+ // add each trait to the list of implemented types so that the
+ // class is considered to be of type T in traits
+ var impl = meta.implemented;
+ for ( var i = 0, n = traits.length; i < n; i++ )
+ {
+ impl.push( traits[ i ] );
+ traits[ i ].__mixinImpl( impl );
+ }
+
+ return C;
}
@@ -454,6 +635,7 @@ function setupProps( func )
{
attachExtend( func );
attachImplement( func );
+ attachUse( func );
}
@@ -503,3 +685,25 @@ function attachImplement( func )
});
}
+
+/**
+ * Attaches use method to the given function (class)
+ *
+ * Please see the `use' export of this module for more information.
+ *
+ * @param {function()} func function (class) to attach method to
+ *
+ * @return {undefined}
+ */
+function attachUse( func )
+{
+ util.defineSecureProp( func, 'use', function()
+ {
+ return createUse(
+ function() { return func; },
+ Array.prototype.slice.call( arguments ),
+ true
+ );
+ } );
+}
+
diff --git a/lib/class_abstract.js b/lib/class_abstract.js
index 42a0327..4654cb6 100644
--- a/lib/class_abstract.js
+++ b/lib/class_abstract.js
@@ -61,19 +61,31 @@ exports.extend = function()
};
+/**
+ * Mixes in a trait
+ *
+ * @return {Object} staged abstract class
+ */
+exports.use = function()
+{
+ return abstractOverride(
+ Class.use.apply( this, arguments )
+ );
+};
+
+
/**
* Creates an abstract class implementing the given members
*
* Simply wraps the class module's implement() method.
*
- * @return {Object} abstract class
+ * @return {Object} staged abstract class
*/
exports.implement = function()
{
- var impl = Class.implement.apply( this, arguments );
-
- abstractOverride( impl );
- return impl;
+ return abstractOverride(
+ Class.implement.apply( this, arguments )
+ );
};
@@ -110,15 +122,22 @@ function markAbstract( args )
function abstractOverride( obj )
{
var extend = obj.extend,
- impl = obj.implement;
+ impl = obj.implement,
+ use = obj.use;
- // wrap and apply the abstract flag, only if the method is defined (it may
- // not be under all circumstances, e.g. after an implement())
+ // wrap and apply the abstract flag, only if the method is defined (it
+ // may not be under all circumstances, e.g. after an implement())
impl && ( obj.implement = function()
{
return abstractOverride( impl.apply( this, arguments ) );
} );
+ var mixin = false;
+ use && ( obj.use = function()
+ {
+ return abstractOverride( use.apply( this, arguments ) );
+ } );
+
// wrap extend, applying the abstract flag
obj.extend = function()
{
@@ -126,6 +145,14 @@ function abstractOverride( obj )
return extend.apply( this, arguments );
};
+ // used by mixins; we need to mark the intermediate subtype as abstract,
+ // but ensure we don't throw any errors if no abstract members are mixed
+ // in (since thay may be mixed in later on)
+ obj.__createBase = function()
+ {
+ return extend( { ___$$auto$abstract$$: true } );
+ };
+
return obj;
}
diff --git a/lib/interface.js b/lib/interface.js
index dcb6beb..b2661f2 100644
--- a/lib/interface.js
+++ b/lib/interface.js
@@ -190,6 +190,9 @@ var extend = ( function( extending )
prototype = new base(),
iname = '',
+ // holds validation state
+ vstate = {},
+
members = member_builder.initMembers(
prototype, prototype, prototype
)
@@ -235,7 +238,8 @@ var extend = ( function( extending )
}
member_builder.buildMethod(
- members, null, name, value, keywords
+ members, null, name, value, keywords,
+ null, 0, {}, vstate
);
},
} );
diff --git a/lib/prop_parser.js b/lib/prop_parser.js
index 3c36b0a..c3f3bbb 100644
--- a/lib/prop_parser.js
+++ b/lib/prop_parser.js
@@ -33,6 +33,7 @@ var _keywords = {
'virtual': true,
'override': true,
'proxy': true,
+ 'weak': true,
};
diff --git a/lib/util.js b/lib/util.js
index eb5b274..868c6c8 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -257,7 +257,7 @@ exports.copyTo = function( dest, src, deep )
*
* @return undefined
*/
-exports.propParse = function( data, options )
+exports.propParse = function( data, options, context )
{
// todo: profile; function calls are more expensive than if statements, so
// it's probably a better idea not to use fvoid
@@ -305,7 +305,10 @@ exports.propParse = function( data, options )
name = parse_data.name || prop;
keywords = parse_data.keywords || {};
- if ( options.assumeAbstract || keywords[ 'abstract' ] )
+ // note the exception for abstract overrides
+ if ( options.assumeAbstract
+ || ( keywords[ 'abstract' ] && !( keywords[ 'override' ] ) )
+ )
{
// may not be set if assumeAbstract is given
keywords[ 'abstract' ] = true;
@@ -324,13 +327,13 @@ exports.propParse = function( data, options )
// if an 'each' callback was provided, pass the data before parsing it
if ( callbackEach )
{
- callbackEach.call( callbackEach, name, value, keywords );
+ callbackEach.call( context, name, value, keywords );
}
// getter/setter
if ( getter || setter )
{
- callbackGetSet.call( callbackGetSet,
+ callbackGetSet.call( context,
name, getter, setter, keywords
);
}
@@ -338,7 +341,7 @@ exports.propParse = function( data, options )
else if ( ( typeof value === 'function' ) || ( keywords[ 'proxy' ] ) )
{
callbackMethod.call(
- callbackMethod,
+ context,
name,
value,
exports.isAbstractMethod( value ),
@@ -348,7 +351,7 @@ exports.propParse = function( data, options )
// simple property
else
{
- callbackProp.call( callbackProp, name, value, keywords );
+ callbackProp.call( context, name, value, keywords );
}
}
};
diff --git a/test/Class/VisibilityTest.js b/test/Class/VisibilityTest.js
index 944a71d..164e90c 100644
--- a/test/Class/VisibilityTest.js
+++ b/test/Class/VisibilityTest.js
@@ -711,6 +711,43 @@ require( 'common' ).testCase(
},
+ /**
+ * Similar to above test, but ensure that overrides also take effect via
+ * the internal visibility object.
+ */
+ 'Protected method overrides are observable by supertype': function()
+ {
+ var _self = this,
+ called = false;
+
+ var C = this.Class(
+ {
+ 'public doFoo': function()
+ {
+ // will be overridden
+ return this.foo();
+ },
+
+ // will be overridden
+ 'virtual protected foo': function()
+ {
+ _self.fail( true, false, "Method not overridden" );
+ },
+ } )
+ .extend(
+ {
+ // should be invoked by doFoo; visibiility escalation
+ 'public override foo': function()
+ {
+ called = true;
+ },
+ } );
+
+ C().doFoo();
+ this.assertOk( called );
+ },
+
+
/**
* There was an issue where the private property object was not proxying
* values to the true protected values. This would mean that when the parent
diff --git a/test/ClassBuilder/MemberRestrictionTest.js b/test/ClassBuilder/MemberRestrictionTest.js
index 7643249..dd79fd1 100644
--- a/test/ClassBuilder/MemberRestrictionTest.js
+++ b/test/ClassBuilder/MemberRestrictionTest.js
@@ -23,18 +23,18 @@ require( 'common' ).testCase(
{
caseSetUp: function()
{
- this.Class = this.require( 'class' );
- this.Sut = this.require( 'ClassBuilder' );
- },
+ // XXX: the Sut is not directly tested; get rid of these!
+ this.Class = this.require( 'class' );
+ this.AbstractClass = this.require( 'class_abstract' );
+ this.Sut = this.require( 'ClassBuilder' );
- setUp: function()
- {
- this.builder = this.Sut(
- this.require( 'MemberBuilder' )(),
- this.require( 'VisibilityObjectFactoryFactory' )
- .fromEnvironment()
- );
+ // weak flag test data
+ this.weak = [
+ [ 'weak foo', 'foo' ], // former weak
+ [ 'foo', 'weak foo' ], // latter weak
+ [ 'weak foo', 'weak foo' ], // both weak
+ ];
},
@@ -227,4 +227,83 @@ require( 'common' ).testCase(
}, Error, "Forced-public methods must be declared as public" );
}
},
+
+
+ /**
+ * If different keywords are used, then a definition object could
+ * contain two members of the same name. This is probably a bug in the
+ * user's implementation, so we should flip our shit.
+ *
+ * But, see the next test.
+ */
+ 'Cannot define two members of the same name': function()
+ {
+ var _self = this;
+ this.assertThrows( function()
+ {
+ // duplicate foos
+ _self.Class(
+ {
+ 'public foo': function() {},
+ 'protected foo': function() {},
+ } );
+ } );
+ },
+
+
+ /**
+ * Code generation tools may find it convenient to declare a duplicate
+ * member without knowing whether or not a duplicate will exist; this
+ * may save time and complexity when ease.js has been designed to handle
+ * certain situations. If at least one of the conflicting members has
+ * been flagged as `weak', then we should ignore the error.
+ *
+ * As an example, this is used interally with ease.js to inherit
+ * abstract members from traits while still permitting concrete
+ * definitions.
+ */
+ '@each(weak) Can define members of the same name if one is weak':
+ function( weak )
+ {
+ // TODO: this makes assumptions about how the code works; the code
+ // needs to be refactored to permit more sane testing (since right
+ // now it'd be a clusterfuck)
+ var dfn = {};
+ dfn[ 'abstract ' + weak[ 0 ] ] = [];
+ dfn[ 'abstract ' + weak[ 1 ] ] = [];
+
+ var _self = this;
+ this.assertDoesNotThrow( function()
+ {
+ _self.AbstractClass( dfn );
+ } );
+ },
+
+
+ /**
+ * During the course of processing, certain data are accumulated into
+ * the member builder state; this state must be post-processed to
+ * complete anything that may be pending.
+ */
+ 'Member builder state is ended after processing': function()
+ {
+ var _self = this,
+ build = this.require( 'MemberBuilder' )();
+
+ var sut = this.Sut(
+ build,
+ this.require( 'VisibilityObjectFactoryFactory' )
+ .fromEnvironment()
+ );
+
+ // TODO: test that we're passed the right state
+ var called = false;
+ build.end = function( state )
+ {
+ called = true;
+ };
+
+ sut.build( {} );
+ this.assertOk( called );
+ },
} );
diff --git a/test/IndexTest.js b/test/IndexTest.js
index a1b22a7..6abe16e 100644
--- a/test/IndexTest.js
+++ b/test/IndexTest.js
@@ -55,6 +55,10 @@ require( 'common' ).testCase(
{
this.exportedAs( 'Interface', 'interface' );
},
+ 'Trait module is exported as `Trait\'': function()
+ {
+ this.exportedAs( 'Trait', 'Trait' );
+ },
'Version information is exported as `version\'': function()
{
this.exportedAs( 'version', 'version' );
diff --git a/test/MemberBuilder/GetterSetterTest.js b/test/MemberBuilder/GetterSetterTest.js
index e09e89f..297016a 100644
--- a/test/MemberBuilder/GetterSetterTest.js
+++ b/test/MemberBuilder/GetterSetterTest.js
@@ -30,30 +30,32 @@ require( 'common' ).testCase(
{
var _self = this;
- this.testArgs = function( args, name, value, keywords )
+ this.testArgs = function( args, name, value, keywords, state )
{
- shared.testArgs( _self, args, name, value, keywords, function(
- prev_default, pval_given, pkey_given
- )
- {
- var expected = _self.members[ 'public' ][ name ];
-
- if ( !expected )
+ shared.testArgs( _self, args, name, value, keywords, state,
+ function(
+ prev_default, pval_given, pkey_given
+ )
{
- return prev_default;
- }
+ var expected = _self.members[ 'public' ][ name ];
- return {
- value: {
- expected: expected,
- given: pval_given.member,
- },
- keywords: {
- expected: null, // XXX
- given: pkey_given,
- },
- };
- } );
+ if ( !expected )
+ {
+ return prev_default;
+ }
+
+ return {
+ value: {
+ expected: expected,
+ given: pval_given.member,
+ },
+ keywords: {
+ expected: null, // XXX
+ given: pkey_given,
+ },
+ };
+ }
+ );
};
},
diff --git a/test/MemberBuilder/MethodTest.js b/test/MemberBuilder/MethodTest.js
index 72b86c8..b88fc0b 100644
--- a/test/MemberBuilder/MethodTest.js
+++ b/test/MemberBuilder/MethodTest.js
@@ -27,31 +27,37 @@ require( 'common' ).testCase(
{
var _self = this;
- this.testArgs = function( args, name, value, keywords )
+ this.testArgs = function( args, name, value, keywords, state )
{
- shared.testArgs( _self, args, name, value, keywords, function(
- prev_default, pval_given, pkey_given
- )
- {
- var expected = _self.members[ 'public' ][ name ];
-
- if ( !expected )
+ shared.testArgs( _self, args, name, value, keywords, state,
+ function(
+ prev_default, pval_given, pkey_given
+ )
{
- return prev_default;
- }
+ var expected = _self.members[ 'public' ][ name ];
- return {
- value: {
- expected: expected,
- given: pval_given.member,
- },
- keywords: {
- expected: expected.___$$keywords$$, // XXX
- given: pkey_given,
- },
- };
- } );
+ if ( !expected )
+ {
+ return prev_default;
+ }
+
+ return {
+ value: {
+ expected: expected,
+ given: pval_given.member,
+ },
+ keywords: {
+ expected: expected.___$$keywords$$, // XXX
+ given: pkey_given,
+ },
+ };
+ }
+ );
};
+
+ // simply intended to execute test two two perspectives
+ this.weakab = [
+ ];
},
@@ -96,18 +102,20 @@ require( 'common' ).testCase(
name = 'foo',
value = function() {},
+ state = {},
keywords = {}
;
this.mockValidate.validateMethod = function()
{
called = true;
- _self.testArgs( arguments, name, value, keywords );
+ _self.testArgs( arguments, name, value, keywords, state );
};
- this.sut.buildMethod(
- this.members, {}, name, value, keywords, function() {}, 1, {}
- );
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, value, keywords, function() {}, 1, {},
+ state
+ ) );
this.assertEqual( true, called, 'validateMethod() was not called' );
},
@@ -133,9 +141,9 @@ require( 'common' ).testCase(
_self.testArgs( arguments, name, value, keywords );
};
- this.sut.buildMethod(
+ this.assertOk( this.sut.buildMethod(
this.members, {}, name, value, keywords, function() {}, 1, {}
- );
+ ) );
this.assertEqual( true, called, 'validateMethod() was not called' );
},
@@ -159,9 +167,9 @@ require( 'common' ).testCase(
;
// build the proxy
- this.sut.buildMethod(
+ this.assertOk( this.sut.buildMethod(
this.members, {}, name, value, keywords, instCallback, cid, {}
- );
+ ) );
this.assertNotEqual( null, this.proxyFactoryCall,
"Proxy factory should be used when `proxy' keyword is provided"
@@ -181,4 +189,97 @@ require( 'common' ).testCase(
"Generated proxy method should be properly assigned to members"
);
},
+
+
+ /**
+ * A weak abstract method may exist in a situation where a code
+ * generator is not certain whether a concrete implementation may be
+ * provided. In this case, we would not want to actually create an
+ * abstract method if a concrete one already exists.
+ */
+ 'Weak abstract methods are not processed if concrete is available':
+ function()
+ {
+ var _self = this,
+ called = false,
+
+ cid = 1,
+ name = 'foo',
+ cval = function() { called = true; },
+ aval = [],
+
+ ckeywords = {},
+ akeywords = { weak: true, 'abstract': true, },
+
+ instCallback = function() {}
+ ;
+
+ // first define abstract
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, aval, akeywords, instCallback, cid, {}
+ ) );
+
+ // concrete should take precedence
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, cval, ckeywords, instCallback, cid, {}
+ ) );
+
+ this.members[ 'public' ].foo();
+ this.assertOk( called, "Concrete method did not take precedence" );
+
+ // now try abstract again to ensure this works from both directions
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, aval, akeywords, instCallback, cid, {}
+ ) === false );
+
+ this.members[ 'public' ].foo();
+ this.assertOk( called, "Concrete method unkept" );
+ },
+
+
+ /**
+ * Same concept as the above, but with virtual methods (which have a
+ * concrete implementation available by default).
+ */
+ 'Weak virtual methods are not processed if override is available':
+ function()
+ {
+ var _self = this,
+ called = false,
+
+ cid = 1,
+ name = 'foo',
+ oval = function() { called = true; },
+ vval = function()
+ {
+ _self.fail( true, false, "Method not overridden." );
+ },
+
+ okeywords = { 'override': true },
+ vkeywords = { weak: true, 'virtual': true },
+
+ instCallback = function() {}
+ ;
+
+ // define the virtual method
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, vval, vkeywords, instCallback, cid, {}
+ ) );
+
+ // override should take precedence
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, oval, okeywords, instCallback, cid, {}
+ ) );
+
+ this.members[ 'public' ].foo();
+ this.assertOk( called, "Override did not take precedence" );
+
+ // now try virtual again to ensure this works from both directions
+ this.assertOk( this.sut.buildMethod(
+ this.members, {}, name, vval, vkeywords, instCallback, cid, {}
+ ) === false );
+
+ this.members[ 'public' ].foo();
+ this.assertOk( called, "Override unkept" );
+ },
} );
diff --git a/test/MemberBuilder/PropTest.js b/test/MemberBuilder/PropTest.js
index fbb530b..8b8ffd2 100644
--- a/test/MemberBuilder/PropTest.js
+++ b/test/MemberBuilder/PropTest.js
@@ -27,30 +27,32 @@ require( 'common' ).testCase(
{
var _self = this;
- this.testArgs = function( args, name, value, keywords )
+ this.testArgs = function( args, name, value, keywords, state )
{
- shared.testArgs( _self, args, name, value, keywords, function(
- prev_default, pval_given, pkey_given
- )
- {
- var expected = _self.members[ 'public' ][ name ];
-
- if ( !expected )
+ shared.testArgs( _self, args, name, value, keywords, state,
+ function(
+ prev_default, pval_given, pkey_given
+ )
{
- return prev_default;
- }
+ var expected = _self.members[ 'public' ][ name ];
- return {
- value: {
- expected: expected[ 0 ],
- given: pval_given.member[ 0 ],
- },
- keywords: {
- expected: expected[ 1 ],
- given: pkey_given,
- },
- };
- } );
+ if ( !expected )
+ {
+ return prev_default;
+ }
+
+ return {
+ value: {
+ expected: expected[ 0 ],
+ given: pval_given.member[ 0 ],
+ },
+ keywords: {
+ expected: expected[ 1 ],
+ given: pkey_given,
+ },
+ };
+ }
+ );
};
},
diff --git a/test/MemberBuilder/inc-common.js b/test/MemberBuilder/inc-common.js
index 7a92e19..1dd23be 100644
--- a/test/MemberBuilder/inc-common.js
+++ b/test/MemberBuilder/inc-common.js
@@ -27,11 +27,14 @@
* @param {string} name member name
* @param {*} value expected value
* @param {Object} keywords expected keywords
- * @param {function()} prevLookup function to use to look up prev member data
+ * @param {Object} state validation state
+ * @param {function()} prevLookup function to look up prev member data
*
* @return {undefined}
*/
-exports.testArgs = function( testcase, args, name, value, keywords, prevLookup )
+exports.testArgs = function(
+ testcase, args, name, value, keywords, state, prevLookup
+)
{
var prev = {
value: { expected: null, given: args[ 3 ] },
@@ -41,24 +44,28 @@ exports.testArgs = function( testcase, args, name, value, keywords, prevLookup )
prev = prevLookup( prev, prev.value.given, prev.keywords.given );
testcase.assertEqual( name, args[ 0 ],
- 'Incorrect name passed to validator'
+ "Incorrect name passed to validator"
);
testcase.assertDeepEqual( value, args[ 1 ],
- 'Incorrect value passed to validator'
+ "Incorrect value passed to validator"
);
testcase.assertStrictEqual( keywords, args[ 2 ],
- 'Incorrect keywords passed to validator'
+ "Incorrect keywords passed to validator"
);
testcase.assertStrictEqual( prev.value.expected, prev.value.given,
- 'Previous data should contain prev value if overriding, ' +
- 'otherwise null'
+ "Previous data should contain prev value if overriding, " +
+ "otherwise null"
);
testcase.assertDeepEqual( prev.keywords.expected, prev.keywords.given,
- 'Previous keywords should contain prev keyword if ' +
- 'overriding, otherwise null'
+ "Previous keywords should contain prev keyword if " +
+ "overriding, otherwise null"
+ );
+
+ testcase.assertStrictEqual( state, args[ 5 ],
+ "State object was not passed to validator"
);
};
diff --git a/test/MemberBuilderValidator/MethodTest.js b/test/MemberBuilderValidator/MethodTest.js
index 637cc46..a32eab2 100644
--- a/test/MemberBuilderValidator/MethodTest.js
+++ b/test/MemberBuilderValidator/MethodTest.js
@@ -27,6 +27,7 @@ require( 'common' ).testCase(
caseSetUp: function()
{
var _self = this;
+ this.util = this.require( 'util' );
this.quickKeywordMethodTest = function( keywords, identifier, prev )
{
@@ -50,13 +51,18 @@ require( 'common' ).testCase(
startobj.virtual = true;
overrideobj.override = true;
+ var state = {};
+
_self.sut.validateMethod(
name,
function() {},
overrideobj,
{ member: function() {} },
- startobj
+ startobj,
+ state
);
+
+ _self.sut.end( state );
},
failstr
);
@@ -127,7 +133,7 @@ require( 'common' ).testCase(
_self.sut.validateMethod(
name, function() {}, {},
{ get: function() {} },
- {}
+ {}, {}
);
} );
@@ -137,7 +143,7 @@ require( 'common' ).testCase(
_self.sut.validateMethod(
name, function() {}, {},
{ set: function() {} },
- {}
+ {}, {}
);
} );
},
@@ -160,7 +166,7 @@ require( 'common' ).testCase(
_self.sut.validateMethod(
name, function() {}, {},
{ member: 'immaprop' },
- {}
+ {}, {}
);
} );
},
@@ -208,6 +214,21 @@ require( 'common' ).testCase(
},
+ /**
+ * Contrary to the above test, an abstract method may appear after its
+ * concrete implementation if the `weak' keyword is provided; this
+ * exists to allow code generation tools to fall back to abstract
+ * without having to invoke the property parser directly, complicating
+ * their logic and duplicating work that ease.js will already do.
+ */
+ 'Concrete method may appear with weak abstract method': function()
+ {
+ this.quickKeywordMethodTest(
+ [ 'weak', 'abstract' ], null, []
+ );
+ },
+
+
/**
* The parameter list is part of the class interface. Changing the length
* will make the interface incompatible with that of its parent and make
@@ -230,7 +251,8 @@ require( 'common' ).testCase(
// this function returns each of its arguments, otherwise
// they'll be optimized away by Closure Compiler.
{ member: function( a, b, c ) { return [a,b,c]; } },
- { 'virtual': true }
+ { 'virtual': true },
+ {}
);
} );
@@ -246,7 +268,8 @@ require( 'common' ).testCase(
function() {},
{ 'override': true },
{ member: parent_method },
- { 'virtual': true }
+ { 'virtual': true },
+ {}
);
} );
@@ -261,12 +284,53 @@ require( 'common' ).testCase(
method,
{ 'override': true },
{ member: function( a, b, c ) {} },
- { 'virtual': true }
+ { 'virtual': true },
+ {}
);
}, Error );
},
+ /**
+ * Same concept as the above test, but ensure that the logic for weak
+ * abstract members does not skip the valiation. Furthermore, if a weak
+ * abstract member is found *after* the concrete definition, the same
+ * restrictions should apply retroacively.
+ */
+ 'Weak abstract overrides must meet compatibility requirements':
+ function()
+ {
+ var _self = this,
+ name = 'foo',
+ amethod = _self.util.createAbstractMethod( [ 'one' ] );
+
+ // abstract appears before
+ this.quickFailureTest( name, 'compatible', function()
+ {
+ _self.sut.validateMethod(
+ name,
+ function() {},
+ {},
+ { member: amethod },
+ { 'weak': true, 'abstract': true },
+ {}
+ );
+ } );
+
+ // abstract appears after
+ this.quickFailureTest( name, 'compatible', function()
+ {
+ _self.sut.validateMethod(
+ name,
+ amethod,
+ { 'weak': true, 'abstract': true },
+ { member: function() {} },
+ {}, {}
+ );
+ } );
+ },
+
+
/**
* One should not be able to, for example, declare a private method it had
* previously been declared protected, or declare it as protected if it has
@@ -359,6 +423,52 @@ require( 'common' ).testCase(
},
+ /**
+ * The above test provides problems if we have a weak method that
+ * follows the definition of the override within the same definition
+ * object (that is---A' is defined before A where A' overrides A and A
+ * is weak); we must ensure that the warning is deferred until we're
+ * certain that we will not encounter a weak method.
+ */
+ 'Does not throw warning when overriding a later weak method': function()
+ {
+ var _self = this;
+ this.warningHandler = function( warning )
+ {
+ _self.fail( true, false, "Warning was issued." );
+ };
+
+ this.assertDoesNotThrow( function()
+ {
+ var state = {};
+
+ // this should place a warning into the state
+ _self.sut.validateMethod(
+ 'foo',
+ function() {},
+ { 'override': true },
+ undefined, // no previous because weak was
+ undefined, // not yet encountered
+ state
+ );
+
+ // this should remove it upon encountering `weak'
+ _self.sut.validateMethod(
+ 'foo',
+ function() {},
+ { 'weak': true, 'abstract': true },
+ { member: function() {} }, // same as previously defined
+ { 'override': true }, // above
+ state
+ );
+
+ // hopefully we don't trigger warnings (if we do, the warning
+ // handler defined above will fail this test)
+ _self.sut.end( state );
+ } );
+ },
+
+
/**
* Wait - what? That doesn't make sense from an OOP perspective, now does
* it! Unfortunately, we're forced into this restriction in order to
@@ -393,7 +503,7 @@ require( 'common' ).testCase(
{
// provide function instead of string
_self.sut.validateMethod(
- name, function() {}, { 'proxy': true }, {}, {}
+ name, function() {}, { 'proxy': true }, {}, {}, {}
);
} );
},
@@ -409,7 +519,7 @@ require( 'common' ).testCase(
this.assertDoesNotThrow( function()
{
_self.sut.validateMethod(
- 'foo', 'dest', { 'proxy': true }, {}, {}
+ 'foo', 'dest', { 'proxy': true }, {}, {}, {}
);
}, TypeError );
},
diff --git a/test/MemberBuilderValidator/inc-common.js b/test/MemberBuilderValidator/inc-common.js
index e717ea9..052286f 100644
--- a/test/MemberBuilderValidator/inc-common.js
+++ b/test/MemberBuilderValidator/inc-common.js
@@ -68,7 +68,7 @@ exports.quickFailureTest = function( name, identifier, action )
return;
}
- _self.fail( "Expected failure" );
+ _self.fail( false, true, "Expected failure" );
};
@@ -87,6 +87,7 @@ exports.quickKeywordTest = function(
prev_obj = {},
prev_data = prev_data || {},
name = exports.testName,
+ state = {},
_self = this;
// convert our convenient array into a keyword obj
@@ -114,7 +115,7 @@ exports.quickKeywordTest = function(
var val = ( keyword_obj[ 'proxy' ] ) ? 'proxyDest': function() {};
_self.sut[ type ](
- name, val, keyword_obj, prev_data, prev_obj
+ name, val, keyword_obj, prev_data, prev_obj, state
);
};
@@ -124,8 +125,10 @@ exports.quickKeywordTest = function(
}
else
{
- this.assertDoesNotThrow( testfunc, Error );
+ this.assertDoesNotThrow( testfunc );
}
+
+ this.sut.end( state );
};
diff --git a/test/MethodWrappersTest.js b/test/MethodWrappersTest.js
index 849da43..890a330 100644
--- a/test/MethodWrappersTest.js
+++ b/test/MethodWrappersTest.js
@@ -389,5 +389,27 @@ require( 'common' ).testCase(
"Should properly proxy to static membesr via static accessor method"
);
},
+
+
+ /**
+ * A proxy method should be able to be used as a concrete implementation
+ * for an abstract method; this means that it must properly expose the
+ * number of arguments of the method that it is proxying to. The problem
+ * is---it can't, because we do not have a type system and so we cannot
+ * know what we will be proxying to at runtime!
+ *
+ * As such, we have no choice (since validations are not at proxy time)
+ * but to set the length to something ridiculous so that it will never
+ * fail.
+ */
+ 'Proxy methods are able to satisfy abstract method param requirements':
+ function()
+ {
+ var f = this._sut.standard.wrapProxy(
+ {}, null, 0, function() {}, '', {}
+ );
+
+ this.assertOk( !( 0 < f.__length ) );
+ },
} );
diff --git a/test/PropParserKeywordsTest.js b/test/PropParserKeywordsTest.js
index 0225d21..0680660 100644
--- a/test/PropParserKeywordsTest.js
+++ b/test/PropParserKeywordsTest.js
@@ -100,7 +100,7 @@ require( 'common' ).testCase(
parse(
'public protected private ' +
'virtual abstract override ' +
- 'static const proxy ' +
+ 'static const proxy weak ' +
'var'
);
}, Error );
diff --git a/test/Trait/AbstractTest.js b/test/Trait/AbstractTest.js
new file mode 100644
index 0000000..9f59e32
--- /dev/null
+++ b/test/Trait/AbstractTest.js
@@ -0,0 +1,363 @@
+/**
+ * Tests abstract trait definition and use
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ this.AbstractClass = this.require( 'class_abstract' );
+ },
+
+
+ /**
+ * If a trait contains an abstract member, then any class that uses it
+ * should too be considered abstract if no concrete implementation is
+ * provided.
+ */
+ 'Abstract traits create abstract classes when used': function()
+ {
+ var T = this.Sut( { 'abstract foo': [] } );
+
+ var _self = this;
+ this.assertDoesNotThrow( function()
+ {
+ // no concrete `foo; should be abstract (this test is sufficient
+ // because AbstractClass will throw an error if there are no
+ // abstract members)
+ _self.AbstractClass.use( T ).extend( {} );
+ }, Error );
+ },
+
+
+ /**
+ * A class may still be concrete even if it uses abstract traits so long
+ * as it provides concrete implementations for each of the trait's
+ * abstract members.
+ */
+ 'Concrete classes may use abstract traits by definining members':
+ function()
+ {
+ var T = this.Sut( { 'abstract traitfoo': [ 'foo' ] } ),
+ C = null,
+ called = false;
+
+ var _self = this;
+ this.assertDoesNotThrow( function()
+ {
+ C = _self.Class.use( T ).extend(
+ {
+ traitfoo: function( foo ) { called = true; },
+ } );
+ } );
+
+ // sanity check
+ C().traitfoo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * The concrete methods provided by a class must be compatible with the
+ * abstract definitions of any used traits. This test ensures not only
+ * that the check is being performed, but that the abstract declaration
+ * is properly inherited from the trait.
+ *
+ * TODO: The error mentions "supertype" compatibility, which (although
+ * true) may be confusing; perhaps reference the trait that declared the
+ * method as abstract.
+ */
+ 'Concrete classes must be compatible with abstract traits': function()
+ {
+ var T = this.Sut( { 'abstract traitfoo': [ 'foo' ] } );
+
+ var _self = this;
+ this.assertThrows( function()
+ {
+ C = _self.Class.use( T ).extend(
+ {
+ // missing param in definition
+ traitfoo: function() {},
+ } );
+ } );
+ },
+
+
+ /**
+ * If a trait defines an abstract method, then it should be able to
+ * invoke a concrete method of the same name defined by a class.
+ */
+ 'Traits can invoke concrete class implementation of abstract method':
+ function()
+ {
+ var expected = 'foobar';
+
+ var T = this.Sut(
+ {
+ 'public getFoo': function()
+ {
+ return this.echo( expected );
+ },
+
+ 'abstract protected echo': [ 'value' ],
+ } );
+
+ var result = this.Class.use( T ).extend(
+ {
+ // concrete implementation of abstract trait method
+ 'protected echo': function( value )
+ {
+ return value;
+ },
+ } )().getFoo();
+
+ this.assertEqual( result, expected );
+ },
+
+
+ /**
+ * Even more kinky is when a trait provides a concrete implementation
+ * for an abstract method that is defined in another trait that is mixed
+ * into the same class. This makes sense, because that class acts as
+ * though the trait's abstract method is its own. This allows for
+ * message passing between two traits with the class as the mediator.
+ *
+ * This is otherwise pretty much the same as the above test. Note that
+ * we use a public `echo' method; this is to ensure that we do not break
+ * in the event that protected trait members break (that is: are not
+ * exposed to the class).
+ */
+ 'Traits can invoke concrete trait implementation of abstract method':
+ function()
+ {
+ var expected = 'traitbar';
+
+ // same as the previous test
+ var Ta = this.Sut(
+ {
+ 'public getFoo': function()
+ {
+ return this.echo( expected );
+ },
+
+ 'abstract public echo': [ 'value' ],
+ } );
+
+ // but this is new
+ var Tc = this.Sut(
+ {
+ // concrete implementation of abstract trait method
+ 'public echo': function( value )
+ {
+ return value;
+ },
+ } );
+
+ this.assertEqual(
+ this.Class.use( Ta, Tc ).extend( {} )().getFoo(),
+ expected
+ );
+
+ // order shouldn't matter (because that'd be confusing and
+ // frustrating to users, depending on how the traits are named), so
+ // let's do this again in reverse order
+ this.assertEqual(
+ this.Class.use( Tc, Ta ).extend( {} )().getFoo(),
+ expected,
+ "Crap; order matters?!"
+ );
+ },
+
+
+ /**
+ * If some trait T used by abstract class C defines abstract method M,
+ * then some subtype C' of C should be able to provide a concrete
+ * definition of M such that T.M() invokes C'.M.
+ */
+ 'Abstract method inherited from trait can be implemented by subtype':
+ function()
+ {
+ var T = this.Sut(
+ {
+ 'public doFoo': function()
+ {
+ // should invoke the concrete implementation
+ this.foo();
+ },
+
+ 'abstract protected foo': [],
+ } );
+
+ var called = false;
+
+ // C is a concrete class that extends an abstract class that uses
+ // trait T
+ var C = this.AbstractClass.use( T ).extend( {} )
+ .extend(
+ {
+ // concrete definition that should be invoked by T.doFoo
+ 'protected foo': function()
+ {
+ called = true;
+ },
+ } );
+
+ C().doFoo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * Ensure that chained mixins (that is, calling `use' multiple times
+ * independently) maintains the use of AbstractClass, and properly
+ * performs the abstract check at the final `extend' call.
+ */
+ 'Chained mixins properly carry abstract flag': function()
+ {
+ var _self = this,
+ Ta = this.Sut( { foo: function() {} } ),
+ Tc = this.Sut( { baz: function() {} } ),
+ Tab = this.Sut( { 'abstract baz': [] } );
+
+ // ensure that abstract definitions are carried through properly
+ this.assertDoesNotThrow( function()
+ {
+ // single, abstract
+ _self.assertOk(
+ _self.AbstractClass
+ .use( Tab )
+ .extend( {} )
+ .isAbstract()
+ );
+
+ // single, concrete
+ _self.assertOk(
+ _self.AbstractClass
+ .use( Ta )
+ .extend( { 'abstract baz': [] } )
+ .isAbstract()
+ );
+
+ // chained, both
+ _self.assertOk(
+ _self.AbstractClass
+ .use( Ta )
+ .use( Tab )
+ .extend( {} )
+ .isAbstract()
+
+ );
+ _self.assertOk(
+ _self.AbstractClass
+ .use( Tab )
+ .use( Ta )
+ .extend( {} )
+ .isAbstract()
+ );
+ } );
+
+ // and then ensure that we will properly throw an exception if not
+ this.assertThrows( function()
+ {
+ // not abstract
+ _self.AbstractClass.use( Tc ).extend( {} );
+ } );
+
+ this.assertThrows( function()
+ {
+ // initially abstract, but then not (by extend)
+ _self.AbstractClass.use( Tab ).extend(
+ {
+ // concrete definition; no longer abstract
+ baz: function() {},
+ } );
+ } );
+
+ this.assertThrows( function()
+ {
+ // initially abstract, but then second mix provides a concrete
+ // definition
+ _self.AbstractClass.use( Tab ).use( Tc ).extend( {} );
+ } );
+ },
+
+
+ /**
+ * Mixins can make a class auto-abstract (that is, not require the use
+ * of AbstractClass for the mixin) in order to permit the use of
+ * Type.use when the intent is not to subclass, but to decorate (yes,
+ * the result is still a subtype). Let's make sure that we're not
+ * breaking the AbstractClass requirement, whose sole purpose is to aid
+ * in documentation by creating self-documenting code.
+ */
+ 'Explicitly-declared class will not be automatically abstract':
+ function()
+ {
+ var _self = this,
+ Tc = this.Sut( { foo: function() {} } ),
+ Ta = this.Sut( { 'abstract foo': [], } );
+
+ // if we provide no abstract methods, then declaring the class as
+ // abstract should result in an error
+ this.assertThrows( function()
+ {
+ // no abstract methods
+ _self.assertOk( !(
+ _self.AbstractClass.use( Tc ).extend( {} ).isAbstract()
+ ) );
+ } );
+
+ // similarily, if we provide abstract methods, then there should be
+ // no error
+ this.assertDoesNotThrow( function()
+ {
+ // abstract methods via extend
+ _self.assertOk(
+ _self.AbstractClass.use( Tc ).extend(
+ {
+ 'abstract bar': [],
+ } ).isAbstract()
+ );
+
+ // abstract via trait
+ _self.assertOk(
+ _self.AbstractClass.use( Ta ).extend( {} ).isAbstract()
+ );
+ } );
+
+ // if we provide abstract methods, then we should not be able to
+ // declare a class as concrete
+ this.assertThrows( function()
+ {
+ _self.Class.use( Tc ).extend(
+ {
+ 'abstract bar': [],
+ } );
+ } );
+
+ // similar to above, but via trait
+ this.assertThrows( function()
+ {
+ _self.Class.use( Ta ).extend();
+ } );
+ },
+} );
diff --git a/test/Trait/ClassVirtualTest.js b/test/Trait/ClassVirtualTest.js
new file mode 100644
index 0000000..49dcf6f
--- /dev/null
+++ b/test/Trait/ClassVirtualTest.js
@@ -0,0 +1,215 @@
+/**
+ * Tests overriding virtual class methods using mixins
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * These tests vary from those in VirtualTest in that, rather than a class
+ * overriding a virtual method defined within a trait, a trait is overriding
+ * a method in the class that it is mixed into. In particular, since
+ * overrides require that the super method actually exist, this means that a
+ * trait must implement or extend a common interface.
+ *
+ * It is this very important (and powerful) system that allows traits to be
+ * used as stackable modifications, similar to how one would use the
+ * decorator pattern (but more tightly coupled).
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ this.AbstractClass = this.require( 'class_abstract' );
+ this.Interface = this.require( 'interface' );
+ },
+
+
+ /**
+ * A trait may implement an interface I for a couple of reasons: to have
+ * the class mixed into be considered to of type I and to override
+ * methods. But, regardless of the reason, let's start with the
+ * fundamentals.
+ */
+ 'Traits may implement an interface': function()
+ {
+ var _self = this;
+
+ // simply make sure that the API is supported; nothing more.
+ this.assertDoesNotThrow( function()
+ {
+ _self.Sut.implement( _self.Interface( {} ) ).extend( {} );
+ } );
+ },
+
+
+ /**
+ * We would expect that the default behavior of implementing an
+ * interface I into a trait would create a trait with all abstract
+ * methods defined by I.
+ */
+ 'Traits implementing interfaces define abstract methods': function()
+ {
+ var I = this.Interface( { foo: [], bar: [] } ),
+ T = this.Sut.implement( I ).extend( {} );
+
+ var Class = this.Class,
+ AbstractClass = this.AbstractClass;
+
+ // T should contain both foo and bar as abstract methods, which we
+ // will test indirectly in the assertions below
+
+ // should fail because of abstract foo and bar
+ this.assertThrows( function()
+ {
+ Class.use( T ).extend( {} );
+ } );
+
+ // should succeed, since we can have abstract methods within an
+ // abstract class
+ this.assertDoesNotThrow( function()
+ {
+ AbstractClass.use( T ).extend( {} );
+ } );
+
+ // one remaining abstract method
+ this.assertDoesNotThrow( function()
+ {
+ AbstractClass.use( T ).extend( { foo: function() {} } );
+ } );
+
+ // both concrete
+ this.assertDoesNotThrow( function()
+ {
+ Class.use( T ).extend(
+ {
+ foo: function() {},
+ bar: function() {},
+ } );
+ } );
+ },
+
+
+ /**
+ * Just as classes implementing interfaces may choose to immediately
+ * provide concrete definitions for the methods declared in the
+ * interface (instead of becoming an abstract class), so too may traits.
+ */
+ 'Traits may provide concrete methods for interfaces': function()
+ {
+ var called = false;
+
+ var I = this.Interface( { foo: [] } ),
+ T = this.Sut.implement( I ).extend(
+ {
+ foo: function()
+ {
+ called = true;
+ },
+ } );
+
+ var Class = this.Class;
+ this.assertDoesNotThrow( function()
+ {
+ // should invoke concrete foo; class definition should not fail,
+ // because foo is no longer abstract
+ Class.use( T ).extend( {} )().foo();
+ } );
+
+ this.assertOk( called );
+ },
+
+
+ /**
+ * Instances of class C mixing in some trait T implementing I will be
+ * considered to be of type I, since any method of I would either be
+ * defined within T, or would be implicitly abstract in T, requiring its
+ * definition within C; otherwise, C would have to be declared astract.
+ */
+ 'Instance of class mixing in trait implementing I is of type I':
+ function()
+ {
+ var I = this.Interface( {} ),
+ T = this.Sut.implement( I ).extend( {} );
+
+ this.assertOk(
+ this.Class.isA( I, this.Class.use( T ).extend( {} )() )
+ );
+ },
+
+
+ /**
+ * The API for multiple interfaces should be the same for traits as it
+ * is for classes.
+ */
+ 'Trait can implement multiple interfaces': function()
+ {
+ var Ia = this.Interface( {} ),
+ Ib = this.Interface( {} ),
+ T = this.Sut.implement( Ia, Ib ).extend( {} ),
+ o = this.Class.use( T ).extend( {} )();
+
+ this.assertOk( this.Class.isA( Ia, o ) );
+ this.assertOk( this.Class.isA( Ib, o ) );
+ },
+
+
+ /**
+ * This is a concept borrowed from Scala: consider class C and trait T,
+ * both implementing interface I which declares method M. T should be
+ * able to override C.M so long as it is concrete, but to do so, we need
+ * some way of telling ease.js that we are overriding at time of mixin;
+ * otherwise, override does not make sense, because I.M is clearly
+ * abstract and there is nothing to override.
+ */
+ 'Mixin can override virtual concrete method defined by interface':
+ function()
+ {
+ var called = false,
+ I = this.Interface( { foo: [] } );
+
+ var T = this.Sut.implement( I ).extend(
+ {
+ // the keyword combination `abstract override' indicates that we
+ // should override whatever concrete implementation was defined
+ // before our having been mixed in
+ 'abstract override foo': function()
+ {
+ called = true;
+ },
+ } );
+
+ var _self = this;
+ var C = this.Class.implement( I ).extend(
+ {
+ // this should be overridden by the mixin and should therefore
+ // never be called (for __super tests, see LinearizationTest)
+ 'virtual foo': function()
+ {
+ _self.fail( false, true,
+ "Concrete class method was not overridden by mixin"
+ );
+ },
+ } );
+
+ // mixing in a trait atop of C should yield the results described
+ // above due to the `abstract override' keyword combination
+ C.use( T )().foo();
+ this.assertOk( called );
+ },
+} );
diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js
new file mode 100644
index 0000000..21325ec
--- /dev/null
+++ b/test/Trait/DefinitionTest.js
@@ -0,0 +1,454 @@
+/**
+ * Tests basic trait definition
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ this.Interface = this.require( 'interface' );
+ this.AbstractClass = this.require( 'class_abstract' );
+
+ this.hasGetSet = !(
+ this.require( 'util' ).definePropertyFallback()
+ );
+
+ // means of creating anonymous traits
+ this.ctor = [
+ this.Sut.extend,
+ this.Sut,
+ ];
+
+ // trait field name conflicts (methods)
+ this.fconflict = [
+ [ 'foo', "same name; no keywords",
+ { foo: function() {} },
+ { foo: function() {} },
+ ],
+
+ [ 'foo', "same keywords; same visibility",
+ { 'public foo': function() {} },
+ { 'public foo': function() {} },
+ ],
+
+ // should (at least for the time being) be picked up by existing
+ // class error checks; TODO: but let's provide trait-specific
+ // error messages to avoid frustration and infuriation
+ [ 'foo', "varying keywords; same visibility",
+ { 'virtual public foo': function() {} },
+ { 'public virtual foo': function() {} },
+ ],
+
+ [ 'foo', "different visibility",
+ { 'public foo': function() {} },
+ { 'protected foo': function() {} },
+ ],
+ ];
+
+ this.base = [ this.Class ];
+ },
+
+
+ /**
+ * We continue with the same concept used for class
+ * definitions---extending the Trait module itself will create an
+ * anonymous trait.
+ */
+ '@each(ctor) Can extend Trait to create anonymous trait': function( T )
+ {
+ this.assertOk( this.Sut.isTrait( T( {} ) ) );
+ },
+
+
+ /**
+ * A trait can only be used by something else---it does not make sense
+ * to instantiate them directly, since they form an incomplete picture.
+ */
+ '@each(ctor) Cannot instantiate trait without error': function( T )
+ {
+ this.assertThrows( function()
+ {
+ T( {} )();
+ }, Error );
+ },
+
+
+ /**
+ * One way that traits acquire meaning is by their use in creating
+ * classes. This also allows us to observe whether traits are actually
+ * working as intended without testing too closely to their
+ * implementation. This test simply ensures that the Class module will
+ * accept our traits.
+ *
+ * Classes consume traits as part of their definition using the `use'
+ * method. We should be able to then invoke the `extend' method to
+ * provide our own definition, without having to inherit from another
+ * class.
+ */
+ '@each(ctor) Base class definition is applied when using traits':
+ function( T )
+ {
+ var expected = 'bar';
+
+ var C = this.Class.use( T( {} ) ).extend(
+ {
+ foo: expected,
+ } );
+
+ this.assertOk( this.Class.isClass( C ) );
+ this.assertEqual( C().foo, expected );
+ },
+
+
+ /**
+ * Traits contribute to the definition of the class that `use's them;
+ * therefore, it would stand to reason that we should still be able to
+ * inherit from a supertype while using traits.
+ */
+ '@each(ctor) Supertype definition is applied when using traits':
+ function( T )
+ {
+ var expected = 'bar';
+ expected2 = 'baz';
+ Foo = this.Class( { foo: expected } ),
+ SubFoo = this.Class.use( T( {} ) )
+ .extend( Foo, { bar: expected2 } );
+
+ var inst = SubFoo();
+
+ this.assertOk( this.Class.isA( Foo, inst ) );
+ this.assertEqual( inst.foo, expected, "Supertype failure" );
+ this.assertEqual( inst.bar, expected2, "Subtype failure" );
+ },
+
+
+ /**
+ * The above tests have ensured that classes are still operable with
+ * traits; we can now test that traits are mixed into the class
+ * definition via `use' by asserting on the trait definitions.
+ */
+ '@each(ctor) Trait definition is mixed into base class definition':
+ function( T )
+ {
+ var called = false;
+
+ var Trait = T( { foo: function() { called = true; } } ),
+ inst = this.Class.use( Trait ).extend( {} )();
+
+ // if mixin was successful, then we should have the `foo' method.
+ this.assertDoesNotThrow( function()
+ {
+ inst.foo();
+ }, Error, "Should have access to mixed in fields" );
+
+ // if our variable was not set, then it was a bs copy
+ this.assertOk( called, "Mixed in field copy error" );
+ },
+
+
+ /**
+ * The above test should apply just the same to subtypes.
+ */
+ '@each(ctor) Trait definition is mixed into subtype definition':
+ function( T )
+ {
+ var called = false;
+
+ var Trait = T( { foo: function() { called = true; } } ),
+ Foo = this.Class( {} ),
+ inst = this.Class.use( Trait ).extend( Foo, {} )();
+
+ inst.foo();
+ this.assertOk( called );
+ },
+
+
+ //
+ // At this point, we assume that each ctor method is working as expected
+ // (that is---the same); we will proceed to test only a single method of
+ // construction under that assumption.
+ //
+
+
+ /**
+ * Traits cannot be instantiated, so they need not define __construct
+ * for themselves; however, they may wish to influence the construction
+ * of anything that uses them. This is poor practice, since that
+ * introduces a war between traits to take over the constructor;
+ * instead, the class using the traits should handle calling the methods
+ * on the traits and we should disallow traits from attempting to set
+ * the constructor.
+ */
+ 'Traits cannot define __construct': function()
+ {
+ try
+ {
+ this.Sut( { __construct: function() {} } );
+ }
+ catch ( e )
+ {
+ this.assertOk( e.message.match( /\b__construct\b/ ) );
+ return;
+ }
+
+ this.fail( "Traits should not be able to define __construct" );
+ },
+
+
+ /**
+ * If two traits attempt to define the same field (by name, regardless
+ * of its type), then an error should be thrown to warn the developer of
+ * a problem; automatic resolution would be a fertile source of nasty
+ * and confusing bugs.
+ *
+ * TODO: conflict resolution through aliasing
+ */
+ '@each(fconflict) Cannot mix in multiple concrete methods of same name':
+ function( dfns )
+ {
+ var fname = dfns[ 0 ],
+ desc = dfns[ 1 ],
+ A = this.Sut( dfns[ 2 ] ),
+ B = this.Sut( dfns[ 3 ] );
+
+ // this, therefore, should error
+ try
+ {
+ this.Class.use( A, B ).extend( {} );
+ }
+ catch ( e )
+ {
+ // the assertion should contain the name of the field that
+ // caused the error
+ this.assertOk(
+ e.message.match( '\\b' + fname + '\\b' ),
+ "Error message missing field name: " + e.message
+ );
+
+ // TODO: we can also make less people hate us if we include the
+ // names of the conflicting traits; in the case of an anonymous
+ // trait, maybe include its index in the use list
+
+ return;
+ }
+
+ this.fail( false, true, "Mixin must fail on conflict: " + desc );
+ },
+
+
+ /**
+ * Traits in ease.js were designed in such a way that an object can be
+ * considered to be a type of any of the traits that its class mixes in;
+ * this is consistent with the concept of interfaces and provides a very
+ * simple and intuitive type system.
+ */
+ 'A class is considered to be a type of each used trait': function()
+ {
+ var Ta = this.Sut( {} ),
+ Tb = this.Sut( {} ),
+ Tc = this.Sut( {} ),
+ o = this.Class.use( Ta, Tb ).extend( {} )();
+
+ // these two were mixed in
+ this.assertOk( this.Class.isA( Ta, o ) );
+ this.assertOk( this.Class.isA( Tb, o ) );
+
+ // this one was not
+ this.assertOk( this.Class.isA( Tc, o ) === false );
+ },
+
+
+ /**
+ * Ensure that the named class staging object permits mixins.
+ */
+ 'Can mix traits into named class': function()
+ {
+ var called = false,
+ T = this.Sut( { foo: function() { called = true; } } );
+
+ this.Class( 'Named' ).use( T ).extend( {} )().foo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * When explicitly defining a class (that is, not mixing into an
+ * existing class definition), which involves the use of Class or
+ * AbstractClass, mixins must be terminated with a call to `extend'.
+ * This allows the system to make a final determination as to whether
+ * the resulting class is abstract.
+ *
+ * Contrast this with Type.use( T )( ... ), where Type is not the base
+ * class (Class) or AbstractClass.
+ */
+ 'Explicit class definitions must be terminated by an extend call':
+ function()
+ {
+ var _self = this,
+ Ta = this.Sut( { foo: function() {} } ),
+ Tb = this.Sut( { bar: function() {} } );
+
+ // does not complete with call to `extend'
+ this.assertThrows( function()
+ {
+ _self.Class.use( Ta )();
+ }, TypeError );
+
+ // nested uses; does not complete
+ this.assertThrows( function()
+ {
+ _self.Class.use( Ta ).use( Tb )();
+ }, TypeError );
+
+ // similar to above, with abstract; note that we're checking for
+ // TypeError here
+ this.assertThrows( function()
+ {
+ _self.AbstractClass.use( Ta )();
+ }, TypeError );
+
+ // does complete; OK
+ this.assertDoesNotThrow( function()
+ {
+ _self.Class.use( Ta ).extend( {} )();
+ _self.Class.use( Ta ).use( Tb ).extend( {} )();
+ } );
+ },
+
+
+ /**
+ * Ensure that the staging object created by the `implement' call
+ * exposes a `use' method (and properly applies it).
+ */
+ 'Can mix traits into class after implementing interface': function()
+ {
+ var _self = this,
+ called = false,
+
+ T = this.Sut( { foo: function() { called = true; } } ),
+ I = this.Interface( { bar: [] } ),
+ A = null;
+
+ // by declaring this abstract, we ensure that the interface was
+ // actually implemented (otherwise, all methods would be concrete,
+ // resulting in an error)
+ this.assertDoesNotThrow( function()
+ {
+ A = _self.AbstractClass.implement( I ).use( T ).extend( {} );
+ _self.assertOk( A.isAbstract() );
+ } );
+
+ // ensure that we actually fail if there's no interface implemented
+ // (and thus no abstract members); if we fail and the previous test
+ // succeeds, that implies that somehow the mixin is causing the
+ // class to become abstract, and that is an issue (and the reason
+ // for this seemingly redundant test)
+ this.assertThrows( function()
+ {
+ _self.Class.implement( I ).use( T ).extend( {} );
+ } );
+
+ A.extend( { bar: function() {} } )().foo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * When a trait is mixed into a class, it acts as though it is part of
+ * that class. Therefore, it should stand to reason that, when a mixed
+ * in method returns `this', it should actually return the instance of
+ * the class that it is mixed into (in the case of this test, its
+ * private member object, since that's our context when invoking the
+ * trait method).
+ */
+ 'Trait method that returns self will return containing class':
+ function()
+ {
+ var _self = this,
+ T = this.Sut( { foo: function() { return this; } } );
+
+ this.Class.use( T ).extend(
+ {
+ go: function()
+ {
+ _self.assertStrictEqual( this, this.foo() );
+ },
+ } )().go();
+ },
+
+
+ /**
+ * Support for static members will be added in future versions; this is
+ * not something that the author wanted to rush for the first trait
+ * release, as static members have their own odd quirks.
+ */
+ 'Trait static members are prohibited': function()
+ {
+ var Sut = this.Sut;
+
+ // property
+ this.assertThrows( function()
+ {
+ Sut( { 'static private foo': 'prop' } );
+ } );
+
+ // method
+ this.assertThrows( function()
+ {
+ Sut( { 'static foo': function() {} } );
+ } );
+ },
+
+
+ /**
+ * For the same reasons as static members (described immediately above),
+ * getters/setters are unsupported until future versions.
+ *
+ * Note that we use defineProperty instead of the short-hand object
+ * literal notation to avoid syntax errors in pre-ES5 environments.
+ */
+ 'Trait getters and setters are prohibited': function()
+ {
+ // perform these tests only when getters/setters are supported by
+ // our environment
+ if ( !( this.hasGetSet ) )
+ {
+ return;
+ }
+
+ var Sut = this.Sut;
+
+ this.assertThrows( function()
+ {
+ var dfn = {};
+ Object.defineProperty( dfn, 'foo',
+ {
+ get: function() {},
+ set: function() {},
+
+ enumerable: true,
+ } );
+
+ Sut( dfn );
+ } );
+ },
+} );
diff --git a/test/Trait/ImmediateTest.js b/test/Trait/ImmediateTest.js
new file mode 100644
index 0000000..5a730f9
--- /dev/null
+++ b/test/Trait/ImmediateTest.js
@@ -0,0 +1,111 @@
+/**
+ * Tests immediate definition/instantiation
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ },
+
+
+ /**
+ * In our most simple case, mixing a trait into an empty base class and
+ * immediately invoking the resulting partial class (without explicitly
+ * extending) should have the effect of instantiating a concrete version
+ * of the trait (so long as that is permitted). While this test exists
+ * to ensure consistency throughout the system, it may be helpful in
+ * situations where a trait is useful on its own.
+ *
+ * Note that we cannot simply use Class.use( T ), because this sets up a
+ * concrete class definition, not an immediate mixin.
+ */
+ 'Invoking partial class after mixin instantiates': function()
+ {
+ var called = false;
+
+ var T = this.Sut(
+ {
+ 'public foo': function()
+ {
+ called = true;
+ },
+ } );
+
+ // mixes T into an empty base class and instantiates
+ this.Class.extend( {} ).use( T )().foo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * This is the most useful and conventional form of mixin---runtime,
+ * atop of an existing class. In this case, we provide a short-hand form
+ * of instantiation to avoid the ugly pattern of `.extend( {} )()'.
+ */
+ 'Can invoke partial mixin atop of non-empty base': function()
+ {
+ var called_foo = false,
+ called_bar = false;
+
+ var C = this.Class(
+ {
+ 'public foo': function() { called_foo = true; },
+ } );
+
+ var T = this.Sut(
+ {
+ 'public bar': function() { called_bar = true; },
+ } );
+
+ // we must ensure not only that we have mixed in the trait, but that
+ // we have also maintained C's interface
+ var inst = C.use( T )();
+ inst.foo();
+ inst.bar();
+
+ this.assertOk( called_foo );
+ this.assertOk( called_bar );
+ },
+
+
+ /**
+ * Ensure that the partial invocation shorthand is equivalent to the
+ * aforementioned `.extend( {} ).apply( null, arguments )'.
+ */
+ 'Partial arguments are passed to class constructor': function()
+ {
+ var given = null,
+ expected = { foo: 'bar' };
+
+ var C = this.Class(
+ {
+ __construct: function() { given = arguments; },
+ } );
+
+ var T = this.Sut( {} );
+
+ C.use( T )( expected );
+ this.assertStrictEqual( given[ 0 ], expected );
+ },
+} );
+
diff --git a/test/Trait/LinearizationTest.js b/test/Trait/LinearizationTest.js
new file mode 100644
index 0000000..579751b
--- /dev/null
+++ b/test/Trait/LinearizationTest.js
@@ -0,0 +1,203 @@
+/**
+ * Tests trait/class linearization
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * GNU ease.js adopts Scala's concept of `linearization' with respect to
+ * resolving calls to supertypes; the tests that follow provide a detailed
+ * description of the concept, but readers may find it helpful to read
+ * through the ease.js manual or Scala documentation.
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ this.Interface = this.require( 'interface' );
+ },
+
+
+ /**
+ * When a class mixes in a trait that defines some method M, and then
+ * overrides it as M', then this.__super within M' should refer to M.
+ * Note that this does not cause any conflicts with any class supertypes
+ * that may define a method of the same name as M, because M must have
+ * been an override, otherwise an error would have occurred.
+ */
+ 'Class super call refers to mixin that is part of a class definition':
+ function()
+ {
+ var _self = this,
+ scalled = false;
+
+ var T = this.Sut(
+ {
+ // after mixin, this should be the super method
+ 'virtual public foo': function()
+ {
+ scalled = true;
+ },
+ } );
+
+ this.Class.use( T ).extend(
+ {
+ // overrides mixed-in foo
+ 'override public foo': function()
+ {
+ // should invoke T.foo
+ try
+ {
+ this.__super();
+ }
+ catch ( e )
+ {
+ _self.fail( false, true,
+ "Super invocation failure: " + e.message
+ );
+ }
+ },
+ } )().foo();
+
+ this.assertOk( scalled );
+ },
+
+
+ /**
+ * If a trait overrides a method of a class that it is mixed into, then
+ * super calls within the trait method should resolve to the class
+ * method.
+ */
+ 'Mixin overriding class method has class method as super method':
+ function()
+ {
+ var _self = this;
+
+ var expected = {},
+ I = this.Interface( { foo: [] } );
+
+ var T = this.Sut.implement( I ).extend(
+ {
+ // see ClassVirtualTest case for details on this
+ 'abstract override foo': function()
+ {
+ // should reference C.foo
+ return this.__super( expected );
+ },
+ } );
+
+ var priv_expected = Math.random();
+
+ var C = this.Class.implement( I ).extend(
+ {
+ // asserting on this value will ensure that the below method is
+ // invoked in the proper context
+ 'private _priv': priv_expected,
+
+ 'virtual foo': function( given )
+ {
+ _self.assertEqual( priv_expected, this._priv );
+ return given;
+ },
+ } );
+
+ this.assertStrictEqual( C.use( T )().foo(), expected );
+ },
+
+
+ /**
+ * Similar in spirit to the previous test: a supertype with a mixin
+ * should be treated just as any other class.
+ *
+ * Another way of phrasing this test is: "traits are stackable".
+ * Importantly, this also means that `virtual' must play nicely with
+ * `abstract override'.
+ */
+ 'Mixin overriding another mixin method M has super method M': function()
+ {
+ var called = {};
+
+ var I = this.Interface( { foo: [] } );
+
+ var Ta = this.Sut.implement( I ).extend(
+ {
+ 'virtual abstract override foo': function()
+ {
+ called.a = true;
+ this.__super();
+ },
+ } );
+
+ var Tb = this.Sut.implement( I ).extend(
+ {
+ 'abstract override foo': function()
+ {
+ called.b = true;
+ this.__super();
+ },
+ } );
+
+ this.Class.implement( I ).extend(
+ {
+ 'virtual foo': function() { called.base = true; },
+ } ).use( Ta ).use( Tb )().foo();
+
+ this.assertOk( called.a );
+ this.assertOk( called.b );
+ this.assertOk( called.base );
+ },
+
+
+ /**
+ * Essentially the same as the above test, but ensures that a mixin can
+ * be stacked multiple times atop of itself with no ill effects. We
+ * assume that all else is working (per the previous test).
+ *
+ * The number of times we stack the mixin is not really relevant, so
+ * long as it is >= 2; we did 3 here just for the hell of it to
+ * demonstrate that there is ideally no limit.
+ */
+ 'Mixin can be mixed in atop of itself': function()
+ {
+ var called = 0,
+ calledbase = false;
+
+ var I = this.Interface( { foo: [] } );
+
+ var T = this.Sut.implement( I ).extend(
+ {
+ 'virtual abstract override foo': function()
+ {
+ called++;
+ this.__super();
+ },
+ } );
+
+ this.Class.implement( I ).extend(
+ {
+ 'virtual foo': function() { calledbase = true; },
+ } ).use( T ).use( T ).use( T )().foo();
+
+
+ // mixed in thrice, so it should have stacked thrice
+ this.assertEqual( called, 3 );
+ this.assertOk( calledbase );
+ },
+} );
+
diff --git a/test/Trait/MixedExtendTest.js b/test/Trait/MixedExtendTest.js
new file mode 100644
index 0000000..879ceff
--- /dev/null
+++ b/test/Trait/MixedExtendTest.js
@@ -0,0 +1,212 @@
+/**
+ * Tests extending a class that mixes in traits
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ },
+
+
+ /**
+ * The supertype should continue to work as it would without the
+ * subtype, which means that the supertype's traits should still be
+ * available. Note that ease.js does not (at least at the time of
+ * writing this test) check to see if a trait is no longer accessible
+ * due to overrides, and so a supertype's traits will always be
+ * instantiated.
+ */
+ 'Subtype instantiates traits of supertype': function()
+ {
+ var called = false;
+
+ var T = this.Sut(
+ {
+ foo: function() { called = true; },
+ } );
+
+ // C is a subtype of a class that mixes in T
+ var C = this.Class.use( T ).extend( {} )
+ .extend(
+ {
+ // ensure that there is no ctor-dependent trait stuff
+ __construct: function() {},
+ } );
+
+ C().foo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * Just as subtypes inherit the same polymorphisms with respect to
+ * interfaces, so too should subtypes inherit supertypes' mixed in
+ * traits' types.
+ */
+ 'Subtype has same polymorphic qualities of parent mixins': function()
+ {
+ var T = this.Sut( {} ),
+ o = this.Class.use( T ).extend( {} ).extend( {} )();
+
+ // o's supertype mixes in T
+ this.assertOk( this.Class.isA( T, o ) );
+ },
+
+
+ /**
+ * Subtyping should impose no limits on mixins (except for the obvious
+ * API compatibility restrictions inherent in OOP).
+ */
+ 'Subtype can mix in additional traits': function()
+ {
+ var a = false,
+ b = false;
+
+ var Ta = this.Sut(
+ {
+ 'public ta': function() { a = true; },
+ } ),
+ Tb = this.Sut(
+ {
+ 'public tb': function() { b = true; },
+ } ),
+ C = null;
+
+ var _self = this;
+ this.assertDoesNotThrow( function()
+ {
+ var sup = _self.Class.use( Ta ).extend( {} );
+
+ // mixes in Tb; supertype already mixed in Ta
+ C = _self.Class.use( Tb ).extend( sup, {} );
+ } );
+
+ this.assertDoesNotThrow( function()
+ {
+ // ensures that instantiation does not throw an error and that
+ // the methods both exist
+ var o = C();
+ o.ta();
+ o.tb();
+ } );
+
+ // ensure both were properly called
+ this.assertOk( a );
+ this.assertOk( b );
+ },
+
+
+ /**
+ * As a sanity check, ensure that subtyping does not override parent
+ * type data with respect to traits.
+ *
+ * Note that this test makes the preceding test redundant, but the
+ * separation is useful for debugging any potential regressions.
+ */
+ 'Subtype trait types do not overwrite supertype types': function()
+ {
+ var Ta = this.Sut( {} ),
+ Tb = this.Sut( {} ),
+ C = this.Class.use( Ta ).extend( {} ),
+ o = this.Class.use( Tb ).extend( C, {} )();
+
+ // o's supertype mixes in Ta
+ this.assertOk( this.Class.isA( Ta, o ) );
+
+ // o mixes in Tb
+ this.assertOk( this.Class.isA( Tb, o ) );
+ },
+
+
+ /**
+ * This alternative syntax mixes a trait directly into a base class and
+ * then omits the base class as an argument to the extend method; this
+ * syntax is most familiar with named classes, but we are not testing
+ * named classes here.
+ */
+ 'Can mix in traits directly atop of existing class': function()
+ {
+ var called_foo = false,
+ called_bar = false,
+ called_baz = false;
+
+ var C = this.Class(
+ {
+ 'public foo': function() { called_foo = true; },
+ } );
+
+ var T = this.Sut(
+ {
+ 'public bar': function() { called_bar = true; },
+ } );
+
+ // we must ensure not only that we have mixed in the trait, but that
+ // we have also maintained C's interface and can further extend it
+ var inst = C.use( T ).extend(
+ {
+ 'public baz': function() { called_baz = true; },
+ } )();
+
+ inst.foo();
+ inst.bar();
+ inst.baz();
+
+ this.assertOk( called_foo );
+ this.assertOk( called_bar );
+ this.assertOk( called_baz );
+ },
+
+
+ /**
+ * This test ensures that we can mix in traits using the syntax
+ * C.use(T1).use(T2), and so on; this may be necessary to disambiguate
+ * overrides if T1 and T2 provide definitions for the same method (and
+ * so the syntax C.use(T1, T2) cannot be used). This syntax is also
+ * important for the concept of stackable traits (see
+ * LinearizationTest).
+ *
+ * Note that this differs from C.use(T1).use(T2).extend({}); we're
+ * talking about C.extend({}).use(T1).use(T2). Therefore, this can be
+ * considered to be syntatic sugar for
+ * C.use( T1 ).extend( {} ).use( T2 ).
+ */
+ 'Can chain use calls': function()
+ {
+ var T1 = this.Sut( { foo: function() {} } ),
+ T2 = this.Sut( { bar: function() {} } ),
+ C = null;
+
+ var Class = this.Class;
+ this.assertDoesNotThrow( function()
+ {
+ C = Class.extend( {} ).use( T1 ).use( T2 );
+ } );
+
+ // ensure that the methods were actually mixed in
+ this.assertDoesNotThrow( function()
+ {
+ C().foo();
+ C().bar();
+ } );
+ },
+} );
diff --git a/test/Trait/NamedTest.js b/test/Trait/NamedTest.js
new file mode 100644
index 0000000..d4a5a48
--- /dev/null
+++ b/test/Trait/NamedTest.js
@@ -0,0 +1,87 @@
+/**
+ * Tests named trait definitions
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ },
+
+
+ /**
+ * If a trait is not given a name, then converting it to a string should
+ * indicate that it is anonymous. Further, to disambiguate from
+ * anonymous classes, we should further indicate that it is a trait.
+ *
+ * This test is fragile in the sense that it tests for an explicit
+ * string: this is intended, since some developers may rely on this
+ * string (even though they really should use Trait.isTrait), and so it
+ * should be explicitly documented.
+ */
+ 'Anonymous trait is properly indicated when converted to string':
+ function()
+ {
+ var given = this.Sut( {} ).toString();
+ this.assertEqual( given, '(Trait)' );
+ },
+
+
+ /**
+ * Analagous to named classes: we should provide the name when
+ * converting to a string to aid in debugging.
+ */
+ 'Named trait contains name when converted to string': function()
+ {
+ var name = 'FooTrait',
+ T = this.Sut( name, {} );
+
+ this.assertOk( T.toString().match( name ) );
+ },
+
+
+ /**
+ * We assume that, if two or more arguments are provided, that the
+ * definition is named.
+ */
+ 'Named trait definition cannot contain zero or more than two arguments':
+ function()
+ {
+ var Sut = this.Sut;
+ this.assertThrows( function() { Sut(); } );
+ this.assertThrows( function() { Sut( 1, 2, 3 ); } );
+ },
+
+
+ /**
+ * Operating on the same assumption as the above test.
+ */
+ 'First argument in named trait definition must be a string':
+ function()
+ {
+ var Sut = this.Sut;
+ this.assertThrows( function()
+ {
+ Sut( {}, {} );
+ } );
+ },
+} );
diff --git a/test/Trait/PropertyTest.js b/test/Trait/PropertyTest.js
new file mode 100644
index 0000000..f288841
--- /dev/null
+++ b/test/Trait/PropertyTest.js
@@ -0,0 +1,73 @@
+/**
+ * Tests trait properties
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Or, rather, lack thereof, at least for the time being---this is something
+ * that is complicated by pre-ES5 fallback and, while a solution is
+ * possible, it is not performant in the case of a fallback and would muddy
+ * up ease.js' code.
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ },
+
+
+ /**
+ * Since private properties cannot be accessed by anything besides the
+ * trait itself, they cannot interfere with anything else and should be
+ * permitted. Indeed, it would be obsurd to think otherwise, since the
+ * trait should be able to maintain its own local state.
+ */
+ 'Private trait properties are permitted': function()
+ {
+ var Sut = this.Sut;
+ this.assertDoesNotThrow( function()
+ {
+ Sut( { 'private _foo': 'bar' } );
+ } );
+ },
+
+
+ /**
+ * See the description at the top of this file. This is something that
+ * may be addressed in future releases.
+ *
+ * Rather than simply ignoring them, we should notify the user that
+ * their code is not going to work as intended and prevent bugs
+ * associated with it.
+ */
+ 'Public and protected trait properties are prohibited': function()
+ {
+ var Sut = this.Sut;
+
+ this.assertThrows( function()
+ {
+ Sut( { 'public foo': 'bar' } );
+ } );
+
+ this.assertThrows( function()
+ {
+ Sut( { 'protected foo': 'bar' } );
+ } );
+ },
+} );
diff --git a/test/Trait/ScopeTest.js b/test/Trait/ScopeTest.js
new file mode 100644
index 0000000..e5ba614
--- /dev/null
+++ b/test/Trait/ScopeTest.js
@@ -0,0 +1,152 @@
+/**
+ * Tests trait scoping
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ },
+
+
+ /**
+ * Since the private scope of classes and the traits that they use are
+ * disjoint, traits should never be able to access any private member of
+ * a class that uses it.
+ *
+ * The beauty of this is that we get this ``feature'' for free with
+ * our composition-based trait implementation.
+ */
+ 'Private class members are not accessible to used traits': function()
+ {
+ var T = this.Sut(
+ {
+ // attempts to access C._priv
+ 'public getPriv': function() { return this._priv; },
+
+ // attempts to invoke C._privMethod
+ 'public invokePriv': function() { this._privMethod(); },
+ } );
+
+ var inst = this.Class.use( T ).extend(
+ {
+ 'private _priv': 'foo',
+ 'private _privMethod': function() {},
+ } )();
+
+ this.assertEqual( inst.getPriv(), undefined );
+ this.assertThrows( function()
+ {
+ inst.invokePriv();
+ }, Error );
+ },
+
+
+ /**
+ * Similar concept to the above---class and trait scopes are disjoint.
+ * This is particularily important, since traits will have no idea what
+ * other traits they will be mixed in with and therefore must be immune
+ * from nasty state clashes.
+ */
+ 'Private trait members are not accessible to containing class':
+ function()
+ {
+ var T = this.Sut(
+ {
+ 'private _priv': 'bar',
+ 'private _privMethod': function() {},
+ } );
+
+ // reverse of the previous test case
+ var inst = this.Class.use( T ).extend(
+ {
+ // attempts to access T._priv
+ 'public getPriv': function() { return this._priv; },
+
+ // attempts to invoke T._privMethod
+ 'public invokePriv': function() { this._privMethod(); },
+ } )();
+
+
+ this.assertEqual( inst.getPriv(), undefined );
+ this.assertThrows( function()
+ {
+ inst.invokePriv();
+ }, Error );
+ },
+
+
+ /**
+ * Since all scopes are disjoint, it would stand to reason that all
+ * traits should also have their own private scope independent of other
+ * traits that are mixed into the same class. This is also very
+ * important for the same reasons as the previous test---we cannot have
+ * state clashes between traits.
+ */
+ 'Traits do not have access to each others\' private members': function()
+ {
+ var T1 = this.Sut(
+ {
+ 'private _priv1': 'foo',
+ 'private _privMethod1': function() {},
+ } ),
+ T2 = this.Sut(
+ {
+ // attempts to access T1._priv1
+ 'public getPriv': function() { return this._priv1; },
+
+ // attempts to invoke T1._privMethod1
+ 'public invokePriv': function() { this._privMethod1(); },
+ } );
+
+ var inst = this.Class.use( T1, T2 ).extend( {} )();
+
+ this.assertEqual( inst.getPriv(), undefined );
+ this.assertThrows( function()
+ {
+ inst.invokePriv();
+ }, Error );
+ },
+
+
+ /**
+ * If this seems odd at first, consider this: traits provide
+ * copy/paste-style functionality, meaning they need to be able to
+ * provide public methods. However, we may not always want to mix trait
+ * features into a public API; therefore, we need the ability to mix in
+ * protected members.
+ */
+ 'Classes can access protected trait members': function()
+ {
+ var T = this.Sut( { 'protected foo': function() {} } );
+
+ var _self = this;
+ this.assertDoesNotThrow( function()
+ {
+ _self.Class.use( T ).extend(
+ {
+ // invokes protected trait method
+ 'public callFoo': function() { this.foo(); }
+ } )().callFoo();
+ } );
+ },
+} );
diff --git a/test/Trait/VirtualTest.js b/test/Trait/VirtualTest.js
new file mode 100644
index 0000000..b18feef
--- /dev/null
+++ b/test/Trait/VirtualTest.js
@@ -0,0 +1,304 @@
+/**
+ * Tests virtual trait methods
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Note that tests for super calls are contained within LinearizationTest;
+ * these test cases simply ensure that overrides are actually taking place.
+ */
+
+require( 'common' ).testCase(
+{
+ caseSetUp: function()
+ {
+ this.Sut = this.require( 'Trait' );
+ this.Class = this.require( 'class' );
+ },
+
+
+ /**
+ * If a trait specifies a virtual method, then the class should expose
+ * the method as virtual.
+ */
+ 'Class inherits virtual trait method': function()
+ {
+ var called = false;
+
+ var T = this.Sut(
+ {
+ 'virtual foo': function()
+ {
+ called = true;
+ }
+ } );
+
+ var C = this.Class.use( T ).extend( {} );
+
+ // ensure that we are actually using the method
+ C().foo();
+ this.assertOk( called, "Virtual method not called" );
+
+ // if virtual, we should be able to override it
+ var called2 = false,
+ C2;
+
+ this.assertDoesNotThrow( function()
+ {
+ C2 = C.extend(
+ {
+ 'override foo': function()
+ {
+ called2 = true;
+ }
+ } );
+ } );
+
+ C2().foo();
+ this.assertOk( called2, "Method not overridden" );
+ },
+
+
+ /**
+ * Virtual trait methods should be treated in a manner similar to
+ * abstract trait methods---a class should be able to provide its own
+ * concrete implementation. Note that this differs from the above test
+ * because we are overriding the method internally at definition time,
+ * not subclassing.
+ */
+ 'Class can override virtual trait method': function()
+ {
+ var _self = this;
+ var T = this.Sut(
+ {
+ 'virtual foo': function()
+ {
+ // we should never execute this (unless we're broken)
+ _self.fail( true, false,
+ "Method was not overridden."
+ );
+ }
+ } );
+
+ var expected = 'foobar';
+ var C = this.Class.use( T ).extend(
+ {
+ 'override foo': function() { return expected; }
+ } );
+
+ this.assertEqual( C().foo(), expected );
+ },
+
+
+ /**
+ * If C uses T and overrides T.Ma, and there is some method T.Mb that
+ * invokes T.Ma, then T.Mb should instead invoke C.Ma.
+ */
+ 'Class-overridden virtual trait method is accessible by trait':
+ function()
+ {
+ var _self = this;
+
+ var T = this.Sut(
+ {
+ 'public doFoo': function()
+ {
+ // should call overridden, not the one below
+ this.foo();
+ },
+
+ // to be overridden
+ 'virtual protected foo': function()
+ {
+ _self.fail( true, false, "Method not overridden." );
+ },
+ } );
+
+ var called = false;
+
+ var C = this.Class.use( T ).extend(
+ {
+ // should be called by T.doFoo
+ 'override protected foo': function() { called = true },
+ } );
+
+ C().doFoo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * If a supertype mixes in a trait that provides a virtual method, a
+ * subtype should be able to provide its own concrete implementation.
+ * This is especially important to test in the case where a trait
+ * invokes its own virtual method---we must ensure that the message is
+ * properly passed to the subtype's override.
+ *
+ * For a more formal description of a similar matter, see the
+ * AbstractTest case; indeed, we're trying to mimic the same behavior
+ * that we'd expect with abstract methods.
+ */
+ 'Subtype can override virtual method of trait mixed into supertype':
+ function()
+ {
+ var _self = this;
+
+ var T = this.Sut(
+ {
+ 'public doFoo': function()
+ {
+ // this call should be passed to any overrides
+ return this.foo();
+ },
+
+ // this is the one we'll try to override
+ 'virtual protected foo': function()
+ {
+ _self.fail( true, false, "Method not overridden." );
+ },
+ } );
+
+ var called = false;
+
+ // C is a subtype of a class that implements T
+ var C = this.Class.use( T ).extend( {} )
+ .extend(
+ {
+ // this should be called instead of T.foo
+ 'override protected foo': function()
+ {
+ called = true;
+ },
+ } );
+
+ C().doFoo();
+ this.assertOk( called );
+ },
+
+
+ /**
+ * This is the same concept as the non-virtual test found in the
+ * DefinitionTest case: since a trait is mixed into a class, if it
+ * returns itself, then it should in actuality return the instance of
+ * the class it is mixed into.
+ */
+ 'Virtual trait method returning self returns class instance':
+ function()
+ {
+ var _self = this;
+
+ var T = this.Sut( { 'virtual foo': function() { return this; } } );
+
+ this.Class.use( T ).extend(
+ {
+ go: function()
+ {
+ _self.assertStrictEqual( this, this.foo() );
+ },
+ } )().go();
+ },
+
+
+ /**
+ * Same concept as the above test case, but ensures that invoking the
+ * super method does not screw anything up.
+ */
+ 'Overridden virtual trait method returning self returns class instance':
+ function()
+ {
+ var _self = this;
+
+ var T = this.Sut( { 'virtual foo': function() { return this; } } );
+
+ this.Class.use( T ).extend(
+ {
+ 'override foo': function()
+ {
+ return this.__super();
+ },
+
+ go: function()
+ {
+ _self.assertStrictEqual( this, this.foo() );
+ },
+ } )().go();
+ },
+
+
+ /**
+ * When a trait method is overridden, ensure that the data are properly
+ * proxied back to the caller. This differs from the above tests, which
+ * just make sure that the method is actually overridden and invoked.
+ */
+ 'Data are properly returned from trait override super call': function()
+ {
+ var _self = this,
+ expected = {};
+
+ var T = this.Sut(
+ {
+ 'virtual foo': function() { return expected; }
+ } );
+
+ this.Class.use( T ).extend(
+ {
+ 'override foo': function()
+ {
+ _self.assertStrictEqual( expected, this.__super() );
+ },
+ } )().foo();
+ },
+
+
+ /**
+ * When a trait method is overridden by the class that it is mixed into,
+ * and the super method is called, then the trait method should execute
+ * within the private member context of the trait itself (as if it were
+ * never overridden). Some kinky stuff would have to be going on (at
+ * least in the implementation at the time this was written) for this
+ * test to fail, but let's be on the safe side.
+ */
+ 'Super trait method overrided in class executed within private context':
+ function()
+ {
+ var expected = {};
+
+ var T = this.Sut(
+ {
+ 'virtual foo': function()
+ {
+ // should succeed
+ return this.priv();
+ },
+
+ 'private priv': function()
+ {
+ return expected;
+ },
+ } );
+
+ this.assertStrictEqual( expected,
+ this.Class.use( T ).extend(
+ {
+ 'override virtual foo': function()
+ {
+ return this.__super();
+ },
+ } )().foo()
+ );
+ },
+} );
diff --git a/test/Util/PropParseKeywordsTest.js b/test/Util/PropParseKeywordsTest.js
index 5f8e45a..d6378ec 100644
--- a/test/Util/PropParseKeywordsTest.js
+++ b/test/Util/PropParseKeywordsTest.js
@@ -55,6 +55,33 @@ require( 'common' ).testCase(
},
+ /**
+ * As an exception to the above rule, a method shall not considered to be
+ * abstract if the `override' keyword is too provided (an abstract
+ * override---see the trait tests for more information).
+ */
+ 'Not considered abstract when `override\' also provided': function()
+ {
+ var _self = this;
+
+ var data = { 'abstract override foo': function() {} },
+ found = null;
+
+ this.Sut.propParse( data, {
+ method: function ( name, func, is_abstract )
+ {
+ _self.assertOk( is_abstract === false );
+ _self.assertEqual( typeof func, 'function' );
+ _self.assertOk( _self.Sut.isAbstractMethod( func ) === false );
+
+ found = name;
+ },
+ } );
+
+ this.assertEqual( found, 'foo' );
+ },
+
+
/**
* The idea behind supporting this functionality---which is unsued at
* the time of writing this test---is to allow eventual customization of
diff --git a/test/Util/PropParseTest.js b/test/Util/PropParseTest.js
index 7257946..6c2910c 100644
--- a/test/Util/PropParseTest.js
+++ b/test/Util/PropParseTest.js
@@ -210,4 +210,46 @@ require( 'common' ).testCase(
propParse( { 'abstract foo': [ 'valid_name' ] }, {} );
}, SyntaxError );
},
+
+
+ /**
+ * The motivation behind this feature is to reduce the number of closures
+ * necessary to perform a particular task: this allows binding `this' of the
+ * handler to a custom context.
+ */
+ 'Supports dynamic context to handlers': function()
+ {
+ var _self = this;
+ context = {};
+
+ // should trigger all of the handlers
+ var all = {
+ prop: 'prop',
+ method: function() {},
+ };
+
+ // run test on getters/setters only if supported by the environment
+ if ( this.hasGetSet )
+ {
+ Object.defineProperty( all, 'getset', {
+ get: ( get = function () {} ),
+ set: ( set = function () {} ),
+
+ enumerable: true,
+ } );
+ }
+
+ function _chk()
+ {
+ _self.assertStrictEqual( this, context );
+ }
+
+ // check each supported handler for conformance
+ this.Sut.propParse( all, {
+ each: _chk,
+ property: _chk,
+ getset: _chk,
+ method: _chk,
+ }, context );
+ },
} );
diff --git a/test/VisibilityObjectFactoryTest.js b/test/VisibilityObjectFactoryTest.js
index 9f7e437..745ab44 100644
--- a/test/VisibilityObjectFactoryTest.js
+++ b/test/VisibilityObjectFactoryTest.js
@@ -267,6 +267,36 @@ require( 'common' ).testCase(
},
+ /**
+ * This test addresses a particularily nasty bug that wasted hours of
+ * development time: When a visibility modifier keyword is omitted, then
+ * it should be implicitly public. In this case, however, the keyword is
+ * not automatically added to the keyword list (maybe one day it will
+ * be, but for now we'll maintain the distinction); therefore, we should
+ * not be checking for the `public' keyword when determining if we
+ * should write to the protected member object.
+ */
+ 'Public methods are not overwritten when keyword is omitted': function()
+ {
+ var f = function() {};
+ f.___$$keywords$$ = {};
+
+ // no keywords; should be implicitly public
+ var dest = { fpub: f };
+
+ // add duplicate method to protected
+ this.methods[ 'protected' ].fpub = function() {};
+
+ this.sut.setup( dest, this.props, this.methods );
+
+ // ensure our public method is still referenced
+ this.assertStrictEqual( dest.fpub, f,
+ "Public methods should not be overwritten by protected methods"
+ );
+ },
+
+
+
/**
* Same situation with private members as protected, with the exception that
* we do not need to worry about the overlay problem (in regards to
diff --git a/test/perf/perf-trait-define.js b/test/perf/perf-trait-define.js
new file mode 100644
index 0000000..f4d6af7
--- /dev/null
+++ b/test/perf/perf-trait-define.js
@@ -0,0 +1,40 @@
+/**
+ * Tests amount of time taken to declare N anonymous traits
+ *
+ * Copyright (C) 2010, 2011, 2013 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Contrast with respective class test.
+ */
+
+var common = require( __dirname + '/common.js' ),
+ Trait = common.require( 'Trait' ),
+
+ count = 1000
+;
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Trait( {} );
+ }
+
+}, count, 'Declare ' + count + ' empty anonymous traits' );
diff --git a/test/perf/perf-trait-invoke-method.js b/test/perf/perf-trait-invoke-method.js
new file mode 100644
index 0000000..948a3a0
--- /dev/null
+++ b/test/perf/perf-trait-invoke-method.js
@@ -0,0 +1,170 @@
+/**
+ * Tests amount of time taken to apply trait (mixin) methods
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Contrast with respective class tests.
+ *
+ * Note that there is a lot of code duplication; this is to reduce
+ * unnecessary lookups for function invocation to gain a more accurate
+ * estimate of invocation time (e.g., no foo[ bar ]()).
+ *
+ * Traits are expected to be considerably slower than conventional classes
+ * due to their very nature---dynamically bound methods. This should not be
+ * alarming under most circumstances, as the method invocation is still
+ * likely much faster than any serious logic contained within the method;
+ * however, performance issues could manifest when recursing heavily, so be
+ * cognisant of such.
+ *
+ * There is, at least at the time of writing this message, much room for
+ * optimization in the trait implementation.
+ */
+
+var common = require( __dirname + '/common.js' ),
+ Trait = common.require( 'Trait' ),
+ Class = common.require( 'class' ),
+ Interface = common.require( 'interface' ),
+
+ // misc. var used to ensure that v8 does not optimize away empty
+ // functions
+ x = 0,
+
+ // method invocation is pretty speedy, so we need a lot of iterations
+ count = 500000
+;
+
+// objects should be pre-created; we don't care about the time taken to
+// instantiate them
+var T = Trait(
+{
+ 'public pub': function() { x++ },
+ 'protected prot': function() { x++ },
+
+ 'virtual public vpub': function() { x++ },
+ 'virtual public vprot': function() { x++ },
+
+ 'virtual public vopub': function() { x++ },
+ 'virtual public voprot': function() { x++ },
+} );
+
+var I = Interface(
+{
+ 'public aopub': [],
+ 'public vaopub': [],
+} );
+
+var Ta = Trait.implement( I ).extend(
+{
+ // TODO: protected once we support extending classes
+ 'abstract public override aopub': function() { x++ },
+ 'virtual abstract public override vaopub': function() { x++ },
+} );
+
+var o = Class.use( T ).extend(
+{
+ // overrides T mixin
+ 'override public vopub': function() { x++ },
+ 'override public voprot': function() { x++ },
+
+ // overridden by Ta mixin
+ 'virtual public aopub': function() { x++ },
+ 'virtual public vaopub': function() { x++ },
+
+
+ 'public internalTest': function()
+ {
+ var _self = this;
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) _self.pub();
+ }, count, "Invoke public mixin method internally" );
+
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) _self.prot();
+ }, count, "Invoke protected mixin method internally" );
+
+ vtest( this, "internally" );
+ },
+} ).use( Ta )();
+
+
+common.test( function()
+{
+ var i = count;
+ while ( i-- )
+ {
+ o.pub();
+ }
+}, count, "Invoke public mixin method externally" );
+
+
+// run applicable external virtual tests
+vtest( o, "externally" );
+
+
+function vtest( context, s )
+{
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) context.vpub();
+ }, count, "Invoke public virtual mixin method " + s );
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) context.vopub();
+ }, count, "Invoke public overridden virtual mixin method " + s );
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) context.aopub();
+ }, count, "Invoke public abstract override mixin method " + s );
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) context.vaopub();
+ }, count, "Invoke public virtual abstract override mixin method " + s );
+
+
+ if ( !( context.vprot ) ) return;
+
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) context.vprot();
+ }, count, "Invoke protected virtual mixin method " + s );
+
+ common.test( function()
+ {
+ var i = count;
+ while ( i-- ) context.voprot();
+ }, count, "Invoke protected overridden virtual mixin method " + s );
+}
+
+
+// run tests internally
+o.internalTest();
diff --git a/test/perf/perf-trait-methods.js b/test/perf/perf-trait-methods.js
new file mode 100644
index 0000000..4a03efd
--- /dev/null
+++ b/test/perf/perf-trait-methods.js
@@ -0,0 +1,126 @@
+/**
+ * Tests amount of time taken defining and invoking methods passing through
+ * traits
+ *
+ * Copyright (C) 2010, 2011, 2013 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Contrast with respective class test.
+ */
+
+var common = require( __dirname + '/common.js' ),
+ Trait = common.require( 'Trait' ),
+ Class = common.require( 'class' ),
+ Interface = common.require( 'interface' ),
+
+ count = 1000
+;
+
+
+var I = Interface(
+{
+ a: [],
+ b: [],
+ c: [],
+} );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Trait(
+ {
+ a: function() {},
+ b: function() {},
+ c: function() {},
+ } );
+ }
+
+}, count,
+'Declare ' + count + ' empty anonymous traits with few concrete methods' );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Trait(
+ {
+ 'virtual a': function() {},
+ 'virtual b': function() {},
+ 'virtual c': function() {},
+ } );
+ }
+
+}, count,
+'Declare ' + count + ' empty anonymous traits with few virtual methods' );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Trait(
+ {
+ 'abstract a': [],
+ 'abstract b': [],
+ 'abstract c': [],
+ } );
+ }
+
+}, count,
+'Declare ' + count + ' empty anonymous traits with few abstract methods' );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Trait.implement( I ).extend( {} );
+ }
+
+}, count,
+'Declare ' + count + ' empty anonymous traits implementing interface ' +
+ 'with few methods' );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Trait.implement( I ).extend(
+ {
+ 'abstract override a': function() {},
+ 'abstract override b': function() {},
+ 'abstract override c': function() {},
+ } );
+ }
+
+}, count,
+'Declare ' + count + ' empty anonymous traits with few ' +
+ 'abstract overrides, implementing interface' );
diff --git a/test/perf/perf-trait-mixin.js b/test/perf/perf-trait-mixin.js
new file mode 100644
index 0000000..23d0200
--- /dev/null
+++ b/test/perf/perf-trait-mixin.js
@@ -0,0 +1,119 @@
+/**
+ * Tests amount of time taken to declare N classes mixing in traits of
+ * various sorts
+ *
+ * Copyright (C) 2010, 2011, 2013 Mike Gerwitz
+ *
+ * This file is part of GNU ease.js.
+ *
+ * ease.js is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * Contrast with respective class test.
+ */
+
+var common = require( __dirname + '/common.js' ),
+ Trait = common.require( 'Trait' ),
+ Class = common.require( 'class' ),
+ Interface = common.require( 'interface' ),
+
+ count = 1000
+;
+
+// we don't care about declare time; we're testing mixin time
+var Te = Trait( {} );
+
+var Tv = Trait(
+{
+ 'virtual a': function() {},
+ 'virtual b': function() {},
+ 'virtual c': function() {},
+} );
+
+var I = Interface(
+{
+ a: [],
+ b: [],
+ c: [],
+} );
+var Cv = Class.implement( I ).extend(
+{
+ 'virtual a': function() {},
+ 'virtual b': function() {},
+ 'virtual c': function() {},
+} );
+
+var To = Trait.implement( I ).extend(
+{
+ 'virtual abstract override a': function() {},
+ 'virtual abstract override b': function() {},
+ 'virtual abstract override c': function() {},
+} );
+
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ // extend to force lazy mixin
+ Class.use( Te ).extend( {} );
+ }
+
+}, count, 'Mix in ' + count + ' empty traits' );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ // extend to force lazy mixin
+ Class.use( Tv ).extend( {} );
+ }
+
+}, count, 'Mix in ' + count + ' traits with few virtual methods' );
+
+
+// now override 'em
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Class.use( Tv ).extend(
+ {
+ 'override virtual a': function() {},
+ 'override virtual b': function() {},
+ 'override virtual c': function() {},
+ } );
+ }
+
+}, count, 'Mix in and override ' + count +
+ ' traits with few virtual methods' );
+
+
+common.test( function()
+{
+ var i = count;
+
+ while ( i-- )
+ {
+ Cv.use( To ).extend( {} );
+ }
+
+}, count, 'Mix in trait that overrides class methods' );