1
0
Fork 0

Transparent Error Subtyping

Error subtyping (creating your own error types) in ECMAScript is notoriously
crude, and getting it to work intuitively is even harder.  ease.js will now
transparently handle all necessarily boilerplate when extending Error or its
subtypes.

Take, for example, the common boilerplate for creating your own Error type:

```javascript
    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;
```

There are a number of problems with this:

  - That's a lot of boilerplate for any type you wish to create;
  - Some of those features may not be supported by your environment
    (e.g. column numbers or stack traces);
  - The stack trace will include `ErrorSubtype` _itself_ (as the top frame);
    and
  - Consequently, the `{line,column}Number` and `fileName` will represent
    that top frame---the error constructor itself.

With ease.js, it's just like extending any other class/constructor:

```javascript
    Class( 'ErrorSubtype' )
        .extend( Error, {} );
```

More information can be found in the "Error Subtypes" section of the manual.

Happy Error hacking.  Maybe you'll actually want to create them now.
master
Mike Gerwitz 2016-07-15 00:21:55 -04:00
commit 86df50d1ca
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
6 changed files with 1197 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,304 @@
/**
* Handles the stupid-complicated error subtyping situation in JavaScript
*
* 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/>.
*
* Before you wonder why this is so stupid-complicated and question this
* effort: ease.js supports ECMAScript 3 and later environments.
*
* Unless you continue to question because this is JavaScript and,
* regardless of ECMAScript version, it's still stupid. Then you'd be
* right.
*
* See test case for comprehensive examples.
*/
/**
* Constructor generator for Error subtypes
*
* BASE should be the supertype of all error prototypes in the environment;
* this is usually `Error'. BASE is used to determine what features are
* available in the particular environment (e.g. `Error.captureStackTrace').
*
* The goal is to generate error constructors that will produce errors as
* close to the form of the environment of BASE as possible: this is _not_
* an attempt to provide a unified Error interface across all environments;
* even if we know about certain data line line numbers, if an error from
* BASE would not normally produce them, then neither will we.
*
* @param {Function} base supertype of all error prototypes
*
* @return {ErrorCtor}
*/
function ErrorCtor( base )
{
if ( !( this instanceof ErrorCtor ) )
{
return new ErrorCtor( base );
}
if ( typeof base !== 'function' )
{
throw TypeError( "Expected constructor for error base" );
}
this._base = base;
this._initDataSupport( base );
};
ErrorCtor.prototype = {
/**
* Stack parser-guesser
*
* This recognizes Mozilla- and V8-style stack traces containing our
* unique identifier; other formats might work by chance, but their
* support is not intentional.
*
* There are four match groups, as noted in the regex itself below:
* 1. The entire stripped stack (if recognized);
* 2. Filename;
* 3. Line number; and
* 4. Column number (might not exist).
*
* @type {RegExp}
*/
_stackre: new RegExp(
'^' +
'(?:' +
'.+?\\n\\s+at ' + // V8-style 'at' on second line
')?' +
'.*?__\\$\\$ector\\$\\$__' + // our unique identifier
'.*(?:\\n|$)' + // ignore rest of line
'(' + // (stripped stack)
'(?:' +
'.*?[@(]' + // skip Mozilla/V8 frame name
'(.*?)' + // (filename)
':(\\d+)' + // (line)
'(?::(\\d+))?' + // (column)
'.*?\\n' + // ignore rest of line
')?' +
'(?:.|\\n)*' + // include rest of stack
')?' +
'$'
),
/**
* Base error constructor (usually Error)
* @type {Function}
*/
_base: {},
/**
* Create error constructor
*
* Note that, as this is intended for use as a constructor for ease.js
* classes, this will _not_ set up the prototype as a subtype of
* SUPERTYPE---the caller is expected to do so.
*
* AFTER, if provided, will be invoked at the end of the constructor;
* this allows the topmost frame to still be the error constructor,
* rather than having it wrapped to introduce additional logic.
*
* @param {Function} supertype parent error constructor
* @param {string} name error subtype name
* @param {?Function} after function to invoke after ctor
*
* @return {function(string)} error constructor
*/
createCtor: function( supertype, name, after )
{
if ( typeof supertype !== 'function' )
{
throw TypeError( "Expected constructor for supertype" );
}
if ( ( after !== undefined ) && ( typeof after !== 'function' ) )
{
throw TypeError( "Expected function as `after' argument" );
}
var _self = this;
// yes, this name is important, as we use it as an identifier for
// stack stripping (see `#_parseStack')
function __$$ector$$__( message )
{
this.message = message;
_self._setStackTrace( this, _self._base, supertype );
after && after.apply( this, arguments );
}
// it's important to let the name fall through if not provided
if ( name !== undefined )
{
__$$ector$$__.prototype.name = name;
}
return __$$ector$$__;
},
/**
* Create stack trace using appropriate method for environment
*
* If BASE has a `captureStackStrace' method, then it will be used with
* DEST as the destination and SUPERTYPE as the relative object for the
* stack frames. Otherwise, `DEST.stack' will be overwritten with the
* `stack' produces by instantiating SUPERTYPE, which is the
* conventional approach.
*
* @param {Object} dest destination object for values
* @param {Function} base supertype of all errors
* @param {Function} supertype supertype of new error
*
* @return {undefined}
*/
_setStackTrace: function( dest, base, supertype )
{
if ( typeof base.captureStackTrace === 'function' )
{
base.captureStackTrace( dest, dest.constructor );
return;
}
var super_inst = new supertype(),
stack_data = this._parseStack( super_inst.stack );
dest.stack = stack_data.stripped;
if ( this._lineSupport )
{
dest.lineNumber = stack_data.line;
}
if ( this._columnSupport )
{
dest.columnNumber = stack_data.column;
}
if ( this._filenameSupport )
{
dest.fileName = stack_data.filename;
}
},
/**
* Attempt to extract stack frames below self, as well as the line and
* column numbers (if available)
*
* The provided string STACK should be the full stack trace. It will be
* parsed to ensure that the first stack frame matches a unique
* identifier for the error constructor, and then return the following:
*
* `full': original STACK;
* `stripped': stack trace with first frame stripped, if matching;
* `filename': filename from the first non-error frame, if matching;
* `line': line number from first non-error frame, if matching;
* `column': column number from first non-error frame, if matching.
*
* @param {string} stack full stack trace
*
* @return {Object} full, stripped, line, column
*/
_parseStack: function( stack )
{
var match = ( typeof stack === 'string' )
? stack.match( this._stackre )
: null;
if ( match )
{
// these undefined defaults deal with older environments
// (e.g. IE<9) returning an empty string rather than undefined
// for non-matches (note that !!"0"===true, so we're okay)
return {
full: stack,
stripped: match[ 1 ] || '',
filename: match[ 2 ] || undefined,
line: match[ 3 ] || undefined,
column: match[ 4 ] || undefined
};
}
return {
full: stack,
stripped: stack
};
},
/**
* Initialize with whether line, column, and/or filenames are supported
* by the environment (of BASE)
*
* Some environments (e.g. GNU IceCat) support line and column
* numbers. Others (like older versions of a certain proprietary
* browser) only support line numbers. Others support neither.
*
* The reason for this very specific distinction is strict consistency:
* we want to produce errors of the exact same form as those created by
* BASE.
*
* Below, we check for the value on the prototype chain first and, upon
* failing to find anything, then check to see if the field exists at
* all on an instance of BASE.
*
* This method sets `_{line,column,filename}Support`.
*
* @param {Function} base supertype of all errors
*
* @return {undefined}
*/
_initDataSupport: function( base )
{
var chk = new base(),
hasOwn = Object.hasOwnProperty;
this._lineSupport = ( chk.lineNumber !== undefined )
|| hasOwn.call( chk, 'lineNumber' );
this._columnSupport = ( chk.columnNumber !== undefined )
|| hasOwn.call( chk, 'columnNumber' );
this._filenameSupport = ( chk.fileName !== undefined )
|| hasOwn.call( chk, 'fileName' );
},
/**
* Whether the given TYPE is our base error constructor or a subtype
*
* @param {Function} type constructor to check against our base
*
* @return {boolean} whether TYPE is our base constructor or a subtype
*/
isError: function( type )
{
return ( type === this._base )
|| ( type.prototype instanceof this._base );
},
};
module.exports = ErrorCtor;

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 );
},
} );

