diff --git a/lib/MethodWrappers.js b/lib/MethodWrappers.js index c568b43..7677d13 100644 --- a/lib/MethodWrappers.js +++ b/lib/MethodWrappers.js @@ -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 = Infinity; + return ret; }, }; diff --git a/lib/Trait.js b/lib/Trait.js index cff6c07..b4e2fe4 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -19,7 +19,8 @@ * along with this program. If not, see . */ -var AbstractClass = require( __dirname + '/class_abstract' ); +var AbstractClass = require( __dirname + '/class_abstract' ), + ClassBuilder = require( __dirname + '/ClassBuilder' ); function Trait() @@ -86,12 +87,39 @@ Trait.isTrait = function( trait ) */ function createConcrete( acls ) { - // start by providing a concrete implementation for our dummy method + // 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 ___$$trait$$': function() {}, + + // protected member object + 'private ___$$pmo$$': null, + __construct: function( pmo ) + { + this.___$$pmo$$ = pmo; + }, + + // mainly for debugging; should really never see this. + __name: '#ConcreteTrait#', }; - // TODO: everything else + // 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; + } + + dfn[ 'public proxy ' + f ] = '___$$pmo$$'; + } return acls.extend( dfn ); } @@ -135,7 +163,6 @@ function mixin( trait, dfn ) */ function mixMethods( src, dest, vis, iname ) { - // TODO: ignore abstract for ( var f in src ) { if ( !( Object.hasOwnProperty.call( src, f ) ) ) @@ -151,18 +178,30 @@ function mixMethods( src, dest, vis, iname ) continue; } - var pname = vis + ' proxy ' + 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 ) + // if abstract, then we are expected to provide the implementation; + // otherwise, we proxy to the trait's implementation + if ( src[ f ].___$$keywords$$['abstract'] ) { - // TODO: between what traits? - throw Error( "Trait member conflict: `" + f + "'" ); + // 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; } + else + { + var pname = vis + ' proxy ' + f; - // proxy this method to what will be the encapsulated trait object - dest[ pname ] = iname; + // 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 + "'" ); + } + + // proxy this method to what will be the encapsulated trait + // object + dest[ pname ] = iname; + } } } diff --git a/test/MemberBuilderValidator/MethodTest.js b/test/MemberBuilderValidator/MethodTest.js index b454dd3..9fe6a00 100644 --- a/test/MemberBuilderValidator/MethodTest.js +++ b/test/MemberBuilderValidator/MethodTest.js @@ -296,7 +296,6 @@ require( 'common' ).testCase( name = 'foo', amethod = _self.util.createAbstractMethod( [ 'one' ] ); - // abstract appears before this.quickFailureTest( name, 'compatible', function() { diff --git a/test/MethodWrappersTest.js b/test/MethodWrappersTest.js index 849da43..28fc6ec 100644 --- a/test/MethodWrappersTest.js +++ b/test/MethodWrappersTest.js @@ -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.assertEqual( f.__length, Infinity ); + }, } ); diff --git a/test/Trait/AbstractTest.js b/test/Trait/AbstractTest.js new file mode 100644 index 0000000..0859673 --- /dev/null +++ b/test/Trait/AbstractTest.js @@ -0,0 +1,103 @@ +/** + * 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 . + */ + +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() { called = true; }, + } ); + } ); + }, +} );