1
0
Fork 0

Preliminary support for traits as mixins

This has turned out to be a very large addition to the project---indeed,
with this release, its comprehensiveness remains elusive, but this is a huge
step in the right direction.

Traits allow for powerful methods of code reuse by defining components that
can be ``mixed into'' classes, almost as if the code were copied and pasted
directly into the class definition. Mixins, as they are so called, carry
with them the type of the trait, just as implementing an interface carries
with it the type of the interface; this means that they integrate into
ease.js' type system such that, given some trait T that mixes into class C
and an instance of C, it will be true that Class.isA( T, inst ).

The trait implementation for GNU ease.js is motivated heavily by Scala's
implementation of mixins using traits. Notable features include:

  1. Traits may be mixed in either prior to or following a class definition;
     this allows coupling traits tightly with a class or allowing them to be
     used in a decorator-style manner prior to instantiation.

  2. By mixing in a trait prior to the class definition, the class may
     override methods of the trait:

       Class( 'Foo' ).use( T ).extend( { /*...*/ } )

     If a trait is mixed in after a class definition, then the trait may
     instead override the functionality of a class:

       Class( 'Foo', { /*...*/ } ).use( T )

  3. Traits are stackable: By using the `abstract override' keyword
     combination, a trait can override the concrete definition of its
     parent, provided that the abstract definition is implemented by the
     trait (e.g. by implementing a common interface). This allows overrides
     to be mixed in any order. For example, consider some class Buffer that
     defines an `add' method, accepting a string. Now consider two traits
     Dup and Upper:

       Buffer.use( Dup ).use( Upper )().add( "foo" )

     This would result in the string "FooFoo" being added to the buffer.
     On the other hand:

       Buffer.use( Reverse ).use( Dup )().add( "foo" )

     would add the string "Foofoo".

  4. A trait may maintain its own private state and API completely disjoint
     from the class that it is mixed into---a class has access only to
     public and protected members of a trait and vice versa. This further
     allows a class and trait to pass messages between one-another without
     having their communications exposed via a public API. A trait may even
     communicate with with other traits mixed into the same class (or its
     parents/children), given the proper overrides.

Traits provide a powerful system of code reuse that solves the multiple
inheritance problems of languages like C++, without introducing the burden
and code duplication concerns of Java's interfaces (note that GNU ease.js
does support interfaces, but not multiple inheritance). However, traits also
run the risk of encouraging overly rich APIs and complicated inheritance
trees that produce a maintenance nightmare: it is important to keep concerns
separated, creating classes (and traits) that do one thing and do it well.
Users should understand the implications of mixing in traits prior to the
class definition, and should understand how decorating an API using mixins
after a class definition tightly couples the trait with all objects derived
from the generated class (as opposed to the flexibility provided by the
composition-based decorator pattern). These issues will be detailed in the
manual once the trait implementation is complete.

The trait implementation is still under development; outstanding tasks are
detailed in `README.traits`. In the meantime, note that the implementation
*is* stable and can be used in the production environment. While
documentation is not yet available in the manual, comprehensive examples and
rationale may be found in the trait test cases.

Happy hacking!
perfodd
Mike Gerwitz 2014-03-15 22:19:38 -04:00
commit ac1a0368cf
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
43 changed files with 4731 additions and 280 deletions

View File

@ -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

98
README.traits 100644
View File

@ -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.

View File

@ -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

View File

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

View File

