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

Expose @babel/eslint-parser/experimental-worker #13398

Merged
merged 2 commits into from Aug 3, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint/babel-eslint-parser/package.json
Expand Up @@ -23,6 +23,7 @@
"type": "commonjs",
"exports": {
".": "./lib/index.cjs",
"./experimental-worker": "./lib/experimental-worker.cjs",
"./package.json": "./package.json"
},
"peerDependencies": {
Expand Down
21 changes: 13 additions & 8 deletions eslint/babel-eslint-parser/src/analyze-scope.cjs
Expand Up @@ -4,13 +4,11 @@ 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) {
function getVisitorValues(nodeType, client) {
if (visitorKeysMap) return visitorKeysMap[nodeType];

const { FLOW_FLIPPED_ALIAS_KEYS, VISITOR_KEYS } = getTypesInfo();
const { FLOW_FLIPPED_ALIAS_KEYS, VISITOR_KEYS } = client.getTypesInfo();

const flowFlippedAliasKeys = FLOW_FLIPPED_ALIAS_KEYS.concat([
"ArrayPattern",
Expand Down Expand Up @@ -63,6 +61,13 @@ class PatternVisitor extends OriginalPatternVisitor {
}

class Referencer extends OriginalReferencer {
#client;

constructor(options, scopeManager, client) {
super(options, scopeManager);
this.#client = client;
}

// inherits.
visitPattern(node, options, callback) {
if (!node) {
Expand Down Expand Up @@ -264,7 +269,7 @@ class Referencer extends OriginalReferencer {
}

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

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

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

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

referencer.visit(ast);

Expand Down
154 changes: 96 additions & 58 deletions eslint/babel-eslint-parser/src/client.cjs
@@ -1,67 +1,105 @@
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;
};
const ACTIONS = {
GET_VERSION: "GET_VERSION",
GET_TYPES_INFO: "GET_TYPES_INFO",
GET_VISITOR_KEYS: "GET_VISITOR_KEYS",
GET_TOKEN_LABELS: "GET_TOKEN_LABELS",
MAYBE_PARSE: "MAYBE_PARSE",
MAYBE_PARSE_SYNC: "MAYBE_PARSE_SYNC",
};

class Client {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reorganized this file so that it can both run Babel using worker_threads or in the same thread. The Client class is the "public interface", while the WorkerClient and LocalClient provide the communication implementation.

#send;

constructor(send) {
this.#send = send;
}

#vCache;
getVersion() {
return (this.#vCache ??= this.#send(ACTIONS.GET_VERSION, undefined));
}

#tiCache;
getTypesInfo() {
return (this.#tiCache ??= this.#send(ACTIONS.GET_TYPES_INFO, undefined));
}

#vkCache;
getVisitorKeys() {
return (this.#vkCache ??= this.#send(ACTIONS.GET_VISITOR_KEYS, undefined));
}

#tlCache;
getTokLabels() {
return (this.#tlCache ??= this.#send(ACTIONS.GET_TOKEN_LABELS, undefined));
}

maybeParse(code, options) {
return this.#send(ACTIONS.MAYBE_PARSE, { code, options });
}
}

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(
// We need to run Babel in a worker for two reasons:
// 1. ESLint workers must be CJS files, and this is a problem
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
// 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.
exports.WorkerClient = class WorkerClient extends Client {
static #worker_threads_cache;
static get #worker_threads() {
return (WorkerClient.#worker_threads_cache ??= require("worker_threads"));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: What's the intention of private static getter? Doesn't static #worker_threads = require("worker_threads") work here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is always executed, but require("worker_threads") is only necessary when actually using the experimental implementation.


#worker = new WorkerClient.#worker_threads.Worker(
path.resolve(__dirname, "../lib/worker/index.cjs"),
{ env: SHARE_ENV },
{ env: WorkerClient.#worker_threads.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;
#signal = new Int32Array(new SharedArrayBuffer(4));

constructor() {
super((action, payload) => {
this.#signal[0] = 0;
const subChannel = new WorkerClient.#worker_threads.MessageChannel();

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

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

if (message.error) throw Object.assign(message.error, message.errorData);
else return message.result;
});

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

if (!process.env.BABEL_8_BREAKING) {
exports.LocalClient = class LocalClient extends Client {
static #handleMessage;

constructor() {
LocalClient.#handleMessage ??= require("./worker/handle-message.cjs");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto.


super((action, payload) => {
return LocalClient.#handleMessage(
action === ACTIONS.MAYBE_PARSE ? ACTIONS.MAYBE_PARSE_SYNC : action,
payload,
);
});
}
};
} else {
send = require("./worker/index.cjs");
}
22 changes: 22 additions & 0 deletions eslint/babel-eslint-parser/src/experimental-worker.cjs
@@ -0,0 +1,22 @@
const [major, minor] = process.versions.node.split(".").map(Number);

if (major < 12 || (major === 12 && minor < 3)) {
throw new Error(
"@babel/eslint-parser/experimental-worker requires Node.js >= 12.3.0",
);
}

const { normalizeESLintConfig } = require("./configuration.cjs");
const analyzeScope = require("./analyze-scope.cjs");
const baseParse = require("./parse.cjs");

const { WorkerClient } = require("./client.cjs");
const client = new WorkerClient();

exports.parseForESLint = function (code, options = {}) {
const normalizedOptions = normalizeESLintConfig(options);
const ast = baseParse(code, normalizedOptions, client);
const scopeManager = analyzeScope(ast, normalizedOptions, client);

return { ast, scopeManager, visitorKeys: client.getVisitorKeys() };
};
61 changes: 9 additions & 52 deletions eslint/babel-eslint-parser/src/index.cjs
@@ -1,63 +1,20 @@
const semver = require("semver");
const { normalizeESLintConfig } = require("./configuration.cjs");
const analyzeScope = require("./analyze-scope.cjs");
const {
getVersion,
getVisitorKeys,
getTokLabels,
maybeParse,
} = require("./client.cjs");
const convert = require("./convert/index.cjs");
const baseParse = require("./parse.cjs");

const babelParser = require(require.resolve("@babel/parser", {
paths: [require.resolve("@babel/core/package.json")],
}));

let isRunningMinSupportedCoreVersion = null;

function baseParse(code, options) {
// Ensure we're using a version of `@babel/core` that includes `parse()` and `tokTypes`.
const minSupportedCoreVersion = ">=7.2.0";

if (typeof isRunningMinSupportedCoreVersion !== "boolean") {
isRunningMinSupportedCoreVersion = semver.satisfies(
getVersion(),
minSupportedCoreVersion,
);
}

if (!isRunningMinSupportedCoreVersion) {
throw new Error(
`@babel/eslint-parser@${
PACKAGE_JSON.version
} does not support @babel/core@${getVersion()}. Please upgrade to @babel/core@${minSupportedCoreVersion}.`,
);
}

const { ast, parserOptions } = maybeParse(code, options);

if (ast) return ast;

try {
return convert.ast(
babelParser.parse(code, parserOptions),
code,
getTokLabels(),
getVisitorKeys(),
);
} catch (err) {
throw convert.error(err);
}
}
const { LocalClient, WorkerClient } = require("./client.cjs");
const client = new (
process.env.BABEL_8_BREAKING ? WorkerClient : LocalClient
)();

exports.parse = function (code, options = {}) {
return baseParse(code, normalizeESLintConfig(options));
return baseParse(code, normalizeESLintConfig(options), client);
};

exports.parseForESLint = function (code, options = {}) {
const normalizedOptions = normalizeESLintConfig(options);
const ast = baseParse(code, normalizedOptions);
const scopeManager = analyzeScope(ast, normalizedOptions);
const ast = baseParse(code, normalizedOptions, client);
const scopeManager = analyzeScope(ast, normalizedOptions, client);

return { ast, scopeManager, visitorKeys: getVisitorKeys() };
return { ast, scopeManager, visitorKeys: client.getVisitorKeys() };
};
45 changes: 45 additions & 0 deletions eslint/babel-eslint-parser/src/parse.cjs
@@ -0,0 +1,45 @@
"use strict";

const semver = require("semver");
const convert = require("./convert/index.cjs");

const babelParser = require(require.resolve("@babel/parser", {
paths: [require.resolve("@babel/core/package.json")],
}));

let isRunningMinSupportedCoreVersion = null;

module.exports = function parse(code, options, client) {
// Ensure we're using a version of `@babel/core` that includes `parse()` and `tokTypes`.
const minSupportedCoreVersion = ">=7.2.0";

if (typeof isRunningMinSupportedCoreVersion !== "boolean") {
isRunningMinSupportedCoreVersion = semver.satisfies(
client.getVersion(),
minSupportedCoreVersion,
);
}

if (!isRunningMinSupportedCoreVersion) {
throw new Error(
`@babel/eslint-parser@${
PACKAGE_JSON.version
} does not support @babel/core@${client.getVersion()}. Please upgrade to @babel/core@${minSupportedCoreVersion}.`,
);
}

const { ast, parserOptions } = client.maybeParse(code, options);

if (ast) return ast;

try {
return convert.ast(
babelParser.parse(code, parserOptions),
code,
client.getTokLabels(),
client.getVisitorKeys(),
);
} catch (err) {
throw convert.error(err);
}
};