diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a04d75934949..9425c3c81e12 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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", @@ -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: { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c4f8d7d2993..254f6981a7e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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. diff --git a/.prettierrc b/.prettierrc index 493de23554b3..b22f88634299 100644 --- a/.prettierrc +++ b/.prettierrc @@ -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": { diff --git a/eslint/babel-eslint-parser/package.json b/eslint/babel-eslint-parser/package.json index 11a17136d941..f244586eff34 100644 --- a/eslint/babel-eslint-parser/package.json +++ b/eslint/babel-eslint-parser/package.json @@ -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": { diff --git a/eslint/babel-eslint-parser/src/analyze-scope.js b/eslint/babel-eslint-parser/src/analyze-scope.cjs similarity index 87% rename from eslint/babel-eslint-parser/src/analyze-scope.js rename to eslint/babel-eslint-parser/src/analyze-scope.cjs index 69829d011b5f..f9fb7472e8d9 100644 --- a/eslint/babel-eslint-parser/src/analyze-scope.js +++ b/eslint/babel-eslint-parser/src/analyze-scope.cjs @@ -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 @@ -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); } @@ -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; } @@ -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, @@ -337,7 +343,7 @@ 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); @@ -345,4 +351,4 @@ export default function analyzeScope(ast, parserOptions) { referencer.visit(ast); return scopeManager; -} +}; diff --git a/eslint/babel-eslint-parser/src/client.cjs b/eslint/babel-eslint-parser/src/client.cjs new file mode 100644 index 000000000000..a4a4777159b9 --- /dev/null +++ b/eslint/babel-eslint-parser/src/client.cjs @@ -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"); +} diff --git a/eslint/babel-eslint-parser/src/configuration.cjs b/eslint/babel-eslint-parser/src/configuration.cjs new file mode 100644 index 000000000000..c39fe1d2dad4 --- /dev/null +++ b/eslint/babel-eslint-parser/src/configuration.cjs @@ -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, + }; +}; diff --git a/eslint/babel-eslint-parser/src/convert/convertAST.cjs b/eslint/babel-eslint-parser/src/convert/convertAST.cjs new file mode 100644 index 000000000000..85c95052216d --- /dev/null +++ b/eslint/babel-eslint-parser/src/convert/convertAST.cjs @@ -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); +}; diff --git a/eslint/babel-eslint-parser/src/convert/convertAST.js b/eslint/babel-eslint-parser/src/convert/convertAST.js deleted file mode 100644 index 76f3c63fb420..000000000000 --- a/eslint/babel-eslint-parser/src/convert/convertAST.js +++ /dev/null @@ -1,148 +0,0 @@ -import { types as t, traverse } from "@babel/core"; -import { newTypes, conflictTypes } from "../visitor-keys"; - -function convertNodes(ast, code) { - const astTransformVisitor = { - noScope: true, - enter(path) { - const { node } = path; - - if (node.innerComments) { - delete node.innerComments; - } - - if (node.trailingComments) { - delete node.trailingComments; - } - - if (node.leadingComments) { - delete node.leadingComments; - } - }, - exit(path) { - const { node } = path; - - // Used internally by @babel/parser. - if (node.extra) { - delete node.extra; - } - - if (node?.loc.identifierName) { - delete node.loc.identifierName; - } - - if (path.isTypeParameter()) { - node.type = "Identifier"; - node.typeAnnotation = node.bound; - delete node.bound; - } - - // flow: prevent "no-undef" - // for "Component" in: "let x: React.Component" - if (path.isQualifiedTypeIdentifier()) { - delete node.id; - } - // for "b" in: "var a: { b: Foo }" - if (path.isObjectTypeProperty()) { - delete node.key; - } - // for "indexer" in: "var a: {[indexer: string]: number}" - if (path.isObjectTypeIndexer()) { - delete node.id; - } - // for "param" in: "var a: { func(param: Foo): Bar };" - if (path.isFunctionTypeParam()) { - delete node.name; - } - - // modules - if (path.isImportDeclaration()) { - delete node.isType; - } - - // template string range fixes - if (path.isTemplateLiteral()) { - 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; - } - } - } - }, - }; - const state = { source: code }; - const oldVisitorKeys = new Map(); - - try { - for (const [type, visitorKey] of Object.entries(conflictTypes)) { - // backup conflicted visitor keys - oldVisitorKeys.set(type, t.VISITOR_KEYS[type]); - - t.VISITOR_KEYS[type] = visitorKey; - } - for (const [type, visitorKey] of Object.entries(newTypes)) { - t.VISITOR_KEYS[type] = visitorKey; - } - - traverse(ast, astTransformVisitor, null, state); - } finally { - // These can be safely deleted because they are not defined in the original visitor keys. - for (const type of Object.keys(newTypes)) { - delete t.VISITOR_KEYS[type]; - } - - // These should be restored - for (const type of Object.keys(conflictTypes)) { - t.VISITOR_KEYS[type] = oldVisitorKeys.get(type); - } - } -} - -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; - } -} - -export default function convertAST(ast, code) { - convertNodes(ast, code); - convertProgramNode(ast); -} diff --git a/eslint/babel-eslint-parser/src/convert/convertComments.js b/eslint/babel-eslint-parser/src/convert/convertComments.cjs similarity index 86% rename from eslint/babel-eslint-parser/src/convert/convertComments.js rename to eslint/babel-eslint-parser/src/convert/convertComments.cjs index 854c3440c590..bcdda9170572 100644 --- a/eslint/babel-eslint-parser/src/convert/convertComments.js +++ b/eslint/babel-eslint-parser/src/convert/convertComments.cjs @@ -1,4 +1,4 @@ -export default function convertComments(comments) { +module.exports = function convertComments(comments) { for (const comment of comments) { if (comment.type === "CommentBlock") { comment.type = "Block"; @@ -11,4 +11,4 @@ export default function convertComments(comments) { comment.range = [comment.start, comment.end]; } } -} +}; diff --git a/eslint/babel-eslint-parser/src/convert/convertTokens.js b/eslint/babel-eslint-parser/src/convert/convertTokens.cjs similarity index 90% rename from eslint/babel-eslint-parser/src/convert/convertTokens.js rename to eslint/babel-eslint-parser/src/convert/convertTokens.cjs index 414067892979..8cdaa566e134 100644 --- a/eslint/babel-eslint-parser/src/convert/convertTokens.js +++ b/eslint/babel-eslint-parser/src/convert/convertTokens.cjs @@ -1,12 +1,4 @@ -import { tokTypes } from "@babel/core"; - -const tl = ( - process.env.BABEL_8_BREAKING - ? Object.fromEntries - : p => p.reduce((o, [k, v]) => ({ ...o, [k]: v }), {}) -)(Object.keys(tokTypes).map(key => [key, tokTypes[key].label])); - -function convertTemplateType(tokens) { +function convertTemplateType(tokens, tl) { let curlyBrace = null; let templateTokens = []; const result = []; @@ -97,7 +89,7 @@ function convertTemplateType(tokens) { return result; } -function convertToken(token, source) { +function convertToken(token, source, tl) { const { type } = token; const { label } = type; token.range = [token.start, token.end]; @@ -192,8 +184,8 @@ function convertToken(token, source) { return token; } -export default function convertTokens(tokens, code) { - return convertTemplateType(tokens) +module.exports = function convertTokens(tokens, code, tl) { + return convertTemplateType(tokens, tl) .filter(t => t.type !== "CommentLine" && t.type !== "CommentBlock") - .map(t => convertToken(t, code)); -} + .map(t => convertToken(t, code, tl)); +}; diff --git a/eslint/babel-eslint-parser/src/convert/index.cjs b/eslint/babel-eslint-parser/src/convert/index.cjs new file mode 100644 index 000000000000..1d5cec94b031 --- /dev/null +++ b/eslint/babel-eslint-parser/src/convert/index.cjs @@ -0,0 +1,18 @@ +const convertTokens = require("./convertTokens.cjs"); +const convertComments = require("./convertComments.cjs"); +const convertAST = require("./convertAST.cjs"); + +exports.ast = function convert(ast, code, tokLabels, visitorKeys) { + ast.tokens = convertTokens(ast.tokens, code, tokLabels); + convertComments(ast.comments); + convertAST(ast, visitorKeys); + return ast; +}; + +exports.error = function convertError(err) { + if (err instanceof SyntaxError) { + err.lineNumber = err.loc.line; + err.column = err.loc.column; + } + return err; +}; diff --git a/eslint/babel-eslint-parser/src/convert/index.js b/eslint/babel-eslint-parser/src/convert/index.js deleted file mode 100644 index 085f49eec44a..000000000000 --- a/eslint/babel-eslint-parser/src/convert/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import convertTokens from "./convertTokens"; -import convertComments from "./convertComments"; -import convertAST from "./convertAST"; - -export default function (ast, code) { - ast.tokens = convertTokens(ast.tokens, code); - convertComments(ast.comments); - convertAST(ast, code); -} diff --git a/eslint/babel-eslint-parser/src/index.cjs b/eslint/babel-eslint-parser/src/index.cjs new file mode 100644 index 000000000000..eef046316976 --- /dev/null +++ b/eslint/babel-eslint-parser/src/index.cjs @@ -0,0 +1,63 @@ +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 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); + } +} + +exports.parse = function (code, options = {}) { + return baseParse(code, normalizeESLintConfig(options)); +}; + +exports.parseForESLint = function (code, options = {}) { + const normalizedOptions = normalizeESLintConfig(options); + const ast = baseParse(code, normalizedOptions); + const scopeManager = analyzeScope(ast, normalizedOptions); + + return { ast, scopeManager, visitorKeys: getVisitorKeys() }; +}; diff --git a/eslint/babel-eslint-parser/src/index.js b/eslint/babel-eslint-parser/src/index.js deleted file mode 100644 index 709232c26973..000000000000 --- a/eslint/babel-eslint-parser/src/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import semver from "semver"; -import { - version as babelCoreVersion, - parseSync as babelParse, -} from "@babel/core"; -import { - normalizeBabelParseConfig, - normalizeESLintConfig, -} from "./configuration"; -import convert from "./convert"; -import analyzeScope from "./analyze-scope"; -import visitorKeys from "./visitor-keys"; - -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( - babelCoreVersion, - minSupportedCoreVersion, - ); - } - - if (!isRunningMinSupportedCoreVersion) { - throw new Error( - `@babel/eslint-parser@${PACKAGE_JSON.version} does not support @babel/core@${babelCoreVersion}. Please upgrade to @babel/core@${minSupportedCoreVersion}.`, - ); - } - - let ast; - - try { - ast = babelParse(code, normalizeBabelParseConfig(options)); - } catch (err) { - if (err instanceof SyntaxError) { - err.lineNumber = err.loc.line; - err.column = err.loc.column; - } - - throw err; - } - - convert(ast, code); - - return ast; -} - -export function parse(code, options = {}) { - return baseParse(code, normalizeESLintConfig(options)); -} - -export function parseForESLint(code, options = {}) { - const normalizedOptions = normalizeESLintConfig(options); - const ast = baseParse(code, normalizedOptions); - const scopeManager = analyzeScope(ast, normalizedOptions); - - return { ast, scopeManager, visitorKeys }; -} diff --git a/eslint/babel-eslint-parser/src/visitor-keys.js b/eslint/babel-eslint-parser/src/visitor-keys.js deleted file mode 100644 index 3440cfcc1168..000000000000 --- a/eslint/babel-eslint-parser/src/visitor-keys.js +++ /dev/null @@ -1,25 +0,0 @@ -import { types as t } from "@babel/core"; -import { KEYS as ESLINT_VISITOR_KEYS } from "eslint-visitor-keys"; - -// AST Types that are not presented in Babel AST -export const newTypes = { - ChainExpression: ESLINT_VISITOR_KEYS.ChainExpression, - ImportExpression: ESLINT_VISITOR_KEYS.ImportExpression, - Literal: ESLINT_VISITOR_KEYS.Literal, - MethodDefinition: ["decorators"].concat(ESLINT_VISITOR_KEYS.MethodDefinition), - Property: ["decorators"].concat(ESLINT_VISITOR_KEYS.Property), - PropertyDefinition: ["decorators"].concat( - ESLINT_VISITOR_KEYS.PropertyDefinition, - ), -}; - -// AST Types that shares `"type"` property with Babel but have different shape -export const conflictTypes = { - // todo: remove this when we drop Babel 7 support - ClassPrivateMethod: ["decorators"].concat( - ESLINT_VISITOR_KEYS.MethodDefinition, - ), - ExportAllDeclaration: ESLINT_VISITOR_KEYS.ExportAllDeclaration, -}; - -export default { ...newTypes, ...t.VISITOR_KEYS, ...conflictTypes }; diff --git a/eslint/babel-eslint-parser/src/worker/ast-info.cjs b/eslint/babel-eslint-parser/src/worker/ast-info.cjs new file mode 100644 index 000000000000..a9ed882c21ac --- /dev/null +++ b/eslint/babel-eslint-parser/src/worker/ast-info.cjs @@ -0,0 +1,46 @@ +const ESLINT_VISITOR_KEYS = require("eslint-visitor-keys").KEYS; +const babel = require("./babel-core.cjs"); + +let visitorKeys; +exports.getVisitorKeys = function getVisitorKeys() { + if (!visitorKeys) { + // AST Types that are not presented in Babel AST + const newTypes = { + ChainExpression: ESLINT_VISITOR_KEYS.ChainExpression, + ImportExpression: ESLINT_VISITOR_KEYS.ImportExpression, + Literal: ESLINT_VISITOR_KEYS.Literal, + MethodDefinition: ["decorators"].concat( + ESLINT_VISITOR_KEYS.MethodDefinition, + ), + Property: ["decorators"].concat(ESLINT_VISITOR_KEYS.Property), + PropertyDefinition: ["decorators"].concat( + ESLINT_VISITOR_KEYS.PropertyDefinition, + ), + }; + + // AST Types that shares `"type"` property with Babel but have different shape + const conflictTypes = { + // todo: remove this when we drop Babel 7 support + ClassPrivateMethod: ["decorators"].concat( + ESLINT_VISITOR_KEYS.MethodDefinition, + ), + ExportAllDeclaration: ESLINT_VISITOR_KEYS.ExportAllDeclaration, + }; + + visitorKeys = { + ...newTypes, + ...babel.types.VISITOR_KEYS, + ...conflictTypes, + }; + } + return visitorKeys; +}; + +let tokLabels; +exports.getTokLabels = function getTokLabels() { + return (tokLabels ||= ( + process.env.BABEL_8_BREAKING + ? Object.fromEntries + : p => p.reduce((o, [k, v]) => ({ ...o, [k]: v }), {}) + )(Object.entries(babel.tokTypes).map(([key, tok]) => [key, tok.label]))); +}; diff --git a/eslint/babel-eslint-parser/src/worker/babel-core.cjs b/eslint/babel-eslint-parser/src/worker/babel-core.cjs new file mode 100644 index 000000000000..47486218db92 --- /dev/null +++ b/eslint/babel-eslint-parser/src/worker/babel-core.cjs @@ -0,0 +1,17 @@ +function initialize(babel) { + exports.init = null; + exports.version = babel.version; + exports.traverse = babel.traverse; + exports.types = babel.types; + exports.tokTypes = babel.tokTypes; + exports.parseSync = babel.parseSync; + exports.loadPartialConfigSync = babel.loadPartialConfigSync; + exports.loadPartialConfigAsync = babel.loadPartialConfigAsync; + exports.createConfigItem = babel.createConfigItem; +} + +if (process.env.BABEL_8_BREAKING) { + exports.init = import("@babel/core").then(ns => initialize(ns.default)); +} else { + initialize(require("@babel/core")); +} diff --git a/eslint/babel-eslint-parser/src/configuration.js b/eslint/babel-eslint-parser/src/worker/configuration.cjs similarity index 71% rename from eslint/babel-eslint-parser/src/configuration.js rename to eslint/babel-eslint-parser/src/worker/configuration.cjs index f935590de4f5..24877f5c59ef 100644 --- a/eslint/babel-eslint-parser/src/configuration.js +++ b/eslint/babel-eslint-parser/src/worker/configuration.cjs @@ -1,25 +1,4 @@ -import { loadPartialConfig } from "@babel/core"; - -export function normalizeESLintConfig(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, - }; -} +const babel = require("./babel-core.cjs"); /** * Merge user supplied estree plugin options to default estree plugin options @@ -41,8 +20,8 @@ function getParserPlugins(babelOptions) { return [["estree", estreeOptions], ...babelParserPlugins]; } -export function normalizeBabelParseConfig(options) { - const parseOptions = { +function normalizeParserOptions(options) { + return { sourceType: options.sourceType, filename: options.filePath, ...options.babelOptions, @@ -60,11 +39,11 @@ export function normalizeBabelParseConfig(options) { ...options.babelOptions.caller, }, }; +} - if (options.requireConfigFile !== false) { - const config = loadPartialConfig(parseOptions); - - if (config !== null) { +function validateResolvedConfig(config, options) { + if (config !== null) { + if (options.requireConfigFile !== false) { if (!config.hasFilesystemConfig()) { let error = `No Babel config file detected for ${config.options.filename}. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files.`; @@ -74,10 +53,20 @@ export function normalizeBabelParseConfig(options) { throw new Error(error); } - - return config.options; } + return config.options; } - - return parseOptions; } + +module.exports = function normalizeBabelParseConfig(options) { + const parseOptions = normalizeParserOptions(options); + + if (process.env.BABEL_8_BREAKING) { + return babel + .loadPartialConfigAsync(parseOptions) + .then(config => validateResolvedConfig(config, options) || parseOptions); + } else { + const config = babel.loadPartialConfigSync(parseOptions); + return validateResolvedConfig(config, options) || parseOptions; + } +}; diff --git a/eslint/babel-eslint-parser/src/worker/extract-parser-options-plugin.cjs b/eslint/babel-eslint-parser/src/worker/extract-parser-options-plugin.cjs new file mode 100644 index 000000000000..bfe925676574 --- /dev/null +++ b/eslint/babel-eslint-parser/src/worker/extract-parser-options-plugin.cjs @@ -0,0 +1,7 @@ +module.exports = function extractParserOptionsPlugin() { + return { + parserOverride(code, opts) { + return opts; + }, + }; +}; diff --git a/eslint/babel-eslint-parser/src/worker/index.cjs b/eslint/babel-eslint-parser/src/worker/index.cjs new file mode 100644 index 000000000000..c212274cc7c2 --- /dev/null +++ b/eslint/babel-eslint-parser/src/worker/index.cjs @@ -0,0 +1,66 @@ +const babel = require("./babel-core.cjs"); +const maybeParse = require("./maybeParse.cjs"); +const { getVisitorKeys, getTokLabels } = require("./ast-info.cjs"); +const normalizeBabelParseConfig = require("./configuration.cjs"); + +function handleMessage(action, payload) { + switch (action) { + case "GET_VERSION": + return babel.version; + case "GET_TYPES_INFO": + return { + FLOW_FLIPPED_ALIAS_KEYS: babel.types.FLIPPED_ALIAS_KEYS.Flow, + VISITOR_KEYS: babel.types.VISITOR_KEYS, + }; + case "GET_TOKEN_LABELS": + return getTokLabels(); + case "GET_VISITOR_KEYS": + return getVisitorKeys(); + case "MAYBE_PARSE": + if (process.env.BABEL_8_BREAKING) { + return normalizeBabelParseConfig(payload.options).then(options => + maybeParse(payload.code, options), + ); + } else { + return maybeParse( + payload.code, + normalizeBabelParseConfig(payload.options), + ); + } + } + + throw new Error(`Unknown internal parser worker action: ${action}`); +} + +if (process.env.BABEL_8_BREAKING) { + const { parentPort } = require("worker_threads"); + + parentPort.addListener( + "message", + async ({ signal, port, action, payload }) => { + let response; + + try { + if (babel.init) await babel.init; + + response = { result: await handleMessage(action, payload) }; + } catch (error) { + response = { error, errorData: { ...error } }; + } + + try { + port.postMessage(response); + } catch { + port.postMessage({ + error: new Error("Cannot serialize worker response"), + }); + } finally { + port.close(); + Atomics.store(signal, 0, 1); + Atomics.notify(signal, 0); + } + }, + ); +} else { + module.exports = handleMessage; +} diff --git a/eslint/babel-eslint-parser/src/worker/maybeParse.cjs b/eslint/babel-eslint-parser/src/worker/maybeParse.cjs new file mode 100644 index 000000000000..22c6fcaa7147 --- /dev/null +++ b/eslint/babel-eslint-parser/src/worker/maybeParse.cjs @@ -0,0 +1,43 @@ +const babel = require("./babel-core.cjs"); +const convert = require("../convert/index.cjs"); +const { getVisitorKeys, getTokLabels } = require("./ast-info.cjs"); +const extractParserOptionsPlugin = require("./extract-parser-options-plugin.cjs"); + +const ref = {}; +let extractParserOptionsConfigItem; + +const MULTIPLE_OVERRIDES = /More than one plugin attempted to override parsing/; + +module.exports = function maybeParse(code, options) { + if (!extractParserOptionsConfigItem) { + extractParserOptionsConfigItem = babel.createConfigItem( + [extractParserOptionsPlugin, ref], + { dirname: __dirname, type: "plugin" }, + ); + } + options.plugins.push(extractParserOptionsConfigItem); + + try { + return { + parserOptions: babel.parseSync(code, options), + ast: null, + }; + } catch (err) { + if (!MULTIPLE_OVERRIDES.test(err.message)) { + throw err; + } + } + + let ast; + + try { + ast = babel.parseSync(code, options); + } catch (err) { + throw convert.error(err); + } + + return { + ast: convert.ast(ast, code, getTokLabels(), getVisitorKeys()), + parserOptions: null, + }; +}; diff --git a/eslint/babel-eslint-parser/test/index.js b/eslint/babel-eslint-parser/test/index.js index fef130a449ec..3522bdd218b8 100644 --- a/eslint/babel-eslint-parser/test/index.js +++ b/eslint/babel-eslint-parser/test/index.js @@ -3,7 +3,7 @@ import escope from "eslint-scope"; import unpad from "dedent"; import { fileURLToPath } from "url"; import { createRequire } from "module"; -import { parseForESLint } from "../src"; +import { parseForESLint } from "../lib/index.cjs"; const BABEL_OPTIONS = { configFile: path.resolve( diff --git a/eslint/babel-eslint-tests/package.json b/eslint/babel-eslint-tests/package.json index 24e2ce7c44fc..4cd906d24cc8 100644 --- a/eslint/babel-eslint-tests/package.json +++ b/eslint/babel-eslint-tests/package.json @@ -6,6 +6,7 @@ "private": true, "dependencies": { "@babel/eslint-parser": "workspace:^7.14.2", + "@babel/preset-react": "workspace:^7.13.13", "dedent": "^0.7.0", "eslint": "^7.5.0", "eslint-plugin-import": "^2.22.0", diff --git a/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/.eslintrc.js b/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/.eslintrc.js new file mode 100644 index 000000000000..486757528be0 --- /dev/null +++ b/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + root: true, + parser: "@babel/eslint-parser", + parserOptions: { + babelOptions: { + configFile: __dirname + "/babel.config.mjs", + sourceType: "module", + }, + }, + rules: { + "template-curly-spacing": "error", + }, +}; diff --git a/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/a.js b/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/a.js new file mode 100644 index 000000000000..60668017bf2c --- /dev/null +++ b/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/a.js @@ -0,0 +1 @@ +export default () =>
; diff --git a/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/babel.config.mjs b/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/babel.config.mjs new file mode 100644 index 000000000000..3d56fe842f6d --- /dev/null +++ b/eslint/babel-eslint-tests/test/fixtures/mjs-config-file/babel.config.mjs @@ -0,0 +1,3 @@ +export default { + presets: ["@babel/preset-react"], +}; diff --git a/eslint/babel-eslint-tests/test/helpers/verifyAndAssertMessages.js b/eslint/babel-eslint-tests/test/helpers/verifyAndAssertMessages.js index b8cec5e450d9..6829c73bbbd6 100644 --- a/eslint/babel-eslint-tests/test/helpers/verifyAndAssertMessages.js +++ b/eslint/babel-eslint-tests/test/helpers/verifyAndAssertMessages.js @@ -2,7 +2,7 @@ import eslint from "eslint"; import unpad from "dedent"; import path from "path"; import { fileURLToPath } from "url"; -import * as parser from "../../../babel-eslint-parser"; +import * as parser from "../../../babel-eslint-parser/lib/index.cjs"; export default function verifyAndAssertMessages( code, diff --git a/eslint/babel-eslint-tests/test/integration/config-files.js b/eslint/babel-eslint-tests/test/integration/config-files.js new file mode 100644 index 000000000000..d99f38df986d --- /dev/null +++ b/eslint/babel-eslint-tests/test/integration/config-files.js @@ -0,0 +1,19 @@ +import eslint from "eslint"; +import path from "path"; +import { fileURLToPath } from "url"; + +describe("Babel config files", () => { + const babel8 = process.env.BABEL_8_BREAKING ? it : it.skip; + + babel8("works with babel.config.mjs", () => { + const engine = new eslint.CLIEngine({ ignore: false }); + expect( + engine.executeOnFiles([ + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + `../fixtures/mjs-config-file/a.js`, + ), + ]), + ).toMatchObject({ errorCount: 0 }); + }); +}); diff --git a/eslint/babel-eslint-tests/test/integration/eslint/rules/strict.js b/eslint/babel-eslint-tests/test/integration/eslint/rules/strict.js index 769ba76d2f29..c57e1366eab0 100644 --- a/eslint/babel-eslint-tests/test/integration/eslint/rules/strict.js +++ b/eslint/babel-eslint-tests/test/integration/eslint/rules/strict.js @@ -1,7 +1,7 @@ import eslint from "eslint"; import fs from "fs"; import path from "path"; -import * as parser from "../../../../../babel-eslint-parser"; +import * as parser from "../../../../../babel-eslint-parser/lib/index.cjs"; import { fileURLToPath } from "url"; eslint.linter.defineParser("@babel/eslint-parser", parser); diff --git a/packages/babel-core/src/parser/index.ts b/packages/babel-core/src/parser/index.ts index 44c5b684a151..f5374ab87061 100644 --- a/packages/babel-core/src/parser/index.ts +++ b/packages/babel-core/src/parser/index.ts @@ -42,6 +42,7 @@ export default function* parser( } return results[0]; } + // TODO: Add an error code throw new Error("More than one plugin attempted to override parsing."); } catch (err) { if (err.code === "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED") { diff --git a/yarn.lock b/yarn.lock index 40f332e893aa..ab2e1c769dde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -311,6 +311,7 @@ __metadata: resolution: "@babel/eslint-tests@workspace:eslint/babel-eslint-tests" dependencies: "@babel/eslint-parser": "workspace:^7.14.2" + "@babel/preset-react": "workspace:^7.13.13" dedent: ^0.7.0 eslint: ^7.5.0 eslint-plugin-import: ^2.22.0 @@ -3278,7 +3279,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@workspace:*, @babel/preset-react@workspace:^7.12.13, @babel/preset-react@workspace:packages/babel-preset-react": +"@babel/preset-react@workspace:*, @babel/preset-react@workspace:^7.12.13, @babel/preset-react@workspace:^7.13.13, @babel/preset-react@workspace:packages/babel-preset-react": version: 0.0.0-use.local resolution: "@babel/preset-react@workspace:packages/babel-preset-react" dependencies: