diff --git a/lib/Trait.js b/lib/Trait.js index eeb340b..eb48ad7 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -41,6 +41,9 @@ Trait.extend = function( dfn ) // just in case DFN does not contain any abstract members itself dfn[ 'abstract protected ___$$trait$$' ] = []; + // give the abstract trait class a distinctive name for debugging + dfn.__name = '#AbstractTrait#'; + function TraitType() { throw Error( "Cannot instantiate trait" ); @@ -118,7 +121,15 @@ function createConcrete( acls ) continue; } - dfn[ 'public proxy ' + f ] = '___$$pmo$$'; + // 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$$'; } return acls.extend( dfn ); @@ -178,13 +189,18 @@ function mixMethods( src, dest, vis, iname ) continue; } + var keywords = src[ f ].___$$keywords$$; + // if abstract, then we are expected to provide the implementation; // otherwise, we proxy to the trait's implementation - if ( src[ f ].___$$keywords$$['abstract'] ) + if ( keywords['abstract'] ) { // copy the abstract definition (N.B. this does not copy the - // param names, since that is not [yet] important) - dest[ 'weak abstract ' + f ] = src[ f ].definition; + // param names, since that is not [yet] important); the + // visibility modified is important to prevent de-escalation + // errors on override + var vis = keywords['protected'] ? 'protected' : 'public'; + dest[ vis + ' weak abstract ' + f ] = src[ f ].definition; } else { diff --git a/test/Trait/AbstractTest.js b/test/Trait/AbstractTest.js index 0859673..130d971 100644 --- a/test/Trait/AbstractTest.js +++ b/test/Trait/AbstractTest.js @@ -1,5 +1,5 @@ /** - * Tests basic trait definition + * Tests abstract trait definition and use * * Copyright (C) 2014 Mike Gerwitz * @@ -88,7 +88,7 @@ require( 'common' ).testCase( */ 'Concrete classes must be compatible with abstract traits': function() { - var T = this.Sut( { 'abstract traitfoo': [ 'foo' ] } ); + var T = this.Sut( { 'abstract traitfoo': [ 'foo' ] } ); var _self = this; this.assertThrows( function() @@ -96,8 +96,94 @@ require( 'common' ).testCase( C = _self.Class.use( T ).extend( { // missing param in definition - traitfoo: function() { called = true; }, + 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?!" + ); + }, } );