@ -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,22 +462,80 @@ 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: {},
util.propParse( props, {
each: function( name, value, keywords )
// 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 )
{
@ -464,8 +543,11 @@ exports.prototype.buildMembers = function buildMembers(
}
// if a member was defined multiple times in the same class
// declaration, throw an error
if ( hasOwn.call( defs, name ) )
// 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"
@ -474,40 +556,51 @@ exports.prototype.buildMembers = function buildMembers(
// keep track of the definitions (only during class declaration)
// to catch duplicates
defs[ name ] = 1;
},
defs[ name ] = keywords;
}
property: function( name, value, keywords )
function _parseProp( name, value, keywords )
{
var dest = ( keywordStatic( keywords ) ) ? sprops : prop_init;
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
_self._memberBuilder.buildProp(
dest, null, name, value, keywords, base
this._cb._memberBuilder.buildProp(
dest, null, name, value, keywords, this.base
);
},
}
getset: function( name, get, set, keywords )
function _parseGetSet( name, get, set, keywords )
{
var dest = ( keywordStatic( keywords ) ) ? smethods : members,
var dest = ( keywordStatic( keywords ) )
? this.static_members.methods
: this.members,
is_static = keywordStatic( keywords ),
instLookup = ( ( is_static )
? staticInstLookup
? this.staticInstLookup
: exports.getMethodInstance
);
_self._memberBuilder.buildGetterSetter(
dest, null, name, get, set, keywords, instLookup, class_id, base
this._cb._memberBuilder.buildGetterSetter(
dest, null, name, get, set, keywords, instLookup,
this.class_id, this.base
);
},
}
method: function( name, func, is_abstract, keywords )
function _parseMethod( name, func, is_abstract, keywords )
{
var is_static = keywordStatic( keywords ),
dest = ( is_static ) ? smethods : members,
dest = ( is_static )
? this.static_members.methods
: this.members,
instLookup = ( is_static )
? staticInstLookup
? this.staticInstLookup
: exports.getMethodInstance
;
@ -522,44 +615,64 @@ exports.prototype.buildMembers = function buildMembers(
}
}
_self._memberBuilder.buildMethod(
var used = this._cb._memberBuilder.buildMethod(
dest, null, name, func, keywords, instLookup,
class_id, base
this.class_id, this.base, this.state
);
// 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;
}
// 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 )
{
abstract_methods[ name ] = true;
abstract_methods.__length++;
this.abstract_methods[ name ] = true;
this.abstract_methods.__length++;
}
else if ( ( hasOwn.call( abstract_methods, name ) )
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 abstract_methods[ name ];
abstract_methods.__length--;
delete this.abstract_methods[ name ];
this.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,17 +680,20 @@ function validateAbstract( ctor, cname, abstract_methods )
);
}
}
else
else if ( abstract_methods.__length > 0 )
{
if ( abstract_methods.__length > 0 )
if ( auto )
{
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'
;
}

View File

@ -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,7 +344,31 @@ exports.buildGetterSetter = function(
*/
function getMemberVisibility( members, keywords, name )
{
var viserr = function()
// there's cleaner ways of doing this, but consider it loop unrolling for
// performance
if ( keywords[ 'private' ] )
{
( keywords[ 'public' ] || keywords[ 'protected' ] )
&& viserr( name );
return members[ 'private' ];
}
else if ( keywords[ 'protected' ] )
{
( 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( name );
return members[ 'public' ];
}
}
function viserr( name )
{
throw TypeError(
"Only one access modifier may be used for definition of '" +
@ -293,26 +376,6 @@ function getMemberVisibility( members, keywords, name )
);
}
// there's cleaner ways of doing this, but consider it loop unrolling for
// performance
if ( keywords[ 'private' ] )
{
( keywords[ 'public' ] || keywords[ 'protected' ] ) && viserr();
return members[ 'private' ];
}
else if ( keywords[ 'protected' ] )
{
( keywords[ 'public' ] || keywords[ 'private' ] ) && viserr();
return members[ 'protected' ];
}
else
{
// public keyword is the default, so explicitly specifying it is only
// for clarity
( keywords[ 'private' ] || keywords[ 'protected' ] ) && viserr();
return members[ 'public' ];
}
}
/**
@ -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 );
};

View File

@ -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' ] )
@ -132,14 +212,31 @@ exports.prototype.validateMethod = function(
// disallow overriding non-virtual methods
if ( keywords[ 'override' ] && !( prev_keywords[ 'virtual' ] ) )
{
if ( !( keywords[ 'abstract' ] ) )
{
throw TypeError(
"Cannot override non-virtual method '" + name + "'"
);
}
// do not allow overriding concrete methods with abstract
if ( keywords[ 'abstract' ] && !( prev_keywords[ 'abstract' ] ) )
// 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 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"
) );

View File

@ -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;
},
};

680
lib/Trait.js 100644
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;

View File

@ -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
*
* 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} unless_keyword do not set if keyword is set on existing
* method
* @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 ];

View File

@ -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.<Function>} 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.<Function>} 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
);
} );
}

View File

@ -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;
}

View File

@ -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
);
},
} );

View File

@ -33,6 +33,7 @@ var _keywords = {
'virtual': true,
'override': true,
'proxy': true,
'weak': true,
};

View File

@ -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 );
}
}
};

View File

@ -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

View File

@ -23,18 +23,18 @@ require( 'common' ).testCase(
{
caseSetUp: function()
{
// 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 );
},
} );

View File

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

View File

@ -30,9 +30,10 @@ 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(
shared.testArgs( _self, args, name, value, keywords, state,
function(
prev_default, pval_given, pkey_given
)
{
@ -53,7 +54,8 @@ require( 'common' ).testCase(
given: pkey_given,
},
};
} );
}
);
};
},

View File

@ -27,9 +27,10 @@ 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(
shared.testArgs( _self, args, name, value, keywords, state,
function(
prev_default, pval_given, pkey_given
)
{
@ -50,8 +51,13 @@ require( 'common' ).testCase(
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" );
},
} );

View File

@ -27,9 +27,10 @@ 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(
shared.testArgs( _self, args, name, value, keywords, state,
function(
prev_default, pval_given, pkey_given
)
{
@ -50,7 +51,8 @@ require( 'common' ).testCase(
given: pkey_given,
},
};
} );
}
);
};
},

View File

@ -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"
);
};

View File

@ -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 );
},

View File

@ -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 );
};

View File

@ -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 ) );
},
} );

View File

@ -100,7 +100,7 @@ require( 'common' ).testCase(
parse(
'public protected private ' +
'virtual abstract override ' +
'static const proxy ' +
'static const proxy weak ' +
'var'
);
}, Error );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
} );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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 );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 );
} );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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 );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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 );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
} );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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( {}, {} );
} );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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' } );
} );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
} );
},
} );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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()
);
},
} );

View File

@ -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

View File

@ -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 );
},
} );

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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' );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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();

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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' );

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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' );