1
0
Fork 0

[#19] Implemented 'virtual' keyword

- Baby steps. 'override' keyword is not yet necessary.
- Final not yet removed
closure/master
Mike Gerwitz 2011-06-08 01:11:53 -04:00
parent 24a04369ae
commit 8b83e85c43
7 changed files with 177 additions and 24 deletions

View File

@ -164,10 +164,13 @@ function validateMethod( keywords, prev_data, value, name )
); );
} }
// final methods cannot be overridden // Can only override virtual methods. Abstract methods are considered to
if ( prev_keywords[ 'final' ] ) // be virtual.
if ( !( prev_keywords[ 'virtual' ] || prev_keywords[ 'abstract' ] ) )
{ {
throw TypeError( "Cannot override final method '" + name + "'" ); throw TypeError(
"Cannot override non-virtual method '" + name + "'"
);
} }
// do not allow overriding concrete methods with abstract // do not allow overriding concrete methods with abstract
@ -391,10 +394,16 @@ function scanMembers( members, name, base )
{ {
if ( member = members[ visibility[ i ] ][ name ] ) if ( member = members[ visibility[ i ] ][ name ] )
{ {
return { // We need to filter out base properties (such as
member: member, // Object.prototype.toString()), but we still need to traverse the
visibility: ( ( fallback ) ? 0 : i ), // prototype chain. As such, we cannot use hasOwnProperty().
}; if ( member !== Object.prototype[ name ] )
{
return {
member: member,
visibility: ( ( fallback ) ? 0 : i ),
};
}
} }
} }

View File

@ -34,6 +34,7 @@ var _keywords = {
'final': true, 'final': true,
'abstract': true, 'abstract': true,
'const': true, 'const': true,
'virtual': true,
}; };

View File

@ -106,6 +106,28 @@ var common = require( './common' ),
} )(); } )();
/**
* Abstract methods should remain virtual until they are overridden. That is, if
* a subtype doesn't provide a concrete implementation, it should still be
* considered virtual.
*/
( function testAbstractMethodsCanBeOverriddenBySubSubTypes()
{
var AbstractFoo = AbstractClass( 'Foo',
{
'abstract foo': [],
} ),
SubAbstractFoo = AbstractClass.extend( AbstractFoo, {} ),
ConcreteFoo = Class.extend( SubAbstractFoo,
{
'foo': function() {},
} )
;
} )();
/** /**
* Just as Class contains an extend method, so should AbstractClass. * Just as Class contains an extend method, so should AbstractClass.
*/ */

View File

