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' );