1
0
Fork 0

Error constructor integration into ClassBuilder

This introduces the transparent subtyping.

* doc/classes.texi (Error Subtyping): Section added

* lib/ClassBuilder.js (ClassBuilder): Accepts ErrorCtor instance
  (build): Transparent Error subtyping

* lib/class.js: Provide ErrorCtor instance to ClassBuilder

* test/ClassBuilder/ErrorExtendTest.js: Add test case for transparent error
  subtyping
master
Mike Gerwitz 2016-07-15 00:15:11 -04:00
parent d99ab2e5fb
commit a7e1d2ad70
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
4 changed files with 336 additions and 5 deletions

View File

@ -700,6 +700,7 @@ classes for their conciseness.
one-another one-another
* Visibility Escalation:: Increasing visibility of inherited * Visibility Escalation:: Increasing visibility of inherited
members members
* Error Subtypes:: Transparent Error subtyping
* Final Classes:: Classes that cannot be inherited from * Final Classes:: Classes that cannot be inherited from
@end menu @end menu
@ -1068,6 +1069,123 @@ Let's take a look at an example.
Note that, in the above example, making the public @var{cannotMakeProtected} Note that, in the above example, making the public @var{cannotMakeProtected}
method protected would throw an error. method protected would throw an error.
@node Error Subtypes
@subsection Error Subtypes
Extending ECMAScript's built-in @var{Error} type is a bit cumbersome (to
say the least)---it involves not only the traditional prototype chain,
but also setting specific properties within the constructor. Further,
different environments support different features (e.g. stack traces and
column numbers), and values are relative to the stack frame of the
@var{Error} subtype constructor itself.
With GNU ease.js, error subtyping is transparent:
@float Figure, f:error-extend
@verbatim
var MyError = Class( 'MyError' )
.extend( Error, {} );
var e = MyError( 'Foo' );
e.message; // Foo
e.name; // MyError
// -- if supported by environment --
e.stack; // stack beginning at caller
e.fileName; // caller filename
e.lineNumber; // caller line number
e.columnNumber; // caller column number
// general case
throw MyError( 'Foo' );
@end verbatim
@caption{Transparent @var{Error} extending in ease.js}
@end float
If ease.js detects that you are extending an @var{Error} object or any
of its subtypes, it will handle a number of things for you, depending on
environment:
@enumerate
@item Produce a default constructor method (@pxref{Constructors}) that
assigns the error message to the string passed as the first argument;
@item Sets the error name to the class name;
@item Provides a stack trace via @var{stack}, if supported by the
environment, stripping itself from the head of the stack; and
@item Sets any of @var{fileName}, @var{lineNumber}, and/or
@var{columnNumber} when supported by the environment.
@end enumerate
If a constructor method is provided in the class definition
(@pxref{Constructors}), then it will be invoked immediately after the
error object is initialized by the aforementioned default
constructor.@footnote{The reason that ease.js
does not permit overriding the generated constructor is an
implementation detail: the generated constructor is not on the
supertype, so there is not anything to actually override. Further, the
generated constructor provides a sane default behavior that should be
implicit in error classes anyway; that behavior can be overridden simply
be re-assigning the values that are assigned for you (e.g. name or line
number).} @var{this.__super} in that context refers to the constructor
of the supertype (as would be expected), @emph{not} the default error
constructor.
ease.js will automatically detect what features are supported by the
current environment, and will @emph{only} set respective values if the
environment itself would normally set them. For example, if ease.js can
determine a column number from the stack trace, but the environment does
not normally set @var{columnNumber} on @var{Error} objects, then neither
will ease.js; this leads to predictable and consistent behavior.
ease.js makes its best attempt to strip itself from the head of the
stack trace. To see why this is important, consider the generally
recommended way of creating an @var{Error} subtype in ECMAScript:
@float Figure, f:ecma-error-extend
@verbatim
function ErrorSubtype( message )
{
var err = new Error();
this.name = 'ErrorSubtype';
this.message = message || 'Error';
this.stack = err.stack;
this.lineNumber = err.lineNumber;
this.columnNumber = err.columnNumber;
this.fileName = err.fileName;
}
ErrorSubtype.prototype = new Error();
ErrorSubtype.prototype.constructor = ErrorSubtype;
@end verbatim
@caption{@var{Error} subtyping in plain ECMAScript 3}
@end float
Not only is @ref{f:ecma-error-extend} all boilerplate and messy, but
it's not entirely truthful: To get a stack trace, @var{Error} is
instantiated within the constructor @var{ErrorSubtype}; this ensures
that the stack trace will actually include the caller. Unfortunately,
it also includes the @emph{current frame}; the topmost frame in the
stack trace will be @var{ErrorSubtype} itself. To make matters worse,
all of @var{lineNumber}, @var{columNumber}, and @var{fileName} (if
defined) will be set to the stack frame of our constructor, @emph{not}
the caller.
ease.js will set each of those values to represent the caller. To do
so, it parses common stack trace formats. Should it fail, it simply
falls back to the default behavior of including itself in the stack
frame.
The end result of all of this is---hopefully---concise @var{Error}
subtypes that actually function as you would expect of an @var{Error},
without any boilerplate at all. The @var{Error} subtypes created with
ease.js can be extended like the built-ins, and may extend any of the
built-in error types (e.g. @var{TypeError} and @var{SyntaxError}).
@node Final Classes @node Final Classes
@subsection Final Classes @subsection Final Classes
@table @code @table @code

