From 548c38503fba11c7a3d8c429eeb97ef0359303a7 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Sun, 26 Jan 2014 00:08:42 -0500 Subject: [PATCH] Added support for weak abstract methods This adds the `weak' keyword and permits abstract method definitions to appear in the same definition object as the concrete implementation. This should never be used with hand-written code---it is intended for code generators (e.g. traits) that do not know if a concrete implementation will be provided, and would waste cycles duplicating the property parsing that ease.js will already be doing. It also allows for more concise code generator code. --- lib/ClassBuilder.js | 12 ++++- lib/MemberBuilder.js | 8 ++- lib/MemberBuilderValidator.js | 33 +++++++++--- lib/class_abstract.js | 26 +++++++--- test/MemberBuilder/MethodTest.js | 62 ++++++++++++++++++++--- test/MemberBuilderValidator/MethodTest.js | 56 ++++++++++++++++++++ test/MemberBuilderValidator/inc-common.js | 4 +- 7 files changed, 176 insertions(+), 25 deletions(-) diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index c736808..65ce8aa 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -525,11 +525,21 @@ exports.prototype.buildMembers = function buildMembers( } } - _self._memberBuilder.buildMethod( + var used = _self._memberBuilder.buildMethod( dest, null, name, func, keywords, instLookup, class_id, base ); + // do nothing more if we didn't end up using this definition + // (this may be the case, for example, with weak members) + if ( !used ) + { + return; + } + + // note the concrete method check; this ensures that weak + // abstract methods will not count if a concrete method of the + // smae name has already been seen if ( is_abstract ) { abstract_methods[ name ] = true; diff --git a/lib/MemberBuilder.js b/lib/MemberBuilder.js index 51f8bb3..0f12c51 100644 --- a/lib/MemberBuilder.js +++ b/lib/MemberBuilder.js @@ -140,7 +140,6 @@ exports.buildMethod = function( } else if ( prev ) { - if ( keywords[ 'override' ] || prev_keywords[ 'abstract' ] ) { // override the method @@ -148,6 +147,12 @@ exports.buildMethod = function( prev, value, instCallback, cid ); } + else if ( keywords.weak && keywords[ 'abstract' ] ) + { + // another member of the same name has been found; discard the + // weak declaration + return false; + } else { // by default, perform method hiding, even if the keyword was not @@ -170,6 +175,7 @@ exports.buildMethod = function( // store keywords for later reference (needed for pre-ES5 fallback) dest[ name ].___$$keywords$$ = keywords; + return true; }; diff --git a/lib/MemberBuilderValidator.js b/lib/MemberBuilderValidator.js index 0730043..707ff88 100644 --- a/lib/MemberBuilderValidator.js +++ b/lib/MemberBuilderValidator.js @@ -138,8 +138,12 @@ exports.prototype.validateMethod = function( ); } - // do not allow overriding concrete methods with abstract - if ( keywords[ 'abstract' ] && !( prev_keywords[ 'abstract' ] ) ) + // do not allow overriding concrete methods with abstract unless the + // abstract method is weak + if ( keywords[ 'abstract' ] + && !( keywords.weak ) + && !( prev_keywords[ 'abstract' ] ) + ) { throw TypeError( "Cannot override concrete method '" + name + "' with " + @@ -147,9 +151,19 @@ exports.prototype.validateMethod = function( ); } + var lenprev = prev, + lennow = value; + if ( keywords.weak && !( prev_keywords[ 'abstract' ] ) ) + { + // weak abstract declaration found after its concrete + // definition; check in reverse order + lenprev = value; + lennow = prev; + } + // ensure parameter list is at least the length of its supertype - if ( ( value.__length || value.length ) - < ( prev.__length || prev.length ) + if ( ( lennow.__length || lennow.length ) + < ( lenprev.__length || lenprev.length ) ) { throw TypeError( @@ -168,10 +182,13 @@ exports.prototype.validateMethod = function( ); } - // Disallow overriding method without override keyword (unless parent - // method is abstract). In the future, this will provide a warning to - // default to method hiding. - if ( !( keywords[ 'override' ] || prev_keywords[ 'abstract' ] ) ) + // Disallow overriding method without override keyword (unless + // parent method is abstract). In the future, this will provide a + // warning to default to method hiding. Note the check for a + if ( !( keywords[ 'override' ] + || prev_keywords[ 'abstract' ] + || ( keywords[ 'abstract' ] && keywords.weak ) + ) ) { throw TypeError( "Attempting to override method '" + name + diff --git a/lib/class_abstract.js b/lib/class_abstract.js index 42a0327..c52df72 100644 --- a/lib/class_abstract.js +++ b/lib/class_abstract.js @@ -61,19 +61,31 @@ exports.extend = function() }; +/** + * Mixes in a trait + * + * @return {Object} staged abstract class + */ +exports.use = function() +{ + return abstractOverride( + Class.use.apply( this, arguments ) + ); +}; + + /** * Creates an abstract class implementing the given members * * Simply wraps the class module's implement() method. * - * @return {Object} abstract class + * @return {Object} staged abstract class */ exports.implement = function() { - var impl = Class.implement.apply( this, arguments ); - - abstractOverride( impl ); - return impl; + return abstractOverride( + Class.implement.apply( this, arguments ) + ); }; @@ -112,8 +124,8 @@ function abstractOverride( obj ) var extend = obj.extend, impl = obj.implement; - // wrap and apply the abstract flag, only if the method is defined (it may - // not be under all circumstances, e.g. after an implement()) + // wrap and apply the abstract flag, only if the method is defined (it + // may not be under all circumstances, e.g. after an implement()) impl && ( obj.implement = function() { return abstractOverride( impl.apply( this, arguments ) ); diff --git a/test/MemberBuilder/MethodTest.js b/test/MemberBuilder/MethodTest.js index 72b86c8..4ca7200 100644 --- a/test/MemberBuilder/MethodTest.js +++ b/test/MemberBuilder/MethodTest.js @@ -52,6 +52,10 @@ require( 'common' ).testCase( }; } ); }; + + // simply intended to execute test two two perspectives + this.weakab = [ + ]; }, @@ -105,9 +109,9 @@ require( 'common' ).testCase( _self.testArgs( arguments, name, value, keywords ); }; - this.sut.buildMethod( + this.assertOk( this.sut.buildMethod( this.members, {}, name, value, keywords, function() {}, 1, {} - ); + ) ); this.assertEqual( true, called, 'validateMethod() was not called' ); }, @@ -133,9 +137,9 @@ require( 'common' ).testCase( _self.testArgs( arguments, name, value, keywords ); }; - this.sut.buildMethod( + this.assertOk( this.sut.buildMethod( this.members, {}, name, value, keywords, function() {}, 1, {} - ); + ) ); this.assertEqual( true, called, 'validateMethod() was not called' ); }, @@ -159,9 +163,9 @@ require( 'common' ).testCase( ; // build the proxy - this.sut.buildMethod( + this.assertOk( this.sut.buildMethod( this.members, {}, name, value, keywords, instCallback, cid, {} - ); + ) ); this.assertNotEqual( null, this.proxyFactoryCall, "Proxy factory should be used when `proxy' keyword is provided" @@ -181,4 +185,50 @@ require( 'common' ).testCase( "Generated proxy method should be properly assigned to members" ); }, + + + /** + * A weak abstract method may exist in a situation where a code + * generator is not certain whether a concrete implementation may be + * provided. In this case, we would not want to actually create an + * abstract method if a concrete one already exists. + */ + 'Weak abstract methods are not processed if concrete is available': + function() + { + var _self = this, + called = false, + + cid = 1, + name = 'foo', + cval = function() { called = true; }, + aval = [], + + ckeywords = {}, + akeywords = { weak: true, 'abstract': true, }, + + instCallback = function() {} + ; + + // first define abstract + this.assertOk( this.sut.buildMethod( + this.members, {}, name, aval, akeywords, instCallback, cid, {} + ) ); + + // concrete should take precedence + this.assertOk( this.sut.buildMethod( + this.members, {}, name, cval, ckeywords, instCallback, cid, {} + ) ); + + this.members[ 'public' ].foo(); + this.assertOk( called, "Concrete method did not take precedence" ); + + // now try abstract again to ensure this works from both directions + this.assertOk( this.sut.buildMethod( + this.members, {}, name, aval, akeywords, instCallback, cid, {} + ) === false ); + + this.members[ 'public' ].foo(); + this.assertOk( called, "Concrete method unkept" ); + }, } ); diff --git a/test/MemberBuilderValidator/MethodTest.js b/test/MemberBuilderValidator/MethodTest.js index 637cc46..b454dd3 100644 --- a/test/MemberBuilderValidator/MethodTest.js +++ b/test/MemberBuilderValidator/MethodTest.js @@ -27,6 +27,7 @@ require( 'common' ).testCase( caseSetUp: function() { var _self = this; + this.util = this.require( 'util' ); this.quickKeywordMethodTest = function( keywords, identifier, prev ) { @@ -208,6 +209,21 @@ require( 'common' ).testCase( }, + /** + * Contrary to the above test, an abstract method may appear after its + * concrete implementation if the `weak' keyword is provided; this + * exists to allow code generation tools to fall back to abstract + * without having to invoke the property parser directly, complicating + * their logic and duplicating work that ease.js will already do. + */ + 'Concrete method may appear with weak abstract method': function() + { + this.quickKeywordMethodTest( + [ 'weak', 'abstract' ], null, [] + ); + }, + + /** * The parameter list is part of the class interface. Changing the length * will make the interface incompatible with that of its parent and make @@ -267,6 +283,46 @@ require( 'common' ).testCase( }, + /** + * Same concept as the above test, but ensure that the logic for weak + * abstract members does not skip the valiation. Furthermore, if a weak + * abstract member is found *after* the concrete definition, the same + * restrictions should apply retroacively. + */ + 'Weak abstract overrides must meet compatibility requirements': + function() + { + var _self = this, + name = 'foo', + amethod = _self.util.createAbstractMethod( [ 'one' ] ); + + + // abstract appears before + this.quickFailureTest( name, 'compatible', function() + { + _self.sut.validateMethod( + name, + function() {}, + {}, + { member: amethod }, + { 'weak': true, 'abstract': true } + ); + } ); + + // abstract appears after + this.quickFailureTest( name, 'compatible', function() + { + _self.sut.validateMethod( + name, + amethod, + { 'weak': true, 'abstract': true }, + { member: function() {} }, + {} + ); + } ); + }, + + /** * One should not be able to, for example, declare a private method it had * previously been declared protected, or declare it as protected if it has diff --git a/test/MemberBuilderValidator/inc-common.js b/test/MemberBuilderValidator/inc-common.js index e717ea9..e6e7233 100644 --- a/test/MemberBuilderValidator/inc-common.js +++ b/test/MemberBuilderValidator/inc-common.js @@ -68,7 +68,7 @@ exports.quickFailureTest = function( name, identifier, action ) return; } - _self.fail( "Expected failure" ); + _self.fail( false, true, "Expected failure" ); }; @@ -124,7 +124,7 @@ exports.quickKeywordTest = function( } else { - this.assertDoesNotThrow( testfunc, Error ); + this.assertDoesNotThrow( testfunc ); } };