diff --git a/espree.js b/espree.js index 7c16b696..accec3cf 100644 --- a/espree.js +++ b/espree.js @@ -129,8 +129,11 @@ function tokenize(code, options) { */ function parse(code, options) { const Parser = parsers.get(options); + const parser = new Parser(options, code); + const ast = parser.parse(); + const recoverableErrors = parser.recoverableErrors; - return new Parser(options, code).parse(); + return { ast, recoverableErrors }; } //------------------------------------------------------------------------------ @@ -141,7 +144,9 @@ exports.version = require("./package.json").version; exports.tokenize = tokenize; -exports.parse = parse; +exports.parseForESLint = parse; + +exports.parse = (code, options) => parse(code, options).ast; // Deep copy. /* istanbul ignore next */ diff --git a/lib/espree.js b/lib/espree.js index 6c57a87c..e9a99586 100644 --- a/lib/espree.js +++ b/lib/espree.js @@ -68,11 +68,36 @@ function normalizeOptions(options) { const sourceType = normalizeSourceType(options.sourceType); const ranges = options.range === true; const locations = options.loc === true; + const recoverableErrors = options.recoverableErrors === true; if (sourceType === "module" && ecmaVersion < 6) { throw new Error("sourceType 'module' is not supported when ecmaVersion < 2015. Consider adding `{ ecmaVersion: 2015 }` to the parser options."); } - return Object.assign({}, options, { ecmaVersion, sourceType, ranges, locations }); + return Object.assign( + {}, + options, + { ecmaVersion, sourceType, ranges, locations, recoverableErrors } + ); +} + +/** + * Create a new syntax error object. + * @param {string} input The source code. + * @param {number} pos The location the error happened. + * @param {string} message The error message. + * @param {SyntaxError[]} recoverableErrors The recovered errors. + * @returns {SyntaxError} The syntax error object. + */ +function createSyntaxError(input, pos, message, recoverableErrors) { + const loc = acorn.getLineInfo(input, pos); + const err = new SyntaxError(message); + + err.index = pos; + err.lineNumber = loc.line; + err.column = loc.column + 1; // acorn uses 0-based columns + err.recoverableErrors = recoverableErrors; + + return err; } /** @@ -167,6 +192,9 @@ module.exports = () => Parser => class Espree extends Parser { jsxAttrValueToken: false, lastToken: null }; + + // Public. + this.recoverableErrors = options.recoverableErrors ? [] : void 0; } tokenize() { @@ -243,13 +271,7 @@ module.exports = () => Parser => class Espree extends Parser { * @returns {void} */ raise(pos, message) { - const loc = acorn.getLineInfo(this.input, pos); - const err = new SyntaxError(message); - - err.index = pos; - err.lineNumber = loc.line; - err.column = loc.column + 1; // acorn uses 0-based columns - throw err; + throw createSyntaxError(this.input, pos, message, this.recoverableErrors); } /** @@ -260,7 +282,11 @@ module.exports = () => Parser => class Espree extends Parser { * @returns {void} */ raiseRecoverable(pos, message) { - this.raise(pos, message); + if (this.recoverableErrors) { + this.recoverableErrors.push(createSyntaxError(this.input, pos, message)); + } else { + this.raise(pos, message); + } } /** diff --git a/tests/lib/recoverable-errors.js b/tests/lib/recoverable-errors.js new file mode 100644 index 00000000..f699a066 --- /dev/null +++ b/tests/lib/recoverable-errors.js @@ -0,0 +1,116 @@ +/** + * @fileoverview Tests for options.recoverableErrors. + * @author Toru Nagashima + */ +"use strict"; + +const assert = require("assert"); +const { parseForESLint } = require("../../espree"); + +/** + * Gets a raw version of the AST that is suitable for comparison. This is necessary + * due to the different order of properties across parsers. + * @param {ASTNode} ast The AST to convert. + * @returns {ASTNode} The converted AST. + * @private + */ +function getRaw(ast) { + return JSON.parse(JSON.stringify(ast, (key, value) => { + + // Delete `node.start` and `node.end`. + if ((key === "start" || key === "end") && typeof value === "number") { + return void 0; + } + + // Delete `error.stack`. + if (value instanceof Error) { + return Object.assign({ message: value.message }, value); + } + + return value; + })); +} + +/** + * Assert a given code to throw the expected error. + * @param {Function} f The code to execute. + * @param {Object} expected The expected properties. + * @returns {void} + */ +function assertError(f, expected) { + try { + f(); + } catch (actual) { + assert.deepStrictEqual(getRaw(actual), expected); + return; + } + + assert.fail("should throw."); +} + +describe("'parseForESLint()' function with 'options.recoverableErrors'", () => { + it("should return AST and errors if `let a, a;` was given.", () => { + const { ast, recoverableErrors } = parseForESLint("let a, a;", { ecmaVersion: 2015, recoverableErrors: true }); + + assert.deepStrictEqual(getRaw(ast), { + type: "Program", + sourceType: "script", + body: [ + { + type: "VariableDeclaration", + kind: "let", + declarations: [ + { + type: "VariableDeclarator", + id: { + name: "a", + type: "Identifier" + }, + init: null + }, + { + type: "VariableDeclarator", + id: { + name: "a", + type: "Identifier" + }, + init: null + } + ] + } + ] + }); + assert.deepStrictEqual(getRaw(recoverableErrors), [ + { + column: 8, + index: 7, + lineNumber: 1, + message: "Identifier 'a' has already been declared" + } + ]); + }); + + it("should throw an error that has 'recoverableErrors' property if `let a, a; {` was given.", () => { + assertError( + () => parseForESLint("let a, a; {", { ecmaVersion: 2015, recoverableErrors: true }), + + // Top-level is the fatal error. + { + column: 12, + index: 11, + lineNumber: 1, + message: "Unexpected token", + + // The fatal error has the recovered syntax errors. + recoverableErrors: [ + { + column: 8, + index: 7, + lineNumber: 1, + message: "Identifier 'a' has already been declared" + } + ] + } + ); + }); +});