Trait class extend support
commit
7ce57b7d97
|
@ -12,12 +12,11 @@ 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).
|
||||
Traits are able to "extend" classes, thereby declaring interface
|
||||
compatability; this is a useful alternative to interfaces when a trait is
|
||||
designed to augment a specific type. This convenience should be extended
|
||||
to traits: a trait should be able to "extend" another trait in the same
|
||||
manner that it may extend a class.
|
||||
|
||||
* TODO Documentation
|
||||
Due to the trait implementation taking longer than expected to
|
||||
|
@ -78,11 +77,5 @@ complete.
|
|||
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.
|
||||
|
|
|
@ -144,15 +144,6 @@ function ClassBuilder( warn_handler, member_builder, visibility_factory )
|
|||
*/
|
||||
this._instanceId = 0;
|
||||
|
||||
/**
|
||||
* Set to TRUE when class is in the process of being extended to ensure that
|
||||
* a constructor can be instantiated (to use as the prototype) without
|
||||
* invoking the class construction logic
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._extending = false;
|
||||
|
||||
/**
|
||||
* A flag to let the system know that we are currently attempting to access
|
||||
* a static property from within a method. This means that the caller should
|
||||
|
@ -242,6 +233,30 @@ exports.getMeta = function( cls )
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allow OBJ to assume an identity as a class
|
||||
*
|
||||
* This is useful to use objects in situations where classes are expected,
|
||||
* as it eliminates the need for handling of special cases.
|
||||
*
|
||||
* This is intended for internal use---there are no guarantees as to what
|
||||
* methods ease.js may expect that a class-like object incorporate. That
|
||||
* guarantee may exist in the future, but until then, stay away.
|
||||
*
|
||||
* @param {Object} obj object to masquerade as an ease.js class
|
||||
*
|
||||
* @return {Object} OBJ
|
||||
*/
|
||||
exports.masquerade = function( obj )
|
||||
{
|
||||
// XXX: this is duplicated; abstract
|
||||
util.defineSecureProp( obj, _priv, {} );
|
||||
|
||||
createMeta( obj, exports.ClassBase );
|
||||
return obj;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the class is an instance of the given type
|
||||
*
|
||||
|
@ -321,9 +336,6 @@ exports.prototype.build = function extend( _, __ )
|
|||
{
|
||||
var build = this;
|
||||
|
||||
// ensure we'll be permitted to instantiate abstract classes for the base
|
||||
this._extending = true;
|
||||
|
||||
var a = arguments,
|
||||
an = a.length,
|
||||
props = ( ( an > 0 ) ? a[ an - 1 ] : 0 ) || {},
|
||||
|
@ -339,18 +351,19 @@ exports.prototype.build = function extend( _, __ )
|
|||
props: this._memberBuilder.initMembers(),
|
||||
},
|
||||
|
||||
meta = exports.getMeta( base ) || {},
|
||||
// constructor may be different than base
|
||||
pmeta = exports.getMeta( prototype.constructor ) || {},
|
||||
|
||||
abstract_methods =
|
||||
util.clone( meta.abstractMethods )
|
||||
util.clone( pmeta.abstractMethods )
|
||||
|| { __length: 0 },
|
||||
|
||||
virtual_members =
|
||||
util.clone( meta.virtualMembers )
|
||||
util.clone( pmeta.virtualMembers )
|
||||
|| {}
|
||||
;
|
||||
|
||||
// prevent extending final classes
|
||||
// prevent extending final classes (TODO: abstract this check)
|
||||
if ( base.___$$final$$ === true )
|
||||
{
|
||||
throw Error(
|
||||
|
@ -464,7 +477,7 @@ exports.prototype.build = function extend( _, __ )
|
|||
util.defineSecureProp( prototype, '__self', new_class.___$$svis$$ );
|
||||
|
||||
// create internal metadata for the new class
|
||||
var meta = createMeta( new_class, base );
|
||||
var meta = createMeta( new_class, base, pmeta );
|
||||
meta.abstractMethods = abstract_methods;
|
||||
meta.virtualMembers = virtual_members;
|
||||
meta.name = cname;
|
||||
|
@ -476,15 +489,13 @@ exports.prototype.build = function extend( _, __ )
|
|||
// (intended for use in prototype chains)
|
||||
new_class.asPrototype = function()
|
||||
{
|
||||
build._extending = true;
|
||||
var inst = new_class();
|
||||
build._extending = false;
|
||||
new_class[ _priv ].extending = true;
|
||||
var inst = new new_class();
|
||||
new_class[ _priv ].extending = false;
|
||||
|
||||
return inst;
|
||||
};
|
||||
|
||||
// we're done with the extension process
|
||||
this._extending = false;
|
||||
|
||||
return new_class;
|
||||
};
|
||||
|
||||
|
@ -498,7 +509,9 @@ exports.prototype._getBase = function( base )
|
|||
// constructor (we could also check to ensure that the return value of
|
||||
// the constructor is an object, but that is not our concern)
|
||||
case 'function':
|
||||
return new base();
|
||||
return ( base[ _priv ] )
|
||||
? base.asPrototype()
|
||||
: new base();
|
||||
|
||||
// we can use objects as the prototype directly
|
||||
case 'object':
|
||||
|
@ -872,7 +885,7 @@ exports.prototype.createConcreteCtor = function( cname, members )
|
|||
// If we're extending, we don't actually want to invoke any class
|
||||
// construction logic. The above is sufficient to use this class in a
|
||||
// prototype, so stop here.
|
||||
if ( _self._extending )
|
||||
if ( ClassInstance[ _priv ].extending )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -968,7 +981,7 @@ exports.prototype.createAbstractCtor = function( cname )
|
|||
|
||||
var __abstract_self = function()
|
||||
{
|
||||
if ( !_self._extending )
|
||||
if ( !__abstract_self[ _priv ].extending )
|
||||
{
|
||||
throw Error(
|
||||
"Abstract class " + ( cname || '(anonymous)' ) +
|
||||
|
@ -1255,18 +1268,23 @@ exports.prototype.attachStatic = function( ctor, members, base, inheriting )
|
|||
/**
|
||||
* Initializes class metadata for the given class
|
||||
*
|
||||
* DYNMETA is used only when CPARENT's metadata are flagged as "lazy",
|
||||
* meaning that the data are not available at the time of its definition,
|
||||
* but are available now as DYNMETA.
|
||||
*
|
||||
* @param {Function} func class to initialize metadata for
|
||||
* @param {Function} cparent class parent
|
||||
* @param {?Object} dynmeta dynamic metadata
|
||||
*
|
||||
* @return {undefined}
|
||||
*
|
||||
* Suppressed due to warnings for use of __cid
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
function createMeta( func, cparent )
|
||||
function createMeta( func, cparent, dynmeta )
|
||||
{
|
||||
var id = func.__cid,
|
||||
parent_meta = ( ( cparent.__cid )
|
||||
parent_meta = ( cparent[ _priv ]
|
||||
? exports.getMeta( cparent )
|
||||
: undefined
|
||||
);
|
||||
|
@ -1274,7 +1292,13 @@ function createMeta( func, cparent )
|
|||
// copy the parent prototype's metadata if it exists (inherit metadata)
|
||||
if ( parent_meta )
|
||||
{
|
||||
return func[ _priv ].meta = util.clone( parent_meta, true );
|
||||
return func[ _priv ].meta = util.clone(
|
||||
// "lazy" metadata are unavailable at the time of definition
|
||||
parent_meta._lazy
|
||||
? dynmeta
|
||||
: parent_meta,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// create empty
|
||||
|
@ -1461,4 +1485,3 @@ function attachFlags( ctor, props )
|
|||
// (v8 performance)
|
||||
props.___$$final$$ = props.___$$abstract$$ = undefined;
|
||||
}
|
||||
|
||||
|
|
84
lib/Trait.js
84
lib/Trait.js
|
@ -97,8 +97,50 @@ function _createStaging( name )
|
|||
}
|
||||
|
||||
|
||||
Trait.extend = function( dfn )
|
||||
Trait.extend = function( /* ... */ )
|
||||
{
|
||||
var an = arguments.length,
|
||||
dfn = arguments[ an - 1 ],
|
||||
has_ext_base = ( an > 1 ),
|
||||
ext_base = ( has_ext_base ) ? arguments[ 0 ] : null;
|
||||
|
||||
if ( an > 2 )
|
||||
{
|
||||
throw Error(
|
||||
"Unexpected number of arguments to Trait.extend"
|
||||
);
|
||||
}
|
||||
|
||||
if ( has_ext_base )
|
||||
{
|
||||
var basetype = typeof ext_base;
|
||||
|
||||
if ( ( ext_base === null )
|
||||
|| !( ( basetype === 'object' )
|
||||
|| ( basetype === 'function' )
|
||||
) )
|
||||
{
|
||||
throw TypeError(
|
||||
"Trait cannot extend base of type '" + basetype + "'"
|
||||
);
|
||||
}
|
||||
|
||||
// prevent extending final classes (TODO: abstract this check; see
|
||||
// also ClassBuilder)
|
||||
if ( ext_base.___$$final$$ === true )
|
||||
{
|
||||
throw TypeError(
|
||||
"Trait cannot extend final class"
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this is intended to be temporary; see Trait/ClassExtendTest
|
||||
if ( module.exports.isTrait( ext_base ) )
|
||||
{
|
||||
throw TypeError( "Traits cannot extend other traits" );
|
||||
}
|
||||
}
|
||||
|
||||
// we may have been passed some additional metadata
|
||||
var meta = ( this || {} ).__$$meta || {};
|
||||
|
||||
|
@ -171,6 +213,7 @@ Trait.extend = function( dfn )
|
|||
Trait.__trait = type;
|
||||
Trait.__acls = tclass;
|
||||
Trait.__ccls = null;
|
||||
Trait.__extbase = ext_base;
|
||||
Trait.toString = function()
|
||||
{
|
||||
return ''+name;
|
||||
|
@ -198,6 +241,41 @@ Trait.extend = function( dfn )
|
|||
};
|
||||
|
||||
|
||||
/**
|
||||
* Validate whether mixin is permitted
|
||||
*
|
||||
* If a mixee (the trait being mixed in) extends some type S, then a
|
||||
* contract has been created mandating that that trait may only be mixed
|
||||
* into something of type S; a `TypeError` will be thrown if this contract
|
||||
* is violated.
|
||||
*
|
||||
* @param {Class} base mixor (target of mixin)
|
||||
* @param {Trait} T mixee (trait being mixed in)
|
||||
*
|
||||
* @return {undefined}
|
||||
*
|
||||
* @throws {TypeError} on type contract violation
|
||||
*/
|
||||
function _validateMixin( base, T )
|
||||
{
|
||||
if ( !T.__extbase )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: isSubtypeOf
|
||||
if ( !( ( T.__extbase === base )
|
||||
|| ClassBuilder.isInstanceOf( T.__extbase, base.asPrototype() )
|
||||
) )
|
||||
{
|
||||
throw TypeError(
|
||||
"Cannot mix trait " + T.toString() + " into " + base.toString() +
|
||||
"; mixor must be of type " + T.__extbase.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a string representation of the trait type
|
||||
*
|
||||
|
@ -530,12 +608,14 @@ function createVirtProxy( acls, dfn )
|
|||
* @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
|
||||
* @param {Class} base target supertype
|
||||
*
|
||||
* @return {Object} dfn
|
||||
*/
|
||||
function mixin( trait, dfn, tc, base )
|
||||
{
|
||||
_validateMixin( base, trait );
|
||||
|
||||
// the abstract class hidden within the trait
|
||||
var acls = trait.__acls;
|
||||
|
||||
|
|
35
lib/class.js
35
lib/class.js
|
@ -34,6 +34,7 @@ var _console = ( typeof console !== 'undefined' ) ? console : undefined;
|
|||
|
||||
var util = require( './util' ),
|
||||
ClassBuilder = require( './ClassBuilder' ),
|
||||
Interface = require( './interface' ),
|
||||
|
||||
warn = require( './warn' ),
|
||||
Warning = warn.Warning,
|
||||
|
@ -179,11 +180,6 @@ module.exports.isClass = function( obj )
|
|||
{
|
||||
obj = obj || _dummyclass;
|
||||
|
||||
if ( !obj.prototype )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var meta = ClassBuilder.getMeta( obj );
|
||||
|
||||
// TODO: we're checking a random field on the meta object; do something
|
||||
|
@ -477,6 +473,11 @@ function createImplement( base, ifaces, cname )
|
|||
* be explicit: in this case, any instantiation attempts will result in an
|
||||
* exception being thrown.
|
||||
*
|
||||
* This staging object may be used as a base for extending. Note, however,
|
||||
* that its metadata are unavailable at the time of definition---its
|
||||
* contents are marked as "lazy" and must be processed using the mixin's
|
||||
* eventual metadata.
|
||||
*
|
||||
* @param {function()} basef returns base from which to lazily
|
||||
* extend
|
||||
* @param {Array.<Function>} traits traits to mix in
|
||||
|
@ -491,6 +492,13 @@ 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()
|
||||
{
|
||||
return partialClass()
|
||||
.apply( null, arguments );
|
||||
};
|
||||
|
||||
|
||||
var partialClass = function()
|
||||
{
|
||||
// this argument will be set only in the case where an existing
|
||||
// (non-base) class is extended, meaning that an explict Class or
|
||||
|
@ -503,8 +511,7 @@ function createUse( basef, traits, nonbase )
|
|||
);
|
||||
}
|
||||
|
||||
return createMixedClass( basef(), traits )
|
||||
.apply( null, arguments );
|
||||
return createMixedClass( basef(), traits );
|
||||
};
|
||||
|
||||
|
||||
|
@ -550,6 +557,20 @@ function createUse( basef, traits, nonbase )
|
|||
return partial.extend( {} );
|
||||
};
|
||||
|
||||
partial.asPrototype = function()
|
||||
{
|
||||
return partialClass().asPrototype();
|
||||
};
|
||||
|
||||
partial.__isInstanceOf = Interface.isInstanceOf;
|
||||
|
||||
// allow the system to recognize this object as a viable base for
|
||||
// extending, but mark the metadata as lazy: since we defer all
|
||||
// processing for mixins, we cannot yet know all metadata
|
||||
// TODO: `_lazy' is a kluge
|
||||
ClassBuilder.masquerade( partial );
|
||||
ClassBuilder.getMeta( partial )._lazy = true;
|
||||
|
||||
return partial;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ require( 'common' ).testCase(
|
|||
setUp: function()
|
||||
{
|
||||
this.Sut = this.require( 'class' );
|
||||
this.ClassBuilder = this.require( 'ClassBuilder' );
|
||||
|
||||
this.Foo = this.Sut.extend(
|
||||
{
|
||||
|
@ -249,6 +250,21 @@ require( 'common' ).testCase(
|
|||
},
|
||||
|
||||
|
||||
/**
|
||||
* There are cases---intended for internal use---where it is beneficial
|
||||
* for an object to be treated as though it were actually a class.
|
||||
*/
|
||||
'Any object may masquerade as a class': function()
|
||||
{
|
||||
var obj = {};
|
||||
|
||||
// XXX: tightly coupled logic here; refactor things
|
||||
this.ClassBuilder.masquerade( obj );
|
||||
|
||||
this.assertOk( this.Sut.isClass( obj ) );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* This really should be encapsulated, probably, but it does exist for
|
||||
* reference.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Tests abstract trait definition and use
|
||||
*
|
||||
* Copyright (C) 2014 Free Software Foundation, Inc.
|
||||
* Copyright (C) 2015 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU ease.js.
|
||||
*
|
||||
|
@ -360,4 +360,70 @@ require( 'common' ).testCase(
|
|||
_self.Class.use( Ta ).extend();
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Before traits, the only way to make an abstract class concrete, or
|
||||
* vice versa, was by extending. Now, however, a mixing in a trait can
|
||||
* introduce abstract or concrete methods. This poses a problem, since
|
||||
* the syntax for providing self-documenting AbstractClass definitions
|
||||
* no longer works: invoking `AbstractClass.use' produces different
|
||||
* results than invoking `SomeAbstractClass.use', with the goal of
|
||||
* extending it.
|
||||
*
|
||||
* Consider this issue: we wish to mix some trait T into abstract class
|
||||
* AC. Trait T does not provide a concrete implementation of the
|
||||
* abstract methods in AT, and so the resulting class after the final
|
||||
* `#extend' call would be abstract.
|
||||
*
|
||||
* We have no choice but to allow extending the intermediate object
|
||||
* produced by a class's `#use' method; otherwise, any call to `#extend'
|
||||
* on the intermediate object would result in an error, because the
|
||||
* class would still have abstract members, but has not been declared to
|
||||
* be abstract. Handling abstract classes in this manner would be
|
||||
* consistent with all other scenarios, and would be transparent: why
|
||||
* should the user care that there is some odd intermediate object being
|
||||
* used rather than an actual class?
|
||||
*/
|
||||
'Abstract classes can be derived from intermediates': function()
|
||||
{
|
||||
var chk = [{}];
|
||||
|
||||
var AC = this.AbstractClass( { 'abstract foo': [] } ),
|
||||
T = this.Sut( { moo: function() { return chk; } } );
|
||||
|
||||
// mix trait into an abstract class
|
||||
var M = this.AbstractClass.extend(
|
||||
AC.use( T ),
|
||||
{}
|
||||
);
|
||||
|
||||
this.assertOk( this.Class.isClass( M ) );
|
||||
this.assertOk( M.isAbstract() );
|
||||
|
||||
var inst = M.extend( { foo: function() {} } )();
|
||||
|
||||
// we should not have lost the original abstract class
|
||||
this.assertOk(
|
||||
this.Class.isA( AC, inst )
|
||||
);
|
||||
|
||||
// not strictly necessary; comfort/sanity check: if this succeeds
|
||||
// but the next fails, then there's a problem marking the
|
||||
// implemented types
|
||||
this.assertStrictEqual(
|
||||
chk,
|
||||
inst.moo()
|
||||
);
|
||||
|
||||
// the trait should have been applied (see above note if this
|
||||
// fails); if this does fail, note that, without
|
||||
// AbstractClass.extend, we have (correctly):
|
||||
// isA( T, AC.use( T ).extend( ... )() )
|
||||
this.assertOk(
|
||||
this.Class.isA( T, inst ),
|
||||
'Instance is not recognized as having mixed in type T, but ' +
|
||||
'incorporates its definition; metadata bug?'
|
||||
);
|
||||
},
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* Tests extending traits from classes
|
||||
*
|
||||
* Copyright (C) 2015 Free Software Foundation, Inc.
|
||||
*
|
||||
* 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' );
|
||||
this.FinalClass = this.require( 'class_final' );
|
||||
|
||||
// nonsensical extend bases that do not support object
|
||||
// representations (TODO: use some system-wide understanding of
|
||||
// "extendable" values)
|
||||
this.nonsense = [
|
||||
null,
|
||||
undefined,
|
||||
false,
|
||||
NaN,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Normally, there are no restrictions on what class a trait may be
|
||||
* mixed into. When ``extending'' a class, we would expect intuitively
|
||||
* that this behavior would remain consistent.
|
||||
*/
|
||||
'Trait T extending class C can be mixed into C': function()
|
||||
{
|
||||
var C = this.Class( {} ),
|
||||
T = this.Sut.extend( C, {} );
|
||||
|
||||
this.assertDoesNotThrow( function()
|
||||
{
|
||||
C.use( T )();
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Restrictions emerge once a disjoint type D attempts to mix in a trait
|
||||
* T extending class C. When C is ``extended'', we are
|
||||
* effectively extracting and implementing interfaces representing its
|
||||
* public and protected members---this has all the same effects that one
|
||||
* would expect from implementing an interface. However, the act of
|
||||
* extension implies a tight coupling between T and C: we're not just
|
||||
* expecting a particular interface; we're also expecting the mixee to
|
||||
* behave in a certain manner, just as a subtype of C would expect.
|
||||
*
|
||||
* Traits extending classes therefore behave like conventional subtypes
|
||||
* extending their parents, but with a greater degree of
|
||||
* flexibility. We would not expect to be able to use a subtype of C as
|
||||
* if it were a disjoint type D, because they are different types: even
|
||||
* if they share an identical interface, their intents are
|
||||
* distinct. This is the case here.
|
||||
*/
|
||||
'Trait T extending class C cannot be mixed into disjoint class D':
|
||||
function()
|
||||
{
|
||||
var C = this.Class( {} ),
|
||||
D = this.Class( {} ),
|
||||
T = this.Sut.extend( C, {} );
|
||||
|
||||
this.assertThrows( function()
|
||||
{
|
||||
D.use( T )();
|
||||
}, TypeError );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Just as some class D' extending supertype D is of both types D' and
|
||||
* D, and a trait T implementing interface I is of both types T and I,
|
||||
* we would expect that a trait T extending C would be of both types T
|
||||
* _and_ C, since T is effectively implementing C's interface.
|
||||
*/
|
||||
'Trait T extending class C is of both types T and C': function()
|
||||
{
|
||||
var C = this.Class( {} ),
|
||||
T = this.Sut.extend( C, {} ),
|
||||
inst = C.use( T )();
|
||||
|
||||
this.assertOk( this.Class.isA( T, inst ) );
|
||||
this.assertOk( this.Class.isA( C, inst ) );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Since a subtype C2 is, by definition, also of type C, we would expect
|
||||
* that any traits that are valid to be mixed into type C would also be
|
||||
* valid to be mixed into subtypes of C. This permits trait
|
||||
* polymorphism in the same manner as classes and interfaces.
|
||||
*/
|
||||
'Trait T extending class C can be mixed into C subtype C2': function()
|
||||
{
|
||||
var C = this.Class( {} ),
|
||||
C2 = C.extend( {} ),
|
||||
T = this.Sut.extend( C, {} );
|
||||
|
||||
this.assertDoesNotThrow( function()
|
||||
{
|
||||
C2.use( T )();
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* This is a corollary of the above associations.
|
||||
*/
|
||||
'Trait T extending subtype C2 cannot be mixed into supertype C':
|
||||
function()
|
||||
{
|
||||
var C = this.Class( {} ),
|
||||
C2 = C.extend( {} ),
|
||||
T = this.Sut.extend( C2, {} );
|
||||
|
||||
this.assertThrows( function()
|
||||
{
|
||||
C.use( T )();
|
||||
}, TypeError );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The trait `#extend' method mirrors the syntax of classes: the first
|
||||
* argument is the class to be extended, and the second is the actual
|
||||
* definition.
|
||||
*/
|
||||
'Trait definition can follow class extension': function()
|
||||
{
|
||||
var a = ['a'],
|
||||
b = ['b'];
|
||||
|
||||
var C = this.Class( {
|
||||
foo: function() { return a; }
|
||||
} ),
|
||||
T = this.Sut.extend( C, {
|
||||
bar: function() { return b; }
|
||||
} );
|
||||
|
||||
var inst = C.use( T )();
|
||||
|
||||
this.assertStrictEqual( inst.foo(), a );
|
||||
this.assertStrictEqual( inst.bar(), b );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* This is a corollary, but is still worth testing for assurance.
|
||||
*
|
||||
* We already stated that a trait Tb extending C's subtype C2 cannot be
|
||||
* mixed into C, because C is not of type C2. But Ta extending C can be
|
||||
* mixed into C2, because C2 _is_ of type C. Therefore, both of these
|
||||
* traits should be able to co-mix in the latter situation, but not the
|
||||
* former.
|
||||
*/
|
||||
'Trait Ta extending C and Tb extending C2 cannot co-mix': function()
|
||||
{
|
||||
var C = this.Class( 'C' ).extend( { _a: null } ),
|
||||
C2 = this.Class( 'C2' ).extend( C, { _b: null } ),
|
||||
Ta = this.Sut.extend( C, {} ),
|
||||
Tb = this.Sut.extend( C2, {} );
|
||||
|
||||
// this is _not_ okay
|
||||
this.assertThrows( function()
|
||||
{
|
||||
C.use( Ta ).use( Tb )();
|
||||
} );
|
||||
|
||||
// but this is, since Tb extends C2 itself, and Ta extends C2's
|
||||
// supertype
|
||||
this.assertDoesNotThrow( function()
|
||||
{
|
||||
C2.use( Tb ).use( Ta )();
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The `#extend' method for traits, when extending a class, must not
|
||||
* accept more than two arguments; otherwise, there may be a bug. It
|
||||
* does not make sense to accept more arguments, since traits can only
|
||||
* extend a single class.
|
||||
*
|
||||
* The reason? Well, as a corollary of the above, given types
|
||||
* C_0,...,C_n to extend: C_x, 0<=x<n, must be equal to or a subtype of
|
||||
* each C_i, 0<=x≠i<n, or the types are incompatible. In that case, the
|
||||
* trait could just extend the subtype that has each other type C_i in
|
||||
* its lineage, making multiple specifications unnecessary.
|
||||
*
|
||||
* Does that mean that it's not possible to combine two disjoint classes
|
||||
* into one API that is a subtype of both? Yes, it does: that's
|
||||
* multiple inheritance; use interfaces or traits, both of which are
|
||||
* designed to solve this problem properly (the latter most closely).
|
||||
*/
|
||||
'Trait class extension cannot supply more than two arguments':
|
||||
function()
|
||||
{
|
||||
var _self = this;
|
||||
|
||||
this.assertThrows( function()
|
||||
{
|
||||
// extra argument
|
||||
_self.Sut.extend( _self.Class( {} ), {}, {} );
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Help out the programmer by letting her know when she provides an
|
||||
* invalid base, which would surely not give her the result that she
|
||||
* expects.
|
||||
*/
|
||||
'@each(nonsense) Traits cannot extend nonsense': function( base )
|
||||
{
|
||||
var _self = this;
|
||||
|
||||
this.assertThrows( function()
|
||||
{
|
||||
_self.Sut.extend( base, {} );
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Eventually, traits will be able to extend other traits just as they
|
||||
* can classes---by asserting and operating on the type. This is just a
|
||||
* generalization that needs to be properly tested and allowed, and
|
||||
* should not function any differently than a class.
|
||||
*
|
||||
* Don't worry; it'll happen in the future.
|
||||
*/
|
||||
'Traits cannot yet extend other traits': function()
|
||||
{
|
||||
var _self = this;
|
||||
|
||||
this.assertThrows( function()
|
||||
{
|
||||
_self.Sut.extend( _self.Sut( {} ), {} );
|
||||
}, TypeError );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* For consistency with the rest of the system, final classes are not
|
||||
* permitted to be extended.
|
||||
*/
|
||||
'Traits cannot extend final classes': function()
|
||||
{
|
||||
var _self = this;
|
||||
|
||||
this.assertThrows( function()
|
||||
{
|
||||
_self.Sut.extend( _self.FinalClass( {} ), {} );
|
||||
}, TypeError );
|
||||
},
|
||||
} );
|
|
@ -455,4 +455,24 @@ require( 'common' ).testCase(
|
|||
Sut( dfn );
|
||||
} );
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The stating object rendered by `#use` calls implement the same
|
||||
* methods as classes, and are even treated as classes when invoked
|
||||
* using the immediate syntax (see ImmediateTest). When defining
|
||||
* abstract classes, staging objects may be extended as if they were
|
||||
* classes (see AbstractTest).
|
||||
*
|
||||
* It makes sense for staging objects to be able to be treated as if
|
||||
* they were classes, which demands reflection API consistency.
|
||||
*/
|
||||
'Staging object for eventual mixin is considered to be class': function()
|
||||
{
|
||||
var T = this.Sut( {} );
|
||||
|
||||
this.assertOk(
|
||||
this.Class.isClass( this.Class( {} ).use( T ) )
|
||||
);
|
||||
},
|
||||
} );
|
||||
|
|
Loading…
Reference in New Issue