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 subtypingmaster
parent
d99ab2e5fb
commit
a7e1d2ad70
118
doc/classes.texi
118
doc/classes.texi
|
@ -700,6 +700,7 @@ classes for their conciseness.
|
|||
one-another
|
||||
* Visibility Escalation:: Increasing visibility of inherited
|
||||
members
|
||||
* Error Subtypes:: Transparent Error subtyping
|
||||
* Final Classes:: Classes that cannot be inherited from
|
||||
@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}
|
||||
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
|
||||
@subsection Final Classes
|
||||
@table @code
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -116,14 +116,14 @@ var util = require( './util' ),
|
|||
* @constructor
|
||||
*/
|
||||
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
|
||||
if ( !( this instanceof exports ) )
|
||||
{
|
||||
// module.exports for Closure Compiler
|
||||
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;
|
||||
|
||||
/**
|
||||
* Error constructor generator
|
||||
* @type {ErrorCtor}
|
||||
*/
|
||||
this._ector = ector;
|
||||
|
||||
/**
|
||||
* 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
|
||||
this._classId++;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -57,7 +57,8 @@ var util = require( './util' ),
|
|||
)
|
||||
),
|
||||
require( './VisibilityObjectFactoryFactory' )
|
||||
.fromEnvironment()
|
||||
.fromEnvironment(),
|
||||
require( './ctor/ErrorCtor' )( Error )
|
||||
)
|
||||
;
|
||||
|
||||
|
|
|
@ -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 );
|
||||
},
|
||||
} );
|
Loading…
Reference in New Issue