1
0
Fork 0

Visibility de-escalation no longer permitted

closure/master
Mike Gerwitz 2011-03-18 23:42:07 -04:00
parent 9b9bcfd150
commit ce736bea55
4 changed files with 196 additions and 21 deletions

View File

@ -439,9 +439,9 @@ var extend = ( function( extending )
prototype = new base(), prototype = new base(),
cname = '', cname = '',
hasOwn = Array.prototype.hasOwnProperty; hasOwn = Array.prototype.hasOwnProperty,
var properties = {}, properties = {},
prop_init = member_builder.initMembers(), prop_init = member_builder.initMembers(),
members = member_builder.initMembers( prototype ), members = member_builder.initMembers( prototype ),
@ -489,7 +489,7 @@ var extend = ( function( extending )
// build a new property, passing in the other members to compare // build a new property, passing in the other members to compare
// against for preventing nonsensical overrides // against for preventing nonsensical overrides
member_builder.buildProp( member_builder.buildProp(
prop_init, null, name, value, keywords, members prop_init, null, name, value, keywords, base
); );
}, },
@ -511,7 +511,7 @@ var extend = ( function( extending )
{ {
member_builder.buildMethod( member_builder.buildMethod(
members, null, name, func, keywords, getMethodInstance, members, null, name, func, keywords, getMethodInstance,
class_id class_id, base
); );
if ( is_abstract ) if ( is_abstract )
@ -539,8 +539,10 @@ var extend = ( function( extending )
attachPropInit( prototype, prop_init, members, class_id ); attachPropInit( prototype, prop_init, members, class_id );
new_class.prototype = prototype; new_class.prototype = prototype;
new_class.constructor = new_class; new_class.constructor = new_class;
new_class.___$$props$$ = prop_init;
new_class.___$$methods$$ = members;
// important: call after setting prototype // important: call after setting prototype
setupProps( new_class, abstract_methods, class_id ); setupProps( new_class, abstract_methods, class_id );

View File

@ -22,7 +22,8 @@
* @package core * @package core
*/ */
var util = require( './util' ), var util = require( './util' ),
fallback = util.definePropertyFallback(),
visibility = [ 'public', 'protected', 'private' ]; visibility = [ 'public', 'protected', 'private' ];
@ -67,13 +68,16 @@ exports.initMembers = function( mpublic, mprotected, mprivate )
* @return {undefined} * @return {undefined}
*/ */
exports.buildMethod = function( exports.buildMethod = function(
members, meta, name, value, keywords, instCallback, cid members, meta, name, value, keywords, instCallback, cid, base
) )
{ {
var prev; // TODO: We can improve performance by not scanning each one individually
// every time this method is called
var prev_data = scanMembers( members, name, base ),
prev = ( prev_data ) ? prev_data.member : null;
// search for any previous instances of this member // search for any previous instances of this member
if ( prev = scanMembers( members, name ) ) if ( prev )
{ {
// disallow overriding properties with methods // disallow overriding properties with methods
if ( !( prev instanceof Function ) ) if ( !( prev instanceof Function ) )
@ -102,6 +106,14 @@ exports.buildMethod = function(
"with that of its supertype" "with that of its supertype"
); );
} }
// do not permit visibility de-escalation
if ( prev_data.visibility < getVisibilityValue( keywords ) )
{
throw TypeError(
"Cannot de-escalate visibility of method '" + name + "'"
);
}
} }
var dest = getMemberVisibility( members, keywords ); var dest = getMemberVisibility( members, keywords );
@ -138,20 +150,33 @@ exports.buildMethod = function(
* *
* @param {Object.<string,boolean>} keywords parsed keywords * @param {Object.<string,boolean>} keywords parsed keywords
* @param {Object=} cmp optional second member object to scan * @param {Object=} base optional base object to scan
* *
* @return {undefined} * @return {undefined}
*/ */
exports.buildProp = function( members, meta, name, value, keywords, cmp ) exports.buildProp = function( members, meta, name, value, keywords, base )
{ {
// TODO: We can improve performance by not scanning each one individually
// every time this method is called
var prev_data = scanMembers( members, name, base ),
prev = ( prev_data ) ? prev_data.member : null;
// disallow overriding methods with properties // disallow overriding methods with properties
if ( scanMembers( members, name, cmp ) instanceof Function ) if ( prev instanceof Function )
{ {
throw new TypeError( throw new TypeError(
"Cannot override method '" + name + "' with property" "Cannot override method '" + name + "' with property"
); );
} }
// do not permit visibility de-escalation
if ( prev && ( prev_data.visibility < getVisibilityValue( keywords ) ) )
{
throw TypeError(
"Cannot de-escalate visibility of property '" + name + "'"
);
}
getMemberVisibility( members, keywords )[ name ] = value; getMemberVisibility( members, keywords )[ name ] = value;
}; };
@ -262,11 +287,12 @@ function getMemberVisibility( members, keywords )
* @param {{public: Object, protected: Object, private: Object}} members * @param {{public: Object, protected: Object, private: Object}} members
* *
* @param {string} name member to locate * @param {string} name member to locate
* @param {Object=} cmp optional second member object to scan * @param {Object=} base optional base object to scan
* *
* @return {*} member, if located, otherwise undefined * @return {Object} Array of member and number corresponding to visibility,
* level if located, otherwise an empty object
*/ */
function scanMembers( members, name, cmp ) function scanMembers( members, name, base )
{ {
var i = visibility.length, var i = visibility.length,
member = null; member = null;
@ -276,19 +302,29 @@ function scanMembers( members, name, cmp )
{ {
if ( member = members[ visibility[ i ] ][ name ] ) if ( member = members[ visibility[ i ] ][ name ] )
{ {
return member; return {
member: member,
visibility: ( ( fallback ) ? 0 : i ),
};
} }
} }
// if a second comparison object was given, try again using it instead of // if a second comparison object was given, try again using it instead of
// the original members object // the original members object
if ( cmp !== undefined ) if ( base !== undefined )
{ {
return scanMembers( cmp, name ); var base_methods = base.___$$methods$$,
base_props = base.___$$props$$;
// scan the base's methods and properties, if they are available
return ( base_methods && scanMembers( base_methods, name ) )
|| ( base_props && scanMembers( base_props, name ) )
|| null
;
} }
// nothing was found // nothing was found
return undefined; return null;
} }
@ -378,3 +414,34 @@ function overrideMethod( super_method, new_method, instCallback, cid )
return override; return override;
} }
/**
* Return the visibility level as a numeric value, where 0 is public and 2 is
* private
*
* @param {Object} keywords keywords to scan for visibility level
*
* @return {number} visibility level as a numeric value
*/
function getVisibilityValue( keywords )
{
if ( fallback )
{
// if we have to fall back, we don't support levels of visibility
return 0;
}
else if ( keywords[ 'protected' ] )
{
return 1;
}
else if ( keywords[ 'private' ] )
{
return 2;
}
else
{
// default is public
return 0;
}
}

View File

@ -240,7 +240,7 @@ exports.propParse = function( data, options )
// if an 'each' callback was provided, pass the data before parsing it // if an 'each' callback was provided, pass the data before parsing it
if ( callbackEach ) if ( callbackEach )
{ {
callbackEach.call( callbackEach, name, value ); callbackEach.call( callbackEach, name, value, keywords );
} }
// getter/setter // getter/setter

View File

@ -503,3 +503,109 @@ var common = require( './common' ),
); );
} )(); } )();
/**
* Visibility escalation (protected -> private) should be permitted
*/
( function testCanEscalateMemberVisibility()
{
// escalate
assert.doesNotThrow( function()
{
Class(
{
'protected foo': 'bar',
'protected baz': function() {},
} ).extend( {
'public foo': 'bar',
'public baz': function() {},
} );
}, Error, "Can escalate visibility of subtype members" );
// same level of visibility
assert.doesNotThrow( function()
{
Class(
{
'protected foo': 'bar',
'protected baz': function() {},
} ).extend( {
'protected foo': 'bar',
'protected baz': function() {},
} );
}, Error, "Can retain level of visibility for subtype members" );
} )();
/**
* We should /not/ be able to de-escalate member visibility
* (public -> {protected,private}
*/
( function testCannotDeescalateMemberVisibility()
{
// public -> protected
assert.throws( function()
{
Class(
{
'public foo': 'bar',
} ).extend( {
'protected foo': 'bar',
} );
}, Error, "Cannot de-escalate visibility of subtype props to protected" );
assert.throws( function()
{
Class(
{
'public baz': function() {},
} ).extend( {
'protected baz': function() {},
} );
}, Error, "Cannot de-escalate visibility of subtype methods to protected" );
// public -> private
assert.throws( function()
{
Class(
{
'public foo': 'bar',
} ).extend( {
'private foo': 'bar',
} );
}, Error, "Cannot de-escalate visibility of subtype props to private" );
assert.throws( function()
{
Class(
{
'public baz': function() {},
} ).extend( {
'private baz': function() {},
} );
}, Error, "Cannot de-escalate visibility of subtype methods to private" );
// protected -> private
assert.throws( function()
{
Class(
{
'protected foo': 'bar',
} ).extend( {
'private foo': 'bar',
} );
}, Error, "Cannot de-escalate visibility of subtype props to private2" );
assert.throws( function()
{
Class(
{
'protected baz': function() {},
} ).extend( {
'private baz': function() {},
} );
}, Error, "Cannot de-escalate visibility of subtype methods to private2" );
} )();