Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use babel parser when not parsed with espree #1183

Merged
merged 1 commit into from
Feb 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 39 additions & 26 deletions eslint-bridge/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import * as espree from "espree";
import * as babel from "babel-eslint";
import { SourceCode, Linter } from "eslint";

const PARSER_CONFIG: Linter.ParserOptions = {
export const PARSER_CONFIG_MODULE: Linter.ParserOptions = {
tokens: true,
comment: true,
loc: true,
Expand All @@ -35,40 +35,53 @@ const PARSER_CONFIG: Linter.ParserOptions = {
},
};

const PARSER_CONFIG_NOT_STRICT: Linter.ParserOptions = { ...PARSER_CONFIG, sourceType: "script" };
// 'script' source type forces not strict
export const PARSER_CONFIG_SCRIPT: Linter.ParserOptions = {
...PARSER_CONFIG_MODULE,
sourceType: "script",
};

export function parseSourceFile(fileContent: string, fileUri: string): SourceCode | undefined {
let parse = espree.parse;
let parser = "espree";
let parseFunctions = [espree.parse, babel.parse];
if (fileContent.includes("@flow")) {
parse = babel.parse;
parser = "babel-eslint";
parseFunctions = [babel.parse];
}

try {
return parseSourceFileAsModule(parse, fileContent);
} catch (exceptionAsModule) {
try {
return parseSourceFileAsScript(parse, fileContent);
} catch (exceptionAsScript) {
console.error(message(fileUri, "module", exceptionAsModule, parser));
console.log(message(fileUri, "script", exceptionAsScript, parser, true));
let exceptionToReport: ParseException | null = null;
for (const parseFunction of parseFunctions) {
for (const config of [PARSER_CONFIG_MODULE, PARSER_CONFIG_SCRIPT]) {
const result = parse(parseFunction, config, fileContent);
if (result instanceof SourceCode) {
return result;
} else if (!exceptionToReport) {
exceptionToReport = result;
}
}
}
}

function message(fileUri: string, mode: string, exception: any, parser: string, debug = false) {
return `${debug ? "DEBUG " : ""}Failed to parse file [${fileUri}] at line ${
exception.lineNumber
}: ${exception.message} (with ${parser} parser in ${mode} mode)`;
if (exceptionToReport) {
console.error(
`Failed to parse file [${fileUri}] at line ${exceptionToReport.lineNumber}: ${
exceptionToReport.message
}`,
);
}
}

export function parseSourceFileAsScript(parse: Function, fileContent: string): SourceCode {
const ast = parse(fileContent, PARSER_CONFIG_NOT_STRICT);
return new SourceCode(fileContent, ast);
export function parse(
parse: Function,
config: Linter.ParserOptions,
fileContent: string,
): SourceCode | ParseException {
try {
const ast = parse(fileContent, config);
return new SourceCode(fileContent, ast);
} catch (exception) {
return exception;
}
}

export function parseSourceFileAsModule(parse: Function, fileContent: string): SourceCode {
const ast = parse(fileContent, PARSER_CONFIG);
return new SourceCode(fileContent, ast);
}
export type ParseException = {
lineNumber: number;
message: string;
};
140 changes: 71 additions & 69 deletions eslint-bridge/tests/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,120 @@
import { parseSourceFile, parseSourceFileAsModule, parseSourceFileAsScript } from "../src/parser";
import {
parseSourceFile,
parse,
PARSER_CONFIG_MODULE,
ParseException,
PARSER_CONFIG_SCRIPT,
} from "../src/parser";
import * as espree from "espree";
import { SourceCode } from "eslint";

describe("parseSourceFile", () => {
beforeEach(() => {
console.error = jest.fn();
console.log = jest.fn();
});

afterEach(() => {
jest.resetAllMocks();
});

it("should not parse when invalid code and log reason", () => {
expect(parseSourceFile("export { a, a }", "foo.js")).toBeUndefined();
expect(console.error).toBeCalledWith(
"Failed to parse file [foo.js] at line 1: Duplicate export 'a' (with espree parser in module mode)",
);
expect(console.log).toBeCalledWith(
"DEBUG Failed to parse file [foo.js] at line 1: 'import' and 'export' may appear only with 'sourceType: module' (with espree parser in script mode)",
);

expect(parseSourceFile("export Foo from 'Foo'", "foo.js")).toBeUndefined();
});

it("should parse jsx", () => {
const sourceCode = parseSourceFile("const foo = <div>bar</div>;", "foo.js");
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
expect(console.error).toBeCalledTimes(0);
expectToParse("const foo = <div>bar</div>;");
});

it("should parse flow when with @flow", () => {
expect(parseSourceFile("/* @flow */ const foo: string = 'hello';", "foo.js")).toBeDefined();
expect(parseSourceFile("/* @flow */ var eval = 42", "foo.js")).toBeDefined();
expect(parseSourceFile("const foo: string = 'hello';", "foo.js")).toBeUndefined();
});

it("should parse scripts (with retry after module)", () => {
expect(parseSourceFile("var eval = 42", "foo.js").ast).toBeDefined();
expect(console.error).toBeCalledTimes(0);
expectToParse("/* @flow */ const foo: string = 'hello';");
expectToParse("/* @flow */ var eval = 42");
// even without @flow annotation
expectToParse("const foo: string = 'hello';");
});

it("should parse as script (non-strict mode)", () => {
expectToParseInNonStrictMode(`var eval = 42`, `"Binding eval in strict mode"`);
expectToParseInNonStrictMode(`eval = 42`, `"Assigning to eval in strict mode"`);
expectToParseInNonStrictMode(`var eval = 42`, `Binding eval in strict mode`);
expectToParseInNonStrictMode(`eval = 42`, `Assigning to eval in strict mode`);
expectToParseInNonStrictMode(
`function foo() {}\n var foo = 42;`,
`"Identifier 'foo' has already been declared"`,
`Identifier 'foo' has already been declared`,
);

expectToParseInNonStrictMode(`x = 043;`, `"Invalid number"`);
expectToParseInNonStrictMode(`'\\033'`, `"Octal literal in strict mode"`);
expectToParseInNonStrictMode(`with (a) {}`, `"'with' in strict mode"`);
expectToParseInNonStrictMode(`public = 42`, `"The keyword 'public' is reserved"`);
expectToParseInNonStrictMode(`function foo(a, a) {}`, `"Argument name clash"`);
expectToParseInNonStrictMode(`delete x`, `"Deleting local variable in strict mode"`);

function expectToParseInNonStrictMode(sourceCode, msgInStrictMode) {
expect(() =>
parseSourceFileAsModule(espree.parse, sourceCode),
).toThrowErrorMatchingInlineSnapshot(msgInStrictMode);
expect(parseSourceFileAsScript(espree.parse, sourceCode)).toBeDefined();
}
expectToParseInNonStrictMode(`x = 043;`, `Invalid number`);
expectToParseInNonStrictMode(`'\\033'`, `Octal literal in strict mode`);
expectToParseInNonStrictMode(`with (a) {}`, `'with' in strict mode`);
expectToParseInNonStrictMode(`public = 42`, `The keyword 'public' is reserved`);
expectToParseInNonStrictMode(`function foo(a, a) {}`, `Argument name clash`);
expectToParseInNonStrictMode(`delete x`, `Deleting local variable in strict mode`);
});

it("should parse recent javascript syntax", () => {
let sourceCode;
// ES2018
sourceCode = parseSourceFile(
expectToParse(
`const obj = {foo: 1, bar: 2, baz: 3};
const {foo, ...rest} = obj;`,
"foo.js",
);
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
// ES2017
sourceCode = parseSourceFile(
expectToParse(
`async function f() {
await readFile();
}`,
"foo.js",
);
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
// ES2016
sourceCode = parseSourceFile(`4**2`, "foo.js");
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
expectToParse(`4**2`);
// ES2015
sourceCode = parseSourceFile(`const f = (x, y) => x + y`, "foo.js");
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
expectToParse(`const f = (x, y) => x + y`);

// Modules
sourceCode = parseSourceFile(
expectToParse(
`import * as Foo from "foo";
export class A{}`,
"foo.js",
);
expect(sourceCode.ast.body.length).toBeGreaterThan(0);

expect(console.error).toBeCalledTimes(0);
});

it("should log when parse errors", () => {
const sourceCode = parseSourceFile("if()", "foo.js");
expect(sourceCode).toBeUndefined();
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
`Failed to parse file [foo.js] at line 1: Unexpected token ) (with espree parser in module mode)`,
it("should parse next javascript syntax", () => {
let sourceCode;
// ES2019
sourceCode = parseSourceFile(`try {} catch {}`, "foo.js");
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
// next
// class fields
expectToParse(`class A {
static a = 1;
b = 2
}`);
// private fields are not supported
expectToNotParse(
`class A { static #x = 2
#privateMethod() { this.#privateField = 42; }
#privateField = 42
set #x(value) {} }`,
"Unexpected character '#'",
);
});

it("should log when parse errors with @flow", () => {
const sourceCode = parseSourceFile("/* @flow */ if()", "foo.js");
expect(sourceCode).toBeUndefined();
expect(console.error).toHaveBeenCalledTimes(1);
expect(console.error).toHaveBeenCalledWith(
"Failed to parse file [foo.js] at line 1: Unexpected token (1:15) (with babel-eslint parser in module mode)",
);
it("should log when parse errors", () => {
expectToNotParse("if()", "Unexpected token )");
expectToNotParse("/* @flow */ if()", "Unexpected token (1:15)");
});
});

function expectToParse(code: string) {
const sourceCode = parseSourceFile(code, "foo.js");
expect(sourceCode).toBeDefined();
expect(sourceCode.ast.body.length).toBeGreaterThan(0);
expect(console.error).toBeCalledTimes(0);
}

function expectToNotParse(code: string, message: string) {
const sourceCode = parseSourceFile(code, "foo.js");
expect(sourceCode).toBeUndefined();
expect(console.error).toHaveBeenCalledWith("Failed to parse file [foo.js] at line 1: " + message);
expect(console.error).toBeCalledTimes(1);
jest.resetAllMocks();
}

function expectToParseInNonStrictMode(code: string, msgInStrictMode: string) {
const result1 = parse(espree.parse, PARSER_CONFIG_MODULE, code);
expect((result1 as ParseException).message).toEqual(msgInStrictMode);

const result2 = parse(espree.parse, PARSER_CONFIG_SCRIPT, code);
expect((result2 as SourceCode).ast.body.length).toBeGreaterThan(0);
}