1
0
Fork 0

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.
perfodd
Mike Gerwitz 2014-01-26 00:08:42 -05:00
parent 18ac37c871
commit 548c38503f
7 changed files with 176 additions and 25 deletions

View File

@ -525,11 +525,21 @@ exports.prototype.buildMembers = function buildMembers(
} }
} }
_self._memberBuilder.buildMethod( var used = _self._memberBuilder.buildMethod(
dest, null, name, func, keywords, instLookup, dest, null, name, func, keywords, instLookup,
class_id, base 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 ) if ( is_abstract )
{ {
abstract_methods[ name ] = true; abstract_methods[ name ] = true;

View File

@ -140,7 +140,6 @@ exports.buildMethod = function(
} }
else if ( prev ) else if ( prev )
{ {
if ( keywords[ 'override' ] || prev_keywords[ 'abstract' ] ) if ( keywords[ 'override' ] || prev_keywords[ 'abstract' ] )
{ {
// override the method // override the method
@ -148,6 +147,12 @@ exports.buildMethod = function(
prev, value, instCallback, cid 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 else
{ {
// by default, perform method hiding, even if the keyword was not // 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) // store keywords for later reference (needed for pre-ES5 fallback)
dest[ name ].___$$keywords$$ = keywords; dest[ name ].___$$keywords$$ = keywords;
return true;
}; };

View File

@ -138,8 +138,12 @@ exports.prototype.validateMethod = function(
); );
} }
// do not allow overriding concrete methods with abstract // do not allow overriding concrete methods with abstract unless the
if ( keywords[ 'abstract' ] && !( prev_keywords[ 'abstract' ] ) ) // abstract method is weak
if ( keywords[ 'abstract' ]
&& !( keywords.weak )
&& !( prev_keywords[ 'abstract' ] )
)
{ {
throw TypeError( throw TypeError(
"Cannot override concrete method '" + name + "' with " + "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 // ensure parameter list is at least the length of its supertype
if ( ( value.__length || value.length ) if ( ( lennow.__length || lennow.length )
< ( prev.__length || prev.length ) < ( lenprev.__length || lenprev.length )
) )
{ {
throw TypeError( throw TypeError(
@ -168,10 +182,13 @@ exports.prototype.validateMethod = function(
); );
} }
// Disallow overriding method without override keyword (unless parent // Disallow overriding method without override keyword (unless
// method is abstract). In the future, this will provide a warning to // parent method is abstract). In the future, this will provide a
// default to method hiding. // warning to default to method hiding. Note the check for a
if ( !( keywords[ 'override' ] || prev_keywords[ 'abstract' ] ) ) if ( !( keywords[ 'override' ]
|| prev_keywords[ 'abstract' ]
|| ( keywords[ 'abstract' ] && keywords.weak )
) )
{ {
throw TypeError( throw TypeError(
"Attempting to override method '" + name + "Attempting to override method '" + name +

View File

@ -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 * Creates an abstract class implementing the given members
* *
* Simply wraps the class module's implement() method. * Simply wraps the class module's implement() method.
* *
* @return {Object} abstract class * @return {Object} staged abstract class
*/ */
exports.implement = function() exports.implement = function()
{ {
var impl = Class.implement.apply( this, arguments ); return abstractOverride(
Class.implement.apply( this, arguments )
abstractOverride( impl ); );
return impl;
}; };
@ -112,8 +124,8 @@ function abstractOverride( obj )
var extend = obj.extend, var extend = obj.extend,
impl = obj.implement; impl = obj.implement;
// wrap and apply the abstract flag, only if the method is defined (it may // wrap and apply the abstract flag, only if the method is defined (it
// not be under all circumstances, e.g. after an implement()) // may not be under all circumstances, e.g. after an implement())
impl && ( obj.implement = function() impl && ( obj.implement = function()
{ {
return abstractOverride( impl.apply( this, arguments ) ); return abstractOverride( impl.apply( this, arguments ) );

View File

@ -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 ); _self.testArgs( arguments, name, value, keywords );
}; };
this.sut.buildMethod( this.assertOk( this.sut.buildMethod(
this.members, {}, name, value, keywords, function() {}, 1, {} this.members, {}, name, value, keywords, function() {}, 1, {}
); ) );
this.assertEqual( true, called, 'validateMethod() was not called' ); this.assertEqual( true, called, 'validateMethod() was not called' );
}, },
@ -133,9 +137,9 @@ require( 'common' ).testCase(
_self.testArgs( arguments, name, value, keywords ); _self.testArgs( arguments, name, value, keywords );
}; };
this.sut.buildMethod( this.assertOk( this.sut.buildMethod(
this.members, {}, name, value, keywords, function() {}, 1, {} this.members, {}, name, value, keywords, function() {}, 1, {}
); ) );
this.assertEqual( true, called, 'validateMethod() was not called' ); this.assertEqual( true, called, 'validateMethod() was not called' );
}, },
@ -159,9 +163,9 @@ require( 'common' ).testCase(
; ;
// build the proxy // build the proxy
this.sut.buildMethod( this.assertOk( this.sut.buildMethod(
this.members, {}, name, value, keywords, instCallback, cid, {} this.members, {}, name, value, keywords, instCallback, cid, {}
); ) );
this.assertNotEqual( null, this.proxyFactoryCall, this.assertNotEqual( null, this.proxyFactoryCall,
"Proxy factory should be used when `proxy' keyword is provided" "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" "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" );
},
} ); } );

View File

@ -27,6 +27,7 @@ require( 'common' ).testCase(
caseSetUp: function() caseSetUp: function()
{ {
var _self = this; var _self = this;
this.util = this.require( 'util' );
this.quickKeywordMethodTest = function( keywords, identifier, prev ) 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 * The parameter list is part of the class interface. Changing the length
* will make the interface incompatible with that of its parent and make * 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 * 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 * previously been declared protected, or declare it as protected if it has

View File

@ -68,7 +68,7 @@ exports.quickFailureTest = function( name, identifier, action )
return; return;
} }
_self.fail( "Expected failure" ); _self.fail( false, true, "Expected failure" );
}; };
@ -124,7 +124,7 @@ exports.quickKeywordTest = function(
} }
else else
{ {
this.assertDoesNotThrow( testfunc, Error ); this.assertDoesNotThrow( testfunc );
} }
}; };