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
|
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
|
||||||
|
|
|
@ -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++;
|
||||||
|
|
||||||
|
|
|
@ -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 )
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
|
@ -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