diff --git a/lib/class.js b/lib/class.js index 59b69e9..dc13e98 100644 --- a/lib/class.js +++ b/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, @@ -472,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.} traits traits to mix in @@ -486,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 @@ -498,8 +511,7 @@ function createUse( basef, traits, nonbase ) ); } - return createMixedClass( basef(), traits ) - .apply( null, arguments ); + return createMixedClass( basef(), traits ); }; @@ -545,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; } diff --git a/test/Trait/AbstractTest.js b/test/Trait/AbstractTest.js index 01287a9..8b8777d 100644 --- a/test/Trait/AbstractTest.js +++ b/test/Trait/AbstractTest.js @@ -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?' + ); + }, } );