Skip to content

Commit

Permalink
New: options.recoverableErrors (eslint/rfcs#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed Jul 25, 2019
1 parent c92c756 commit 8208b4d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 11 deletions.
9 changes: 7 additions & 2 deletions espree.js
Expand Up @@ -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 };
}

//------------------------------------------------------------------------------
Expand All @@ -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 */
Expand Down
44 changes: 35 additions & 9 deletions lib/espree.js
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -167,6 +192,9 @@ module.exports = () => Parser => class Espree extends Parser {
jsxAttrValueToken: false,
lastToken: null
};

// Public.
this.recoverableErrors = options.recoverableErrors ? [] : void 0;
}

tokenize() {
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}
}

/**
Expand Down
116 changes: 116 additions & 0 deletions tests/lib/recoverable-errors.js
@@ -0,0 +1,116 @@
/**
* @fileoverview Tests for options.recoverableErrors.
* @author Toru Nagashima <https://github.com/mysticatea>
*/
"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"
}
]
}
);
});
});

0 comments on commit 8208b4d

Please sign in to comment.