diff --git a/index.js b/index.js index cd7d1c5..66a3f40 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /** * Provides ease of access to all submodules * - * Copyright (C) 2010, 2011, 2013, 2014 Free Software Foundation, Inc. + * Copyright (C) 2010, 2011, 2013, 2014, 2015 Free Software Foundation, Inc. * * This file is part of GNU ease.js. * @@ -19,10 +19,26 @@ * along with this program. If not, see . */ +/** + * Wrap a prototype using ease.js + * + * This function is the entry point for ease.js; its fields expose all of + * its core features. When invoked, it wraps the given prototype using + * ease.js, producing an ease.js Class. This is more natural when using the + * ECMAScript 6 `class` syntax to define prototypes. + * + * @param {Function} proto prototype to wrap + * + * @return {Function} ease.js Class wrapping PROTO + */ +var exports = module.exports = function( proto ) +{ + return exports.Class.extend( proto, {} ); +}; + exports.Class = require( './lib/class' ); exports.AbstractClass = require( './lib/class_abstract' ); exports.FinalClass = require( './lib/class_final' ); exports.Interface = require( './lib/interface' ); exports.Trait = require( './lib/Trait' ); exports.version = require( './lib/version' ); - diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index cb0331a..1789592 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -28,6 +28,8 @@ var util = require( './util' ), Warning = require( './warn' ).Warning, Symbol = require( './util/Symbol' ), + parseKeywords = require( './prop_parser' ).parseKeywords, + hasOwn = Object.prototype.hasOwnProperty, @@ -50,15 +52,27 @@ var util = require( './util' ), /** * Hash of reserved members * - * These methods cannot be defined in the class. They are for internal use - * only. We must check both properties and methods to ensure that neither is - * defined. + * These methods cannot be defined in the class; they are for internal + * use only. We must check both properties and methods to ensure that + * neither is defined. * * @type {Object.} */ reserved_members = { '__initProps': true, - 'constructor': true, + }, + + /** + * Hash of aliased members + * + * These are members that alias to another. Ideally, this should be a + * very small list. It is useful for introducing features without + * deprecating old. + * + * @type {Object.} + */ + aliased_members = { + 'constructor': '__construct', }, /** @@ -641,12 +655,56 @@ exports.prototype.buildMembers = function buildMembers( parser.method && hjoin( 'method', handlers.method ); } + handlers.keywordParser = _keywordParser; + // parse members and process accumulated member state util.propParse( props, handlers, context ); this._memberBuilder.end( context.state ); } +/** + * Member keyword parser + * + * In reality, this parser is simply intended to override names where there + * are applicable aliases; all keyword parsing is kept to the original + * implementation. + * + * @param {string} prop property to parse + * + * @return {{name: string, bitwords: number, keywords: Object.}} + */ +function _keywordParser( prop ) +{ + var result = parseKeywords( prop ), + alias = _getMemberAlias( result.name ); + + if ( alias !== undefined ) + { + result.name = alias; + } + + return result; +} + + +/** + * Return a member alias for NAME + * + * If NAME has no alias, then the result is `undefined`. + * + * @param {string} name member name + * + * @return {string|undefined} + */ +function _getMemberAlias( name ) +{ + return ( hasOwn.call( aliased_members, name ) ) + ? aliased_members[ name ] + : undefined; +} + + function _parseEach( name, value, keywords ) { var defs = this.defs; diff --git a/lib/prop_parser.js b/lib/prop_parser.js index 6938328..0b43302 100644 --- a/lib/prop_parser.js +++ b/lib/prop_parser.js @@ -1,7 +1,8 @@ /** * Property keyword parser module * - * Copyright (C) 2010, 2011, 2012, 2013, 2014 Free Software Foundation, Inc. + * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015 + * Free Software Foundation, Inc. * * This file is part of GNU ease.js. * @@ -60,7 +61,7 @@ exports.kmasks = _kmasks; * * @param {string} prop property string, which may contain keywords * - * @return {{name: string, keywords: Object.}} + * @return {{name: string, bitwords: number, keywords: Object.}} */ exports.parseKeywords = function ( prop ) { diff --git a/test/Class/ConstructorTest.js b/test/Class/ConstructorTest.js index 78dfd50..d382870 100644 --- a/test/Class/ConstructorTest.js +++ b/test/Class/ConstructorTest.js @@ -1,7 +1,7 @@ /** * Tests class module constructor creation * - * Copyright (C) 2014 Free Software Foundation, Inc. + * Copyright (C) 2014, 2015 Free Software Foundation, Inc. * * This file is part of GNU ease.js. * @@ -21,6 +21,24 @@ require( 'common' ).testCase( { + caseSetUp: function() + { + // ease.js was written long before ES6 drafts began providing class + // support. Now that they do, we should support their constructor + // decision as well as our own. + this.ctors = [ '__construct', 'constructor' ]; + + // we only use ES3 features, thus this + this.mkctor = function( name, f ) + { + var o = {}; + o[ name ] = f; + + return o; + }; + }, + + setUp: function() { this.Sut = this.require( 'class' ); @@ -32,10 +50,15 @@ require( 'common' ).testCase( * defining the class. (Note that the case of ensuring that it is not * called when creating a subtype is handled by the ExtendTest case.) */ - 'Constructor should not be invoked before instantiation': function() + '@each(ctors) Should not be invoked before instantiation': + function( name ) { - var called = false; - this.Sut.extend( { __construct: function() { called = true; } } ); + var called = false, + dfn = {}; + + this.Sut.extend( + this.mkctor( name, function() { called = true; } ) + ); this.assertNotEqual( called, true ); }, @@ -47,14 +70,14 @@ require( 'common' ).testCase( * is instantiated. Further, it should only be called a single time, * which is particularly important if it produces side-effects. */ - 'Constructor should be invoked once upon instantiation': function() + '@each(ctors) Should be invoked once upon instantiation': + function( name ) { var called = 0; var Foo = this.Sut.extend( - { - __construct: function() { called++; } - } ); + this.mkctor( name, function() { called++; } ) + ); // note that we're not yet testing the more consise new-less // invocation style @@ -67,16 +90,19 @@ require( 'common' ).testCase( * Once invoked, the __construct method should be bound to the newly * created instance. */ - 'Constructor should be invoked within context of new instance': - function() + '@each(ctors) Should be invoked within context of new instance': + function( name ) { var expected = Math.random(); - var Foo = this.Sut.extend( - { - val: null, - __construct: function() { this.val = expected; } - } ); + var dfn = this.mkctor( name, function() + { + this.val = expected; + } ); + + dfn.val = null; + + var Foo = this.Sut.extend( dfn ); // if `this' was bound to the instance, then __construct should set // VAL to EXPECTED @@ -90,19 +116,18 @@ require( 'common' ).testCase( * ``class'') should be passed to __construct, unchanged and * uncopied---that is, references should be retained. */ - 'Constructor arguments should be passed unchanged to __construct': - function() + '@each(ctors) Arguments should be passed unchanged to __construct': + function( name ) { var args = [ "foo", { bar: 'baz' }, [ 'moo', 'cow' ] ], given = null; var Foo = this.Sut.extend( - { - __construct: function() + this.mkctor( name, function() { given = Array.prototype.slice.call( arguments, 0 ); - } - } ); + } ) + ); new Foo( args[ 0 ], args[ 1 ], args[ 2 ] ); @@ -125,15 +150,16 @@ require( 'common' ).testCase( * the name __construct---is modelled after PHP; Java classes, for * instance, do not inherit their parents' constructors. */ - 'Parent constructor should be invoked for subtype if not overridden': - function() + '@each(ctors)Parent constructor invoked for subtype if not overridden': + function( name ) { var called = false; - var Sub = this.Sut.extend( - { - __construct: function() { called = true; } - } ).extend( {} ); + var dfn = {}; + dfn[ name ] = function() { called = true; }; + + var Sub = this.Sut.extend( dfn ) + .extend( {} ); new Sub(); this.assertOk( called ); @@ -172,18 +198,22 @@ require( 'common' ).testCase( * __construct, since public is the default and there is no other * option.) */ - '__construct must be public': function() + '@each(ctors) Constructor must be public': function( name ) { var Sut = this.Sut; this.assertThrows( function() { - Sut( { 'protected __construct': function() {} } ); + var dfn = {}; + dfn[ 'protected ' + name ] = function() {}; + Sut( dfn ); }, TypeError, "Constructor should not be able to be protected" ); this.assertThrows( function() { - Sut( { 'private __construct': function() {} } ); + var dfn = {}; + dfn[ 'private ' + name ] = function() {}; + Sut( dfn ); }, TypeError, "Constructor should not be able to be private" ); }, @@ -200,4 +230,35 @@ require( 'common' ).testCase( var Foo = this.Sut.extend( {} ); this.assertStrictEqual( Foo().constructor, Foo ); }, + + + /** + * We support multiple constructor styles; only one may be provided. + * + * This error should happen as a consequence of other method checks that + * prohibit redefinitions. + */ + 'Cannot provide multiple constructor styles': function() + { + var Sut = this.Sut, + len = this.ctors.length; + + // this will not test every permutation, but will hopefully be a + // reasonable test in the event that any additional constructors are + // added in the future (we start at 1 because it'll wrap modulo LEN) + for ( var i = 1; i < len; i++ ) + { + var dfn = {}, + a = this.ctors[ i ], + b = this.ctors[ ( i + 1 ) % len ]; + + dfn[ a ] = function() {}; + dfn[ b ] = function() {}; + + this.assertThrows( function() + { + Sut( dfn ); + }, Error, "Multiple constructors should not be permitted" ); + } + }, } ); diff --git a/test/ClassBuilder/MemberRestrictionTest.js b/test/ClassBuilder/MemberRestrictionTest.js index fa5b798..608b211 100644 --- a/test/ClassBuilder/MemberRestrictionTest.js +++ b/test/ClassBuilder/MemberRestrictionTest.js @@ -1,7 +1,7 @@ /** * Tests class builder member restrictions * - * Copyright (C) 2014 Free Software Foundation, Inc. + * Copyright (C) 2014, 2015 Free Software Foundation, Inc. * * This file is part of GNU ease.js. * @@ -88,7 +88,7 @@ require( 'common' ).testCase( */ 'Proper members are reserved': function() { - var chk = [ '__initProps', 'constructor' ], + var chk = [ '__initProps' ], i = chk.length, reserved = this.Sut.getReservedMembers(); diff --git a/test/IndexTest.js b/test/IndexTest.js index 59675ae..87cf064 100644 --- a/test/IndexTest.js +++ b/test/IndexTest.js @@ -1,7 +1,7 @@ /** * Tests index.js * - * Copyright (C) 2014 Free Software Foundation, Inc. + * Copyright (C) 2014, 2015 Free Software Foundation, Inc. * * This file is part of GNU ease.js. * @@ -63,4 +63,27 @@ require( 'common' ).testCase( { this.exportedAs( 'version', 'version' ); }, + + + /** + * Since ECMAScript 6 introduces the ability to define prototypes using + * the `class` keyword and some syntatic sugar, it is awkward to use the + * traditional ease.js class abstraction with it. Instead, if a user + * wishes to wrap a prototype defined in this manner (to take advantage + * of ease.js' features), it would be more appropriate to make it look + * like we're doing just that. + * + * This is a shorthand for Class.extend( proto, {} ). + */ + 'Invoking is equivalent to extending with empty definition': function() + { + var proto = function() {}, + result = this.Sut( proto ); + + this.assertOk( this.Sut.Class.isClass( result ) ); + + // this is not a comprehensive test (once we add reflection of some + // sort, verify that nothing is added to the prototype) + this.assertOk( this.Sut.Class.isA( proto, result() ) ); + }, } );