diff --git a/doc/classes.texi b/doc/classes.texi
index 652461f..9fd510b 100644
--- a/doc/classes.texi
+++ b/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
diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js
index 21bcc1e..68de6a3 100644
--- a/lib/ClassBuilder.js
+++ b/lib/ClassBuilder.js
@@ -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++;
diff --git a/lib/class.js b/lib/class.js
index 65351e9..e67d5db 100644
--- a/lib/class.js
+++ b/lib/class.js
@@ -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 )
)
;
diff --git a/lib/ctor/ErrorCtor.js b/lib/ctor/ErrorCtor.js
new file mode 100644
index 0000000..8e29530
--- /dev/null
+++ b/lib/ctor/ErrorCtor.js
@@ -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 .
+ *
+ * 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;
diff --git a/test/ClassBuilder/ErrorExtendTest.js b/test/ClassBuilder/ErrorExtendTest.js
new file mode 100644
index 0000000..5bd6007
--- /dev/null
+++ b/test/ClassBuilder/ErrorExtendTest.js
@@ -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 .
+ */
+
+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 );
+ },
+} );
diff --git a/test/ctor/ErrorCtorTest.js b/test/ctor/ErrorCtorTest.js
new file mode 100644
index 0000000..562bfdd
--- /dev/null
+++ b/test/ctor/ErrorCtorTest.js
@@ -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 .
+ */
+
+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 );
+ },
+} );