Skip to content

Commit

Permalink
Add ESLint 8 support to @babel/eslint-parser
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Sep 22, 2021
1 parent 2e2d202 commit 9ab25e8
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 56 deletions.
5 changes: 3 additions & 2 deletions eslint/babel-eslint-parser/package.json
Expand Up @@ -28,7 +28,7 @@
},
"peerDependencies": {
"@babel/core": ">=7.11.0",
"eslint": ">=7.5.0"
"eslint": "^7.5.0 || ^8.0.0-beta.2"
},
"dependencies": {
"eslint-scope": "^5.1.1",
Expand All @@ -38,6 +38,7 @@
"devDependencies": {
"@babel/core": "workspace:*",
"dedent": "^0.7.0",
"eslint": "^7.27.0"
"eslint": "^7.27.0",
"eslint-8": "npm:eslint@^8.0.0-beta.2"
}
}
21 changes: 20 additions & 1 deletion eslint/babel-eslint-parser/src/convert/convertAST.cjs
@@ -1,3 +1,5 @@
const getEslintVersion = require("../utils/get-eslint-version.cjs");

function* it(children) {
if (Array.isArray(children)) yield* children;
else yield children;
Expand Down Expand Up @@ -40,7 +42,7 @@ const convertNodesVisitor = {
delete node.extra;
}

if (node?.loc.identifierName) {
if (node.loc.identifierName) {
delete node.loc.identifierName;
}

Expand Down Expand Up @@ -89,6 +91,15 @@ const convertNodesVisitor = {
} else {
q.loc.end.column += 2;
}

if (getEslintVersion() >= 8) {
q.start -= 1;
if (q.tail) {
q.end += 1;
} else {
q.end += 2;
}
}
}
}
},
Expand Down Expand Up @@ -117,6 +128,10 @@ function convertProgramNode(ast) {
ast.range[1] = lastToken.end;
ast.loc.end.line = lastToken.loc.end.line;
ast.loc.end.column = lastToken.loc.end.column;

if (getEslintVersion() >= 8) {
ast.end = lastToken.end;
}
}
}
} else {
Expand All @@ -129,6 +144,10 @@ function convertProgramNode(ast) {
if (ast.body && ast.body.length > 0) {
ast.loc.start.line = ast.body[0].loc.start.line;
ast.range[0] = ast.body[0].start;

if (getEslintVersion() >= 8) {
ast.start = ast.body[0].start;
}
}
}

Expand Down
37 changes: 32 additions & 5 deletions eslint/babel-eslint-parser/src/convert/convertTokens.cjs
@@ -1,3 +1,5 @@
const getEslintVersion = require("../utils/get-eslint-version.cjs");

function convertTemplateType(tokens, tl) {
let curlyBrace = null;
let templateTokens = [];
Expand Down Expand Up @@ -190,12 +192,37 @@ function convertToken(token, source, tl) {
// Acorn does not have rightAssociative
delete token.type.rightAssociative;
}

return token;
}

module.exports = function convertTokens(tokens, code, tl) {
return convertTemplateType(tokens, tl)
.filter(t => t.type !== "CommentLine" && t.type !== "CommentBlock")
.map(t => convertToken(t, code, tl));
const result = [];

const withoutComments = convertTemplateType(tokens, tl).filter(
t => t.type !== "CommentLine" && t.type !== "CommentBlock",
);
for (let i = 0; i < withoutComments.length; i++) {
let token = withoutComments[i];

if (!process.env.BABEL_8_BREAKING) {
// Babel 8 already produces a single token

if (getEslintVersion() >= 8 && token.type.label === tl.hash) {
i++;
token = withoutComments[i];

token.type = "PrivateIdentifier";
token.start -= 1;
token.loc.start.column -= 1;
token.range = [token.start, token.end];

result.push(token);
continue;
}
}

convertToken(token, code, tl);
result.push(token);
}

return result;
};
6 changes: 6 additions & 0 deletions eslint/babel-eslint-parser/src/utils/get-eslint-version.cjs
@@ -0,0 +1,6 @@
module.exports = function eslintVersion() {
const version =
process.env.ESLINT_VERSION_FOR_BABEL ||
require("eslint/package.json").version;
return parseInt(version, 10);
};
4 changes: 2 additions & 2 deletions eslint/babel-eslint-parser/src/worker/configuration.cjs
@@ -1,4 +1,5 @@
const babel = require("./babel-core.cjs");
const getEslintVersion = require("../utils/get-eslint-version.cjs");

/**
* Merge user supplied estree plugin options to default estree plugin options
Expand All @@ -8,8 +9,7 @@ const babel = require("./babel-core.cjs");
*/
function getParserPlugins(babelOptions) {
const babelParserPlugins = babelOptions.parserOpts?.plugins ?? [];
// todo: enable classFeatures when it is supported by ESLint
const estreeOptions = { classFeatures: false };
const estreeOptions = { classFeatures: getEslintVersion() >= 8 };
for (const plugin of babelParserPlugins) {
if (Array.isArray(plugin) && plugin[0] === "estree") {
Object.assign(estreeOptions, plugin[1]);
Expand Down
190 changes: 158 additions & 32 deletions eslint/babel-eslint-parser/test/index.js
Expand Up @@ -5,6 +5,16 @@ 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 = {
Expand All @@ -18,6 +28,8 @@ const PROPS_TO_REMOVE = [
"exportKind",
"variance",
"typeArguments",
"filename",
"identifierName",
];

function deeplyRemoveProperties(obj, props) {
Expand Down Expand Up @@ -46,47 +58,77 @@ function deeplyRemoveProperties(obj, props) {
}

describe("Babel and Espree", () => {
let espree;

function parseAndAssertSame(code) {
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,
// allow experimental object rest/spread
experimentalObjectRestSpread: true,
},
tokens: true,
loc: true,
range: true,
comment: true,
sourceType: "module",
};

function parseAndAssertSame(code, /* optional */ eslintVersion) {
code = unpad(code);
const espreeAST = espree.parse(code, {
ecmaFeatures: {
// enable JSX parsing
jsx: true,
// enable return in global scope
globalReturn: true,
// enable implied strict mode (if ecmaVersion >= 5)
impliedStrict: true,
// allow experimental object rest/spread
experimentalObjectRestSpread: true,
},
tokens: true,
loc: true,
range: true,
comment: true,
ecmaVersion: 2021,
sourceType: "module",
});
const babelAST = parseForESLint(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: BABEL_OPTIONS,
}).ast;
deeplyRemoveProperties(babelAST, PROPS_TO_REMOVE);
expect(babelAST).toEqual(espreeAST);

if (eslintVersion !== 8) {
// ESLint 7
const espreeAST = espree7.parse(code, {
...espreeOptions,
ecmaVersion: 2021,
});
const babelAST = parseForESLint(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: BABEL_OPTIONS,
}).ast;

deeplyRemoveProperties(babelAST, PROPS_TO_REMOVE);
expect(babelAST).toEqual(espreeAST);
}

if (eslintVersion !== 7) {
// ESLint 8
const espreeAST = espree8.parse(code, {
...espreeOptions,
ecmaVersion: 2022,
});

const babelAST = parseForESLint8(code, {
eslintVisitorKeys: true,
eslintScopeManager: true,
babelOptions: BABEL_OPTIONS,
}).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 espreePath = require.resolve("espree", {
const espree7Path = require.resolve("espree", {
paths: [path.dirname(require.resolve("eslint"))],
});
const espree8Path = require.resolve("espree", {
paths: [path.dirname(require.resolve("eslint-8"))],
});

espree = require(espreePath);
espree7 = require(espree7Path);
espree8 = require(espree8Path);
});

describe("compatibility", () => {
Expand Down Expand Up @@ -381,7 +423,7 @@ describe("Babel and Espree", () => {
});

if (process.env.BABEL_8_BREAKING) {
it("hash (token)", () => {
it("hash (token) - ESLint 7", () => {
const code = "class A { #x }";
const babylonAST = parseForESLint(code, {
eslintVisitorKeys: true,
Expand All @@ -393,7 +435,7 @@ describe("Babel and Espree", () => {
});
} else {
// Espree doesn't support private fields yet
it("hash (token)", () => {
it("hash (token) - ESLint 7", () => {
const code = "class A { #x }";
const babylonAST = parseForESLint(code, {
eslintVisitorKeys: true,
Expand All @@ -405,6 +447,18 @@ describe("Babel and Espree", () => {
});
}

it("hash (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";
Expand Down Expand Up @@ -450,6 +504,78 @@ describe("Babel and Espree", () => {
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);
});

it("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);
});

it("empty program with line comment", () => {
parseAndAssertSame("// single comment");
});
Expand Down

0 comments on commit 9ab25e8

Please sign in to comment.