From 283860745345b125e545e8311d861a7babeba515 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 21 Jun 2025 14:34:25 -0500 Subject: [PATCH 01/82] change test-setup to ts --- tests/utils/{test-setup.js => test-setup.ts} | 0 vitest.config.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/utils/{test-setup.js => test-setup.ts} (100%) diff --git a/tests/utils/test-setup.js b/tests/utils/test-setup.ts similarity index 100% rename from tests/utils/test-setup.js rename to tests/utils/test-setup.ts diff --git a/vitest.config.ts b/vitest.config.ts index 56cb0fcc..5a9a60c0 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,9 +2,9 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['tests/lib/**/*.js'], + include: ['tests/lib/**/*.ts'], exclude: ['tests/lib/fixtures/**'], - setupFiles: ['tests/utils/test-setup.js'], + setupFiles: ['tests/utils/test-setup.ts'], clearMocks: true, coverage: { all: true, From 8582fabd10f133fca7c1b4f0d82d057210ec2c8a Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 21 Jun 2025 14:35:08 -0500 Subject: [PATCH 02/82] add build config --- .gitignore | 1 + eslint.config.ts | 2 +- package.json | 7 +++++-- tsconfig.json | 8 ++++++-- tsdown.config.ts | 9 +++++++++ 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 tsdown.config.ts diff --git a/.gitignore b/.gitignore index 281b1c34..ba291291 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ npm-debug.log yarn.lock .eslintcache +dist # eslint-remote-tester eslint-remote-tester-results diff --git a/eslint.config.ts b/eslint.config.ts index 36cdf25c..59e5d749 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -17,7 +17,7 @@ const compat = new FlatCompat({ export default defineConfig([ // Global ignores { - ignores: ['node_modules', 'coverage'], + ignores: ['node_modules', 'coverage', 'dist'], }, // Global settings { diff --git a/package.json b/package.json index aaa146d1..017d6078 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "license": "MIT", "scripts": { + "build": "tsdown", "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*", "lint:docs": "markdownlint \"**/*.md\"", "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", @@ -19,12 +20,13 @@ "lint:package-json": "npmPkgJsonLint .", "release": "release-it", "test": "vitest run --coverage", - "test:remote": "eslint-remote-tester -c ./eslint-remote-tester.config.ts", + "test:remote": "eslint-remote-tester", + "typecheck": "tsc", "update:eslint-docs": "eslint-doc-generator" }, "files": [ "CHANGELOG.md", - "lib/" + "dist/" ], "keywords": [ "eslint", @@ -75,6 +77,7 @@ "npm-run-all2": "^7.0.1", "prettier": "^3.4.1", "release-it": "^17.2.0", + "tsdown": "^0.12.8", "typescript": "^5.8.3", "vitest": "^3.2.4" }, diff --git a/tsconfig.json b/tsconfig.json index 977142eb..3607b466 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,10 @@ "target": "ES2024", "verbatimModuleSyntax": true, "erasableSyntaxOnly": true, - "forceConsistentCasingInFileNames": true - } + "forceConsistentCasingInFileNames": true, + "paths": { + "eslint-plugin-eslint-plugin": ["./lib/index.ts"] + } + }, + "include": ["lib/**/*", "tests/**/*", "types/**/*"] } diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 00000000..cf6d811d --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + clean: true, + dts: true, + entry: ['lib/index.ts'], + format: ['esm'], + outDir: 'dist', +}); From 046e58dfecbdc5db113351124b82c807913889de Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 21 Jun 2025 14:35:34 -0500 Subject: [PATCH 03/82] add types packages --- package.json | 4 ++++ types/estree.d.ts | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 types/estree.d.ts diff --git a/package.json b/package.json index 017d6078..62ee3798 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,11 @@ "@eslint/js": "^9.31.0", "@release-it/conventional-changelog": "^9.0.3", "@types/eslint-plugin-markdown": "^2.0.2", + "@types/eslint-scope": "^8.3.0", + "@types/espree": "^10.1.0", + "@types/estraverse": "^5.1.7", "@types/estree": "^1.0.8", + "@types/lodash": "^4.17.18", "@types/node": "^20.19.0", "@typescript-eslint/parser": "^8.34.1", "@typescript-eslint/utils": "^8.34.1", diff --git a/types/estree.d.ts b/types/estree.d.ts new file mode 100644 index 00000000..2e9a6058 --- /dev/null +++ b/types/estree.d.ts @@ -0,0 +1,5 @@ +declare module 'estree' { + interface BaseNode { + parent?: Node; + } +} From d554cd3353e4f527b316cf9c8fafd7e26370cedc Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 21 Jun 2025 14:34:03 -0500 Subject: [PATCH 04/82] rename files to ts --- lib/{index.js => index.ts} | 0 lib/rules/{consistent-output.js => consistent-output.ts} | 0 lib/rules/{fixer-return.js => fixer-return.ts} | 0 .../{meta-property-ordering.js => meta-property-ordering.ts} | 0 ...ecated-context-methods.js => no-deprecated-context-methods.ts} | 0 .../{no-deprecated-report-api.js => no-deprecated-report-api.ts} | 0 lib/rules/{no-identical-tests.js => no-identical-tests.ts} | 0 lib/rules/{no-meta-replaced-by.js => no-meta-replaced-by.ts} | 0 .../{no-meta-schema-default.js => no-meta-schema-default.ts} | 0 .../{no-missing-message-ids.js => no-missing-message-ids.ts} | 0 .../{no-missing-placeholders.js => no-missing-placeholders.ts} | 0 lib/rules/{no-only-tests.js => no-only-tests.ts} | 0 lib/rules/{no-property-in-node.js => no-property-in-node.ts} | 0 lib/rules/{no-unused-message-ids.js => no-unused-message-ids.ts} | 0 .../{no-unused-placeholders.js => no-unused-placeholders.ts} | 0 .../{no-useless-token-range.js => no-useless-token-range.ts} | 0 lib/rules/{prefer-message-ids.js => prefer-message-ids.ts} | 0 lib/rules/{prefer-object-rule.js => prefer-object-rule.ts} | 0 lib/rules/{prefer-output-null.js => prefer-output-null.ts} | 0 lib/rules/{prefer-placeholders.js => prefer-placeholders.ts} | 0 lib/rules/{prefer-replace-text.js => prefer-replace-text.ts} | 0 lib/rules/{report-message-format.js => report-message-format.ts} | 0 ...re-meta-default-options.js => require-meta-default-options.ts} | 0 ...-meta-docs-description.js => require-meta-docs-description.ts} | 0 ...-meta-docs-recommended.js => require-meta-docs-recommended.ts} | 0 lib/rules/{require-meta-docs-url.js => require-meta-docs-url.ts} | 0 lib/rules/{require-meta-fixable.js => require-meta-fixable.ts} | 0 ...re-meta-has-suggestions.js => require-meta-has-suggestions.ts} | 0 ...a-schema-description.js => require-meta-schema-description.ts} | 0 lib/rules/{require-meta-schema.js => require-meta-schema.ts} | 0 lib/rules/{require-meta-type.js => require-meta-type.ts} | 0 ...t-case-property-ordering.js => test-case-property-ordering.ts} | 0 ...t-case-shorthand-strings.js => test-case-shorthand-strings.ts} | 0 lib/{utils.js => utils.ts} | 0 tests/lib/{index.js => index.ts} | 0 tests/lib/{rule-setup.js => rule-setup.ts} | 0 tests/lib/{utils.js => utils.ts} | 0 37 files changed, 0 insertions(+), 0 deletions(-) rename lib/{index.js => index.ts} (100%) rename lib/rules/{consistent-output.js => consistent-output.ts} (100%) rename lib/rules/{fixer-return.js => fixer-return.ts} (100%) rename lib/rules/{meta-property-ordering.js => meta-property-ordering.ts} (100%) rename lib/rules/{no-deprecated-context-methods.js => no-deprecated-context-methods.ts} (100%) rename lib/rules/{no-deprecated-report-api.js => no-deprecated-report-api.ts} (100%) rename lib/rules/{no-identical-tests.js => no-identical-tests.ts} (100%) rename lib/rules/{no-meta-replaced-by.js => no-meta-replaced-by.ts} (100%) rename lib/rules/{no-meta-schema-default.js => no-meta-schema-default.ts} (100%) rename lib/rules/{no-missing-message-ids.js => no-missing-message-ids.ts} (100%) rename lib/rules/{no-missing-placeholders.js => no-missing-placeholders.ts} (100%) rename lib/rules/{no-only-tests.js => no-only-tests.ts} (100%) rename lib/rules/{no-property-in-node.js => no-property-in-node.ts} (100%) rename lib/rules/{no-unused-message-ids.js => no-unused-message-ids.ts} (100%) rename lib/rules/{no-unused-placeholders.js => no-unused-placeholders.ts} (100%) rename lib/rules/{no-useless-token-range.js => no-useless-token-range.ts} (100%) rename lib/rules/{prefer-message-ids.js => prefer-message-ids.ts} (100%) rename lib/rules/{prefer-object-rule.js => prefer-object-rule.ts} (100%) rename lib/rules/{prefer-output-null.js => prefer-output-null.ts} (100%) rename lib/rules/{prefer-placeholders.js => prefer-placeholders.ts} (100%) rename lib/rules/{prefer-replace-text.js => prefer-replace-text.ts} (100%) rename lib/rules/{report-message-format.js => report-message-format.ts} (100%) rename lib/rules/{require-meta-default-options.js => require-meta-default-options.ts} (100%) rename lib/rules/{require-meta-docs-description.js => require-meta-docs-description.ts} (100%) rename lib/rules/{require-meta-docs-recommended.js => require-meta-docs-recommended.ts} (100%) rename lib/rules/{require-meta-docs-url.js => require-meta-docs-url.ts} (100%) rename lib/rules/{require-meta-fixable.js => require-meta-fixable.ts} (100%) rename lib/rules/{require-meta-has-suggestions.js => require-meta-has-suggestions.ts} (100%) rename lib/rules/{require-meta-schema-description.js => require-meta-schema-description.ts} (100%) rename lib/rules/{require-meta-schema.js => require-meta-schema.ts} (100%) rename lib/rules/{require-meta-type.js => require-meta-type.ts} (100%) rename lib/rules/{test-case-property-ordering.js => test-case-property-ordering.ts} (100%) rename lib/rules/{test-case-shorthand-strings.js => test-case-shorthand-strings.ts} (100%) rename lib/{utils.js => utils.ts} (100%) rename tests/lib/{index.js => index.ts} (100%) rename tests/lib/{rule-setup.js => rule-setup.ts} (100%) rename tests/lib/{utils.js => utils.ts} (100%) diff --git a/lib/index.js b/lib/index.ts similarity index 100% rename from lib/index.js rename to lib/index.ts diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.ts similarity index 100% rename from lib/rules/consistent-output.js rename to lib/rules/consistent-output.ts diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.ts similarity index 100% rename from lib/rules/fixer-return.js rename to lib/rules/fixer-return.ts diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.ts similarity index 100% rename from lib/rules/meta-property-ordering.js rename to lib/rules/meta-property-ordering.ts diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.ts similarity index 100% rename from lib/rules/no-deprecated-context-methods.js rename to lib/rules/no-deprecated-context-methods.ts diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.ts similarity index 100% rename from lib/rules/no-deprecated-report-api.js rename to lib/rules/no-deprecated-report-api.ts diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.ts similarity index 100% rename from lib/rules/no-identical-tests.js rename to lib/rules/no-identical-tests.ts diff --git a/lib/rules/no-meta-replaced-by.js b/lib/rules/no-meta-replaced-by.ts similarity index 100% rename from lib/rules/no-meta-replaced-by.js rename to lib/rules/no-meta-replaced-by.ts diff --git a/lib/rules/no-meta-schema-default.js b/lib/rules/no-meta-schema-default.ts similarity index 100% rename from lib/rules/no-meta-schema-default.js rename to lib/rules/no-meta-schema-default.ts diff --git a/lib/rules/no-missing-message-ids.js b/lib/rules/no-missing-message-ids.ts similarity index 100% rename from lib/rules/no-missing-message-ids.js rename to lib/rules/no-missing-message-ids.ts diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.ts similarity index 100% rename from lib/rules/no-missing-placeholders.js rename to lib/rules/no-missing-placeholders.ts diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.ts similarity index 100% rename from lib/rules/no-only-tests.js rename to lib/rules/no-only-tests.ts diff --git a/lib/rules/no-property-in-node.js b/lib/rules/no-property-in-node.ts similarity index 100% rename from lib/rules/no-property-in-node.js rename to lib/rules/no-property-in-node.ts diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.ts similarity index 100% rename from lib/rules/no-unused-message-ids.js rename to lib/rules/no-unused-message-ids.ts diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.ts similarity index 100% rename from lib/rules/no-unused-placeholders.js rename to lib/rules/no-unused-placeholders.ts diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.ts similarity index 100% rename from lib/rules/no-useless-token-range.js rename to lib/rules/no-useless-token-range.ts diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.ts similarity index 100% rename from lib/rules/prefer-message-ids.js rename to lib/rules/prefer-message-ids.ts diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.ts similarity index 100% rename from lib/rules/prefer-object-rule.js rename to lib/rules/prefer-object-rule.ts diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.ts similarity index 100% rename from lib/rules/prefer-output-null.js rename to lib/rules/prefer-output-null.ts diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.ts similarity index 100% rename from lib/rules/prefer-placeholders.js rename to lib/rules/prefer-placeholders.ts diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.ts similarity index 100% rename from lib/rules/prefer-replace-text.js rename to lib/rules/prefer-replace-text.ts diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.ts similarity index 100% rename from lib/rules/report-message-format.js rename to lib/rules/report-message-format.ts diff --git a/lib/rules/require-meta-default-options.js b/lib/rules/require-meta-default-options.ts similarity index 100% rename from lib/rules/require-meta-default-options.js rename to lib/rules/require-meta-default-options.ts diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.ts similarity index 100% rename from lib/rules/require-meta-docs-description.js rename to lib/rules/require-meta-docs-description.ts diff --git a/lib/rules/require-meta-docs-recommended.js b/lib/rules/require-meta-docs-recommended.ts similarity index 100% rename from lib/rules/require-meta-docs-recommended.js rename to lib/rules/require-meta-docs-recommended.ts diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.ts similarity index 100% rename from lib/rules/require-meta-docs-url.js rename to lib/rules/require-meta-docs-url.ts diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.ts similarity index 100% rename from lib/rules/require-meta-fixable.js rename to lib/rules/require-meta-fixable.ts diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.ts similarity index 100% rename from lib/rules/require-meta-has-suggestions.js rename to lib/rules/require-meta-has-suggestions.ts diff --git a/lib/rules/require-meta-schema-description.js b/lib/rules/require-meta-schema-description.ts similarity index 100% rename from lib/rules/require-meta-schema-description.js rename to lib/rules/require-meta-schema-description.ts diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.ts similarity index 100% rename from lib/rules/require-meta-schema.js rename to lib/rules/require-meta-schema.ts diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.ts similarity index 100% rename from lib/rules/require-meta-type.js rename to lib/rules/require-meta-type.ts diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.ts similarity index 100% rename from lib/rules/test-case-property-ordering.js rename to lib/rules/test-case-property-ordering.ts diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.ts similarity index 100% rename from lib/rules/test-case-shorthand-strings.js rename to lib/rules/test-case-shorthand-strings.ts diff --git a/lib/utils.js b/lib/utils.ts similarity index 100% rename from lib/utils.js rename to lib/utils.ts diff --git a/tests/lib/index.js b/tests/lib/index.ts similarity index 100% rename from tests/lib/index.js rename to tests/lib/index.ts diff --git a/tests/lib/rule-setup.js b/tests/lib/rule-setup.ts similarity index 100% rename from tests/lib/rule-setup.js rename to tests/lib/rule-setup.ts diff --git a/tests/lib/utils.js b/tests/lib/utils.ts similarity index 100% rename from tests/lib/utils.js rename to tests/lib/utils.ts From bc56d00370ce3f7a75cbc56203bcd58aa43c1594 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 21 Jun 2025 17:21:29 -0500 Subject: [PATCH 05/82] migrate fixer-return migrate fixer-return migrate fixer-return --- lib/rules/fixer-return.ts | 63 ++++-- lib/types.ts | 34 +++ lib/utils.ts | 465 +++++++++++++++++++++++--------------- types/estree.d.ts | 22 +- 4 files changed, 382 insertions(+), 202 deletions(-) create mode 100644 lib/types.ts diff --git a/lib/rules/fixer-return.ts b/lib/rules/fixer-return.ts index 3fb92911..3c880166 100644 --- a/lib/rules/fixer-return.ts +++ b/lib/rules/fixer-return.ts @@ -4,6 +4,31 @@ */ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { + ArrowFunctionExpression, + FunctionExpression, + Identifier, + Node, + Position, + SourceLocation, +} from 'estree'; + +import { + getContextIdentifiers, + isAutoFixerFunction, + isSuggestionFixerFunction, +} from '../utils'; +import type { FunctionInfo } from '../types'; + +const DEFAULT_FUNC_INFO: FunctionInfo = { + upper: null, + codePath: null, + hasReturnWithFixer: false, + hasYieldWithFixer: false, + shouldCheck: false, + node: null, +}; import { getContextIdentifiers, @@ -14,9 +39,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -25,7 +48,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/fixer-return.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { missingFix: 'Fixer function never returned a fix.', @@ -33,15 +56,8 @@ const rule = { }, create(context) { - let funcInfo = { - upper: null, - codePath: null, - hasReturnWithFixer: false, - hasYieldWithFixer: false, - shouldCheck: false, - node: null, - }; - let contextIdentifiers; + let funcInfo: FunctionInfo = DEFAULT_FUNC_INFO; + let contextIdentifiers = new Set(); /** * As we exit the fix() function, ensure we have returned or yielded a real fix by this point. @@ -52,9 +68,13 @@ const rule = { * @returns {void} */ function ensureFunctionReturnedFix( - node, - loc = (node.id || node).loc.start, - ) { + node: ArrowFunctionExpression | FunctionExpression, + loc: Position | SourceLocation | undefined = (node.type === + 'FunctionExpression' && node.id + ? node.id + : node + ).loc?.start, + ): void { if ( (node.generator && !funcInfo.hasYieldWithFixer) || // Generator function never yielded a fix (!node.generator && !funcInfo.hasReturnWithFixer) // Non-generator function never returned a fix @@ -70,10 +90,9 @@ const rule = { /** * Check if a returned/yielded node is likely to be a fix or not. * A fix is an object created by fixer.replaceText() for example and returned by the fix function. - * @param {ASTNode} node - node to check - * @returns {boolean} + * @param node - node to check */ - function isFix(node) { + function isFix(node: Node): boolean { if (node.type === 'ArrayExpression' && node.elements.length === 0) { // An empty array is not a fix. return false; @@ -104,7 +123,7 @@ const rule = { }, // Stacks this function's information. - onCodePathStart(codePath, node) { + onCodePathStart(codePath: Rule.CodePath, node: Node) { funcInfo = { upper: funcInfo, codePath, @@ -119,7 +138,7 @@ const rule = { // Pops this function's information. onCodePathEnd() { - funcInfo = funcInfo.upper; + funcInfo = funcInfo.upper ?? DEFAULT_FUNC_INFO; }, // Yield in generators @@ -147,7 +166,7 @@ const rule = { 'ArrowFunctionExpression:exit'(node) { if (funcInfo.shouldCheck) { const sourceCode = context.sourceCode; - const loc = sourceCode.getTokenBefore(node.body).loc; // Show violation on arrow (=>). + const loc = sourceCode.getTokenBefore(node.body)?.loc; // Show violation on arrow (=>). if (node.expression) { // When the return is implied (no curly braces around the body), we have to check the single body node directly. if (!isFix(node.body)) { diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..be945a93 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,34 @@ +import type { Rule } from 'eslint'; +import type { + ArrowFunctionExpression, + Expression, + FunctionDeclaration, + FunctionExpression, + Node, + SpreadElement, +} from 'estree'; + +export interface FunctionInfo { + codePath: Rule.CodePath | null; + hasReturnWithFixer: boolean; + hasYieldWithFixer: boolean; + node: Node | null; + shouldCheck: boolean; + upper: FunctionInfo | null; +} + +export interface PartialRuleInfo { + create?: Node | null; + isNewStyle?: boolean; + meta?: Node | null; +} + +export interface RuleInfo extends PartialRuleInfo { + create: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration; + isNewStyle: boolean; +} + +export type TestInfo = { + invalid: (Expression | SpreadElement | null)[]; + valid: (Expression | SpreadElement | null)[]; +}; diff --git a/lib/utils.ts b/lib/utils.ts index 23e1e4b8..33b926e6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,27 +1,56 @@ import { getStaticValue, findVariable } from '@eslint-community/eslint-utils'; +import type { Rule, Scope } from 'eslint'; import estraverse from 'estraverse'; +import type { + ArrowFunctionExpression, + CallExpression, + Directive, + Expression, + FunctionDeclaration, + FunctionExpression, + Identifier, + MemberExpression, + ModuleDeclaration, + Node, + ObjectExpression, + Pattern, + Program, + Property, + SpreadElement, + Statement, + Super, + TSExportAssignment, +} from 'estree'; + +import type { PartialRuleInfo, RuleInfo, TestInfo } from './types'; const functionTypes = new Set([ 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration', ]); +const isFunctionType = ( + node: Node | null | undefined, +): node is FunctionExpression | ArrowFunctionExpression | FunctionDeclaration => + !!node && functionTypes.has(node.type); /** * Determines whether a node is a 'normal' (i.e. non-async, non-generator) function expression. - * @param {ASTNode} node The node in question - * @returns {boolean} `true` if the node is a normal function expression + * @param node The node in question + * @returns `true` if the node is a normal function expression */ -function isNormalFunctionExpression(node) { - return functionTypes.has(node.type) && !node.generator && !node.async; +function isNormalFunctionExpression( + node: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, +): boolean { + return !node.generator && !node.async; } /** * Determines whether a node is constructing a RuleTester instance * @param {ASTNode} node The node in question - * @returns {boolean} `true` if the node is probably constructing a RuleTester instance + * @returns `true` if the node is probably constructing a RuleTester instance */ -function isRuleTesterConstruction(node) { +function isRuleTesterConstruction(node: Expression | Super): boolean { return ( node.type === 'NewExpression' && ((node.callee.type === 'Identifier' && node.callee.name === 'RuleTester') || @@ -31,34 +60,46 @@ function isRuleTesterConstruction(node) { ); } -const INTERESTING_RULE_KEYS = new Set(['create', 'meta']); +const interestingRuleKeys = ['create', 'meta'] as const; +type InterestingRuleKey = (typeof interestingRuleKeys)[number]; +const INTERESTING_RULE_KEYS = new Set(interestingRuleKeys); + +const isInterestingRuleKey = (key: string): key is InterestingRuleKey => + INTERESTING_RULE_KEYS.has(key as InterestingRuleKey); /** * Collect properties from an object that have interesting key names into a new object - * @param {Node[]} properties - * @param {Set} interestingKeys - * @returns Object + * @param properties + * @param interestingKeys */ -function collectInterestingProperties(properties, interestingKeys) { - return properties.reduce((parsedProps, prop) => { - const keyValue = getKeyName(prop); - if (interestingKeys.has(keyValue)) { - // In TypeScript, unwrap any usage of `{} as const`. - parsedProps[keyValue] = - prop.value.type === 'TSAsExpression' - ? prop.value.expression - : prop.value; - } - return parsedProps; - }, {}); +function collectInterestingProperties( + properties: (Property | SpreadElement)[], + interestingKeys: Set, +): Record { + return properties.reduce>( + (parsedProps, prop) => { + const keyValue = getKeyName(prop); + if ( + prop.type === 'Property' && + keyValue && + interestingKeys.has(keyValue as T) + ) { + // In TypeScript, unwrap any usage of `{} as const`. + parsedProps[keyValue] = + prop.value.type === 'TSAsExpression' + ? prop.value.expression + : prop.value; + } + return parsedProps; + }, + {}, + ); } /** * Check if there is a return statement that returns an object somewhere inside the given node. - * @param {Node} node - * @returns {boolean} */ -function hasObjectReturn(node) { +function hasObjectReturn(node: Node): boolean { let foundMatch = false; estraverse.traverse(node, { enter(child) { @@ -77,11 +118,11 @@ function hasObjectReturn(node) { /** * Determine if the given node is likely to be a function-style rule. - * @param {*} node - * @returns {boolean} + * @param node */ -function isFunctionRule(node) { +function isFunctionRule(node: Node): boolean { return ( + isFunctionType(node) && // Is a function expression or declaration. isNormalFunctionExpression(node) && // Is a function definition. node.params.length === 1 && // The function has a single `context` argument. hasObjectReturn(node) // Returns an object containing the visitor functions. @@ -90,10 +131,10 @@ function isFunctionRule(node) { /** * Check if the given node is a function call representing a known TypeScript rule creator format. - * @param {Node} node - * @returns {boolean} */ -function isTypeScriptRuleHelper(node) { +function isTypeScriptRuleHelper( + node: Node, +): node is CallExpression & { arguments: ObjectExpression[] } { return ( node.type === 'CallExpression' && node.arguments.length === 1 && @@ -116,14 +157,19 @@ function isTypeScriptRuleHelper(node) { /** * Helper for `getRuleInfo`. Handles ESM and TypeScript rules. */ -function getRuleExportsESM(ast, scopeManager) { - const possibleNodes = []; +function getRuleExportsESM( + ast: Omit & { + body: (Directive | Statement | ModuleDeclaration | TSExportAssignment)[]; + }, + scopeManager: Scope.ScopeManager, +): PartialRuleInfo { + const possibleNodes: Node[] = []; for (const statement of ast.body) { switch (statement.type) { // export default rule; case 'ExportDefaultDeclaration': { - possibleNodes.push(statement.declaration); + possibleNodes.push(statement.declaration as Identifier); break; } // export = rule; @@ -141,7 +187,7 @@ function getRuleExportsESM(ast, scopeManager) { const nodes = statement.declaration.type === 'VariableDeclaration' ? statement.declaration.declarations.map( - (declarator) => declarator.init, + (declarator) => declarator.init!, ) : [statement.declaration]; @@ -149,7 +195,7 @@ function getRuleExportsESM(ast, scopeManager) { // skip if it's function-style to avoid false positives // refs: https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/450 possibleNodes.push( - ...nodes.filter((node) => node && !functionTypes.has(node.type)), + ...nodes.filter((node) => node && !isFunctionType(node)), ); } break; @@ -157,7 +203,7 @@ function getRuleExportsESM(ast, scopeManager) { } } - return possibleNodes.reduce((currentExports, node) => { + return possibleNodes.reduce((currentExports, node) => { if (node.type === 'ObjectExpression') { // Check `export default { create() {}, meta: {} }` return collectInterestingProperties( @@ -196,13 +242,16 @@ function getRuleExportsESM(ast, scopeManager) { } } return currentExports; - }, {}); + }, {} as PartialRuleInfo); } /** * Helper for `getRuleInfo`. Handles CJS rules. */ -function getRuleExportsCJS(ast, scopeManager) { +function getRuleExportsCJS( + ast: Program, + scopeManager: Scope.ScopeManager, +): PartialRuleInfo { let exportsVarOverridden = false; let exportsIsFunction = false; return ast.body @@ -210,13 +259,13 @@ function getRuleExportsCJS(ast, scopeManager) { .map((statement) => statement.expression) .filter((expression) => expression.type === 'AssignmentExpression') .filter((expression) => expression.left.type === 'MemberExpression') - - .reduce((currentExports, node) => { + .reduce((currentExports, node) => { + const leftExpression = node.left as MemberExpression; if ( - node.left.object.type === 'Identifier' && - node.left.object.name === 'module' && - node.left.property.type === 'Identifier' && - node.left.property.name === 'exports' + leftExpression.object.type === 'Identifier' && + leftExpression.object.name === 'module' && + leftExpression.property.type === 'Identifier' && + leftExpression.property.name === 'exports' ) { exportsVarOverridden = true; if (isFunctionRule(node.right)) { @@ -250,66 +299,71 @@ function getRuleExportsCJS(ast, scopeManager) { return {}; } else if ( !exportsIsFunction && - node.left.object.type === 'MemberExpression' && - node.left.object.object.type === 'Identifier' && - node.left.object.object.name === 'module' && - node.left.object.property.type === 'Identifier' && - node.left.object.property.name === 'exports' && - node.left.property.type === 'Identifier' && - INTERESTING_RULE_KEYS.has(node.left.property.name) + leftExpression.object.type === 'MemberExpression' && + leftExpression.object.object.type === 'Identifier' && + leftExpression.object.object.name === 'module' && + leftExpression.object.property.type === 'Identifier' && + leftExpression.object.property.name === 'exports' && + leftExpression.property.type === 'Identifier' && + isInterestingRuleKey(leftExpression.property.name) ) { // Check `module.exports.create = () => {}` - currentExports[node.left.property.name] = node.right; + currentExports[leftExpression.property.name] = node.right; } else if ( !exportsVarOverridden && - node.left.object.type === 'Identifier' && - node.left.object.name === 'exports' && - node.left.property.type === 'Identifier' && - INTERESTING_RULE_KEYS.has(node.left.property.name) + leftExpression.object.type === 'Identifier' && + leftExpression.object.name === 'exports' && + leftExpression.property.type === 'Identifier' && + isInterestingRuleKey(leftExpression.property.name) ) { // Check `exports.create = () => {}` - currentExports[node.left.property.name] = node.right; + currentExports[leftExpression.property.name] = node.right; } return currentExports; - }, {}); + }, {} as PartialRuleInfo); } /** * Find the value of a property in an object by its property key name. - * @param {Object} obj - * @param {String} keyName + * @param obj * @returns property value */ -function findObjectPropertyValueByKeyName(obj, keyName) { +function findObjectPropertyValueByKeyName( + obj: ObjectExpression, + keyName: String, +): Property['value'] | undefined { const property = obj.properties.find( - (prop) => prop.key.type === 'Identifier' && prop.key.name === keyName, - ); + (prop) => + prop.type === 'Property' && + prop.key.type === 'Identifier' && + prop.key.name === keyName, + ) as Property | undefined; return property ? property.value : undefined; } /** * Get the first value (or function) that a variable is initialized to. - * @param {Node} node - the Identifier node for the variable. - * @param {ScopeManager} scopeManager + * @param node - the Identifier node for the variable. * @returns the first value (or function) that the given variable is initialized to. */ -function findVariableValue(node, scopeManager) { +function findVariableValue( + node: Identifier, + scopeManager: Scope.ScopeManager, +): Expression | FunctionDeclaration | undefined { const variable = findVariable( - scopeManager.acquire(node) || scopeManager.globalScope, + scopeManager.acquire(node) || scopeManager.globalScope!, node, ); if (variable && variable.defs && variable.defs[0] && variable.defs[0].node) { - if ( - variable.defs[0].node.type === 'VariableDeclarator' && - variable.defs[0].node.init - ) { + const variableDefNode: Node = variable.defs[0].node; + if (variableDefNode.type === 'VariableDeclarator' && variableDefNode.init) { // Given node `x`, get `123` from `const x = 123;`. - return variable.defs[0].node.init; - } else if (variable.defs[0].node.type === 'FunctionDeclaration') { + return variableDefNode.init; + } else if (variableDefNode.type === 'FunctionDeclaration') { // Given node `foo`, get `function foo() {}` from `function foo() {}`. - return variable.defs[0].node; + return variableDefNode; } } } @@ -319,10 +373,10 @@ function findVariableValue(node, scopeManager) { * If a ternary conditional expression is involved, retrieve the elements that may exist on both sides of it. * Ex: [a, b, c] will return [a, b, c] * Ex: foo ? [a, b, c] : [d, e, f] will return [a, b, c, d, e, f] - * @param {Node} node - * @returns {Node[]} the list of elements + * @param node + * @returns the list of elements */ -function collectArrayElements(node) { +function collectArrayElements(node: Node): (Node | null)[] { if (!node) { return []; } @@ -340,57 +394,64 @@ function collectArrayElements(node) { /** * Performs static analysis on an AST to try to determine the final value of `module.exports`. -* @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager` -* @returns {Object} An object with keys `meta`, `create`, and `isNewStyle`. `meta` and `create` correspond to the AST nodes +* @param sourceCode The object contains `Program` AST node, and optional `scopeManager` +* @returns An object with keys `meta`, `create`, and `isNewStyle`. `meta` and `create` correspond to the AST nodes for the final values of `module.exports.meta` and `module.exports.create`. `isNewStyle` will be `true` if `module.exports` is an object, and `false` if `module.exports` is just the `create` function. If no valid ESLint rule info can be extracted from the file, the return value will be `null`. */ -export function getRuleInfo({ ast, scopeManager }) { +export function getRuleInfo({ + ast, + scopeManager, +}: { + ast: Program; + scopeManager: Scope.ScopeManager; +}): RuleInfo | null { const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast, scopeManager) : getRuleExportsCJS(ast, scopeManager); - const createExists = Object.prototype.hasOwnProperty.call( - exportNodes, - 'create', - ); + const createExists = 'create' in exportNodes; if (!createExists) { return null; } // If create/meta are defined in variables, get their values. - for (const key of Object.keys(exportNodes)) { - if (exportNodes[key] && exportNodes[key].type === 'Identifier') { - const value = findVariableValue(exportNodes[key], scopeManager); + for (const key of interestingRuleKeys) { + const exportNode = exportNodes[key]; + if (exportNode && exportNode.type === 'Identifier') { + const value = findVariableValue(exportNode, scopeManager); if (value) { exportNodes[key] = value; } } } - const createIsFunction = isNormalFunctionExpression(exportNodes.create); - if (!createIsFunction) { + const { create, ...remainingExportNodes } = exportNodes; + if (!(isFunctionType(create) && isNormalFunctionExpression(create))) { return null; } - return Object.assign({ isNewStyle: true, meta: null }, exportNodes); + return { isNewStyle: true, create, ...remainingExportNodes }; } /** * Gets all the identifiers referring to the `context` variable in a rule source file. Note that this function will * only work correctly after traversing the AST has started (e.g. in the first `Program` node). - * @param {RuleContext} scopeManager - * @param {ASTNode} ast The `Program` node for the file - * @returns {Set} A Set of all `Identifier` nodes that are references to the `context` value for the file + * @param scopeManager + * @param ast The `Program` node for the file + * @returns A Set of all `Identifier` nodes that are references to the `context` value for the file */ -export function getContextIdentifiers(scopeManager, ast) { +export function getContextIdentifiers( + scopeManager: Scope.ScopeManager, + ast: Program, +): Set { const ruleInfo = getRuleInfo({ ast, scopeManager }); if ( !ruleInfo || - ruleInfo.create.params.length === 0 || + ruleInfo.create?.params.length === 0 || ruleInfo.create.params[0].type !== 'Identifier' ) { return new Set(); @@ -399,19 +460,25 @@ export function getContextIdentifiers(scopeManager, ast) { return new Set( scopeManager .getDeclaredVariables(ruleInfo.create) - .find((variable) => variable.name === ruleInfo.create.params[0].name) + .find( + (variable) => + variable.name === (ruleInfo.create.params[0] as Identifier).name, + )! .references.map((ref) => ref.identifier), ); } /** * Gets the key name of a Property, if it can be determined statically. - * @param {ASTNode} node The `Property` node - * @param {Scope} scope - * @returns {string|null} The key name, or `null` if the name cannot be determined statically. + * @param node The `Property` node + * @param scope + * @returns The key name, or `null` if the name cannot be determined statically. */ -export function getKeyName(property, scope) { - if (!property.key) { +export function getKeyName( + property: Property | SpreadElement, + scope?: Scope.Scope, +): string | null { + if (!('key' in property)) { // likely a SpreadElement or another non-standard node return null; } @@ -420,7 +487,7 @@ export function getKeyName(property, scope) { // Variable key: { [myVariable]: 'hello world' } if (scope) { const staticValue = getStaticValue(property.key, scope); - return staticValue ? staticValue.value : null; + return staticValue ? (staticValue.value as string) : null; } // TODO: ensure scope is always passed to getKeyName() so we don't need to handle the case where it's not passed. return null; @@ -434,7 +501,7 @@ export function getKeyName(property, scope) { property.key.type === 'TemplateLiteral' && property.key.quasis.length === 1 ) { - return property.key.quasis[0].value.cooked; + return property.key.quasis[0].value.cooked ?? null; } return null; } @@ -442,10 +509,11 @@ export function getKeyName(property, scope) { /** * Extracts the body of a function if the given node is a function * - * @param {ASTNode} node - * @returns {ExpressionStatement[]} + * @param node */ -export function extractFunctionBody(node) { +function extractFunctionBody( + node: Expression | SpreadElement, +): (Statement | Expression)[] { if ( node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression' @@ -463,17 +531,16 @@ export function extractFunctionBody(node) { /** * Checks the given statements for possible test info * - * @param {RuleContext} context The `context` variable for the source file itself - * @param {ASTNode[]} statements The statements to check - * @param {Set} variableIdentifiers - * @returns {CallExpression[]} + * @param context The `context` variable for the source file itself + * @param statements The statements to check + * @param variableIdentifiers */ -export function checkStatementsForTestInfo( - context, - statements, - variableIdentifiers = new Set(), -) { - const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9 +function checkStatementsForTestInfo( + context: Rule.RuleContext, + statements: (ModuleDeclaration | Statement | Directive | Expression)[], + variableIdentifiers = new Set(), +): CallExpression[] { + const sourceCode = context.sourceCode; const runCalls = []; for (const statement of statements) { @@ -497,9 +564,7 @@ export function checkStatementsForTestInfo( isRuleTesterConstruction(declarator.init) && declarator.id.type === 'Identifier' ) { - const vars = sourceCode.getDeclaredVariables - ? sourceCode.getDeclaredVariables(declarator) - : context.getDeclaredVariables(declarator); + const vars = sourceCode.getDeclaredVariables(declarator); vars.forEach((variable) => { variable.references .filter((ref) => ref.isRead()) @@ -565,20 +630,20 @@ export function checkStatementsForTestInfo( /** * Performs static analysis on an AST to try to find test cases - * @param {RuleContext} context The `context` variable for the source file itself - * @param {ASTNode} ast The `Program` node for the file. - * @returns {object} An object with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests + * @param context The `context` variable for the source file itself + * @param ast The `Program` node for the file. + * @returns A list of objects with `valid` and `invalid` keys containing a list of AST nodes corresponding to tests */ -export function getTestInfo(context, ast) { +export function getTestInfo( + context: Rule.RuleContext, + ast: Program, +): TestInfo[] { const runCalls = checkStatementsForTestInfo(context, ast.body); return runCalls - .filter( - (call) => - call.arguments.length >= 3 && - call.arguments[2].type === 'ObjectExpression', - ) + .filter((call) => call.arguments.length >= 3) .map((call) => call.arguments[2]) + .filter((call) => call.type === 'ObjectExpression') .map((run) => { const validProperty = run.properties.find( (prop) => getKeyName(prop) === 'valid', @@ -589,11 +654,15 @@ export function getTestInfo(context, ast) { return { valid: - validProperty && validProperty.value.type === 'ArrayExpression' + validProperty && + validProperty.type !== 'SpreadElement' && + validProperty.value.type === 'ArrayExpression' ? validProperty.value.elements.filter(Boolean) : [], invalid: - invalidProperty && invalidProperty.value.type === 'ArrayExpression' + invalidProperty && + invalidProperty.type !== 'SpreadElement' && + invalidProperty.value.type === 'ArrayExpression' ? invalidProperty.value.elements.filter(Boolean) : [], }; @@ -602,10 +671,9 @@ export function getTestInfo(context, ast) { /** * Gets information on a report, given the ASTNode of context.report(). - * @param {ASTNode} node The ASTNode of context.report() - * @param {Context} context + * @param node The ASTNode of context.report() */ -export function getReportInfo(node, context) { +export function getReportInfo(node: CallExpression, context: Rule.RuleContext) { const reportArgs = node.arguments; // If there is exactly one argument, the API expects an object. @@ -632,8 +700,8 @@ export function getReportInfo(node, context) { } let keys; - const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: use context.sourceCode when dropping eslint < v9 - const scope = sourceCode.getScope?.(node) || context.getScope(); // TODO: just use sourceCode.getScope() when dropping eslint < v9 + const sourceCode = context.sourceCode; + const scope = sourceCode.getScope(node); const secondArgStaticValue = getStaticValue(reportArgs[1], scope); if ( @@ -664,11 +732,14 @@ export function getReportInfo(node, context) { /** * Gets a set of all `sourceCode` identifiers. - * @param {ScopeManager} scopeManager + * @param scopeManager * @param {ASTNode} ast The AST of the file. This must have `parent` properties. * @returns {Set} A set of all identifiers referring to the `SourceCode` object. */ -export function getSourceCodeIdentifiers(scopeManager, ast) { +export function getSourceCodeIdentifiers( + scopeManager: Scope.ScopeManager, + ast, +) { return new Set( [...getContextIdentifiers(scopeManager, ast)] .filter( @@ -711,10 +782,12 @@ export function insertProperty(fixer, node, propertyText, sourceCode) { /** * Collect all context.report({...}) violation/suggestion-related nodes into a standardized array for convenience. - * @param {Object} reportInfo - Result of getReportInfo(). + * @param reportInfo - Result of getReportInfo(). * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[] */ -export function collectReportViolationAndSuggestionData(reportInfo) { +export function collectReportViolationAndSuggestionData( + reportInfo: ReturnType, +) { return [ // Violation message { @@ -746,29 +819,35 @@ export function collectReportViolationAndSuggestionData(reportInfo) { /** * Whether the provided node represents an autofixer function. - * @param {Node} node - * @param {Node[]} contextIdentifiers - * @returns {boolean} + * @param node + * @param contextIdentifiers */ -export function isAutoFixerFunction(node, contextIdentifiers) { +export function isAutoFixerFunction( + node: Node, + contextIdentifiers: Set, +): node is FunctionExpression | ArrowFunctionExpression { const parent = node.parent; return ( ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) && parent.parent.type === 'ObjectExpression' && parent.parent.parent.type === 'CallExpression' && - contextIdentifiers.has(parent.parent.parent.callee.object) && + parent.parent.parent.callee.type === 'MemberExpression' && + contextIdentifiers.has(parent.parent.parent.callee.object as Identifier) && + parent.parent.parent.callee.property.type === 'Identifier' && parent.parent.parent.callee.property.name === 'report' && - getReportInfo(parent.parent.parent).fix === node + getReportInfo(parent.parent.parent)?.fix === node ); } /** * Whether the provided node represents a suggestion fixer function. - * @param {Node} node - * @param {Node[]} contextIdentifiers - * @returns {boolean} + * @param node + * @param contextIdentifiers */ -export function isSuggestionFixerFunction(node, contextIdentifiers) { +export function isSuggestionFixerFunction( + node: Node, + contextIdentifiers: Set, +): boolean { const parent = node.parent; return ( (node.type === 'FunctionExpression' || @@ -796,11 +875,14 @@ export function isSuggestionFixerFunction(node, contextIdentifiers) { /** * List all properties contained in an object. * Evaluates and includes any properties that may be behind spreads. - * @param {Node} objectNode - * @param {ScopeManager} scopeManager - * @returns {Node[]} the list of all properties that could be found + * @param objectNode + * @param scopeManager + * @returns the list of all properties that could be found */ -export function evaluateObjectProperties(objectNode, scopeManager) { +export function evaluateObjectProperties( + objectNode: Node, + scopeManager: Scope.ScopeManager, +): Node[] { if (!objectNode || objectNode.type !== 'ObjectExpression') { return []; } @@ -817,7 +899,11 @@ export function evaluateObjectProperties(objectNode, scopeManager) { }); } -export function getMetaDocsProperty(propertyName, ruleInfo, scopeManager) { +export function getMetaDocsProperty( + propertyName: string, + ruleInfo, + scopeManager: Scope.ScopeManager, +) { const metaNode = ruleInfo.meta; const docsNode = evaluateObjectProperties(metaNode, scopeManager).find( @@ -835,10 +921,12 @@ export function getMetaDocsProperty(propertyName, ruleInfo, scopeManager) { /** * Get the `meta.messages` node from a rule. * @param {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @returns {Node|undefined} + * @param scopeManager */ -export function getMessagesNode(ruleInfo, scopeManager) { +export function getMessagesNode( + ruleInfo, + scopeManager: Scope.ScopeManager, +): Node | undefined { if (!ruleInfo) { return; } @@ -862,10 +950,12 @@ export function getMessagesNode(ruleInfo, scopeManager) { /** * Get the list of messageId properties from `meta.messages` for a rule. * @param {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @returns {Node[]|undefined} + * @param scopeManager */ -export function getMessageIdNodes(ruleInfo, scopeManager) { +export function getMessageIdNodes( + ruleInfo, + scopeManager: Scope.ScopeManager, +): Node | undefined { const messagesNode = getMessagesNode(ruleInfo, scopeManager); return messagesNode && messagesNode.type === 'ObjectExpression' @@ -875,25 +965,36 @@ export function getMessageIdNodes(ruleInfo, scopeManager) { /** * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`. - * @param {String} messageId - the messageId to check for + * @param messageId - the messageId to check for * @param {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @param {Scope} scope - * @returns {Node|undefined} The matching messageId property from `meta.messages`. + * @param scopeManager + * @param scope + * @returns The matching messageId property from `meta.messages`. */ -export function getMessageIdNodeById(messageId, ruleInfo, scopeManager, scope) { +export function getMessageIdNodeById( + messageId: string, + ruleInfo, + scopeManager: Scope.ScopeManager, + scope: Scope.Scope, +): Node | undefined { return getMessageIdNodes(ruleInfo, scopeManager).find( (p) => p.type === 'Property' && getKeyName(p, scope) === messageId, ); } -export function getMetaSchemaNode(metaNode, scopeManager) { +export function getMetaSchemaNode( + metaNode, + scopeManager: Scope.ScopeManager, +): Node | undefined { return evaluateObjectProperties(metaNode, scopeManager).find( (p) => p.type === 'Property' && getKeyName(p) === 'schema', ); } -export function getMetaSchemaNodeProperty(schemaNode, scopeManager) { +export function getMetaSchemaNodeProperty( + schemaNode, + scopeManager: Scope.ScopeManager, +) { if (!schemaNode) { return null; } @@ -925,11 +1026,14 @@ export function getMetaSchemaNodeProperty(schemaNode, scopeManager) { /** * Get the possible values that a variable was initialized to at some point. - * @param {Node} node - the Identifier node for the variable. - * @param {ScopeManager} scopeManager - * @returns {Node[]} the values that the given variable could be initialized to. + * @param node - the Identifier node for the variable. + * @param scopeManager + * @returns the values that the given variable could be initialized to. */ -export function findPossibleVariableValues(node, scopeManager) { +export function findPossibleVariableValues( + node: Node, + scopeManager: Scope.ScopeManager, +): Node[] { const variable = findVariable( scopeManager.acquire(node) || scopeManager.globalScope, node, @@ -949,20 +1053,23 @@ export function findPossibleVariableValues(node, scopeManager) { } /** - * @param {Node} node - * @returns {boolean} Whether the node is an Identifier with name `undefined`. + * @param node + * @returns Whether the node is an Identifier with name `undefined`. */ -export function isUndefinedIdentifier(node) { +export function isUndefinedIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'undefined'; } /** * Check whether a variable's definition is from a function parameter. - * @param {Node} node - the Identifier node for the variable. - * @param {ScopeManager} scopeManager - * @returns {boolean} whether the variable comes from a function parameter + * @param node - the Identifier node for the variable. + * @param scopeManager + * @returns whether the variable comes from a function parameter */ -export function isVariableFromParameter(node, scopeManager) { +export function isVariableFromParameter( + node: Node, + scopeManager: Scope.ScopeManager, +): boolean { const variable = findVariable( scopeManager.acquire(node) || scopeManager.globalScope, node, diff --git a/types/estree.d.ts b/types/estree.d.ts index 2e9a6058..bf1fbfeb 100644 --- a/types/estree.d.ts +++ b/types/estree.d.ts @@ -1,5 +1,25 @@ +import { Program as EstreeProgram } from 'estree'; + declare module 'estree' { interface BaseNode { - parent?: Node; + parent: Node; + } + + interface TSAsExpression extends BaseExpression { + type: 'TSAsExpression'; + expression: Expression | Identifier; + } + + interface TSExportAssignment extends BaseNode { + type: 'TSExportAssignment'; + expression: Expression; + } + + interface ExpressionMap { + TSAsExpression: TSAsExpression; + } + + interface NodeMap { + TSExportAssignment: TSExportAssignment; } } From 7e346f5c41cc64a046658d89d7fb38b1879424e0 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 21 Jun 2025 18:13:02 -0500 Subject: [PATCH 06/82] migrate consistent-output migrate consistent-output --- lib/rules/consistent-output.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/rules/consistent-output.ts b/lib/rules/consistent-output.ts index 3c12e0ee..65abc332 100644 --- a/lib/rules/consistent-output.ts +++ b/lib/rules/consistent-output.ts @@ -2,15 +2,17 @@ * @fileoverview Enforce consistent use of `output` assertions in rule tests * @author Teddy Katz */ +import type { Rule } from 'eslint'; -import { getKeyName, getTestInfo } from '../utils.js'; +import { getKeyName, getTestInfo } from '../utils'; + +const keyNameMapper = (property: Parameters[0]) => + getKeyName(property); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -20,7 +22,7 @@ const rule = { recommended: false, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/consistent-output.md', }, - fixable: null, // or "code" or "whitespace" + fixable: undefined, // or "code" or "whitespace" schema: [ { type: 'string', @@ -37,20 +39,17 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- const always = context.options[0] && context.options[0] === 'always'; return { Program(ast) { getTestInfo(context, ast).forEach((testRun) => { const readableCases = testRun.invalid.filter( - (testCase) => testCase.type === 'ObjectExpression', + (testCase) => testCase?.type === 'ObjectExpression', ); const casesWithoutOutput = readableCases.filter( (testCase) => - !testCase.properties.map(getKeyName).includes('output'), + !testCase.properties.map(keyNameMapper).includes('output'), ); if ( From 98cf8a3f4db1ea8e1184286f106b0aacd2e7132d Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 22 Jun 2025 11:38:04 -0500 Subject: [PATCH 07/82] migrate meta-property-ordering --- lib/rules/meta-property-ordering.ts | 32 ++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/rules/meta-property-ordering.ts b/lib/rules/meta-property-ordering.ts index e66d9320..88125d70 100644 --- a/lib/rules/meta-property-ordering.ts +++ b/lib/rules/meta-property-ordering.ts @@ -1,6 +1,7 @@ /** * @fileoverview Enforces the order of meta properties */ +import type { Rule } from 'eslint'; import { getKeyName, getRuleInfo } from '../utils.js'; @@ -16,12 +17,13 @@ const defaultOrder = [ 'messages', ]; +const keyNameMapper = (property: Parameters[0]) => + getKeyName(property); + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -52,24 +54,29 @@ const rule = { return {}; } - const order = context.options[0] || defaultOrder; + const order: string[] = context.options[0] || defaultOrder; - const orderMap = new Map(order.map((name, i) => [name, i])); + const orderMap = new Map( + order.map((name, i) => [name, i]), + ); return { Program() { - if (!ruleInfo.meta || ruleInfo.meta.properties.length < 2) { + if ( + !ruleInfo.meta || + ruleInfo.meta.type !== 'ObjectExpression' || + ruleInfo.meta.properties.length < 2 + ) { return; } const props = ruleInfo.meta.properties; - let last; + let last = Number.NEGATIVE_INFINITY; const violatingProps = props.filter((prop) => { - const curr = orderMap.has(getKeyName(prop)) - ? orderMap.get(getKeyName(prop)) - : Number.POSITIVE_INFINITY; + const curr = + orderMap.get(getKeyName(prop)) ?? Number.POSITIVE_INFINITY; return last > (last = curr); }); @@ -80,7 +87,8 @@ const rule = { const knownProps = props .filter((prop) => orderMap.has(getKeyName(prop))) .sort( - (a, b) => orderMap.get(getKeyName(a)) - orderMap.get(getKeyName(b)), + (a, b) => + orderMap.get(getKeyName(a))! - orderMap.get(getKeyName(b))!, ); const unknownProps = props.filter( (prop) => !orderMap.has(getKeyName(prop)), @@ -91,7 +99,7 @@ const rule = { node: violatingProp, messageId: 'inconsistentOrder', data: { - order: knownProps.map(getKeyName).join(', '), + order: knownProps.map(keyNameMapper).join(', '), }, fix(fixer) { const expectedProps = [...knownProps, ...unknownProps]; From ce6e93bc2d5db615b90cefbea2e28e622e64c1da Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 22 Jun 2025 12:00:24 -0500 Subject: [PATCH 08/82] migrate no-deprecated-context-methods --- lib/rules/no-deprecated-context-methods.ts | 35 +++++++++++----------- types/estree.d.ts | 1 + 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/rules/no-deprecated-context-methods.ts b/lib/rules/no-deprecated-context-methods.ts index 490dd096..74ff39c1 100644 --- a/lib/rules/no-deprecated-context-methods.ts +++ b/lib/rules/no-deprecated-context-methods.ts @@ -3,7 +3,9 @@ * @author Teddy Katz */ -import { getContextIdentifiers } from '../utils.js'; +import type { Rule } from 'eslint'; +import { getContextIdentifiers } from '../utils'; +import type { Identifier, MemberExpression } from 'estree'; const DEPRECATED_PASSTHROUGHS = { getSource: 'getText', @@ -26,14 +28,12 @@ const DEPRECATED_PASSTHROUGHS = { getTokensAfter: 'getTokensAfter', getTokensBefore: 'getTokensBefore', getTokensBetween: 'getTokensBetween', -}; +} satisfies Record; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -66,30 +66,29 @@ const rule = { contextId.parent.type === 'MemberExpression' && contextId === contextId.parent.object && contextId.parent.property.type === 'Identifier' && - Object.prototype.hasOwnProperty.call( - DEPRECATED_PASSTHROUGHS, - contextId.parent.property.name, - ), + contextId.parent.property.name in DEPRECATED_PASSTHROUGHS, ) - .forEach((contextId) => - context.report({ + .forEach((contextId) => { + const parentPropertyName = ( + (contextId.parent as MemberExpression).property as Identifier + ).name as keyof typeof DEPRECATED_PASSTHROUGHS; + return context.report({ node: contextId.parent, messageId: 'newFormat', data: { contextName: contextId.name, - original: contextId.parent.property.name, - replacement: - DEPRECATED_PASSTHROUGHS[contextId.parent.property.name], + original: parentPropertyName, + replacement: DEPRECATED_PASSTHROUGHS[parentPropertyName], }, fix: (fixer) => [ fixer.insertTextAfter(contextId, '.getSourceCode()'), fixer.replaceText( - contextId.parent.property, - DEPRECATED_PASSTHROUGHS[contextId.parent.property.name], + (contextId.parent as MemberExpression).property, + DEPRECATED_PASSTHROUGHS[parentPropertyName], ), ], - }), - ); + }); + }); }, }; }, diff --git a/types/estree.d.ts b/types/estree.d.ts index bf1fbfeb..6d640f30 100644 --- a/types/estree.d.ts +++ b/types/estree.d.ts @@ -20,6 +20,7 @@ declare module 'estree' { } interface NodeMap { + TSAsExpression: TSAsExpression; TSExportAssignment: TSExportAssignment; } } From baedd5b4a96dcb9dcc791572fdd231ae70335351 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 22 Jun 2025 16:59:00 -0500 Subject: [PATCH 09/82] migrate no-deprecated-report-api --- lib/rules/no-deprecated-report-api.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/rules/no-deprecated-report-api.ts b/lib/rules/no-deprecated-report-api.ts index cae105ac..4483c698 100644 --- a/lib/rules/no-deprecated-report-api.ts +++ b/lib/rules/no-deprecated-report-api.ts @@ -2,15 +2,15 @@ * @fileoverview Disallow the version of `context.report()` with multiple arguments * @author Teddy Katz */ +import type { Rule } from 'eslint'; +import type { Identifier } from 'estree'; import { getContextIdentifiers, getReportInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -29,7 +29,7 @@ const rule = { create(context) { const sourceCode = context.sourceCode; - let contextIdentifiers; + let contextIdentifiers: Set = new Set(); // ---------------------------------------------------------------------- // Public @@ -45,7 +45,7 @@ const rule = { CallExpression(node) { if ( node.callee.type === 'MemberExpression' && - contextIdentifiers.has(node.callee.object) && + contextIdentifiers.has(node.callee.object as Identifier) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' && (node.arguments.length > 1 || @@ -56,8 +56,10 @@ const rule = { node: node.callee.property, messageId: 'useNewAPI', fix(fixer) { - const openingParen = sourceCode.getTokenBefore(node.arguments[0]); - const closingParen = sourceCode.getLastToken(node); + const openingParen = sourceCode.getTokenBefore( + node.arguments[0], + )!; + const closingParen = sourceCode.getLastToken(node)!; const reportInfo = getReportInfo(node, context); if (!reportInfo) { @@ -68,7 +70,8 @@ const rule = { [openingParen.range[1], closingParen.range[0]], `{${Object.keys(reportInfo) .map( - (key) => `${key}: ${sourceCode.getText(reportInfo[key])}`, + (key) => + `${key}: ${sourceCode.getText(reportInfo[key as keyof typeof reportInfo])}`, ) .join(', ')}}`, ); From 5cee9cd998a26790e70f6e3938631f2e09fa483e Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 22 Jun 2025 17:07:54 -0500 Subject: [PATCH 10/82] migrate no-identical-tests --- lib/rules/no-identical-tests.ts | 66 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/lib/rules/no-identical-tests.ts b/lib/rules/no-identical-tests.ts index 5c192e01..cbc32080 100644 --- a/lib/rules/no-identical-tests.ts +++ b/lib/rules/no-identical-tests.ts @@ -3,14 +3,15 @@ * @author 薛定谔的猫 */ -import { getTestInfo } from '../utils.js'; +import type { Rule } from 'eslint'; +import type { Expression, SpreadElement } from 'estree'; + +import { getTestInfo } from '../utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -27,20 +28,13 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- const sourceCode = context.sourceCode; - // ---------------------------------------------------------------------- - // Helpers - // ---------------------------------------------------------------------- /** * Create a unique cache key - * @param {object} test - * @returns {string} + * @param test */ - function toKey(test) { + function toKey(test: Expression | SpreadElement): string { if (test.type !== 'ObjectExpression') { return JSON.stringify([test.type, sourceCode.getText(test)]); } @@ -55,28 +49,30 @@ const rule = { getTestInfo(context, ast).forEach((testRun) => { [testRun.valid, testRun.invalid].forEach((tests) => { const cache = new Set(); - tests.forEach((test) => { - const key = toKey(test); - if (cache.has(key)) { - context.report({ - node: test, - messageId: 'identical', - fix(fixer) { - const start = sourceCode.getTokenBefore(test); - const end = sourceCode.getTokenAfter(test); - return fixer.removeRange( - // should remove test's trailing comma - [ - start.range[1], - end.value === ',' ? end.range[1] : test.range[1], - ], - ); - }, - }); - } else { - cache.add(key); - } - }); + tests + .filter((test) => !!test) + .forEach((test) => { + const key = toKey(test); + if (cache.has(key)) { + context.report({ + node: test, + messageId: 'identical', + fix(fixer) { + const start = sourceCode.getTokenBefore(test)!; + const end = sourceCode.getTokenAfter(test)!; + return fixer.removeRange( + // should remove test's trailing comma + [ + start.range[1], + end.value === ',' ? end.range[1] : test.range![1], + ], + ); + }, + }); + } else { + cache.add(key); + } + }); }); }); }, From cb1d41da0784a6974f90771c0842aa54c96de799 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 22 Jun 2025 17:09:32 -0500 Subject: [PATCH 11/82] migrate no-meta-replaced-by --- lib/rules/no-meta-replaced-by.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rules/no-meta-replaced-by.ts b/lib/rules/no-meta-replaced-by.ts index 8ceb6f41..6b595486 100644 --- a/lib/rules/no-meta-replaced-by.ts +++ b/lib/rules/no-meta-replaced-by.ts @@ -2,14 +2,14 @@ * @fileoverview Disallows the usage of `meta.replacedBy` property */ -import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; +import type { Rule } from 'eslint'; + +import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { From 562fa66d144343ecf020e5829f8317a3ef09ee96 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 22 Jun 2025 18:01:03 -0500 Subject: [PATCH 12/82] migrate no-meta-schema-default --- lib/rules/no-meta-schema-default.ts | 35 ++++++++++++++++------------- lib/utils.ts | 23 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/rules/no-meta-schema-default.ts b/lib/rules/no-meta-schema-default.ts index 89aafc0b..59108030 100644 --- a/lib/rules/no-meta-schema-default.ts +++ b/lib/rules/no-meta-schema-default.ts @@ -1,17 +1,17 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; import { getMetaSchemaNode, getMetaSchemaNodeProperty, getRuleInfo, -} from '../utils.js'; +} from '../utils'; +import type { Expression, SpreadElement } from 'estree'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -31,7 +31,7 @@ const rule = { const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; const ruleInfo = getRuleInfo(sourceCode); - if (!ruleInfo) { + if (!ruleInfo || !ruleInfo.meta) { return {}; } @@ -43,31 +43,32 @@ const rule = { const schemaProperty = getMetaSchemaNodeProperty(schemaNode, scopeManager); if (schemaProperty?.type === 'ObjectExpression') { - checkSchemaElement(schemaProperty, true); + checkSchemaElement(schemaProperty); } else if (schemaProperty?.type === 'ArrayExpression') { for (const element of schemaProperty.elements) { - checkSchemaElement(element, true); + checkSchemaElement(element); } } return {}; - function checkSchemaElement(node) { - if (node.type !== 'ObjectExpression') { + function checkSchemaElement(node: Expression | SpreadElement | null) { + if (node?.type !== 'ObjectExpression') { return; } - for (const { type, key, value } of node.properties) { - if (type !== 'Property') { + for (const property of node.properties) { + if (property.type !== 'Property') { continue; } + const { key, value } = property; const staticKey = key.type === 'Identifier' ? { value: key.name } : getStaticValue(key); if (!staticKey?.value) { continue; } - switch (key.name ?? key.value) { + switch ('name' in key ? key.name : 'value' in key ? key.value : '') { case 'allOf': case 'anyOf': case 'oneOf': { @@ -81,9 +82,12 @@ const rule = { } case 'properties': { - if (Array.isArray(value.properties)) { + if ('properties' in value && Array.isArray(value.properties)) { for (const property of value.properties) { - if (property.value?.type === 'ObjectExpression') { + if ( + 'value' in property && + property.value.type === 'ObjectExpression' + ) { checkSchemaElement(property.value); } } @@ -93,8 +97,7 @@ const rule = { } case 'elements': { - checkSchemaElement(value); - + checkSchemaElement(value as Expression | SpreadElement); break; } diff --git a/lib/utils.ts b/lib/utils.ts index 33b926e6..1fe6ae24 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -3,6 +3,7 @@ import type { Rule, Scope } from 'eslint'; import estraverse from 'estraverse'; import type { ArrowFunctionExpression, + AssignmentProperty, CallExpression, Directive, Expression, @@ -20,6 +21,7 @@ import type { Statement, Super, TSExportAssignment, + VariableDeclarator, } from 'estree'; import type { PartialRuleInfo, RuleInfo, TestInfo } from './types'; @@ -982,19 +984,20 @@ export function getMessageIdNodeById( ); } +const isProperty = (node: Node): node is Property => node.type === 'Property'; export function getMetaSchemaNode( - metaNode, + metaNode: Node, scopeManager: Scope.ScopeManager, -): Node | undefined { - return evaluateObjectProperties(metaNode, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p) === 'schema', - ); +): AssignmentProperty | Property | undefined { + return evaluateObjectProperties(metaNode, scopeManager) + .filter(isProperty) + .find((p) => getKeyName(p) === 'schema'); } export function getMetaSchemaNodeProperty( - schemaNode, + schemaNode: AssignmentProperty | Property, scopeManager: Scope.ScopeManager, -) { +): Node | null { if (!schemaNode) { return null; } @@ -1002,7 +1005,7 @@ export function getMetaSchemaNodeProperty( let { value } = schemaNode; if (value.type === 'Identifier' && value.name !== 'undefined') { const variable = findVariable( - scopeManager.acquire(value) || scopeManager.globalScope, + scopeManager.acquire(value) || scopeManager.globalScope!, value, ); @@ -1015,10 +1018,10 @@ export function getMetaSchemaNodeProperty( variable.defs[0].node.type !== 'VariableDeclarator' || !variable.defs[0].node.init ) { - return; + return null; } - value = variable.defs[0].node.init; + value = (variable.defs[0].node as VariableDeclarator).init! as Expression; } return value; From 9c5e68ee01e9d42ca6f7b03ff7c4ad52243f3275 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 23 Jun 2025 16:51:10 -0500 Subject: [PATCH 13/82] migrate no-missing-message-ids --- lib/rules/no-deprecated-report-api.ts | 6 +++--- lib/rules/no-missing-message-ids.ts | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/rules/no-deprecated-report-api.ts b/lib/rules/no-deprecated-report-api.ts index 4483c698..91a3b622 100644 --- a/lib/rules/no-deprecated-report-api.ts +++ b/lib/rules/no-deprecated-report-api.ts @@ -3,7 +3,7 @@ * @author Teddy Katz */ import type { Rule } from 'eslint'; -import type { Identifier } from 'estree'; +import type { Node } from 'estree'; import { getContextIdentifiers, getReportInfo } from '../utils.js'; @@ -29,7 +29,7 @@ const rule: Rule.RuleModule = { create(context) { const sourceCode = context.sourceCode; - let contextIdentifiers: Set = new Set(); + let contextIdentifiers: Set; // ---------------------------------------------------------------------- // Public @@ -45,7 +45,7 @@ const rule: Rule.RuleModule = { CallExpression(node) { if ( node.callee.type === 'MemberExpression' && - contextIdentifiers.has(node.callee.object as Identifier) && + contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' && (node.arguments.length > 1 || diff --git a/lib/rules/no-missing-message-ids.ts b/lib/rules/no-missing-message-ids.ts index 81dfdf0a..5dd87754 100644 --- a/lib/rules/no-missing-message-ids.ts +++ b/lib/rules/no-missing-message-ids.ts @@ -1,19 +1,20 @@ +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; + import { collectReportViolationAndSuggestionData, findPossibleVariableValues, getContextIdentifiers, - getMessagesNode, getMessageIdNodeById, + getMessagesNode, getReportInfo, getRuleInfo, -} from '../utils.js'; +} from '../utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -23,7 +24,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-message-ids.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { missingMessage: @@ -41,7 +42,7 @@ const rule = { const messagesNode = getMessagesNode(ruleInfo, scopeManager); - let contextIdentifiers; + let contextIdentifiers: Set; if (!messagesNode || messagesNode.type !== 'ObjectExpression') { // If we can't find `meta.messages`, disable the rule. @@ -54,7 +55,7 @@ const rule = { }, CallExpression(node) { - const scope = sourceCode.getScope(node); + const scope = context.sourceCode.getScope(node); // Check for messageId properties used in known calls to context.report(); if ( node.callee.type === 'MemberExpression' && From 3694a1e0b2e3dfd7279c4a9a6f620f0b2dde2861 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 23 Jun 2025 20:04:11 -0500 Subject: [PATCH 14/82] migrate no-missing-placeholders --- lib/rules/no-missing-message-ids.ts | 13 ++- lib/rules/no-missing-placeholders.ts | 39 +++---- lib/types.ts | 42 ++++++++ lib/utils.ts | 148 +++++++++++++++------------ 4 files changed, 154 insertions(+), 88 deletions(-) diff --git a/lib/rules/no-missing-message-ids.ts b/lib/rules/no-missing-message-ids.ts index 5dd87754..f8a145a4 100644 --- a/lib/rules/no-missing-message-ids.ts +++ b/lib/rules/no-missing-message-ids.ts @@ -1,5 +1,5 @@ import type { Rule } from 'eslint'; -import type { Node } from 'estree'; +import type { Identifier, Node } from 'estree'; import { collectReportViolationAndSuggestionData, @@ -70,13 +70,16 @@ const rule: Rule.RuleModule = { const reportMessagesAndDataArray = collectReportViolationAndSuggestionData(reportInfo); - for (const { messageId } of reportMessagesAndDataArray.filter( - (obj) => obj.messageId, - )) { + for (const messageId of reportMessagesAndDataArray + .map((obj) => obj.messageId) + .filter((messageId) => !!messageId)) { const values = messageId.type === 'Literal' ? [messageId] - : findPossibleVariableValues(messageId, scopeManager); + : findPossibleVariableValues( + messageId as Identifier, + scopeManager, + ); // Look for any possible string values we found for this messageId. values.forEach((val) => { diff --git a/lib/rules/no-missing-placeholders.ts b/lib/rules/no-missing-placeholders.ts index 522ed207..474e64d2 100644 --- a/lib/rules/no-missing-placeholders.ts +++ b/lib/rules/no-missing-placeholders.ts @@ -3,23 +3,23 @@ * @author Teddy Katz */ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; import { collectReportViolationAndSuggestionData, + getContextIdentifiers, getKeyName, + getMessageIdNodeById, + getMessagesNode, getReportInfo, getRuleInfo, - getMessagesNode, - getMessageIdNodeById, - getContextIdentifiers, -} from '../utils.js'; +} from '../utils'; +import type { Node } from 'estree'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -28,7 +28,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-placeholders.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { placeholderDoesNotExist: @@ -40,7 +40,7 @@ const rule = { const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - let contextIdentifiers; + let contextIdentifiers: Set; const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { @@ -96,9 +96,9 @@ const rule = { messageId, data, } of reportMessagesAndDataArray.filter((obj) => obj.message)) { - const messageStaticValue = getStaticValue(message, scope); + const messageStaticValue = getStaticValue(message!, scope); if ( - ((message.type === 'Literal' && + ((message?.type === 'Literal' && typeof message.value === 'string') || (messageStaticValue && typeof messageStaticValue.value === 'string')) && @@ -107,20 +107,21 @@ const rule = { // Same regex as the one ESLint uses // https://github.com/eslint/eslint/blob/e5446449d93668ccbdb79d78cc69f165ce4fde07/lib/eslint.js#L990 const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g; - let match; + let match: RegExpExecArray | null; - while ( - (match = PLACEHOLDER_MATCHER.exec( - message.value || messageStaticValue.value, - )) - ) { + const messageText: string = + // @ts-expect-error + message.value || messageStaticValue.value; + while ((match = PLACEHOLDER_MATCHER.exec(messageText))) { const matchingProperty = data && - data.properties.find((prop) => getKeyName(prop) === match[1]); + data.properties.find( + (prop) => getKeyName(prop) === match![1], + ); if (!matchingProperty) { context.report({ - node: data || messageId || message, + node: (data || messageId || message) as Node, messageId: 'placeholderDoesNotExist', data: { missingKey: match[1] }, }); diff --git a/lib/types.ts b/lib/types.ts index be945a93..552806cb 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,10 +1,15 @@ import type { Rule } from 'eslint'; import type { + ArrayPattern, ArrowFunctionExpression, + AssignmentPattern, Expression, FunctionDeclaration, FunctionExpression, Node, + ObjectPattern, + Property, + RestElement, SpreadElement, } from 'estree'; @@ -32,3 +37,40 @@ export type TestInfo = { invalid: (Expression | SpreadElement | null)[]; valid: (Expression | SpreadElement | null)[]; }; + +export type ViolationAndSuppressionData = { + messageId?: + | Expression + | SpreadElement + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern; + message?: + | Expression + | SpreadElement + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern; + data?: + | Expression + | SpreadElement + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern; + fix?: + | Expression + | SpreadElement + | ObjectPattern + | ArrayPattern + | RestElement + | AssignmentPattern; +}; + +export type MetaDocsProperty = { + docsNode: Property | undefined; + metaNode: Node | undefined; + metaPropertyNode: Property | undefined; +}; diff --git a/lib/utils.ts b/lib/utils.ts index 1fe6ae24..6db43226 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,5 @@ import { getStaticValue, findVariable } from '@eslint-community/eslint-utils'; -import type { Rule, Scope } from 'eslint'; +import type { Rule, Scope, SourceCode } from 'eslint'; import estraverse from 'estraverse'; import type { ArrowFunctionExpression, @@ -24,7 +24,13 @@ import type { VariableDeclarator, } from 'estree'; -import type { PartialRuleInfo, RuleInfo, TestInfo } from './types'; +import type { + MetaDocsProperty, + PartialRuleInfo, + RuleInfo, + TestInfo, + ViolationAndSuppressionData, +} from './types'; const functionTypes = new Set([ 'FunctionExpression', @@ -81,11 +87,7 @@ function collectInterestingProperties( return properties.reduce>( (parsedProps, prop) => { const keyValue = getKeyName(prop); - if ( - prop.type === 'Property' && - keyValue && - interestingKeys.has(keyValue as T) - ) { + if (isProperty(prop) && keyValue && interestingKeys.has(keyValue as T)) { // In TypeScript, unwrap any usage of `{} as const`. parsedProps[keyValue] = prop.value.type === 'TSAsExpression' @@ -338,7 +340,7 @@ function findObjectPropertyValueByKeyName( ): Property['value'] | undefined { const property = obj.properties.find( (prop) => - prop.type === 'Property' && + isProperty(prop) && prop.key.type === 'Identifier' && prop.key.name === keyName, ) as Property | undefined; @@ -378,12 +380,12 @@ function findVariableValue( * @param node * @returns the list of elements */ -function collectArrayElements(node: Node): (Node | null)[] { +function collectArrayElements(node: Node): Node[] { if (!node) { return []; } if (node.type === 'ArrayExpression') { - return node.elements; + return node.elements.filter((element) => element !== null); } if (node.type === 'ConditionalExpression') { return [ @@ -675,7 +677,13 @@ export function getTestInfo( * Gets information on a report, given the ASTNode of context.report(). * @param node The ASTNode of context.report() */ -export function getReportInfo(node: CallExpression, context: Rule.RuleContext) { +export function getReportInfo( + node: CallExpression, + context: Rule.RuleContext, +): + | Record + | Record + | null { const reportArgs = node.arguments; // If there is exactly one argument, the API expects an object. @@ -689,19 +697,22 @@ export function getReportInfo(node: CallExpression, context: Rule.RuleContext) { if (reportArgs.length === 1) { if (reportArgs[0].type === 'ObjectExpression') { - return reportArgs[0].properties.reduce((reportInfo, property) => { - const propName = getKeyName(property); + return reportArgs[0].properties.reduce>( + (reportInfo, property) => { + const propName = getKeyName(property); - if (propName !== null) { - return Object.assign(reportInfo, { [propName]: property.value }); - } - return reportInfo; - }, {}); + if (propName !== null && 'value' in property) { + return Object.assign(reportInfo, { [propName]: property.value }); + } + return reportInfo; + }, + {}, + ); } return null; } - let keys; + let keys: string[]; const sourceCode = context.sourceCode; const scope = sourceCode.getScope(node); const secondArgStaticValue = getStaticValue(reportArgs[1], scope); @@ -735,13 +746,13 @@ export function getReportInfo(node: CallExpression, context: Rule.RuleContext) { /** * Gets a set of all `sourceCode` identifiers. * @param scopeManager - * @param {ASTNode} ast The AST of the file. This must have `parent` properties. - * @returns {Set} A set of all identifiers referring to the `SourceCode` object. + * @param ast The AST of the file. This must have `parent` properties. + * @returns A set of all identifiers referring to the `SourceCode` object. */ export function getSourceCodeIdentifiers( scopeManager: Scope.ScopeManager, - ast, -) { + ast: Program, +): Set { return new Set( [...getContextIdentifiers(scopeManager, ast)] .filter( @@ -767,17 +778,21 @@ export function getSourceCodeIdentifiers( /** * Insert a given property into a given object literal. - * @param {SourceCodeFixer} fixer The fixer. - * @param {Node} node The ObjectExpression node to insert a property. - * @param {string} propertyText The property code to insert. - * @returns {void} + * @param fixer The fixer. + * @param node The ObjectExpression node to insert a property. + * @param propertyText The property code to insert. */ -export function insertProperty(fixer, node, propertyText, sourceCode) { +export function insertProperty( + fixer: Rule.RuleFixer, + node: ObjectExpression, + propertyText: string, + sourceCode: SourceCode, +): Rule.Fix { if (node.properties.length === 0) { return fixer.replaceText(node, `{\n${propertyText}\n}`); } return fixer.insertTextAfter( - sourceCode.getLastToken(node.properties.at(-1)), + sourceCode.getLastToken(node.properties.at(-1)!)!, `,\n${propertyText}`, ); } @@ -788,8 +803,8 @@ export function insertProperty(fixer, node, propertyText, sourceCode) { * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[] */ export function collectReportViolationAndSuggestionData( - reportInfo: ReturnType, -) { + reportInfo: NonNullable>, +): ViolationAndSuppressionData[] { return [ // Violation message { @@ -854,12 +869,12 @@ export function isSuggestionFixerFunction( return ( (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && - parent.type === 'Property' && + isProperty(parent) && parent.key.type === 'Identifier' && parent.key.name === 'fix' && parent.parent.type === 'ObjectExpression' && parent.parent.parent.type === 'ArrayExpression' && - parent.parent.parent.parent.type === 'Property' && + isProperty(parent.parent.parent.parent) && parent.parent.parent.parent.key.type === 'Identifier' && parent.parent.parent.parent.key.name === 'suggest' && parent.parent.parent.parent.parent.type === 'ObjectExpression' && @@ -882,7 +897,7 @@ export function isSuggestionFixerFunction( * @returns the list of all properties that could be found */ export function evaluateObjectProperties( - objectNode: Node, + objectNode: Node | undefined, scopeManager: Scope.ScopeManager, ): Node[] { if (!objectNode || objectNode.type !== 'ObjectExpression') { @@ -903,46 +918,51 @@ export function evaluateObjectProperties( export function getMetaDocsProperty( propertyName: string, - ruleInfo, + ruleInfo: RuleInfo, scopeManager: Scope.ScopeManager, -) { - const metaNode = ruleInfo.meta; +): MetaDocsProperty { + const metaNode = ruleInfo.meta ?? undefined; - const docsNode = evaluateObjectProperties(metaNode, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p) === 'docs', - ); + const docsNode = evaluateObjectProperties(metaNode, scopeManager) + .filter(isProperty) + .find((p) => getKeyName(p) === 'docs'); const metaPropertyNode = evaluateObjectProperties( docsNode?.value, scopeManager, - ).find((p) => p.type === 'Property' && getKeyName(p) === propertyName); + ) + .filter(isProperty) + .find((p) => getKeyName(p) === propertyName); return { docsNode, metaNode, metaPropertyNode }; } /** * Get the `meta.messages` node from a rule. - * @param {RuleInfo} ruleInfo + * @param ruleInfo * @param scopeManager */ export function getMessagesNode( - ruleInfo, + ruleInfo: RuleInfo | null, scopeManager: Scope.ScopeManager, -): Node | undefined { +): ObjectExpression | undefined { if (!ruleInfo) { return; } - const metaNode = ruleInfo.meta; - const messagesNode = evaluateObjectProperties(metaNode, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p) === 'messages', - ); + const metaNode = ruleInfo.meta ?? undefined; + const messagesNode = evaluateObjectProperties(metaNode, scopeManager) + .filter(isProperty) + .find((p) => getKeyName(p) === 'messages'); if (messagesNode) { if (messagesNode.value.type === 'ObjectExpression') { return messagesNode.value; } - const value = findVariableValue(messagesNode.value, scopeManager); + const value = findVariableValue( + messagesNode.value as Identifier, + scopeManager, + ); if (value && value.type === 'ObjectExpression') { return value; } @@ -951,13 +971,13 @@ export function getMessagesNode( /** * Get the list of messageId properties from `meta.messages` for a rule. - * @param {RuleInfo} ruleInfo + * @param ruleInfo * @param scopeManager */ export function getMessageIdNodes( - ruleInfo, + ruleInfo: RuleInfo, scopeManager: Scope.ScopeManager, -): Node | undefined { +): Node[] | undefined { const messagesNode = getMessagesNode(ruleInfo, scopeManager); return messagesNode && messagesNode.type === 'ObjectExpression' @@ -965,30 +985,30 @@ export function getMessageIdNodes( : undefined; } +const isProperty = (node: Node): node is Property => node.type === 'Property'; /** * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`. * @param messageId - the messageId to check for - * @param {RuleInfo} ruleInfo + * @param ruleInfo * @param scopeManager * @param scope * @returns The matching messageId property from `meta.messages`. */ export function getMessageIdNodeById( messageId: string, - ruleInfo, + ruleInfo: RuleInfo, scopeManager: Scope.ScopeManager, scope: Scope.Scope, -): Node | undefined { - return getMessageIdNodes(ruleInfo, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p, scope) === messageId, - ); +): Property | undefined { + return getMessageIdNodes(ruleInfo, scopeManager) + ?.filter(isProperty) + .find((p) => getKeyName(p, scope) === messageId); } -const isProperty = (node: Node): node is Property => node.type === 'Property'; export function getMetaSchemaNode( metaNode: Node, scopeManager: Scope.ScopeManager, -): AssignmentProperty | Property | undefined { +): Property | undefined { return evaluateObjectProperties(metaNode, scopeManager) .filter(isProperty) .find((p) => getKeyName(p) === 'schema'); @@ -1034,11 +1054,11 @@ export function getMetaSchemaNodeProperty( * @returns the values that the given variable could be initialized to. */ export function findPossibleVariableValues( - node: Node, + node: Identifier, scopeManager: Scope.ScopeManager, ): Node[] { const variable = findVariable( - scopeManager.acquire(node) || scopeManager.globalScope, + scopeManager.acquire(node) || scopeManager.globalScope!, node, ); return ((variable && variable.references) || []).flatMap((ref) => { @@ -1070,11 +1090,11 @@ export function isUndefinedIdentifier(node: Node): boolean { * @returns whether the variable comes from a function parameter */ export function isVariableFromParameter( - node: Node, + node: Identifier, scopeManager: Scope.ScopeManager, ): boolean { const variable = findVariable( - scopeManager.acquire(node) || scopeManager.globalScope, + scopeManager.acquire(node) || scopeManager.globalScope!, node, ); From 02314b4ae81fd3ca3449c3ecbd93a31871422ac5 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 23 Jun 2025 20:15:16 -0500 Subject: [PATCH 15/82] migrate no-only-tests --- lib/rules/no-only-tests.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-only-tests.ts b/lib/rules/no-only-tests.ts index 87246d94..ee4c3e9a 100644 --- a/lib/rules/no-only-tests.ts +++ b/lib/rules/no-only-tests.ts @@ -3,11 +3,11 @@ import { isOpeningBraceToken, isClosingBraceToken, } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; -import { getTestInfo } from '../utils.js'; +import { getTestInfo } from '../utils'; -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -30,7 +30,7 @@ const rule = { Program(ast) { for (const testRun of getTestInfo(context, ast)) { for (const test of [...testRun.valid, ...testRun.invalid]) { - if (test.type === 'ObjectExpression') { + if (test?.type === 'ObjectExpression') { // Test case object: { code: 'const x = 123;', ... } const onlyProperty = test.properties.find( @@ -53,9 +53,9 @@ const rule = { const sourceCode = context.sourceCode; const tokenBefore = - sourceCode.getTokenBefore(onlyProperty); + sourceCode.getTokenBefore(onlyProperty)!; const tokenAfter = - sourceCode.getTokenAfter(onlyProperty); + sourceCode.getTokenAfter(onlyProperty)!; if ( (isCommaToken(tokenBefore) && isCommaToken(tokenAfter)) || // In middle of properties @@ -79,7 +79,7 @@ const rule = { }); } } else if ( - test.type === 'CallExpression' && + test?.type === 'CallExpression' && test.callee.type === 'MemberExpression' && test.callee.object.type === 'Identifier' && test.callee.object.name === 'RuleTester' && From d86155c815978cd31cfebb6eff2308f56ae92e99 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 23 Jun 2025 20:31:34 -0500 Subject: [PATCH 16/82] migrate no-property-in-node --- lib/rules/no-property-in-node.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/rules/no-property-in-node.ts b/lib/rules/no-property-in-node.ts index 86f2fa41..0384af7e 100644 --- a/lib/rules/no-property-in-node.ts +++ b/lib/rules/no-property-in-node.ts @@ -1,3 +1,6 @@ +import type { Rule } from 'eslint'; +import type { Type } from 'typescript'; + const defaultTypedNodeSourceFileTesters = [ /@types[/\\]estree[/\\]index\.d\.ts/, /@typescript-eslint[/\\]types[/\\]dist[/\\]generated[/\\]ast-spec\.d\.ts/, @@ -23,11 +26,14 @@ const defaultTypedNodeSourceFileTesters = [ * } * ``` * - * @param {import('typescript').Type} type - * @param {RegExp[]} typedNodeSourceFileTesters + * @param type + * @param typedNodeSourceFileTesters * @returns Whether the type seems to include a known ESTree or TSESTree AST node. */ -function isAstNodeType(type, typedNodeSourceFileTesters) { +function isAstNodeType( + type: Type & { types?: Type[] }, + typedNodeSourceFileTesters: RegExp[], +): boolean { return (type.types || [type]) .filter((typePart) => typePart.getProperty('type')) .flatMap( @@ -42,8 +48,7 @@ function isAstNodeType(type, typedNodeSourceFileTesters) { }); } -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -51,6 +56,7 @@ const rule = { 'disallow using `in` to narrow node types instead of looking at properties', category: 'Rules', recommended: false, + // @ts-expect-error -- need to augment the type of `Rule.RuleMetaData` to include `requiresTypeChecking` requiresTypeChecking: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-property-in-node.md', }, @@ -75,11 +81,14 @@ const rule = { }, create(context) { + const additionalNodeTypeFiles: string[] = + context.options[0]?.additionalNodeTypeFiles ?? []; + const typedNodeSourceFileTesters = [ ...defaultTypedNodeSourceFileTesters, - ...(context.options[0]?.additionalNodeTypeFiles?.map( - (filePath) => new RegExp(filePath), - ) ?? []), + ...additionalNodeTypeFiles.map( + (filePath: string) => new RegExp(filePath), + ), ]; return { From 326cfce79780debc04baac263995f68e0422b62d Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 30 Jun 2025 14:29:11 -0500 Subject: [PATCH 17/82] migrate no-unused-message-ids --- lib/rules/no-unused-message-ids.ts | 45 +++++++++++++++++++----------- lib/utils.ts | 9 ++++-- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/rules/no-unused-message-ids.ts b/lib/rules/no-unused-message-ids.ts index 8b434d1c..482f244c 100644 --- a/lib/rules/no-unused-message-ids.ts +++ b/lib/rules/no-unused-message-ids.ts @@ -1,3 +1,5 @@ +import type { Rule } from 'eslint'; + import { collectReportViolationAndSuggestionData, findPossibleVariableValues, @@ -7,14 +9,14 @@ import { getReportInfo, getRuleInfo, isVariableFromParameter, -} from '../utils.js'; +} from '../utils'; +import type { Identifier, Node } from 'estree'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -23,7 +25,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-unused-message-ids.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { unusedMessage: 'The messageId "{{messageId}}" is never used.', @@ -38,8 +40,8 @@ const rule = { return {}; } - const messageIdsUsed = new Set(); - let contextIdentifiers; + const messageIdsUsed = new Set(); + let contextIdentifiers: Set; let hasSeenUnknownMessageId = false; let hasSeenViolationReport = false; @@ -64,7 +66,7 @@ const rule = { return; } - const scope = sourceCode.getScope(ast); + const scope = sourceCode.getScope(sourceCode.ast); const messageIdNodesUnused = messageIdNodes.filter( (node) => !messageIdsUsed.has(getKeyName(node, scope)), @@ -76,7 +78,7 @@ const rule = { node: messageIdNode, messageId: 'unusedMessage', data: { - messageId: getKeyName(messageIdNode, scope), + messageId: getKeyName(messageIdNode, scope)!, }, }); } @@ -99,13 +101,16 @@ const rule = { const reportMessagesAndDataArray = collectReportViolationAndSuggestionData(reportInfo); - for (const { messageId } of reportMessagesAndDataArray.filter( - (obj) => obj.messageId, - )) { + for (const messageId of reportMessagesAndDataArray + .map((obj) => obj.messageId) + .filter((messageId) => !!messageId)) { const values = messageId.type === 'Literal' ? [messageId] - : findPossibleVariableValues(messageId, scopeManager); + : findPossibleVariableValues( + messageId as Identifier, + scopeManager, + ); if ( values.length === 0 || values.some((val) => val.type !== 'Literal') @@ -113,7 +118,10 @@ const rule = { // When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives. hasSeenUnknownMessageId = true; } - values.forEach((val) => messageIdsUsed.add(val.value)); + values.forEach( + (val) => + 'value' in val && messageIdsUsed.add(val.value as string), + ); } } }, @@ -127,18 +135,23 @@ const rule = { const values = node.value.type === 'Literal' ? [node.value] - : findPossibleVariableValues(node.value, scopeManager); + : findPossibleVariableValues( + node.value as Identifier, + scopeManager, + ); if ( values.length === 0 || values.some((val) => val.type !== 'Literal') || - isVariableFromParameter(node.value, scopeManager) + isVariableFromParameter(node.value as Identifier, scopeManager) ) { // When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives. hasSeenUnknownMessageId = true; } - values.forEach((val) => messageIdsUsed.add(val.value)); + values.forEach( + (val) => 'value' in val && messageIdsUsed.add(val.value as string), + ); } }, }; diff --git a/lib/utils.ts b/lib/utils.ts index 6db43226..4936fbdc 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -899,14 +899,17 @@ export function isSuggestionFixerFunction( export function evaluateObjectProperties( objectNode: Node | undefined, scopeManager: Scope.ScopeManager, -): Node[] { +): (Property | SpreadElement)[] { if (!objectNode || objectNode.type !== 'ObjectExpression') { return []; } return objectNode.properties.flatMap((property) => { if (property.type === 'SpreadElement') { - const value = findVariableValue(property.argument, scopeManager); + const value = findVariableValue( + property.argument as Identifier, + scopeManager, + ); if (value && value.type === 'ObjectExpression') { return value.properties; } @@ -977,7 +980,7 @@ export function getMessagesNode( export function getMessageIdNodes( ruleInfo: RuleInfo, scopeManager: Scope.ScopeManager, -): Node[] | undefined { +): (Property | SpreadElement)[] | undefined { const messagesNode = getMessagesNode(ruleInfo, scopeManager); return messagesNode && messagesNode.type === 'ObjectExpression' From dbac215f5518e20b72cca5c55bc3b802c9da41dd Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 30 Jun 2025 20:05:33 -0500 Subject: [PATCH 18/82] migrate no-unused-placeholders --- lib/rules/no-unused-placeholders.ts | 34 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/rules/no-unused-placeholders.ts b/lib/rules/no-unused-placeholders.ts index 914eba56..a06a84a9 100644 --- a/lib/rules/no-unused-placeholders.ts +++ b/lib/rules/no-unused-placeholders.ts @@ -4,6 +4,8 @@ */ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; import { collectReportViolationAndSuggestionData, @@ -18,9 +20,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -29,7 +29,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-unused-placeholders.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { placeholderUnused: @@ -41,7 +41,7 @@ const rule = { const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - let contextIdentifiers; + let contextIdentifiers = new Set(); const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { @@ -94,30 +94,32 @@ const rule = { for (const { message, data } of reportMessagesAndDataArray.filter( (obj) => obj.message, )) { - const messageStaticValue = getStaticValue(message, scope); + const messageStaticValue = getStaticValue(message!, scope); if ( - ((message.type === 'Literal' && + ((message?.type === 'Literal' && typeof message.value === 'string') || (messageStaticValue && typeof messageStaticValue.value === 'string')) && data && data.type === 'ObjectExpression' ) { - const messageValue = message.value || messageStaticValue.value; + const messageValue: string = + // @ts-expect-error + message.value || messageStaticValue.value; // https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986 const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g; - const placeholdersInMessage = new Set(); + const placeholdersInMessage = new Set(); - messageValue.replaceAll( - PLACEHOLDER_MATCHER, - (fullMatch, term) => { - placeholdersInMessage.add(term); - }, - ); + const matches = messageValue.matchAll(PLACEHOLDER_MATCHER); + for (const match of matches) { + if (match[1]) { + placeholdersInMessage.add(match[1]); + } + } data.properties.forEach((prop) => { const key = getKeyName(prop); - if (!placeholdersInMessage.has(key)) { + if (key && !placeholdersInMessage.has(key)) { context.report({ node: prop, messageId: 'placeholderUnused', From 912ab48612c34b5d223d022c800b5fb54e0ddae2 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 1 Jul 2025 18:00:23 -0500 Subject: [PATCH 19/82] migrate no-useless-token-range --- lib/rules/no-useless-token-range.ts | 83 +++++++++++++++++------------ 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/lib/rules/no-useless-token-range.ts b/lib/rules/no-useless-token-range.ts index 0ae826cb..85b2f923 100644 --- a/lib/rules/no-useless-token-range.ts +++ b/lib/rules/no-useless-token-range.ts @@ -2,15 +2,22 @@ * @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` * @author Teddy Katz */ +import type { Rule } from 'eslint'; +import type { + CallExpression, + Expression, + MemberExpression, + Node, + Property, + SpreadElement, +} from 'estree'; import { getKeyName, getSourceCodeIdentifiers } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -37,10 +44,10 @@ const rule = { /** * Determines whether a second argument to getFirstToken or getLastToken changes the output of the function. * This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`. - * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken()` or `sourceCode.getLastToken()` - * @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken + * @param arg The second argument to `sourceCode.getFirstToken()` or `sourceCode.getLastToken()` + * @returns `true` if the argument affects the output of getFirstToken or getLastToken */ - function affectsGetTokenOutput(arg) { + function affectsGetTokenOutput(arg: Expression | SpreadElement): boolean { if (!arg) { return false; } @@ -51,33 +58,35 @@ const rule = { arg.properties.length >= 2 || (arg.properties.length === 1 && (getKeyName(arg.properties[0]) !== 'includeComments' || - arg.properties[0].value.type !== 'Literal')) + (arg.properties[0] as Property).value.type !== 'Literal')) ); } + function isMemberExpression(node: Node): node is MemberExpression { + return node.type === 'MemberExpression'; + } + /** * Determines whether a node is a MemberExpression that accesses the `range` property - * @param {ASTNode} node The node - * @returns {boolean} `true` if the node is a MemberExpression that accesses the `range` property + * @param node The node + * @returns `true` if the node is a MemberExpression that accesses the `range` property */ - function isRangeAccess(node) { + function isRangeAccess(node: MemberExpression): boolean { return ( - node.type === 'MemberExpression' && - node.property.type === 'Identifier' && - node.property.name === 'range' + node.property.type === 'Identifier' && node.property.name === 'range' ); } /** * Determines whether a MemberExpression accesses the `start` property (either `.range[0]` or `.start`). * Note that this will also work correctly if the `.range` MemberExpression is passed. - * @param {ASTNode} memberExpression The MemberExpression node to check - * @returns {boolean} `true` if this node accesses either `.range[0]` or `.start` + * @param memberExpression The MemberExpression node to check + * @returns `true` if this node accesses either `.range[0]` or `.start` */ - function isStartAccess(memberExpression) { + function isStartAccess(memberExpression: MemberExpression): boolean { if ( isRangeAccess(memberExpression) && - memberExpression.parent.type === 'MemberExpression' + isMemberExpression(memberExpression.parent) ) { return isStartAccess(memberExpression.parent); } @@ -87,6 +96,7 @@ const rule = { (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 0 && + isMemberExpression(memberExpression.object) && isRangeAccess(memberExpression.object)) ); } @@ -94,13 +104,13 @@ const rule = { /** * Determines whether a MemberExpression accesses the `start` property (either `.range[1]` or `.end`). * Note that this will also work correctly if the `.range` MemberExpression is passed. - * @param {ASTNode} memberExpression The MemberExpression node to check - * @returns {boolean} `true` if this node accesses either `.range[1]` or `.end` + * @param memberExpression The MemberExpression node to check + * @returns `true` if this node accesses either `.range[1]` or `.end` */ - function isEndAccess(memberExpression) { + function isEndAccess(memberExpression: MemberExpression): boolean { if ( isRangeAccess(memberExpression) && - memberExpression.parent.type === 'MemberExpression' + isMemberExpression(memberExpression.parent) ) { return isEndAccess(memberExpression.parent); } @@ -110,6 +120,7 @@ const rule = { (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 1 && + isMemberExpression(memberExpression.object) && isRangeAccess(memberExpression.object)) ); } @@ -123,14 +134,14 @@ const rule = { [...getSourceCodeIdentifiers(sourceCode.scopeManager, ast)] .filter( (identifier) => - identifier.parent.type === 'MemberExpression' && + isMemberExpression(identifier.parent) && identifier.parent.object === identifier && identifier.parent.property.type === 'Identifier' && identifier.parent.parent.type === 'CallExpression' && identifier.parent === identifier.parent.parent.callee && identifier.parent.parent.arguments.length <= 2 && !affectsGetTokenOutput(identifier.parent.parent.arguments[1]) && - identifier.parent.parent.parent.type === 'MemberExpression' && + isMemberExpression(identifier.parent.parent.parent) && identifier.parent.parent === identifier.parent.parent.parent.object && ((isStartAccess(identifier.parent.parent.parent) && @@ -139,20 +150,22 @@ const rule = { identifier.parent.property.name === 'getLastToken')), ) .forEach((identifier) => { - const fullRangeAccess = isRangeAccess( - identifier.parent.parent.parent, - ) - ? identifier.parent.parent.parent.parent - : identifier.parent.parent.parent; + const fullRangeAccess = + isMemberExpression(identifier.parent.parent.parent) && + isRangeAccess(identifier.parent.parent.parent) + ? identifier.parent.parent.parent.parent + : identifier.parent.parent.parent; const replacementText = sourceCode.text.slice( - fullRangeAccess.range[0], - identifier.parent.parent.range[0], + fullRangeAccess.range![0], + identifier.parent.parent.range![0], + ) + + sourceCode.getText( + (identifier.parent.parent as CallExpression).arguments[0], ) + - sourceCode.getText(identifier.parent.parent.arguments[0]) + sourceCode.text.slice( - identifier.parent.parent.range[1], - fullRangeAccess.range[1], + identifier.parent.parent.range![1], + fullRangeAccess.range![1], ); context.report({ node: identifier.parent.parent, @@ -161,7 +174,9 @@ const rule = { fix(fixer) { return fixer.replaceText( identifier.parent.parent, - sourceCode.getText(identifier.parent.parent.arguments[0]), + sourceCode.getText( + (identifier.parent.parent as CallExpression).arguments[0], + ), ); }, }); From 16430e061995796f582cf402eaf6472fea9c9181 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 4 Jul 2025 03:44:43 -0500 Subject: [PATCH 20/82] migrate prefer-message-ids --- lib/rules/prefer-message-ids.ts | 29 ++++++++++++++++------------- lib/types.ts | 3 ++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/rules/prefer-message-ids.ts b/lib/rules/prefer-message-ids.ts index fc5cc51f..1d1ccedb 100644 --- a/lib/rules/prefer-message-ids.ts +++ b/lib/rules/prefer-message-ids.ts @@ -1,4 +1,6 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; import { collectReportViolationAndSuggestionData, @@ -11,9 +13,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -23,7 +23,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-message-ids.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { messagesMissing: @@ -40,7 +40,7 @@ const rule = { return {}; } - let contextIdentifiers; + let contextIdentifiers: Set; // ---------------------------------------------------------------------- // Public @@ -57,10 +57,11 @@ const rule = { const metaNode = ruleInfo.meta; const messagesNode = metaNode && + metaNode.type === 'ObjectExpression' && metaNode.properties && - metaNode.properties.find( - (p) => p.type === 'Property' && getKeyName(p) === 'messages', - ); + metaNode.properties + .filter((p) => p.type === 'Property') + .find((p) => getKeyName(p) === 'messages'); if (!messagesNode) { context.report({ @@ -76,6 +77,7 @@ const rule = { } if ( + staticValue.value && typeof staticValue.value === 'object' && staticValue.value.constructor === Object && Object.keys(staticValue.value).length === 0 @@ -98,11 +100,12 @@ const rule = { return; } - const reportMessagesAndDataArray = - collectReportViolationAndSuggestionData(reportInfo).filter( - (obj) => obj.message, - ); - for (const { message } of reportMessagesAndDataArray) { + const reportMessages = collectReportViolationAndSuggestionData( + reportInfo, + ) + .map((obj) => obj.message) + .filter((message) => !!message); + for (const message of reportMessages) { context.report({ node: message.parent, messageId: 'foundMessage', diff --git a/lib/types.ts b/lib/types.ts index 552806cb..e596782c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -8,6 +8,7 @@ import type { FunctionExpression, Node, ObjectPattern, + Pattern, Property, RestElement, SpreadElement, @@ -25,7 +26,7 @@ export interface FunctionInfo { export interface PartialRuleInfo { create?: Node | null; isNewStyle?: boolean; - meta?: Node | null; + meta?: Expression | Pattern | FunctionDeclaration | null; } export interface RuleInfo extends PartialRuleInfo { From c3b82577f50bd3e529d754d607de83cd23db452b Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 4 Jul 2025 03:49:12 -0500 Subject: [PATCH 21/82] migrate prefer-object-rule --- lib/rules/prefer-object-rule.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/rules/prefer-object-rule.ts b/lib/rules/prefer-object-rule.ts index ec0a0470..d07d7cf2 100644 --- a/lib/rules/prefer-object-rule.ts +++ b/lib/rules/prefer-object-rule.ts @@ -1,15 +1,14 @@ /** * @author Brad Zacher */ +import type { Rule } from 'eslint'; import { getRuleInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -26,10 +25,6 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - const sourceCode = context.sourceCode; const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { @@ -48,7 +43,6 @@ const rule = { *fix(fixer) { // note - we intentionally don't worry about formatting here, as otherwise we have // to indent the function correctly - if ( ruleInfo.create.type === 'FunctionExpression' || ruleInfo.create.type === 'FunctionDeclaration' @@ -59,7 +53,7 @@ const rule = { ); /* istanbul ignore if */ - if (!openParenToken) { + if (!openParenToken || !ruleInfo.create.range) { // this shouldn't happen, but guarding against crashes just in case return null; } From 6080c2a062fcc3adafb4062c9fd0668d44999149 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 4 Jul 2025 15:32:38 -0500 Subject: [PATCH 22/82] migrate prefer-output-null --- lib/rules/prefer-output-null.ts | 74 ++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/lib/rules/prefer-output-null.ts b/lib/rules/prefer-output-null.ts index 7fa78a66..1b2cc8f6 100644 --- a/lib/rules/prefer-output-null.ts +++ b/lib/rules/prefer-output-null.ts @@ -3,14 +3,15 @@ * @author 薛定谔的猫 */ +import type { Rule } from 'eslint'; +import type { Property } from 'estree'; + import { getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -29,45 +30,50 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - const sourceCode = context.sourceCode; return { Program(ast) { getTestInfo(context, ast).forEach((testRun) => { - testRun.invalid.forEach((test) => { - /** - * Get a test case's giving keyname node. - * @param {string} the keyname to find. - * @returns {Node} found node; if not found, return null; - */ - function getTestInfoProperty(key) { - if (test.type === 'ObjectExpression') { - return test.properties.find( - (item) => item.type === 'Property' && item.key.name === key, - ); + testRun.invalid + .filter((test) => !!test) + .forEach((test) => { + /** + * Get a test case's given key name node. + * @param the keyname to find. + * @returns found node; if not found, return null; + */ + function getTestInfoProperty(key: string): Property | null { + if (test.type === 'ObjectExpression') { + return ( + test.properties + .filter((item) => item.type === 'Property') + .find( + (item) => + item.key.type === 'Identifier' && + item.key.name === key, + ) ?? null + ); + } + return null; } - return null; - } - const code = getTestInfoProperty('code'); - const output = getTestInfoProperty('output'); + const code = getTestInfoProperty('code'); + const output = getTestInfoProperty('output'); - if ( - output && - sourceCode.getText(output.value) === - sourceCode.getText(code.value) - ) { - context.report({ - node: output, - messageId: 'useOutputNull', - fix: (fixer) => fixer.replaceText(output.value, 'null'), - }); - } - }); + if ( + output && + code && + sourceCode.getText(output.value) === + sourceCode.getText(code.value) + ) { + context.report({ + node: output, + messageId: 'useOutputNull', + fix: (fixer) => fixer.replaceText(output.value, 'null'), + }); + } + }); }); }, }; From dbab18b0d904259e2a97049b6a68cca1becf24cb Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 6 Jul 2025 17:38:43 -0500 Subject: [PATCH 23/82] migrate prefer-placeholders --- lib/rules/prefer-placeholders.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/rules/prefer-placeholders.ts b/lib/rules/prefer-placeholders.ts index 18868883..58b7afba 100644 --- a/lib/rules/prefer-placeholders.ts +++ b/lib/rules/prefer-placeholders.ts @@ -4,6 +4,8 @@ */ import { findVariable } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import { Node } from 'estree'; import { collectReportViolationAndSuggestionData, @@ -14,9 +16,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -25,7 +25,7 @@ const rule = { recommended: false, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-placeholders.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { usePlaceholders: @@ -34,15 +34,11 @@ const rule = { }, create(context) { - let contextIdentifiers; + let contextIdentifiers = new Set(); const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - return { Program(ast) { contextIdentifiers = getContextIdentifiers(scopeManager, ast); @@ -60,16 +56,17 @@ const rule = { return; } - const reportMessagesAndDataArray = - collectReportViolationAndSuggestionData(reportInfo).filter( - (obj) => obj.message, - ); - for (let { message: messageNode } of reportMessagesAndDataArray) { + const reportMessages = collectReportViolationAndSuggestionData( + reportInfo, + ).map((obj) => obj.message); + for (let messageNode of reportMessages.filter( + (message) => !!message, + )) { if (messageNode.type === 'Identifier') { // See if we can find the variable declaration. const variable = findVariable( - scopeManager.acquire(messageNode) || scopeManager.globalScope, + scopeManager.acquire(messageNode) || scopeManager.globalScope!, messageNode, ); From 4a187f58f44ca119b451c2cfee7c5affed2dc364 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 7 Jul 2025 18:16:51 -0500 Subject: [PATCH 24/82] migrate prefer-replace-text --- lib/rules/fixer-return.ts | 4 ++-- lib/rules/prefer-replace-text.ts | 33 +++++++++++++++++--------------- lib/types.ts | 4 ++-- lib/utils.ts | 11 ++++++++--- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/rules/fixer-return.ts b/lib/rules/fixer-return.ts index 3c880166..01aa41c4 100644 --- a/lib/rules/fixer-return.ts +++ b/lib/rules/fixer-return.ts @@ -130,8 +130,8 @@ const rule: Rule.RuleModule = { hasYieldWithFixer: false, hasReturnWithFixer: false, shouldCheck: - isAutoFixerFunction(node, contextIdentifiers) || - isSuggestionFixerFunction(node, contextIdentifiers), + isAutoFixerFunction(node, contextIdentifiers, context) || + isSuggestionFixerFunction(node, contextIdentifiers, context), node, }; }, diff --git a/lib/rules/prefer-replace-text.ts b/lib/rules/prefer-replace-text.ts index 3d5db946..1a96d65e 100644 --- a/lib/rules/prefer-replace-text.ts +++ b/lib/rules/prefer-replace-text.ts @@ -2,19 +2,27 @@ * @fileoverview prefer using `replaceText()` instead of `replaceTextRange()` * @author 薛定谔的猫 */ +import type { Rule } from 'eslint'; +import type { Identifier, Node } from 'estree'; +import type { FunctionInfo } from '../types.js'; import { getContextIdentifiers, isAutoFixerFunction, isSuggestionFixerFunction, } from '../utils.js'; +const DEFAULT_FUNC_INFO: FunctionInfo = { + upper: null, + codePath: null, + shouldCheck: false, + node: null, +}; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -24,7 +32,7 @@ const rule = { recommended: false, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-replace-text.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { useReplaceText: 'Use replaceText instead of replaceTextRange.', @@ -33,13 +41,8 @@ const rule = { create(context) { const sourceCode = context.sourceCode; - let funcInfo = { - upper: null, - codePath: null, - shouldCheck: false, - node: null, - }; - let contextIdentifiers; + let funcInfo = DEFAULT_FUNC_INFO; + let contextIdentifiers: Set; return { Program(ast) { @@ -50,20 +53,20 @@ const rule = { }, // Stacks this function's information. - onCodePathStart(codePath, node) { + onCodePathStart(codePath: Rule.CodePath, node: Node) { funcInfo = { upper: funcInfo, codePath, shouldCheck: - isAutoFixerFunction(node, contextIdentifiers) || - isSuggestionFixerFunction(node, contextIdentifiers), + isAutoFixerFunction(node, contextIdentifiers, context) || + isSuggestionFixerFunction(node, contextIdentifiers, context), node, }; }, // Pops this function's information. onCodePathEnd() { - funcInfo = funcInfo.upper; + funcInfo = funcInfo.upper ?? DEFAULT_FUNC_INFO; }, // Checks the replaceTextRange arguments. diff --git a/lib/types.ts b/lib/types.ts index e596782c..2861289b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -16,8 +16,8 @@ import type { export interface FunctionInfo { codePath: Rule.CodePath | null; - hasReturnWithFixer: boolean; - hasYieldWithFixer: boolean; + hasReturnWithFixer?: boolean; + hasYieldWithFixer?: boolean; node: Node | null; shouldCheck: boolean; upper: FunctionInfo | null; diff --git a/lib/utils.ts b/lib/utils.ts index 4936fbdc..65252b3c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -842,6 +842,7 @@ export function collectReportViolationAndSuggestionData( export function isAutoFixerFunction( node: Node, contextIdentifiers: Set, + context: Rule.RuleContext, ): node is FunctionExpression | ArrowFunctionExpression { const parent = node.parent; return ( @@ -852,7 +853,7 @@ export function isAutoFixerFunction( contextIdentifiers.has(parent.parent.parent.callee.object as Identifier) && parent.parent.parent.callee.property.type === 'Identifier' && parent.parent.parent.callee.property.name === 'report' && - getReportInfo(parent.parent.parent)?.fix === node + getReportInfo(parent.parent.parent, context)?.fix === node ); } @@ -860,10 +861,12 @@ export function isAutoFixerFunction( * Whether the provided node represents a suggestion fixer function. * @param node * @param contextIdentifiers + * @param context */ export function isSuggestionFixerFunction( node: Node, contextIdentifiers: Set, + context: Rule.RuleContext, ): boolean { const parent = node.parent; return ( @@ -880,12 +883,14 @@ export function isSuggestionFixerFunction( parent.parent.parent.parent.parent.type === 'ObjectExpression' && parent.parent.parent.parent.parent.parent.type === 'CallExpression' && contextIdentifiers.has( + // @ts-expect-error -- Property 'object' does not exist on type 'Expression | Super'. Property 'object' does not exist on type 'ClassExpression'.ts(2339) parent.parent.parent.parent.parent.parent.callee.object, ) && + // @ts-expect-error -- Property 'property' does not exist on type 'Expression | Super'. Property 'property' does not exist on type 'ClassExpression'.ts(2339) parent.parent.parent.parent.parent.parent.callee.property.name === 'report' && - getReportInfo(parent.parent.parent.parent.parent.parent).suggest === - parent.parent.parent + getReportInfo(parent.parent.parent.parent.parent.parent, context) + ?.suggest === parent.parent.parent ); } From 10b11351f6b5b49b92ca62e1de1e8814214d8da4 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 9 Jul 2025 17:51:54 -0500 Subject: [PATCH 25/82] migrate report-message-format --- lib/rules/report-message-format.ts | 41 +++++++++++++----------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/lib/rules/report-message-format.ts b/lib/rules/report-message-format.ts index 7d6790b5..3912f67d 100644 --- a/lib/rules/report-message-format.ts +++ b/lib/rules/report-message-format.ts @@ -2,8 +2,9 @@ * @fileoverview enforce a consistent format for rule report messages * @author Teddy Katz */ - import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule, Scope } from 'eslint'; +import type { Expression, Node, Pattern, SpreadElement } from 'estree'; import { getContextIdentifiers, @@ -15,9 +16,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -26,7 +25,7 @@ const rule = { recommended: false, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/report-message-format.md', }, - fixable: null, + fixable: undefined, schema: [ { description: 'Format that all report messages must match.', @@ -41,14 +40,16 @@ const rule = { create(context) { const pattern = new RegExp(context.options[0] || ''); - let contextIdentifiers; + let contextIdentifiers: Set; /** * Report a message node if it doesn't match the given formatting - * @param {ASTNode} message The message AST node - * @returns {void} + * @param message The message AST node */ - function processMessageNode(message, scope) { + function processMessageNode( + message: Expression | Pattern | SpreadElement, + scope: Scope.Scope, + ): void { const staticValue = getStaticValue(message, scope); if ( (message.type === 'Literal' && @@ -56,8 +57,8 @@ const rule = { !pattern.test(message.value)) || (message.type === 'TemplateLiteral' && message.quasis.length === 1 && - !pattern.test(message.quasis[0].value.cooked)) || - (staticValue && !pattern.test(staticValue.value)) + !pattern.test(message.quasis[0].value.cooked ?? '')) || + (staticValue && !pattern.test(staticValue.value as string)) ) { context.report({ node: message, @@ -73,10 +74,6 @@ const rule = { return {}; } - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - return { Program(ast) { const scope = sourceCode.getScope(ast); @@ -89,10 +86,9 @@ const rule = { ruleInfo && ruleInfo.meta && ruleInfo.meta.type === 'ObjectExpression' && - ruleInfo.meta.properties.find( - (prop) => - prop.type === 'Property' && getKeyName(prop) === 'messages', - ); + ruleInfo.meta.properties + .filter((prop) => prop.type === 'Property') + .find((prop) => getKeyName(prop) === 'messages'); if ( !messagesObject || @@ -125,13 +121,12 @@ const rule = { if (suggest && suggest.type === 'ArrayExpression') { suggest.elements .flatMap((obj) => - obj.type === 'ObjectExpression' ? obj.properties : [], + !!obj && obj.type === 'ObjectExpression' ? obj.properties : [], ) + .filter((prop) => prop.type === 'Property') .filter( (prop) => - prop.type === 'Property' && - prop.key.type === 'Identifier' && - prop.key.name === 'message', + prop.key.type === 'Identifier' && prop.key.name === 'message', ) .map((prop) => prop.value) .forEach((it) => processMessageNode(it, scope)); From 80b563c65d25625811873956b9cffbdc6bf66380 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 11 Jul 2025 04:16:22 -0500 Subject: [PATCH 26/82] migrate require-meta-default-options --- lib/rules/require-meta-default-options.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/rules/require-meta-default-options.ts b/lib/rules/require-meta-default-options.ts index 24c7e2cc..a65456bd 100644 --- a/lib/rules/require-meta-default-options.ts +++ b/lib/rules/require-meta-default-options.ts @@ -1,3 +1,5 @@ +import type { Rule } from 'eslint'; + import { evaluateObjectProperties, getKeyName, @@ -6,8 +8,7 @@ import { getRuleInfo, } from '../utils.js'; -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -45,10 +46,9 @@ const rule = { return {}; } - const metaDefaultOptions = evaluateObjectProperties( - metaNode, - scopeManager, - ).find((p) => p.type === 'Property' && getKeyName(p) === 'defaultOptions'); + const metaDefaultOptions = evaluateObjectProperties(metaNode, scopeManager) + .filter((p) => p.type === 'Property') + .find((p) => getKeyName(p) === 'defaultOptions'); if ( schemaProperty.type === 'ArrayExpression' && @@ -68,7 +68,7 @@ const rule = { if (!metaDefaultOptions) { context.report({ - node: metaNode, + node: metaNode!, messageId: 'missingDefaultOptions', fix(fixer) { return fixer.insertTextAfter(schemaProperty, ', defaultOptions: []'); @@ -87,8 +87,11 @@ const rule = { const isArrayRootSchema = schemaProperty.type === 'ObjectExpression' && - schemaProperty.properties.find((property) => property.key.name === 'type') - ?.value.value === 'array'; + schemaProperty.properties + .filter((property) => property.type === 'Property') + // @ts-expect-error + .find((property) => property.key.name === 'type')?.value.value === + 'array'; if (metaDefaultOptions.value.elements.length === 0 && !isArrayRootSchema) { context.report({ From 900ae8c1b5d697361fff80cfc4a1387ec4732860 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 11 Jul 2025 18:00:29 -0500 Subject: [PATCH 27/82] migrate require-meta-docs-description --- lib/rules/require-meta-docs-description.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/rules/require-meta-docs-description.ts b/lib/rules/require-meta-docs-description.ts index 4f1dc6ea..dd0af4eb 100644 --- a/lib/rules/require-meta-docs-description.ts +++ b/lib/rules/require-meta-docs-description.ts @@ -1,15 +1,11 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; import { getMetaDocsProperty, getRuleInfo } from '../utils.js'; -// ------------------------------------------------------------------------------ -// Rule Definition -// ------------------------------------------------------------------------------ - const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)'); -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -19,7 +15,7 @@ const rule = { recommended: false, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-docs-description.md', }, - fixable: null, + fixable: undefined, schema: [ { type: 'object', @@ -94,7 +90,7 @@ const rule = { context.report({ node: descriptionNode.value, messageId: 'mismatch', - data: { pattern }, + data: { pattern: String(pattern) }, }); } }, From 0eb77ccb4330b771a2708e0ed3cb5a8f88861318 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 11 Jul 2025 18:01:12 -0500 Subject: [PATCH 28/82] migrate prefer-replace-text --- lib/types.ts | 2 +- lib/utils.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index 2861289b..b9378e18 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -26,7 +26,7 @@ export interface FunctionInfo { export interface PartialRuleInfo { create?: Node | null; isNewStyle?: boolean; - meta?: Expression | Pattern | FunctionDeclaration | null; + meta?: Expression | Pattern | FunctionDeclaration; } export interface RuleInfo extends PartialRuleInfo { diff --git a/lib/utils.ts b/lib/utils.ts index 65252b3c..0d10e710 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -216,7 +216,7 @@ function getRuleExportsESM( ); } else if (isFunctionRule(node)) { // Check `export default function(context) { return { ... }; }` - return { create: node, meta: null, isNewStyle: false }; + return { create: node, meta: undefined, isNewStyle: false }; } else if (isTypeScriptRuleHelper(node)) { // Check `export default someTypeScriptHelper({ create() {}, meta: {} }); return collectInterestingProperties( @@ -235,7 +235,7 @@ function getRuleExportsESM( ); } else if (isFunctionRule(possibleRule)) { // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;` - return { create: possibleRule, meta: null, isNewStyle: false }; + return { create: possibleRule, meta: undefined, isNewStyle: false }; } else if (isTypeScriptRuleHelper(possibleRule)) { // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule; return collectInterestingProperties( @@ -276,7 +276,7 @@ function getRuleExportsCJS( // Check `module.exports = function (context) { return { ... }; }` exportsIsFunction = true; - return { create: node.right, meta: null, isNewStyle: false }; + return { create: node.right, meta: undefined, isNewStyle: false }; } else if (node.right.type === 'ObjectExpression') { // Check `module.exports = { create: function () {}, meta: {} }` @@ -296,7 +296,11 @@ function getRuleExportsCJS( ); } else if (isFunctionRule(possibleRule)) { // Check `const possibleRule = function(context) { return { ... } }; module.exports = possibleRule;` - return { create: possibleRule, meta: null, isNewStyle: false }; + return { + create: possibleRule, + meta: undefined, + isNewStyle: false, + }; } } } @@ -1014,7 +1018,7 @@ export function getMessageIdNodeById( } export function getMetaSchemaNode( - metaNode: Node, + metaNode: Node | undefined, scopeManager: Scope.ScopeManager, ): Property | undefined { return evaluateObjectProperties(metaNode, scopeManager) @@ -1023,7 +1027,7 @@ export function getMetaSchemaNode( } export function getMetaSchemaNodeProperty( - schemaNode: AssignmentProperty | Property, + schemaNode: AssignmentProperty | Property | undefined, scopeManager: Scope.ScopeManager, ): Node | null { if (!schemaNode) { From 11fdcbf849bd8002f74ed36eae706d62aaf462d3 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 12 Jul 2025 17:57:46 -0500 Subject: [PATCH 29/82] migrate require-meta-docs-recommended --- lib/rules/require-meta-docs-recommended.ts | 29 +++++++++++----------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/rules/require-meta-docs-recommended.ts b/lib/rules/require-meta-docs-recommended.ts index 3038ea3c..4eb943c8 100644 --- a/lib/rules/require-meta-docs-recommended.ts +++ b/lib/rules/require-meta-docs-recommended.ts @@ -1,4 +1,6 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { ObjectExpression } from 'estree'; import { getMetaDocsProperty, @@ -6,12 +8,11 @@ import { isUndefinedIdentifier, } from '../utils.js'; -/** - * @param {import('eslint').Rule.RuleFixer} fixer - * @param {import('estree').ObjectExpression} objectNode - * @param {boolean} recommendedValue - */ -function insertRecommendedProperty(fixer, objectNode, recommendedValue) { +function insertRecommendedProperty( + fixer: Rule.RuleFixer, + objectNode: ObjectExpression, + recommendedValue: boolean, +) { if (objectNode.properties.length === 0) { return fixer.replaceText( objectNode, @@ -19,13 +20,12 @@ function insertRecommendedProperty(fixer, objectNode, recommendedValue) { ); } return fixer.insertTextAfter( - objectNode.properties.at(-1), + objectNode.properties.at(-1)!, `, recommended: ${recommendedValue}`, ); } -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -35,7 +35,7 @@ const rule = { recommended: false, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-docs-recommended.md', }, - fixable: null, + fixable: undefined, hasSuggestions: true, schema: [ { @@ -74,18 +74,19 @@ const rule = { } = getMetaDocsProperty('recommended', ruleInfo, scopeManager); if (!descriptionNode) { - const suggestions = - docsNode?.value?.type === 'ObjectExpression' + const docNodeValue = docsNode?.value; + const suggestions: Rule.SuggestionReportDescriptor[] = + docNodeValue?.type === 'ObjectExpression' ? [ { messageId: 'setRecommendedTrue', fix: (fixer) => - insertRecommendedProperty(fixer, docsNode.value, true), + insertRecommendedProperty(fixer, docNodeValue, true), }, { messageId: 'setRecommendedFalse', fix: (fixer) => - insertRecommendedProperty(fixer, docsNode.value, false), + insertRecommendedProperty(fixer, docNodeValue, false), }, ] : []; From 1ce2806aeb36454abbc7dee6cc1a1101d7b73d3a Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 14 Jul 2025 13:42:39 -0500 Subject: [PATCH 30/82] migrate require-meta-docs-url --- lib/rules/require-meta-docs-url.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/rules/require-meta-docs-url.ts b/lib/rules/require-meta-docs-url.ts index 41e3a350..22c58b7f 100644 --- a/lib/rules/require-meta-docs-url.ts +++ b/lib/rules/require-meta-docs-url.ts @@ -1,9 +1,10 @@ /** * @author Toru Nagashima */ - import path from 'node:path'; + import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; import { getMetaDocsProperty, @@ -16,8 +17,7 @@ import { // Rule Definition // ----------------------------------------------------------------------------- -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -50,8 +50,8 @@ const rule = { /** * Creates AST event handlers for require-meta-docs-url. - * @param {RuleContext} context - The rule context. - * @returns {Object} AST event handlers. + * @param context - The rule context. + * @returns AST event handlers. */ create(context) { const options = context.options[0] || {}; @@ -67,10 +67,10 @@ const rule = { /** * Check whether a given URL is the expected URL. - * @param {string} url The URL to check. - * @returns {boolean} `true` if the node is the expected URL. + * @param url The URL to check. + * @returns `true` if the node is the expected URL. */ - function isExpectedUrl(url) { + function isExpectedUrl(url: string | undefined | null): boolean { return Boolean( typeof url === 'string' && (expectedUrl === undefined || url === expectedUrl), @@ -102,7 +102,7 @@ const rule = { return; } - if (isExpectedUrl(staticValue && staticValue.value)) { + if (isExpectedUrl(staticValue && (staticValue.value as string))) { return; } From ab5242509230db300931596514be81c2de5e2cbb Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 14 Jul 2025 17:14:46 -0500 Subject: [PATCH 31/82] migrate require-meta-fixable --- lib/rules/require-meta-fixable.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/rules/require-meta-fixable.ts b/lib/rules/require-meta-fixable.ts index 67607aca..339a0fe6 100644 --- a/lib/rules/require-meta-fixable.ts +++ b/lib/rules/require-meta-fixable.ts @@ -2,8 +2,9 @@ * @fileoverview require rules to implement a `meta.fixable` property * @author Teddy Katz */ - import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; import { evaluateObjectProperties, @@ -15,9 +16,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -57,8 +56,8 @@ const rule = { const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; const ruleInfo = getRuleInfo(sourceCode); - let contextIdentifiers; - let usesFixFunctions; + let contextIdentifiers: Set; + let usesFixFunctions = false; if (!ruleInfo) { return {}; @@ -87,9 +86,9 @@ const rule = { const scope = sourceCode.getScope(ast); const metaFixableProp = ruleInfo && - evaluateObjectProperties(ruleInfo.meta, scopeManager).find( - (prop) => getKeyName(prop) === 'fixable', - ); + evaluateObjectProperties(ruleInfo.meta, scopeManager) + .filter((prop) => prop.type === 'Property') + .find((prop) => getKeyName(prop) === 'fixable'); if (metaFixableProp) { const staticValue = getStaticValue(metaFixableProp.value, scope); @@ -99,7 +98,9 @@ const rule = { } if ( - !['code', 'whitespace', null, undefined].includes(staticValue.value) + !['code', 'whitespace', null, undefined].includes( + staticValue.value as string, + ) ) { // `fixable` property has an invalid value. context.report({ @@ -111,7 +112,7 @@ const rule = { if ( usesFixFunctions && - !['code', 'whitespace'].includes(staticValue.value) + !['code', 'whitespace'].includes(staticValue.value as string) ) { // Rule is fixable but `fixable` property does not have a fixable value. context.report({ @@ -121,7 +122,7 @@ const rule = { } else if ( catchNoFixerButFixableProperty && !usesFixFunctions && - ['code', 'whitespace'].includes(staticValue.value) + ['code', 'whitespace'].includes(staticValue.value as string) ) { // Rule is NOT fixable but `fixable` property has a fixable value. context.report({ From b7971877c3ca9103cba2602e7bd568f90df26905 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 15 Jul 2025 16:36:09 -0500 Subject: [PATCH 32/82] migrate require-meta-has-suggestions --- lib/rules/require-meta-has-suggestions.ts | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/rules/require-meta-has-suggestions.ts b/lib/rules/require-meta-has-suggestions.ts index 18b483ec..c031bfcd 100644 --- a/lib/rules/require-meta-has-suggestions.ts +++ b/lib/rules/require-meta-has-suggestions.ts @@ -1,4 +1,6 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { Node, Property } from 'estree'; import { evaluateObjectProperties, @@ -11,9 +13,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -40,15 +40,15 @@ const rule = { if (!ruleInfo) { return {}; } - let contextIdentifiers; - let ruleReportsSuggestions; + let contextIdentifiers: Set; + let ruleReportsSuggestions = false; /** * Check if a "suggest" object property from a rule violation report should be considered to contain suggestions. - * @param {Node} node - the "suggest" object property to check - * @returns {boolean} whether this property should be considered to contain suggestions + * @param node - the "suggest" object property to check + * @returns whether this property should be considered to contain suggestions */ - function doesPropertyContainSuggestions(node) { + function doesPropertyContainSuggestions(node: Property): boolean { const scope = sourceCode.getScope(node); const staticValue = getStaticValue(node.value, scope); if ( @@ -84,7 +84,9 @@ const rule = { const suggestProp = evaluateObjectProperties( node.arguments[0], scopeManager, - ).find((prop) => getKeyName(prop) === 'suggest'); + ) + .filter((prop) => prop.type === 'Property') + .find((prop) => getKeyName(prop) === 'suggest'); if (suggestProp && doesPropertyContainSuggestions(suggestProp)) { ruleReportsSuggestions = true; } @@ -107,7 +109,9 @@ const rule = { const hasSuggestionsProperty = evaluateObjectProperties( metaNode, scopeManager, - ).find((prop) => getKeyName(prop) === 'hasSuggestions'); + ) + .filter((prop) => prop.type === 'Property') + .find((prop) => getKeyName(prop) === 'hasSuggestions'); const hasSuggestionsStaticValue = hasSuggestionsProperty && getStaticValue(hasSuggestionsProperty.value, scope); @@ -133,6 +137,7 @@ const rule = { 'hasSuggestions: true, ', ); } + return null; }, }); } else if ( @@ -153,6 +158,7 @@ const rule = { 'true', ); } + return null; }, }); } From 44c1bb47a25dc56ae83406d7b502d3ba0e33a0ad Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 15 Jul 2025 16:43:28 -0500 Subject: [PATCH 33/82] migrate require-meta-schema-description --- lib/rules/require-meta-schema-description.ts | 25 +++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/rules/require-meta-schema-description.ts b/lib/rules/require-meta-schema-description.ts index b15669ec..dba60771 100644 --- a/lib/rules/require-meta-schema-description.ts +++ b/lib/rules/require-meta-schema-description.ts @@ -1,4 +1,6 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { Expression, SpreadElement } from 'estree'; import { getMetaSchemaNode, @@ -9,9 +11,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -51,15 +51,20 @@ const rule = { return {}; - function checkSchemaElement(node, isRoot) { - if (node.type !== 'ObjectExpression') { + function checkSchemaElement( + node: Expression | SpreadElement | null, + isRoot = false, + ): void { + if (!node || node.type !== 'ObjectExpression') { return; } let hadChildren = false; let hadDescription = false; - for (const { key, value } of node.properties) { + for (const { key, value } of node.properties.filter( + (prop) => prop.type === 'Property', + )) { if (!key) { continue; } @@ -69,6 +74,7 @@ const rule = { continue; } + // @ts-expect-error switch (key.name ?? key.value) { case 'description': { hadDescription = true; @@ -90,9 +96,12 @@ const rule = { case 'properties': { hadChildren = true; - if (Array.isArray(value.properties)) { + if ('properties' in value && Array.isArray(value.properties)) { for (const property of value.properties) { - if (property.value?.type === 'ObjectExpression') { + if ( + 'value' in property && + property.value?.type === 'ObjectExpression' + ) { checkSchemaElement(property.value); } } From 7404dec1a56a31ce49a641b38200c9f7edaab163 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 15 Jul 2025 16:59:47 -0500 Subject: [PATCH 34/82] migrate require-meta-schema --- lib/rules/require-meta-schema.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/rules/require-meta-schema.ts b/lib/rules/require-meta-schema.ts index da65ad9a..909fdd58 100644 --- a/lib/rules/require-meta-schema.ts +++ b/lib/rules/require-meta-schema.ts @@ -1,3 +1,6 @@ +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; + import { getContextIdentifiers, getMetaSchemaNode, @@ -10,9 +13,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -55,7 +56,7 @@ const rule = { return {}; } - let contextIdentifiers; + let contextIdentifiers: Set; const metaNode = ruleInfo.meta; // Options From db487807e362b34b80b0a2e94088755ad7a2f048 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 15 Jul 2025 17:01:54 -0500 Subject: [PATCH 35/82] migrate require-meta-type --- lib/rules/require-meta-type.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/rules/require-meta-type.ts b/lib/rules/require-meta-type.ts index 31ad1aba..501224ed 100644 --- a/lib/rules/require-meta-type.ts +++ b/lib/rules/require-meta-type.ts @@ -2,8 +2,8 @@ * @fileoverview require rules to implement a `meta.type` property * @author 薛定谔的猫 */ - import { getStaticValue } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; @@ -12,9 +12,7 @@ const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']); // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -23,7 +21,7 @@ const rule = { recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-type.md', }, - fixable: null, + fixable: undefined, schema: [], messages: { missing: @@ -34,10 +32,6 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - const sourceCode = context.sourceCode; const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { @@ -50,9 +44,9 @@ const rule = { const { scopeManager } = sourceCode; const metaNode = ruleInfo.meta; - const typeNode = evaluateObjectProperties(metaNode, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p) === 'type', - ); + const typeNode = evaluateObjectProperties(metaNode, scopeManager) + .filter((p) => p.type === 'Property') + .find((p) => getKeyName(p) === 'type'); if (!typeNode) { context.report({ @@ -68,7 +62,7 @@ const rule = { return; } - if (!VALID_TYPES.has(staticValue.value)) { + if (!VALID_TYPES.has(staticValue.value as string)) { context.report({ node: typeNode.value, messageId: 'unexpected' }); } }, From 6f0cd122d88fefbeb494fba60576cb256d558b30 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 15 Jul 2025 17:34:02 -0500 Subject: [PATCH 36/82] migrate test-case-property-ordering --- lib/rules/consistent-output.ts | 3 ++- lib/rules/test-case-property-ordering.ts | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/rules/consistent-output.ts b/lib/rules/consistent-output.ts index 65abc332..a8c7900d 100644 --- a/lib/rules/consistent-output.ts +++ b/lib/rules/consistent-output.ts @@ -39,7 +39,8 @@ const rule: Rule.RuleModule = { }, create(context) { - const always = context.options[0] && context.options[0] === 'always'; + const always: boolean = + context.options[0] && context.options[0] === 'always'; return { Program(ast) { diff --git a/lib/rules/test-case-property-ordering.ts b/lib/rules/test-case-property-ordering.ts index 73728e9a..50eee786 100644 --- a/lib/rules/test-case-property-ordering.ts +++ b/lib/rules/test-case-property-ordering.ts @@ -2,6 +2,7 @@ * @fileoverview Requires the properties of a test case to be placed in a consistent order. * @author 薛定谔的猫 */ +import type { Rule } from 'eslint'; import { getKeyName, getTestInfo } from '../utils.js'; @@ -18,12 +19,13 @@ const defaultOrder = [ 'errors', ]; +const keyNameMapper = (property: Parameters[0]) => + getKeyName(property); + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -52,7 +54,7 @@ const rule = { // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- - const order = context.options[0] || defaultOrder; + const order: string[] = context.options[0] || defaultOrder; const sourceCode = context.sourceCode; return { @@ -60,10 +62,14 @@ const rule = { getTestInfo(context, ast).forEach((testRun) => { [testRun.valid, testRun.invalid].forEach((tests) => { tests.forEach((test) => { - const properties = (test && test.properties) || []; - const keyNames = properties.map(getKeyName); + const properties = + (test && test.type === 'ObjectExpression' && test.properties) || + []; + const keyNames = properties + .map(keyNameMapper) + .filter((keyName) => keyName !== null); - for (let i = 0, lastChecked; i < keyNames.length; i++) { + for (let i = 0, lastChecked = 0; i < keyNames.length; i++) { const current = order.indexOf(keyNames[i]); // current < lastChecked to catch unordered; From cf361fa6e2765a58e3325e272da7b6f081727516 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 16 Jul 2025 04:12:28 -0500 Subject: [PATCH 37/82] migrate test-case-shorthand-strings --- lib/rules/test-case-shorthand-strings.ts | 84 +++++++++++++----------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/lib/rules/test-case-shorthand-strings.ts b/lib/rules/test-case-shorthand-strings.ts index 2d67dd8e..64352316 100644 --- a/lib/rules/test-case-shorthand-strings.ts +++ b/lib/rules/test-case-shorthand-strings.ts @@ -2,15 +2,15 @@ * @fileoverview Enforce consistent usage of shorthand strings for test cases with no options * @author Teddy Katz */ +import type { Rule } from 'eslint'; import { getKeyName, getTestInfo } from '../utils.js'; +import type { TestInfo } from '../types.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -45,11 +45,11 @@ const rule = { /** * Reports test cases as necessary - * @param {object[]} cases A list of test case nodes - * @returns {void} + * @param cases A list of test case nodes */ - function reportTestCases(cases) { + function reportTestCases(cases: TestInfo['valid']): void { const caseInfoList = cases + .filter((testCase) => !!testCase) .map((testCase) => { if ( testCase.type === 'Literal' || @@ -69,7 +69,7 @@ const rule = { } return null; }) - .filter(Boolean); + .filter((testCase) => !!testCase); const isConsistent = new Set(caseInfoList.map((caseInfo) => caseInfo.shorthand)).size <= 1; @@ -77,37 +77,47 @@ const rule = { (caseInfo) => caseInfo.needsLongform, ); - caseInfoList - .filter( - { - 'as-needed': (caseInfo) => - !caseInfo.shorthand && !caseInfo.needsLongform, - never: (caseInfo) => caseInfo.shorthand, - consistent: isConsistent - ? () => false - : (caseInfo) => caseInfo.shorthand, - 'consistent-as-needed': (caseInfo) => - caseInfo.shorthand === hasCaseNeedingLongform, - }[shorthandOption], - ) - .forEach((badCaseInfo) => { - context.report({ - node: badCaseInfo.node, - messageId: 'useShorthand', - data: { - preferred: badCaseInfo.shorthand ? 'an object' : 'a string', - actual: badCaseInfo.shorthand ? 'a string' : 'an object', - }, - fix(fixer) { - return fixer.replaceText( - badCaseInfo.node, - badCaseInfo.shorthand - ? `{code: ${sourceCode.getText(badCaseInfo.node)}}` - : sourceCode.getText(badCaseInfo.node.properties[0].value), - ); - }, - }); + let caseInfoFilter: (caseInfo: (typeof caseInfoList)[number]) => boolean; + switch (shorthandOption) { + case 'as-needed': + caseInfoFilter = (caseInfo) => + !caseInfo.shorthand && !caseInfo.needsLongform; + break; + case 'never': + caseInfoFilter = (caseInfo) => caseInfo.shorthand; + break; + case 'consistent': + caseInfoFilter = isConsistent + ? () => false + : (caseInfo) => caseInfo.shorthand; + break; + case 'consistent-as-needed': + caseInfoFilter = (caseInfo) => + caseInfo.shorthand === hasCaseNeedingLongform; + break; + default: + return; // invalid option + } + + caseInfoList.filter(caseInfoFilter).forEach((badCaseInfo) => { + context.report({ + node: badCaseInfo.node, + messageId: 'useShorthand', + data: { + preferred: badCaseInfo.shorthand ? 'an object' : 'a string', + actual: badCaseInfo.shorthand ? 'a string' : 'an object', + }, + fix(fixer) { + return fixer.replaceText( + badCaseInfo.node, + badCaseInfo.shorthand + ? `{code: ${sourceCode.getText(badCaseInfo.node)}}` + : // @ts-expect-error + sourceCode.getText(badCaseInfo.node.properties[0].value), + ); + }, }); + }); } // ---------------------------------------------------------------------- From 833e1d111bb32e5f7025a34d6b44f16a81756bee Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 16 Jul 2025 04:13:52 -0500 Subject: [PATCH 38/82] migrate plugin (index) --- lib/index.ts | 110 ++++++++++++++++++++++++-------------------------- tsconfig.json | 3 +- 2 files changed, 55 insertions(+), 58 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index f512a196..f720cb09 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,56 +2,54 @@ * @fileoverview An ESLint plugin for linting ESLint plugins * @author Teddy Katz */ - -// ------------------------------------------------------------------------------ -// Requirements -// ------------------------------------------------------------------------------ - import packageMetadata from '../package.json' with { type: 'json' }; -import consistentOutput from './rules/consistent-output.js'; -import fixerReturn from './rules/fixer-return.js'; -import metaPropertyOrdering from './rules/meta-property-ordering.js'; -import noDeprecatedContextMethods from './rules/no-deprecated-context-methods.js'; -import noDeprecatedReportApi from './rules/no-deprecated-report-api.js'; -import noIdenticalTests from './rules/no-identical-tests.js'; -import noMetaReplacedBy from './rules/no-meta-replaced-by.js'; -import noMetaSchemaDefault from './rules/no-meta-schema-default.js'; -import noMissingMessageIds from './rules/no-missing-message-ids.js'; -import noMissingPlaceholders from './rules/no-missing-placeholders.js'; -import noOnlyTests from './rules/no-only-tests.js'; -import noPropertyInNode from './rules/no-property-in-node.js'; -import noUnusedMessageIds from './rules/no-unused-message-ids.js'; -import noUnusedPlaceholders from './rules/no-unused-placeholders.js'; -import noUselessTokenRange from './rules/no-useless-token-range.js'; -import preferMessageIds from './rules/prefer-message-ids.js'; -import preferObjectRule from './rules/prefer-object-rule.js'; -import preferOutputNull from './rules/prefer-output-null.js'; -import preferPlaceholders from './rules/prefer-placeholders.js'; -import preferReplaceText from './rules/prefer-replace-text.js'; -import reportMessageFormat from './rules/report-message-format.js'; -import requireMetaDefaultOptions from './rules/require-meta-default-options.js'; -import requireMetaDocsDescription from './rules/require-meta-docs-description.js'; -import requireMetaDocsRecommended from './rules/require-meta-docs-recommended.js'; -import requireMetaDocsUrl from './rules/require-meta-docs-url.js'; -import requireMetaFixable from './rules/require-meta-fixable.js'; -import requireMetaHasSuggestions from './rules/require-meta-has-suggestions.js'; -import requireMetaSchemaDescription from './rules/require-meta-schema-description.js'; -import requireMetaSchema from './rules/require-meta-schema.js'; -import requireMetaType from './rules/require-meta-type.js'; -import testCasePropertyOrdering from './rules/test-case-property-ordering.js'; -import testCaseShorthandStrings from './rules/test-case-shorthand-strings.js'; +import consistentOutput from './rules/consistent-output'; +import fixerReturn from './rules/fixer-return'; +import metaPropertyOrdering from './rules/meta-property-ordering'; +import noDeprecatedContextMethods from './rules/no-deprecated-context-methods'; +import noDeprecatedReportApi from './rules/no-deprecated-report-api'; +import noIdenticalTests from './rules/no-identical-tests'; +import noMetaReplacedBy from './rules/no-meta-replaced-by'; +import noMetaSchemaDefault from './rules/no-meta-schema-default'; +import noMissingMessageIds from './rules/no-missing-message-ids'; +import noMissingPlaceholders from './rules/no-missing-placeholders'; +import noOnlyTests from './rules/no-only-tests'; +import noPropertyInNode from './rules/no-property-in-node'; +import noUnusedMessageIds from './rules/no-unused-message-ids'; +import noUnusedPlaceholders from './rules/no-unused-placeholders'; +import noUselessTokenRange from './rules/no-useless-token-range'; +import preferMessageIds from './rules/prefer-message-ids'; +import preferObjectRule from './rules/prefer-object-rule'; +import preferOutputNull from './rules/prefer-output-null'; +import preferPlaceholders from './rules/prefer-placeholders'; +import preferReplaceText from './rules/prefer-replace-text'; +import reportMessageFormat from './rules/report-message-format'; +import requireMetaDefaultOptions from './rules/require-meta-default-options'; +import requireMetaDocsDescription from './rules/require-meta-docs-description'; +import requireMetaDocsRecommended from './rules/require-meta-docs-recommended'; +import requireMetaDocsUrl from './rules/require-meta-docs-url'; +import requireMetaFixable from './rules/require-meta-fixable'; +import requireMetaHasSuggestions from './rules/require-meta-has-suggestions'; +import requireMetaSchemaDescription from './rules/require-meta-schema-description'; +import requireMetaSchema from './rules/require-meta-schema'; +import requireMetaType from './rules/require-meta-type'; +import testCasePropertyOrdering from './rules/test-case-property-ordering'; +import testCaseShorthandStrings from './rules/test-case-shorthand-strings'; +import type { ESLint, Rule } from 'eslint'; const PLUGIN_NAME = packageMetadata.name.replace(/^eslint-plugin-/, ''); +const CONFIG_NAMES = ['all', 'all-type-checked', 'recommended', 'rules', 'tests', 'rules-recommended', 'tests-recommended'] as const; +type ConfigName = (typeof CONFIG_NAMES)[number]; -const configFilters = { - all: (rule) => !rule.meta.docs.requiresTypeChecking, +const configFilters: Record boolean> = { + all: (rule: Rule.RuleModule) => !(rule.meta?.docs && 'requiresTypeChecking' in rule.meta.docs && rule.meta.docs.requiresTypeChecking), 'all-type-checked': () => true, - recommended: (rule) => rule.meta.docs.recommended, - rules: (rule) => rule.meta.docs.category === 'Rules', - tests: (rule) => rule.meta.docs.category === 'Tests', - 'rules-recommended': (rule) => + recommended: (rule: Rule.RuleModule) => !!rule.meta?.docs?.recommended, + rules: (rule: Rule.RuleModule) => rule.meta?.docs?.category === 'Rules', + tests: (rule: Rule.RuleModule) => rule.meta?.docs?.category === 'Tests', + 'rules-recommended': (rule: Rule.RuleModule) => configFilters.recommended(rule) && configFilters.rules(rule), - 'tests-recommended': (rule) => + 'tests-recommended': (rule: Rule.RuleModule) => configFilters.recommended(rule) && configFilters.tests(rule), }; @@ -93,34 +91,32 @@ const allRules = { 'require-meta-type': requireMetaType, 'test-case-property-ordering': testCasePropertyOrdering, 'test-case-shorthand-strings': testCaseShorthandStrings, -}; +} satisfies Record; -/** @type {import("eslint").ESLint.Plugin} */ const plugin = { meta: { name: packageMetadata.name, version: packageMetadata.version, }, rules: allRules, - configs: {}, // assigned later -}; - -// configs -Object.assign( - plugin.configs, - Object.keys(configFilters).reduce((configs, configName) => { + configs: CONFIG_NAMES.reduce((configs, configName) => { return Object.assign(configs, { [configName]: { name: `${PLUGIN_NAME}/${configName}`, - plugins: { [PLUGIN_NAME]: plugin }, + plugins: { + get PLUGIN_NAME() { + return plugin; + } + }, rules: Object.fromEntries( - Object.keys(allRules) + (Object.keys(allRules) as (keyof typeof allRules)[]) .filter((ruleName) => configFilters[configName](allRules[ruleName])) .map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']), ), }, }); - }, {}), -); + }, {}) +} satisfies ESLint.Plugin; + export default plugin; diff --git a/tsconfig.json b/tsconfig.json index 3607b466..1411ce1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "forceConsistentCasingInFileNames": true, "paths": { "eslint-plugin-eslint-plugin": ["./lib/index.ts"] - } + }, + "types": ["eslint-scope", "espree", "estree", "lodash", "node"] }, "include": ["lib/**/*", "tests/**/*", "types/**/*"] } From 0fe29c2e9968774046afd172ab6a59abee5b920b Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 05:26:50 -0500 Subject: [PATCH 39/82] migrate indext.ts test --- tests/lib/index.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/lib/index.ts b/tests/lib/index.ts index 96f52e44..0eeda49d 100644 --- a/tests/lib/index.ts +++ b/tests/lib/index.ts @@ -1,18 +1,13 @@ -import { assert, describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import plugin from '../../lib/index.js'; -const RULE_NAMES = Object.keys(plugin.rules); - describe('exported plugin', () => { describe('has a meta.docs.url property on each rule', () => { - RULE_NAMES.forEach((ruleName) => { - it(ruleName, () => { - assert.match( - plugin.rules[ruleName].meta.docs.url, - /^https:\/\/github.com\/eslint-community\/eslint-plugin-eslint-plugin\/tree\/HEAD\/docs\/rules\/[\w-]+\.md$/, - ); - }); - }); + it.each(Object.entries(plugin.rules))('$0', (_, rule) => + expect(rule.meta?.docs?.url).toMatch( + /^https:\/\/github.com\/eslint-community\/eslint-plugin-eslint-plugin\/tree\/HEAD\/docs\/rules\/[\w-]+\.md$/, + ), + ); }); }); From b1ea5c490d7da5e5fc4ce82c95a477ba45a08633 Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 05:33:14 -0500 Subject: [PATCH 40/82] git mv all rule tests --- tests/lib/rules/{consistent-output.js => consistent-output.ts} | 0 tests/lib/rules/{fixer-return.js => fixer-return.ts} | 0 .../{meta-property-ordering.js => meta-property-ordering.ts} | 0 ...ecated-context-methods.js => no-deprecated-context-methods.ts} | 0 .../{no-deprecated-report-api.js => no-deprecated-report-api.ts} | 0 tests/lib/rules/{no-identical-tests.js => no-identical-tests.ts} | 0 .../lib/rules/{no-meta-replaced-by.js => no-meta-replaced-by.ts} | 0 .../{no-meta-schema-default.js => no-meta-schema-default.ts} | 0 .../{no-missing-message-ids.js => no-missing-message-ids.ts} | 0 .../{no-missing-placeholders.js => no-missing-placeholders.ts} | 0 tests/lib/rules/{no-only-tests.js => no-only-tests.ts} | 0 .../lib/rules/{no-property-in-node.js => no-property-in-node.ts} | 0 .../rules/{no-unused-message-ids.js => no-unused-message-ids.ts} | 0 .../{no-unused-placeholders.js => no-unused-placeholders.ts} | 0 .../{no-useless-token-range.js => no-useless-token-range.ts} | 0 tests/lib/rules/{prefer-message-ids.js => prefer-message-ids.ts} | 0 tests/lib/rules/{prefer-object-rule.js => prefer-object-rule.ts} | 0 tests/lib/rules/{prefer-output-null.js => prefer-output-null.ts} | 0 .../lib/rules/{prefer-placeholders.js => prefer-placeholders.ts} | 0 .../lib/rules/{prefer-replace-text.js => prefer-replace-text.ts} | 0 .../rules/{report-message-format.js => report-message-format.ts} | 0 ...re-meta-default-options.js => require-meta-default-options.ts} | 0 ...-meta-docs-description.js => require-meta-docs-description.ts} | 0 ...-meta-docs-recommended.js => require-meta-docs-recommended.ts} | 0 .../rules/{require-meta-docs-url.js => require-meta-docs-url.ts} | 0 .../rules/{require-meta-fixable.js => require-meta-fixable.ts} | 0 ...re-meta-has-suggestions.js => require-meta-has-suggestions.ts} | 0 ...a-schema-description.js => require-meta-schema-description.ts} | 0 .../lib/rules/{require-meta-schema.js => require-meta-schema.ts} | 0 tests/lib/rules/{require-meta-type.js => require-meta-type.ts} | 0 ...t-case-property-ordering.js => test-case-property-ordering.ts} | 0 ...t-case-shorthand-strings.js => test-case-shorthand-strings.ts} | 0 32 files changed, 0 insertions(+), 0 deletions(-) rename tests/lib/rules/{consistent-output.js => consistent-output.ts} (100%) rename tests/lib/rules/{fixer-return.js => fixer-return.ts} (100%) rename tests/lib/rules/{meta-property-ordering.js => meta-property-ordering.ts} (100%) rename tests/lib/rules/{no-deprecated-context-methods.js => no-deprecated-context-methods.ts} (100%) rename tests/lib/rules/{no-deprecated-report-api.js => no-deprecated-report-api.ts} (100%) rename tests/lib/rules/{no-identical-tests.js => no-identical-tests.ts} (100%) rename tests/lib/rules/{no-meta-replaced-by.js => no-meta-replaced-by.ts} (100%) rename tests/lib/rules/{no-meta-schema-default.js => no-meta-schema-default.ts} (100%) rename tests/lib/rules/{no-missing-message-ids.js => no-missing-message-ids.ts} (100%) rename tests/lib/rules/{no-missing-placeholders.js => no-missing-placeholders.ts} (100%) rename tests/lib/rules/{no-only-tests.js => no-only-tests.ts} (100%) rename tests/lib/rules/{no-property-in-node.js => no-property-in-node.ts} (100%) rename tests/lib/rules/{no-unused-message-ids.js => no-unused-message-ids.ts} (100%) rename tests/lib/rules/{no-unused-placeholders.js => no-unused-placeholders.ts} (100%) rename tests/lib/rules/{no-useless-token-range.js => no-useless-token-range.ts} (100%) rename tests/lib/rules/{prefer-message-ids.js => prefer-message-ids.ts} (100%) rename tests/lib/rules/{prefer-object-rule.js => prefer-object-rule.ts} (100%) rename tests/lib/rules/{prefer-output-null.js => prefer-output-null.ts} (100%) rename tests/lib/rules/{prefer-placeholders.js => prefer-placeholders.ts} (100%) rename tests/lib/rules/{prefer-replace-text.js => prefer-replace-text.ts} (100%) rename tests/lib/rules/{report-message-format.js => report-message-format.ts} (100%) rename tests/lib/rules/{require-meta-default-options.js => require-meta-default-options.ts} (100%) rename tests/lib/rules/{require-meta-docs-description.js => require-meta-docs-description.ts} (100%) rename tests/lib/rules/{require-meta-docs-recommended.js => require-meta-docs-recommended.ts} (100%) rename tests/lib/rules/{require-meta-docs-url.js => require-meta-docs-url.ts} (100%) rename tests/lib/rules/{require-meta-fixable.js => require-meta-fixable.ts} (100%) rename tests/lib/rules/{require-meta-has-suggestions.js => require-meta-has-suggestions.ts} (100%) rename tests/lib/rules/{require-meta-schema-description.js => require-meta-schema-description.ts} (100%) rename tests/lib/rules/{require-meta-schema.js => require-meta-schema.ts} (100%) rename tests/lib/rules/{require-meta-type.js => require-meta-type.ts} (100%) rename tests/lib/rules/{test-case-property-ordering.js => test-case-property-ordering.ts} (100%) rename tests/lib/rules/{test-case-shorthand-strings.js => test-case-shorthand-strings.ts} (100%) diff --git a/tests/lib/rules/consistent-output.js b/tests/lib/rules/consistent-output.ts similarity index 100% rename from tests/lib/rules/consistent-output.js rename to tests/lib/rules/consistent-output.ts diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.ts similarity index 100% rename from tests/lib/rules/fixer-return.js rename to tests/lib/rules/fixer-return.ts diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.ts similarity index 100% rename from tests/lib/rules/meta-property-ordering.js rename to tests/lib/rules/meta-property-ordering.ts diff --git a/tests/lib/rules/no-deprecated-context-methods.js b/tests/lib/rules/no-deprecated-context-methods.ts similarity index 100% rename from tests/lib/rules/no-deprecated-context-methods.js rename to tests/lib/rules/no-deprecated-context-methods.ts diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.ts similarity index 100% rename from tests/lib/rules/no-deprecated-report-api.js rename to tests/lib/rules/no-deprecated-report-api.ts diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.ts similarity index 100% rename from tests/lib/rules/no-identical-tests.js rename to tests/lib/rules/no-identical-tests.ts diff --git a/tests/lib/rules/no-meta-replaced-by.js b/tests/lib/rules/no-meta-replaced-by.ts similarity index 100% rename from tests/lib/rules/no-meta-replaced-by.js rename to tests/lib/rules/no-meta-replaced-by.ts diff --git a/tests/lib/rules/no-meta-schema-default.js b/tests/lib/rules/no-meta-schema-default.ts similarity index 100% rename from tests/lib/rules/no-meta-schema-default.js rename to tests/lib/rules/no-meta-schema-default.ts diff --git a/tests/lib/rules/no-missing-message-ids.js b/tests/lib/rules/no-missing-message-ids.ts similarity index 100% rename from tests/lib/rules/no-missing-message-ids.js rename to tests/lib/rules/no-missing-message-ids.ts diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.ts similarity index 100% rename from tests/lib/rules/no-missing-placeholders.js rename to tests/lib/rules/no-missing-placeholders.ts diff --git a/tests/lib/rules/no-only-tests.js b/tests/lib/rules/no-only-tests.ts similarity index 100% rename from tests/lib/rules/no-only-tests.js rename to tests/lib/rules/no-only-tests.ts diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.ts similarity index 100% rename from tests/lib/rules/no-property-in-node.js rename to tests/lib/rules/no-property-in-node.ts diff --git a/tests/lib/rules/no-unused-message-ids.js b/tests/lib/rules/no-unused-message-ids.ts similarity index 100% rename from tests/lib/rules/no-unused-message-ids.js rename to tests/lib/rules/no-unused-message-ids.ts diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.ts similarity index 100% rename from tests/lib/rules/no-unused-placeholders.js rename to tests/lib/rules/no-unused-placeholders.ts diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.ts similarity index 100% rename from tests/lib/rules/no-useless-token-range.js rename to tests/lib/rules/no-useless-token-range.ts diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.ts similarity index 100% rename from tests/lib/rules/prefer-message-ids.js rename to tests/lib/rules/prefer-message-ids.ts diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.ts similarity index 100% rename from tests/lib/rules/prefer-object-rule.js rename to tests/lib/rules/prefer-object-rule.ts diff --git a/tests/lib/rules/prefer-output-null.js b/tests/lib/rules/prefer-output-null.ts similarity index 100% rename from tests/lib/rules/prefer-output-null.js rename to tests/lib/rules/prefer-output-null.ts diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.ts similarity index 100% rename from tests/lib/rules/prefer-placeholders.js rename to tests/lib/rules/prefer-placeholders.ts diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.ts similarity index 100% rename from tests/lib/rules/prefer-replace-text.js rename to tests/lib/rules/prefer-replace-text.ts diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.ts similarity index 100% rename from tests/lib/rules/report-message-format.js rename to tests/lib/rules/report-message-format.ts diff --git a/tests/lib/rules/require-meta-default-options.js b/tests/lib/rules/require-meta-default-options.ts similarity index 100% rename from tests/lib/rules/require-meta-default-options.js rename to tests/lib/rules/require-meta-default-options.ts diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.ts similarity index 100% rename from tests/lib/rules/require-meta-docs-description.js rename to tests/lib/rules/require-meta-docs-description.ts diff --git a/tests/lib/rules/require-meta-docs-recommended.js b/tests/lib/rules/require-meta-docs-recommended.ts similarity index 100% rename from tests/lib/rules/require-meta-docs-recommended.js rename to tests/lib/rules/require-meta-docs-recommended.ts diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.ts similarity index 100% rename from tests/lib/rules/require-meta-docs-url.js rename to tests/lib/rules/require-meta-docs-url.ts diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.ts similarity index 100% rename from tests/lib/rules/require-meta-fixable.js rename to tests/lib/rules/require-meta-fixable.ts diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.ts similarity index 100% rename from tests/lib/rules/require-meta-has-suggestions.js rename to tests/lib/rules/require-meta-has-suggestions.ts diff --git a/tests/lib/rules/require-meta-schema-description.js b/tests/lib/rules/require-meta-schema-description.ts similarity index 100% rename from tests/lib/rules/require-meta-schema-description.js rename to tests/lib/rules/require-meta-schema-description.ts diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.ts similarity index 100% rename from tests/lib/rules/require-meta-schema.js rename to tests/lib/rules/require-meta-schema.ts diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.ts similarity index 100% rename from tests/lib/rules/require-meta-type.js rename to tests/lib/rules/require-meta-type.ts diff --git a/tests/lib/rules/test-case-property-ordering.js b/tests/lib/rules/test-case-property-ordering.ts similarity index 100% rename from tests/lib/rules/test-case-property-ordering.js rename to tests/lib/rules/test-case-property-ordering.ts diff --git a/tests/lib/rules/test-case-shorthand-strings.js b/tests/lib/rules/test-case-shorthand-strings.ts similarity index 100% rename from tests/lib/rules/test-case-shorthand-strings.js rename to tests/lib/rules/test-case-shorthand-strings.ts From 02e1d60c97b7b417d370527274a1b218df30834c Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 06:07:48 -0500 Subject: [PATCH 41/82] fix type issues in no-meta-replaced-by test --- tests/lib/fixtures/tsconfig.json | 1 + tests/lib/rules/no-meta-replaced-by.ts | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/lib/fixtures/tsconfig.json b/tests/lib/fixtures/tsconfig.json index 403ce01b..baf78709 100644 --- a/tests/lib/fixtures/tsconfig.json +++ b/tests/lib/fixtures/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "module": "NodeNext", "moduleResolution": "NodeNext" }, "include": ["*.ts"] diff --git a/tests/lib/rules/no-meta-replaced-by.ts b/tests/lib/rules/no-meta-replaced-by.ts index 9ec810fa..f27fa93d 100644 --- a/tests/lib/rules/no-meta-replaced-by.ts +++ b/tests/lib/rules/no-meta-replaced-by.ts @@ -13,7 +13,7 @@ import { RuleTester } from 'eslint'; // Tests // ------------------------------------------------------------------------------ -const valid = [ +const valid: string[] = [ 'module.exports = {};', ` module.exports = { @@ -34,8 +34,7 @@ const valid = [ create(context) {}, }; `, - { - code: ` + ` module.exports = { meta: { deprecated: { @@ -51,11 +50,9 @@ const valid = [ create(context) {}, }; `, - errors: 0, - }, ]; -const invalid = [ +const invalid: RuleTester.InvalidTestCase[] = [ { code: ` module.exports = { @@ -109,7 +106,13 @@ const invalid = [ }, ]; -const testToESM = (test) => { +type ValidTest = (typeof valid)[number]; +type InvalidTest = (typeof invalid)[number]; +type TestCase = ValidTest | InvalidTest; + +function testToESM(test: ValidTest): ValidTest; +function testToESM(test: InvalidTest): InvalidTest; +function testToESM(test: TestCase): TestCase { if (typeof test === 'string') { return test.replace('module.exports =', 'export default'); } @@ -120,7 +123,7 @@ const testToESM = (test) => { ...test, code, }; -}; +} new RuleTester({ languageOptions: { sourceType: 'commonjs' }, @@ -132,6 +135,6 @@ new RuleTester({ new RuleTester({ languageOptions: { sourceType: 'module' }, }).run('no-meta-replaced-by', rule, { - valid: valid.map(testToESM), - invalid: invalid.map(testToESM), + valid: valid.map((testCase) => testToESM(testCase)), + invalid: invalid.map((testCase) => testToESM(testCase)), }); From 6077cd4243825694eca6a4f8894526a61bcd065b Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 20:13:24 -0500 Subject: [PATCH 42/82] fix type issues with no-missing-placeholders tests --- tests/lib/rules/no-missing-placeholders.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/lib/rules/no-missing-placeholders.ts b/tests/lib/rules/no-missing-placeholders.ts index 2791fee2..f725590e 100644 --- a/tests/lib/rules/no-missing-placeholders.ts +++ b/tests/lib/rules/no-missing-placeholders.ts @@ -12,10 +12,14 @@ import { RuleTester } from 'eslint'; /** * Create an error for the given key - * @param {string} missingKey The placeholder that is missing - * @returns {object} An expected error + * @param missingKey The placeholder that is missing + * @returns An expected error */ -function error(missingKey, type, extra) { +function error( + missingKey: string, + type?: string, + extra?: Partial, +): RuleTester.TestCaseError { return { type, message: `The placeholder {{${missingKey}}} is missing (must provide it in the report's \`data\` object).`, From c460affda6f37d67b566c568b49c1b708569aac4 Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 20:15:25 -0500 Subject: [PATCH 43/82] fix type issues with no-unused-placeholders tests --- tests/lib/rules/no-unused-placeholders.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/lib/rules/no-unused-placeholders.ts b/tests/lib/rules/no-unused-placeholders.ts index b9d55158..9f16166b 100644 --- a/tests/lib/rules/no-unused-placeholders.ts +++ b/tests/lib/rules/no-unused-placeholders.ts @@ -12,10 +12,13 @@ import { RuleTester } from 'eslint'; /** * Create an error for the given key - * @param {string} unusedKey The placeholder that is unused - * @returns {object} An expected error + * @param unusedKey The placeholder that is unused + * @returns An expected error */ -function error(unusedKey, extra) { +function error( + unusedKey: string, + extra?: Partial, +): RuleTester.TestCaseError { return { type: 'Property', // The property in the report's `data` object for the unused placeholder. message: `The placeholder {{${unusedKey}}} is unused (does not exist in the actual message).`, From aa681ad9781a7120fc2ad095a6784e31b5605b4e Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 20:16:05 -0500 Subject: [PATCH 44/82] fix type issues with no-useless-token-range tests --- tests/lib/rules/no-useless-token-range.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/lib/rules/no-useless-token-range.ts b/tests/lib/rules/no-useless-token-range.ts index 7b6f44e7..72499c80 100644 --- a/tests/lib/rules/no-useless-token-range.ts +++ b/tests/lib/rules/no-useless-token-range.ts @@ -12,10 +12,10 @@ import { RuleTester } from 'eslint'; /** * Wraps a code sample as an eslint rule - * @param {string} code source text given a `sourceCode` variable - * @returns {string} rule code containing that source text + * @param code source text given a `sourceCode` variable + * @returns rule code containing that source text */ -function wrapRule(code) { +function wrapRule(code: string): string { return ` module.exports = { create(context) { From be5b2bd76d95d5e5f07d095e50e70f1db4a3669f Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 17 Jul 2025 20:17:27 -0500 Subject: [PATCH 45/82] fix type issues with report-message-format tests --- tests/lib/rules/report-message-format.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/report-message-format.ts b/tests/lib/rules/report-message-format.ts index be260092..dd7f9e61 100644 --- a/tests/lib/rules/report-message-format.ts +++ b/tests/lib/rules/report-message-format.ts @@ -218,7 +218,7 @@ ruleTester.run('report-message-format', rule, { }; `, options: ['foo'], - languageOptions: { sourceType: 'module' }, + languageOptions: { sourceType: 'module' as const }, }, { // With message as variable. From 96987faded3bfa1192291b617d91ea3dd554c5f6 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 18 Jul 2025 18:01:24 -0500 Subject: [PATCH 46/82] remove invalid case from valid array --- tests/lib/rules/require-meta-type.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/lib/rules/require-meta-type.ts b/tests/lib/rules/require-meta-type.ts index 7d44b041..535f3393 100644 --- a/tests/lib/rules/require-meta-type.ts +++ b/tests/lib/rules/require-meta-type.ts @@ -66,16 +66,6 @@ ruleTester.run('require-meta-type', rule, { create(context) {} }; `, - { - code: ` - const create = {}; - module.exports = { - meta: {}, - create, - }; - `, - errors: [{ messageId: 'missing' }], - }, // Spread. ` const extra = { type: 'problem' }; From 603573506b1529aa2a3a0a1cd9eead4f35016e54 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 18 Jul 2025 18:03:06 -0500 Subject: [PATCH 47/82] fix type issues with test-case-shorthand-strings --- tests/lib/rules/test-case-shorthand-strings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/rules/test-case-shorthand-strings.ts b/tests/lib/rules/test-case-shorthand-strings.ts index c65d0c18..1d2aea0d 100644 --- a/tests/lib/rules/test-case-shorthand-strings.ts +++ b/tests/lib/rules/test-case-shorthand-strings.ts @@ -12,10 +12,10 @@ import { RuleTester } from 'eslint'; /** * Returns the code for some valid test cases - * @param {string[]} cases The code representation of valid test cases + * @param cases The code representation of valid test cases * @returns {string} Code representing the test cases */ -function getTestCases(cases) { +function getTestCases(cases: string[]): string { return ` new RuleTester().run('foo', bar, { valid: [ From 9e11e4842c7f5d83e4bb355d1b1e5d51e18f967e Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 19 Jul 2025 16:55:54 -0500 Subject: [PATCH 48/82] fix type issues in rule-setup tests --- tests/lib/rule-setup.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/lib/rule-setup.ts b/tests/lib/rule-setup.ts index ab097bc3..9285f9e5 100644 --- a/tests/lib/rule-setup.ts +++ b/tests/lib/rule-setup.ts @@ -6,7 +6,9 @@ import { assert, describe, it } from 'vitest'; import plugin from '../../lib/index.js'; -const RULE_NAMES = Object.keys(plugin.rules); +const RULE_NAMES = Object.keys(plugin.rules) as Array< + keyof typeof plugin.rules +>; const dirname = path.dirname(fileURLToPath(import.meta.url)); describe('rule setup is correct', () => { @@ -29,7 +31,8 @@ describe('rule setup is correct', () => { it('has the right properties', () => { const ALLOWED_CATEGORIES = ['Rules', 'Tests']; assert.ok( - ALLOWED_CATEGORIES.includes(rule.meta.docs.category), + !rule.meta?.docs?.category || + ALLOWED_CATEGORIES.includes(rule.meta.docs.category), 'has an allowed category', ); }); From 1c64770987dbb036120be4bd6e7367c1c278f966 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 19 Jul 2025 16:56:29 -0500 Subject: [PATCH 49/82] Add explicit extensions to imports without them --- lib/index.ts | 89 +++++++++++++--------- lib/rules/consistent-output.ts | 2 +- lib/rules/fixer-return.ts | 2 +- lib/rules/no-deprecated-context-methods.ts | 2 +- lib/rules/no-identical-tests.ts | 2 +- lib/rules/no-meta-replaced-by.ts | 2 +- lib/rules/no-meta-schema-default.ts | 2 +- lib/rules/no-missing-message-ids.ts | 2 +- lib/rules/no-missing-placeholders.ts | 2 +- lib/rules/no-only-tests.ts | 2 +- lib/rules/no-unused-message-ids.ts | 2 +- lib/utils.ts | 2 +- 12 files changed, 62 insertions(+), 49 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index f720cb09..51172fcb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,47 +2,61 @@ * @fileoverview An ESLint plugin for linting ESLint plugins * @author Teddy Katz */ -import packageMetadata from '../package.json' with { type: 'json' }; -import consistentOutput from './rules/consistent-output'; -import fixerReturn from './rules/fixer-return'; -import metaPropertyOrdering from './rules/meta-property-ordering'; -import noDeprecatedContextMethods from './rules/no-deprecated-context-methods'; -import noDeprecatedReportApi from './rules/no-deprecated-report-api'; -import noIdenticalTests from './rules/no-identical-tests'; -import noMetaReplacedBy from './rules/no-meta-replaced-by'; -import noMetaSchemaDefault from './rules/no-meta-schema-default'; -import noMissingMessageIds from './rules/no-missing-message-ids'; -import noMissingPlaceholders from './rules/no-missing-placeholders'; -import noOnlyTests from './rules/no-only-tests'; -import noPropertyInNode from './rules/no-property-in-node'; -import noUnusedMessageIds from './rules/no-unused-message-ids'; -import noUnusedPlaceholders from './rules/no-unused-placeholders'; -import noUselessTokenRange from './rules/no-useless-token-range'; -import preferMessageIds from './rules/prefer-message-ids'; -import preferObjectRule from './rules/prefer-object-rule'; -import preferOutputNull from './rules/prefer-output-null'; -import preferPlaceholders from './rules/prefer-placeholders'; -import preferReplaceText from './rules/prefer-replace-text'; -import reportMessageFormat from './rules/report-message-format'; -import requireMetaDefaultOptions from './rules/require-meta-default-options'; -import requireMetaDocsDescription from './rules/require-meta-docs-description'; -import requireMetaDocsRecommended from './rules/require-meta-docs-recommended'; -import requireMetaDocsUrl from './rules/require-meta-docs-url'; -import requireMetaFixable from './rules/require-meta-fixable'; -import requireMetaHasSuggestions from './rules/require-meta-has-suggestions'; -import requireMetaSchemaDescription from './rules/require-meta-schema-description'; -import requireMetaSchema from './rules/require-meta-schema'; -import requireMetaType from './rules/require-meta-type'; -import testCasePropertyOrdering from './rules/test-case-property-ordering'; -import testCaseShorthandStrings from './rules/test-case-shorthand-strings'; import type { ESLint, Rule } from 'eslint'; +import packageMetadata from '../package.json' with { type: 'json' }; +import consistentOutput from './rules/consistent-output.js'; +import fixerReturn from './rules/fixer-return.js'; +import metaPropertyOrdering from './rules/meta-property-ordering.js'; +import noDeprecatedContextMethods from './rules/no-deprecated-context-methods.js'; +import noDeprecatedReportApi from './rules/no-deprecated-report-api.js'; +import noIdenticalTests from './rules/no-identical-tests.js'; +import noMetaReplacedBy from './rules/no-meta-replaced-by.js'; +import noMetaSchemaDefault from './rules/no-meta-schema-default.js'; +import noMissingMessageIds from './rules/no-missing-message-ids.js'; +import noMissingPlaceholders from './rules/no-missing-placeholders.js'; +import noOnlyTests from './rules/no-only-tests.js'; +import noPropertyInNode from './rules/no-property-in-node.js'; +import noUnusedMessageIds from './rules/no-unused-message-ids.js'; +import noUnusedPlaceholders from './rules/no-unused-placeholders.js'; +import noUselessTokenRange from './rules/no-useless-token-range.js'; +import preferMessageIds from './rules/prefer-message-ids.js'; +import preferObjectRule from './rules/prefer-object-rule.js'; +import preferOutputNull from './rules/prefer-output-null.js'; +import preferPlaceholders from './rules/prefer-placeholders.js'; +import preferReplaceText from './rules/prefer-replace-text.js'; +import reportMessageFormat from './rules/report-message-format.js'; +import requireMetaDefaultOptions from './rules/require-meta-default-options.js'; +import requireMetaDocsDescription from './rules/require-meta-docs-description.js'; +import requireMetaDocsRecommended from './rules/require-meta-docs-recommended.js'; +import requireMetaDocsUrl from './rules/require-meta-docs-url.js'; +import requireMetaFixable from './rules/require-meta-fixable.js'; +import requireMetaHasSuggestions from './rules/require-meta-has-suggestions.js'; +import requireMetaSchemaDescription from './rules/require-meta-schema-description.js'; +import requireMetaSchema from './rules/require-meta-schema.js'; +import requireMetaType from './rules/require-meta-type.js'; +import testCasePropertyOrdering from './rules/test-case-property-ordering.js'; +import testCaseShorthandStrings from './rules/test-case-shorthand-strings.js'; + const PLUGIN_NAME = packageMetadata.name.replace(/^eslint-plugin-/, ''); -const CONFIG_NAMES = ['all', 'all-type-checked', 'recommended', 'rules', 'tests', 'rules-recommended', 'tests-recommended'] as const; +const CONFIG_NAMES = [ + 'all', + 'all-type-checked', + 'recommended', + 'rules', + 'tests', + 'rules-recommended', + 'tests-recommended', +] as const; type ConfigName = (typeof CONFIG_NAMES)[number]; const configFilters: Record boolean> = { - all: (rule: Rule.RuleModule) => !(rule.meta?.docs && 'requiresTypeChecking' in rule.meta.docs && rule.meta.docs.requiresTypeChecking), + all: (rule: Rule.RuleModule) => + !( + rule.meta?.docs && + 'requiresTypeChecking' in rule.meta.docs && + rule.meta.docs.requiresTypeChecking + ), 'all-type-checked': () => true, recommended: (rule: Rule.RuleModule) => !!rule.meta?.docs?.recommended, rules: (rule: Rule.RuleModule) => rule.meta?.docs?.category === 'Rules', @@ -106,7 +120,7 @@ const plugin = { plugins: { get PLUGIN_NAME() { return plugin; - } + }, }, rules: Object.fromEntries( (Object.keys(allRules) as (keyof typeof allRules)[]) @@ -115,8 +129,7 @@ const plugin = { ), }, }); - }, {}) + }, {}), } satisfies ESLint.Plugin; - export default plugin; diff --git a/lib/rules/consistent-output.ts b/lib/rules/consistent-output.ts index a8c7900d..6495fec3 100644 --- a/lib/rules/consistent-output.ts +++ b/lib/rules/consistent-output.ts @@ -4,7 +4,7 @@ */ import type { Rule } from 'eslint'; -import { getKeyName, getTestInfo } from '../utils'; +import { getKeyName, getTestInfo } from '../utils.js'; const keyNameMapper = (property: Parameters[0]) => getKeyName(property); diff --git a/lib/rules/fixer-return.ts b/lib/rules/fixer-return.ts index 01aa41c4..fbaaa469 100644 --- a/lib/rules/fixer-return.ts +++ b/lib/rules/fixer-return.ts @@ -18,7 +18,7 @@ import { getContextIdentifiers, isAutoFixerFunction, isSuggestionFixerFunction, -} from '../utils'; +} from '../utils.js'; import type { FunctionInfo } from '../types'; const DEFAULT_FUNC_INFO: FunctionInfo = { diff --git a/lib/rules/no-deprecated-context-methods.ts b/lib/rules/no-deprecated-context-methods.ts index 74ff39c1..e5576219 100644 --- a/lib/rules/no-deprecated-context-methods.ts +++ b/lib/rules/no-deprecated-context-methods.ts @@ -4,7 +4,7 @@ */ import type { Rule } from 'eslint'; -import { getContextIdentifiers } from '../utils'; +import { getContextIdentifiers } from '../utils.js'; import type { Identifier, MemberExpression } from 'estree'; const DEPRECATED_PASSTHROUGHS = { diff --git a/lib/rules/no-identical-tests.ts b/lib/rules/no-identical-tests.ts index cbc32080..a9ba7f46 100644 --- a/lib/rules/no-identical-tests.ts +++ b/lib/rules/no-identical-tests.ts @@ -6,7 +6,7 @@ import type { Rule } from 'eslint'; import type { Expression, SpreadElement } from 'estree'; -import { getTestInfo } from '../utils'; +import { getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-meta-replaced-by.ts b/lib/rules/no-meta-replaced-by.ts index 6b595486..fb23f165 100644 --- a/lib/rules/no-meta-replaced-by.ts +++ b/lib/rules/no-meta-replaced-by.ts @@ -4,7 +4,7 @@ import type { Rule } from 'eslint'; -import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils'; +import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-meta-schema-default.ts b/lib/rules/no-meta-schema-default.ts index 59108030..cd749a76 100644 --- a/lib/rules/no-meta-schema-default.ts +++ b/lib/rules/no-meta-schema-default.ts @@ -5,7 +5,7 @@ import { getMetaSchemaNode, getMetaSchemaNodeProperty, getRuleInfo, -} from '../utils'; +} from '../utils.js'; import type { Expression, SpreadElement } from 'estree'; // ------------------------------------------------------------------------------ diff --git a/lib/rules/no-missing-message-ids.ts b/lib/rules/no-missing-message-ids.ts index f8a145a4..3ece7a2a 100644 --- a/lib/rules/no-missing-message-ids.ts +++ b/lib/rules/no-missing-message-ids.ts @@ -9,7 +9,7 @@ import { getMessagesNode, getReportInfo, getRuleInfo, -} from '../utils'; +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-missing-placeholders.ts b/lib/rules/no-missing-placeholders.ts index 474e64d2..0ee3353d 100644 --- a/lib/rules/no-missing-placeholders.ts +++ b/lib/rules/no-missing-placeholders.ts @@ -13,7 +13,7 @@ import { getMessagesNode, getReportInfo, getRuleInfo, -} from '../utils'; +} from '../utils.js'; import type { Node } from 'estree'; // ------------------------------------------------------------------------------ diff --git a/lib/rules/no-only-tests.ts b/lib/rules/no-only-tests.ts index ee4c3e9a..e35a22d3 100644 --- a/lib/rules/no-only-tests.ts +++ b/lib/rules/no-only-tests.ts @@ -5,7 +5,7 @@ import { } from '@eslint-community/eslint-utils'; import type { Rule } from 'eslint'; -import { getTestInfo } from '../utils'; +import { getTestInfo } from '../utils.js'; const rule: Rule.RuleModule = { meta: { diff --git a/lib/rules/no-unused-message-ids.ts b/lib/rules/no-unused-message-ids.ts index 482f244c..55483ca3 100644 --- a/lib/rules/no-unused-message-ids.ts +++ b/lib/rules/no-unused-message-ids.ts @@ -9,7 +9,7 @@ import { getReportInfo, getRuleInfo, isVariableFromParameter, -} from '../utils'; +} from '../utils.js'; import type { Identifier, Node } from 'estree'; // ------------------------------------------------------------------------------ diff --git a/lib/utils.ts b/lib/utils.ts index 0d10e710..e1c9c20d 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -30,7 +30,7 @@ import type { RuleInfo, TestInfo, ViolationAndSuppressionData, -} from './types'; +} from './types.js'; const functionTypes = new Set([ 'FunctionExpression', From 30a79eea6b4796ed2229615031c6121775d6e2cd Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 20 Jul 2025 17:58:02 -0500 Subject: [PATCH 50/82] fix type issues in utils tests --- tests/lib/utils.ts | 557 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 422 insertions(+), 135 deletions(-) diff --git a/tests/lib/utils.ts b/tests/lib/utils.ts index 22e2a539..30759e80 100644 --- a/tests/lib/utils.ts +++ b/tests/lib/utils.ts @@ -8,6 +8,39 @@ import lodash from 'lodash'; import { assert, describe, it } from 'vitest'; import * as utils from '../../lib/utils.js'; +import type { + ArrayExpression, + ArrowFunctionExpression, + AssignmentExpression, + AssignmentPattern, + BlockStatement, + CallExpression, + ExpressionStatement, + FunctionDeclaration, + FunctionExpression, + Identifier, + IfStatement, + Literal, + MemberExpression, + ObjectExpression, + Program, + Property, + SpreadElement, + VariableDeclaration, +} from 'estree'; +import type { Rule, Scope } from 'eslint'; +import type { RuleInfo } from '../../lib/types.js'; + +type MockRuleInfo = { + create: { + id?: { name: string }; + type: string; + }; + meta: { + type: string; + } | null; + isNewStyle: boolean; +}; describe('utils', () => { describe('getRuleInfo', () => { @@ -58,7 +91,10 @@ describe('utils', () => { 'const rule = { create: function() {} }; exports.rule = rule;', ].forEach((noRuleCase) => { it(`returns null for ${noRuleCase}`, () => { - const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true }); + const ast = espree.parse(noRuleCase, { + ecmaVersion: 8, + range: true, + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); assert.isNull( utils.getRuleInfo({ ast, scopeManager }), @@ -108,7 +144,7 @@ describe('utils', () => { ecmaVersion: 8, range: true, sourceType: 'module', - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); assert.isNull( utils.getRuleInfo({ ast, scopeManager }), @@ -139,7 +175,7 @@ describe('utils', () => { ecmaVersion: 8, range: true, sourceType: 'module', - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); assert.isNull( utils.getRuleInfo({ ast, scopeManager }), @@ -160,7 +196,7 @@ describe('utils', () => { const ast = typescriptEslintParser.parse(noRuleCase, { range: true, sourceType: 'script', - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); assert.isNull( utils.getRuleInfo({ ast, scopeManager }), @@ -171,7 +207,7 @@ describe('utils', () => { }); describe('the file has a valid rule (TypeScript + TypeScript parser + ESM)', () => { - const CASES = { + const CASES: Record = { // Util function only 'export default createESLintRule({ create() {}, meta: {} });': { @@ -336,11 +372,11 @@ describe('utils', () => { ecmaVersion: 6, range: true, sourceType: 'module', - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( - lodash.isMatch(ruleInfo, CASES[ruleSource]), + ruleInfo && lodash.isMatch(ruleInfo, CASES[ruleSource]), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect( CASES[ruleSource], )}`, @@ -350,7 +386,7 @@ describe('utils', () => { }); describe('the file has a valid rule (CJS)', () => { - const CASES = { + const CASES: Record = { 'module.exports = { create: function foo() {} };': { create: { type: 'FunctionExpression', id: { name: 'foo' } }, // (This property will actually contain the AST node.) meta: null, @@ -469,11 +505,11 @@ describe('utils', () => { ecmaVersion: 6, range: true, sourceType: 'script', - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( - lodash.isMatch(ruleInfo, CASES[ruleSource]), + ruleInfo && lodash.isMatch(ruleInfo, CASES[ruleSource]), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect( CASES[ruleSource], )}`, @@ -483,7 +519,7 @@ describe('utils', () => { }); describe('the file has a valid rule (ESM)', () => { - const CASES = { + const CASES: Record = { // ESM (object style) 'export default { create() {} }': { create: { type: 'FunctionExpression' }, @@ -558,11 +594,11 @@ describe('utils', () => { ecmaVersion: 6, range: true, sourceType: 'module', - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( - lodash.isMatch(ruleInfo, CASES[ruleSource]), + ruleInfo && lodash.isMatch(ruleInfo, CASES[ruleSource]), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect( CASES[ruleSource], )}`, @@ -581,7 +617,7 @@ describe('utils', () => { }, { ignoreEval: true, ecmaVersion: 6, sourceType: 'script' }, { ignoreEval: true, ecmaVersion: 6, sourceType: 'module' }, - ]) { + ] as eslintScope.AnalyzeOptions[]) { const ast = espree.parse( ` const create = (context) => {}; @@ -589,7 +625,7 @@ describe('utils', () => { module.exports = { create, meta }; `, { ecmaVersion: 6, range: true }, - ); + ) as unknown as Program; const expected = { create: { type: 'ArrowFunctionExpression' }, meta: { type: 'ObjectExpression' }, @@ -599,7 +635,7 @@ describe('utils', () => { const scopeManager = eslintScope.analyze(ast, scopeOptions); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( - lodash.isMatch(ruleInfo, expected), + ruleInfo && lodash.isMatch(ruleInfo, expected), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect(expected)}`, ); }); @@ -607,7 +643,11 @@ describe('utils', () => { }); describe('the file has newer syntax', () => { - const CASES = [ + const CASES: { + source: string; + options: { sourceType: 'script' | 'module' }; + expected: MockRuleInfo; + }[] = [ { source: 'module.exports = function(context) { class Foo { @someDecorator() someProp }; return {}; };', @@ -635,11 +675,11 @@ describe('utils', () => { const ast = typescriptEslintParser.parse( testCase.source, testCase.options, - ); + ) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert( - lodash.isMatch(ruleInfo, testCase.expected), + ruleInfo && lodash.isMatch(ruleInfo, testCase.expected), `Expected \n${inspect(ruleInfo)}\nto match\n${inspect( testCase.expected, )}`, @@ -651,43 +691,71 @@ describe('utils', () => { }); describe('getContextIdentifiers', () => { - const CASES = { + type ContextIdentifierMapFn = (ast: Program) => Identifier[]; + const CASES: Record = { 'module.exports = context => { context; context; context; return {}; }'( ast, ) { + const expression = (ast.body[0] as ExpressionStatement) + .expression as AssignmentExpression; + const blockStatement = (expression.right as ArrowFunctionExpression) + .body as BlockStatement; return [ - ast.body[0].expression.right.body.body[0].expression, - ast.body[0].expression.right.body.body[1].expression, - ast.body[0].expression.right.body.body[2].expression, + (blockStatement.body[0] as ExpressionStatement) + .expression as Identifier, + (blockStatement.body[0] as ExpressionStatement) + .expression as Identifier, + (blockStatement.body[0] as ExpressionStatement) + .expression as Identifier, ]; }, 'module.exports = { meta: {}, create(context, foo = context) {} }'(ast) { + const expression = (ast.body[0] as ExpressionStatement) + .expression as AssignmentExpression; + const functionExpression = ( + (expression.right as ObjectExpression).properties[1] as Property + ).value as FunctionExpression; return [ - ast.body[0].expression.right.properties[1].value.params[1].right, + (functionExpression.params[1] as AssignmentPattern) + .right as Identifier, ]; }, 'module.exports = { meta: {}, create(notContext) { notContext; notContext; notContext; } }'( ast, ) { + const expression = (ast.body[0] as ExpressionStatement) + .expression as AssignmentExpression; + const functionExpression = ( + (expression.right as ObjectExpression).properties[1] as Property + ).value as FunctionExpression; return [ - ast.body[0].expression.right.properties[1].value.body.body[0] - .expression, - ast.body[0].expression.right.properties[1].value.body.body[1] - .expression, - ast.body[0].expression.right.properties[1].value.body.body[2] - .expression, + (functionExpression.body.body[0] as ExpressionStatement) + .expression as Identifier, + (functionExpression.body.body[1] as ExpressionStatement) + .expression as Identifier, + (functionExpression.body.body[2] as ExpressionStatement) + .expression as Identifier, ]; }, 'const create = function(context) { context }; module.exports = { meta: {}, create };'( ast, ) { - return [ast.body[0].declarations[0].init.body.body[0].expression]; + const declaration = ast.body[0] as VariableDeclaration; + const functionExpression = declaration.declarations[0] + .init as FunctionExpression; + return [ + (functionExpression?.body.body[0] as ExpressionStatement) + .expression as Identifier, + ]; }, }; Object.keys(CASES).forEach((ruleSource) => { it(ruleSource, () => { - const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(ruleSource, { + ecmaVersion: 6, + range: true, + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast, { ignoreEval: true, ecmaVersion: 6, @@ -713,7 +781,16 @@ describe('utils', () => { }); describe('getKeyName', () => { - const CASES = { + const CASES: Record< + string, + | string + | null + | { + getNode: (ast: Program) => Property | SpreadElement; + result: string; + resultWithoutScope?: string | null; + } + > = { '({ foo: 1 })': 'foo', '({ "foo": 1 })': 'foo', '({ ["foo"]: 1 })': 'foo', @@ -730,7 +807,9 @@ describe('utils', () => { '({ [key]: 1 })': null, 'const key = "foo"; ({ [key]: 1 });': { getNode(ast) { - return ast.body[1].expression.properties[0]; + const expression = (ast.body[1] as ExpressionStatement) + .expression as ObjectExpression; + return expression.properties[0]; }, result: 'foo', resultWithoutScope: null, @@ -738,7 +817,10 @@ describe('utils', () => { }; Object.keys(CASES).forEach((objectSource) => { it(objectSource, () => { - const ast = espree.parse(objectSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(objectSource, { + ecmaVersion: 6, + range: true, + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast, { ignoreEval: true, ecmaVersion: 6, @@ -764,9 +846,11 @@ describe('utils', () => { ); } } else { + const expression = (ast.body[0] as ExpressionStatement) + .expression as ObjectExpression; assert.strictEqual( utils.getKeyName( - ast.body[0].expression.properties[0], + expression.properties[0], scopeManager.globalScope, ), caseInfo, @@ -775,15 +859,20 @@ describe('utils', () => { }); }); - const CASES_ES9 = { + const CASES_ES9: Record = { '({ ...foo })': null, }; Object.keys(CASES_ES9).forEach((objectSource) => { it(objectSource, () => { - const ast = espree.parse(objectSource, { ecmaVersion: 9, range: true }); + const ast = espree.parse(objectSource, { + ecmaVersion: 9, + range: true, + }) as unknown as Program; + const expression = (ast.body[0] as ExpressionStatement) + .expression as ObjectExpression; assert.strictEqual( - utils.getKeyName(ast.body[0].expression.properties[0]), + utils.getKeyName(expression.properties[0]), CASES_ES9[objectSource], ); }); @@ -808,7 +897,7 @@ describe('utils', () => { const ast = espree.parse(noTestsCase, { ecmaVersion: 8, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast, { ignoreEval: true, ecmaVersion: 6, @@ -820,7 +909,7 @@ describe('utils', () => { getDeclaredVariables: scopeManager.getDeclaredVariables.bind(scopeManager), }, - }; // mock object + } as unknown as Rule.RuleContext; // mock object assert.deepEqual( utils.getTestInfo(context, ast), [], @@ -831,7 +920,7 @@ describe('utils', () => { }); describe('the file has valid tests', () => { - const CASES = { + const CASES: Record = { 'new RuleTester().run(bar, baz, { valid: [foo], invalid: [bar, baz] })': { valid: 1, invalid: 2 }, 'var foo = new RuleTester(); foo.run(bar, baz, { valid: [foo], invalid: [bar] })': @@ -880,7 +969,10 @@ describe('utils', () => { Object.keys(CASES).forEach((testSource) => { it(testSource, () => { - const ast = espree.parse(testSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(testSource, { + ecmaVersion: 6, + range: true, + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast, { ignoreEval: true, ecmaVersion: 6, @@ -892,7 +984,7 @@ describe('utils', () => { getDeclaredVariables: scopeManager.getDeclaredVariables.bind(scopeManager), }, - }; // mock object + } as unknown as Rule.RuleContext; // mock object const testInfo = utils.getTestInfo(context, ast); assert.strictEqual( @@ -917,7 +1009,7 @@ describe('utils', () => { }); describe('the file has multiple test runs', () => { - const CASES = { + const CASES: Record = { [` new RuleTester().run(foo, bar, { valid: [foo], invalid: [] }); new RuleTester().run(foo, bar, { valid: [], invalid: [foo, bar] }); @@ -1080,7 +1172,10 @@ describe('utils', () => { Object.keys(CASES).forEach((testSource) => { it(testSource, () => { - const ast = espree.parse(testSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(testSource, { + ecmaVersion: 6, + range: true, + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast, { ignoreEval: true, ecmaVersion: 6, @@ -1092,7 +1187,7 @@ describe('utils', () => { getDeclaredVariables: scopeManager.getDeclaredVariables.bind(scopeManager), }, - }; // mock object + } as unknown as Rule.RuleContext; // mock object const testInfo = utils.getTestInfo(context, ast); assert.strictEqual( @@ -1123,7 +1218,28 @@ describe('utils', () => { }); describe('getReportInfo', () => { - const CASES = new Map([ + type GetReportInfoFn = { + (args: readonly (Identifier | ObjectExpression)[]): { + node: Identifier | ObjectExpression; + message: Identifier | ObjectExpression; + data: Identifier | ObjectExpression; + fix: Identifier | ObjectExpression; + loc?: Identifier | ObjectExpression; + }; + (): null; + (): { + node: { type: string; name: string; start: number; end: number }; + message: { + type: string; + name: string; + start: number; + end: number; + }; + }; + }; + + // @ts-expect-error - These types need some more work + const CASES = new Map([ [[], () => null], [['foo', 'bar'], () => null], [ @@ -1166,28 +1282,30 @@ describe('utils', () => { for (const args of CASES.keys()) { it(args.join(', '), () => { - const node = espree.parse(`context.report(${args.join(', ')})`, { + const program = espree.parse(`context.report(${args.join(', ')})`, { ecmaVersion: 6, loc: false, range: false, - }).body[0].expression; - const parsedArgs = node.arguments; + }) as unknown as Program; + const node = (program.body[0] as ExpressionStatement) + .expression as CallExpression; + const parsedArgs = node.arguments as (Identifier | ObjectExpression)[]; const context = { sourceCode: { getScope() { return {}; }, }, - }; // mock object + } as unknown as Rule.RuleContext; // mock object const reportInfo = utils.getReportInfo(node, context); - assert.deepEqual(reportInfo, CASES.get(args)(parsedArgs)); + assert.deepEqual(reportInfo, CASES.get(args)?.(parsedArgs)); }); } }); describe('getSourceCodeIdentifiers', () => { - const CASES = { + const CASES: Record = { 'module.exports = context => { const sourceCode = context.getSourceCode(); sourceCode; foo; return {}; }': 2, 'module.exports = context => { const x = 1, sc = context.getSourceCode(); sc; sc; sc; sourceCode; return {}; }': 4, 'module.exports = context => { const sourceCode = context.getNotSourceCode(); return {}; }': 0, @@ -1195,7 +1313,10 @@ describe('utils', () => { Object.keys(CASES).forEach((testSource) => { it(testSource, () => { - const ast = espree.parse(testSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(testSource, { + ecmaVersion: 6, + range: true, + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast, { ignoreEval: true, ecmaVersion: 6, @@ -1205,7 +1326,9 @@ describe('utils', () => { estraverse.traverse(ast, { enter(node, parent) { - node.parent = parent; + if (parent) { + node.parent = parent; + } }, }); @@ -1218,7 +1341,17 @@ describe('utils', () => { }); describe('collectReportViolationAndSuggestionData', () => { - const CASES = [ + type Data = { + message?: { type: string; value: string }; + messageId?: { type: string; value: string }; + data?: { type: string; properties?: { key: { name: string } }[] }; + fix?: { type: string }; + }; + type TestCase = { + code: string; + shouldMatch: Data[]; + }; + const CASES: TestCase[] = [ { // One suggestion. code: ` @@ -1350,19 +1483,22 @@ describe('utils', () => { const ast = espree.parse(testCase.code, { ecmaVersion: 6, range: true, - }); + }) as unknown as Program; const context = { sourceCode: { getScope() { return {}; }, }, - }; // mock object - const reportNode = ast.body[0].expression; + } as unknown as Rule.RuleContext; // mock object + const reportNode = (ast.body[0] as ExpressionStatement) + .expression as CallExpression; const reportInfo = utils.getReportInfo(reportNode, context); - const data = utils.collectReportViolationAndSuggestionData(reportInfo); + const data = + reportInfo && + utils.collectReportViolationAndSuggestionData(reportInfo); assert( - lodash.isMatch(data, testCase.shouldMatch), + data && lodash.isMatch(data, testCase.shouldMatch), `Expected \n${inspect(data)}\nto match\n${inspect( testCase.shouldMatch, )}`, @@ -1372,36 +1508,55 @@ describe('utils', () => { }); describe('isAutoFixerFunction / isSuggestionFixerFunction', () => { - const CASES = { + type TestCase = { + expected: boolean; + node: ArrayExpression | FunctionExpression; + context: Identifier | undefined; + fn: + | typeof utils.isAutoFixerFunction + | typeof utils.isSuggestionFixerFunction; + }; + + const getReportCallExpression = (ast: Program): CallExpression => + (ast.body[0] as ExpressionStatement).expression as CallExpression; + const getReportParamObjectExpression = (ast: Program): ObjectExpression => + getReportCallExpression(ast).arguments[0] as ObjectExpression; + const getReportParamObjectProperty = (ast: Program): Property => + getReportParamObjectExpression(ast).properties[0] as Property; + const getReportCalleeIdentifier = (ast: Program): Identifier => + (getReportCallExpression(ast).callee as MemberExpression) + .object as Identifier; + + const CASES: Record TestCase> = { // isAutoFixerFunction 'context.report({ fix(fixer) {} });'(ast) { return { expected: true, - node: ast.body[0].expression.arguments[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: getReportParamObjectProperty(ast).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isAutoFixerFunction, }; }, 'context.notReport({ fix(fixer) {} });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: getReportParamObjectProperty(ast).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isAutoFixerFunction, }; }, 'context.report({ notFix(fixer) {} });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: getReportParamObjectProperty(ast).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isAutoFixerFunction, }; }, 'notContext.report({ notFix(fixer) {} });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value, + node: getReportParamObjectProperty(ast).value as FunctionExpression, context: undefined, fn: utils.isAutoFixerFunction, }; @@ -1411,43 +1566,59 @@ describe('utils', () => { 'context.report({ suggest: [{ fix(fixer) {} }] });'(ast) { return { expected: true, - node: ast.body[0].expression.arguments[0].properties[0].value - .elements[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: ( + ( + (getReportParamObjectProperty(ast).value as ArrayExpression) + .elements[0] as ObjectExpression + ).properties[0] as Property + ).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isSuggestionFixerFunction, }; }, 'context.notReport({ suggest: [{ fix(fixer) {} }] });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value - .elements[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: ( + ( + (getReportParamObjectProperty(ast).value as ArrayExpression) + .elements[0] as ObjectExpression + ).properties[0] as Property + ).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isSuggestionFixerFunction, }; }, 'context.report({ notSuggest: [{ fix(fixer) {} }] });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value - .elements[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: ( + ( + (getReportParamObjectProperty(ast).value as ArrayExpression) + .elements[0] as ObjectExpression + ).properties[0] as Property + ).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isSuggestionFixerFunction, }; }, 'context.report({ suggest: [{ notFix(fixer) {} }] });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value - .elements[0].properties[0].value, - context: ast.body[0].expression.callee.object, + node: ( + ( + (getReportParamObjectProperty(ast).value as ArrayExpression) + .elements[0] as ObjectExpression + ).properties[0] as Property + ).value as FunctionExpression, + context: getReportCalleeIdentifier(ast), fn: utils.isSuggestionFixerFunction, }; }, 'notContext.report({ suggest: [{ fix(fixer) {} }] });'(ast) { return { expected: false, - node: ast.body[0].expression.arguments[0].properties[0].value, + node: getReportParamObjectProperty(ast).value as ArrayExpression, context: undefined, fn: utils.isSuggestionFixerFunction, }; @@ -1456,18 +1627,32 @@ describe('utils', () => { Object.keys(CASES).forEach((ruleSource) => { it(ruleSource, () => { - const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true }); + const ast = espree.parse(ruleSource, { + ecmaVersion: 6, + range: true, + }) as unknown as Program; + const context = { + sourceCode: { + getScope() { + return {}; + }, + }, + } as unknown as Rule.RuleContext; // mock object // Add parent to each node. estraverse.traverse(ast, { enter(node, parent) { - node.parent = parent; + if (parent) { + node.parent = parent; + } }, }); const testCase = CASES[ruleSource](ast); - const contextIdentifiers = new Set([testCase.context]); - const result = testCase.fn(testCase.node, contextIdentifiers); + const contextIdentifiers = new Set( + [testCase.context].filter((node) => !!node), + ); + const result = testCase.fn(testCase.node, contextIdentifiers, context); assert.strictEqual(result, testCase.expected); }); }); @@ -1475,19 +1660,29 @@ describe('utils', () => { describe('evaluateObjectProperties', function () { it('behaves correctly with simple object expression', function () { + const getObjectExpression = (ast: Program): ObjectExpression => + (ast.body[0] as VariableDeclaration).declarations[0] + .init as ObjectExpression; const ast = espree.parse('const obj = { a: 123, b: foo() };', { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const result = utils.evaluateObjectProperties( - ast.body[0].declarations[0].init, + getObjectExpression(ast), scopeManager, ); - assert.deepEqual(result, ast.body[0].declarations[0].init.properties); + assert.deepEqual(result, getObjectExpression(ast).properties); }); it('behaves correctly with spreads of objects', function () { + const getObjectExpression = ( + ast: Program, + bodyElement: number, + ): ObjectExpression => + (ast.body[bodyElement] as VariableDeclaration).declarations[0] + .init as ObjectExpression; + const ast = espree.parse( ` const extra1 = { a: 123 }; @@ -1498,29 +1693,33 @@ describe('utils', () => { ecmaVersion: 9, range: true, }, - ); + ) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const result = utils.evaluateObjectProperties( - ast.body[2].declarations[0].init, + getObjectExpression(ast, 2), scopeManager, ); assert.deepEqual(result, [ - ...ast.body[0].declarations[0].init.properties, // First spread properties - ...ast.body[2].declarations[0].init.properties.filter( + ...getObjectExpression(ast, 0).properties, // First spread properties + ...getObjectExpression(ast, 2).properties.filter( (property) => property.type !== 'SpreadElement', ), // Non-spread properties - ...ast.body[1].declarations[0].init.properties, // Second spread properties + ...getObjectExpression(ast, 1).properties, // Second spread properties ]); }); it('behaves correctly with non-variable spreads', function () { + const getObjectExpression = (ast: Program): ObjectExpression => + (ast.body[1] as VariableDeclaration).declarations[0] + .init as ObjectExpression; + const ast = espree.parse(`function foo() {} const obj = { ...foo() };`, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const result = utils.evaluateObjectProperties( - ast.body[1].declarations[0].init, + getObjectExpression(ast), scopeManager, ); assert.deepEqual(result, []); @@ -1530,10 +1729,11 @@ describe('utils', () => { const ast = espree.parse(`const obj = { ...foo };`, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const result = utils.evaluateObjectProperties( - ast.body[0].declarations[0].init, + (ast.body[0] as VariableDeclaration).declarations[0] + .init as ObjectExpression, scopeManager, ); assert.deepEqual(result, []); @@ -1543,7 +1743,7 @@ describe('utils', () => { const ast = espree.parse(`foo();`, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const result = utils.evaluateObjectProperties(ast.body[0], scopeManager); assert.deepEqual(result, []); @@ -1551,12 +1751,26 @@ describe('utils', () => { }); describe('getMessagesNode', function () { - [ + type TestCase = { + code: string; + getResult: ((ast: Program) => ObjectExpression) | (() => void); + }; + const CASES: TestCase[] = [ { code: 'module.exports = { meta: { messages: {} }, create(context) {} };', getResult(ast) { - return ast.body[0].expression.right.properties[0].value.properties[0] - .value; + return ( + ( + ( + ( + ( + (ast.body[0] as ExpressionStatement) + .expression as AssignmentExpression + ).right as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression; }, }, { @@ -1566,7 +1780,8 @@ describe('utils', () => { module.exports = { meta: { messages }, create(context) {} }; `, getResult(ast) { - return ast.body[0].declarations[0].init; + return (ast.body[0] as VariableDeclaration).declarations[0] + .init as ObjectExpression; }, }, { @@ -1576,24 +1791,32 @@ describe('utils', () => { module.exports = { meta: { ...extra }, create(context) {} }; `, getResult(ast) { - return ast.body[0].declarations[0].init.properties[0].value; + return ( + ( + (ast.body[0] as VariableDeclaration).declarations[0] + .init as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression; }, }, { code: `module.exports = { meta: FOO, create(context) {} };`, - getResult() {}, // returns undefined + getResult() { + return undefined; + }, // returns undefined }, { code: `module.exports = { create(context) {} };`, getResult() {}, // returns undefined }, - ].forEach((testCase) => { + ]; + CASES.forEach((testCase) => { describe(testCase.code, () => { it('returns the right node', () => { const ast = espree.parse(testCase.code, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert.strictEqual( @@ -1606,12 +1829,28 @@ describe('utils', () => { }); describe('getMessageIdNodes', function () { - [ + type TestCase = { + code: string; + getResult: (ast: Program) => Property[]; + }; + const CASES: TestCase[] = [ { code: 'module.exports = { meta: { messages: { foo: "hello world" } }, create(context) {} };', getResult(ast) { - return ast.body[0].expression.right.properties[0].value.properties[0] - .value.properties; + return ( + ( + ( + ( + ( + ( + (ast.body[0] as ExpressionStatement) + .expression as AssignmentExpression + ).right as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression + ).properties as Property[]; }, }, { @@ -1621,7 +1860,10 @@ describe('utils', () => { module.exports = { meta: { messages }, create(context) {} }; `, getResult(ast) { - return ast.body[0].declarations[0].init.properties; + return ( + (ast.body[0] as VariableDeclaration).declarations[0] + .init as ObjectExpression + ).properties as Property[]; }, }, { @@ -1632,20 +1874,24 @@ describe('utils', () => { module.exports = { meta: { ...extra }, create(context) {} }; `, getResult(ast) { - return ast.body[0].declarations[0].init.properties; + return ( + (ast.body[0] as VariableDeclaration).declarations[0] + .init as ObjectExpression + ).properties as Property[]; }, }, - ].forEach((testCase) => { + ]; + CASES.forEach((testCase) => { describe(testCase.code, () => { it('returns the right node', () => { const ast = espree.parse(testCase.code, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert.deepEqual( - utils.getMessageIdNodes(ruleInfo, scopeManager), + ruleInfo && utils.getMessageIdNodes(ruleInfo, scopeManager), testCase.getResult(ast), ); }); @@ -1654,7 +1900,15 @@ describe('utils', () => { }); describe('getMessageIdNodeById', function () { - [ + type TestCase = { + code: string; + run: ( + ruleInfo: RuleInfo, + scopeManager: Scope.ScopeManager, + ) => Property | undefined; + getResult: ((ast: Program) => Property) | (() => void); + }; + const CASES: TestCase[] = [ { code: 'module.exports = { meta: { messages: { foo: "hello world" } }, create(context) {} };', run(ruleInfo, scopeManager) { @@ -1662,12 +1916,24 @@ describe('utils', () => { 'foo', ruleInfo, scopeManager, - scopeManager.globalScope, + scopeManager.globalScope!, ); }, getResult(ast) { - return ast.body[0].expression.right.properties[0].value.properties[0] - .value.properties[0]; + return ( + ( + ( + ( + ( + ( + (ast.body[0] as ExpressionStatement) + .expression as AssignmentExpression + ).right as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression + ).properties[0] as Property + ).value as ObjectExpression + ).properties[0] as Property; }, }, { @@ -1677,22 +1943,24 @@ describe('utils', () => { 'bar', ruleInfo, scopeManager, - scopeManager.globalScope, + scopeManager.globalScope!, ); }, getResult() {}, // returns undefined }, - ].forEach((testCase) => { + ]; + + CASES.forEach((testCase) => { describe(testCase.code, () => { it('returns the right node', () => { const ast = espree.parse(testCase.code, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); const ruleInfo = utils.getRuleInfo({ ast, scopeManager }); assert.strictEqual( - testCase.run(ruleInfo, scopeManager), + ruleInfo && testCase.run(ruleInfo, scopeManager), testCase.getResult(ast), ); }); @@ -1707,26 +1975,39 @@ describe('utils', () => { const ast = espree.parse(code, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; // Add parent to each node. estraverse.traverse(ast, { enter(node, parent) { - node.parent = parent; + if (parent) { + node.parent = parent; + } }, }); const scopeManager = eslintScope.analyze(ast); assert.deepEqual( utils.findPossibleVariableValues( - ast.body[0].declarations[0].id, + (ast.body[0] as VariableDeclaration).declarations[0].id as Identifier, scopeManager, ), [ - ast.body[0].declarations[0].init, - ast.body[1].expression.right, - ast.body[2].expression.right, - ast.body[3].consequent.body[0].expression.right, + (ast.body[0] as VariableDeclaration).declarations[0].init as Literal, + ( + (ast.body[1] as ExpressionStatement) + .expression as AssignmentExpression + ).right, + ( + (ast.body[2] as ExpressionStatement) + .expression as AssignmentExpression + ).right, + ( + ( + ((ast.body[3] as IfStatement).consequent as BlockStatement) + .body[0] as ExpressionStatement + ).expression as AssignmentExpression + ).right, ], ); }); @@ -1739,12 +2020,17 @@ describe('utils', () => { const ast = espree.parse(code, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); assert.ok( utils.isVariableFromParameter( - ast.body[0].body.body[1].expression.arguments[0], + ( + ( + (ast.body[0] as FunctionDeclaration).body + .body[1] as ExpressionStatement + ).expression as CallExpression + ).arguments[0] as Identifier, scopeManager, ), ); @@ -1755,12 +2041,13 @@ describe('utils', () => { const ast = espree.parse(code, { ecmaVersion: 9, range: true, - }); + }) as unknown as Program; const scopeManager = eslintScope.analyze(ast); assert.notOk( utils.isVariableFromParameter( - ast.body[1].expression.arguments[0], + ((ast.body[1] as ExpressionStatement).expression as CallExpression) + .arguments[0] as Identifier, scopeManager, ), ); From 2b1d318eb8e264520d2b0f6de2dc4b997689a68e Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 23 Jul 2025 04:06:20 -0500 Subject: [PATCH 51/82] fix merge issue --- lib/rules/fixer-return.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/rules/fixer-return.ts b/lib/rules/fixer-return.ts index fbaaa469..028c7c26 100644 --- a/lib/rules/fixer-return.ts +++ b/lib/rules/fixer-return.ts @@ -19,7 +19,7 @@ import { isAutoFixerFunction, isSuggestionFixerFunction, } from '../utils.js'; -import type { FunctionInfo } from '../types'; +import type { FunctionInfo } from '../types.js'; const DEFAULT_FUNC_INFO: FunctionInfo = { upper: null, @@ -30,12 +30,6 @@ const DEFAULT_FUNC_INFO: FunctionInfo = { node: null, }; -import { - getContextIdentifiers, - isAutoFixerFunction, - isSuggestionFixerFunction, -} from '../utils.js'; - // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ From ba1d1087ce500fa278ad9d7f4a3e5c20891ae40e Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 23 Jul 2025 04:09:32 -0500 Subject: [PATCH 52/82] adjust import order --- lib/rules/fixer-return.ts | 1 - lib/rules/no-deprecated-context-methods.ts | 4 ++-- lib/rules/no-identical-tests.ts | 1 - lib/rules/no-meta-replaced-by.ts | 1 - lib/rules/no-meta-schema-default.ts | 2 +- lib/rules/no-missing-placeholders.ts | 2 +- lib/rules/no-unused-message-ids.ts | 2 +- lib/rules/no-unused-placeholders.ts | 1 - lib/rules/prefer-placeholders.ts | 2 +- 9 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/rules/fixer-return.ts b/lib/rules/fixer-return.ts index 028c7c26..20a4c909 100644 --- a/lib/rules/fixer-return.ts +++ b/lib/rules/fixer-return.ts @@ -2,7 +2,6 @@ * @fileoverview require fixer functions to return a fix * @author 薛定谔的猫 */ - import { getStaticValue } from '@eslint-community/eslint-utils'; import type { Rule } from 'eslint'; import type { diff --git a/lib/rules/no-deprecated-context-methods.ts b/lib/rules/no-deprecated-context-methods.ts index e5576219..bd2a126f 100644 --- a/lib/rules/no-deprecated-context-methods.ts +++ b/lib/rules/no-deprecated-context-methods.ts @@ -2,11 +2,11 @@ * @fileoverview Disallows usage of deprecated methods on rule context objects * @author Teddy Katz */ - import type { Rule } from 'eslint'; -import { getContextIdentifiers } from '../utils.js'; import type { Identifier, MemberExpression } from 'estree'; +import { getContextIdentifiers } from '../utils.js'; + const DEPRECATED_PASSTHROUGHS = { getSource: 'getText', getSourceLines: 'getLines', diff --git a/lib/rules/no-identical-tests.ts b/lib/rules/no-identical-tests.ts index a9ba7f46..365ee45b 100644 --- a/lib/rules/no-identical-tests.ts +++ b/lib/rules/no-identical-tests.ts @@ -2,7 +2,6 @@ * @fileoverview disallow identical tests * @author 薛定谔的猫 */ - import type { Rule } from 'eslint'; import type { Expression, SpreadElement } from 'estree'; diff --git a/lib/rules/no-meta-replaced-by.ts b/lib/rules/no-meta-replaced-by.ts index fb23f165..56d04f6f 100644 --- a/lib/rules/no-meta-replaced-by.ts +++ b/lib/rules/no-meta-replaced-by.ts @@ -1,7 +1,6 @@ /** * @fileoverview Disallows the usage of `meta.replacedBy` property */ - import type { Rule } from 'eslint'; import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; diff --git a/lib/rules/no-meta-schema-default.ts b/lib/rules/no-meta-schema-default.ts index cd749a76..938ff54a 100644 --- a/lib/rules/no-meta-schema-default.ts +++ b/lib/rules/no-meta-schema-default.ts @@ -1,12 +1,12 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; import type { Rule } from 'eslint'; +import type { Expression, SpreadElement } from 'estree'; import { getMetaSchemaNode, getMetaSchemaNodeProperty, getRuleInfo, } from '../utils.js'; -import type { Expression, SpreadElement } from 'estree'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-missing-placeholders.ts b/lib/rules/no-missing-placeholders.ts index 0ee3353d..c0c43dbd 100644 --- a/lib/rules/no-missing-placeholders.ts +++ b/lib/rules/no-missing-placeholders.ts @@ -4,6 +4,7 @@ */ import { getStaticValue } from '@eslint-community/eslint-utils'; import type { Rule } from 'eslint'; +import type { Node } from 'estree'; import { collectReportViolationAndSuggestionData, @@ -14,7 +15,6 @@ import { getReportInfo, getRuleInfo, } from '../utils.js'; -import type { Node } from 'estree'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-unused-message-ids.ts b/lib/rules/no-unused-message-ids.ts index 55483ca3..cf8f8acb 100644 --- a/lib/rules/no-unused-message-ids.ts +++ b/lib/rules/no-unused-message-ids.ts @@ -1,4 +1,5 @@ import type { Rule } from 'eslint'; +import type { Identifier, Node } from 'estree'; import { collectReportViolationAndSuggestionData, @@ -10,7 +11,6 @@ import { getRuleInfo, isVariableFromParameter, } from '../utils.js'; -import type { Identifier, Node } from 'estree'; // ------------------------------------------------------------------------------ // Rule Definition diff --git a/lib/rules/no-unused-placeholders.ts b/lib/rules/no-unused-placeholders.ts index a06a84a9..a9dc14cd 100644 --- a/lib/rules/no-unused-placeholders.ts +++ b/lib/rules/no-unused-placeholders.ts @@ -2,7 +2,6 @@ * @fileoverview Disallow unused placeholders in rule report messages * @author 薛定谔的猫 */ - import { getStaticValue } from '@eslint-community/eslint-utils'; import type { Rule } from 'eslint'; import type { Node } from 'estree'; diff --git a/lib/rules/prefer-placeholders.ts b/lib/rules/prefer-placeholders.ts index 58b7afba..d8a9d217 100644 --- a/lib/rules/prefer-placeholders.ts +++ b/lib/rules/prefer-placeholders.ts @@ -5,7 +5,7 @@ import { findVariable } from '@eslint-community/eslint-utils'; import type { Rule } from 'eslint'; -import { Node } from 'estree'; +import type { Node } from 'estree'; import { collectReportViolationAndSuggestionData, From 93dd0ac51c5cbf4e2ef1e11483f795e51015e5e2 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 23 Jul 2025 04:12:46 -0500 Subject: [PATCH 53/82] fix plugin type --- lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.ts b/lib/index.ts index 51172fcb..948641cc 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -118,7 +118,7 @@ const plugin = { [configName]: { name: `${PLUGIN_NAME}/${configName}`, plugins: { - get PLUGIN_NAME() { + get [PLUGIN_NAME](): ESLint.Plugin { return plugin; }, }, From ac937dba613032bed11e1ec8d78220fae5514dd5 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 23 Jul 2025 04:22:41 -0500 Subject: [PATCH 54/82] fix utils tests --- tests/lib/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/utils.ts b/tests/lib/utils.ts index 30759e80..3ccaa3c1 100644 --- a/tests/lib/utils.ts +++ b/tests/lib/utils.ts @@ -703,9 +703,9 @@ describe('utils', () => { return [ (blockStatement.body[0] as ExpressionStatement) .expression as Identifier, - (blockStatement.body[0] as ExpressionStatement) + (blockStatement.body[1] as ExpressionStatement) .expression as Identifier, - (blockStatement.body[0] as ExpressionStatement) + (blockStatement.body[2] as ExpressionStatement) .expression as Identifier, ]; }, From e97458e12ceda9314ba33fff3ffe0eafacb8b676 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 23 Jul 2025 04:43:13 -0500 Subject: [PATCH 55/82] switch to tsup for build --- lib/index.ts | 42 +++++++++++++++++------------- package.json | 8 +++--- tsdown.config.ts => tsup.config.ts | 5 ++-- 3 files changed, 31 insertions(+), 24 deletions(-) rename tsdown.config.ts => tsup.config.ts (55%) diff --git a/lib/index.ts b/lib/index.ts index 948641cc..1fe44b03 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,7 +2,7 @@ * @fileoverview An ESLint plugin for linting ESLint plugins * @author Teddy Katz */ -import type { ESLint, Rule } from 'eslint'; +import type { ESLint, Linter, Rule } from 'eslint'; import packageMetadata from '../package.json' with { type: 'json' }; import consistentOutput from './rules/consistent-output.js'; @@ -67,6 +67,20 @@ const configFilters: Record boolean> = { configFilters.recommended(rule) && configFilters.tests(rule), }; +const createConfig = (configName: ConfigName): Linter.Config => ({ + name: `${PLUGIN_NAME}/${configName}`, + plugins: { + get [PLUGIN_NAME](): ESLint.Plugin { + return plugin; + }, + }, + rules: Object.fromEntries( + (Object.keys(allRules) as (keyof typeof allRules)[]) + .filter((ruleName) => configFilters[configName](allRules[ruleName])) + .map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']), + ), +}); + // ------------------------------------------------------------------------------ // Plugin Definition // ------------------------------------------------------------------------------ @@ -113,23 +127,15 @@ const plugin = { version: packageMetadata.version, }, rules: allRules, - configs: CONFIG_NAMES.reduce((configs, configName) => { - return Object.assign(configs, { - [configName]: { - name: `${PLUGIN_NAME}/${configName}`, - plugins: { - get [PLUGIN_NAME](): ESLint.Plugin { - return plugin; - }, - }, - rules: Object.fromEntries( - (Object.keys(allRules) as (keyof typeof allRules)[]) - .filter((ruleName) => configFilters[configName](allRules[ruleName])) - .map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']), - ), - }, - }); - }, {}), + configs: { + all: createConfig('all'), + 'all-type-checked': createConfig('all-type-checked'), + recommended: createConfig('recommended'), + rules: createConfig('rules'), + tests: createConfig('tests'), + 'rules-recommended': createConfig('rules-recommended'), + 'tests-recommended': createConfig('tests-recommended'), + }, } satisfies ESLint.Plugin; export default plugin; diff --git a/package.json b/package.json index 62ee3798..3c7101c2 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,15 @@ "version": "6.5.0", "description": "An ESLint plugin for linting ESLint plugins", "author": "Teddy Katz", - "main": "./lib/index.js", + "main": "./dist/index.js", "type": "module", "exports": { - ".": "./lib/index.js", + ".": "./dist/index.js", "./package.json": "./package.json" }, "license": "MIT", "scripts": { - "build": "tsdown", + "build": "tsup", "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*", "lint:docs": "markdownlint \"**/*.md\"", "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", @@ -81,7 +81,7 @@ "npm-run-all2": "^7.0.1", "prettier": "^3.4.1", "release-it": "^17.2.0", - "tsdown": "^0.12.8", + "tsup": "^8.5.0", "typescript": "^5.8.3", "vitest": "^3.2.4" }, diff --git a/tsdown.config.ts b/tsup.config.ts similarity index 55% rename from tsdown.config.ts rename to tsup.config.ts index cf6d811d..3542a8da 100644 --- a/tsdown.config.ts +++ b/tsup.config.ts @@ -1,9 +1,10 @@ -import { defineConfig } from 'tsdown'; +import { defineConfig } from 'tsup'; export default defineConfig({ + bundle: false, clean: true, dts: true, - entry: ['lib/index.ts'], + entry: ['lib/**/*.ts'], format: ['esm'], outDir: 'dist', }); From cb5eda99d2b89e0bf9486805e48bafde2b8421dc Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 26 Jul 2025 15:22:16 -0500 Subject: [PATCH 56/82] Change import of `package.json` to require, for backwards compatibility --- lib/index.ts | 10 +++++++++- package.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/index.ts b/lib/index.ts index 1fe44b03..1fbcb0af 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,9 +2,10 @@ * @fileoverview An ESLint plugin for linting ESLint plugins * @author Teddy Katz */ +import { createRequire } from 'node:module'; + import type { ESLint, Linter, Rule } from 'eslint'; -import packageMetadata from '../package.json' with { type: 'json' }; import consistentOutput from './rules/consistent-output.js'; import fixerReturn from './rules/fixer-return.js'; import metaPropertyOrdering from './rules/meta-property-ordering.js'; @@ -38,6 +39,13 @@ import requireMetaType from './rules/require-meta-type.js'; import testCasePropertyOrdering from './rules/test-case-property-ordering.js'; import testCaseShorthandStrings from './rules/test-case-shorthand-strings.js'; +const require = createRequire(import.meta.url); + +const packageMetadata = require("../package.json") as { + name: string; + version: string; +}; + const PLUGIN_NAME = packageMetadata.name.replace(/^eslint-plugin-/, ''); const CONFIG_NAMES = [ 'all', diff --git a/package.json b/package.json index 3c7101c2..cf9f6e1d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "tsup", "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*", "lint:docs": "markdownlint \"**/*.md\"", - "lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"", + "lint:eslint-docs": "npm-run-all -s build \"update:eslint-docs -- --check\"", "lint:js": "eslint --cache --ignore-pattern \"**/*.md\" .", "lint:js-docs": "eslint --no-inline-config \"**/*.md\"", "lint:package-json": "npmPkgJsonLint .", From ff74f460e5a375efa49b09be222750269e027b8e Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 26 Jul 2025 16:48:04 -0500 Subject: [PATCH 57/82] fix utils tests --- tests/lib/utils.ts | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/tests/lib/utils.ts b/tests/lib/utils.ts index 3ccaa3c1..292e392d 100644 --- a/tests/lib/utils.ts +++ b/tests/lib/utils.ts @@ -36,9 +36,9 @@ type MockRuleInfo = { id?: { name: string }; type: string; }; - meta: { + meta?: { type: string; - } | null; + } | undefined; isNewStyle: boolean; }; @@ -389,12 +389,10 @@ describe('utils', () => { const CASES: Record = { 'module.exports = { create: function foo() {} };': { create: { type: 'FunctionExpression', id: { name: 'foo' } }, // (This property will actually contain the AST node.) - meta: null, isNewStyle: true, }, 'module.exports = { create: () => { } };': { create: { type: 'ArrowFunctionExpression' }, - meta: null, isNewStyle: true, }, 'module.exports = { create() {}, meta: { } };': { @@ -416,12 +414,10 @@ describe('utils', () => { 'module.exports = { create: () => { } }; exports.create = function foo() {}; exports.meta = {};': { create: { type: 'ArrowFunctionExpression' }, - meta: null, isNewStyle: true, }, 'exports.meta = {}; module.exports = { create: () => { } };': { create: { type: 'ArrowFunctionExpression' }, - meta: null, isNewStyle: true, }, 'module.exports = { create: () => { } }; module.exports.meta = {};': { @@ -441,44 +437,43 @@ describe('utils', () => { }, 'module.exports = { create: (context) => { } }; exports.meta = {};': { create: { type: 'ArrowFunctionExpression' }, - meta: null, isNewStyle: true, }, 'module.exports = function foo(context) { return {}; }': { create: { type: 'FunctionExpression', id: { name: 'foo' } }, - meta: null, + meta: undefined, isNewStyle: false, }, 'module.exports = function foo(slightlyDifferentContextName) { return {}; }': { create: { type: 'FunctionExpression', id: { name: 'foo' } }, - meta: null, + meta: undefined, isNewStyle: false, }, 'module.exports = function foo({ report }) { return {}; }': { create: { type: 'FunctionExpression', id: { name: 'foo' } }, - meta: null, + meta: undefined, isNewStyle: false, }, 'module.exports = (context) => { return {}; }': { create: { type: 'ArrowFunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, 'module.exports = (context) => { if (foo) { return {}; } }': { create: { type: 'ArrowFunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, 'exports.meta = {}; module.exports = (context) => { return {}; }': { create: { type: 'ArrowFunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, 'module.exports = (context) => { return {}; }; module.exports.meta = {};': { create: { type: 'ArrowFunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, 'const create = function(context) { return {}; }; const meta = {}; module.exports = { create, meta };': @@ -494,7 +489,7 @@ describe('utils', () => { }, 'const rule = function(context) {return{};}; module.exports = rule;': { create: { type: 'FunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, }; @@ -523,7 +518,6 @@ describe('utils', () => { // ESM (object style) 'export default { create() {} }': { create: { type: 'FunctionExpression' }, - meta: null, isNewStyle: true, }, 'export default { create() {}, meta: {} }': { @@ -568,22 +562,21 @@ describe('utils', () => { // ESM (function style) 'export default function (context) { return {}; }': { create: { type: 'FunctionDeclaration' }, - meta: null, isNewStyle: false, }, 'export default function (context) { if (foo) { return {}; } }': { create: { type: 'FunctionDeclaration' }, - meta: null, + meta: undefined, isNewStyle: false, }, 'export default (context) => { return {}; }': { create: { type: 'ArrowFunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, 'const rule = function(context) {return {};}; export default rule;': { create: { type: 'FunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, }; @@ -654,7 +647,7 @@ describe('utils', () => { options: { sourceType: 'script' }, expected: { create: { type: 'FunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, }, @@ -664,7 +657,7 @@ describe('utils', () => { options: { sourceType: 'module' }, expected: { create: { type: 'FunctionDeclaration' }, - meta: null, + meta: undefined, isNewStyle: false, }, }, From 4bfc4b0d778f77652791394b713ea6accef69d19 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 27 Jul 2025 10:27:04 -0500 Subject: [PATCH 58/82] update rule-setup tests --- tests/lib/rule-setup.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/lib/rule-setup.ts b/tests/lib/rule-setup.ts index 9285f9e5..dfa0fbba 100644 --- a/tests/lib/rule-setup.ts +++ b/tests/lib/rule-setup.ts @@ -1,6 +1,5 @@ import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { assert, describe, it } from 'vitest'; @@ -9,18 +8,17 @@ import plugin from '../../lib/index.js'; const RULE_NAMES = Object.keys(plugin.rules) as Array< keyof typeof plugin.rules >; -const dirname = path.dirname(fileURLToPath(import.meta.url)); describe('rule setup is correct', () => { it('should have a list of exported rules and rules directory that match', () => { - const filePath = path.join(dirname, '..', 'lib', 'rules'); + const filePath = path.join(import.meta.dirname, '..', 'lib', 'rules'); const files = readdirSync(filePath); assert.deepStrictEqual( RULE_NAMES, files .filter((file) => !file.startsWith('.')) - .map((file) => file.replace('.js', '')), + .map((file) => file.replace('.ts', '')), ); }); @@ -39,18 +37,18 @@ describe('rule setup is correct', () => { it('should have the right contents', () => { const filePath = path.join( - dirname, + import.meta.dirname, '..', '..', 'lib', 'rules', - `${ruleName}.js`, + `${ruleName}.ts`, ); const file = readFileSync(filePath, 'utf8'); assert.ok( - file.includes("/** @type {import('eslint').Rule.RuleModule} */"), - 'includes jsdoc comment for rule type', + file.includes("const rule: Rule.RuleModule"), + 'is defined as type RuleModule', ); }); }); @@ -58,19 +56,19 @@ describe('rule setup is correct', () => { }); it('should have tests for all rules', () => { - const filePath = path.join(dirname, 'rules'); + const filePath = path.join(import.meta.dirname, 'rules'); const files = readdirSync(filePath); assert.deepStrictEqual( RULE_NAMES, files .filter((file) => !file.startsWith('.')) - .map((file) => file.replace('.js', '')), + .map((file) => file.replace('.ts', '')), ); }); it('should have documentation for all rules', () => { - const filePath = path.join(dirname, '..', '..', 'docs', 'rules'); + const filePath = path.join(import.meta.dirname, '..', '..', 'docs', 'rules'); const files = readdirSync(filePath); assert.deepStrictEqual( From 4da3c01dbca63243a833f83db57480578e9cddec Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 27 Jul 2025 14:01:05 -0500 Subject: [PATCH 59/82] add build to publish workflow --- .github/workflows/release-please.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b9c4391b..cc25a226 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -38,6 +38,7 @@ jobs: if: ${{ steps.release.outputs.release_created }} - run: | npm install --force + npm run build npm publish --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 9a6060c701c4bed7a7e2a826303243376c0b56a0 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 28 Jul 2025 16:54:11 -0500 Subject: [PATCH 60/82] add slashes to .gitignore --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index ba291291..302cc701 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -.idea -coverage +.idea/ +coverage/ .vscode node_modules/ npm-debug.log yarn.lock .eslintcache -dist +dist/ # eslint-remote-tester eslint-remote-tester-results From 0259d01dc019bfdad54cc09f08ec1602e5212d2b Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 28 Jul 2025 16:55:27 -0500 Subject: [PATCH 61/82] remove jsdoc type annotation from `fixer-return` --- lib/rules/fixer-return.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/rules/fixer-return.ts b/lib/rules/fixer-return.ts index 20a4c909..97aedf04 100644 --- a/lib/rules/fixer-return.ts +++ b/lib/rules/fixer-return.ts @@ -56,9 +56,8 @@ const rule: Rule.RuleModule = { * As we exit the fix() function, ensure we have returned or yielded a real fix by this point. * If not, report the function as a violation. * - * @param {ASTNode} node - A node to check. - * @param {Location} loc - Optional location to report violation on. - * @returns {void} + * @param node - A node to check. + * @param loc - Optional location to report violation on. */ function ensureFunctionReturnedFix( node: ArrowFunctionExpression | FunctionExpression, From 2559024fce7e47f0c71285e86427ce2ce05819c0 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 28 Jul 2025 17:27:20 -0500 Subject: [PATCH 62/82] remove unnecessary param from `no-indentical-tests` --- lib/rules/no-identical-tests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rules/no-identical-tests.ts b/lib/rules/no-identical-tests.ts index 365ee45b..d60b5a48 100644 --- a/lib/rules/no-identical-tests.ts +++ b/lib/rules/no-identical-tests.ts @@ -31,7 +31,6 @@ const rule: Rule.RuleModule = { /** * Create a unique cache key - * @param test */ function toKey(test: Expression | SpreadElement): string { if (test.type !== 'ObjectExpression') { From a3cf33a04a54e756c7185b363b1fbe87cb94aaca Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 28 Jul 2025 17:39:07 -0500 Subject: [PATCH 63/82] add early return in `no-missing-placeholders` --- lib/rules/no-missing-placeholders.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rules/no-missing-placeholders.ts b/lib/rules/no-missing-placeholders.ts index c0c43dbd..fa39d453 100644 --- a/lib/rules/no-missing-placeholders.ts +++ b/lib/rules/no-missing-placeholders.ts @@ -96,7 +96,8 @@ const rule: Rule.RuleModule = { messageId, data, } of reportMessagesAndDataArray.filter((obj) => obj.message)) { - const messageStaticValue = getStaticValue(message!, scope); + if (!message) continue; + const messageStaticValue = getStaticValue(message, scope); if ( ((message?.type === 'Literal' && typeof message.value === 'string') || From 0052ccf80a6cb2b4f9f32482e787589025ea3460 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 28 Jul 2025 17:42:37 -0500 Subject: [PATCH 64/82] removed assert in `no-only-tests` --- lib/rules/no-only-tests.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/rules/no-only-tests.ts b/lib/rules/no-only-tests.ts index e35a22d3..6ed93ce5 100644 --- a/lib/rules/no-only-tests.ts +++ b/lib/rules/no-only-tests.ts @@ -53,18 +53,18 @@ const rule: Rule.RuleModule = { const sourceCode = context.sourceCode; const tokenBefore = - sourceCode.getTokenBefore(onlyProperty)!; + sourceCode.getTokenBefore(onlyProperty); const tokenAfter = - sourceCode.getTokenAfter(onlyProperty)!; - if ( - (isCommaToken(tokenBefore) && + sourceCode.getTokenAfter(onlyProperty); + if ((tokenBefore && tokenAfter) && + ((isCommaToken(tokenBefore) && isCommaToken(tokenAfter)) || // In middle of properties (isOpeningBraceToken(tokenBefore) && - isCommaToken(tokenAfter)) // At beginning of properties + isCommaToken(tokenAfter))) // At beginning of properties ) { yield fixer.remove(tokenAfter); // Remove extra comma. } - if ( + if ((tokenBefore && tokenAfter) && isCommaToken(tokenBefore) && isClosingBraceToken(tokenAfter) ) { From ead0948c8f3ae99c458c9edb91be5b75ef132de7 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 28 Jul 2025 17:43:43 -0500 Subject: [PATCH 65/82] remove empty param annotation from `no-property-in-node` --- lib/rules/no-property-in-node.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/rules/no-property-in-node.ts b/lib/rules/no-property-in-node.ts index 0384af7e..7ef564a2 100644 --- a/lib/rules/no-property-in-node.ts +++ b/lib/rules/no-property-in-node.ts @@ -26,8 +26,6 @@ const defaultTypedNodeSourceFileTesters = [ * } * ``` * - * @param type - * @param typedNodeSourceFileTesters * @returns Whether the type seems to include a known ESTree or TSESTree AST node. */ function isAstNodeType( From d91a0afff8ecd2d031df796677245a929b44b28f Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 30 Jul 2025 17:07:16 -0500 Subject: [PATCH 66/82] remove casting from no-unused-message-ids --- lib/rules/no-unused-message-ids.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/rules/no-unused-message-ids.ts b/lib/rules/no-unused-message-ids.ts index cf8f8acb..e260e5ce 100644 --- a/lib/rules/no-unused-message-ids.ts +++ b/lib/rules/no-unused-message-ids.ts @@ -56,7 +56,7 @@ const rule: Rule.RuleModule = { contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, - 'Program:exit'(ast) { + 'Program:exit'() { if (hasSeenUnknownMessageId || !hasSeenViolationReport) { /* Bail out when the rule is likely to have false positives. @@ -107,10 +107,9 @@ const rule: Rule.RuleModule = { const values = messageId.type === 'Literal' ? [messageId] - : findPossibleVariableValues( - messageId as Identifier, - scopeManager, - ); + : messageId.type === 'Identifier' + ? findPossibleVariableValues(messageId, scopeManager) + : []; if ( values.length === 0 || values.some((val) => val.type !== 'Literal') @@ -118,10 +117,11 @@ const rule: Rule.RuleModule = { // When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives. hasSeenUnknownMessageId = true; } - values.forEach( - (val) => - 'value' in val && messageIdsUsed.add(val.value as string), - ); + values + .filter((value) => value.type === 'Literal') + .map((value) => value.value) + .filter((value) => typeof value === 'string') + .forEach((value) => messageIdsUsed.add(value)); } } }, @@ -143,15 +143,18 @@ const rule: Rule.RuleModule = { if ( values.length === 0 || values.some((val) => val.type !== 'Literal') || - isVariableFromParameter(node.value as Identifier, scopeManager) + (node.value.type === 'Identifier' && + isVariableFromParameter(node.value, scopeManager)) ) { // When a dynamic messageId is used and we can't detect its value, disable the rule to avoid false positives. hasSeenUnknownMessageId = true; } - values.forEach( - (val) => 'value' in val && messageIdsUsed.add(val.value as string), - ); + values + .filter((val) => val.type === 'Literal') + .map((val) => val.value) + .filter((val) => typeof val === 'string') + .forEach((val) => messageIdsUsed.add(val)); } }, }; From 1ffacf7c33bdec2d1a3cc73200d26f9293785639 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 30 Jul 2025 17:29:05 -0500 Subject: [PATCH 67/82] add back valid test case to require-meta-type --- tests/lib/rules/require-meta-type.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/lib/rules/require-meta-type.ts b/tests/lib/rules/require-meta-type.ts index 535f3393..a2326db2 100644 --- a/tests/lib/rules/require-meta-type.ts +++ b/tests/lib/rules/require-meta-type.ts @@ -75,6 +75,16 @@ ruleTester.run('require-meta-type', rule, { }; `, 'module.exports = {};', // No rule. + // No `create` function. + { + code: ` + const create = {}; + module.exports = { + meta: {}, + create, + }; + `, + }, ], invalid: [ From ff19420b5a6e0feaed2970562d6ff559c3cf86c1 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 30 Jul 2025 17:31:03 -0500 Subject: [PATCH 68/82] add explanatory comment to estree.d.ts --- types/estree.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/types/estree.d.ts b/types/estree.d.ts index 6d640f30..93339cfa 100644 --- a/types/estree.d.ts +++ b/types/estree.d.ts @@ -1,5 +1,13 @@ import { Program as EstreeProgram } from 'estree'; +/** + * This file augments the `estree` types to include a couple of types that are not built-in to `estree` that we're using. + * This is necessary because the `estree` types are used by ESLint, and ESLint does not natively support + * TypeScript types. Since we're only using a couple of them, we can just add them here, rather than + * installing typescript estree types. + * + * This also adds support for the AST mutation that ESLint does to add parent nodes. + */ declare module 'estree' { interface BaseNode { parent: Node; From 64429ad306a405869b91feb6daf769c69e6d27ed Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 30 Jul 2025 17:33:27 -0500 Subject: [PATCH 69/82] remove type annotation from comment in test-case-shorthand-string --- tests/lib/rules/test-case-shorthand-strings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/rules/test-case-shorthand-strings.ts b/tests/lib/rules/test-case-shorthand-strings.ts index 1d2aea0d..0b989397 100644 --- a/tests/lib/rules/test-case-shorthand-strings.ts +++ b/tests/lib/rules/test-case-shorthand-strings.ts @@ -13,7 +13,7 @@ import { RuleTester } from 'eslint'; /** * Returns the code for some valid test cases * @param cases The code representation of valid test cases - * @returns {string} Code representing the test cases + * @returns Code representing the test cases */ function getTestCases(cases: string[]): string { return ` From 3768cfae6155855f5141c531de47c04a1013a794 Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 30 Jul 2025 18:01:37 -0500 Subject: [PATCH 70/82] removed casting from no-meta-schema-default --- lib/rules/no-meta-schema-default.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-meta-schema-default.ts b/lib/rules/no-meta-schema-default.ts index 938ff54a..46a29b0b 100644 --- a/lib/rules/no-meta-schema-default.ts +++ b/lib/rules/no-meta-schema-default.ts @@ -68,7 +68,7 @@ const rule: Rule.RuleModule = { continue; } - switch ('name' in key ? key.name : 'value' in key ? key.value : '') { + switch (staticKey.value) { case 'allOf': case 'anyOf': case 'oneOf': { From 2f80268c4b78830dc56deeaf588ab7cf4ae7532e Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 31 Jul 2025 17:10:34 -0500 Subject: [PATCH 71/82] remove casts from no-useless-token-range --- lib/rules/no-useless-token-range.ts | 76 ++++++++++++++--------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/lib/rules/no-useless-token-range.ts b/lib/rules/no-useless-token-range.ts index 85b2f923..653ef53d 100644 --- a/lib/rules/no-useless-token-range.ts +++ b/lib/rules/no-useless-token-range.ts @@ -58,14 +58,11 @@ const rule: Rule.RuleModule = { arg.properties.length >= 2 || (arg.properties.length === 1 && (getKeyName(arg.properties[0]) !== 'includeComments' || - (arg.properties[0] as Property).value.type !== 'Literal')) + (arg.properties[0].type === 'Property' && + arg.properties[0].value.type !== 'Literal'))) ); } - function isMemberExpression(node: Node): node is MemberExpression { - return node.type === 'MemberExpression'; - } - /** * Determines whether a node is a MemberExpression that accesses the `range` property * @param node The node @@ -86,7 +83,7 @@ const rule: Rule.RuleModule = { function isStartAccess(memberExpression: MemberExpression): boolean { if ( isRangeAccess(memberExpression) && - isMemberExpression(memberExpression.parent) + memberExpression.parent.type === 'MemberExpression' ) { return isStartAccess(memberExpression.parent); } @@ -96,7 +93,7 @@ const rule: Rule.RuleModule = { (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 0 && - isMemberExpression(memberExpression.object) && + memberExpression.object.type === 'MemberExpression' && isRangeAccess(memberExpression.object)) ); } @@ -110,7 +107,7 @@ const rule: Rule.RuleModule = { function isEndAccess(memberExpression: MemberExpression): boolean { if ( isRangeAccess(memberExpression) && - isMemberExpression(memberExpression.parent) + memberExpression.parent.type === 'MemberExpression' ) { return isEndAccess(memberExpression.parent); } @@ -120,7 +117,7 @@ const rule: Rule.RuleModule = { (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 1 && - isMemberExpression(memberExpression.object) && + memberExpression.object.type === 'MemberExpression' && isRangeAccess(memberExpression.object)) ); } @@ -134,14 +131,14 @@ const rule: Rule.RuleModule = { [...getSourceCodeIdentifiers(sourceCode.scopeManager, ast)] .filter( (identifier) => - isMemberExpression(identifier.parent) && + identifier.parent.type === 'MemberExpression' && identifier.parent.object === identifier && identifier.parent.property.type === 'Identifier' && identifier.parent.parent.type === 'CallExpression' && identifier.parent === identifier.parent.parent.callee && identifier.parent.parent.arguments.length <= 2 && !affectsGetTokenOutput(identifier.parent.parent.arguments[1]) && - isMemberExpression(identifier.parent.parent.parent) && + identifier.parent.parent.parent.type === 'MemberExpression' && identifier.parent.parent === identifier.parent.parent.parent.object && ((isStartAccess(identifier.parent.parent.parent) && @@ -150,36 +147,35 @@ const rule: Rule.RuleModule = { identifier.parent.property.name === 'getLastToken')), ) .forEach((identifier) => { - const fullRangeAccess = - isMemberExpression(identifier.parent.parent.parent) && - isRangeAccess(identifier.parent.parent.parent) - ? identifier.parent.parent.parent.parent - : identifier.parent.parent.parent; - const replacementText = - sourceCode.text.slice( - fullRangeAccess.range![0], - identifier.parent.parent.range![0], - ) + - sourceCode.getText( - (identifier.parent.parent as CallExpression).arguments[0], - ) + - sourceCode.text.slice( - identifier.parent.parent.range![1], - fullRangeAccess.range![1], - ); - context.report({ - node: identifier.parent.parent, - messageId: 'useReplacement', - data: { replacementText }, - fix(fixer) { - return fixer.replaceText( - identifier.parent.parent, - sourceCode.getText( - (identifier.parent.parent as CallExpression).arguments[0], - ), + const callExpression = identifier.parent.parent; + if (callExpression.type === 'CallExpression') { + const fullRangeAccess = + identifier.parent.parent.parent.type === 'MemberExpression' && + isRangeAccess(identifier.parent.parent.parent) + ? identifier.parent.parent.parent.parent + : identifier.parent.parent.parent; + const replacementText = + sourceCode.text.slice( + fullRangeAccess.range![0], + identifier.parent.parent.range![0], + ) + + sourceCode.getText(callExpression.arguments[0]) + + sourceCode.text.slice( + identifier.parent.parent.range![1], + fullRangeAccess.range![1], ); - }, - }); + context.report({ + node: identifier.parent.parent, + messageId: 'useReplacement', + data: { replacementText }, + fix(fixer) { + return fixer.replaceText( + identifier.parent.parent, + sourceCode.getText(callExpression.arguments[0]), + ); + }, + }); + } }); }, }; From 1abe593ce5bbedd4cd43f5b2f5d90d1c8cba52a9 Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 31 Jul 2025 17:37:15 -0500 Subject: [PATCH 72/82] remove cast from report-message-format --- lib/rules/report-message-format.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/rules/report-message-format.ts b/lib/rules/report-message-format.ts index 3912f67d..4ba7506d 100644 --- a/lib/rules/report-message-format.ts +++ b/lib/rules/report-message-format.ts @@ -58,7 +58,9 @@ const rule: Rule.RuleModule = { (message.type === 'TemplateLiteral' && message.quasis.length === 1 && !pattern.test(message.quasis[0].value.cooked ?? '')) || - (staticValue && !pattern.test(staticValue.value as string)) + (staticValue && + typeof staticValue.value === 'string' && + !pattern.test(staticValue.value)) ) { context.report({ node: message, From 22cf264e54230a6a90508529d7812c3fc908f27a Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 1 Aug 2025 03:50:52 -0500 Subject: [PATCH 73/82] remove cast from require-meta-default-options --- lib/rules/require-meta-default-options.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/rules/require-meta-default-options.ts b/lib/rules/require-meta-default-options.ts index a65456bd..c8640f55 100644 --- a/lib/rules/require-meta-default-options.ts +++ b/lib/rules/require-meta-default-options.ts @@ -67,13 +67,17 @@ const rule: Rule.RuleModule = { } if (!metaDefaultOptions) { - context.report({ - node: metaNode!, - messageId: 'missingDefaultOptions', - fix(fixer) { - return fixer.insertTextAfter(schemaProperty, ', defaultOptions: []'); - }, - }); + metaNode && + context.report({ + node: metaNode, + messageId: 'missingDefaultOptions', + fix(fixer) { + return fixer.insertTextAfter( + schemaProperty, + ', defaultOptions: []', + ); + }, + }); return {}; } From 88bc975648a6f403ba8f7f426fb7e3e7b7b8b29e Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 1 Aug 2025 03:52:58 -0500 Subject: [PATCH 74/82] remove cast from require-meta-docs-url --- lib/rules/require-meta-docs-url.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rules/require-meta-docs-url.ts b/lib/rules/require-meta-docs-url.ts index 22c58b7f..ae09e8f1 100644 --- a/lib/rules/require-meta-docs-url.ts +++ b/lib/rules/require-meta-docs-url.ts @@ -102,7 +102,11 @@ const rule: Rule.RuleModule = { return; } - if (isExpectedUrl(staticValue && (staticValue.value as string))) { + if ( + staticValue && + typeof staticValue.value === 'string' && + isExpectedUrl(staticValue.value) + ) { return; } From 9f0af6f9192deb5db0f0e989de274b86858105e9 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 1 Aug 2025 18:02:06 -0500 Subject: [PATCH 75/82] remove casts from require-meta-fixables --- lib/rules/require-meta-fixable.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/rules/require-meta-fixable.ts b/lib/rules/require-meta-fixable.ts index 339a0fe6..d3d23bf7 100644 --- a/lib/rules/require-meta-fixable.ts +++ b/lib/rules/require-meta-fixable.ts @@ -98,9 +98,9 @@ const rule: Rule.RuleModule = { } if ( - !['code', 'whitespace', null, undefined].includes( - staticValue.value as string, - ) + staticValue.value && + (typeof staticValue.value !== 'string' || + !['code', 'whitespace'].includes(staticValue.value)) ) { // `fixable` property has an invalid value. context.report({ @@ -112,7 +112,8 @@ const rule: Rule.RuleModule = { if ( usesFixFunctions && - !['code', 'whitespace'].includes(staticValue.value as string) + (typeof staticValue.value !== 'string' || + !['code', 'whitespace'].includes(staticValue.value)) ) { // Rule is fixable but `fixable` property does not have a fixable value. context.report({ @@ -122,7 +123,8 @@ const rule: Rule.RuleModule = { } else if ( catchNoFixerButFixableProperty && !usesFixFunctions && - ['code', 'whitespace'].includes(staticValue.value as string) + typeof staticValue.value === 'string' && + ['code', 'whitespace'].includes(staticValue.value) ) { // Rule is NOT fixable but `fixable` property has a fixable value. context.report({ From fb85ff614c6f8a363280e81475c306e844e3e6d9 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 1 Aug 2025 18:04:10 -0500 Subject: [PATCH 76/82] remove cast from require-meta-type --- lib/rules/require-meta-type.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/rules/require-meta-type.ts b/lib/rules/require-meta-type.ts index 501224ed..7001936b 100644 --- a/lib/rules/require-meta-type.ts +++ b/lib/rules/require-meta-type.ts @@ -62,7 +62,10 @@ const rule: Rule.RuleModule = { return; } - if (!VALID_TYPES.has(staticValue.value as string)) { + if ( + typeof staticValue.value !== 'string' || + !VALID_TYPES.has(staticValue.value) + ) { context.report({ node: typeNode.value, messageId: 'unexpected' }); } }, From 1f22290a2bdd93c0e032484e776621eee1de75b7 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 1 Aug 2025 18:23:43 -0500 Subject: [PATCH 77/82] Adjust PartialRuleInfo types --- lib/types.ts | 8 +++++++- lib/utils.ts | 24 +++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/types.ts b/lib/types.ts index b9378e18..72c9899a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -6,6 +6,8 @@ import type { Expression, FunctionDeclaration, FunctionExpression, + MaybeNamedClassDeclaration, + MaybeNamedFunctionDeclaration, Node, ObjectPattern, Pattern, @@ -24,7 +26,11 @@ export interface FunctionInfo { } export interface PartialRuleInfo { - create?: Node | null; + create?: + | Node + | MaybeNamedFunctionDeclaration + | MaybeNamedClassDeclaration + | null; isNewStyle?: boolean; meta?: Expression | Pattern | FunctionDeclaration; } diff --git a/lib/utils.ts b/lib/utils.ts index e1c9c20d..88d58a32 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -10,6 +10,8 @@ import type { FunctionDeclaration, FunctionExpression, Identifier, + MaybeNamedClassDeclaration, + MaybeNamedFunctionDeclaration, MemberExpression, ModuleDeclaration, Node, @@ -38,7 +40,12 @@ const functionTypes = new Set([ 'FunctionDeclaration', ]); const isFunctionType = ( - node: Node | null | undefined, + node: + | MaybeNamedClassDeclaration + | MaybeNamedFunctionDeclaration + | Node + | null + | undefined, ): node is FunctionExpression | ArrowFunctionExpression | FunctionDeclaration => !!node && functionTypes.has(node.type); @@ -124,7 +131,9 @@ function hasObjectReturn(node: Node): boolean { * Determine if the given node is likely to be a function-style rule. * @param node */ -function isFunctionRule(node: Node): boolean { +function isFunctionRule( + node: Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, +): boolean { return ( isFunctionType(node) && // Is a function expression or declaration. isNormalFunctionExpression(node) && // Is a function definition. @@ -137,7 +146,7 @@ function isFunctionRule(node: Node): boolean { * Check if the given node is a function call representing a known TypeScript rule creator format. */ function isTypeScriptRuleHelper( - node: Node, + node: Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, ): node is CallExpression & { arguments: ObjectExpression[] } { return ( node.type === 'CallExpression' && @@ -167,13 +176,18 @@ function getRuleExportsESM( }, scopeManager: Scope.ScopeManager, ): PartialRuleInfo { - const possibleNodes: Node[] = []; + const possibleNodes: ( + | Node + | MaybeNamedClassDeclaration + | Expression + | MaybeNamedFunctionDeclaration + )[] = []; for (const statement of ast.body) { switch (statement.type) { // export default rule; case 'ExportDefaultDeclaration': { - possibleNodes.push(statement.declaration as Identifier); + possibleNodes.push(statement.declaration); break; } // export = rule; From 68904440ca22865dd29ba4d145df5b6cc3d7e5c0 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 2 Aug 2025 15:55:15 -0500 Subject: [PATCH 78/82] addressed feedback in utils --- lib/utils.ts | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 88d58a32..8bce43bd 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -94,7 +94,11 @@ function collectInterestingProperties( return properties.reduce>( (parsedProps, prop) => { const keyValue = getKeyName(prop); - if (isProperty(prop) && keyValue && interestingKeys.has(keyValue as T)) { + if ( + prop.type === 'Property' && + keyValue && + interestingKeys.has(keyValue as T) + ) { // In TypeScript, unwrap any usage of `{} as const`. parsedProps[keyValue] = prop.value.type === 'TSAsExpression' @@ -204,9 +208,9 @@ function getRuleExportsESM( if (statement.declaration) { const nodes = statement.declaration.type === 'VariableDeclaration' - ? statement.declaration.declarations.map( - (declarator) => declarator.init!, - ) + ? statement.declaration.declarations + .map((declarator) => declarator.init) + .filter((init) => !!init) : [statement.declaration]; // named exports like `export const rule = { ... };` @@ -278,7 +282,8 @@ function getRuleExportsCJS( .filter((expression) => expression.type === 'AssignmentExpression') .filter((expression) => expression.left.type === 'MemberExpression') .reduce((currentExports, node) => { - const leftExpression = node.left as MemberExpression; + const leftExpression = node.left; + if (leftExpression.type !== 'MemberExpression') return currentExports; if ( leftExpression.object.type === 'Identifier' && leftExpression.object.name === 'module' && @@ -358,7 +363,7 @@ function findObjectPropertyValueByKeyName( ): Property['value'] | undefined { const property = obj.properties.find( (prop) => - isProperty(prop) && + prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === keyName, ) as Property | undefined; @@ -395,7 +400,6 @@ function findVariableValue( * If a ternary conditional expression is involved, retrieve the elements that may exist on both sides of it. * Ex: [a, b, c] will return [a, b, c] * Ex: foo ? [a, b, c] : [d, e, f] will return [a, b, c, d, e, f] - * @param node * @returns the list of elements */ function collectArrayElements(node: Node): Node[] { @@ -471,10 +475,11 @@ export function getContextIdentifiers( ): Set { const ruleInfo = getRuleInfo({ ast, scopeManager }); + const firstCreateParam = ruleInfo?.create.params[0]; if ( !ruleInfo || ruleInfo.create?.params.length === 0 || - ruleInfo.create.params[0].type !== 'Identifier' + firstCreateParam?.type !== 'Identifier' ) { return new Set(); } @@ -482,10 +487,7 @@ export function getContextIdentifiers( return new Set( scopeManager .getDeclaredVariables(ruleInfo.create) - .find( - (variable) => - variable.name === (ruleInfo.create.params[0] as Identifier).name, - )! + .find((variable) => variable.name === firstCreateParam.name)! .references.map((ref) => ref.identifier), ); } @@ -509,7 +511,9 @@ export function getKeyName( // Variable key: { [myVariable]: 'hello world' } if (scope) { const staticValue = getStaticValue(property.key, scope); - return staticValue ? (staticValue.value as string) : null; + return staticValue && typeof staticValue.value === 'string' + ? staticValue.value + : null; } // TODO: ensure scope is always passed to getKeyName() so we don't need to handle the case where it's not passed. return null; @@ -890,12 +894,12 @@ export function isSuggestionFixerFunction( return ( (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && - isProperty(parent) && + parent.type === 'Property' && parent.key.type === 'Identifier' && parent.key.name === 'fix' && parent.parent.type === 'ObjectExpression' && parent.parent.parent.type === 'ArrayExpression' && - isProperty(parent.parent.parent.parent) && + parent.parent.parent.parent.type === 'Property' && parent.parent.parent.parent.key.type === 'Identifier' && parent.parent.parent.parent.key.name === 'suggest' && parent.parent.parent.parent.parent.type === 'ObjectExpression' && @@ -950,14 +954,14 @@ export function getMetaDocsProperty( const metaNode = ruleInfo.meta ?? undefined; const docsNode = evaluateObjectProperties(metaNode, scopeManager) - .filter(isProperty) + .filter((node) => node.type === 'Property') .find((p) => getKeyName(p) === 'docs'); const metaPropertyNode = evaluateObjectProperties( docsNode?.value, scopeManager, ) - .filter(isProperty) + .filter((node) => node.type === 'Property') .find((p) => getKeyName(p) === propertyName); return { docsNode, metaNode, metaPropertyNode }; @@ -978,7 +982,7 @@ export function getMessagesNode( const metaNode = ruleInfo.meta ?? undefined; const messagesNode = evaluateObjectProperties(metaNode, scopeManager) - .filter(isProperty) + .filter((node) => node.type === 'Property') .find((p) => getKeyName(p) === 'messages'); if (messagesNode) { @@ -1011,7 +1015,6 @@ export function getMessageIdNodes( : undefined; } -const isProperty = (node: Node): node is Property => node.type === 'Property'; /** * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`. * @param messageId - the messageId to check for @@ -1027,7 +1030,7 @@ export function getMessageIdNodeById( scope: Scope.Scope, ): Property | undefined { return getMessageIdNodes(ruleInfo, scopeManager) - ?.filter(isProperty) + ?.filter((node) => node.type === 'Property') .find((p) => getKeyName(p, scope) === messageId); } @@ -1036,7 +1039,7 @@ export function getMetaSchemaNode( scopeManager: Scope.ScopeManager, ): Property | undefined { return evaluateObjectProperties(metaNode, scopeManager) - .filter(isProperty) + .filter((node) => node.type === 'Property') .find((p) => getKeyName(p) === 'schema'); } From ce4f96f144cb33cb508acb3d5765222f969af5b3 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 2 Aug 2025 19:58:48 -0500 Subject: [PATCH 79/82] ci: add typecheck step to CI workflow --- .github/workflows/main.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c68199d4..0636ad67 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,3 +45,13 @@ jobs: node-version: 'lts/*' - run: npm install - run: npm run test:remote + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - run: npm install + - run: npm run typecheck From 11fe1790be607bee2ae62ba46d4965cee53f438f Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 3 Aug 2025 16:03:51 -0500 Subject: [PATCH 80/82] removed unneeded ts-expect-error from eslint.config.ts --- eslint.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/eslint.config.ts b/eslint.config.ts index 59e5d749..ad8f1362 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -5,7 +5,6 @@ import { FlatCompat } from '@eslint/eslintrc'; import { defineConfig } from 'eslint/config'; import markdown from 'eslint-plugin-markdown'; import pluginN from 'eslint-plugin-n'; -// @ts-expect-error - eslint-plugin is not typed yet import eslintPlugin from './lib/index.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); From f28756600bf3ff3e5b64cf9ad0cc0bafee5df18d Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 3 Aug 2025 16:23:41 -0500 Subject: [PATCH 81/82] Address feedback in utils --- lib/utils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/utils.ts b/lib/utils.ts index 8bce43bd..75355270 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -813,8 +813,12 @@ export function insertProperty( if (node.properties.length === 0) { return fixer.replaceText(node, `{\n${propertyText}\n}`); } + const lastProperty = node.properties.at(-1); + if (!lastProperty) { + return fixer.replaceText(node, `{\n${propertyText}\n}`); + } return fixer.insertTextAfter( - sourceCode.getLastToken(node.properties.at(-1)!)!, + sourceCode.getLastToken(lastProperty)!, `,\n${propertyText}`, ); } From 9eeef961451b97d06e78d52f14c997130bacab57 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 3 Aug 2025 16:29:48 -0500 Subject: [PATCH 82/82] remove non-null assertion from require-meta-docs-recommended --- lib/rules/require-meta-docs-recommended.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rules/require-meta-docs-recommended.ts b/lib/rules/require-meta-docs-recommended.ts index 4eb943c8..ae3f7d8e 100644 --- a/lib/rules/require-meta-docs-recommended.ts +++ b/lib/rules/require-meta-docs-recommended.ts @@ -19,8 +19,15 @@ function insertRecommendedProperty( `{ recommended: ${recommendedValue} }`, ); } + const lastProperty = objectNode.properties.at(-1); + if (!lastProperty) { + return fixer.replaceText( + objectNode, + `{ recommended: ${recommendedValue} }`, + ); + } return fixer.insertTextAfter( - objectNode.properties.at(-1)!, + lastProperty, `, recommended: ${recommendedValue}`, ); }