diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js
index 3b716c6..c214a5c 100644
--- a/lib/ClassBuilder.js
+++ b/lib/ClassBuilder.js
@@ -363,7 +363,7 @@ exports.prototype.build = function extend( _, __ )
|| {}
;
- // prevent extending final classes
+ // prevent extending final classes (TODO: abstract this check)
if ( base.___$$final$$ === true )
{
throw Error(
diff --git a/lib/Trait.js b/lib/Trait.js
index b622586..2545147 100644
--- a/lib/Trait.js
+++ b/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 || {};
@@ -168,10 +210,11 @@ Trait.extend = function( dfn )
// and here we can see that traits are quite literally abstract classes
var tclass = base.extend( dfn );
- Trait.__trait = type;
- Trait.__acls = tclass;
- Trait.__ccls = null;
- Trait.toString = function()
+ 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;
diff --git a/test/Trait/ClassExtendTest.js b/test/Trait/ClassExtendTest.js
new file mode 100644
index 0000000..345890c
--- /dev/null
+++ b/test/Trait/ClassExtendTest.js
@@ -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 .
+ */
+
+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