2011-10-22 01:00:17 -04:00
|
|
|
/**
|
|
|
|
* Tests member builder validation rules
|
|
|
|
*
|
2011-12-23 00:09:01 -05:00
|
|
|
* Copyright (C) 2010,2011 Mike Gerwitz
|
2011-10-22 01:00:17 -04:00
|
|
|
*
|
|
|
|
* This file is part of ease.js.
|
|
|
|
*
|
|
|
|
* ease.js is free software: you can redistribute it and/or modify it under the
|
|
|
|
* terms of the GNU Lesser 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 Lesser General Public License
|
|
|
|
* for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*
|
|
|
|
* @author Mike Gerwitz
|
|
|
|
*/
|
|
|
|
|
2011-10-23 01:11:09 -04:00
|
|
|
var shared = require( __dirname + '/inc-common' );
|
|
|
|
|
|
|
|
|
2011-10-22 01:00:17 -04:00
|
|
|
require( 'common' ).testCase(
|
|
|
|
{
|
2011-10-22 16:49:51 -04:00
|
|
|
caseSetUp: function()
|
2011-10-22 01:00:17 -04:00
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
2011-10-22 13:57:17 -04:00
|
|
|
this.quickKeywordMethodTest = function( keywords, identifier, prev )
|
2011-10-22 01:00:17 -04:00
|
|
|
{
|
2011-10-23 01:11:09 -04:00
|
|
|
shared.quickKeywordTest.call( this,
|
|
|
|
'validateMethod', keywords, identifier, prev
|
|
|
|
);
|
2011-10-22 01:00:17 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2011-12-06 18:26:28 -05:00
|
|
|
this.quickFailureTest = function()
|
|
|
|
{
|
|
|
|
shared.quickFailureTest.apply( _self, arguments );
|
|
|
|
};
|
2011-10-22 01:00:17 -04:00
|
|
|
|
|
|
|
|
2011-12-03 00:30:22 -05:00
|
|
|
this.quickVisChangeTest = function( start, override, failtest, failstr )
|
2011-10-22 01:00:17 -04:00
|
|
|
{
|
2011-10-29 10:08:08 -04:00
|
|
|
shared.quickVisChangeTest.call( _self, start, override, failtest,
|
|
|
|
function( name, startobj, overrideobj )
|
|
|
|
{
|
|
|
|
startobj.virtual = true;
|
|
|
|
overrideobj.override = true;
|
|
|
|
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name,
|
|
|
|
function() {},
|
|
|
|
overrideobj,
|
|
|
|
{ member: function() {} },
|
|
|
|
startobj
|
|
|
|
);
|
2011-12-03 00:30:22 -05:00
|
|
|
},
|
|
|
|
failstr
|
2011-10-29 10:08:08 -04:00
|
|
|
);
|
2011-10-22 01:00:17 -04:00
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2011-10-22 16:49:51 -04:00
|
|
|
setUp: function()
|
|
|
|
{
|
2011-11-05 11:56:28 -04:00
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
// can be used to intercept warnings; redefine in test
|
|
|
|
this.warningHandler = function( warning ) {};
|
|
|
|
|
|
|
|
this.sut = this.require( 'MemberBuilderValidator' )(
|
|
|
|
function( warning )
|
|
|
|
{
|
|
|
|
_self.warningHandler( warning );
|
|
|
|
}
|
|
|
|
);
|
2011-10-22 16:49:51 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
|
2011-10-22 01:00:17 -04:00
|
|
|
/**
|
|
|
|
* Private, abstract methods do not make sense. Private methods cannot be
|
|
|
|
* overridden.
|
|
|
|
*/
|
|
|
|
'Method cannot be both private and abstract': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [ 'private', 'abstract' ],
|
|
|
|
'private and abstract'
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Methods (in terms of a class) are always immutable. As such, `const'
|
|
|
|
* would be redundant.
|
|
|
|
*/
|
|
|
|
'Methods cannot be declared const': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [ 'const' ], 'const' );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Virtual static methods do not make sense because static methods can only
|
|
|
|
* be hidden, not overridden.
|
|
|
|
*/
|
|
|
|
'Method cannot be both virtual and static': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [ 'virtual', 'static' ], 'static' );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Getters/setters are treated as properties and should not be able to be
|
|
|
|
* overridden with methods.
|
|
|
|
*/
|
|
|
|
'Cannot override getter/setter with method': function()
|
|
|
|
{
|
|
|
|
var name = 'foo',
|
|
|
|
_self = this;
|
|
|
|
|
|
|
|
// test getter
|
|
|
|
this.quickFailureTest( name, 'getter/setter', function()
|
|
|
|
{
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name, function() {}, {},
|
|
|
|
{ get: function() {} },
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
// test setter
|
|
|
|
this.quickFailureTest( name, 'getter/setter', function()
|
|
|
|
{
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name, function() {}, {},
|
|
|
|
{ set: function() {} },
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Although a function can certainly be assigned to a property, we cannot
|
|
|
|
* allow /declaring/ a method in place of a parent property, as that alters
|
2011-10-22 17:21:59 -04:00
|
|
|
* the interface. One may still assign a callback or other function to a
|
|
|
|
* property after instantiation.
|
2011-10-22 01:00:17 -04:00
|
|
|
*/
|
|
|
|
'Cannot override property with method': function()
|
|
|
|
{
|
|
|
|
var name = 'foo',
|
|
|
|
_self = this;
|
|
|
|
|
|
|
|
this.quickFailureTest( name, 'property', function()
|
|
|
|
{
|
|
|
|
// attempt to override a property
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name, function() {}, {},
|
|
|
|
{ member: 'immaprop' },
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The `virtual' keyword denotes a method that may be overridden. Without
|
|
|
|
* it, we should not allow overriding.
|
|
|
|
*/
|
|
|
|
'Cannot override non-virtual methods': function()
|
|
|
|
{
|
2011-10-22 16:37:07 -04:00
|
|
|
this.quickKeywordMethodTest( [ 'override' ], 'non-virtual', [] );
|
2011-10-22 01:00:17 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
|
2011-10-22 16:47:09 -04:00
|
|
|
/**
|
|
|
|
* Ensure we do not prevent legitimate method overriding
|
|
|
|
*/
|
2011-10-22 17:21:59 -04:00
|
|
|
'Can override virtual method with concrete method': function()
|
2011-10-22 16:47:09 -04:00
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [ 'override' ], null, [ 'virtual' ] );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2011-10-22 17:21:59 -04:00
|
|
|
/**
|
|
|
|
* Overriding a method in ease.js does not immediately make it virtual.
|
|
|
|
* Rather, the virtual keyword must be explicitly specified. Let's ensure
|
|
|
|
* that it is permitted.
|
|
|
|
*/
|
|
|
|
'Can declare override as virtual': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [ 'virtual', 'override' ] );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
2011-10-22 01:00:17 -04:00
|
|
|
/**
|
|
|
|
* Abstract methods act as a sort of placeholder, requiring an
|
|
|
|
* implementation. Once an implementation has been defined, it does not make
|
|
|
|
* sense (in the context of inheritance) to remove it entirely by reverting
|
|
|
|
* back to an abstract method.
|
|
|
|
*/
|
|
|
|
'Cannot override concrete method with abstract method': function()
|
|
|
|
{
|
2011-10-22 16:37:07 -04:00
|
|
|
this.quickKeywordMethodTest( [ 'abstract' ], 'concrete', [] );
|
2011-10-22 01:00:17 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The parameter list is part of the class interface. Changing the length
|
|
|
|
* will make the interface incompatible with that of its parent and make
|
|
|
|
* polymorphism difficult. However, since all parameters in JS are
|
|
|
|
* technically optional, we can permit extending the parameter list (which
|
|
|
|
* itself has its dangers since the compiler cannot detect type errors).
|
|
|
|
*/
|
|
|
|
'Override parameter list must match or exceed parent length': function()
|
|
|
|
{
|
|
|
|
var name = 'foo',
|
|
|
|
_self = this;
|
|
|
|
|
|
|
|
// check with parent with three params
|
|
|
|
this.quickFailureTest( name, 'compatible', function()
|
|
|
|
{
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name,
|
|
|
|
function() {},
|
|
|
|
{ 'override': true },
|
2011-12-06 18:28:16 -05:00
|
|
|
// this function returns each of its arguments, otherwise
|
|
|
|
// they'll be optimized away by Closure Compiler.
|
|
|
|
{ member: function( a, b, c ) { return [a,b,c]; } },
|
2011-10-22 01:00:17 -04:00
|
|
|
{ 'virtual': true }
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
// also check with __length property (XXX: testing too closely to the
|
|
|
|
// implementation; provide abstraction)
|
|
|
|
this.quickFailureTest( name, 'compatible', function()
|
|
|
|
{
|
|
|
|
var parent_method = function() {};
|
|
|
|
parent_method.__length = 3;
|
|
|
|
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name,
|
|
|
|
function() {},
|
|
|
|
{ 'override': true },
|
|
|
|
{ member: parent_method },
|
|
|
|
{ 'virtual': true }
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
|
|
|
|
// finally, check __length of override will actually work (no error)
|
|
|
|
this.assertDoesNotThrow( function()
|
|
|
|
{
|
|
|
|
var method = function() {};
|
|
|
|
method.__length = 3;
|
|
|
|
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name,
|
|
|
|
method,
|
|
|
|
{ 'override': true },
|
|
|
|
{ member: function( a, b, c ) {} },
|
|
|
|
{ 'virtual': true }
|
|
|
|
);
|
|
|
|
}, Error );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 public. Again - the reason being interface
|
|
|
|
* consistency. Otherwise the concept of polymorphism doesn't work.
|
|
|
|
*/
|
|
|
|
'Methods do not support visibiliy de-escalation': function()
|
|
|
|
{
|
|
|
|
this.quickVisChangeTest( 'public', 'protected', true );
|
|
|
|
this.quickVisChangeTest( 'protected', 'private', true );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* To ensure we don't have a bug in our validation, let's also test the
|
|
|
|
* reverse - ensure that we support escalation and staying at the same
|
|
|
|
* level.
|
|
|
|
*/
|
|
|
|
'Methods support visibility escalation or equality': function()
|
|
|
|
{
|
2011-10-28 20:22:14 -04:00
|
|
|
var _self = this;
|
|
|
|
shared.visEscalationTest( function( cur )
|
2011-10-22 01:00:17 -04:00
|
|
|
{
|
2011-10-28 20:22:14 -04:00
|
|
|
_self.quickVisChangeTest( cur[ 0 ], cur[ 1 ], false );
|
|
|
|
} );
|
2011-10-22 01:00:17 -04:00
|
|
|
},
|
2011-10-22 13:57:17 -04:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If a parent method is defined and the 'override' keyword is not provided,
|
|
|
|
* regardless of whether or not it is declared as virtual, we need to
|
|
|
|
* provide an error.
|
|
|
|
*
|
|
|
|
* Note: In the future, this will be replaced with the method hiding
|
|
|
|
* implementation.
|
|
|
|
*/
|
|
|
|
'Must provide "override" keyword when overriding methods': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [], 'override', [] );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Building off of the previous test - we should be able to omit the
|
|
|
|
* 'override' keyword if we are providing a concrete method for an abstract
|
|
|
|
* method. In terms of ease.js, this is still "overriding".
|
|
|
|
*/
|
|
|
|
'Can provide abstract method impl. without override keyword': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [], null, [ 'abstract' ] );
|
|
|
|
},
|
2011-11-05 11:56:28 -04:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If a developer uses the 'override' keyword when there is no super method
|
|
|
|
* to override, this could hint at a number of problems, including:
|
|
|
|
* - Misunderstanding the keyword
|
|
|
|
* - Misspelling the method name
|
|
|
|
* - Forgetting to specify a class to extend from
|
|
|
|
*
|
|
|
|
* All of the above possibilities are pretty significant. In order to safe
|
|
|
|
* developers from themselves (everyone screws up eventually), let's provide
|
|
|
|
* a warning. Since this only hints at a potential bug but does not affect
|
|
|
|
* the functionality, there's no use in throwing an error and preventing the
|
|
|
|
* class from being defined.
|
|
|
|
*/
|
|
|
|
'Throws warning when using override with no super method': function()
|
|
|
|
{
|
|
|
|
var given = null;
|
|
|
|
|
|
|
|
this.warningHandler = function( warning )
|
|
|
|
{
|
|
|
|
given = warning;
|
|
|
|
};
|
|
|
|
|
|
|
|
// trigger warning (override keyword with no super method)
|
|
|
|
this.quickKeywordMethodTest( [ 'override' ] );
|
|
|
|
|
|
|
|
this.assertNotEqual( null, given,
|
|
|
|
'No warning was provided'
|
|
|
|
);
|
|
|
|
|
|
|
|
this.assertOk( given instanceof Error,
|
|
|
|
'Provided warning is not of type Error'
|
|
|
|
);
|
|
|
|
|
|
|
|
this.assertOk( ( given.message.search( shared.testName ) > -1 ),
|
|
|
|
'Override warning should contain method name'
|
|
|
|
);
|
|
|
|
},
|
2011-12-03 00:30:22 -05:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wait - what? That doesn't make sense from an OOP perspective, now does
|
|
|
|
* it! Unfortunately, we're forced into this restriction in order to
|
|
|
|
* properly support fallback to pre-ES5 environments where the visibility
|
|
|
|
* object is a single layer, rather than three. With this impl, all members
|
|
|
|
* are public and private name conflicts would result in supertypes and
|
|
|
|
* subtypes altering eachothers' private members (see manual for more
|
|
|
|
* information).
|
|
|
|
*/
|
|
|
|
'Cannot redeclare private members in subtypes': function()
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
shared.privateNamingConflictTest( function( cur )
|
|
|
|
{
|
|
|
|
_self.quickVisChangeTest( cur[ 0 ], cur[ 1 ], true, 'conflict' );
|
|
|
|
} );
|
|
|
|
},
|
Added `proxy' keyword support
The concept of proxy methods will become an important, core concept in ease.js
that will provide strong benefits for creating decorators and proxies, removing
boilerplate code and providing useful metadata to the system. Consider the
following example:
Class( 'Foo',
{
// ...
'public performOperation': function( bar )
{
this._doSomethingWith( bar );
return this;
},
} );
Class( 'FooDecorator',
{
'private _foo': null,
// ...
'public performOperation': function( bar )
{
return this._foo.performOperation( bar );
},
} );
In the above example, `FooDecorator` is a decorator for `Foo`. Assume that the
`getValueOf()` method is undecorated and simply needs to be proxied to its
component --- an instance of `Foo`. (It is not uncommon that a decorator, proxy,
or related class will alter certain functionality while leaving much of it
unchanged.) In order to do so, we can use this generic, boilerplate code
return this.obj.func.apply( this.obj, arguments );
which would need to be repeated again and again for *each method that needs to
be proxied*. We also have another problem --- `Foo.getValueOf()` returns
*itself*, which `FooDecorator` *also* returns. This breaks encapsulation, so we
instead need to return ourself:
'public performOperation': function( bar )
{
this._foo.performOperation( bar );
return this;
},
Our boilerplate code then becomes:
var ret = this.obj.func.apply( this.obj, arguments );
return ( ret === this.obj )
? this
: ret;
Alternatively, we could use the `proxy' keyword:
Class( 'FooDecorator2',
{
'private _foo': null,
// ...
'public proxy performOperation': '_foo',
} );
`FooDecorator2.getValueOf()` and `FooDecorator.getValueOf()` both perform the
exact same task --- proxy the entire call to another object and return its
result, unless the result is the component, in which case the decorator itself
is returned.
Proxies, as of this commit, accomplish the following:
- All arguments are forwarded to the destination
- The return value is forwarded to the caller
- If the destination returns a reference to itself, it will be replaced with
a reference to the caller's context (`this`).
- If the call is expected to fail, either because the destination is not an
object or because the requested method is not a function, a useful error
will be immediately thrown (rather than the potentially cryptic one that
would otherwise result, requiring analysis of the stack trace).
N.B. As of this commit, static proxies do not yet function properly.
2012-05-02 13:26:47 -04:00
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Proxies forward calls to other properties of a given instance. The only
|
|
|
|
* way to represent those properties is by name, which we will use a string
|
|
|
|
* to accomplish. Therefore, the value of a proxy method must be the name of
|
|
|
|
* the property to proxy to (as a string).
|
|
|
|
*/
|
|
|
|
"`proxy' keyword must provide string value": function()
|
|
|
|
{
|
|
|
|
var name = 'foo',
|
|
|
|
_self = this;
|
|
|
|
|
|
|
|
this.quickFailureTest( name, 'string value expected', function()
|
|
|
|
{
|
|
|
|
// provide function instead of string
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
name, function() {}, { 'proxy': true }, {}, {}
|
|
|
|
);
|
|
|
|
} );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Similar to the above test, but asserts that string values are permitted.
|
|
|
|
*/
|
|
|
|
"`proxy' keyword can provide string value": function()
|
|
|
|
{
|
|
|
|
var _self = this;
|
|
|
|
|
|
|
|
this.assertDoesNotThrow( function()
|
|
|
|
{
|
|
|
|
_self.sut.validateMethod(
|
|
|
|
'foo', 'dest', { 'proxy': true }, {}, {}
|
|
|
|
);
|
|
|
|
}, TypeError );
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* It does not make sense for a proxy to be abstract; proxies are concrete
|
|
|
|
* by definition (in ease.js' context, at least).
|
|
|
|
*/
|
|
|
|
'Method proxy cannot be abstract': function()
|
|
|
|
{
|
|
|
|
this.quickKeywordMethodTest( [ 'proxy', 'abstract' ],
|
|
|
|
'cannot be abstract'
|
|
|
|
);
|
|
|
|
},
|
2011-10-22 01:00:17 -04:00
|
|
|
} );
|
|
|
|
|