From a931796bdf7fc9041f9c3bfbf701c801bf8552a2 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 9 Sep 2015 23:25:37 -0400 Subject: [PATCH 1/3] Prototype wrapping using index function This redefines the index as a function (rather than a vanilla object) so that it may be invoked to yield an ease.js Class that wraps the given prototype. --- index.js | 20 ++++++++++++++++++-- test/IndexTest.js | 25 ++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) 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/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() ) ); + }, } ); From ba2605f8365c9c767340622591ff155da326881e Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 15 Sep 2015 00:10:07 -0400 Subject: [PATCH 2/3] Alias `constructor` member to `__construct` This allows ease.js classes to mimic the structure of ES6 classes, which use `constructor` to denote the constructor. This patch simply aliases it to `__construct`, which ease.js handles as it would normally. To that note, since the ES6 `class` keyword is purely syntatic sugar around the prototype model, there is not much benefit to using it over ease.js if benefits of ease.js are still desired, since the member definition syntax is a feature of object literals: ``` // ease.js using ES6 let Person = Class( { _name: '', // note that __construct still works as well constructor( name ) { this._name = ''+name; }, sayHi() { return "Hi, I'm " + this.getName(); }, // keywords still work as expected 'protected getName'() { return this._name; } } ); // ES6 using `class` keyword class Person { // note that ES6 will _not_ make this private _name: '', constructor( name ) { this._name = ''+name; }, sayHi() { return "Hi, I'm " + this.getName(); } // keywords unsupported (you'd have to use Symbols) getName() { return this._name; } } // ES3/5 ease.js var Person = Class( { _name: '', __construct: function( name ) { this._name = ''+name; }, sayHi: function() { return "Hi, I'm " + this._name; }, 'protected getName': function() { return this._name; } } ); ``` As you can see, the only change between writing ES6-style method definitions is the syntax; all keywords and other features continue to work as expected. --- lib/ClassBuilder.js | 66 +++++++++++++++- lib/prop_parser.js | 5 +- test/Class/ConstructorTest.js | 90 ++++++++++++++-------- test/ClassBuilder/MemberRestrictionTest.js | 4 +- 4 files changed, 127 insertions(+), 38 deletions(-) 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..111e892 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" ); }, 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(); From d3f1f0dee2a7a05c7e4d5b3354e9e651235ff582 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 15 Sep 2015 23:57:46 -0400 Subject: [PATCH 3/3] Add test to ensure multiple constructors cannot be defined --- test/Class/ConstructorTest.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/Class/ConstructorTest.js b/test/Class/ConstructorTest.js index 111e892..d382870 100644 --- a/test/Class/ConstructorTest.js +++ b/test/Class/ConstructorTest.js @@ -230,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" ); + } + }, } );