View File

@ -0,0 +1,557 @@
/**
* Tests error constructor generation
*
* 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/>.
*/
function DummyError() {};
function SubDummyError() {};
SubDummyError.prototype = new DummyError();
SubDummyError.prototype.name = 'sub dummy name';
require( 'common' ).testCase(
{
caseSetUp: function()
{
this.Sut = this.require( 'ctor/ErrorCtor' );
// no need to be comprehensive
this.bases = [ '', {} ];
// space is intentional, since in IceCat (for example) you get
// '@debugger eval code' if you get an error in the console
this.frames = [
// average case, multiple frames, Mozilla-style
{
frames: [
"__$$ector$$__@foo.js:1:2",
"bar@first-frame.js:1:2",
"baz@second other frame:2:2",
],
strip: 1,
fileName: 'first-frame.js',
lineNumber: 1,
columnNumber: 2,
},
// average case, multiple frames, Mozilla-style, no-match
{
frames: [
"__$$no$$__@foo.js:1:2",
"bar@first-frame2.js:1:2",
"baz@second other frame:2:2",
],
strip: 0,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
},
// average case, Mozilla-style, no column number
{
frames: [
"__$$ector$$__@foo.js:1",
"bar@first-frame2.js:2",
"baz@second other frame:3",
],
strip: 1,
fileName: 'first-frame2.js',
lineNumber: 2,
columnNumber: undefined,
},
// average case, Mozilla-style, no frame name
{
frames: [
"__$$ector$$__@foo.js:1:1",
"@first-frame3.js:2:2",
"baz@second other frame:3:3",
],
strip: 1,
fileName: 'first-frame3.js',
lineNumber: 2,
columnNumber: 2,
},
// no other frames (but notice the trailing
// newline), Mozilla-style
{
frames: [
"__$$ector$$__@foo.js:1:2\n",
],
strip: 1,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
},
// average case, multiple frames, V8-style
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1:2)",
" at bar (first-frame.js:1:2)",
" at baz (second other frame:2:2)",
],
strip: 2,
fileName: 'first-frame.js',
lineNumber: 1,
columnNumber: 2,
},
// average case, V8-style, multiple frames, no-match
{
frames: [
"SomeError",
" at __$$nomatch$$__ (foo.js:1:2)",
" at bar (first-frame.js:1:2)",
" at baz (second other frame:2:2)",
],
strip: 0,
fileName: undefined,
lineNumber: undefined,
columnNumber: undefined,
},
// average case, V8-style, no column number
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1)",
" at bar (first-frame2.js:1)",
" at baz (second other frame:2)",
],
strip: 2,
fileName: 'first-frame2.js',
lineNumber: 1,
columnNumber: undefined,
},
// average case, V8-style, no frame name
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1:2)",
" at (first-frame.js:1:2)",
" at (second other frame:2:2)",
],
strip: 2,
fileName: 'first-frame.js',
lineNumber: 1,
columnNumber: 2,
},
// no other frames, V8-style
{
frames: [
"SomeError",
" at __$$ector$$__ (foo.js:1:2)"
],
strip: 2,
fileName: 'first-frame.js',
lineNumber: undefined,
columnNumber: undefined,
},
];
// whether line, column, or filename are available in environment
this.linecolf = [
{ lineNumber: false, columnNumber: false, fileName: true },
{ lineNumber: true, columnNumber: false, fileName: true },
{ lineNumber: false, columnNumber: true, fileName: true },
{ lineNumber: true , columnNumber: true, fileName: true },
{ lineNumber: false, columnNumber: false, fileName: false },
{ lineNumber: true, columnNumber: false, fileName: false },
{ lineNumber: false, columnNumber: true, fileName: false },
{ lineNumber: true , columnNumber: true, fileName: false },
];
},
'@each(bases) Throws error if base is not a function': function( obj )
{
this.assertThrows( function()
{
this.Sut( obj );
}, TypeError );
},
'@each(bases) Throws error if supertype is not a function': function( obj )
{
this.assertThrows( function()
{
this.Sut( DummyError, obj );
}, TypeError );
},
/**
* Error messages are set by a `message' property.
*/
'Sets message via constructor': function()
{
var expected = 'foo message';
this.assertEqual(
new ( this.Sut( DummyError ).createCtor( DummyError ) )
( expected ).message,
expected
);
},
/**
* The build-in ECMAScript Error constructors don't cast MESSAGE to a
* string, so we shouldn't either.
*/
'Does not cast message to string': function()
{
var expected = {};
this.assertStrictEqual(
new ( this.Sut( DummyError ).createCtor( DummyError ) )
( expected ).message,
expected
);
},
/**
* The name of the error is derived from the `name' property.
*/
'Sets name to class name': function( Type )
{
var expected = 'MyError';
this.assertEqual(
new (
this.Sut( DummyError ).createCtor( DummyError, expected )
)().name,
expected
);
},
/**
* ...unless one is not provided, in which case we should retain the
* parent's.
*
* Since the constructor generator doesn't set up the constructor's
* supertype, this amounts to seeing if it'll fall through if we set the
* supertype. We don't want to just check whether `name' is or is not
* defined on the prototype, since we only care that it works, not how
* it's done.
*/
'Defaults name to supertype': function( Type )
{
var ctor = this.Sut( DummyError )
.createCtor( SubDummyError );
ctor.prototype = new SubDummyError();
this.assertEqual(
new ctor().name,
new SubDummyError().name
);
},
/**
* JavaScript doesn't make extending Error pleasent---we need to take
* care of our own stack trace, and that trace isn't going to be
* entirely correct (because we have an extra stack frame, being in the
* Error itself). Furthermore, not all environments support stack.
*
* To make matters worse, the proper method of obtaining or overwriting
* a stack trace also varies. So, let's emulate some environments.
*/
/**
* Certain browsers (like Chromium) support `Error.captureStackTrace',
* which sets the `stack' property on the given object to either a
* complete stack trace, or a stack trace below a reference to a given
* object. The `stack' property is also defined as a getter, which
* makes for confusing and frustrating development when you are
* wondering why setting it does nothing. (Personal experience
* perhaps?)
*
* Note that the previous tests will implicitly test that
* `captureStackTrace' is _not_ called when unavailable, because they
* will fail to call an undefined function.
*/
'Uses Error.captureStackTrace when available': function()
{
var _self = this,
expected = 'as expected',
capture_args;
function DummyErrorCapture() {};
DummyErrorCapture.captureStackTrace = function()
{
capture_args = arguments;
};
var given_ctor = this.Sut( DummyErrorCapture )
.createCtor( DummyError );
// if the stack trace were generated now, then that would be bad (as
// it would be incorrect when the error is actually instantiated)
this.assertEqual( undefined, capture_args );
var inst = new given_ctor();
// destination for `stack' property set
this.assertStrictEqual( capture_args[ 0 ], inst );
// relative stack frame
this.assertStrictEqual( capture_args[ 1 ], given_ctor );
},
/**
* If `Error.captureStackTrace' is _not_ available, we fall back to the
* good-ol'-fashion overwrite-stack-with-a-super-instance-stack
* approach, which is conventional.
*/
'Overwrites `stack\' property if no `captureStackTrace\'': function()
{
var expected = 'as expected',
allow = false;
// just something that we can mock the stack on
function SubDummyError()
{
if ( !allow ) return;
this.stack = expected;
}
// this is why stack traces are traditionally a problem unless you
// remember to explicitly set it; ease.js does it for you
SubDummyError.prototype = new DummyError();
SubDummyError.prototype.stack = 'stack not set';
var given = this.Sut( DummyError )
.createCtor( SubDummyError );
// ensures that this stack is actually from a new object, not the
// stack that was set on the prototype
allow = true;
var result = new given().stack;
this.assertEqual( result, expected );
},
/**
* If `Error.captureStackTrace' is available and used, then the error
* constructor itself will not appear in the stack trace. If we have to
* set it, however, then it will---this is a consequence of
* instantiating the supertype within the error constructor in order to
* get a proper stack trace.
*
* We will attempt to strip ourselves from the string if the stack trace
* string meets certain critiera.
*
* Also make sure we don't strip if there is a non-match. Generally
* speaking, this won't often (if ever) be the case in practice, but
* let's never make assumptions.
*/
'@each(frames) Strips self from stack if no `captureStackTrace\'':
function( framedata )
{
var lines = Array.prototype.slice.call( framedata.frames );
function SubDummyError()
{
this.stack = lines.join( '\n' );
}
var ctor = this.Sut( DummyError )
.createCtor( SubDummyError );
var result = new ctor().stack;
this.assertEqual(
lines.slice( framedata.strip ).join( '\n' ),
result
);
},
/**
* Certain browsers (like GNU IceCat) support `{line,column}Number` and
* `fileName`; if those are defined, we will propagate them.
*
* ...but there's a caveat: the values set on the supertype's error
* object aren't going to be correct, because the first frame is not our
* own. That means we have to do some string parsing on the second
* frame; this will only happen if stack stripping was successful, since
* we otherwise have no idea if the second frame is actually what we
* want.
*
* Even if we do happen to know the values, if the environment in which
* we are running does not normally provide those data, then neither
* will we (for strict consistency).
*/
'@each(linecolf) Sets line, column, and filename data if available':
function( linecolf )
{
var expected = {
lineNumber: 5,
columnNumber: 3,
fileName: 'foofile.js',
}
var lines = [
"@__$$ector$$__ foo:1:1",
"@" + expected.fileName + ":" + expected.lineNumber
+ ":" + expected.columnNumber,
"@second other frame:2:2"
];
function LineColDummyError()
{
this.stack = lines.join( '\n' );
for ( var prop in linecolf )
{
if ( !linecolf[ prop ] ) continue;
// purposefully not an integer; should apply if the key
// exists at all
this[ prop ] = undefined;
}
}
var ctor = this.Sut( LineColDummyError )
.createCtor( LineColDummyError );
var errobj = new ctor();
for ( var prop in linecolf )
{
if ( linecolf[ prop ] )
{
this.assertEqual( expected[ prop ], errobj[ prop ] );
}
else
{
this.assertOk(
!Object.hasOwnProperty.call( errobj, prop )
);
}
}
},
/**
* This tests various situations with regards to the data available in
* stack traces; see `this.frames` for those cases.
*/
'@each(frames) Recognizes line, column, and filename when available':
function( framedata )
{
function LineColDummyError()
{
this.stack = framedata.frames.join( '\n' );
this.lineNumber = undefined;
this.columnNumber = undefined;
this.fileName = undefined;
}
var ctor = this.Sut( LineColDummyError )
.createCtor( LineColDummyError );
var errobj = new ctor();
this.assertEqual( framedata.lineNumber, errobj.lineNumber );
this.assertEqual( framedata.columnNumber, errobj.columnNumber );
},
/**
* A predicate is provided to allow callers to determine if the given
* object is our base constructor or a subtype thereof.
*/
'Provides predicate to recognize base match': function()
{
var sut = this.Sut( DummyError );
this.assertOk( sut.isError( DummyError ) );
this.assertOk( !sut.isError( new DummyError() ) );
this.assertOk( sut.isError( SubDummyError ) );
this.assertOk( !sut.isError( new SubDummyError() ) );
this.assertOk( !sut.isError( function() {} ) );
},
/**
* A function may optionally be provided to be invoked after the
* constructor has completed---this allows for the constructor to be
* augmented in such a way that the top stack frame is still the
* generated constructor when the error is instantiated.
*/
'Invokes provided function after self': function()
{
var called = false,
context = undefined,
argchk = {},
message = 'stillrunctor';
var result = new (
this.Sut( DummyError )
.createCtor( DummyError, '', function()
{
called = arguments;
context = this;
} )
)( message, argchk );
this.assertOk( called );
this.assertStrictEqual( argchk, called[ 1 ] );
this.assertStrictEqual( result, context );
// the ctor itself should also still be called (this depends on
// previous test also succeeding)
this.assertEqual( message, result.message );
},
/**
* Don't wait until instantiation to blow up on an invalid AFTER.
*/
'Throws error given a non-function `after\' argument': function()
{
var Sut = this.Sut;
this.assertThrows( function()
{
Sut( DummyError )
.createCtor( DummyError, '', "oops" );
}, TypeError );
},
} );