refactor(eslint): remove compat layers for ESLint < 9 (#17944)

This commit is contained in:
Huáng Jùnliàng
2026-04-24 13:48:47 +02:00
committed by GitHub
parent 2a47553eac
commit cffd9599ce
15 changed files with 87 additions and 357 deletions
+2
View File
@@ -48,6 +48,8 @@ packages/babel-standalone/babel.min.js
packages/babel-standalone/babel.min.js.map
/codemods/*/lib
/packages/babel-parser/build
# directory for third-party parser tests
/build
.idea/
/.changelog
-1
View File
@@ -42,7 +42,6 @@
"@babel/core": "workspace:^",
"@babel/helper-fixtures": "workspace:^",
"@types/estree": "^1.0.5",
"@typescript-eslint/scope-manager": "^8.55.0",
"dedent": "^1.5.3",
"eslint": "^10.0.0",
"typescript-eslint": "^8.55.0"
@@ -176,31 +176,14 @@ class Referencer extends OriginalReferencer {
}
}
ClassProperty(node: any) {
this._visitClassProperty(node);
}
ClassPrivateProperty(node: any) {
this._visitClassProperty(node);
}
AccessorProperty(node: any) {
this._visitClassProperty(node);
}
ClassAccessorProperty(node: any) {
this._visitClassProperty(node);
}
PropertyDefinition(node: any) {
this._visitClassProperty(node);
}
// TODO: Update to visit type annotations when TypeScript/Flow support this syntax.
ClassPrivateMethod(node: any) {
super.MethodDefinition(node);
}
DeclareModule(node: any) {
this._visitDeclareX(node);
}
@@ -217,11 +200,6 @@ class Referencer extends OriginalReferencer {
this._visitDeclareX(node);
}
// visit OptionalMemberExpression as a MemberExpression.
OptionalMemberExpression(node: any) {
super.MemberExpression(node);
}
_visitClassProperty(node: any) {
const { computed, key, typeAnnotation, decorators, value } = node;
@@ -230,21 +208,7 @@ class Referencer extends OriginalReferencer {
this._visitTypeAnnotation(typeAnnotation);
if (value) {
if (this.scopeManager.__nestClassFieldInitializerScope) {
this.scopeManager.__nestClassFieldInitializerScope(value);
} else {
// Given that ESLint 7 didn't have a "class field initializer" scope,
// we create a plain method scope. Semantics are the same.
this.scopeManager.__nestScope(
new Scope(
this.scopeManager,
"function",
this.scopeManager.__currentScope,
value,
true,
),
);
}
this.scopeManager.__nestClassFieldInitializerScope(value);
this.visit(value);
this.close(value);
}
@@ -384,6 +348,7 @@ export default function analyzeScope(ast: any, parserOptions: any) {
childVisitorKeys: t.VISITOR_KEYS,
};
// @ts-expect-error upstream typing issue in the `fallback` property: (node: estree.Node) => readonly string[] is not assignable to type (node: estree.Node) => string[]
const scopeManager = new ScopeManager(options);
const referencer = new Referencer(options, scopeManager);
+26 -73
View File
@@ -4,12 +4,21 @@ import type { Token as tokenizerToken } from "../../../packages/babel-parser/src
import type { ExportedTokenType } from "../../../packages/babel-parser/src/tokenizer/types";
import type * as estree from "estree";
import type {
Definition as OriginalScopeDefinition,
PatternVisitor,
Reference,
ScopeManager,
Variable,
Visitor,
} from "@typescript-eslint/scope-manager";
Referencer as OriginalReferencer,
Reference,
Scope as OriginalESLintScope,
ScopeManager as OriginalScopeManager,
} from "eslint-scope";
type ScopeDefinition = OriginalScopeDefinition &
(new (
type: OriginalScopeDefinition["type"] | "TypeParameter",
name: estree.Identifier,
node: estree.Node,
) => ScopeDefinition);
export type Options = Linter.ParserOptions & { babelOptions: InputOptions };
export type BabelToken = tokenizerToken & {
@@ -19,23 +28,6 @@ export type { Comment } from "../../../packages/babel-parser/src/types";
export type { ParseResult } from "../../../packages/babel-core/src/parser";
export type { AST } from "eslint";
declare class ScopeDefinition {
constructor(
type: string,
name: estree.Identifier,
node: estree.Node,
parent?: estree.Node,
index?: number,
kind?: string,
);
type: string;
name: estree.Identifier;
node: estree.Node;
parent?: estree.Node;
index?: number;
kind?: string;
}
declare enum ReferenceFlag {
Read = 1,
Write = 2,
@@ -47,27 +39,13 @@ interface ReferenceImplicitGlobal {
ref?: Reference;
}
declare class ESLintScope {
declare type: string;
declare set: Map<string, Variable>;
declare taints: Map<string, boolean>;
declare dynamic: boolean;
declare block: estree.Node;
declare through: Reference[];
declare variables: Variable[];
declare references: Reference[];
declare variableScope: ESLintScope;
declare functionExpressionScope: boolean;
declare directCallToEvalScope: boolean;
declare thisFound: boolean;
declare upper: ESLintScope;
declare isStrict: boolean;
declare childScopes: ESLintScope[];
declare __declaredVariables: Map<estree.Node, Variable[]>;
declare __left: Reference[];
declare class ESLintScope extends OriginalESLintScope {
variableScope: ESLintScope;
__declaredVariables: Map<estree.Node, Variable[]>;
__left: Reference[];
constructor(
scopeManager: ScopeManager,
type: string,
type: OriginalESLintScope["type"] | "type-parameters",
upperScope: ESLintScope,
block: estree.Node,
isMethodDefinition: boolean,
@@ -117,51 +95,26 @@ declare class ESLintScope {
__detectThis(): void;
__isClosed(): boolean;
}
resolve(ident: estree.Identifier): Reference | null;
isStatic(): boolean;
isArgumentsMaterialized(): boolean;
isThisMaterialized(): boolean;
isUsedName(name: any): boolean;
declare class ScopeManager extends OriginalScopeManager {
__currentScope: ESLintScope;
__nestScope(scope: ESLintScope): ESLintScope;
__nestClassFieldInitializerScope(node: estree.Node): ESLintScope;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare class Referencer extends Visitor {
readonly scopeManager: ScopeManager & {
__currentScope: ESLintScope;
__nestScope(scope: ESLintScope): ESLintScope;
__nestClassFieldInitializerScope?(scope: ESLintScope): ESLintScope;
};
options: any;
declare class Referencer extends OriginalReferencer {
readonly scopeManager: ScopeManager;
constructor(options: any, scopeManager: ScopeManager);
currentScope(): ESLintScope;
currentScope(throwOnNull: true): ESLintScope | null;
close(node: estree.Node): void;
referencingDefaultValue(
pattern: estree.Identifier,
assignments: (estree.AssignmentExpression | estree.AssignmentPattern)[],
maybeImplicitGlobal: ReferenceImplicitGlobal | null,
init: boolean,
): void;
visitProperty(node: estree.Property): void;
visitClass(node: estree.ClassDeclaration | estree.ClassExpression): void;
visitFunction(
node: estree.FunctionDeclaration | estree.FunctionExpression,
): void;
MethodDefinition(node: estree.MethodDefinition): void;
MemberExpression(node: estree.MemberExpression): void;
}
export type Scope = {
Definition: typeof ScopeDefinition;
Definition: ScopeDefinition;
PatternVisitor: typeof PatternVisitor;
Referencer: typeof Referencer;
Scope: typeof ESLintScope;
@@ -10,9 +10,7 @@ export default {
return {
CallExpression(node) {
const { callee } = node;
const scope = context.sourceCode
? context.sourceCode.getScope(node)
: context.getScope();
const scope = context.sourceCode.getScope(node);
const origin = getReferenceOrigin(callee, scope);
if (!origin) return;
@@ -17,9 +17,7 @@ export default {
return {
CallExpression(node) {
const { callee } = node;
const scope = context.sourceCode
? context.sourceCode.getScope(node)
: context.getScope();
const scope = context.sourceCode.getScope(node);
const origin = getReferenceOrigin(callee, scope);
if (!origin) return;
+6 -162
View File
@@ -1,170 +1,15 @@
// This file is revised from https://github.com/not-an-aardvark/eslint-rule-composer/blob/7e8ec43d3e62d9766e87b09c1958ce2863b421f4/lib/rule-composer.js
/**
* Translates a multi-argument context.report() call into a single object argument call
* @param {...*} arguments A list of arguments passed to `context.report`
* @returns {MessageDescriptor} A normalized object containing report information
*/
function normalizeMultiArgReportCall() {
// If there is one argument, it is considered to be a new-style call already.
if (arguments.length === 1) {
return arguments[0];
}
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
if (typeof arguments[1] === "string") {
return {
node: arguments[0],
message: arguments[1],
data: arguments[2],
fix: arguments[3],
};
}
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
return {
node: arguments[0],
loc: arguments[1],
message: arguments[2],
data: arguments[3],
fix: arguments[4],
};
}
/**
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
*/
function normalizeReportLoc(descriptor) {
if (descriptor.loc) {
if (descriptor.loc.start) {
return descriptor.loc;
}
return { start: descriptor.loc, end: null };
}
return descriptor.node.loc;
}
/**
* Interpolates data placeholders in report messages
* @param {MessageDescriptor} descriptor The report message descriptor.
* @param {Object} messageIds Message IDs from rule metadata
* @returns {{message: string, data: Object}} The interpolated message and data for the descriptor
*/
function normalizeMessagePlaceholders(descriptor, messageIds) {
const message =
typeof descriptor.messageId === "string"
? messageIds[descriptor.messageId]
: descriptor.message;
if (!descriptor.data) {
return {
message,
data: typeof descriptor.messageId === "string" ? {} : null,
};
}
const normalizedData = Object.create(null);
const interpolatedMessage = message.replace(/\{\{[^{}]+\}\}/g, fullMatch => {
const term = fullMatch.slice(2, -2).trim();
if (term in descriptor.data) {
normalizedData[term] = descriptor.data[term];
return descriptor.data[term];
}
return fullMatch;
});
return {
message: interpolatedMessage,
data: Object.freeze(normalizedData),
};
}
function getRuleMeta(rule) {
return typeof rule === "object" && rule.meta && typeof rule.meta === "object"
? rule.meta
: {};
}
function getMessageIds(rule) {
const meta = getRuleMeta(rule);
return meta.messages && typeof rule.meta.messages === "object"
? meta.messages
: {};
}
function getReportNormalizer(rule) {
const messageIds = getMessageIds(rule);
return function normalizeReport() {
const descriptor = normalizeMultiArgReportCall.apply(null, arguments);
const interpolatedMessageAndData = normalizeMessagePlaceholders(
descriptor,
messageIds,
);
return {
node: descriptor.node,
message: interpolatedMessageAndData.message,
messageId:
typeof descriptor.messageId === "string" ? descriptor.messageId : null,
data:
typeof descriptor.messageId === "string"
? interpolatedMessageAndData.data
: null,
loc: normalizeReportLoc(descriptor),
fix: descriptor.fix,
};
};
}
function getRuleCreateFunc(rule) {
return typeof rule === "function" ? rule : rule.create;
}
function removeMessageIfMessageIdPresent(reportDescriptor) {
const newDescriptor = Object.assign({}, reportDescriptor);
if (
typeof reportDescriptor.messageId === "string" &&
typeof reportDescriptor.message === "string"
) {
delete newDescriptor.message;
}
return newDescriptor;
}
export function filterReports(rule, predicate) {
return Object.freeze({
create(context) {
const filename = context.filename || context.getFilename();
const sourceCode = context.sourceCode || context.getSourceCode();
const settings = context.settings;
const options = context.options;
return getRuleCreateFunc(rule)(
return rule.create(
Object.freeze(
Object.create(context, {
report: {
enumerable: true,
value() {
const reportDescriptor = getReportNormalizer(rule).apply(
null,
arguments,
);
if (
predicate(reportDescriptor, {
sourceCode,
settings,
options,
filename,
})
) {
context.report(
removeMessageIfMessageIdPresent(reportDescriptor),
);
value(...args) {
const violation = args[0];
if (predicate(violation, context)) {
context.report(...args);
}
},
},
@@ -172,7 +17,6 @@ export function filterReports(rule, predicate) {
),
);
},
schema: rule.schema,
meta: getRuleMeta(rule),
meta: rule.meta,
});
}
@@ -1,23 +1,5 @@
import * as ruleComposer from "../rule-composer.js";
// eslint-disable-next-line import/no-unresolved
import { builtinRules } from "eslint/use-at-your-own-risk";
const rule = builtinRules.get("no-undef");
/**
* Returns whether a node is an accessor field name.
* @param {ASTNode} node CallExpression node
* @returns {Boolean} Returns true if the node is an accessor field name.
*/
function isAccessorFieldName(node) {
const parent = node.parent;
return (
parent.type === "AccessorProperty" &&
parent.key === node &&
!parent.computed
);
}
export default ruleComposer.filterReports(
rule,
problem => !isAccessorFieldName(problem.node),
);
export default rule;
@@ -1,6 +1,3 @@
"use strict";
module.exports = {
presets: [["@babel/preset-env", { forceAllTransforms: true }]],
plugins: [["@babel/plugin-proposal-decorators", { version: "legacy" }]],
export default {
plugins: [["@babel/plugin-syntax-decorators", { version: "legacy" }]],
};
@@ -1,17 +1,11 @@
"use strict";
module.exports = {
presets: [
["@babel/preset-env", { forceAllTransforms: true }],
["@babel/preset-flow", { all: true }],
"@babel/preset-react",
],
export default {
plugins: [
["@babel/plugin-syntax-flow", { all: true }],
"@babel/plugin-syntax-jsx",
"@babel/plugin-syntax-export-default-from",
"@babel/plugin-transform-class-properties",
["@babel/plugin-proposal-decorators", { version: "2023-11" }],
"@babel/plugin-transform-private-methods",
"@babel/plugin-proposal-do-expressions",
"@babel/plugin-transform-explicit-resource-management",
["@babel/plugin-syntax-decorators", { version: "2023-11" }],
"@babel/plugin-syntax-async-do-expressions",
"@babel/plugin-syntax-do-expressions",
],
};
@@ -6,17 +6,13 @@
"dependencies": {
"@babel/core": "workspace:^",
"@babel/eslint-parser": "workspace:^",
"@babel/plugin-proposal-decorators": "workspace:^",
"@babel/plugin-proposal-do-expressions": "workspace:^",
"@babel/plugin-proposal-pipeline-operator": "workspace:^",
"@babel/plugin-syntax-async-do-expressions": "workspace:^",
"@babel/plugin-syntax-decorators": "workspace:^",
"@babel/plugin-syntax-do-expressions": "workspace:^",
"@babel/plugin-syntax-export-default-from": "workspace:^",
"@babel/plugin-transform-class-properties": "workspace:^",
"@babel/plugin-transform-explicit-resource-management": "workspace:^",
"@babel/plugin-transform-private-methods": "workspace:^",
"@babel/preset-env": "workspace:^",
"@babel/preset-flow": "workspace:^",
"@babel/preset-react": "workspace:^",
"@babel/plugin-syntax-jsx": "workspace:^",
"@babel/plugin-syntax-pipeline-operator": "workspace:^",
"eslint": "^10.0.0"
},
"type": "commonjs"
"type": "module"
}
@@ -1,13 +1,17 @@
const { RuleTester } = require("eslint");
import { RuleTester } from "eslint";
import babelESLintParser from "@babel/eslint-parser";
import { fileURLToPath } from "node:url";
const defaultConfig = {
languageOptions: {
parser: require("@babel/eslint-parser"),
parser: babelESLintParser,
parserOptions: {
sourceType: "module",
ecmaVersion: "latest",
babelOptions: {
configFile: require.resolve("../config/babel.config.js"),
configFile: fileURLToPath(
new URL("../config/babel.config.js", import.meta.url)
),
},
},
},
@@ -15,4 +19,4 @@ const defaultConfig = {
RuleTester.setDefaultConfig(defaultConfig);
module.exports = RuleTester;
export default RuleTester;
@@ -1,6 +1,5 @@
import { Linter } from "eslint";
import unpad from "dedent";
import path from "node:path";
import { fileURLToPath } from "node:url";
import babelEslintParser from "@babel/eslint-parser";
import globals from "globals";
@@ -24,9 +23,11 @@ export default function verifyAndAssertMessages(
requireConfigFile: false,
...(overrideConfig && overrideConfig.parserOptions),
babelOptions: {
configFile: path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../babel-eslint-shared-fixtures/config/babel.config.js",
configFile: fileURLToPath(
new URL(
"../../../babel-eslint-shared-fixtures/config/babel.config.js",
import.meta.url,
),
),
...(overrideConfig &&
overrideConfig.parserOptions &&
@@ -1,5 +1,4 @@
import verifyAndAssertMessages from "../../helpers/verifyAndAssertMessages.js";
import path from "node:path";
import { fileURLToPath } from "node:url";
function verifyDecoratorsLegacyAndAssertMessages(
@@ -12,9 +11,11 @@ function verifyDecoratorsLegacyAndAssertMessages(
parserOptions: {
sourceType,
babelOptions: {
configFile: path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../../babel-eslint-shared-fixtures/config/babel.config.decorators-legacy.js",
configFile: fileURLToPath(
new URL(
"../../../../babel-eslint-shared-fixtures/config/babel.config.decorators-legacy.js",
import.meta.url,
),
),
},
},
@@ -1294,22 +1295,23 @@ describe("verify", () => {
]);
});
// This two tests are disabled, as the feature to visit properties when
// there is a spread/rest operator has been removed as it caused problems
// with other rules #249
// eslint-disable-next-line jest/no-disabled-tests
it.skip("visits excluded properties left of spread #95", () => {
it("visits excluded properties left of spread #95", () => {
verifyAndAssertMessages(
"var originalObject = {}; var {field1, field2, ...clone} = originalObject;",
{ "no-unused-vars": 1 },
[
"1:31 'field1' is assigned a value but never used. no-unused-vars",
"1:39 'field2' is assigned a value but never used. no-unused-vars",
"1:50 'clone' is assigned a value but never used. no-unused-vars",
],
);
});
// eslint-disable-next-line jest/no-disabled-tests
it.skip("visits excluded properties left of spread #210", () => {
it("visits excluded properties left of spread #210", () => {
verifyAndAssertMessages(
"const props = { yo: 'yo' }; const { ...otherProps } = props;",
{ "no-unused-vars": 1 },
["1:40 'otherProps' is assigned a value but never used. no-unused-vars"],
);
});
+7 -12
View File
@@ -503,7 +503,6 @@ __metadata:
"@babel/core": "workspace:^"
"@babel/helper-fixtures": "workspace:^"
"@types/estree": "npm:^1.0.5"
"@typescript-eslint/scope-manager": "npm:^8.55.0"
dedent: "npm:^1.5.3"
eslint: "npm:^10.0.0"
eslint-scope: "npm:^9.1.0"
@@ -552,16 +551,12 @@ __metadata:
dependencies:
"@babel/core": "workspace:^"
"@babel/eslint-parser": "workspace:^"
"@babel/plugin-proposal-decorators": "workspace:^"
"@babel/plugin-proposal-do-expressions": "workspace:^"
"@babel/plugin-proposal-pipeline-operator": "workspace:^"
"@babel/plugin-syntax-async-do-expressions": "workspace:^"
"@babel/plugin-syntax-decorators": "workspace:^"
"@babel/plugin-syntax-do-expressions": "workspace:^"
"@babel/plugin-syntax-export-default-from": "workspace:^"
"@babel/plugin-transform-class-properties": "workspace:^"
"@babel/plugin-transform-explicit-resource-management": "workspace:^"
"@babel/plugin-transform-private-methods": "workspace:^"
"@babel/preset-env": "workspace:^"
"@babel/preset-flow": "workspace:^"
"@babel/preset-react": "workspace:^"
"@babel/plugin-syntax-jsx": "workspace:^"
"@babel/plugin-syntax-pipeline-operator": "workspace:^"
eslint: "npm:^10.0.0"
languageName: unknown
linkType: soft
@@ -1718,7 +1713,7 @@ __metadata:
languageName: unknown
linkType: soft
"@babel/plugin-syntax-async-do-expressions@workspace:packages/babel-plugin-syntax-async-do-expressions":
"@babel/plugin-syntax-async-do-expressions@workspace:^, @babel/plugin-syntax-async-do-expressions@workspace:packages/babel-plugin-syntax-async-do-expressions":
version: 0.0.0-use.local
resolution: "@babel/plugin-syntax-async-do-expressions@workspace:packages/babel-plugin-syntax-async-do-expressions"
dependencies:
@@ -5924,7 +5919,7 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.56.0, @typescript-eslint/scope-manager@npm:^8.55.0":
"@typescript-eslint/scope-manager@npm:8.56.0":
version: 8.56.0
resolution: "@typescript-eslint/scope-manager@npm:8.56.0"
dependencies: