import path from "path"; import escope from "eslint-scope"; import unpad from "dedent"; import { fileURLToPath } from "url"; import { createRequire } from "module"; import { parseForESLint } from "../lib/index.cjs"; function parseForESLint8(code, options) { const { ESLINT_VERSION_FOR_BABEL } = process.env; process.env.ESLINT_VERSION_FOR_BABEL = "8"; try { return parseForESLint(code, options); } finally { process.env.ESLINT_VERSION_FOR_BABEL = ESLINT_VERSION_FOR_BABEL; } } const dirname = path.dirname(fileURLToPath(import.meta.url)); const BABEL_OPTIONS = { configFile: path.resolve( dirname, "../../babel-eslint-shared-fixtures/config/babel.config.js", ), }; const PROPS_TO_REMOVE = [ "importKind", "exportKind", "variance", "typeArguments", "filename", "identifierName", ]; function deeplyRemoveProperties(obj, props) { for (const [k, v] of Object.entries(obj)) { if (typeof v === "object") { if (Array.isArray(v)) { for (const el of v) { if (el != null) { deeplyRemoveProperties(el, props); } } } if (props.includes(k)) { delete obj[k]; } else if (v != null) { deeplyRemoveProperties(v, props); } continue; } if (props.includes(k)) { delete obj[k]; } } } describe("Babel and Espree", () => { let espree7, espree8; const espreeOptions = { ecmaFeatures: { // enable JSX parsing jsx: true, // enable return in global scope globalReturn: true, // enable implied strict mode (if ecmaVersion >= 5) impliedStrict: true, }, tokens: true, loc: true, range: true, comment: true, sourceType: "module", }; function parseAndAssertSame( code, eslintVersion = undefined, babelEcmaFeatures = null, ) { code = unpad(code); if (eslintVersion !== 8) { // ESLint 7 const espreeAST = espree7.parse(code, { ...espreeOptions, ecmaVersion: 2021, }); const babelAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, ecmaFeatures: babelEcmaFeatures, }).ast; deeplyRemoveProperties(babelAST, PROPS_TO_REMOVE); expect(babelAST).toEqual(espreeAST); } if (eslintVersion !== 7) { if (process.env.IS_PUBLISH) { console.warn("Skipping ESLint 8 test because using a release build."); return; } // ESLint 8 const espreeAST = espree8.parse(code, { ...espreeOptions, ecmaVersion: 2022, }); const babelAST = parseForESLint8(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, ecmaFeatures: babelEcmaFeatures, }).ast; deeplyRemoveProperties(babelAST, PROPS_TO_REMOVE); expect(babelAST).toEqual(espreeAST); } } beforeAll(() => { const require = createRequire(import.meta.url); // Use the version of Espree that is a dependency of // the version of ESLint we are testing against. const espree7Path = require.resolve("espree", { paths: [path.dirname(require.resolve("eslint"))], }); const espree8Path = require.resolve("espree", { paths: [path.dirname(require.resolve("eslint-8"))], }); espree7 = require(espree7Path); espree8 = require(espree8Path); const espree7pkg = require(require.resolve("espree/package.json", { paths: [path.dirname(require.resolve("eslint"))], })); const espree8pkg = require(require.resolve("espree/package.json", { paths: [path.dirname(require.resolve("eslint-8"))], })); if (!espree7pkg.version.startsWith("7.")) { throw new Error(`Expected espree 7, but found ${espree7pkg.version}`); } if (!espree8pkg.version.startsWith("9.")) { throw new Error(`Expected espree 9, but found ${espree8pkg.version}`); } }); describe("compatibility", () => { it("should allow ast.analyze to be called without options", () => { const ast = parseForESLint("`test`", { eslintScopeManager: true, eslintVisitorKeys: true, babelOptions: BABEL_OPTIONS, }).ast; expect(() => { escope.analyze(ast); }).not.toThrow(new TypeError("Should allow no options argument.")); }); it("should not crash when `loadPartialConfigSync` returns `null`", () => { const thunk = () => parseForESLint("`test`", { eslintScopeManager: true, eslintVisitorKeys: true, babelOptions: { filename: "test.js", ignore: [/./] }, }); expect(thunk).not.toThrow(); }); }); describe("templates", () => { it("empty template string", () => { parseAndAssertSame("``"); }); it("template string", () => { parseAndAssertSame("`test`"); }); it("template string using $", () => { parseAndAssertSame("`$`"); }); it("template string with expression", () => { parseAndAssertSame("`${a}`"); }); it("template string with multiple expressions", () => { parseAndAssertSame("`${a}${b}${c}`"); }); it("template string with expression and strings", () => { parseAndAssertSame("`a${a}a`"); }); it("template string with binary expression", () => { parseAndAssertSame("`a${a + b}a`"); }); it("tagged template", () => { parseAndAssertSame("jsx``"); }); it("tagged template with expression", () => { parseAndAssertSame("jsx``"); }); it("tagged template with new operator", () => { parseAndAssertSame("new raw`42`"); }); it("template with nested function/object", () => { parseAndAssertSame( "`outer${{x: {y: 10}}}bar${`nested${function(){return 1;}}endnest`}end`", ); }); it("template with braces inside and outside of template string #96", () => { parseAndAssertSame( "if (a) { var target = `{}a:${webpackPort}{}}}}`; } else { app.use(); }", ); }); it("template also with braces #96", () => { parseAndAssertSame(` export default function f1() { function f2(foo) { const bar = 3; return \`\${foo} \${bar}\`; } return f2; } `); }); it("template with destructuring #31", () => { parseAndAssertSame(` module.exports = { render() { var {name} = this.props; return Math.max(null, \`Name: \${name}, Name: \${name}\`); } }; `); }); it("template with arrow returning template #603", () => { parseAndAssertSame(` var a = \`\${() => { \`\${''}\` }}\`; `); }); it("template string with object with template string inside", () => { parseAndAssertSame("`${ { a:`${2}` } }`"); }); }); it("simple expression", () => { parseAndAssertSame("a = 1"); }); it("logical NOT", () => { parseAndAssertSame("!0"); }); it("bitwise NOT", () => { parseAndAssertSame("~0"); }); it("class declaration", () => { parseAndAssertSame("class Foo {}"); }); it("class expression", () => { parseAndAssertSame("var a = class Foo {}"); }); it("jsx expression", () => { parseAndAssertSame(""); }); it("jsx expression with 'this' as identifier", () => { parseAndAssertSame(""); }); it("jsx expression with a dynamic attribute", () => { parseAndAssertSame(""); }); it("jsx expression with a member expression as identifier", () => { parseAndAssertSame(""); }); it("jsx expression with spread", () => { parseAndAssertSame("var myDivElement =
;"); }); it("empty jsx text", () => { parseAndAssertSame(""); }); it("jsx text with content", () => { parseAndAssertSame("Hello, world!"); }); it("nested jsx", () => { parseAndAssertSame("
\n

Wat

\n
"); }); it("default import", () => { parseAndAssertSame('import foo from "foo";'); }); it("import specifier", () => { parseAndAssertSame('import { foo } from "foo";'); }); it("import specifier with name", () => { parseAndAssertSame('import { foo as bar } from "foo";'); }); it("import bare", () => { parseAndAssertSame('import "foo";'); }); it("import meta", () => { parseAndAssertSame("const url = import.meta.url"); }); it("export default class declaration", () => { parseAndAssertSame("export default class Foo {}"); }); it("export default class expression", () => { parseAndAssertSame("export default class {}"); }); it("export default function declaration", () => { parseAndAssertSame("export default function Foo() {}"); }); it("export default function expression", () => { parseAndAssertSame("export default function () {}"); }); it("export all", () => { parseAndAssertSame('export * from "foo";'); }); it("export * as ns", () => { parseAndAssertSame('export * as Foo from "foo";'); }); it("export named", () => { parseAndAssertSame("var foo = 1;export { foo };"); }); it("export named alias", () => { parseAndAssertSame("var foo = 1;export { foo as bar };"); }); it("optional chaining operator", () => { parseAndAssertSame("foo?.bar?.().qux()"); }); it("nullish coalescing operator", () => { parseAndAssertSame("foo ?? bar"); }); it("logical assignment", () => { parseAndAssertSame("foo ??= bar &&= qux ||= quux"); }); it("numeric separator", () => { parseAndAssertSame("1_0.0_0e0_1"); }); // Espree doesn't support the pipeline operator yet it("pipeline operator (token)", () => { const code = "foo |> bar"; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; expect(babylonAST.tokens[1].type).toEqual("Punctuator"); }); it("brace and bracket hash operator (token)", () => { const code = "#[]; #{}"; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: { filename: "test.js", parserOpts: { plugins: [["recordAndTuple", { syntaxType: "hash" }]], tokens: true, }, }, }).ast; expect(babylonAST.tokens[0]).toEqual( expect.objectContaining({ type: "Punctuator", value: "#[" }), ); expect(babylonAST.tokens[3]).toEqual( expect.objectContaining({ type: "Punctuator", value: "#{" }), ); }); it("brace and bracket bar operator (token)", () => { const code = "{||}; [||]"; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: { filename: "test.js", parserOpts: { plugins: [["recordAndTuple", { syntaxType: "bar" }]], tokens: true, }, }, }).ast; expect(babylonAST.tokens[0]).toEqual( expect.objectContaining({ type: "Punctuator", value: "{|" }), ); expect(babylonAST.tokens[1]).toEqual( expect.objectContaining({ type: "Punctuator", value: "|}" }), ); expect(babylonAST.tokens[3]).toEqual( expect.objectContaining({ type: "Punctuator", value: "[|" }), ); expect(babylonAST.tokens[4]).toEqual( expect.objectContaining({ type: "Punctuator", value: "|]" }), ); }); if (process.env.BABEL_8_BREAKING) { it("private identifier (token) - ESLint 7", () => { const code = "class A { #x }"; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; expect(babylonAST.tokens[3].type).toEqual("PrivateIdentifier"); expect(babylonAST.tokens[3].value).toEqual("x"); }); } else { it("hash (token) - ESLint 7", () => { const code = "class A { #x }"; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; expect(babylonAST.tokens[3].type).toEqual("Punctuator"); expect(babylonAST.tokens[3].value).toEqual("#"); }); } const itNotPublish = process.env.IS_PUBLISH ? it.skip : it; itNotPublish("private identifier (token) - ESLint 8", () => { const code = "class A { #x }"; const babylonAST = parseForESLint8(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; expect(babylonAST.tokens[3].type).toEqual("PrivateIdentifier"); expect(babylonAST.tokens[3].value).toEqual("x"); }); it("static (token)", () => { const code = ` import { static as foo } from "foo"; class A { static m() {} static() {} } `; parseAndAssertSame(code); const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; const staticKw = { type: "Keyword", value: "static" }; expect(babylonAST.tokens[2]).toMatchObject(staticKw); expect(babylonAST.tokens[12]).toMatchObject(staticKw); expect(babylonAST.tokens[18]).toMatchObject(staticKw); }); it("parse to PropertyDeclaration when `classFeatures: true`", () => { const code = "class A { #x }"; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: { filename: "test.js", parserOpts: { plugins: [ ["estree", { classFeatures: true }], "classPrivateProperties", "classProperties", ], }, }, }).ast; const classDeclaration = babylonAST.body[0]; expect(classDeclaration.body.body[0].type).toEqual("PropertyDefinition"); }); it("class fields with ESLint 8", () => { parseAndAssertSame( ` class A { x = 2; static #y = 3; asi #m() {} } `, 8, ); }); it("static (token) - ESLint 7", () => { const code = ` class A { static m() {} static() {} static x; static #y; static; static = 2; } `; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; const staticKw = { type: "Keyword", value: "static" }; expect(babylonAST.tokens[3]).toMatchObject(staticKw); expect(babylonAST.tokens[9]).toMatchObject(staticKw); expect(babylonAST.tokens[14]).toMatchObject(staticKw); expect(babylonAST.tokens[17]).toMatchObject(staticKw); expect( babylonAST.tokens[process.env.BABEL_8_BREAKING ? 20 : 21], ).toMatchObject(staticKw); expect( babylonAST.tokens[process.env.BABEL_8_BREAKING ? 22 : 23], ).toMatchObject(staticKw); }); itNotPublish("static (token) - ESLint 8", () => { const code = ` class A { static m() {} static() {} static x; static #y; static; static = 2; } `; const babylonAST = parseForESLint8(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: BABEL_OPTIONS, }).ast; const staticKw = { type: "Keyword", value: "static" }; expect(babylonAST.tokens[3]).toMatchObject(staticKw); expect(babylonAST.tokens[9]).toMatchObject(staticKw); expect(babylonAST.tokens[14]).toMatchObject(staticKw); expect(babylonAST.tokens[17]).toMatchObject(staticKw); expect(babylonAST.tokens[20]).toMatchObject(staticKw); expect(babylonAST.tokens[22]).toMatchObject(staticKw); }); if (process.env.BABEL_8_BREAKING) { it("pipeline # topic token - ESLint 7", () => { const code = ` x |> # y |> #[0] class A { #x = y |> # z } `; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: { filename: "test.js", parserOpts: { plugins: [ ["pipelineOperator", { proposal: "hack", topicToken: "#" }], ], tokens: true, }, }, }).ast; const topicToken = { type: "Punctuator", value: "#" }; expect(babylonAST.tokens[2]).toMatchObject(topicToken); expect(babylonAST.tokens[5]).toMatchObject(topicToken); expect(babylonAST.tokens[16]).toMatchObject(topicToken); }); } else { it("pipeline # topic token - ESLint 7", () => { const code = ` x |> # y |> #[0] class A { #x = y |> # z } `; const babylonAST = parseForESLint(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: { filename: "test.js", parserOpts: { plugins: [ ["pipelineOperator", { proposal: "hack", topicToken: "#" }], ], tokens: true, }, }, }).ast; const topicToken = { type: "Punctuator", value: "#" }; expect(babylonAST.tokens[2]).toMatchObject(topicToken); expect(babylonAST.tokens[5]).toMatchObject(topicToken); expect(babylonAST.tokens[17]).toMatchObject(topicToken); }); } itNotPublish("pipeline # topic token - ESLint 8", () => { const code = ` x |> # y |> #[0] class A { #x = y |> # z } `; const babylonAST = parseForESLint8(code, { eslintVisitorKeys: true, eslintScopeManager: true, babelOptions: { filename: "test.js", parserOpts: { plugins: [ ["pipelineOperator", { proposal: "hack", topicToken: "#" }], ], tokens: true, }, }, }).ast; const topicToken = { type: "Punctuator", value: "#" }; expect(babylonAST.tokens[2]).toMatchObject(topicToken); expect(babylonAST.tokens[5]).toMatchObject(topicToken); expect(babylonAST.tokens[16]).toMatchObject(topicToken); }); it("empty program with line comment", () => { parseAndAssertSame("// single comment"); }); it("empty program with block comment", () => { parseAndAssertSame(" /* multiline\n * comment\n*/"); }); it("line comments", () => { parseAndAssertSame(` // single comment var foo = 15; // comment next to statement // second comment after statement `); }); it("block comments", () => { parseAndAssertSame(` /* single comment */ var foo = 15; /* comment next to statement */ /* * multiline * comment */ `); }); it("block comments #124", () => { parseAndAssertSame(` React.createClass({ render() { // return ( //
// ); // <-- this is the line that is reported } }); `); }); it("null", () => { parseAndAssertSame("null"); }); it("boolean", () => { parseAndAssertSame("if (true) {} else if (false) {}"); }); it("regexp", () => { parseAndAssertSame("/affix-top|affix-bottom|affix|[a-z]/"); }); it("regexp", () => { parseAndAssertSame("const foo = /foo/;"); }); it("regexp y flag", () => { parseAndAssertSame("const foo = /foo/y;"); }); it("regexp u flag", () => { parseAndAssertSame("const foo = /foo/u;"); }); it("regexp in a template string", () => { parseAndAssertSame('`${/\\d/.exec("1")[0]}`'); }); it("first line is empty", () => { parseAndAssertSame('\nimport Immutable from "immutable";'); }); it("empty", () => { parseAndAssertSame(""); }); it("jsdoc", () => { parseAndAssertSame(` /** * @param {object} options * @return {number} */ const test = function({ a, b, c }) { return a + b + c; }; module.exports = test; `); }); it("empty block with comment", () => { parseAndAssertSame(` function a () { try { b(); } catch (e) { // asdf } } `); }); describe("babel tests", () => { it("MethodDefinition", () => { parseAndAssertSame(` export default class A { a() {} } `); }); it("MethodDefinition 2", () => { parseAndAssertSame( "export default class Bar { get bar() { return 42; }}", ); }); it("ClassMethod", () => { parseAndAssertSame(` class A { constructor() { } } `); }); it("ClassMethod multiple params", () => { parseAndAssertSame(` class A { constructor(a, b, c) { } } `); }); it("ClassMethod multiline", () => { parseAndAssertSame(` class A { constructor ( a, b, c ) { } } `); }); it("ClassMethod oneline", () => { parseAndAssertSame("class A { constructor(a, b, c) {} }"); }); it("ObjectMethod", () => { parseAndAssertSame(` var a = { b(c) { } } `); }); it("do not allow import export everywhere", () => { expect(() => { parseForESLint('function F() { import a from "a"; }', { babelOptions: BABEL_OPTIONS, }); }).toThrow(/'import' and 'export' may only appear at the top level/); }); it("allowImportExportEverywhere", () => { expect(() => { parseForESLint('function F() { import a from "a"; }', { babelOptions: { ...BABEL_OPTIONS, parserOpts: { allowImportExportEverywhere: true, }, }, }); }).not.toThrow(); }); if (!process.env.BABEL_8_BREAKING) { it("top-level allowImportExportEverywhere", () => { expect(() => { parseForESLint('function F() { import a from "a"; }', { babelOptions: BABEL_OPTIONS, allowImportExportEverywhere: true, }); }).not.toThrow(); }); } if (process.env.BABEL_8_BREAKING) { it("return outside function with ecmaFeatures.globalReturn: true", () => { parseAndAssertSame("return;", /* version */ undefined, { globalReturn: true, }); }); it("return outside function with ecmaFeatures.globalReturn: false", () => { expect(() => parseForESLint("return;", { babelOptions: BABEL_OPTIONS, ecmaVersion: { globalReturn: false }, }), ).toThrow(new SyntaxError("'return' outside of function. (1:0)")); expect(() => parseForESLint8("return;", { babelOptions: BABEL_OPTIONS, ecmaVersion: { globalReturn: false }, }), ).toThrow(new SyntaxError("'return' outside of function. (1:0)")); }); it("return outside function without ecmaFeatures.globalReturn", () => { expect(() => parseForESLint("return;", { babelOptions: BABEL_OPTIONS }), ).toThrow(new SyntaxError("'return' outside of function. (1:0)")); expect(() => parseForESLint8("return;", { babelOptions: BABEL_OPTIONS }), ).toThrow(new SyntaxError("'return' outside of function. (1:0)")); }); } else { it("return outside function", () => { parseAndAssertSame("return;"); }); } if (process.env.BABEL_8_BREAKING) { it("super outside method", () => { expect(() => { parseForESLint("function F() { super(); }", { babelOptions: BABEL_OPTIONS, }); }).toThrow( /`super\(\)` is only valid inside a class constructor of a subclass\./, ); }); it("super outside method - enabled", () => { expect(() => { parseForESLint("function F() { super(); }", { babelOptions: { ...BABEL_OPTIONS, parserOpts: { allowSuperOutsideMethod: true, }, }, }); }).not.toThrow(); }); } else { it("super outside method", () => { expect(() => { parseForESLint("function F() { super(); }", { babelOptions: BABEL_OPTIONS, }); }).not.toThrow(); }); it("super outside method - disabled", () => { expect(() => { parseForESLint("function F() { super(); }", { babelOptions: { ...BABEL_OPTIONS, parserOpts: { allowSuperOutsideMethod: false, }, }, }); }).toThrow( /`super\(\)` is only valid inside a class constructor of a subclass\./, ); }); } it("StringLiteral", () => { parseAndAssertSame(""); parseAndAssertSame(""); parseAndAssertSame("a"); }); it("getters and setters", () => { parseAndAssertSame("class A { get x ( ) { ; } }"); parseAndAssertSame(` class A { get x( ) { ; } } `); parseAndAssertSame("class A { set x (a) { ; } }"); parseAndAssertSame(` class A { set x(a ) { ; } } `); parseAndAssertSame(` var B = { get x () { return this.ecks; }, set x (ecks) { this.ecks = ecks; } }; `); }); it("RestOperator", () => { parseAndAssertSame("var { a, ...b } = c"); parseAndAssertSame("var [ a, ...b ] = c"); parseAndAssertSame("var a = function (...b) {}"); }); it("SpreadOperator", () => { parseAndAssertSame("var a = { b, ...c }"); parseAndAssertSame("var a = [ a, ...b ]"); parseAndAssertSame("var a = sum(...b)"); }); it("Async/Await", () => { parseAndAssertSame(` async function a() { await 1; } `); }); it("BigInt", () => { parseAndAssertSame(` const a = 1n; `); }); it("Dynamic Import", () => { parseAndAssertSame(` const a = import(moduleName); `); }); }); });