Skip to content

Commit 9d620c2

Browse files
[babel 8] Move ESLint parsing to a Worker (#13199)
1 parent c218134 commit 9d620c2

32 files changed

Lines changed: 610 additions & 330 deletions

.eslintrc.cjs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ module.exports = {
3737
},
3838
{
3939
files: [
40-
"packages/*/src/**/*.{js,ts}",
41-
"codemods/*/src/**/*.{js,ts}",
42-
"eslint/*/src/**/*.{js,ts}",
40+
"packages/*/src/**/*.{js,ts,cjs}",
41+
"codemods/*/src/**/*.{js,ts,cjs}",
42+
"eslint/*/src/**/*.{js,ts,cjs}",
4343
],
4444
rules: {
4545
"@babel/development/no-undefined-identifier": "error",
@@ -130,6 +130,12 @@ module.exports = {
130130
],
131131
},
132132
},
133+
{
134+
files: ["eslint/babel-eslint-parser/src/**/*.js"],
135+
rules: {
136+
"no-restricted-imports": ["error", "@babel/core"],
137+
},
138+
},
133139
{
134140
files: ["packages/babel-plugin-transform-runtime/scripts/**/*.js"],
135141
rules: {

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ jobs:
196196
BABEL_ENV: test
197197
BABEL_8_BREAKING: true
198198
STRIP_BABEL_8_FLAG: true
199+
- name: Lint
200+
run: make lint
201+
env:
202+
BABEL_ENV: test
203+
BABEL_8_BREAKING: true
204+
BABEL_TYPES_8_BREAKING: true
199205
- name: Test
200206
# Hack: --color has supports-color@5 returned true for GitHub CI
201207
# Remove once `chalk` is bumped to 4.0.

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"printWidth": 80,
1111
"overrides": [{
1212
"files": [
13-
"**/{codemods,eslint,packages}/*/{src,test}/**/*.{js,ts}"
13+
"**/{codemods,eslint,packages}/*/{src,test}/**/*.{js,ts,cjs}"
1414
],
1515
"excludeFiles": ["**/packages/babel-helpers/src/helpers/**/*.js"],
1616
"options": {

eslint/babel-eslint-parser/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
"engines": {
2020
"node": "^10.13.0 || ^12.13.0 || >=14.0.0"
2121
},
22-
"main": "./lib/index.js",
22+
"main": "./lib/index.cjs",
2323
"type": "commonjs",
2424
"exports": {
25-
".": "./lib/index.js",
25+
".": "./lib/index.cjs",
2626
"./package.json": "./package.json"
2727
},
2828
"peerDependencies": {

eslint/babel-eslint-parser/src/analyze-scope.js renamed to eslint/babel-eslint-parser/src/analyze-scope.cjs

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
1-
import { types as t } from "@babel/core";
2-
import escope from "eslint-scope";
3-
import { Definition } from "eslint-scope/lib/definition";
4-
import OriginalPatternVisitor from "eslint-scope/lib/pattern-visitor";
5-
import OriginalReferencer from "eslint-scope/lib/referencer";
6-
import { getKeys as fallback } from "eslint-visitor-keys";
7-
import childVisitorKeys from "./visitor-keys";
8-
9-
const flowFlippedAliasKeys = t.FLIPPED_ALIAS_KEYS.Flow.concat([
10-
"ArrayPattern",
11-
"ClassDeclaration",
12-
"ClassExpression",
13-
"FunctionDeclaration",
14-
"FunctionExpression",
15-
"Identifier",
16-
"ObjectPattern",
17-
"RestElement",
18-
]);
19-
20-
const visitorKeysMap = Object.entries(t.VISITOR_KEYS).reduce(
21-
(acc, [key, value]) => {
1+
const escope = require("eslint-scope");
2+
const { Definition } = require("eslint-scope/lib/definition");
3+
const OriginalPatternVisitor = require("eslint-scope/lib/pattern-visitor");
4+
const OriginalReferencer = require("eslint-scope/lib/referencer");
5+
const { getKeys: fallback } = require("eslint-visitor-keys");
6+
7+
const { getTypesInfo, getVisitorKeys } = require("./client.cjs");
8+
9+
let visitorKeysMap;
10+
function getVisitorValues(nodeType) {
11+
if (visitorKeysMap) return visitorKeysMap[nodeType];
12+
13+
const { FLOW_FLIPPED_ALIAS_KEYS, VISITOR_KEYS } = getTypesInfo();
14+
15+
const flowFlippedAliasKeys = FLOW_FLIPPED_ALIAS_KEYS.concat([
16+
"ArrayPattern",
17+
"ClassDeclaration",
18+
"ClassExpression",
19+
"FunctionDeclaration",
20+
"FunctionExpression",
21+
"Identifier",
22+
"ObjectPattern",
23+
"RestElement",
24+
]);
25+
26+
visitorKeysMap = Object.entries(VISITOR_KEYS).reduce((acc, [key, value]) => {
2227
if (!flowFlippedAliasKeys.includes(value)) {
2328
acc[key] = value;
2429
}
2530
return acc;
26-
},
27-
{},
28-
);
31+
}, {});
32+
33+
return visitorKeysMap[nodeType];
34+
}
2935

3036
const propertyTypes = {
3137
// loops
@@ -65,7 +71,7 @@ class Referencer extends OriginalReferencer {
6571

6672
// Visit type annotations.
6773
this._checkIdentifierOrVisit(node.typeAnnotation);
68-
if (t.isAssignmentPattern(node)) {
74+
if (node.type === "AssignmentPattern") {
6975
this._checkIdentifierOrVisit(node.left.typeAnnotation);
7076
}
7177

@@ -258,7 +264,7 @@ class Referencer extends OriginalReferencer {
258264
}
259265

260266
// get property to check (params, id, etc...)
261-
const visitorValues = visitorKeysMap[node.type];
267+
const visitorValues = getVisitorValues(node.type);
262268
if (!visitorValues) {
263269
return;
264270
}
@@ -322,7 +328,7 @@ class Referencer extends OriginalReferencer {
322328
}
323329
}
324330

325-
export default function analyzeScope(ast, parserOptions) {
331+
module.exports = function analyzeScope(ast, parserOptions) {
326332
const options = {
327333
ignoreEval: true,
328334
optimistic: false,
@@ -337,12 +343,12 @@ export default function analyzeScope(ast, parserOptions) {
337343
fallback,
338344
};
339345

340-
options.childVisitorKeys = childVisitorKeys;
346+
options.childVisitorKeys = getVisitorKeys();
341347

342348
const scopeManager = new escope.ScopeManager(options);
343349
const referencer = new Referencer(options, scopeManager);
344350

345351
referencer.visit(ast);
346352

347353
return scopeManager;
348-
}
354+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const path = require("path");
2+
3+
let send;
4+
5+
exports.getVersion = sendCached("GET_VERSION");
6+
7+
exports.getTypesInfo = sendCached("GET_TYPES_INFO");
8+
9+
exports.getVisitorKeys = sendCached("GET_VISITOR_KEYS");
10+
11+
exports.getTokLabels = sendCached("GET_TOKEN_LABELS");
12+
13+
exports.maybeParse = (code, options) => send("MAYBE_PARSE", { code, options });
14+
15+
function sendCached(action) {
16+
let cache = null;
17+
18+
return () => {
19+
if (!cache) cache = send(action, undefined);
20+
return cache;
21+
};
22+
}
23+
24+
if (process.env.BABEL_8_BREAKING) {
25+
const {
26+
Worker,
27+
receiveMessageOnPort,
28+
MessageChannel,
29+
SHARE_ENV,
30+
} = require("worker_threads");
31+
32+
// We need to run Babel in a worker for two reasons:
33+
// 1. ESLint workers must be CJS files, and this is a problem
34+
// since Babel 8+ uses native ESM
35+
// 2. ESLint parsers must run synchronously, but many steps
36+
// of Babel's config loading (which is done for each file)
37+
// can be asynchronous
38+
// If ESLint starts supporting async parsers, we can move
39+
// everything back to the main thread.
40+
const worker = new Worker(
41+
path.resolve(__dirname, "../lib/worker/index.cjs"),
42+
{ env: SHARE_ENV },
43+
);
44+
45+
// The worker will never exit by itself. Prevent it from keeping
46+
// the main process alive.
47+
worker.unref();
48+
49+
const signal = new Int32Array(new SharedArrayBuffer(4));
50+
51+
send = (action, payload) => {
52+
signal[0] = 0;
53+
const subChannel = new MessageChannel();
54+
55+
worker.postMessage({ signal, port: subChannel.port1, action, payload }, [
56+
subChannel.port1,
57+
]);
58+
59+
Atomics.wait(signal, 0, 0);
60+
const { message } = receiveMessageOnPort(subChannel.port2);
61+
62+
if (message.error) throw Object.assign(message.error, message.errorData);
63+
else return message.result;
64+
};
65+
} else {
66+
send = require("./worker/index.cjs");
67+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
exports.normalizeESLintConfig = function (options) {
2+
const {
3+
babelOptions = {},
4+
// ESLint sets ecmaVersion: undefined when ecmaVersion is not set in the config.
5+
ecmaVersion = 2020,
6+
sourceType = "module",
7+
allowImportExportEverywhere = false,
8+
requireConfigFile = true,
9+
...otherOptions
10+
} = options;
11+
12+
return {
13+
babelOptions,
14+
ecmaVersion,
15+
sourceType,
16+
allowImportExportEverywhere,
17+
requireConfigFile,
18+
...otherOptions,
19+
};
20+
};
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
function* it(children) {
2+
if (Array.isArray(children)) yield* children;
3+
else yield children;
4+
}
5+
6+
function traverse(node, visitorKeys, visitor) {
7+
const { type } = node;
8+
if (!type) return;
9+
const keys = visitorKeys[type];
10+
if (!keys) return;
11+
12+
for (const key of keys) {
13+
for (const child of it(node[key])) {
14+
if (child && typeof child === "object") {
15+
visitor.enter(child);
16+
traverse(child, visitorKeys, visitor);
17+
visitor.exit(child);
18+
}
19+
}
20+
}
21+
}
22+
23+
const convertNodesVisitor = {
24+
enter(node) {
25+
if (node.innerComments) {
26+
delete node.innerComments;
27+
}
28+
29+
if (node.trailingComments) {
30+
delete node.trailingComments;
31+
}
32+
33+
if (node.leadingComments) {
34+
delete node.leadingComments;
35+
}
36+
},
37+
exit(node) {
38+
// Used internally by @babel/parser.
39+
if (node.extra) {
40+
delete node.extra;
41+
}
42+
43+
if (node?.loc.identifierName) {
44+
delete node.loc.identifierName;
45+
}
46+
47+
if (node.type === "TypeParameter") {
48+
node.type = "Identifier";
49+
node.typeAnnotation = node.bound;
50+
delete node.bound;
51+
}
52+
53+
// flow: prevent "no-undef"
54+
// for "Component" in: "let x: React.Component"
55+
if (node.type === "QualifiedTypeIdentifier") {
56+
delete node.id;
57+
}
58+
// for "b" in: "var a: { b: Foo }"
59+
if (node.type === "ObjectTypeProperty") {
60+
delete node.key;
61+
}
62+
// for "indexer" in: "var a: {[indexer: string]: number}"
63+
if (node.type === "ObjectTypeIndexer") {
64+
delete node.id;
65+
}
66+
// for "param" in: "var a: { func(param: Foo): Bar };"
67+
if (node.type === "FunctionTypeParam") {
68+
delete node.name;
69+
}
70+
71+
// modules
72+
if (node.type === "ImportDeclaration") {
73+
delete node.isType;
74+
}
75+
76+
// template string range fixes
77+
if (node.type === "TemplateLiteral") {
78+
for (let i = 0; i < node.quasis.length; i++) {
79+
const q = node.quasis[i];
80+
q.range[0] -= 1;
81+
if (q.tail) {
82+
q.range[1] += 1;
83+
} else {
84+
q.range[1] += 2;
85+
}
86+
q.loc.start.column -= 1;
87+
if (q.tail) {
88+
q.loc.end.column += 1;
89+
} else {
90+
q.loc.end.column += 2;
91+
}
92+
}
93+
}
94+
},
95+
};
96+
97+
function convertNodes(ast, visitorKeys) {
98+
traverse(ast, visitorKeys, convertNodesVisitor);
99+
}
100+
101+
function convertProgramNode(ast) {
102+
ast.type = "Program";
103+
ast.sourceType = ast.program.sourceType;
104+
ast.body = ast.program.body;
105+
delete ast.program;
106+
delete ast.errors;
107+
108+
if (ast.comments.length) {
109+
const lastComment = ast.comments[ast.comments.length - 1];
110+
111+
if (ast.tokens.length) {
112+
const lastToken = ast.tokens[ast.tokens.length - 1];
113+
114+
if (lastComment.end > lastToken.end) {
115+
// If there is a comment after the last token, the program ends at the
116+
// last token and not the comment
117+
ast.range[1] = lastToken.end;
118+
ast.loc.end.line = lastToken.loc.end.line;
119+
ast.loc.end.column = lastToken.loc.end.column;
120+
}
121+
}
122+
} else {
123+
if (!ast.tokens.length) {
124+
ast.loc.start.line = 1;
125+
ast.loc.end.line = 1;
126+
}
127+
}
128+
129+
if (ast.body && ast.body.length > 0) {
130+
ast.loc.start.line = ast.body[0].loc.start.line;
131+
ast.range[0] = ast.body[0].start;
132+
}
133+
}
134+
135+
module.exports = function convertAST(ast, visitorKeys) {
136+
convertNodes(ast, visitorKeys);
137+
convertProgramNode(ast);
138+
};

0 commit comments

Comments
 (0)