@ -175,7 +175,8 @@ var Type = Interface.extend( {
foo2: function() {}, foo2: function() {},
}), }),
concrete_inst = new ConcreteFoo(); concrete_inst = ConcreteFoo()
;
assert.ok( assert.ok(
( concrete_inst.isInstanceOf( Type ) ( concrete_inst.isInstanceOf( Type )

View File

@ -34,13 +34,13 @@ var common = require( './common' ),
Foo = Class.extend( Foo = Class.extend(
{ {
myMethod: function() 'virtual myMethod': function()
{ {
hitMethod = true; hitMethod = true;
return this; return this;
}, },
myMethod2: function( arg ) 'virtual myMethod2': function( arg )
{ {
hitMethod2 = true; hitMethod2 = true;
method2Arg = arg; method2Arg = arg;

View File

@ -47,7 +47,7 @@ var common = require( './common' ),
'private privf': privf, 'private privf': privf,
'public getProp': function( name ) 'virtual public getProp': function( name )
{ {
// return property, allowing us to break encapsulation for // return property, allowing us to break encapsulation for
// protected/private properties (for testing purposes) // protected/private properties (for testing purposes)
@ -79,7 +79,7 @@ var common = require( './common' ),
}, },
'public getSelfOverride': function() 'virtual public getSelfOverride': function()
{ {
// override me // override me
}, },
@ -516,7 +516,7 @@ var common = require( './common' ),
Class( Class(
{ {
'protected foo': 'bar', 'protected foo': 'bar',
'protected baz': function() {}, 'virtual protected baz': function() {},
} ).extend( { } ).extend( {
'public foo': 'bar', 'public foo': 'bar',
'public baz': function() {}, 'public baz': function() {},
@ -529,7 +529,7 @@ var common = require( './common' ),
Class( Class(
{ {
'protected foo': 'bar', 'protected foo': 'bar',
'protected baz': function() {}, 'virtual protected baz': function() {},
} ).extend( { } ).extend( {
'protected foo': 'bar', 'protected foo': 'bar',
'protected baz': function() {}, 'protected baz': function() {},
@ -621,7 +621,7 @@ var common = require( './common' ),
{ {
var val = 'foobar', var val = 'foobar',
result = Class( { result = Class( {
'protected foo': function() 'virtual protected foo': function()
{ {
return val; return val;
}, },
@ -672,7 +672,7 @@ var common = require( './common' ),
// get the result of invoking overridden foo() // get the result of invoking overridden foo()
var result = Class( var result = Class(
{ {
'protected foo': function() 'virtual protected foo': function()
{ {
return false; return false;
}, },
@ -739,7 +739,7 @@ var common = require( './common' ),
var val = 'foobar', var val = 'foobar',
result = Class( result = Class(
{ {
'protected foo': function() {}, 'virtual protected foo': function() {},
} ).extend( } ).extend(
{ {
// provide concrete implementation // provide concrete implementation

View File

@ -65,6 +65,122 @@ mb_common.assertCommon();
} )(); } )();
/**
* Unlike Java, PHP, Python and similar languages, methods in ease.js are *not*
* virtual by default. In order to make them override-able, the 'virtual'
* keyword must be specified for that method in the supertype.
*
* Therefore, let's ensure that non-virtual methods cannot be overridden.
*/
( function testCannotOverrideNonVirtualMethod()
{
mb_common.value = function() {};
mb_common.buildMemberQuick();
try
{
// attempt to override (should throw exception; non-virtual)
mb_common.buildMemberQuick( {}, true );
// should not get to this point
assert.fail( "Should not be able to override non-virtual method" );
}
catch ( e )
{
// ensure we have the correct error
assert.ok(
e.message.search( 'virtual' ) !== -1,
"Error message for non-virtual override should mention virtual"
);
assert.ok(
e.message.search( mb_common.name ) !== -1,
"Method name should be provided in non-virtual error message"
);
}
} )();
/**
* Working off of what was said in the test directly above, we *should* be able
* to override virtual methods.
*/
( function testCanOverrideVirtualMethods()
{
// build a virtual method
mb_common.value = function() {};
mb_common.buildMemberQuick( { 'virtual': true } );
// attempt to override it
assert.doesNotThrow( function()
{
mb_common.buildMemberQuick( {}, true );
}, Error, "Should be able to override virtual methods" );
} )();
/**
* Unlike languages like C++, ease.js does not automatically mark overridden
* methods as virtual. C# and some other languages offer a 'seal' keyword or
* similar in order to make overridden methods non-virtual. In that sense,
* ease.js will "seal" overrides by default.
*/
( function testOverriddenMethodsAreNotVirtualByDefault()
{
// build a virtual method
mb_common.value = function() {};
mb_common.buildMemberQuick( { 'virtual': true } );
// override it (non-virtual)
mb_common.buildMemberQuick( {}, true );
// attempt to override again (should fail)
assert.throws( function()
{
mb_common.buildMemberQuick( {}, true );
}, TypeError, "Overrides are not declared as virtual by default" );
} )();
/**
* Given the test directly above, we can therefore assume that it should be
* permitted to declare overridden methods as virtual.
*/
( function testCanDeclareOverridesAsVirtual()
{
// build a virtual method
mb_common.value = function() {};
mb_common.buildMemberQuick( { 'virtual': true } );
// override it (virtual)
mb_common.buildMemberQuick( { 'virtual': true }, true );
// attempt to override again
assert.doesNotThrow( function()
{
mb_common.buildMemberQuick( {}, true );
}, Error, "Can override an override if declared virtual" );
} )();
/**
* Abstract members exist to be overridden. As such, they should be considered
* virtual.
*/
( function testAbstractMethodsAreConsideredVirtual()
{
// build abstract method
mb_common.value = function() {};
mb_common.buildMemberQuick( { 'abstract': true } );
// we should be able to override it
assert.doesNotThrow( function()
{
mb_common.buildMemberQuick( {}, true );
}, Error, "Can overrde abstract methods" );
} )();
/** /**
* To ensure interfaces of subtypes remain compatible with that of their * To ensure interfaces of subtypes remain compatible with that of their
* supertypes, the parameter lists must match and build upon each other. * supertypes, the parameter lists must match and build upon each other.
@ -72,17 +188,17 @@ mb_common.assertCommon();
( function testMethodOverridesMustHaveEqualOrGreaterParameters() ( function testMethodOverridesMustHaveEqualOrGreaterParameters()
{ {
mb_common.value = function( one, two ) {}; mb_common.value = function( one, two ) {};
mb_common.buildMemberQuick(); mb_common.buildMemberQuick( { 'virtual': true } );
assert.doesNotThrow( function() assert.doesNotThrow( function()
{ {
mb_common.buildMemberQuick( {}, true ); mb_common.buildMemberQuick( { 'virtual': true }, true );
}, TypeError, "Method can have equal number of parameters" ); }, TypeError, "Method can have equal number of parameters" );
assert.doesNotThrow( function() assert.doesNotThrow( function()
{ {
mb_common.value = function( one, two, three ) {}; mb_common.value = function( one, two, three ) {};
mb_common.buildMemberQuick( {}, true ); mb_common.buildMemberQuick( { 'virtual': true }, true );
}, TypeError, "Method can have greater number of parameters" ); }, TypeError, "Method can have greater number of parameters" );
assert.throws( function() assert.throws( function()
@ -112,7 +228,7 @@ mb_common.assertCommon();
orig_called = true; orig_called = true;
}; };
mb_common.buildMemberQuick(); mb_common.buildMemberQuick( { 'virtual': true } );
// override method // override method
mb_common.value = function() mb_common.value = function()
@ -169,6 +285,10 @@ mb_common.assertCommon();
super_called = true; super_called = true;
}; };
// XXX: Bad idea. Set the keyword in another manner. This is likely to break
// in the future.
members['public'].foo.___$$keywords$$ = { 'virtual': true };
// override // override
builder.buildMethod( members, {}, 'foo', newfunc, {}, instCallback ); builder.buildMethod( members, {}, 'foo', newfunc, {}, instCallback );
@ -236,7 +356,7 @@ mb_common.assertCommon();
return instance; return instance;
}, },
members = { 'public': {}, 'protected': {}, 'private': {} } members = builder.initMembers()
; ;
// set instance values // set instance values
@ -249,7 +369,7 @@ mb_common.assertCommon();
exports.meta, exports.meta,
'func', 'func',
func, func,
[ 'public' ], { 'virtual': true },
instCallback instCallback
); );
@ -265,7 +385,7 @@ mb_common.assertCommon();
exports.meta, exports.meta,
'func', 'func',
func2, func2,
[ 'public' ], {},
instCallback instCallback
); );