From 9690663d1cea836643194e721d649bb99ed97e96 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Sun, 22 May 2011 11:11:18 -0400 Subject: [PATCH] Added support for final classes - This commit was originally many. Unfortunately, certain Git objects became corrupt shortly after my 500th commit due to HDD issues. Due to the scope, I was unable to recover the set of commits I needed (after an hour of trying every method). - Fortunately, vim's swap files came to the rescue. Had I been able to properly shut down my PC, I would have been rather frustrated. --- lib/class_builder.js | 29 ++++++++++++++ lib/class_final.js | 41 +++++++++++++++++++ lib/member_builder.js | 23 ++++++++--- test/test-class_builder-final.js | 69 +++++++++++++++++++++++++++++++- tools/combine | 2 +- 5 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 lib/class_final.js diff --git a/lib/class_builder.js b/lib/class_builder.js index 4c3eee6..21e9732 100644 --- a/lib/class_builder.js +++ b/lib/class_builder.js @@ -200,6 +200,15 @@ exports.build = function extend() || { __length: 0 } ; + // prevent extending final classes + if ( base.___$$final$$ === true ) + { + throw Error( + "Cannot extend final class " + + ( base.___$$meta$$.name || '(anonymous)' ) + ); + } + // grab the name, if one was provided if ( cname = props.__name ) { @@ -252,6 +261,8 @@ exports.build = function extend() new_class.___$$methods$$ = members; new_class.___$$sinit$$ = staticInit; + attachFlags( new_class, props ); + // We reduce the overall cost of this definition by defining it on the // prototype rather than during instantiation. While this does increase the // amount of time it takes to access the property through the prototype @@ -1032,3 +1043,21 @@ function attachId( ctor, id ) util.defineSecureProp( ctor.prototype, '__cid', id ); } + +/** + * Sets class flags + * + * @param {Class} ctor class to flag + * @param {Object} props class properties + * + * @return {undefined} + */ +function attachFlags( ctor, props ) +{ + ctor.___$$final$$ = !!( props.___$$final$$ ); + + // The properties are no longer needed. Set to undefined rather than delete + // (v8 performance) + props.___$$final$$ = undefined; +} + diff --git a/lib/class_final.js b/lib/class_final.js new file mode 100644 index 0000000..313e1f7 --- /dev/null +++ b/lib/class_final.js @@ -0,0 +1,41 @@ +/** + * Wrapper permitting the definition of final classes + * + * Copyright (C) 2010 Mike Gerwitz + * + * 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 . + * + * @author Mike Gerwitz + * @package core + */ + +var Class = require( __dirname + '/class' ); + +module.exports = function() +{ + // the last argument _should_ be the definition + var dfn = arguments[ arguments.length - 1 ]; + + if ( typeof dfn === 'object' ) + { + // mark it as final + dfn.___$$final$$ = true; + } + + // forward everything to Class + return Class.apply( this, arguments ); +}; + diff --git a/lib/member_builder.js b/lib/member_builder.js index ed14d2e..559e6bc 100644 --- a/lib/member_builder.js +++ b/lib/member_builder.js @@ -81,13 +81,24 @@ exports.buildMethod = function( : {} ; - // do not permit private abstract methods (doesn't make sense, since - // they cannot be inherited/overridden) - if ( keywords[ 'abstract' ] && keywords[ 'private' ] ) + if ( keywords[ 'abstract' ] ) { - throw TypeError( - "Method '" + name + "' cannot be both private and abstract" - ); + // do not permit private abstract methods (doesn't make sense, since + // they cannot be inherited/overridden) + if ( keywords[ 'private' ] ) + { + throw TypeError( + "Method '" + name + "' cannot be both private and abstract" + ); + } + + // abstract final also does not make sense + if ( keywords[ 'final' ] ) + { + throw TypeError( + "Method '" + name + "' cannot be both abstract and final" + ); + } } // const doesn't make sense for methods; they're always immutable diff --git a/test/test-class_builder-final.js b/test/test-class_builder-final.js index e689ed6..648495e 100644 --- a/test/test-class_builder-final.js +++ b/test/test-class_builder-final.js @@ -24,7 +24,10 @@ var common = require( './common' ), assert = require( 'assert' ), - builder = common.require( 'class_builder' ) + builder = common.require( 'class_builder' ), + + Class = common.require( 'class' ) + FinalClass = common.require( 'class_final' ) ; @@ -83,7 +86,7 @@ var common = require( './common' ), { assert.ok( e.message.search( 'foo' ) !== -1, - "Final property error message contains name of method" + "Final property error message contains name of property" ); return; @@ -92,3 +95,65 @@ var common = require( './common' ), assert.fail( "Should not be able to use final keyword with properties" ); } )(); + +/** + * The 'abstract' keyword's very point is to state that no definition is + * provided and that a subtype must provide one. Therefore, declaring something + * 'abstract final' is rather contradictory and should not be permitted. + */ +( function testFinalyKeywordCannotBeUsedWithAbstract() +{ + try + { + // should fail + builder.build( { 'abstract final foo': [] } ); + } + catch ( e ) + { + assert.ok( + e.message.search( 'foo' ) !== -1, + "Abstract final error message contains name of method" + ); + + return; + } + + assert.fail( "Should not be able to use final keyword with abstract" ); +} )(); + + +/** + * Ensure that FinalClass properly forwards data to create a new Class. + */ +( function testFinalClassesAreValidClasses() +{ + assert.ok( Class.isClass( FinalClass( {} ) ), + "Final classes should generate valid classes" + ); +} )(); + + +/** + * When a class is declared as final, it should prevent it from ever being + * extended. Ever. + */ +( function testFinalClassesCannotBeExtended() +{ + try + { + // this should fail + FinalClass( 'Foo', {} ).extend( {} ); + } + catch ( e ) + { + assert.ok( + e.message.search( 'Foo' ) !== -1, + "Final class error message should contain name of class" + ); + + return; + } + + assert.fail( "Should not be able to extend final classes" ); +} )(); + diff --git a/tools/combine b/tools/combine index 609192f..4cc3b53 100755 --- a/tools/combine +++ b/tools/combine @@ -29,7 +29,7 @@ RMTRAIL="$PATH_TOOLS/rmtrail" # order matters CAT_MODULES="prop_parser util propobj member_builder class_builder" -CAT_MODULES="$CAT_MODULES class interface" +CAT_MODULES="$CAT_MODULES class class_final interface" ## # Output template header