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