View File

@ -1,7 +1,7 @@
/** /**
* Handles building of classes * Handles building of classes
* *
* Copyright (C) 2011, 2012, 2013, 2014, 2015 Free Software Foundation, Inc. * Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016 Free Software Foundation, Inc.
* *
* This file is part of GNU ease.js. * This file is part of GNU ease.js.
* *
@ -116,14 +116,14 @@ var util = require( './util' ),
* @constructor * @constructor
*/ */
module.exports = exports = module.exports = exports =
function ClassBuilder( warn_handler, member_builder, visibility_factory ) function ClassBuilder( warn_handler, member_builder, visibility_factory, ector )
{ {
// allow ommitting the 'new' keyword // allow ommitting the 'new' keyword
if ( !( this instanceof exports ) ) if ( !( this instanceof exports ) )
{ {
// module.exports for Closure Compiler // module.exports for Closure Compiler
return new module.exports( return new module.exports(
warn_handler, member_builder, visibility_factory warn_handler, member_builder, visibility_factory, ector
); );
} }
@ -145,6 +145,11 @@ function ClassBuilder( warn_handler, member_builder, visibility_factory )
*/ */
this._visFactory = visibility_factory; this._visFactory = visibility_factory;
/**
* Error constructor generator
* @type {ErrorCtor}
*/
this._ector = ector;
/** /**
* Class id counter, to be increment on each new definition * Class id counter, to be increment on each new definition
@ -408,6 +413,22 @@ exports.prototype.build = function extend( _, __ )
} }
} }
// we transparently handle extending errors in a sane manner, which is
// traditionally a huge mess (you're welcome)
if ( this._ector && this._ector.isError( base ) )
{
// declare public properties (otherwise, they'll be confined to the
// private visibility object in ES5+ environments)
props.message = '';
props.stack = '';
// user-provided constructor
var ector_own = props.__construct;
// everything else is handled by the constructor
props.__construct = this._ector.createCtor( base, cname, ector_own );
}
// increment class identifier // increment class identifier
this._classId++; this._classId++;

View File

@ -1,7 +1,7 @@
/** /**
* Contains basic inheritance mechanism * Contains basic inheritance mechanism
* *
* Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015 Free Software Foundation, Inc. * Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 2016 Free Software Foundation, Inc.
* *
* This file is part of GNU ease.js. * This file is part of GNU ease.js.
* *
@ -57,7 +57,8 @@ var util = require( './util' ),
) )
), ),
require( './VisibilityObjectFactoryFactory' ) require( './VisibilityObjectFactoryFactory' )
.fromEnvironment() .fromEnvironment(),
require( './ctor/ErrorCtor' )( Error )
) )
; ;

View File

@ -0,0 +1,191 @@
/**
* Tests special handling of Error subtyping
*
* Copyright (C) 2016 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
* ease.js is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require( 'common' ).testCase(
{
caseSetUp: function()
{
this.Sut = this.require( 'ClassBuilder' );
this.MethodWrapperFactory = this.require( 'MethodWrapperFactory' );
this.wrappers = this.require( 'MethodWrappers' ).standard;
this.util = this.require( 'util' );
this.errtypes = [
Error,
TypeError,
SyntaxError,
ReferenceError,
EvalError,
RangeError,
URIError,
];
},
setUp: function()
{
this.stubEctor = {
createCtor: function() {},
isError: function() { return true; },
};
// XXX: get rid of this disgusting mess; we're mid-refactor and all
// these dependencies should not be necessary for testing
this.builder = this.Sut(
this.require( 'warn' ).DismissiveHandler(),
this.require( '/MemberBuilder' )(
this.MethodWrapperFactory( this.wrappers.wrapNew ),
this.MethodWrapperFactory( this.wrappers.wrapOverride ),
this.MethodWrapperFactory( this.wrappers.wrapProxy ),
this.getMock( 'MemberBuilderValidator' )
),
this.require( '/VisibilityObjectFactoryFactory' ).fromEnvironment(),
this.stubEctor
);
},
/**
* Any determination as to whether we're extending an error should be
* left to the error constructor.
*
* Note that this test only ensures that the SUT will recognizs
* non-errors as such; the other tests that follow implicitly test the
* reverse.
*/
'Uses constructor generator for error extension determination': function()
{
var called = false;
this.stubEctor.isError = function() { return false; };
// should not be called
this.stubEctor.createCtor = function()
{
called = true;
};
// will invoke createCtor if the isError check fails
this.builder.build( Error, {} )();
this.assertOk( !called );
},
/**
* Simple verification that we're passing the correct data to the error
* constructor.
*/
'@each(errtypes) Produces error constructor': function( Type )
{
this.stubEctor.createCtor = function( supertype, name )
{
return function()
{
this.givenSupertype = supertype;
this.givenName = name;
};
};
var expected_name = 'ename',
result = this.builder.build( Type, {
__name: expected_name,
givenSupertype: '',
givenName: '',
} )();
this.assertEqual( Type, result.givenSupertype );
this.assertEqual( expected_name, result.givenName );
},
/**
* This is obvious, but since Error is a special case, let's just be
* sure.
*/
'@each(errtypes) Error subtype is instanceof parent': function( Type )
{
this.assertOk(
this.builder.build( Type, {} )() instanceof Type
);
},
/**
* By default, in ES5+ environments that support visibility objects will
* write to the private visibility object by default, unless the property
* is declared public.
*/
'Message and stack are public': function()
{
var expected_msg = 'expected msg',
expected_stack = 'expected stack';
this.stubEctor.createCtor = function( supertype, name )
{
return function()
{
this.message = expected_msg;
this.stack = expected_stack;
};
};
var result = this.builder.build( {}, {} )();
// will only be visible (in ES5 environments at least) if the
// properties are actually public
this.assertEqual( expected_msg, result.message );
this.assertEqual( expected_stack, result.stack );
},
/**
* The default constructor cannot be overridden---it isn't a method on
* the supertype at all; it's rather just a default
* implementation. However, a user can provide a method to be invoked
* after the generated constructor.
*/
'Can override generated constructor': function()
{
var called_gen = false,
called_own = false;
this.stubEctor.createCtor = function( supertype, name, after )
{
return function()
{
called_gen = true;
after();
};
};
var result = this.builder.build( {}, {
__construct: function()
{
called_own = true;
},
} )();
this.assertOk( called_gen );
this.assertOk( called_own );
},
} );