Skip to content

Commit

Permalink
[babel 8] Move ESLint parsing to a Worker (babel#13199)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo authored May 17, 2021
1 parent c218134 commit 9d620c2
Show file tree
Hide file tree
Showing 32 changed files with 610 additions and 330 deletions.
12 changes: 9 additions & 3 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ module.exports = {
},
{
files: [
"packages/*/src/**/*.{js,ts}",
"codemods/*/src/**/*.{js,ts}",
"eslint/*/src/**/*.{js,ts}",
"packages/*/src/**/*.{js,ts,cjs}",
"codemods/*/src/**/*.{js,ts,cjs}",
"eslint/*/src/**/*.{js,ts,cjs}",
],
rules: {
"@babel/development/no-undefined-identifier": "error",
Expand Down Expand Up @@ -130,6 +130,12 @@ module.exports = {
],
},
},
{
files: ["eslint/babel-eslint-parser/src/**/*.js"],
rules: {
"no-restricted-imports": ["error", "@babel/core"],
},
},
{
files: ["packages/babel-plugin-transform-runtime/scripts/**/*.js"],
rules: {
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ jobs:
BABEL_ENV: test
BABEL_8_BREAKING: true
STRIP_BABEL_8_FLAG: true
- name: Lint
run: make lint
env:
BABEL_ENV: test
BABEL_8_BREAKING: true
BABEL_TYPES_8_BREAKING: true
- name: Test
# Hack: --color has supports-color@5 returned true for GitHub CI
# Remove once `chalk` is bumped to 4.0.
Expand Down
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"printWidth": 80,
"overrides": [{
"files": [
"**/{codemods,eslint,packages}/*/{src,test}/**/*.{js,ts}"
"**/{codemods,eslint,packages}/*/{src,test}/**/*.{js,ts,cjs}"
],
"excludeFiles": ["**/packages/babel-helpers/src/helpers/**/*.js"],
"options": {
Expand Down
4 changes: 2 additions & 2 deletions eslint/babel-eslint-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
"engines": {
"node": "^10.13.0 || ^12.13.0 || >=14.0.0"
},
"main": "./lib/index.js",
"main": "./lib/index.cjs",
"type": "commonjs",
"exports": {
".": "./lib/index.js",
".": "./lib/index.cjs",
"./package.json": "./package.json"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import { types as t } from "@babel/core";
import escope from "eslint-scope";
import { Definition } from "eslint-scope/lib/definition";
import OriginalPatternVisitor from "eslint-scope/lib/pattern-visitor";
import OriginalReferencer from "eslint-scope/lib/referencer";
import { getKeys as fallback } from "eslint-visitor-keys";
import childVisitorKeys from "./visitor-keys";

const flowFlippedAliasKeys = t.FLIPPED_ALIAS_KEYS.Flow.concat([
"ArrayPattern",
"ClassDeclaration",
"ClassExpression",
"FunctionDeclaration",
"FunctionExpression",
"Identifier",
"ObjectPattern",
"RestElement",
]);

const visitorKeysMap = Object.entries(t.VISITOR_KEYS).reduce(
(acc, [key, value]) => {
const escope = require("eslint-scope");
const { Definition } = require("eslint-scope/lib/definition");
const OriginalPatternVisitor = require("eslint-scope/lib/pattern-visitor");
const OriginalReferencer = require("eslint-scope/lib/referencer");
const { getKeys: fallback } = require("eslint-visitor-keys");

const { getTypesInfo, getVisitorKeys } = require("./client.cjs");

let visitorKeysMap;
function getVisitorValues(nodeType) {
if (visitorKeysMap) return visitorKeysMap[nodeType];

const { FLOW_FLIPPED_ALIAS_KEYS, VISITOR_KEYS } = getTypesInfo();

const flowFlippedAliasKeys = FLOW_FLIPPED_ALIAS_KEYS.concat([
"ArrayPattern",
"ClassDeclaration",
"ClassExpression",
"FunctionDeclaration",
"FunctionExpression",
"Identifier",
"ObjectPattern",
"RestElement",
]);

visitorKeysMap = Object.entries(VISITOR_KEYS).reduce((acc, [key, value]) => {
if (!flowFlippedAliasKeys.includes(value)) {
acc[key] = value;
}
return acc;
},
{},
);
}, {});

return visitorKeysMap[nodeType];
}

const propertyTypes = {
// loops
Expand Down Expand Up @@ -65,7 +71,7 @@ class Referencer extends OriginalReferencer {

// Visit type annotations.
this._checkIdentifierOrVisit(node.typeAnnotation);
if (t.isAssignmentPattern(node)) {
if (node.type === "AssignmentPattern") {
this._checkIdentifierOrVisit(node.left.typeAnnotation);
}

Expand Down Expand Up @@ -258,7 +264,7 @@ class Referencer extends OriginalReferencer {
}

// get property to check (params, id, etc...)
const visitorValues = visitorKeysMap[node.type];
const visitorValues = getVisitorValues(node.type);
if (!visitorValues) {
return;
}
Expand Down Expand Up @@ -322,7 +328,7 @@ class Referencer extends OriginalReferencer {
}
}

export default function analyzeScope(ast, parserOptions) {
module.exports = function analyzeScope(ast, parserOptions) {
const options = {
ignoreEval: true,
optimistic: false,
Expand All @@ -337,12 +343,12 @@ export default function analyzeScope(ast, parserOptions) {
fallback,
};

options.childVisitorKeys = childVisitorKeys;
options.childVisitorKeys = getVisitorKeys();

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

referencer.visit(ast);

return scopeManager;
}
};
67 changes: 67 additions & 0 deletions eslint/babel-eslint-parser/src/client.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const path = require("path");

let send;

exports.getVersion = sendCached("GET_VERSION");

exports.getTypesInfo = sendCached("GET_TYPES_INFO");

exports.getVisitorKeys = sendCached("GET_VISITOR_KEYS");

exports.getTokLabels = sendCached("GET_TOKEN_LABELS");

exports.maybeParse = (code, options) => send("MAYBE_PARSE", { code, options });

function sendCached(action) {
let cache = null;

return () => {
if (!cache) cache = send(action, undefined);
return cache;
};
}

if (process.env.BABEL_8_BREAKING) {
const {
Worker,
receiveMessageOnPort,
MessageChannel,
SHARE_ENV,
} = require("worker_threads");

// We need to run Babel in a worker for two reasons:
// 1. ESLint workers must be CJS files, and this is a problem
// since Babel 8+ uses native ESM
// 2. ESLint parsers must run synchronously, but many steps
// of Babel's config loading (which is done for each file)
// can be asynchronous
// If ESLint starts supporting async parsers, we can move
// everything back to the main thread.
const worker = new Worker(
path.resolve(__dirname, "../lib/worker/index.cjs"),
{ env: SHARE_ENV },
);

// The worker will never exit by itself. Prevent it from keeping
// the main process alive.
worker.unref();

const signal = new Int32Array(new SharedArrayBuffer(4));

send = (action, payload) => {
signal[0] = 0;
const subChannel = new MessageChannel();

worker.postMessage({ signal, port: subChannel.port1, action, payload }, [
subChannel.port1,
]);

Atomics.wait(signal, 0, 0);
const { message } = receiveMessageOnPort(subChannel.port2);

if (message.error) throw Object.assign(message.error, message.errorData);
else return message.result;
};
} else {
send = require("./worker/index.cjs");
}
20 changes: 20 additions & 0 deletions eslint/babel-eslint-parser/src/configuration.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
exports.normalizeESLintConfig = function (options) {
const {
babelOptions = {},
// ESLint sets ecmaVersion: undefined when ecmaVersion is not set in the config.
ecmaVersion = 2020,
sourceType = "module",
allowImportExportEverywhere = false,
requireConfigFile = true,
...otherOptions
} = options;

return {
babelOptions,
ecmaVersion,
sourceType,
allowImportExportEverywhere,
requireConfigFile,
...otherOptions,
};
};
138 changes: 138 additions & 0 deletions eslint/babel-eslint-parser/src/convert/convertAST.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
function* it(children) {
if (Array.isArray(children)) yield* children;
else yield children;
}

function traverse(node, visitorKeys, visitor) {
const { type } = node;
if (!type) return;
const keys = visitorKeys[type];
if (!keys) return;

for (const key of keys) {
for (const child of it(node[key])) {
if (child && typeof child === "object") {
visitor.enter(child);
traverse(child, visitorKeys, visitor);
visitor.exit(child);
}
}
}
}

const convertNodesVisitor = {
enter(node) {
if (node.innerComments) {
delete node.innerComments;
}

if (node.trailingComments) {
delete node.trailingComments;
}

if (node.leadingComments) {
delete node.leadingComments;
}
},
exit(node) {
// Used internally by @babel/parser.
if (node.extra) {
delete node.extra;
}

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

if (node.type === "TypeParameter") {
node.type = "Identifier";
node.typeAnnotation = node.bound;
delete node.bound;
}

// flow: prevent "no-undef"
// for "Component" in: "let x: React.Component"
if (node.type === "QualifiedTypeIdentifier") {
delete node.id;
}
// for "b" in: "var a: { b: Foo }"
if (node.type === "ObjectTypeProperty") {
delete node.key;
}
// for "indexer" in: "var a: {[indexer: string]: number}"
if (node.type === "ObjectTypeIndexer") {
delete node.id;
}
// for "param" in: "var a: { func(param: Foo): Bar };"
if (node.type === "FunctionTypeParam") {
delete node.name;
}

// modules
if (node.type === "ImportDeclaration") {
delete node.isType;
}

// template string range fixes
if (node.type === "TemplateLiteral") {
for (let i = 0; i < node.quasis.length; i++) {
const q = node.quasis[i];
q.range[0] -= 1;
if (q.tail) {
q.range[1] += 1;
} else {
q.range[1] += 2;
}
q.loc.start.column -= 1;
if (q.tail) {
q.loc.end.column += 1;
} else {
q.loc.end.column += 2;
}
}
}
},
};

function convertNodes(ast, visitorKeys) {
traverse(ast, visitorKeys, convertNodesVisitor);
}

function convertProgramNode(ast) {
ast.type = "Program";
ast.sourceType = ast.program.sourceType;
ast.body = ast.program.body;
delete ast.program;
delete ast.errors;

if (ast.comments.length) {
const lastComment = ast.comments[ast.comments.length - 1];

if (ast.tokens.length) {
const lastToken = ast.tokens[ast.tokens.length - 1];

if (lastComment.end > lastToken.end) {
// If there is a comment after the last token, the program ends at the
// last token and not the comment
ast.range[1] = lastToken.end;
ast.loc.end.line = lastToken.loc.end.line;
ast.loc.end.column = lastToken.loc.end.column;
}
}
} else {
if (!ast.tokens.length) {
ast.loc.start.line = 1;
ast.loc.end.line = 1;
}
}

if (ast.body && ast.body.length > 0) {
ast.loc.start.line = ast.body[0].loc.start.line;
ast.range[0] = ast.body[0].start;
}
}

module.exports = function convertAST(ast, visitorKeys) {
convertNodes(ast, visitorKeys);
convertProgramNode(ast);
};
Loading

0 comments on commit 9d620c2

Please sign in to comment.