From 5fce6f878a8c94975e627e85ab4f3d34e7c5b31e Mon Sep 17 00:00:00 2001 From: michael faith Date: Wed, 18 Jun 2025 20:52:15 -0500 Subject: [PATCH 01/15] ci: add node v24 to test matrix (#522) * ci: add node v24 to test matrix This change adds node v24 to the matrix for ci testing * ci: add node v24 to test matrix This change adds node v24 to the matrix for ci testing --- .github/workflows/main.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4f9204f..51902405 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,7 @@ jobs: fail-fast: false matrix: node-version: + - 24 - 23 - 22 - 21 @@ -40,13 +41,13 @@ jobs: eslint8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "lts/*" - - run: npm install - - run: npm install --save-dev eslint@8 - - run: npm test + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "lts/*" + - run: npm install + - run: npm install --save-dev eslint@8 + - run: npm test test-remote: name: eslint-remote-tester From 41a9166b6e22ea4b1ce88d405f5a9cde874ea135 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:31:05 -0400 Subject: [PATCH 02/15] chore: update typescript deps (#523) * chore: update typescript deps * try to fix test --- package.json | 8 ++++---- tests/lib/rules/no-property-in-node.js | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index be4d50fc..939af51f 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,9 @@ "@eslint/js": "^9.16.0", "@release-it/conventional-changelog": "^9.0.3", "@types/eslint": "^9.6.1", - "@types/estree": "^1.0.5", - "@typescript-eslint/parser": "^7.7.0", - "@typescript-eslint/utils": "^7.7.0", + "@types/estree": "^1.0.8", + "@typescript-eslint/parser": "^8.34.1", + "@typescript-eslint/utils": "^8.34.1", "chai": "^4.5.0", "eslint": "^9.16.0", "eslint-config-not-an-aardvark": "^2.1.0", @@ -83,7 +83,7 @@ "nyc": "^17.1.0", "prettier": "^3.4.1", "release-it": "^17.2.0", - "typescript": "^5.7.2" + "typescript": "^5.8.3" }, "peerDependencies": { "eslint": ">=8.23.0" diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.js index 62181a3b..952e976f 100644 --- a/tests/lib/rules/no-property-in-node.js +++ b/tests/lib/rules/no-property-in-node.js @@ -8,7 +8,9 @@ const ruleTester = new RuleTester({ languageOptions: { parser: require('@typescript-eslint/parser'), parserOptions: { - project: './tsconfig.json', + projectService: { + defaultProject: 'tsconfig.json', + }, tsconfigRootDir: path.join(__dirname, '../fixtures'), }, }, From c316040b2bf532d0a44a29e04720596ca3e0ebe9 Mon Sep 17 00:00:00 2001 From: michael faith Date: Thu, 19 Jun 2025 18:05:39 -0500 Subject: [PATCH 03/15] test: change test runner to vitest (#525) * test: change test runner to vitest This change moves the test runner from `mocha` + `nyc` to `vitest`. In order to import `vitest` the tests had to be in esm, so I checked out everything under the `tests` folder from https://github.com/eslint-community/eslint-plugin-eslint-plugin/pull/516 (leaving the source as it was). I also moved the `eslint-rule-tester` out of the `tests/lib` folder and into its own `utils` folder. That way vitest didn't treat it as a test. Note: rather than using `vitest`'s preferred `v8` coverage reporter, I used the `istanbul` one, to ensure the coverages between old and new were the same. I did notice that when I tried the `v8` coverage reporter, the coverage numbers were much less. Something to consider as a follow-up change. The v8 reporter *should* be more accurate, so the coverage may not be as high as it seems. I'd recommend moving up to the `v8` reporter after the esm branch lands. * test: update fixtures tsconfig to use wildcard * build: remove `globals` and packageManager config --- .gitignore | 2 +- eslint.config.js | 3 +-- package.json | 16 ++++---------- tests/lib/eslint-rule-tester.js | 15 ------------- tests/lib/fixtures/tsconfig.json | 3 ++- tests/lib/index.js | 5 ++--- tests/lib/rule-setup.js | 20 ++++++++++-------- tests/lib/rules/consistent-output.js | 6 ++---- tests/lib/rules/fixer-return.js | 6 ++---- tests/lib/rules/meta-property-ordering.js | 6 ++---- .../rules/no-deprecated-context-methods.js | 6 ++---- tests/lib/rules/no-deprecated-report-api.js | 6 ++---- tests/lib/rules/no-identical-tests.js | 6 ++---- tests/lib/rules/no-meta-replaced-by.js | 6 ++---- tests/lib/rules/no-meta-schema-default.js | 6 ++---- tests/lib/rules/no-missing-message-ids.js | 6 ++---- tests/lib/rules/no-missing-placeholders.js | 6 ++---- tests/lib/rules/no-only-tests.js | 6 ++---- tests/lib/rules/no-property-in-node.js | 11 +++++----- tests/lib/rules/no-unused-message-ids.js | 6 ++---- tests/lib/rules/no-unused-placeholders.js | 6 ++---- tests/lib/rules/no-useless-token-range.js | 6 ++---- tests/lib/rules/prefer-message-ids.js | 6 ++---- tests/lib/rules/prefer-object-rule.js | 6 ++---- tests/lib/rules/prefer-output-null.js | 6 ++---- tests/lib/rules/prefer-placeholders.js | 6 ++---- tests/lib/rules/prefer-replace-text.js | 6 ++---- tests/lib/rules/report-message-format.js | 6 ++---- .../lib/rules/require-meta-default-options.js | 9 ++++---- .../rules/require-meta-docs-description.js | 9 ++++---- .../rules/require-meta-docs-recommended.js | 9 ++++---- tests/lib/rules/require-meta-docs-url.js | 6 ++---- tests/lib/rules/require-meta-fixable.js | 6 ++---- .../lib/rules/require-meta-has-suggestions.js | 6 ++---- .../rules/require-meta-schema-description.js | 6 ++---- tests/lib/rules/require-meta-schema.js | 6 ++---- tests/lib/rules/require-meta-type.js | 6 ++---- .../lib/rules/test-case-property-ordering.js | 6 ++---- .../lib/rules/test-case-shorthand-strings.js | 6 ++---- tests/lib/utils.js | 20 +++++++++--------- tests/utils/eslint-rule-tester.js | 21 +++++++++++++++++++ vitest.config.ts | 21 +++++++++++++++++++ 42 files changed, 146 insertions(+), 186 deletions(-) delete mode 100644 tests/lib/eslint-rule-tester.js create mode 100644 tests/utils/eslint-rule-tester.js create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 642ad460..281b1c34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea -.nyc_output +coverage .vscode node_modules/ npm-debug.log diff --git a/eslint.config.js b/eslint.config.js index 24f9f4ea..b25c54b1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,6 @@ const js = require('@eslint/js'); const { FlatCompat } = require('@eslint/eslintrc'); -const globals = require('globals'); const markdown = require('eslint-plugin-markdown'); const pluginN = require('eslint-plugin-n'); const eslintPluginConfig = require('eslint-plugin-eslint-plugin/configs/all'); @@ -54,7 +53,7 @@ module.exports = [ }, { files: ['tests/**/*.js'], - languageOptions: { globals: globals.mocha }, + languageOptions: { sourceType: 'module' }, }, { files: ['**/*.md'], diff --git a/package.json b/package.json index 939af51f..63da8091 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:js-docs": "eslint --no-inline-config \"**/*.md\"", "lint:package-json": "npmPkgJsonLint .", "release": "release-it", - "test": "nyc --all --check-coverage --include lib mocha tests --recursive", + "test": "vitest run --coverage", "test:remote": "eslint-remote-tester -c ./eslint-remote-tester.config.mjs", "update:eslint-docs": "eslint-doc-generator" }, @@ -43,12 +43,6 @@ "@eslint-community/eslint-utils": "^4.4.0", "estraverse": "^5.3.0" }, - "nyc": { - "branches": 95, - "functions": 99, - "lines": 99, - "statements": 99 - }, "devDependencies": { "@commitlint/cli": "^19.6.0", "@commitlint/config-conventional": "^19.6.0", @@ -60,7 +54,7 @@ "@types/estree": "^1.0.8", "@typescript-eslint/parser": "^8.34.1", "@typescript-eslint/utils": "^8.34.1", - "chai": "^4.5.0", + "@vitest/coverage-istanbul": "^3.2.4", "eslint": "^9.16.0", "eslint-config-not-an-aardvark": "^2.1.0", "eslint-config-prettier": "^9.1.0", @@ -73,17 +67,15 @@ "eslint-remote-tester": "^4.0.1", "eslint-scope": "^8.0.1", "espree": "^10.0.1", - "globals": "^15.13.0", "husky": "^9.1.7", "lodash": "^4.17.21", "markdownlint-cli": "^0.43.0", - "mocha": "^11.0.0", "npm-package-json-lint": "^8.0.0", "npm-run-all2": "^7.0.1", - "nyc": "^17.1.0", "prettier": "^3.4.1", "release-it": "^17.2.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.4" }, "peerDependencies": { "eslint": ">=8.23.0" diff --git a/tests/lib/eslint-rule-tester.js b/tests/lib/eslint-rule-tester.js deleted file mode 100644 index dd16a8e2..00000000 --- a/tests/lib/eslint-rule-tester.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @fileoverview Helpers for tests. - * @author 唯然 - */ - -'use strict'; - -const eslintVersion = require('eslint/package.json').version; -const { RuleTester } = require('eslint'); -const { FlatRuleTester } = require('eslint/use-at-your-own-risk'); - -// greater than or equal to ESLint v9 -exports.gteEslintV9 = +eslintVersion.split('.')[0] >= 9; - -exports.RuleTester = exports.gteEslintV9 ? RuleTester : FlatRuleTester; diff --git a/tests/lib/fixtures/tsconfig.json b/tests/lib/fixtures/tsconfig.json index 0d505be7..403ce01b 100644 --- a/tests/lib/fixtures/tsconfig.json +++ b/tests/lib/fixtures/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { "moduleResolution": "NodeNext" - } + }, + "include": ["*.ts"] } diff --git a/tests/lib/index.js b/tests/lib/index.js index 794d0d57..96f52e44 100644 --- a/tests/lib/index.js +++ b/tests/lib/index.js @@ -1,7 +1,6 @@ -'use strict'; +import { assert, describe, it } from 'vitest'; -const assert = require('chai').assert; -const plugin = require('../..'); +import plugin from '../../lib/index.js'; const RULE_NAMES = Object.keys(plugin.rules); diff --git a/tests/lib/rule-setup.js b/tests/lib/rule-setup.js index a1c75e82..ab097bc3 100644 --- a/tests/lib/rule-setup.js +++ b/tests/lib/rule-setup.js @@ -1,15 +1,17 @@ -'use strict'; +import { readdirSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const { readdirSync, readFileSync } = require('fs'); -const path = require('path'); -const assert = require('chai').assert; -const plugin = require('../..'); +import { assert, describe, it } from 'vitest'; + +import plugin from '../../lib/index.js'; const RULE_NAMES = Object.keys(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(dirname, '..', 'lib', 'rules'); const files = readdirSync(filePath); assert.deepStrictEqual( @@ -34,7 +36,7 @@ describe('rule setup is correct', () => { it('should have the right contents', () => { const filePath = path.join( - __dirname, + dirname, '..', '..', 'lib', @@ -53,7 +55,7 @@ describe('rule setup is correct', () => { }); it('should have tests for all rules', () => { - const filePath = path.join(__dirname, 'rules'); + const filePath = path.join(dirname, 'rules'); const files = readdirSync(filePath); assert.deepStrictEqual( @@ -65,7 +67,7 @@ describe('rule setup is correct', () => { }); it('should have documentation for all rules', () => { - const filePath = path.join(__dirname, '..', '..', 'docs', 'rules'); + const filePath = path.join(dirname, '..', '..', 'docs', 'rules'); const files = readdirSync(filePath); assert.deepStrictEqual( diff --git a/tests/lib/rules/consistent-output.js b/tests/lib/rules/consistent-output.js index 75ecabe8..60ac1899 100644 --- a/tests/lib/rules/consistent-output.js +++ b/tests/lib/rules/consistent-output.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/consistent-output'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/consistent-output.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; const ERROR = { messageId: 'missingOutput', type: 'ObjectExpression' }; diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js index 349a9bf4..cc3798c5 100644 --- a/tests/lib/rules/fixer-return.js +++ b/tests/lib/rules/fixer-return.js @@ -3,14 +3,12 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/fixer-return'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/fixer-return.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js index 1d4dfec8..1a843e94 100644 --- a/tests/lib/rules/meta-property-ordering.js +++ b/tests/lib/rules/meta-property-ordering.js @@ -2,14 +2,12 @@ * @fileoverview Enforces the order of meta properties */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/meta-property-ordering'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/meta-property-ordering.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-deprecated-context-methods.js b/tests/lib/rules/no-deprecated-context-methods.js index 84770ccd..7338f37e 100644 --- a/tests/lib/rules/no-deprecated-context-methods.js +++ b/tests/lib/rules/no-deprecated-context-methods.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-deprecated-context-methods'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-deprecated-context-methods.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.js index 6483fbde..ed1bd422 100644 --- a/tests/lib/rules/no-deprecated-report-api.js +++ b/tests/lib/rules/no-deprecated-report-api.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-deprecated-report-api'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-deprecated-report-api.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.js index 6f0e444e..bcfcd8cd 100644 --- a/tests/lib/rules/no-identical-tests.js +++ b/tests/lib/rules/no-identical-tests.js @@ -3,14 +3,12 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-identical-tests'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-identical-tests.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; const ERROR_OBJECT_TEST = { messageId: 'identical', type: 'ObjectExpression' }; const ERROR_STRING_TEST = { messageId: 'identical', type: 'Literal' }; diff --git a/tests/lib/rules/no-meta-replaced-by.js b/tests/lib/rules/no-meta-replaced-by.js index d72f41ac..2d357a39 100644 --- a/tests/lib/rules/no-meta-replaced-by.js +++ b/tests/lib/rules/no-meta-replaced-by.js @@ -2,14 +2,12 @@ * @fileoverview Disallows the usage of `meta.replacedBy` property */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-meta-replaced-by'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-meta-replaced-by.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-meta-schema-default.js b/tests/lib/rules/no-meta-schema-default.js index 869f4971..06376284 100644 --- a/tests/lib/rules/no-meta-schema-default.js +++ b/tests/lib/rules/no-meta-schema-default.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-meta-schema-default'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-meta-schema-default.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-missing-message-ids.js b/tests/lib/rules/no-missing-message-ids.js index 78b9e600..efee8a9d 100644 --- a/tests/lib/rules/no-missing-message-ids.js +++ b/tests/lib/rules/no-missing-message-ids.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-missing-message-ids'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-missing-message-ids.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.js index 43bb137b..13767ec2 100644 --- a/tests/lib/rules/no-missing-placeholders.js +++ b/tests/lib/rules/no-missing-placeholders.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-missing-placeholders'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-missing-placeholders.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; /** * Create an error for the given key diff --git a/tests/lib/rules/no-only-tests.js b/tests/lib/rules/no-only-tests.js index 0df7e600..25257c65 100644 --- a/tests/lib/rules/no-only-tests.js +++ b/tests/lib/rules/no-only-tests.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-only-tests'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-only-tests.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.js index 952e976f..c7225404 100644 --- a/tests/lib/rules/no-property-in-node.js +++ b/tests/lib/rules/no-property-in-node.js @@ -1,12 +1,11 @@ -'use strict'; - -const RuleTester = require('../eslint-rule-tester').RuleTester; -const path = require('path'); -const rule = require('../../../lib/rules/no-property-in-node'); +import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import path from 'path'; +import rule from '../../../lib/rules/no-property-in-node.js'; +import parser from '@typescript-eslint/parser'; const ruleTester = new RuleTester({ languageOptions: { - parser: require('@typescript-eslint/parser'), + parser, parserOptions: { projectService: { defaultProject: 'tsconfig.json', diff --git a/tests/lib/rules/no-unused-message-ids.js b/tests/lib/rules/no-unused-message-ids.js index 8f5f6fa8..54184cdc 100644 --- a/tests/lib/rules/no-unused-message-ids.js +++ b/tests/lib/rules/no-unused-message-ids.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-unused-message-ids'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-unused-message-ids.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js index fd93b777..87de5cb9 100644 --- a/tests/lib/rules/no-unused-placeholders.js +++ b/tests/lib/rules/no-unused-placeholders.js @@ -3,14 +3,12 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-unused-placeholders'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-unused-placeholders.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; /** * Create an error for the given key diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.js index c4b13960..0ad6b61f 100644 --- a/tests/lib/rules/no-useless-token-range.js +++ b/tests/lib/rules/no-useless-token-range.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/no-useless-token-range'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/no-useless-token-range.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; /** * Wraps a code sample as an eslint rule diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js index ac54184e..342fb501 100644 --- a/tests/lib/rules/prefer-message-ids.js +++ b/tests/lib/rules/prefer-message-ids.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/prefer-message-ids'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/prefer-message-ids.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js index 9c0aae74..0e1583f7 100644 --- a/tests/lib/rules/prefer-object-rule.js +++ b/tests/lib/rules/prefer-object-rule.js @@ -2,14 +2,12 @@ * @author Brad Zacher */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/prefer-object-rule'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/prefer-object-rule.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-output-null.js b/tests/lib/rules/prefer-output-null.js index b7c0eda1..22e7bfb4 100644 --- a/tests/lib/rules/prefer-output-null.js +++ b/tests/lib/rules/prefer-output-null.js @@ -3,14 +3,12 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/prefer-output-null'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/prefer-output-null.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; const ERROR = { messageId: 'useOutputNull', type: 'Property' }; diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.js index f7fe49e0..2da2c535 100644 --- a/tests/lib/rules/prefer-placeholders.js +++ b/tests/lib/rules/prefer-placeholders.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/prefer-placeholders'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/prefer-placeholders.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js index c83a9559..e8dc8041 100644 --- a/tests/lib/rules/prefer-replace-text.js +++ b/tests/lib/rules/prefer-replace-text.js @@ -3,14 +3,12 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/prefer-replace-text'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/prefer-replace-text.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.js index 7ca4cf10..8a1c7338 100644 --- a/tests/lib/rules/report-message-format.js +++ b/tests/lib/rules/report-message-format.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/report-message-format'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/report-message-format.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-default-options.js b/tests/lib/rules/require-meta-default-options.js index da2b4209..b0e3df4f 100644 --- a/tests/lib/rules/require-meta-default-options.js +++ b/tests/lib/rules/require-meta-default-options.js @@ -1,7 +1,6 @@ -'use strict'; - -const rule = require('../../../lib/rules/require-meta-default-options'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-default-options.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import parser from '@typescript-eslint/parser'; const ruleTester = new RuleTester({ languageOptions: { sourceType: 'commonjs' }, @@ -149,7 +148,7 @@ ruleTester.run('require-meta-default-options', rule, { const ruleTesterTypeScript = new RuleTester({ languageOptions: { - parser: require('@typescript-eslint/parser'), + parser, parserOptions: { sourceType: 'module' }, }, }); diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js index 2b4ded37..d3aa09bd 100644 --- a/tests/lib/rules/require-meta-docs-description.js +++ b/tests/lib/rules/require-meta-docs-description.js @@ -1,11 +1,10 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/require-meta-docs-description'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-docs-description.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import parser from '@typescript-eslint/parser'; // ------------------------------------------------------------------------------ // Tests @@ -298,7 +297,7 @@ ruleTester.run('require-meta-docs-description', rule, { const ruleTesterTypeScript = new RuleTester({ languageOptions: { sourceType: 'module', - parser: require('@typescript-eslint/parser'), + parser, }, }); ruleTesterTypeScript.run('require-meta-docs-description (TypeScript)', rule, { diff --git a/tests/lib/rules/require-meta-docs-recommended.js b/tests/lib/rules/require-meta-docs-recommended.js index 1a58bacc..b50a3b93 100644 --- a/tests/lib/rules/require-meta-docs-recommended.js +++ b/tests/lib/rules/require-meta-docs-recommended.js @@ -1,7 +1,6 @@ -'use strict'; - -const rule = require('../../../lib/rules/require-meta-docs-recommended'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-docs-recommended.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import parser from '@typescript-eslint/parser'; const ruleTester = new RuleTester({ languageOptions: { sourceType: 'commonjs' }, @@ -269,7 +268,7 @@ ruleTester.run('require-meta-docs-recommended', rule, { const ruleTesterTypeScript = new RuleTester({ languageOptions: { - parser: require('@typescript-eslint/parser'), + parser, parserOptions: { sourceType: 'module' }, }, }); diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js index da7a9048..a13d63b2 100644 --- a/tests/lib/rules/require-meta-docs-url.js +++ b/tests/lib/rules/require-meta-docs-url.js @@ -4,14 +4,12 @@ * See LICENSE file in root directory for full license. */ -'use strict'; - // ----------------------------------------------------------------------------- // Requirements // ----------------------------------------------------------------------------- -const RuleTester = require('../eslint-rule-tester').RuleTester; -const rule = require('../../../lib/rules/require-meta-docs-url'); +import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import rule from '../../../lib/rules/require-meta-docs-url.js'; // ----------------------------------------------------------------------------- // Tests diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js index 8aef6c04..a0d343f3 100644 --- a/tests/lib/rules/require-meta-fixable.js +++ b/tests/lib/rules/require-meta-fixable.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/require-meta-fixable'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-fixable.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js index 0a1e96ab..9c19d011 100644 --- a/tests/lib/rules/require-meta-has-suggestions.js +++ b/tests/lib/rules/require-meta-has-suggestions.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/require-meta-has-suggestions'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-has-suggestions.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-schema-description.js b/tests/lib/rules/require-meta-schema-description.js index 1b3f4256..6fb5b741 100644 --- a/tests/lib/rules/require-meta-schema-description.js +++ b/tests/lib/rules/require-meta-schema-description.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/require-meta-schema-description'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-schema-description.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js index dfb1754d..aff9df9f 100644 --- a/tests/lib/rules/require-meta-schema.js +++ b/tests/lib/rules/require-meta-schema.js @@ -1,11 +1,9 @@ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/require-meta-schema'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-schema.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js index 2180e23e..ae52476f 100644 --- a/tests/lib/rules/require-meta-type.js +++ b/tests/lib/rules/require-meta-type.js @@ -3,14 +3,12 @@ * @author 唯然 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/require-meta-type'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/require-meta-type.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/test-case-property-ordering.js b/tests/lib/rules/test-case-property-ordering.js index 149278d1..65b8970a 100644 --- a/tests/lib/rules/test-case-property-ordering.js +++ b/tests/lib/rules/test-case-property-ordering.js @@ -3,14 +3,12 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/test-case-property-ordering'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/test-case-property-ordering.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/test-case-shorthand-strings.js b/tests/lib/rules/test-case-shorthand-strings.js index 4deddade..f358d3ee 100644 --- a/tests/lib/rules/test-case-shorthand-strings.js +++ b/tests/lib/rules/test-case-shorthand-strings.js @@ -3,14 +3,12 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const rule = require('../../../lib/rules/test-case-shorthand-strings'); -const RuleTester = require('../eslint-rule-tester').RuleTester; +import rule from '../../../lib/rules/test-case-shorthand-strings.js'; +import { RuleTester } from '../../utils/eslint-rule-tester.js'; /** * Returns the code for some valid test cases diff --git a/tests/lib/utils.js b/tests/lib/utils.js index d299abcc..22e2a539 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -1,13 +1,13 @@ -'use strict'; - -const { inspect } = require('util'); -const lodash = require('lodash'); -const espree = require('espree'); -const eslintScope = require('eslint-scope'); -const estraverse = require('estraverse'); -const assert = require('chai').assert; -const utils = require('../../lib/utils'); -const typescriptEslintParser = require('@typescript-eslint/parser'); +import { inspect } from 'node:util'; + +import typescriptEslintParser from '@typescript-eslint/parser'; +import * as eslintScope from 'eslint-scope'; +import * as espree from 'espree'; +import * as estraverse from 'estraverse'; +import lodash from 'lodash'; +import { assert, describe, it } from 'vitest'; + +import * as utils from '../../lib/utils.js'; describe('utils', () => { describe('getRuleInfo', () => { diff --git a/tests/utils/eslint-rule-tester.js b/tests/utils/eslint-rule-tester.js new file mode 100644 index 00000000..5b3c5239 --- /dev/null +++ b/tests/utils/eslint-rule-tester.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Helpers for tests. + * @author 唯然 + */ + +import { RuleTester as ESLintRuleTester } from 'eslint'; +import * as unsupportedApi from 'eslint/use-at-your-own-risk'; +import packageConfig from 'eslint/package.json' with { type: 'json' }; +import * as vitest from 'vitest'; + +const { version: eslintVersion } = packageConfig; + +const FlatRuleTester = unsupportedApi.FlatRuleTester; + +// greater than or equal to ESLint v9 +export const gteEslintV9 = +eslintVersion.split('.')[0] >= 9; + +export const RuleTester = gteEslintV9 ? ESLintRuleTester : FlatRuleTester; +RuleTester.describe = vitest.describe; +RuleTester.it = vitest.it; +RuleTester.itOnly = vitest.it.only; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..64bc7458 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/lib/**/*.js'], + exclude: ['tests/lib/fixtures/**'], + clearMocks: true, + coverage: { + all: true, + include: ['lib'], + reporter: ['html', 'lcov', 'text'], + provider: 'istanbul', + thresholds: { + statements: 95, + branches: 93, + functions: 95, + lines: 95, + }, + }, + }, +}); From 9cd5af882bf992a63d05a9b21dd4366c384b5150 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:58:02 +0100 Subject: [PATCH 04/15] feat!: move to ESM only (#516) * feat: move to ESM only * chore: move to c8 for esm support * chore: revert some mistaken replaces * fix: add no-meta-replaced-by to index * mocha timeout from 2000 to 3000 for slow no-property-in-node test * git ignore coverage/ * Revert "mocha timeout from 2000 to 3000 for slow no-property-in-node test" This reverts commit 8a8fb60a41a2449b59d2880cd73ad84dd7eed8be. * chore: remove duplicate gitignore * chore: revert comment change --------- Co-authored-by: Bryan Mishkin <698306+bmish@users.noreply.github.com> --- .eslint-doc-generatorrc.js | 8 +- .prettierrc.js | 4 +- commitlint.config.js | 4 +- configs/all-type-checked.js | 6 +- configs/all.js | 6 +- configs/recommended.js | 6 +- configs/rules-recommended.js | 6 +- configs/rules.js | 6 +- configs/tests-recommended.js | 6 +- configs/tests.js | 6 +- eslint.config.js | 19 +- lib/index.js | 85 +- lib/rules/consistent-output.js | 8 +- lib/rules/fixer-return.js | 10 +- lib/rules/meta-property-ordering.js | 7 +- lib/rules/no-deprecated-context-methods.js | 8 +- lib/rules/no-deprecated-report-api.js | 8 +- lib/rules/no-identical-tests.js | 8 +- lib/rules/no-meta-replaced-by.js | 8 +- lib/rules/no-meta-schema-default.js | 10 +- lib/rules/no-missing-message-ids.js | 8 +- lib/rules/no-missing-placeholders.js | 10 +- lib/rules/no-only-tests.js | 12 +- lib/rules/no-property-in-node.js | 8 +- lib/rules/no-unused-message-ids.js | 8 +- lib/rules/no-unused-placeholders.js | 10 +- lib/rules/no-useless-token-range.js | 8 +- lib/rules/prefer-message-ids.js | 10 +- lib/rules/prefer-object-rule.js | 8 +- lib/rules/prefer-output-null.js | 8 +- lib/rules/prefer-placeholders.js | 10 +- lib/rules/prefer-replace-text.js | 8 +- lib/rules/report-message-format.js | 10 +- lib/rules/require-meta-default-options.js | 8 +- lib/rules/require-meta-docs-description.js | 10 +- lib/rules/require-meta-docs-recommended.js | 10 +- lib/rules/require-meta-docs-url.js | 12 +- lib/rules/require-meta-fixable.js | 10 +- lib/rules/require-meta-has-suggestions.js | 10 +- lib/rules/require-meta-schema-description.js | 10 +- lib/rules/require-meta-schema.js | 8 +- lib/rules/require-meta-type.js | 10 +- lib/rules/test-case-property-ordering.js | 8 +- lib/rules/test-case-shorthand-strings.js | 8 +- lib/utils.js | 1214 +++++++++--------- package.json | 1 + tests/lib/rules/no-property-in-node.js | 4 +- 47 files changed, 845 insertions(+), 825 deletions(-) diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.js index 5f25c0a6..a3756beb 100644 --- a/.eslint-doc-generatorrc.js +++ b/.eslint-doc-generatorrc.js @@ -1,9 +1,7 @@ -'use strict'; - -const prettier = require('prettier'); +import prettier from 'prettier'; /** @type {import('eslint-doc-generator').GenerateOptions} */ -module.exports = { +const config = { ignoreConfig: [ 'all', 'all-type-checked', @@ -29,3 +27,5 @@ module.exports = { urlConfigs: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets', }; + +export default config; diff --git a/.prettierrc.js b/.prettierrc.js index 534e6d35..ae4b0277 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,5 +1,3 @@ -'use strict'; - -module.exports = { +export default { singleQuote: true, }; diff --git a/commitlint.config.js b/commitlint.config.js index 0cf61d7e..3f5e287f 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1 @@ -'use strict'; - -module.exports = { extends: ['@commitlint/config-conventional'] }; +export default { extends: ['@commitlint/config-conventional'] }; diff --git a/configs/all-type-checked.js b/configs/all-type-checked.js index 59bbf2b3..84309cba 100644 --- a/configs/all-type-checked.js +++ b/configs/all-type-checked.js @@ -3,8 +3,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/all-type-checked']; -module.exports = plugin.configs['flat/all-type-checked']; +export default config; diff --git a/configs/all.js b/configs/all.js index e237d769..cf48e487 100644 --- a/configs/all.js +++ b/configs/all.js @@ -4,8 +4,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/all']; -module.exports = plugin.configs['flat/all']; +export default config; diff --git a/configs/recommended.js b/configs/recommended.js index bb282caa..17c2ffad 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -4,8 +4,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/recommended']; -module.exports = plugin.configs['flat/recommended']; +export default config; diff --git a/configs/rules-recommended.js b/configs/rules-recommended.js index 09d8f57a..8e79f61a 100644 --- a/configs/rules-recommended.js +++ b/configs/rules-recommended.js @@ -4,8 +4,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/rules-recommended']; -module.exports = plugin.configs['flat/rules-recommended']; +export default config; diff --git a/configs/rules.js b/configs/rules.js index 71ca5852..f3414360 100644 --- a/configs/rules.js +++ b/configs/rules.js @@ -4,8 +4,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/rules']; -module.exports = plugin.configs['flat/rules']; +export default config; diff --git a/configs/tests-recommended.js b/configs/tests-recommended.js index 0a467239..367f5d17 100644 --- a/configs/tests-recommended.js +++ b/configs/tests-recommended.js @@ -4,8 +4,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/tests-recommended']; -module.exports = plugin.configs['flat/tests-recommended']; +export default config; diff --git a/configs/tests.js b/configs/tests.js index 184d3bab..38ced25c 100644 --- a/configs/tests.js +++ b/configs/tests.js @@ -4,8 +4,8 @@ * @author 唯然 */ -'use strict'; +import plugin from '../lib/index.js'; -const plugin = require('../lib/index.js'); +const config = plugin.configs['flat/tests']; -module.exports = plugin.configs['flat/tests']; +export default config; diff --git a/eslint.config.js b/eslint.config.js index b25c54b1..e789fccf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,17 +1,18 @@ -'use strict'; - -const js = require('@eslint/js'); -const { FlatCompat } = require('@eslint/eslintrc'); -const markdown = require('eslint-plugin-markdown'); -const pluginN = require('eslint-plugin-n'); -const eslintPluginConfig = require('eslint-plugin-eslint-plugin/configs/all'); +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; +import markdown from 'eslint-plugin-markdown'; +import pluginN from 'eslint-plugin-n'; +import eslintPluginConfig from 'eslint-plugin-eslint-plugin/configs/all'; +const dirname = path.dirname(fileURLToPath(import.meta.url)); const compat = new FlatCompat({ - baseDirectory: __dirname, + baseDirectory: dirname, recommendedConfig: js.configs.recommended, }); -module.exports = [ +export default [ ...compat.extends( 'not-an-aardvark/node', 'plugin:@eslint-community/eslint-comments/recommended', diff --git a/lib/index.js b/lib/index.js index d71340a3..83f4a93e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,15 +3,44 @@ * @author Teddy Katz */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const fs = require('fs'); -const path = require('path'); -const packageMetadata = require('../package'); +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 configFilters = { @@ -31,16 +60,40 @@ const configFilters = { // ------------------------------------------------------------------------------ // import all rules in lib/rules -const allRules = Object.fromEntries( - fs - .readdirSync(`${__dirname}/rules`) - .filter((fileName) => fileName.endsWith('.js') && /^[^._]/.test(fileName)) - .map((fileName) => fileName.replace(/\.js$/, '')) - .map((ruleName) => [ - ruleName, - require(path.join(__dirname, 'rules', ruleName)), - ]), -); +const allRules = { + 'consistent-output': consistentOutput, + 'fixer-return': fixerReturn, + 'meta-property-ordering': metaPropertyOrdering, + 'no-deprecated-context-methods': noDeprecatedContextMethods, + 'no-deprecated-report-api': noDeprecatedReportApi, + 'no-identical-tests': noIdenticalTests, + 'no-meta-replaced-by': noMetaReplacedBy, + 'no-meta-schema-default': noMetaSchemaDefault, + 'no-missing-message-ids': noMissingMessageIds, + 'no-missing-placeholders': noMissingPlaceholders, + 'no-only-tests': noOnlyTests, + 'no-property-in-node': noPropertyInNode, + 'no-unused-message-ids': noUnusedMessageIds, + 'no-unused-placeholders': noUnusedPlaceholders, + 'no-useless-token-range': noUselessTokenRange, + 'prefer-message-ids': preferMessageIds, + 'prefer-object-rule': preferObjectRule, + 'prefer-output-null': preferOutputNull, + 'prefer-placeholders': preferPlaceholders, + 'prefer-replace-text': preferReplaceText, + 'report-message-format': reportMessageFormat, + 'require-meta-default-options': requireMetaDefaultOptions, + 'require-meta-docs-description': requireMetaDocsDescription, + 'require-meta-docs-recommended': requireMetaDocsRecommended, + 'require-meta-docs-url': requireMetaDocsUrl, + 'require-meta-fixable': requireMetaFixable, + 'require-meta-has-suggestions': requireMetaHasSuggestions, + 'require-meta-schema-description': requireMetaSchemaDescription, + 'require-meta-schema': requireMetaSchema, + 'require-meta-type': requireMetaType, + 'test-case-property-ordering': testCasePropertyOrdering, + 'test-case-shorthand-strings': testCaseShorthandStrings, +}; /** @type {import("eslint").ESLint.Plugin} */ const plugin = { @@ -87,4 +140,4 @@ Object.assign( }, {}), ); -module.exports = plugin; +export default plugin; diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.js index 42cbf3ae..b0eb7f93 100644 --- a/lib/rules/consistent-output.js +++ b/lib/rules/consistent-output.js @@ -3,16 +3,14 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -71,3 +69,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js index 11e7d57f..0f29d8ac 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.js @@ -3,21 +3,19 @@ * @author 薛定谔的猫 */ -'use strict'; - // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ -const utils = require('../utils'); -const { getStaticValue } = require('@eslint-community/eslint-utils'); +import * as utils from '../utils.js'; +import { getStaticValue } from '@eslint-community/eslint-utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -167,3 +165,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.js index 27e29218..1871187e 100644 --- a/lib/rules/meta-property-ordering.js +++ b/lib/rules/meta-property-ordering.js @@ -2,9 +2,8 @@ * @fileoverview Enforces the order of meta properties */ -'use strict'; +import * as utils from '../utils.js'; -const utils = require('../utils'); const { getKeyName, getRuleInfo } = utils; const defaultOrder = [ @@ -24,7 +23,7 @@ const defaultOrder = [ // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -111,3 +110,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js index c4f6134b..416ec988 100644 --- a/lib/rules/no-deprecated-context-methods.js +++ b/lib/rules/no-deprecated-context-methods.js @@ -3,9 +3,7 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; const DEPRECATED_PASSTHROUGHS = { getSource: 'getText', @@ -35,7 +33,7 @@ const DEPRECATED_PASSTHROUGHS = { // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -96,3 +94,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.js index eb0c07c2..a0e3f830 100644 --- a/lib/rules/no-deprecated-report-api.js +++ b/lib/rules/no-deprecated-report-api.js @@ -3,16 +3,14 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -81,3 +79,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.js index c7c5867f..2dfce64c 100644 --- a/lib/rules/no-identical-tests.js +++ b/lib/rules/no-identical-tests.js @@ -3,16 +3,14 @@ * @author 薛定谔的猫 */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -85,3 +83,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-meta-replaced-by.js b/lib/rules/no-meta-replaced-by.js index 36aa1297..e6f59523 100644 --- a/lib/rules/no-meta-replaced-by.js +++ b/lib/rules/no-meta-replaced-by.js @@ -2,16 +2,14 @@ * @fileoverview Disallows the usage of `meta.replacedBy` property */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -61,3 +59,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-meta-schema-default.js b/lib/rules/no-meta-schema-default.js index ccba978a..26190088 100644 --- a/lib/rules/no-meta-schema-default.js +++ b/lib/rules/no-meta-schema-default.js @@ -1,14 +1,12 @@ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -111,3 +109,5 @@ module.exports = { } }, }; + +export default rule; diff --git a/lib/rules/no-missing-message-ids.js b/lib/rules/no-missing-message-ids.js index 0bc2d9ed..214a9e35 100644 --- a/lib/rules/no-missing-message-ids.js +++ b/lib/rules/no-missing-message-ids.js @@ -1,13 +1,11 @@ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -99,3 +97,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js index 996e8004..f4f126db 100644 --- a/lib/rules/no-missing-placeholders.js +++ b/lib/rules/no-missing-placeholders.js @@ -3,17 +3,15 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); -const { getStaticValue } = require('@eslint-community/eslint-utils'); +import * as utils from '../utils.js'; +import { getStaticValue } from '@eslint-community/eslint-utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -129,3 +127,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js index 3aa267d2..43bf79d1 100644 --- a/lib/rules/no-only-tests.js +++ b/lib/rules/no-only-tests.js @@ -1,14 +1,12 @@ -'use strict'; - -const utils = require('../utils'); -const { +import * as utils from '../utils.js'; +import { isCommaToken, isOpeningBraceToken, isClosingBraceToken, -} = require('@eslint-community/eslint-utils'); +} from '@eslint-community/eslint-utils'; /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -96,3 +94,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-property-in-node.js b/lib/rules/no-property-in-node.js index f21e0060..5a92b749 100644 --- a/lib/rules/no-property-in-node.js +++ b/lib/rules/no-property-in-node.js @@ -1,6 +1,4 @@ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; const defaultTypedNodeSourceFileTesters = [ /@types[/\\]estree[/\\]index\.d\.ts/, @@ -47,7 +45,7 @@ function isAstNodeType(type, typedNodeSourceFileTesters) { } /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -108,3 +106,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.js index 69036140..492e817d 100644 --- a/lib/rules/no-unused-message-ids.js +++ b/lib/rules/no-unused-message-ids.js @@ -1,13 +1,11 @@ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -137,3 +135,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js index d7169c9a..7fbf9cf6 100644 --- a/lib/rules/no-unused-placeholders.js +++ b/lib/rules/no-unused-placeholders.js @@ -3,17 +3,15 @@ * @author 薛定谔的猫 */ -'use strict'; - -const utils = require('../utils'); -const { getStaticValue } = require('@eslint-community/eslint-utils'); +import * as utils from '../utils.js'; +import { getStaticValue } from '@eslint-community/eslint-utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -125,3 +123,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.js index 6c6d6214..5cf35fec 100644 --- a/lib/rules/no-useless-token-range.js +++ b/lib/rules/no-useless-token-range.js @@ -3,16 +3,14 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -172,3 +170,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js index cdf66642..cefbab65 100644 --- a/lib/rules/prefer-message-ids.js +++ b/lib/rules/prefer-message-ids.js @@ -1,14 +1,12 @@ -'use strict'; - -const utils = require('../utils'); -const { getStaticValue } = require('@eslint-community/eslint-utils'); +import * as utils from '../utils.js'; +import { getStaticValue } from '@eslint-community/eslint-utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -107,3 +105,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js index 1b0e98f1..85faaedf 100644 --- a/lib/rules/prefer-object-rule.js +++ b/lib/rules/prefer-object-rule.js @@ -2,16 +2,14 @@ * @author Brad Zacher */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -81,3 +79,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js index 4d9e90e5..99819814 100644 --- a/lib/rules/prefer-output-null.js +++ b/lib/rules/prefer-output-null.js @@ -3,16 +3,14 @@ * @author 薛定谔的猫 */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -75,3 +73,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.js index f5c88fc1..4ace1011 100644 --- a/lib/rules/prefer-placeholders.js +++ b/lib/rules/prefer-placeholders.js @@ -3,17 +3,15 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); -const { findVariable } = require('@eslint-community/eslint-utils'); +import * as utils from '../utils.js'; +import { findVariable } from '@eslint-community/eslint-utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -100,3 +98,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js index 173cc509..2a1041e1 100644 --- a/lib/rules/prefer-replace-text.js +++ b/lib/rules/prefer-replace-text.js @@ -3,16 +3,14 @@ * @author 薛定谔的猫 */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -89,3 +87,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.js index 4fce24b8..b9d7588f 100644 --- a/lib/rules/report-message-format.js +++ b/lib/rules/report-message-format.js @@ -3,17 +3,15 @@ * @author Teddy Katz */ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -137,3 +135,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/require-meta-default-options.js b/lib/rules/require-meta-default-options.js index 9ac37575..1e5d0a9c 100644 --- a/lib/rules/require-meta-default-options.js +++ b/lib/rules/require-meta-default-options.js @@ -1,9 +1,7 @@ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -102,3 +100,5 @@ module.exports = { return {}; }, }; + +export default rule; diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js index b3cf5272..d2101144 100644 --- a/lib/rules/require-meta-docs-description.js +++ b/lib/rules/require-meta-docs-description.js @@ -1,7 +1,5 @@ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -10,7 +8,7 @@ const utils = require('../utils'); const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)'); /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -102,3 +100,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/require-meta-docs-recommended.js b/lib/rules/require-meta-docs-recommended.js index 67851ffc..5ba260c4 100644 --- a/lib/rules/require-meta-docs-recommended.js +++ b/lib/rules/require-meta-docs-recommended.js @@ -1,7 +1,5 @@ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; /** * @param {import('eslint').Rule.RuleFixer} fixer @@ -22,7 +20,7 @@ function insertRecommendedProperty(fixer, objectNode, recommendedValue) { } /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -124,3 +122,5 @@ module.exports = { return {}; }, }; + +export default rule; diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js index cb3e35d3..12bb2b35 100644 --- a/lib/rules/require-meta-docs-url.js +++ b/lib/rules/require-meta-docs-url.js @@ -2,22 +2,20 @@ * @author Toru Nagashima */ -'use strict'; - // ----------------------------------------------------------------------------- // Requirements // ----------------------------------------------------------------------------- -const path = require('path'); -const utils = require('../utils'); -const { getStaticValue } = require('@eslint-community/eslint-utils'); +import path from 'path'; +import * as utils from '../utils.js'; +import { getStaticValue } from '@eslint-community/eslint-utils'; // ----------------------------------------------------------------------------- // Rule Definition // ----------------------------------------------------------------------------- /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -165,3 +163,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.js index e69ca4fc..0eb99eb6 100644 --- a/lib/rules/require-meta-fixable.js +++ b/lib/rules/require-meta-fixable.js @@ -3,17 +3,15 @@ * @author Teddy Katz */ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -136,3 +134,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js index c8f75b3f..b9e2f44b 100644 --- a/lib/rules/require-meta-has-suggestions.js +++ b/lib/rules/require-meta-has-suggestions.js @@ -1,14 +1,12 @@ -'use strict'; - -const utils = require('../utils'); -const { getStaticValue } = require('@eslint-community/eslint-utils'); +import * as utils from '../utils.js'; +import { getStaticValue } from '@eslint-community/eslint-utils'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -165,3 +163,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/require-meta-schema-description.js b/lib/rules/require-meta-schema-description.js index 842d576f..f9bfe7b0 100644 --- a/lib/rules/require-meta-schema-description.js +++ b/lib/rules/require-meta-schema-description.js @@ -1,14 +1,12 @@ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -112,3 +110,5 @@ module.exports = { } }, }; + +export default rule; diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js index 59cf4575..45d28c91 100644 --- a/lib/rules/require-meta-schema.js +++ b/lib/rules/require-meta-schema.js @@ -1,13 +1,11 @@ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -139,3 +137,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js index a6aec027..d0d77103 100644 --- a/lib/rules/require-meta-type.js +++ b/lib/rules/require-meta-type.js @@ -3,10 +3,8 @@ * @author 薛定谔的猫 */ -'use strict'; - -const { getStaticValue } = require('@eslint-community/eslint-utils'); -const utils = require('../utils'); +import { getStaticValue } from '@eslint-community/eslint-utils'; +import * as utils from '../utils.js'; const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']); // ------------------------------------------------------------------------------ @@ -14,7 +12,7 @@ const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']); // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'problem', docs: { @@ -75,3 +73,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.js index f6b4dd2f..911d39a1 100644 --- a/lib/rules/test-case-property-ordering.js +++ b/lib/rules/test-case-property-ordering.js @@ -3,9 +3,7 @@ * @author 薛定谔的猫 */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; const defaultOrder = [ 'filename', @@ -25,7 +23,7 @@ const defaultOrder = [ // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -109,3 +107,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.js index 0cf22333..81cd4f57 100644 --- a/lib/rules/test-case-shorthand-strings.js +++ b/lib/rules/test-case-shorthand-strings.js @@ -3,16 +3,14 @@ * @author Teddy Katz */ -'use strict'; - -const utils = require('../utils'); +import * as utils from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const rule = { meta: { type: 'suggestion', docs: { @@ -127,3 +125,5 @@ module.exports = { }; }, }; + +export default rule; diff --git a/lib/utils.js b/lib/utils.js index bc15d2c0..a672d491 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,10 +1,5 @@ -'use strict'; - -const { - getStaticValue, - findVariable, -} = require('@eslint-community/eslint-utils'); -const estraverse = require('estraverse'); +import { getStaticValue, findVariable } from '@eslint-community/eslint-utils'; +import estraverse from 'estraverse'; const functionTypes = new Set([ 'FunctionExpression', @@ -46,7 +41,7 @@ const INTERESTING_RULE_KEYS = new Set(['create', 'meta']); */ function collectInterestingProperties(properties, interestingKeys) { return properties.reduce((parsedProps, prop) => { - const keyValue = module.exports.getKeyName(prop); + const keyValue = getKeyName(prop); if (interestingKeys.has(keyValue)) { // In TypeScript, unwrap any usage of `{} as const`. parsedProps[keyValue] = @@ -343,685 +338,656 @@ function collectArrayElements(node) { return []; } -module.exports = { - /** - * 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 - 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`. - */ - getRuleInfo({ ast, scopeManager }) { - const exportNodes = - ast.sourceType === 'module' - ? getRuleExportsESM(ast, scopeManager) - : getRuleExportsCJS(ast, scopeManager); - - const createExists = Object.prototype.hasOwnProperty.call( - exportNodes, - 'create', - ); - if (!createExists) { - return null; - } +/** +* 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 +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 }) { + const exportNodes = + ast.sourceType === 'module' + ? getRuleExportsESM(ast, scopeManager) + : getRuleExportsCJS(ast, scopeManager); + + const createExists = Object.prototype.hasOwnProperty.call( + exportNodes, + 'create', + ); + 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); - if (value) { - exportNodes[key] = value; - } + // 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); + if (value) { + exportNodes[key] = value; } } + } - const createIsFunction = isNormalFunctionExpression(exportNodes.create); - if (!createIsFunction) { - return null; - } + const createIsFunction = isNormalFunctionExpression(exportNodes.create); + if (!createIsFunction) { + return null; + } - return Object.assign({ isNewStyle: true, meta: null }, exportNodes); - }, + return Object.assign({ isNewStyle: true, meta: null }, exportNodes); +} - /** - * 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 - */ - getContextIdentifiers(scopeManager, ast) { - const ruleInfo = module.exports.getRuleInfo({ ast, scopeManager }); +/** + * 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 + */ +export function getContextIdentifiers(scopeManager, ast) { + const ruleInfo = getRuleInfo({ ast, scopeManager }); - if ( - !ruleInfo || - ruleInfo.create.params.length === 0 || - ruleInfo.create.params[0].type !== 'Identifier' - ) { - return new Set(); - } + if ( + !ruleInfo || + ruleInfo.create.params.length === 0 || + ruleInfo.create.params[0].type !== 'Identifier' + ) { + return new Set(); + } - return new Set( - scopeManager - .getDeclaredVariables(ruleInfo.create) - .find((variable) => variable.name === ruleInfo.create.params[0].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. - */ - getKeyName(property, scope) { - if (!property.key) { - // likely a SpreadElement or another non-standard node - return null; - } - if (property.key.type === 'Identifier') { - if (property.computed) { - // Variable key: { [myVariable]: 'hello world' } - if (scope) { - const staticValue = getStaticValue(property.key, scope); - return staticValue ? 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; - } - return property.key.name; - } - if (property.key.type === 'Literal') { - return '' + property.key.value; - } - if ( - property.key.type === 'TemplateLiteral' && - property.key.quasis.length === 1 - ) { - return property.key.quasis[0].value.cooked; - } + return new Set( + scopeManager + .getDeclaredVariables(ruleInfo.create) + .find((variable) => variable.name === ruleInfo.create.params[0].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. + */ +export function getKeyName(property, scope) { + if (!property.key) { + // likely a SpreadElement or another non-standard node return null; - }, - - /** - * Extracts the body of a function if the given node is a function - * - * @param {ASTNode} node - * @returns {ExpressionStatement[]} - */ - extractFunctionBody(node) { - if ( - node.type === 'ArrowFunctionExpression' || - node.type === 'FunctionExpression' - ) { - if (node.body.type === 'BlockStatement') { - return node.body.body; + } + if (property.key.type === 'Identifier') { + if (property.computed) { + // Variable key: { [myVariable]: 'hello world' } + if (scope) { + const staticValue = getStaticValue(property.key, scope); + return staticValue ? staticValue.value : null; } - - return [node.body]; + // TODO: ensure scope is always passed to getKeyName() so we don't need to handle the case where it's not passed. + return null; } + return property.key.name; + } + if (property.key.type === 'Literal') { + return '' + property.key.value; + } + if ( + property.key.type === 'TemplateLiteral' && + property.key.quasis.length === 1 + ) { + return property.key.quasis[0].value.cooked; + } + return null; +} - return []; - }, - - /** - * 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[]} - */ - checkStatementsForTestInfo( - context, - statements, - variableIdentifiers = new Set(), +/** + * Extracts the body of a function if the given node is a function + * + * @param {ASTNode} node + * @returns {ExpressionStatement[]} + */ +export function extractFunctionBody(node) { + if ( + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' ) { - const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9 - const runCalls = []; - - for (const statement of statements) { - if (statement.type === 'VariableDeclaration') { - for (const declarator of statement.declarations) { - if (!declarator.init) { - continue; - } + if (node.body.type === 'BlockStatement') { + return node.body.body; + } - const extracted = module.exports.extractFunctionBody(declarator.init); + return [node.body]; + } - runCalls.push( - ...module.exports.checkStatementsForTestInfo( - context, - extracted, - variableIdentifiers, - ), - ); + return []; +} - if ( - isRuleTesterConstruction(declarator.init) && - declarator.id.type === 'Identifier' - ) { - const vars = sourceCode.getDeclaredVariables - ? sourceCode.getDeclaredVariables(declarator) - : context.getDeclaredVariables(declarator); - vars.forEach((variable) => { - variable.references - .filter((ref) => ref.isRead()) - .forEach((ref) => variableIdentifiers.add(ref.identifier)); - }); - } +/** + * 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[]} + */ +export function checkStatementsForTestInfo( + context, + statements, + variableIdentifiers = new Set(), +) { + const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9 + const runCalls = []; + + for (const statement of statements) { + if (statement.type === 'VariableDeclaration') { + for (const declarator of statement.declarations) { + if (!declarator.init) { + continue; } - } - if (statement.type === 'FunctionDeclaration') { + const extracted = extractFunctionBody(declarator.init); + runCalls.push( - ...module.exports.checkStatementsForTestInfo( + ...checkStatementsForTestInfo( context, - statement.body.body, + extracted, variableIdentifiers, ), ); + + if ( + isRuleTesterConstruction(declarator.init) && + declarator.id.type === 'Identifier' + ) { + const vars = sourceCode.getDeclaredVariables + ? sourceCode.getDeclaredVariables(declarator) + : context.getDeclaredVariables(declarator); + vars.forEach((variable) => { + variable.references + .filter((ref) => ref.isRead()) + .forEach((ref) => variableIdentifiers.add(ref.identifier)); + }); + } } + } - if (statement.type === 'IfStatement') { - const body = - statement.consequent.type === 'BlockStatement' - ? statement.consequent.body - : [statement.consequent]; + if (statement.type === 'FunctionDeclaration') { + runCalls.push( + ...checkStatementsForTestInfo( + context, + statement.body.body, + variableIdentifiers, + ), + ); + } - runCalls.push( - ...module.exports.checkStatementsForTestInfo( - context, - body, - variableIdentifiers, - ), - ); + if (statement.type === 'IfStatement') { + const body = + statement.consequent.type === 'BlockStatement' + ? statement.consequent.body + : [statement.consequent]; - continue; - } + runCalls.push( + ...checkStatementsForTestInfo(context, body, variableIdentifiers), + ); - const expression = - statement.type === 'ExpressionStatement' - ? statement.expression - : statement; + continue; + } - if (expression.type !== 'CallExpression') { - continue; - } + const expression = + statement.type === 'ExpressionStatement' + ? statement.expression + : statement; - for (const arg of expression.arguments) { - const extracted = module.exports.extractFunctionBody(arg); + if (expression.type !== 'CallExpression') { + continue; + } - runCalls.push( - ...module.exports.checkStatementsForTestInfo( - context, - extracted, - variableIdentifiers, - ), - ); - } + for (const arg of expression.arguments) { + const extracted = extractFunctionBody(arg); - if ( - expression.callee.type === 'MemberExpression' && - (isRuleTesterConstruction(expression.callee.object) || - variableIdentifiers.has(expression.callee.object)) && - expression.callee.property.type === 'Identifier' && - expression.callee.property.name === 'run' - ) { - runCalls.push(expression); - } + runCalls.push( + ...checkStatementsForTestInfo(context, extracted, variableIdentifiers), + ); } - return runCalls; - }, - - /** - * 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 - */ - getTestInfo(context, ast) { - const runCalls = module.exports.checkStatementsForTestInfo( - context, - ast.body, - ); + if ( + expression.callee.type === 'MemberExpression' && + (isRuleTesterConstruction(expression.callee.object) || + variableIdentifiers.has(expression.callee.object)) && + expression.callee.property.type === 'Identifier' && + expression.callee.property.name === 'run' + ) { + runCalls.push(expression); + } + } + + return runCalls; +} + +/** + * 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 + */ +export function getTestInfo(context, ast) { + const runCalls = checkStatementsForTestInfo(context, ast.body); + + return runCalls + .filter( + (call) => + call.arguments.length >= 3 && + call.arguments[2].type === 'ObjectExpression', + ) + .map((call) => call.arguments[2]) + .map((run) => { + const validProperty = run.properties.find( + (prop) => getKeyName(prop) === 'valid', + ); + const invalidProperty = run.properties.find( + (prop) => getKeyName(prop) === 'invalid', + ); + + return { + valid: + validProperty && validProperty.value.type === 'ArrayExpression' + ? validProperty.value.elements.filter(Boolean) + : [], + invalid: + invalidProperty && invalidProperty.value.type === 'ArrayExpression' + ? invalidProperty.value.elements.filter(Boolean) + : [], + }; + }); +} + +/** + * Gets information on a report, given the ASTNode of context.report(). + * @param {ASTNode} node The ASTNode of context.report() + * @param {Context} context + */ +export function getReportInfo(node, context) { + const reportArgs = node.arguments; + + // If there is exactly one argument, the API expects an object. + // Otherwise, if the second argument is a string, the arguments are interpreted as + // ['node', 'message', 'data', 'fix']. + // Otherwise, the arguments are interpreted as ['node', 'loc', 'message', 'data', 'fix']. + + if (reportArgs.length === 0) { + return null; + } + + if (reportArgs.length === 1) { + if (reportArgs[0].type === 'ObjectExpression') { + return reportArgs[0].properties.reduce((reportInfo, property) => { + const propName = getKeyName(property); + + if (propName !== null) { + return Object.assign(reportInfo, { [propName]: property.value }); + } + return reportInfo; + }, {}); + } + return null; + } + + 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 secondArgStaticValue = getStaticValue(reportArgs[1], scope); - return runCalls + if ( + (secondArgStaticValue && typeof secondArgStaticValue.value === 'string') || + reportArgs[1].type === 'TemplateLiteral' + ) { + keys = ['node', 'message', 'data', 'fix']; + } else if ( + reportArgs[1].type === 'ObjectExpression' || + reportArgs[1].type === 'ArrayExpression' || + (reportArgs[1].type === 'Literal' && + typeof reportArgs[1].value !== 'string') || + (secondArgStaticValue && + ['object', 'number'].includes(typeof secondArgStaticValue.value)) + ) { + keys = ['node', 'loc', 'message', 'data', 'fix']; + } else { + // Otherwise, we can't statically determine what argument means what, so no safe fix is possible. + return null; + } + + return Object.fromEntries( + keys + .slice(0, reportArgs.length) + .map((key, index) => [key, reportArgs[index]]), + ); +} + +/** + * Gets a set of all `sourceCode` identifiers. + * @param {ScopeManager} 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) { + return new Set( + [...getContextIdentifiers(scopeManager, ast)] .filter( - (call) => - call.arguments.length >= 3 && - call.arguments[2].type === 'ObjectExpression', + (identifier) => + identifier.parent && + identifier.parent.type === 'MemberExpression' && + identifier === identifier.parent.object && + identifier.parent.property.type === 'Identifier' && + identifier.parent.property.name === 'getSourceCode' && + identifier.parent.parent.type === 'CallExpression' && + identifier.parent === identifier.parent.parent.callee && + identifier.parent.parent.parent.type === 'VariableDeclarator' && + identifier.parent.parent === identifier.parent.parent.parent.init && + identifier.parent.parent.parent.id.type === 'Identifier', ) - .map((call) => call.arguments[2]) - .map((run) => { - const validProperty = run.properties.find( - (prop) => module.exports.getKeyName(prop) === 'valid', - ); - const invalidProperty = run.properties.find( - (prop) => module.exports.getKeyName(prop) === 'invalid', - ); + .flatMap((identifier) => + scopeManager.getDeclaredVariables(identifier.parent.parent.parent), + ) + .flatMap((variable) => variable.references) + .map((ref) => ref.identifier), + ); +} + +/** + * 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} + */ +export function insertProperty(fixer, node, propertyText, sourceCode) { + if (node.properties.length === 0) { + return fixer.replaceText(node, `{\n${propertyText}\n}`); + } + return fixer.insertTextAfter( + sourceCode.getLastToken(node.properties.at(-1)), + `,\n${propertyText}`, + ); +} +/** + * Collect all context.report({...}) violation/suggestion-related nodes into a standardized array for convenience. + * @param {Object} reportInfo - Result of getReportInfo(). + * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[] + */ +export function collectReportViolationAndSuggestionData(reportInfo) { + return [ + // Violation message + { + messageId: reportInfo.messageId, + message: reportInfo.message, + data: reportInfo.data, + fix: reportInfo.fix, + }, + // Suggestion messages + ...collectArrayElements(reportInfo.suggest) + .map((suggestObjNode) => { + if (suggestObjNode.type !== 'ObjectExpression') { + // Ignore non-objects (like variables or function calls). + return null; + } return { - valid: - validProperty && validProperty.value.type === 'ArrayExpression' - ? validProperty.value.elements.filter(Boolean) - : [], - invalid: - invalidProperty && invalidProperty.value.type === 'ArrayExpression' - ? invalidProperty.value.elements.filter(Boolean) - : [], + messageId: findObjectPropertyValueByKeyName( + suggestObjNode, + 'messageId', + ), + message: findObjectPropertyValueByKeyName(suggestObjNode, 'desc'), // Note: suggestion message named `desc` + data: findObjectPropertyValueByKeyName(suggestObjNode, 'data'), + fix: findObjectPropertyValueByKeyName(suggestObjNode, 'fix'), }; - }); - }, - - /** - * Gets information on a report, given the ASTNode of context.report(). - * @param {ASTNode} node The ASTNode of context.report() - * @param {Context} context - */ - getReportInfo(node, context) { - const reportArgs = node.arguments; - - // If there is exactly one argument, the API expects an object. - // Otherwise, if the second argument is a string, the arguments are interpreted as - // ['node', 'message', 'data', 'fix']. - // Otherwise, the arguments are interpreted as ['node', 'loc', 'message', 'data', 'fix']. - - if (reportArgs.length === 0) { - return null; - } + }) + .filter((item) => item !== null), + ]; +} - if (reportArgs.length === 1) { - if (reportArgs[0].type === 'ObjectExpression') { - return reportArgs[0].properties.reduce((reportInfo, property) => { - const propName = module.exports.getKeyName(property); +/** + * Whether the provided node represents an autofixer function. + * @param {Node} node + * @param {Node[]} contextIdentifiers + * @returns {boolean} + */ +export function isAutoFixerFunction(node, contextIdentifiers) { + 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.property.name === 'report' && + getReportInfo(parent.parent.parent).fix === node + ); +} - if (propName !== null) { - return Object.assign(reportInfo, { [propName]: property.value }); - } - return reportInfo; - }, {}); +/** + * Whether the provided node represents a suggestion fixer function. + * @param {Node} node + * @param {Node[]} contextIdentifiers + * @returns {boolean} + */ +export function isSuggestionFixerFunction(node, contextIdentifiers) { + const parent = node.parent; + return ( + (node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression') && + parent.type === 'Property' && + parent.key.type === 'Identifier' && + parent.key.name === 'fix' && + parent.parent.type === 'ObjectExpression' && + parent.parent.parent.type === 'ArrayExpression' && + 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' && + parent.parent.parent.parent.parent.parent.type === 'CallExpression' && + contextIdentifiers.has( + parent.parent.parent.parent.parent.parent.callee.object, + ) && + parent.parent.parent.parent.parent.parent.callee.property.name === + 'report' && + getReportInfo(parent.parent.parent.parent.parent.parent).suggest === + parent.parent.parent + ); +} + +/** + * 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 + */ +export function evaluateObjectProperties(objectNode, scopeManager) { + if (!objectNode || objectNode.type !== 'ObjectExpression') { + return []; + } + + return objectNode.properties.flatMap((property) => { + if (property.type === 'SpreadElement') { + const value = findVariableValue(property.argument, scopeManager); + if (value && value.type === 'ObjectExpression') { + return value.properties; } - return null; + return []; } + return [property]; + }); +} - 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 secondArgStaticValue = getStaticValue(reportArgs[1], scope); +export function getMetaDocsProperty(propertyName, ruleInfo, scopeManager) { + const metaNode = ruleInfo.meta; - if ( - (secondArgStaticValue && - typeof secondArgStaticValue.value === 'string') || - reportArgs[1].type === 'TemplateLiteral' - ) { - keys = ['node', 'message', 'data', 'fix']; - } else if ( - reportArgs[1].type === 'ObjectExpression' || - reportArgs[1].type === 'ArrayExpression' || - (reportArgs[1].type === 'Literal' && - typeof reportArgs[1].value !== 'string') || - (secondArgStaticValue && - ['object', 'number'].includes(typeof secondArgStaticValue.value)) - ) { - keys = ['node', 'loc', 'message', 'data', 'fix']; - } else { - // Otherwise, we can't statically determine what argument means what, so no safe fix is possible. - return null; - } + const docsNode = evaluateObjectProperties(metaNode, scopeManager).find( + (p) => p.type === 'Property' && getKeyName(p) === 'docs', + ); - return Object.fromEntries( - keys - .slice(0, reportArgs.length) - .map((key, index) => [key, reportArgs[index]]), - ); - }, - - /** - * Gets a set of all `sourceCode` identifiers. - * @param {ScopeManager} 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. - */ - getSourceCodeIdentifiers(scopeManager, ast) { - return new Set( - [...module.exports.getContextIdentifiers(scopeManager, ast)] - .filter( - (identifier) => - identifier.parent && - identifier.parent.type === 'MemberExpression' && - identifier === identifier.parent.object && - identifier.parent.property.type === 'Identifier' && - identifier.parent.property.name === 'getSourceCode' && - identifier.parent.parent.type === 'CallExpression' && - identifier.parent === identifier.parent.parent.callee && - identifier.parent.parent.parent.type === 'VariableDeclarator' && - identifier.parent.parent === identifier.parent.parent.parent.init && - identifier.parent.parent.parent.id.type === 'Identifier', - ) - .flatMap((identifier) => - scopeManager.getDeclaredVariables(identifier.parent.parent.parent), - ) - .flatMap((variable) => variable.references) - .map((ref) => ref.identifier), - ); - }, - - /** - * 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} - */ - insertProperty(fixer, node, propertyText, sourceCode) { - if (node.properties.length === 0) { - return fixer.replaceText(node, `{\n${propertyText}\n}`); + const metaPropertyNode = evaluateObjectProperties( + docsNode?.value, + scopeManager, + ).find((p) => p.type === 'Property' && getKeyName(p) === propertyName); + + return { docsNode, metaNode, metaPropertyNode }; +} + +/** + * Get the `meta.messages` node from a rule. + * @param {RuleInfo} ruleInfo + * @param {ScopeManager} scopeManager + * @returns {Node|undefined} + */ +export function getMessagesNode(ruleInfo, scopeManager) { + if (!ruleInfo) { + return; + } + + const metaNode = ruleInfo.meta; + const messagesNode = evaluateObjectProperties(metaNode, scopeManager).find( + (p) => p.type === 'Property' && getKeyName(p) === 'messages', + ); + + if (messagesNode) { + if (messagesNode.value.type === 'ObjectExpression') { + return messagesNode.value; } - return fixer.insertTextAfter( - sourceCode.getLastToken(node.properties.at(-1)), - `,\n${propertyText}`, - ); - }, - - /** - * Collect all context.report({...}) violation/suggestion-related nodes into a standardized array for convenience. - * @param {Object} reportInfo - Result of getReportInfo(). - * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[] - */ - collectReportViolationAndSuggestionData(reportInfo) { - return [ - // Violation message - { - messageId: reportInfo.messageId, - message: reportInfo.message, - data: reportInfo.data, - fix: reportInfo.fix, - }, - // Suggestion messages - ...collectArrayElements(reportInfo.suggest) - .map((suggestObjNode) => { - if (suggestObjNode.type !== 'ObjectExpression') { - // Ignore non-objects (like variables or function calls). - return null; - } - return { - messageId: findObjectPropertyValueByKeyName( - suggestObjNode, - 'messageId', - ), - message: findObjectPropertyValueByKeyName(suggestObjNode, 'desc'), // Note: suggestion message named `desc` - data: findObjectPropertyValueByKeyName(suggestObjNode, 'data'), - fix: findObjectPropertyValueByKeyName(suggestObjNode, 'fix'), - }; - }) - .filter((item) => item !== null), - ]; - }, - - /** - * Whether the provided node represents an autofixer function. - * @param {Node} node - * @param {Node[]} contextIdentifiers - * @returns {boolean} - */ - isAutoFixerFunction(node, contextIdentifiers) { - 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.property.name === 'report' && - module.exports.getReportInfo(parent.parent.parent).fix === node - ); - }, - - /** - * Whether the provided node represents a suggestion fixer function. - * @param {Node} node - * @param {Node[]} contextIdentifiers - * @returns {boolean} - */ - isSuggestionFixerFunction(node, contextIdentifiers) { - const parent = node.parent; - return ( - (node.type === 'FunctionExpression' || - node.type === 'ArrowFunctionExpression') && - parent.type === 'Property' && - parent.key.type === 'Identifier' && - parent.key.name === 'fix' && - parent.parent.type === 'ObjectExpression' && - parent.parent.parent.type === 'ArrayExpression' && - 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' && - parent.parent.parent.parent.parent.parent.type === 'CallExpression' && - contextIdentifiers.has( - parent.parent.parent.parent.parent.parent.callee.object, - ) && - parent.parent.parent.parent.parent.parent.callee.property.name === - 'report' && - module.exports.getReportInfo(parent.parent.parent.parent.parent.parent) - .suggest === parent.parent.parent - ); - }, - - /** - * 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 - */ - evaluateObjectProperties(objectNode, scopeManager) { - if (!objectNode || objectNode.type !== 'ObjectExpression') { - return []; + const value = findVariableValue(messagesNode.value, scopeManager); + if (value && value.type === 'ObjectExpression') { + return value; } + } +} - return objectNode.properties.flatMap((property) => { - if (property.type === 'SpreadElement') { - const value = findVariableValue(property.argument, scopeManager); - if (value && value.type === 'ObjectExpression') { - return value.properties; - } - return []; - } - return [property]; - }); - }, +/** + * Get the list of messageId properties from `meta.messages` for a rule. + * @param {RuleInfo} ruleInfo + * @param {ScopeManager} scopeManager + * @returns {Node[]|undefined} + */ +export function getMessageIdNodes(ruleInfo, scopeManager) { + const messagesNode = getMessagesNode(ruleInfo, scopeManager); - getMetaDocsProperty(propertyName, ruleInfo, scopeManager) { - const metaNode = ruleInfo.meta; + return messagesNode && messagesNode.type === 'ObjectExpression' + ? evaluateObjectProperties(messagesNode, scopeManager) + : undefined; +} - const docsNode = module.exports - .evaluateObjectProperties(metaNode, scopeManager) - .find( - (p) => p.type === 'Property' && module.exports.getKeyName(p) === 'docs', - ); +/** + * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`. + * @param {String} 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`. + */ +export function getMessageIdNodeById(messageId, ruleInfo, scopeManager, scope) { + return getMessageIdNodes(ruleInfo, scopeManager).find( + (p) => p.type === 'Property' && getKeyName(p, scope) === messageId, + ); +} - const metaPropertyNode = module.exports - .evaluateObjectProperties(docsNode?.value, scopeManager) - .find( - (p) => - p.type === 'Property' && - module.exports.getKeyName(p) === propertyName, - ); +export function getMetaSchemaNode(metaNode, scopeManager) { + return evaluateObjectProperties(metaNode, scopeManager).find( + (p) => p.type === 'Property' && getKeyName(p) === 'schema', + ); +} + +export function getMetaSchemaNodeProperty(schemaNode, scopeManager) { + if (!schemaNode) { + return null; + } - return { docsNode, metaNode, metaPropertyNode }; - }, - - /** - * Get the `meta.messages` node from a rule. - * @param {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @returns {Node|undefined} - */ - getMessagesNode(ruleInfo, scopeManager) { - if (!ruleInfo) { + let { value } = schemaNode; + if (value.type === 'Identifier' && value.name !== 'undefined') { + const variable = findVariable( + scopeManager.acquire(value) || scopeManager.globalScope, + value, + ); + + // If we can't find the declarator, we have to assume it's in correct type + if ( + !variable || + !variable.defs || + !variable.defs[0] || + !variable.defs[0].node || + variable.defs[0].node.type !== 'VariableDeclarator' || + !variable.defs[0].node.init + ) { return; } - const metaNode = ruleInfo.meta; - const messagesNode = module.exports - .evaluateObjectProperties(metaNode, scopeManager) - .find( - (p) => - p.type === 'Property' && module.exports.getKeyName(p) === 'messages', - ); + value = variable.defs[0].node.init; + } - if (messagesNode) { - if (messagesNode.value.type === 'ObjectExpression') { - return messagesNode.value; - } - const value = findVariableValue(messagesNode.value, scopeManager); - if (value && value.type === 'ObjectExpression') { - return value; - } - } - }, - - /** - * Get the list of messageId properties from `meta.messages` for a rule. - * @param {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @returns {Node[]|undefined} - */ - getMessageIdNodes(ruleInfo, scopeManager) { - const messagesNode = module.exports.getMessagesNode(ruleInfo, scopeManager); - - return messagesNode && messagesNode.type === 'ObjectExpression' - ? module.exports.evaluateObjectProperties(messagesNode, scopeManager) - : undefined; - }, - - /** - * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`. - * @param {String} 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`. - */ - getMessageIdNodeById(messageId, ruleInfo, scopeManager, scope) { - return module.exports - .getMessageIdNodes(ruleInfo, scopeManager) - .find( - (p) => - p.type === 'Property' && - module.exports.getKeyName(p, scope) === messageId, - ); - }, - - getMetaSchemaNode(metaNode, scopeManager) { - return module.exports - .evaluateObjectProperties(metaNode, scopeManager) - .find( - (p) => - p.type === 'Property' && module.exports.getKeyName(p) === 'schema', - ); - }, + return value; +} - getMetaSchemaNodeProperty(schemaNode, scopeManager) { - if (!schemaNode) { - return null; +/** + * 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. + */ +export function findPossibleVariableValues(node, scopeManager) { + const variable = findVariable( + scopeManager.acquire(node) || scopeManager.globalScope, + node, + ); + return ((variable && variable.references) || []).flatMap((ref) => { + if ( + ref.writeExpr && + (ref.writeExpr.parent.type !== 'AssignmentExpression' || + ref.writeExpr.parent.operator === '=') + ) { + // Given node `x`, get `123` from `x = 123;`. + // Ignore assignments with other operators like `x += 'abc';'`; + return [ref.writeExpr]; } + return []; + }); +} - let { value } = schemaNode; - if (value.type === 'Identifier' && value.name !== 'undefined') { - const variable = findVariable( - scopeManager.acquire(value) || scopeManager.globalScope, - value, - ); +/** + * @param {Node} node + * @returns {boolean} Whether the node is an Identifier with name `undefined`. + */ +export function isUndefinedIdentifier(node) { + return node.type === 'Identifier' && node.name === 'undefined'; +} - // If we can't find the declarator, we have to assume it's in correct type - if ( - !variable || - !variable.defs || - !variable.defs[0] || - !variable.defs[0].node || - variable.defs[0].node.type !== 'VariableDeclarator' || - !variable.defs[0].node.init - ) { - return; - } +/** + * 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 + */ +export function isVariableFromParameter(node, scopeManager) { + const variable = findVariable( + scopeManager.acquire(node) || scopeManager.globalScope, + node, + ); - value = variable.defs[0].node.init; - } + return variable?.defs[0]?.type === 'Parameter'; +} - return value; - }, +export function getSourceCode(context) { + // TODO: remove contet.getSourceCode() when dropping eslint < v9 + return context.sourceCode || context.getSourceCode(); +} - /** - * 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. - */ - findPossibleVariableValues(node, scopeManager) { - const variable = findVariable( - scopeManager.acquire(node) || scopeManager.globalScope, - node, - ); - return ((variable && variable.references) || []).flatMap((ref) => { - if ( - ref.writeExpr && - (ref.writeExpr.parent.type !== 'AssignmentExpression' || - ref.writeExpr.parent.operator === '=') - ) { - // Given node `x`, get `123` from `x = 123;`. - // Ignore assignments with other operators like `x += 'abc';'`; - return [ref.writeExpr]; - } - return []; - }); - }, - - /** - * @param {Node} node - * @returns {boolean} Whether the node is an Identifier with name `undefined`. - */ - isUndefinedIdentifier(node) { - 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 - */ - isVariableFromParameter(node, scopeManager) { - const variable = findVariable( - scopeManager.acquire(node) || scopeManager.globalScope, - node, - ); +export function getScope(context) { + // TODO: remove contet.getScope() when dropping eslint < v9 + const sourceCode = context.sourceCode || context.getSourceCode(); + return sourceCode.getScope?.(sourceCode.ast) || context.getScope(); +} + +export function getparserServices(context) { + // TODO: remove context.parserServices when dropping eslint < v9 + return (context.sourceCode || context).parserServices; +} - return variable?.defs[0]?.type === 'Parameter'; - }, - - getSourceCode(context) { - // TODO: remove contet.getSourceCode() when dropping eslint < v9 - return context.sourceCode || context.getSourceCode(); - }, - - getScope(context) { - // TODO: remove contet.getScope() when dropping eslint < v9 - const sourceCode = context.sourceCode || context.getSourceCode(); - return sourceCode.getScope?.(sourceCode.ast) || context.getScope(); - }, - - getparserServices(context) { - // TODO: remove context.parserServices when dropping eslint < v9 - return (context.sourceCode || context).parserServices; - }, - - getFilename(context) { - // TODO: just use context.filename when dropping eslint < v9 - return context.filename || context.getFilename(); - }, -}; +export function getFilename(context) { + // TODO: just use context.filename when dropping eslint < v9 + return context.filename || context.getFilename(); +} diff --git a/package.json b/package.json index 63da8091..2ec7b244 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "An ESLint plugin for linting ESLint plugins", "author": "Teddy Katz", "main": "./lib/index.js", + "type": "module", "exports": { ".": "./lib/index.js", "./configs/*": "./configs/*.js", diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.js index c7225404..d5614cf7 100644 --- a/tests/lib/rules/no-property-in-node.js +++ b/tests/lib/rules/no-property-in-node.js @@ -1,8 +1,10 @@ import { RuleTester } from '../../utils/eslint-rule-tester.js'; import path from 'path'; +import { fileURLToPath } from 'url'; import rule from '../../../lib/rules/no-property-in-node.js'; import parser from '@typescript-eslint/parser'; +const dirname = path.dirname(fileURLToPath(import.meta.url)); const ruleTester = new RuleTester({ languageOptions: { parser, @@ -10,7 +12,7 @@ const ruleTester = new RuleTester({ projectService: { defaultProject: 'tsconfig.json', }, - tsconfigRootDir: path.join(__dirname, '../fixtures'), + tsconfigRootDir: path.join(dirname, '../fixtures'), }, }, }); From 0b708d7612a4441d5e9b9deb37c9730c108a3416 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 20 Jun 2025 13:10:06 -0500 Subject: [PATCH 05/15] build: fix eslint.config following esm update (#527) This change updates the eslint config to use `sourceType: 'module'`, and swaps n's `flat/mixed-esm-and-cjs` config for `flat/recommended` which will use the module type from the nearest `package.json`. --- eslint.config.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index e789fccf..f34afb74 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,13 +13,21 @@ const compat = new FlatCompat({ }); export default [ + // Global ignores + { + ignores: ['node_modules', 'coverage'], + }, + // Global settings + { + languageOptions: { sourceType: 'module' }, + }, ...compat.extends( 'not-an-aardvark/node', 'plugin:@eslint-community/eslint-comments/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended', ), - ...pluginN.configs['flat/mixed-esm-and-cjs'], + pluginN.configs['flat/recommended'], { rules: { '@eslint-community/eslint-comments/no-unused-disable': 'error', @@ -52,10 +60,6 @@ export default [ ], }, }, - { - files: ['tests/**/*.js'], - languageOptions: { sourceType: 'module' }, - }, { files: ['**/*.md'], plugins: { markdown }, From 03cf3d7b857561d24fd0bfdbeeb926cb6bc02d10 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 20 Jun 2025 18:48:31 -0500 Subject: [PATCH 06/15] feat!: remove eslint v8 / eslintrc support and remove `flat/` prefix from configs (#528) * feat!: remove eslint v8 support This change removes support for legacy rc-based configs, and moves minimum supported version to 9.0. I've also removed the deprecated `/configs` entry point. * use test setup file * docs: update examples * build: disable n/no-missing-import for md files * build: disable n/no-missing-import for md files --------- Co-authored-by: Bryan Mishkin <698306+bmish@users.noreply.github.com> --- .eslint-doc-generatorrc.js | 7 -- .github/workflows/main.yml | 15 +--- README.md | 70 +++++-------------- configs/all-type-checked.js | 10 --- configs/all.js | 11 --- configs/recommended.js | 11 --- configs/rules-recommended.js | 11 --- configs/rules.js | 11 --- configs/tests-recommended.js | 11 --- configs/tests.js | 11 --- docs/rules/require-meta-docs-url.md | 53 ++++++++++---- eslint.config.js | 8 ++- lib/index.js | 23 +----- package.json | 7 +- tests/lib/rules/consistent-output.js | 2 +- tests/lib/rules/fixer-return.js | 2 +- tests/lib/rules/meta-property-ordering.js | 2 +- .../rules/no-deprecated-context-methods.js | 2 +- tests/lib/rules/no-deprecated-report-api.js | 2 +- tests/lib/rules/no-identical-tests.js | 2 +- tests/lib/rules/no-meta-replaced-by.js | 2 +- tests/lib/rules/no-meta-schema-default.js | 2 +- tests/lib/rules/no-missing-message-ids.js | 2 +- tests/lib/rules/no-missing-placeholders.js | 2 +- tests/lib/rules/no-only-tests.js | 2 +- tests/lib/rules/no-property-in-node.js | 2 +- tests/lib/rules/no-unused-message-ids.js | 2 +- tests/lib/rules/no-unused-placeholders.js | 2 +- tests/lib/rules/no-useless-token-range.js | 2 +- tests/lib/rules/prefer-message-ids.js | 2 +- tests/lib/rules/prefer-object-rule.js | 2 +- tests/lib/rules/prefer-output-null.js | 2 +- tests/lib/rules/prefer-placeholders.js | 2 +- tests/lib/rules/prefer-replace-text.js | 2 +- tests/lib/rules/report-message-format.js | 2 +- .../lib/rules/require-meta-default-options.js | 2 +- .../rules/require-meta-docs-description.js | 2 +- .../rules/require-meta-docs-recommended.js | 2 +- tests/lib/rules/require-meta-docs-url.js | 2 +- tests/lib/rules/require-meta-fixable.js | 2 +- .../lib/rules/require-meta-has-suggestions.js | 2 +- .../rules/require-meta-schema-description.js | 2 +- tests/lib/rules/require-meta-schema.js | 2 +- tests/lib/rules/require-meta-type.js | 2 +- .../lib/rules/test-case-property-ordering.js | 2 +- .../lib/rules/test-case-shorthand-strings.js | 2 +- tests/utils/eslint-rule-tester.js | 21 ------ tests/utils/test-setup.js | 6 ++ vitest.config.ts | 1 + 49 files changed, 107 insertions(+), 244 deletions(-) delete mode 100644 configs/all-type-checked.js delete mode 100644 configs/all.js delete mode 100644 configs/recommended.js delete mode 100644 configs/rules-recommended.js delete mode 100644 configs/rules.js delete mode 100644 configs/tests-recommended.js delete mode 100644 configs/tests.js delete mode 100644 tests/utils/eslint-rule-tester.js create mode 100644 tests/utils/test-setup.js diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.js index a3756beb..df9f4712 100644 --- a/.eslint-doc-generatorrc.js +++ b/.eslint-doc-generatorrc.js @@ -9,13 +9,6 @@ const config = { 'rules-recommended', 'tests', 'tests-recommended', - 'flat/recommended', - 'flat/all', - 'flat/all-type-checked', - 'flat/rules', - 'flat/rules-recommended', - 'flat/tests', - 'flat/tests-recommended', ], postprocess: async (content, path) => prettier.format(content, { diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 51902405..b46076ce 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,21 +34,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: "lts/*" + node-version: 'lts/*' - run: npm install - run: npm run lint - eslint8: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "lts/*" - - run: npm install - - run: npm install --save-dev eslint@8 - - run: npm test - test-remote: name: eslint-remote-tester runs-on: ubuntu-latest @@ -56,6 +45,6 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: "lts/*" + node-version: 'lts/*' - run: npm install - run: npm run test:remote diff --git a/README.md b/README.md index f6eeb235..3b6e5000 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ An ESLint plugin for linting ESLint plugins. Rules written in CJS, ESM, and Type - [Installation](#installation) - [Usage](#usage) - - [**.eslintrc.json**](#eslintrcjson) - - [`eslint.config.js` (requires eslint\>=v8.23.0)](#eslintconfigjs-requires-eslintv8230) - [Rules](#rules) - [Rules](#rules-1) - [Tests](#tests) @@ -42,25 +40,14 @@ Here's an example ESLint configuration that: - Enables the `recommended` configuration - Enables an optional/non-recommended rule -Note: you might need to set `sourceType` to `script` (most users) (use `module` for ESM/TypeScript). - -### **[.eslintrc.json](https://eslint.org/docs/latest/use/configure/configuration-files)** - -```json -{ - "extends": ["plugin:eslint-plugin/recommended"], - "rules": { - "eslint-plugin/require-meta-docs-description": "error" - } -} -``` - -### [`eslint.config.js`](https://eslint.org/docs/latest/use/configure/configuration-files-new) (requires eslint>=v8.23.0) +Note: you might need to set `sourceType` to `module` or `script` depending on your codebase. ```js -const eslintPlugin = require('eslint-plugin-eslint-plugin'); -module.exports = [ - eslintPlugin.configs['flat/recommended'], +// eslint.config.js +import eslintPlugin from 'eslint-plugin-eslint-plugin'; + +export default [ + eslintPlugin.configs.recommended, { rules: { 'eslint-plugin/require-meta-docs-description': 'error', @@ -141,54 +128,29 @@ The list of recommended rules will only change in a major release of this plugin ### Preset usage -Both flat and eslintrc configs are supported. For example, to enable the `recommended` preset, use: - -eslint.config.js +Example of applying the `recommended` config to all files. ```js -const eslintPlugin = require('eslint-plugin-eslint-plugin'); -module.exports = [eslintPlugin.configs['flat/recommended']]; -``` +// eslint.config.js +import eslintPlugin from 'eslint-plugin-eslint-plugin'; -.eslintrc.json - -```json -{ - "extends": ["plugin:eslint-plugin/recommended"] -} +export default [eslintPlugin.configs.recommended]; ``` Or to apply linting only to the appropriate rule or test files: -eslint.config.js - ```js -const eslintPlugin = require('eslint-plugin-eslint-plugin'); -module.exports = [ +// eslint.config.js +import eslintPlugin from 'eslint-plugin-eslint-plugin'; + +export default [ { files: ['lib/rules/*.{js,ts}'], - ...eslintPlugin.configs['flat/rules-recommended'], + ...eslintPlugin.configs['rules-recommended'], }, { files: ['tests/lib/rules/*.{js,ts}'], - ...eslintPlugin.configs['flat/tests-recommended'], + ...eslintPlugin.configs['tests-recommended'], }, ]; ``` - -.eslintrc.js - -```json -{ - "overrides": [ - { - "files": ["lib/rules/*.{js,ts}"], - "extends": ["plugin:eslint-plugin/rules-recommended"] - }, - { - "files": ["tests/lib/rules/*.{js,ts}"], - "extends": ["plugin:eslint-plugin/tests-recommended"] - } - ] -} -``` diff --git a/configs/all-type-checked.js b/configs/all-type-checked.js deleted file mode 100644 index 84309cba..00000000 --- a/configs/all-type-checked.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @deprecated use 'flat/all-type-checked' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/all-type-checked']; - -export default config; diff --git a/configs/all.js b/configs/all.js deleted file mode 100644 index cf48e487..00000000 --- a/configs/all.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @fileoverview the `all` config for `eslint.config.js` - * @deprecated use 'flat/all' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/all']; - -export default config; diff --git a/configs/recommended.js b/configs/recommended.js deleted file mode 100644 index 17c2ffad..00000000 --- a/configs/recommended.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @fileoverview the `recommended` config for `eslint.config.js` - * @deprecated use 'flat/recommended' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/recommended']; - -export default config; diff --git a/configs/rules-recommended.js b/configs/rules-recommended.js deleted file mode 100644 index 8e79f61a..00000000 --- a/configs/rules-recommended.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @fileoverview the `rules-recommended` config for `eslint.config.js` - * @deprecated use 'flat/rules-recommended' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/rules-recommended']; - -export default config; diff --git a/configs/rules.js b/configs/rules.js deleted file mode 100644 index f3414360..00000000 --- a/configs/rules.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @fileoverview the `rules` config for `eslint.config.js` - * @deprecated use 'flat/rules' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/rules']; - -export default config; diff --git a/configs/tests-recommended.js b/configs/tests-recommended.js deleted file mode 100644 index 367f5d17..00000000 --- a/configs/tests-recommended.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @fileoverview the `tests-recommended` config for `eslint.config.js` - * @deprecated use 'flat/tests-recommended' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/tests-recommended']; - -export default config; diff --git a/configs/tests.js b/configs/tests.js deleted file mode 100644 index 38ced25c..00000000 --- a/configs/tests.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @fileoverview the `tests` config for `eslint.config.js` - * @deprecated use 'flat/tests' instead - * @author 唯然 - */ - -import plugin from '../lib/index.js'; - -const config = plugin.configs['flat/tests']; - -export default config; diff --git a/docs/rules/require-meta-docs-url.md b/docs/rules/require-meta-docs-url.md index 4bb71a41..4509fb85 100644 --- a/docs/rules/require-meta-docs-url.md +++ b/docs/rules/require-meta-docs-url.md @@ -96,29 +96,54 @@ module.exports = { } ``` +```js +// eslint.config.js +import eslintPlugin from 'eslint-plugin-eslint-plugin'; + +export default [ + { + plugins: { 'eslint-plugin': eslintPlugin }, + rules: { + 'eslint-plugin/require-meta-docs-url': [ + 'error', + { + pattern: + 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/blob/master/docs/rules/{{name}}.md', + }, + ], + }, + }, +]; +``` + If you set the `pattern` option, this rule adds `meta.docs.url` property automatically when you execute `eslint --fix` command. ## Version specific URL -If you want to enforce version-specific URLs, it's feasible easily with `.eslintrc.js` and `npm version ` script. +If you want to enforce version-specific URLs, it's feasible easily with `eslint.config.js` and `npm version ` script. For example: -**.eslintrc.js**: +**eslint.config.js**: ```js -// const version = require("./package.json").version; - -module.exports = { - plugins: ['eslint-plugin'], - rules: { - 'eslint-plugin/require-meta-docs-url': [ - 'error', - { - pattern: `path/to/v${version}/docs/rules/{{name}}.md`, - }, - ], +import eslintPlugin from 'eslint-plugin-eslint-plugin'; +import packageMetadata from './package.json' with { type: 'json' }; + +const { version } = packageMetadata; + +export default [ + { + plugins: { 'eslint-plugin': eslintPlugin }, + rules: { + 'eslint-plugin/require-meta-docs-url': [ + 'error', + { + pattern: `path/to/v${version}/docs/rules/{{name}}.md`, + }, + ], + }, }, -}; +]; ``` **package.json**: diff --git a/eslint.config.js b/eslint.config.js index f34afb74..3bf9f756 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,7 +4,7 @@ import js from '@eslint/js'; import { FlatCompat } from '@eslint/eslintrc'; import markdown from 'eslint-plugin-markdown'; import pluginN from 'eslint-plugin-n'; -import eslintPluginConfig from 'eslint-plugin-eslint-plugin/configs/all'; +import eslintPlugin from './lib/index.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); const compat = new FlatCompat({ @@ -46,9 +46,9 @@ export default [ { // Apply eslint-plugin rules to our own rules/tests (but not docs). files: ['lib/**/*.js', 'tests/**/*.js'], - plugins: eslintPluginConfig.plugins, + plugins: { 'eslint-plugin': eslintPlugin }, rules: { - ...eslintPluginConfig.rules, + ...eslintPlugin.configs.all.rules, 'eslint-plugin/no-meta-schema-default': 'off', // TODO: enable once https://github.com/bmish/eslint-doc-generator/issues/513 is fixed and released 'eslint-plugin/report-message-format': ['error', '^[^a-z].*.$'], 'eslint-plugin/require-meta-docs-url': [ @@ -77,6 +77,8 @@ export default [ '@eslint-community/eslint-comments/require-description': 'off', + 'n/no-missing-import': 'off', + 'unicorn/filename-case': 'off', }, }, diff --git a/lib/index.js b/lib/index.js index 83f4a93e..f512a196 100644 --- a/lib/index.js +++ b/lib/index.js @@ -105,31 +105,14 @@ const plugin = { configs: {}, // assigned later }; -// eslintrc configs +// configs Object.assign( plugin.configs, Object.keys(configFilters).reduce((configs, configName) => { return Object.assign(configs, { [configName]: { - plugins: ['eslint-plugin'], - rules: Object.fromEntries( - Object.keys(allRules) - .filter((ruleName) => configFilters[configName](allRules[ruleName])) - .map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']), - ), - }, - }); - }, {}), -); - -// flat configs -Object.assign( - plugin.configs, - Object.keys(configFilters).reduce((configs, configName) => { - return Object.assign(configs, { - [`flat/${configName}`]: { - name: `eslint-plugin/flat/${configName}`, - plugins: { 'eslint-plugin': plugin }, + name: `${PLUGIN_NAME}/${configName}`, + plugins: { [PLUGIN_NAME]: plugin }, rules: Object.fromEntries( Object.keys(allRules) .filter((ruleName) => configFilters[configName](allRules[ruleName])) diff --git a/package.json b/package.json index 2ec7b244..5bd6c441 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "type": "module", "exports": { ".": "./lib/index.js", - "./configs/*": "./configs/*.js", "./package.json": "./package.json" }, "license": "MIT", @@ -24,8 +23,8 @@ "update:eslint-docs": "eslint-doc-generator" }, "files": [ - "lib/", - "configs/" + "CHANGELOG.md", + "lib/" ], "keywords": [ "eslint", @@ -79,7 +78,7 @@ "vitest": "^3.2.4" }, "peerDependencies": { - "eslint": ">=8.23.0" + "eslint": ">=9.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/tests/lib/rules/consistent-output.js b/tests/lib/rules/consistent-output.js index 60ac1899..e63d62f7 100644 --- a/tests/lib/rules/consistent-output.js +++ b/tests/lib/rules/consistent-output.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/consistent-output.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; const ERROR = { messageId: 'missingOutput', type: 'ObjectExpression' }; diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js index cc3798c5..bff61ee3 100644 --- a/tests/lib/rules/fixer-return.js +++ b/tests/lib/rules/fixer-return.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/fixer-return.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js index 1a843e94..632dfe59 100644 --- a/tests/lib/rules/meta-property-ordering.js +++ b/tests/lib/rules/meta-property-ordering.js @@ -7,7 +7,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/meta-property-ordering.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-deprecated-context-methods.js b/tests/lib/rules/no-deprecated-context-methods.js index 7338f37e..14eaaccc 100644 --- a/tests/lib/rules/no-deprecated-context-methods.js +++ b/tests/lib/rules/no-deprecated-context-methods.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-deprecated-context-methods.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.js index ed1bd422..636b2c11 100644 --- a/tests/lib/rules/no-deprecated-report-api.js +++ b/tests/lib/rules/no-deprecated-report-api.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-deprecated-report-api.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.js index bcfcd8cd..169d9140 100644 --- a/tests/lib/rules/no-identical-tests.js +++ b/tests/lib/rules/no-identical-tests.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-identical-tests.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; const ERROR_OBJECT_TEST = { messageId: 'identical', type: 'ObjectExpression' }; const ERROR_STRING_TEST = { messageId: 'identical', type: 'Literal' }; diff --git a/tests/lib/rules/no-meta-replaced-by.js b/tests/lib/rules/no-meta-replaced-by.js index 2d357a39..9ec810fa 100644 --- a/tests/lib/rules/no-meta-replaced-by.js +++ b/tests/lib/rules/no-meta-replaced-by.js @@ -7,7 +7,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-meta-replaced-by.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-meta-schema-default.js b/tests/lib/rules/no-meta-schema-default.js index 06376284..07dd50d1 100644 --- a/tests/lib/rules/no-meta-schema-default.js +++ b/tests/lib/rules/no-meta-schema-default.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-meta-schema-default.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-missing-message-ids.js b/tests/lib/rules/no-missing-message-ids.js index efee8a9d..aa0045a0 100644 --- a/tests/lib/rules/no-missing-message-ids.js +++ b/tests/lib/rules/no-missing-message-ids.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-missing-message-ids.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.js index 13767ec2..2791fee2 100644 --- a/tests/lib/rules/no-missing-placeholders.js +++ b/tests/lib/rules/no-missing-placeholders.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-missing-placeholders.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; /** * Create an error for the given key diff --git a/tests/lib/rules/no-only-tests.js b/tests/lib/rules/no-only-tests.js index 25257c65..b7f52125 100644 --- a/tests/lib/rules/no-only-tests.js +++ b/tests/lib/rules/no-only-tests.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-only-tests.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.js index d5614cf7..8f2674d0 100644 --- a/tests/lib/rules/no-property-in-node.js +++ b/tests/lib/rules/no-property-in-node.js @@ -1,4 +1,4 @@ -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; import path from 'path'; import { fileURLToPath } from 'url'; import rule from '../../../lib/rules/no-property-in-node.js'; diff --git a/tests/lib/rules/no-unused-message-ids.js b/tests/lib/rules/no-unused-message-ids.js index 54184cdc..244651e1 100644 --- a/tests/lib/rules/no-unused-message-ids.js +++ b/tests/lib/rules/no-unused-message-ids.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-unused-message-ids.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js index 87de5cb9..b9d55158 100644 --- a/tests/lib/rules/no-unused-placeholders.js +++ b/tests/lib/rules/no-unused-placeholders.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-unused-placeholders.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; /** * Create an error for the given key diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.js index 0ad6b61f..7b6f44e7 100644 --- a/tests/lib/rules/no-useless-token-range.js +++ b/tests/lib/rules/no-useless-token-range.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/no-useless-token-range.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; /** * Wraps a code sample as an eslint rule diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js index 342fb501..9a45cb28 100644 --- a/tests/lib/rules/prefer-message-ids.js +++ b/tests/lib/rules/prefer-message-ids.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/prefer-message-ids.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js index 0e1583f7..4c25013c 100644 --- a/tests/lib/rules/prefer-object-rule.js +++ b/tests/lib/rules/prefer-object-rule.js @@ -7,7 +7,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/prefer-object-rule.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-output-null.js b/tests/lib/rules/prefer-output-null.js index 22e7bfb4..082e41c4 100644 --- a/tests/lib/rules/prefer-output-null.js +++ b/tests/lib/rules/prefer-output-null.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/prefer-output-null.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; const ERROR = { messageId: 'useOutputNull', type: 'Property' }; diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.js index 2da2c535..4b2f6447 100644 --- a/tests/lib/rules/prefer-placeholders.js +++ b/tests/lib/rules/prefer-placeholders.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/prefer-placeholders.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js index e8dc8041..06117252 100644 --- a/tests/lib/rules/prefer-replace-text.js +++ b/tests/lib/rules/prefer-replace-text.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/prefer-replace-text.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.js index 8a1c7338..be260092 100644 --- a/tests/lib/rules/report-message-format.js +++ b/tests/lib/rules/report-message-format.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/report-message-format.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-default-options.js b/tests/lib/rules/require-meta-default-options.js index b0e3df4f..d6ceb410 100644 --- a/tests/lib/rules/require-meta-default-options.js +++ b/tests/lib/rules/require-meta-default-options.js @@ -1,5 +1,5 @@ import rule from '../../../lib/rules/require-meta-default-options.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; import parser from '@typescript-eslint/parser'; const ruleTester = new RuleTester({ diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js index d3aa09bd..28d49910 100644 --- a/tests/lib/rules/require-meta-docs-description.js +++ b/tests/lib/rules/require-meta-docs-description.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/require-meta-docs-description.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; import parser from '@typescript-eslint/parser'; // ------------------------------------------------------------------------------ diff --git a/tests/lib/rules/require-meta-docs-recommended.js b/tests/lib/rules/require-meta-docs-recommended.js index b50a3b93..60a9d72a 100644 --- a/tests/lib/rules/require-meta-docs-recommended.js +++ b/tests/lib/rules/require-meta-docs-recommended.js @@ -1,5 +1,5 @@ import rule from '../../../lib/rules/require-meta-docs-recommended.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; import parser from '@typescript-eslint/parser'; const ruleTester = new RuleTester({ diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js index a13d63b2..50895f05 100644 --- a/tests/lib/rules/require-meta-docs-url.js +++ b/tests/lib/rules/require-meta-docs-url.js @@ -8,7 +8,7 @@ // Requirements // ----------------------------------------------------------------------------- -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; import rule from '../../../lib/rules/require-meta-docs-url.js'; // ----------------------------------------------------------------------------- diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js index a0d343f3..3bde53c7 100644 --- a/tests/lib/rules/require-meta-fixable.js +++ b/tests/lib/rules/require-meta-fixable.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/require-meta-fixable.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js index 9c19d011..8bb7eb84 100644 --- a/tests/lib/rules/require-meta-has-suggestions.js +++ b/tests/lib/rules/require-meta-has-suggestions.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/require-meta-has-suggestions.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-schema-description.js b/tests/lib/rules/require-meta-schema-description.js index 6fb5b741..fa0b414f 100644 --- a/tests/lib/rules/require-meta-schema-description.js +++ b/tests/lib/rules/require-meta-schema-description.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/require-meta-schema-description.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js index aff9df9f..5c6c6af0 100644 --- a/tests/lib/rules/require-meta-schema.js +++ b/tests/lib/rules/require-meta-schema.js @@ -3,7 +3,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/require-meta-schema.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js index ae52476f..7d44b041 100644 --- a/tests/lib/rules/require-meta-type.js +++ b/tests/lib/rules/require-meta-type.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/require-meta-type.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/test-case-property-ordering.js b/tests/lib/rules/test-case-property-ordering.js index 65b8970a..05fb8554 100644 --- a/tests/lib/rules/test-case-property-ordering.js +++ b/tests/lib/rules/test-case-property-ordering.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/test-case-property-ordering.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; // ------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/rules/test-case-shorthand-strings.js b/tests/lib/rules/test-case-shorthand-strings.js index f358d3ee..c65d0c18 100644 --- a/tests/lib/rules/test-case-shorthand-strings.js +++ b/tests/lib/rules/test-case-shorthand-strings.js @@ -8,7 +8,7 @@ // ------------------------------------------------------------------------------ import rule from '../../../lib/rules/test-case-shorthand-strings.js'; -import { RuleTester } from '../../utils/eslint-rule-tester.js'; +import { RuleTester } from 'eslint'; /** * Returns the code for some valid test cases diff --git a/tests/utils/eslint-rule-tester.js b/tests/utils/eslint-rule-tester.js deleted file mode 100644 index 5b3c5239..00000000 --- a/tests/utils/eslint-rule-tester.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @fileoverview Helpers for tests. - * @author 唯然 - */ - -import { RuleTester as ESLintRuleTester } from 'eslint'; -import * as unsupportedApi from 'eslint/use-at-your-own-risk'; -import packageConfig from 'eslint/package.json' with { type: 'json' }; -import * as vitest from 'vitest'; - -const { version: eslintVersion } = packageConfig; - -const FlatRuleTester = unsupportedApi.FlatRuleTester; - -// greater than or equal to ESLint v9 -export const gteEslintV9 = +eslintVersion.split('.')[0] >= 9; - -export const RuleTester = gteEslintV9 ? ESLintRuleTester : FlatRuleTester; -RuleTester.describe = vitest.describe; -RuleTester.it = vitest.it; -RuleTester.itOnly = vitest.it.only; diff --git a/tests/utils/test-setup.js b/tests/utils/test-setup.js new file mode 100644 index 00000000..342265eb --- /dev/null +++ b/tests/utils/test-setup.js @@ -0,0 +1,6 @@ +import { RuleTester } from 'eslint'; +import * as vitest from 'vitest'; + +RuleTester.describe = vitest.describe; +RuleTester.it = vitest.it; +RuleTester.itOnly = vitest.it.only; diff --git a/vitest.config.ts b/vitest.config.ts index 64bc7458..56cb0fcc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { include: ['tests/lib/**/*.js'], exclude: ['tests/lib/fixtures/**'], + setupFiles: ['tests/utils/test-setup.js'], clearMocks: true, coverage: { all: true, From b2994c73ba71576cdd96d95469613b4d573740c2 Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 20 Jun 2025 20:38:24 -0500 Subject: [PATCH 07/15] feat!: require Node 20, 22, 24+ (#529) * feat!: remove support for EoL Node versions This change removes support for Node v18, 21, and 23. Supported versions are now `"^20.9.0 || ^22.11.0 || >=24.0.0"` * Update package.json updated to newer node versions Co-authored-by: Bryan Mishkin <698306+bmish@users.noreply.github.com> --------- Co-authored-by: Bryan Mishkin <698306+bmish@users.noreply.github.com> --- .github/workflows/main.yml | 3 --- package.json | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b46076ce..c68199d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,11 +13,8 @@ jobs: matrix: node-version: - 24 - - 23 - 22 - - 21 - 20 - - 18 os: - ubuntu-latest steps: diff --git a/package.json b/package.json index 5bd6c441..70866622 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "eslint": ">=9.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.1 || >=24.0.0" }, "release-it": { "git": { From f8775e4a650c38167867f8631d896ff0e5429b55 Mon Sep 17 00:00:00 2001 From: michael faith Date: Sat, 19 Jul 2025 15:55:55 -0500 Subject: [PATCH 08/15] build: migrate eslint.config.js to typescript (#531) --- eslint.config.js => eslint.config.ts | 7 ++++--- lib/rules/require-meta-docs-url.js | 2 +- package.json | 21 +++++++++++---------- tests/lib/rules/no-property-in-node.js | 4 ++-- tsconfig.json | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 16 deletions(-) rename eslint.config.js => eslint.config.ts (94%) create mode 100644 tsconfig.json diff --git a/eslint.config.js b/eslint.config.ts similarity index 94% rename from eslint.config.js rename to eslint.config.ts index 3bf9f756..36cdf25c 100644 --- a/eslint.config.js +++ b/eslint.config.ts @@ -2,8 +2,10 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import js from '@eslint/js'; 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)); @@ -12,7 +14,7 @@ const compat = new FlatCompat({ recommendedConfig: js.configs.recommended, }); -export default [ +export default defineConfig([ // Global ignores { ignores: ['node_modules', 'coverage'], @@ -39,7 +41,6 @@ export default [ 'unicorn/no-array-reduce': 'off', 'unicorn/no-null': 'off', 'unicorn/prefer-module': 'off', - 'unicorn/prefer-node-protocol': 'off', // TODO: enable once we drop support for Node 14.17. 'unicorn/prevent-abbreviations': 'off', }, }, @@ -82,4 +83,4 @@ export default [ 'unicorn/filename-case': 'off', }, }, -]; +]); diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js index 12bb2b35..4e3f38e3 100644 --- a/lib/rules/require-meta-docs-url.js +++ b/lib/rules/require-meta-docs-url.js @@ -6,7 +6,7 @@ // Requirements // ----------------------------------------------------------------------------- -import path from 'path'; +import path from 'node:path'; import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; diff --git a/package.json b/package.json index 70866622..6e93218a 100644 --- a/package.json +++ b/package.json @@ -48,26 +48,27 @@ "@commitlint/config-conventional": "^19.6.0", "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "^9.16.0", + "@eslint/js": "^9.31.0", "@release-it/conventional-changelog": "^9.0.3", - "@types/eslint": "^9.6.1", + "@types/eslint-plugin-markdown": "^2.0.2", "@types/estree": "^1.0.8", + "@types/node": "^20.19.0", "@typescript-eslint/parser": "^8.34.1", "@typescript-eslint/utils": "^8.34.1", "@vitest/coverage-istanbul": "^3.2.4", - "eslint": "^9.16.0", + "eslint": "^9.31.0", "eslint-config-not-an-aardvark": "^2.1.0", - "eslint-config-prettier": "^9.1.0", - "eslint-doc-generator": "^2.0.0", - "eslint-plugin-eslint-plugin": "file:./", - "eslint-plugin-markdown": "^5.0.0", - "eslint-plugin-n": "^17.14.0", - "eslint-plugin-prettier": "^5.1.3", + "eslint-config-prettier": "^10.1.8", + "eslint-doc-generator": "^2.2.2", + "eslint-plugin-markdown": "^5.1.0", + "eslint-plugin-n": "^17.21.0", + "eslint-plugin-prettier": "^5.5.3", "eslint-plugin-unicorn": "^56.0.1", - "eslint-remote-tester": "^4.0.1", + "eslint-remote-tester": "^4.0.2", "eslint-scope": "^8.0.1", "espree": "^10.0.1", "husky": "^9.1.7", + "jiti": "^2.4.2", "lodash": "^4.17.21", "markdownlint-cli": "^0.43.0", "npm-package-json-lint": "^8.0.0", diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.js index 8f2674d0..c21e1cb1 100644 --- a/tests/lib/rules/no-property-in-node.js +++ b/tests/lib/rules/no-property-in-node.js @@ -1,6 +1,6 @@ import { RuleTester } from 'eslint'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import rule from '../../../lib/rules/no-property-in-node.js'; import parser from '@typescript-eslint/parser'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..977142eb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "rootDir": ".", + "declaration": true, + "esModuleInterop": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2024", + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "forceConsistentCasingInFileNames": true + } +} From f834dc2e48221403a1411667d5e0f7204a989441 Mon Sep 17 00:00:00 2001 From: michael faith Date: Tue, 22 Jul 2025 20:38:09 -0500 Subject: [PATCH 09/15] refactor: remove context compat functions (#532) This change is in support of the larger migration to TypeScript., and a followup to the removal of support for ESLint v8. The context compatibility functions in the `utils` module were no longer necessary. I originally did this in the TypeScript branch, but that PR's going to be large enough. So, I'm trying to peel off smaller independent changes I can land separately first. --- lib/rules/consistent-output.js | 6 ++-- lib/rules/fixer-return.js | 24 ++++++------- lib/rules/meta-property-ordering.js | 6 ++-- lib/rules/no-deprecated-context-methods.js | 6 ++-- lib/rules/no-deprecated-report-api.js | 8 ++--- lib/rules/no-identical-tests.js | 6 ++-- lib/rules/no-meta-replaced-by.js | 16 ++++----- lib/rules/no-meta-schema-default.js | 18 +++++----- lib/rules/no-missing-message-ids.js | 33 +++++++++-------- lib/rules/no-missing-placeholders.js | 32 ++++++++++------- lib/rules/no-only-tests.js | 7 ++-- lib/rules/no-property-in-node.js | 4 +-- lib/rules/no-unused-message-ids.js | 37 ++++++++++++-------- lib/rules/no-unused-placeholders.js | 29 +++++++++------ lib/rules/no-useless-token-range.js | 8 ++--- lib/rules/prefer-message-ids.js | 28 +++++++++------ lib/rules/prefer-object-rule.js | 6 ++-- lib/rules/prefer-output-null.js | 12 +++---- lib/rules/prefer-placeholders.js | 20 +++++++---- lib/rules/prefer-replace-text.js | 14 +++++--- lib/rules/report-message-format.js | 22 +++++++----- lib/rules/require-meta-default-options.js | 29 +++++++-------- lib/rules/require-meta-docs-description.js | 13 +++---- lib/rules/require-meta-docs-recommended.js | 15 +++++--- lib/rules/require-meta-docs-url.js | 30 ++++++++-------- lib/rules/require-meta-fixable.js | 30 +++++++++------- lib/rules/require-meta-has-suggestions.js | 37 ++++++++++++-------- lib/rules/require-meta-schema-description.js | 18 +++++----- lib/rules/require-meta-schema.js | 26 ++++++++------ lib/rules/require-meta-type.js | 18 +++++----- lib/rules/test-case-property-ordering.js | 8 ++--- lib/rules/test-case-shorthand-strings.js | 9 +++-- lib/utils.js | 21 ----------- 33 files changed, 327 insertions(+), 269 deletions(-) diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.js index b0eb7f93..3c12e0ee 100644 --- a/lib/rules/consistent-output.js +++ b/lib/rules/consistent-output.js @@ -3,7 +3,7 @@ * @author Teddy Katz */ -import * as utils from '../utils.js'; +import { getKeyName, getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -44,13 +44,13 @@ const rule = { return { Program(ast) { - utils.getTestInfo(context, ast).forEach((testRun) => { + getTestInfo(context, ast).forEach((testRun) => { const readableCases = testRun.invalid.filter( (testCase) => testCase.type === 'ObjectExpression', ); const casesWithoutOutput = readableCases.filter( (testCase) => - !testCase.properties.map(utils.getKeyName).includes('output'), + !testCase.properties.map(getKeyName).includes('output'), ); if ( diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js index 0f29d8ac..3fb92911 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.js @@ -3,13 +3,14 @@ * @author 薛定谔的猫 */ -// ------------------------------------------------------------------------------ -// Requirements -// ------------------------------------------------------------------------------ - -import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; +import { + getContextIdentifiers, + isAutoFixerFunction, + isSuggestionFixerFunction, +} from '../utils.js'; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -70,7 +71,6 @@ 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 - * @param {Context} context * @returns {boolean} */ function isFix(node) { @@ -78,7 +78,7 @@ const rule = { // An empty array is not a fix. return false; } - const scope = utils.getScope(context); + const scope = context.sourceCode.getScope(node); const staticValue = getStaticValue(node, scope); if (!staticValue) { // If we can't find a static value, assume it's a real fix value. @@ -96,8 +96,8 @@ const rule = { return { Program(ast) { - const sourceCode = utils.getSourceCode(context); - contextIdentifiers = utils.getContextIdentifiers( + const sourceCode = context.sourceCode; + contextIdentifiers = getContextIdentifiers( sourceCode.scopeManager, ast, ); @@ -111,8 +111,8 @@ const rule = { hasYieldWithFixer: false, hasReturnWithFixer: false, shouldCheck: - utils.isAutoFixerFunction(node, contextIdentifiers) || - utils.isSuggestionFixerFunction(node, contextIdentifiers), + isAutoFixerFunction(node, contextIdentifiers) || + isSuggestionFixerFunction(node, contextIdentifiers), node, }; }, @@ -146,7 +146,7 @@ const rule = { // Ensure the current (arrow) fixer function returned a fix. 'ArrowFunctionExpression:exit'(node) { if (funcInfo.shouldCheck) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; 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. diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.js index 1871187e..e66d9320 100644 --- a/lib/rules/meta-property-ordering.js +++ b/lib/rules/meta-property-ordering.js @@ -2,9 +2,7 @@ * @fileoverview Enforces the order of meta properties */ -import * as utils from '../utils.js'; - -const { getKeyName, getRuleInfo } = utils; +import { getKeyName, getRuleInfo } from '../utils.js'; const defaultOrder = [ 'type', @@ -48,7 +46,7 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js index 416ec988..490dd096 100644 --- a/lib/rules/no-deprecated-context-methods.js +++ b/lib/rules/no-deprecated-context-methods.js @@ -3,7 +3,7 @@ * @author Teddy Katz */ -import * as utils from '../utils.js'; +import { getContextIdentifiers } from '../utils.js'; const DEPRECATED_PASSTHROUGHS = { getSource: 'getText', @@ -52,7 +52,7 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; // ---------------------------------------------------------------------- // Public @@ -60,7 +60,7 @@ const rule = { return { 'Program:exit'(ast) { - [...utils.getContextIdentifiers(sourceCode.scopeManager, ast)] + [...getContextIdentifiers(sourceCode.scopeManager, ast)] .filter( (contextId) => contextId.parent.type === 'MemberExpression' && diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.js index a0e3f830..cae105ac 100644 --- a/lib/rules/no-deprecated-report-api.js +++ b/lib/rules/no-deprecated-report-api.js @@ -3,7 +3,7 @@ * @author Teddy Katz */ -import * as utils from '../utils.js'; +import { getContextIdentifiers, getReportInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -28,7 +28,7 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; let contextIdentifiers; // ---------------------------------------------------------------------- @@ -37,7 +37,7 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers( + contextIdentifiers = getContextIdentifiers( sourceCode.scopeManager, ast, ); @@ -58,7 +58,7 @@ const rule = { fix(fixer) { const openingParen = sourceCode.getTokenBefore(node.arguments[0]); const closingParen = sourceCode.getLastToken(node); - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return null; diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.js index 2dfce64c..5c192e01 100644 --- a/lib/rules/no-identical-tests.js +++ b/lib/rules/no-identical-tests.js @@ -3,7 +3,7 @@ * @author 薛定谔的猫 */ -import * as utils from '../utils.js'; +import { getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -30,7 +30,7 @@ const rule = { // ---------------------------------------------------------------------- // Public // ---------------------------------------------------------------------- - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; // ---------------------------------------------------------------------- // Helpers @@ -52,7 +52,7 @@ const rule = { return { Program(ast) { - utils.getTestInfo(context, ast).forEach((testRun) => { + getTestInfo(context, ast).forEach((testRun) => { [testRun.valid, testRun.invalid].forEach((tests) => { const cache = new Set(); tests.forEach((test) => { diff --git a/lib/rules/no-meta-replaced-by.js b/lib/rules/no-meta-replaced-by.js index e6f59523..8ceb6f41 100644 --- a/lib/rules/no-meta-replaced-by.js +++ b/lib/rules/no-meta-replaced-by.js @@ -2,7 +2,7 @@ * @fileoverview Disallows the usage of `meta.replacedBy` property */ -import * as utils from '../utils.js'; +import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -25,8 +25,8 @@ const rule = { }, }, create(context) { - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; @@ -40,12 +40,10 @@ const rule = { return; } - const replacedByNode = utils - .evaluateObjectProperties(metaNode, sourceCode.scopeManager) - .find( - (p) => - p.type === 'Property' && utils.getKeyName(p) === 'replacedBy', - ); + const replacedByNode = evaluateObjectProperties( + metaNode, + sourceCode.scopeManager, + ).find((p) => p.type === 'Property' && getKeyName(p) === 'replacedBy'); if (!replacedByNode) { return; diff --git a/lib/rules/no-meta-schema-default.js b/lib/rules/no-meta-schema-default.js index 26190088..89aafc0b 100644 --- a/lib/rules/no-meta-schema-default.js +++ b/lib/rules/no-meta-schema-default.js @@ -1,5 +1,10 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { + getMetaSchemaNode, + getMetaSchemaNodeProperty, + getRuleInfo, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -23,22 +28,19 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } - const schemaNode = utils.getMetaSchemaNode(ruleInfo.meta, scopeManager); + const schemaNode = getMetaSchemaNode(ruleInfo.meta, scopeManager); if (!schemaNode) { return {}; } - const schemaProperty = utils.getMetaSchemaNodeProperty( - schemaNode, - scopeManager, - ); + const schemaProperty = getMetaSchemaNodeProperty(schemaNode, scopeManager); if (schemaProperty?.type === 'ObjectExpression') { checkSchemaElement(schemaProperty, true); diff --git a/lib/rules/no-missing-message-ids.js b/lib/rules/no-missing-message-ids.js index 214a9e35..81dfdf0a 100644 --- a/lib/rules/no-missing-message-ids.js +++ b/lib/rules/no-missing-message-ids.js @@ -1,4 +1,12 @@ -import * as utils from '../utils.js'; +import { + collectReportViolationAndSuggestionData, + findPossibleVariableValues, + getContextIdentifiers, + getMessagesNode, + getMessageIdNodeById, + getReportInfo, + getRuleInfo, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -24,14 +32,14 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } - const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager); + const messagesNode = getMessagesNode(ruleInfo, scopeManager); let contextIdentifiers; @@ -42,11 +50,11 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, CallExpression(node) { - const scope = utils.getScope(context); + const scope = sourceCode.getScope(node); // Check for messageId properties used in known calls to context.report(); if ( node.callee.type === 'MemberExpression' && @@ -54,20 +62,20 @@ const rule = { node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return; } const reportMessagesAndDataArray = - utils.collectReportViolationAndSuggestionData(reportInfo); + collectReportViolationAndSuggestionData(reportInfo); for (const { messageId } of reportMessagesAndDataArray.filter( (obj) => obj.messageId, )) { const values = messageId.type === 'Literal' ? [messageId] - : utils.findPossibleVariableValues(messageId, scopeManager); + : findPossibleVariableValues(messageId, scopeManager); // Look for any possible string values we found for this messageId. values.forEach((val) => { @@ -75,12 +83,7 @@ const rule = { val.type === 'Literal' && typeof val.value === 'string' && val.value !== '' && - !utils.getMessageIdNodeById( - val.value, - ruleInfo, - scopeManager, - scope, - ) + !getMessageIdNodeById(val.value, ruleInfo, scopeManager, scope) ) // Couldn't find this messageId in `meta.messages`. context.report({ diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js index f4f126db..522ed207 100644 --- a/lib/rules/no-missing-placeholders.js +++ b/lib/rules/no-missing-placeholders.js @@ -2,10 +2,18 @@ * @fileoverview Disallow missing placeholders in rule report messages * @author Teddy Katz */ - -import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; +import { + collectReportViolationAndSuggestionData, + getKeyName, + getReportInfo, + getRuleInfo, + getMessagesNode, + getMessageIdNodeById, + getContextIdentifiers, +} from '../utils.js'; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -29,37 +37,37 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; let contextIdentifiers; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } - const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager); + const messagesNode = getMessagesNode(ruleInfo, scopeManager); return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, CallExpression(node) { - const scope = utils.getScope(context); + const scope = sourceCode.getScope(node); if ( node.callee.type === 'MemberExpression' && contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return; } const reportMessagesAndDataArray = - utils.collectReportViolationAndSuggestionData(reportInfo); + collectReportViolationAndSuggestionData(reportInfo); if (messagesNode) { // Check for any potential instances where we can use the messageId to fill in the message for convenience. @@ -70,7 +78,7 @@ const rule = { obj.messageId.type === 'Literal' && typeof obj.messageId.value === 'string' ) { - const correspondingMessage = utils.getMessageIdNodeById( + const correspondingMessage = getMessageIdNodeById( obj.messageId.value, ruleInfo, scopeManager, @@ -108,9 +116,7 @@ const rule = { ) { const matchingProperty = data && - data.properties.find( - (prop) => utils.getKeyName(prop) === match[1], - ); + data.properties.find((prop) => getKeyName(prop) === match[1]); if (!matchingProperty) { context.report({ diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js index 43bf79d1..87246d94 100644 --- a/lib/rules/no-only-tests.js +++ b/lib/rules/no-only-tests.js @@ -1,10 +1,11 @@ -import * as utils from '../utils.js'; import { isCommaToken, isOpeningBraceToken, isClosingBraceToken, } from '@eslint-community/eslint-utils'; +import { getTestInfo } from '../utils.js'; + /** @type {import('eslint').Rule.RuleModule} */ const rule = { meta: { @@ -27,7 +28,7 @@ const rule = { create(context) { return { Program(ast) { - for (const testRun of utils.getTestInfo(context, ast)) { + for (const testRun of getTestInfo(context, ast)) { for (const test of [...testRun.valid, ...testRun.invalid]) { if (test.type === 'ObjectExpression') { // Test case object: { code: 'const x = 123;', ... } @@ -49,7 +50,7 @@ const rule = { { messageId: 'removeOnly', *fix(fixer) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const tokenBefore = sourceCode.getTokenBefore(onlyProperty); diff --git a/lib/rules/no-property-in-node.js b/lib/rules/no-property-in-node.js index 5a92b749..86f2fa41 100644 --- a/lib/rules/no-property-in-node.js +++ b/lib/rules/no-property-in-node.js @@ -1,5 +1,3 @@ -import * as utils from '../utils.js'; - const defaultTypedNodeSourceFileTesters = [ /@types[/\\]estree[/\\]index\.d\.ts/, /@typescript-eslint[/\\]types[/\\]dist[/\\]generated[/\\]ast-spec\.d\.ts/, @@ -88,7 +86,7 @@ const rule = { 'BinaryExpression[operator=in]'(node) { // TODO: Switch this to ESLintUtils.getParserServices with typescript-eslint@>=6 // https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/269 - const services = utils.getparserServices(context); + const services = context.sourceCode.parserServices; if (!services.program) { throw new Error( 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.js index 492e817d..8b434d1c 100644 --- a/lib/rules/no-unused-message-ids.js +++ b/lib/rules/no-unused-message-ids.js @@ -1,4 +1,13 @@ -import * as utils from '../utils.js'; +import { + collectReportViolationAndSuggestionData, + findPossibleVariableValues, + getContextIdentifiers, + getKeyName, + getMessageIdNodes, + getReportInfo, + getRuleInfo, + isVariableFromParameter, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -22,9 +31,9 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } @@ -34,7 +43,7 @@ const rule = { let hasSeenUnknownMessageId = false; let hasSeenViolationReport = false; - const messageIdNodes = utils.getMessageIdNodes(ruleInfo, scopeManager); + const messageIdNodes = getMessageIdNodes(ruleInfo, scopeManager); if (!messageIdNodes) { // If we can't find `meta.messages`, disable the rule. return {}; @@ -42,10 +51,10 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, - 'Program:exit'() { + 'Program:exit'(ast) { if (hasSeenUnknownMessageId || !hasSeenViolationReport) { /* Bail out when the rule is likely to have false positives. @@ -55,10 +64,10 @@ const rule = { return; } - const scope = utils.getScope(context); + const scope = sourceCode.getScope(ast); const messageIdNodesUnused = messageIdNodes.filter( - (node) => !messageIdsUsed.has(utils.getKeyName(node, scope)), + (node) => !messageIdsUsed.has(getKeyName(node, scope)), ); // Report any messageIds that were never used. @@ -67,7 +76,7 @@ const rule = { node: messageIdNode, messageId: 'unusedMessage', data: { - messageId: utils.getKeyName(messageIdNode, scope), + messageId: getKeyName(messageIdNode, scope), }, }); } @@ -81,7 +90,7 @@ const rule = { node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return; } @@ -89,14 +98,14 @@ const rule = { hasSeenViolationReport = true; const reportMessagesAndDataArray = - utils.collectReportViolationAndSuggestionData(reportInfo); + collectReportViolationAndSuggestionData(reportInfo); for (const { messageId } of reportMessagesAndDataArray.filter( (obj) => obj.messageId, )) { const values = messageId.type === 'Literal' ? [messageId] - : utils.findPossibleVariableValues(messageId, scopeManager); + : findPossibleVariableValues(messageId, scopeManager); if ( values.length === 0 || values.some((val) => val.type !== 'Literal') @@ -118,12 +127,12 @@ const rule = { const values = node.value.type === 'Literal' ? [node.value] - : utils.findPossibleVariableValues(node.value, scopeManager); + : findPossibleVariableValues(node.value, scopeManager); if ( values.length === 0 || values.some((val) => val.type !== 'Literal') || - utils.isVariableFromParameter(node.value, scopeManager) + 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; diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js index 7fbf9cf6..914eba56 100644 --- a/lib/rules/no-unused-placeholders.js +++ b/lib/rules/no-unused-placeholders.js @@ -3,9 +3,18 @@ * @author 薛定谔的猫 */ -import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; +import { + collectReportViolationAndSuggestionData, + getContextIdentifiers, + getKeyName, + getMessageIdNodeById, + getMessagesNode, + getReportInfo, + getRuleInfo, +} from '../utils.js'; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -29,36 +38,36 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; let contextIdentifiers; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } - const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager); + const messagesNode = getMessagesNode(ruleInfo, scopeManager); return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, CallExpression(node) { - const scope = utils.getScope(context); + const scope = sourceCode.getScope(node); if ( node.callee.type === 'MemberExpression' && contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return; } const reportMessagesAndDataArray = - utils.collectReportViolationAndSuggestionData(reportInfo); + collectReportViolationAndSuggestionData(reportInfo); if (messagesNode) { // Check for any potential instances where we can use the messageId to fill in the message for convenience. @@ -69,7 +78,7 @@ const rule = { obj.messageId.type === 'Literal' && typeof obj.messageId.value === 'string' ) { - const correspondingMessage = utils.getMessageIdNodeById( + const correspondingMessage = getMessageIdNodeById( obj.messageId.value, ruleInfo, scopeManager, @@ -107,7 +116,7 @@ const rule = { ); data.properties.forEach((prop) => { - const key = utils.getKeyName(prop); + const key = getKeyName(prop); if (!placeholdersInMessage.has(key)) { context.report({ node: prop, diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.js index 5cf35fec..0ae826cb 100644 --- a/lib/rules/no-useless-token-range.js +++ b/lib/rules/no-useless-token-range.js @@ -3,7 +3,7 @@ * @author Teddy Katz */ -import * as utils from '../utils.js'; +import { getKeyName, getSourceCodeIdentifiers } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -28,7 +28,7 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; // ---------------------------------------------------------------------- // Helpers @@ -50,7 +50,7 @@ const rule = { return ( arg.properties.length >= 2 || (arg.properties.length === 1 && - (utils.getKeyName(arg.properties[0]) !== 'includeComments' || + (getKeyName(arg.properties[0]) !== 'includeComments' || arg.properties[0].value.type !== 'Literal')) ); } @@ -120,7 +120,7 @@ const rule = { return { 'Program:exit'(ast) { - [...utils.getSourceCodeIdentifiers(sourceCode.scopeManager, ast)] + [...getSourceCodeIdentifiers(sourceCode.scopeManager, ast)] .filter( (identifier) => identifier.parent.type === 'MemberExpression' && diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js index cefbab65..fc5cc51f 100644 --- a/lib/rules/prefer-message-ids.js +++ b/lib/rules/prefer-message-ids.js @@ -1,6 +1,13 @@ -import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; +import { + collectReportViolationAndSuggestionData, + getContextIdentifiers, + getKeyName, + getReportInfo, + getRuleInfo, +} from '../utils.js'; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -27,8 +34,8 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } @@ -41,8 +48,8 @@ const rule = { return { Program(ast) { - const scope = utils.getScope(context); - contextIdentifiers = utils.getContextIdentifiers( + const scope = sourceCode.getScope(ast); + contextIdentifiers = getContextIdentifiers( sourceCode.scopeManager, ast, ); @@ -52,7 +59,7 @@ const rule = { metaNode && metaNode.properties && metaNode.properties.find( - (p) => p.type === 'Property' && utils.getKeyName(p) === 'messages', + (p) => p.type === 'Property' && getKeyName(p) === 'messages', ); if (!messagesNode) { @@ -86,14 +93,15 @@ const rule = { node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return; } - const reportMessagesAndDataArray = utils - .collectReportViolationAndSuggestionData(reportInfo) - .filter((obj) => obj.message); + const reportMessagesAndDataArray = + collectReportViolationAndSuggestionData(reportInfo).filter( + (obj) => obj.message, + ); for (const { message } of reportMessagesAndDataArray) { context.report({ node: message.parent, diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js index 85faaedf..ec0a0470 100644 --- a/lib/rules/prefer-object-rule.js +++ b/lib/rules/prefer-object-rule.js @@ -2,7 +2,7 @@ * @author Brad Zacher */ -import * as utils from '../utils.js'; +import { getRuleInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -30,8 +30,8 @@ const rule = { // Public // ---------------------------------------------------------------------- - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js index 99819814..7fa78a66 100644 --- a/lib/rules/prefer-output-null.js +++ b/lib/rules/prefer-output-null.js @@ -3,7 +3,7 @@ * @author 薛定谔的猫 */ -import * as utils from '../utils.js'; +import { getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -33,18 +33,18 @@ const rule = { // Public // ---------------------------------------------------------------------- - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; return { Program(ast) { - utils.getTestInfo(context, ast).forEach((testRun) => { + 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 getTestInfo(key) { + function getTestInfoProperty(key) { if (test.type === 'ObjectExpression') { return test.properties.find( (item) => item.type === 'Property' && item.key.name === key, @@ -53,8 +53,8 @@ const rule = { return null; } - const code = getTestInfo('code'); - const output = getTestInfo('output'); + const code = getTestInfoProperty('code'); + const output = getTestInfoProperty('output'); if ( output && diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.js index 4ace1011..18868883 100644 --- a/lib/rules/prefer-placeholders.js +++ b/lib/rules/prefer-placeholders.js @@ -3,9 +3,14 @@ * @author Teddy Katz */ -import * as utils from '../utils.js'; import { findVariable } from '@eslint-community/eslint-utils'; +import { + collectReportViolationAndSuggestionData, + getContextIdentifiers, + getReportInfo, +} from '../utils.js'; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -31,7 +36,7 @@ const rule = { create(context) { let contextIdentifiers; - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; // ---------------------------------------------------------------------- @@ -40,7 +45,7 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, CallExpression(node) { if ( @@ -49,15 +54,16 @@ const rule = { node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); if (!reportInfo) { return; } - const reportMessagesAndDataArray = utils - .collectReportViolationAndSuggestionData(reportInfo) - .filter((obj) => obj.message); + const reportMessagesAndDataArray = + collectReportViolationAndSuggestionData(reportInfo).filter( + (obj) => obj.message, + ); for (let { message: messageNode } of reportMessagesAndDataArray) { if (messageNode.type === 'Identifier') { // See if we can find the variable declaration. diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js index 2a1041e1..3d5db946 100644 --- a/lib/rules/prefer-replace-text.js +++ b/lib/rules/prefer-replace-text.js @@ -3,7 +3,11 @@ * @author 薛定谔的猫 */ -import * as utils from '../utils.js'; +import { + getContextIdentifiers, + isAutoFixerFunction, + isSuggestionFixerFunction, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -28,7 +32,7 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; let funcInfo = { upper: null, codePath: null, @@ -39,7 +43,7 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers( + contextIdentifiers = getContextIdentifiers( sourceCode.scopeManager, ast, ); @@ -51,8 +55,8 @@ const rule = { upper: funcInfo, codePath, shouldCheck: - utils.isAutoFixerFunction(node, contextIdentifiers) || - utils.isSuggestionFixerFunction(node, contextIdentifiers), + isAutoFixerFunction(node, contextIdentifiers) || + isSuggestionFixerFunction(node, contextIdentifiers), node, }; }, diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.js index b9d7588f..7d6790b5 100644 --- a/lib/rules/report-message-format.js +++ b/lib/rules/report-message-format.js @@ -4,7 +4,13 @@ */ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { + getContextIdentifiers, + getKeyName, + getReportInfo, + getRuleInfo, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -61,8 +67,8 @@ const rule = { } } - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } @@ -73,8 +79,8 @@ const rule = { return { Program(ast) { - const scope = utils.getScope(context); - contextIdentifiers = utils.getContextIdentifiers( + const scope = sourceCode.getScope(ast); + contextIdentifiers = getContextIdentifiers( sourceCode.scopeManager, ast, ); @@ -85,7 +91,7 @@ const rule = { ruleInfo.meta.type === 'ObjectExpression' && ruleInfo.meta.properties.find( (prop) => - prop.type === 'Property' && utils.getKeyName(prop) === 'messages', + prop.type === 'Property' && getKeyName(prop) === 'messages', ); if ( @@ -101,14 +107,14 @@ const rule = { .forEach((it) => processMessageNode(it, scope)); }, CallExpression(node) { - const scope = utils.getScope(context); + const scope = sourceCode.getScope(node); if ( node.callee.type === 'MemberExpression' && contextIdentifiers.has(node.callee.object) && node.callee.property.type === 'Identifier' && node.callee.property.name === 'report' ) { - const reportInfo = utils.getReportInfo(node, context); + const reportInfo = getReportInfo(node, context); const message = reportInfo && reportInfo.message; const suggest = reportInfo && reportInfo.suggest; diff --git a/lib/rules/require-meta-default-options.js b/lib/rules/require-meta-default-options.js index 1e5d0a9c..24c7e2cc 100644 --- a/lib/rules/require-meta-default-options.js +++ b/lib/rules/require-meta-default-options.js @@ -1,4 +1,10 @@ -import * as utils from '../utils.js'; +import { + evaluateObjectProperties, + getKeyName, + getMetaSchemaNode, + getMetaSchemaNodeProperty, + getRuleInfo, +} from '../utils.js'; /** @type {import('eslint').Rule.RuleModule} */ const rule = { @@ -24,30 +30,25 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } const metaNode = ruleInfo.meta; - const schemaNode = utils.getMetaSchemaNode(metaNode, scopeManager); - const schemaProperty = utils.getMetaSchemaNodeProperty( - schemaNode, - scopeManager, - ); + const schemaNode = getMetaSchemaNode(metaNode, scopeManager); + const schemaProperty = getMetaSchemaNodeProperty(schemaNode, scopeManager); if (!schemaProperty) { return {}; } - const metaDefaultOptions = utils - .evaluateObjectProperties(metaNode, scopeManager) - .find( - (p) => - p.type === 'Property' && utils.getKeyName(p) === 'defaultOptions', - ); + const metaDefaultOptions = evaluateObjectProperties( + metaNode, + scopeManager, + ).find((p) => p.type === 'Property' && getKeyName(p) === 'defaultOptions'); if ( schemaProperty.type === 'ArrayExpression' && diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js index d2101144..4f1dc6ea 100644 --- a/lib/rules/require-meta-docs-description.js +++ b/lib/rules/require-meta-docs-description.js @@ -1,5 +1,6 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { getMetaDocsProperty, getRuleInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -44,22 +45,22 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } return { - Program() { - const scope = utils.getScope(context); + Program(ast) { + const scope = sourceCode.getScope(ast); const { scopeManager } = sourceCode; const { docsNode, metaNode, metaPropertyNode: descriptionNode, - } = utils.getMetaDocsProperty('description', ruleInfo, scopeManager); + } = getMetaDocsProperty('description', ruleInfo, scopeManager); if (!descriptionNode) { context.report({ diff --git a/lib/rules/require-meta-docs-recommended.js b/lib/rules/require-meta-docs-recommended.js index 5ba260c4..3038ea3c 100644 --- a/lib/rules/require-meta-docs-recommended.js +++ b/lib/rules/require-meta-docs-recommended.js @@ -1,5 +1,10 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { + getMetaDocsProperty, + getRuleInfo, + isUndefinedIdentifier, +} from '../utils.js'; /** * @param {import('eslint').Rule.RuleFixer} fixer @@ -55,8 +60,8 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } @@ -66,7 +71,7 @@ const rule = { docsNode, metaNode, metaPropertyNode: descriptionNode, - } = utils.getMetaDocsProperty('recommended', ruleInfo, scopeManager); + } = getMetaDocsProperty('recommended', ruleInfo, scopeManager); if (!descriptionNode) { const suggestions = @@ -97,7 +102,7 @@ const rule = { return {}; } - const staticValue = utils.isUndefinedIdentifier(descriptionNode.value) + const staticValue = isUndefinedIdentifier(descriptionNode.value) ? { value: undefined } : getStaticValue(descriptionNode.value); diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js index 4e3f38e3..41e3a350 100644 --- a/lib/rules/require-meta-docs-url.js +++ b/lib/rules/require-meta-docs-url.js @@ -2,14 +2,16 @@ * @author Toru Nagashima */ -// ----------------------------------------------------------------------------- -// Requirements -// ----------------------------------------------------------------------------- - import path from 'node:path'; -import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; +import { + getMetaDocsProperty, + getRuleInfo, + insertProperty, + isUndefinedIdentifier, +} from '../utils.js'; + // ----------------------------------------------------------------------------- // Rule Definition // ----------------------------------------------------------------------------- @@ -53,7 +55,7 @@ const rule = { */ create(context) { const options = context.options[0] || {}; - const filename = utils.getFilename(context); + const filename = context.filename; const ruleName = filename === '' ? undefined @@ -75,22 +77,22 @@ const rule = { ); } - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } return { - Program() { - const scope = utils.getScope(context); + Program(ast) { + const scope = sourceCode.getScope(ast); const { scopeManager } = sourceCode; const { docsNode, metaNode, metaPropertyNode: urlPropNode, - } = utils.getMetaDocsProperty('url', ruleInfo, scopeManager); + } = getMetaDocsProperty('url', ruleInfo, scopeManager); const staticValue = urlPropNode ? getStaticValue(urlPropNode.value, scope) @@ -132,12 +134,12 @@ const rule = { if (urlPropNode) { if ( urlPropNode.value.type === 'Literal' || - utils.isUndefinedIdentifier(urlPropNode.value) + isUndefinedIdentifier(urlPropNode.value) ) { return fixer.replaceText(urlPropNode.value, urlString); } } else if (docsNode && docsNode.value.type === 'ObjectExpression') { - return utils.insertProperty( + return insertProperty( fixer, docsNode.value, `url: ${urlString}`, @@ -148,7 +150,7 @@ const rule = { metaNode && metaNode.type === 'ObjectExpression' ) { - return utils.insertProperty( + return insertProperty( fixer, metaNode, `docs: {\nurl: ${urlString}\n}`, diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.js index 0eb99eb6..67607aca 100644 --- a/lib/rules/require-meta-fixable.js +++ b/lib/rules/require-meta-fixable.js @@ -4,7 +4,13 @@ */ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { + evaluateObjectProperties, + getContextIdentifiers, + getKeyName, + getRuleInfo, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -48,9 +54,9 @@ const rule = { const catchNoFixerButFixableProperty = context.options[0] && context.options[0].catchNoFixerButFixableProperty; - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); let contextIdentifiers; let usesFixFunctions; @@ -60,7 +66,7 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, CallExpression(node) { if ( @@ -70,20 +76,20 @@ const rule = { node.callee.property.name === 'report' && (node.arguments.length > 4 || (node.arguments.length === 1 && - utils - .evaluateObjectProperties(node.arguments[0], scopeManager) - .some((prop) => utils.getKeyName(prop) === 'fix'))) + evaluateObjectProperties(node.arguments[0], scopeManager).some( + (prop) => getKeyName(prop) === 'fix', + ))) ) { usesFixFunctions = true; } }, - 'Program:exit'() { - const scope = utils.getScope(context); + 'Program:exit'(ast) { + const scope = sourceCode.getScope(ast); const metaFixableProp = ruleInfo && - utils - .evaluateObjectProperties(ruleInfo.meta, scopeManager) - .find((prop) => utils.getKeyName(prop) === 'fixable'); + evaluateObjectProperties(ruleInfo.meta, scopeManager).find( + (prop) => getKeyName(prop) === 'fixable', + ); if (metaFixableProp) { const staticValue = getStaticValue(metaFixableProp.value, scope); diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js index b9e2f44b..18b483ec 100644 --- a/lib/rules/require-meta-has-suggestions.js +++ b/lib/rules/require-meta-has-suggestions.js @@ -1,6 +1,13 @@ -import * as utils from '../utils.js'; import { getStaticValue } from '@eslint-community/eslint-utils'; +import { + evaluateObjectProperties, + getContextIdentifiers, + getKeyName, + getRuleInfo, + isUndefinedIdentifier, +} from '../utils.js'; + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -27,9 +34,9 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } @@ -42,7 +49,7 @@ const rule = { * @returns {boolean} whether this property should be considered to contain suggestions */ function doesPropertyContainSuggestions(node) { - const scope = utils.getScope(context); + const scope = sourceCode.getScope(node); const staticValue = getStaticValue(node.value, scope); if ( !staticValue || @@ -62,7 +69,7 @@ const rule = { return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, CallExpression(node) { if ( @@ -74,9 +81,10 @@ const rule = { (node.arguments.length === 1 && node.arguments[0].type === 'ObjectExpression')) ) { - const suggestProp = utils - .evaluateObjectProperties(node.arguments[0], scopeManager) - .find((prop) => utils.getKeyName(prop) === 'suggest'); + const suggestProp = evaluateObjectProperties( + node.arguments[0], + scopeManager, + ).find((prop) => getKeyName(prop) === 'suggest'); if (suggestProp && doesPropertyContainSuggestions(suggestProp)) { ruleReportsSuggestions = true; } @@ -93,12 +101,13 @@ const rule = { ruleReportsSuggestions = true; } }, - 'Program:exit'() { - const scope = utils.getScope(context); + 'Program:exit'(ast) { + const scope = sourceCode.getScope(ast); const metaNode = ruleInfo && ruleInfo.meta; - const hasSuggestionsProperty = utils - .evaluateObjectProperties(metaNode, scopeManager) - .find((prop) => utils.getKeyName(prop) === 'hasSuggestions'); + const hasSuggestionsProperty = evaluateObjectProperties( + metaNode, + scopeManager, + ).find((prop) => getKeyName(prop) === 'hasSuggestions'); const hasSuggestionsStaticValue = hasSuggestionsProperty && getStaticValue(hasSuggestionsProperty.value, scope); @@ -137,7 +146,7 @@ const rule = { fix(fixer) { if ( hasSuggestionsProperty.value.type === 'Literal' || - utils.isUndefinedIdentifier(hasSuggestionsProperty.value) + isUndefinedIdentifier(hasSuggestionsProperty.value) ) { return fixer.replaceText( hasSuggestionsProperty.value, diff --git a/lib/rules/require-meta-schema-description.js b/lib/rules/require-meta-schema-description.js index f9bfe7b0..b15669ec 100644 --- a/lib/rules/require-meta-schema-description.js +++ b/lib/rules/require-meta-schema-description.js @@ -1,5 +1,10 @@ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { + getMetaSchemaNode, + getMetaSchemaNodeProperty, + getRuleInfo, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -23,22 +28,19 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } - const schemaNode = utils.getMetaSchemaNode(ruleInfo.meta, scopeManager); + const schemaNode = getMetaSchemaNode(ruleInfo.meta, scopeManager); if (!schemaNode) { return {}; } - const schemaProperty = utils.getMetaSchemaNodeProperty( - schemaNode, - scopeManager, - ); + const schemaProperty = getMetaSchemaNodeProperty(schemaNode, scopeManager); if (schemaProperty?.type !== 'ArrayExpression') { return {}; } diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js index 45d28c91..da65ad9a 100644 --- a/lib/rules/require-meta-schema.js +++ b/lib/rules/require-meta-schema.js @@ -1,4 +1,11 @@ -import * as utils from '../utils.js'; +import { + getContextIdentifiers, + getMetaSchemaNode, + getMetaSchemaNodeProperty, + getRuleInfo, + insertProperty, + isUndefinedIdentifier, +} from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -41,9 +48,9 @@ const rule = { }, create(context) { - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - const ruleInfo = utils.getRuleInfo(sourceCode); + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } @@ -59,15 +66,12 @@ const rule = { let hasEmptySchema = false; let isUsingOptions = false; - const schemaNode = utils.getMetaSchemaNode(metaNode, scopeManager); - const schemaProperty = utils.getMetaSchemaNodeProperty( - schemaNode, - scopeManager, - ); + const schemaNode = getMetaSchemaNode(metaNode, scopeManager); + const schemaProperty = getMetaSchemaNodeProperty(schemaNode, scopeManager); return { Program(ast) { - contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast); + contextIdentifiers = getContextIdentifiers(scopeManager, ast); if (!schemaProperty) { return; @@ -85,7 +89,7 @@ const rule = { if ( schemaProperty.type === 'Literal' || - utils.isUndefinedIdentifier(schemaProperty) + isUndefinedIdentifier(schemaProperty) ) { context.report({ node: schemaProperty, messageId: 'wrongType' }); } @@ -104,7 +108,7 @@ const rule = { { messageId: 'addEmptySchema', fix(fixer) { - return utils.insertProperty( + return insertProperty( fixer, metaNode, 'schema: []', diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js index d0d77103..31ad1aba 100644 --- a/lib/rules/require-meta-type.js +++ b/lib/rules/require-meta-type.js @@ -4,7 +4,9 @@ */ import { getStaticValue } from '@eslint-community/eslint-utils'; -import * as utils from '../utils.js'; + +import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; + const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']); // ------------------------------------------------------------------------------ @@ -36,21 +38,21 @@ const rule = { // Public // ---------------------------------------------------------------------- - const sourceCode = utils.getSourceCode(context); - const ruleInfo = utils.getRuleInfo(sourceCode); + const sourceCode = context.sourceCode; + const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { return {}; } return { - Program() { - const scope = utils.getScope(context); + Program(ast) { + const scope = sourceCode.getScope(ast); const { scopeManager } = sourceCode; const metaNode = ruleInfo.meta; - const typeNode = utils - .evaluateObjectProperties(metaNode, scopeManager) - .find((p) => p.type === 'Property' && utils.getKeyName(p) === 'type'); + const typeNode = evaluateObjectProperties(metaNode, scopeManager).find( + (p) => p.type === 'Property' && getKeyName(p) === 'type', + ); if (!typeNode) { context.report({ diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.js index 911d39a1..73728e9a 100644 --- a/lib/rules/test-case-property-ordering.js +++ b/lib/rules/test-case-property-ordering.js @@ -3,7 +3,7 @@ * @author 薛定谔的猫 */ -import * as utils from '../utils.js'; +import { getKeyName, getTestInfo } from '../utils.js'; const defaultOrder = [ 'filename', @@ -53,15 +53,15 @@ const rule = { // Public // ---------------------------------------------------------------------- const order = context.options[0] || defaultOrder; - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; return { Program(ast) { - utils.getTestInfo(context, ast).forEach((testRun) => { + getTestInfo(context, ast).forEach((testRun) => { [testRun.valid, testRun.invalid].forEach((tests) => { tests.forEach((test) => { const properties = (test && test.properties) || []; - const keyNames = properties.map(utils.getKeyName); + const keyNames = properties.map(getKeyName); for (let i = 0, lastChecked; i < keyNames.length; i++) { const current = order.indexOf(keyNames[i]); diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.js index 81cd4f57..2d67dd8e 100644 --- a/lib/rules/test-case-shorthand-strings.js +++ b/lib/rules/test-case-shorthand-strings.js @@ -3,7 +3,7 @@ * @author Teddy Katz */ -import * as utils from '../utils.js'; +import { getKeyName, getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition @@ -37,7 +37,7 @@ const rule = { create(context) { const shorthandOption = context.options[0] || 'as-needed'; - const sourceCode = utils.getSourceCode(context); + const sourceCode = context.sourceCode; // ---------------------------------------------------------------------- // Helpers @@ -63,7 +63,7 @@ const rule = { shorthand: false, needsLongform: !( testCase.properties.length === 1 && - utils.getKeyName(testCase.properties[0]) === 'code' + getKeyName(testCase.properties[0]) === 'code' ), }; } @@ -116,8 +116,7 @@ const rule = { return { Program(ast) { - utils - .getTestInfo(context, ast) + getTestInfo(context, ast) .map((testRun) => testRun.valid) .filter(Boolean) .forEach(reportTestCases); diff --git a/lib/utils.js b/lib/utils.js index a672d491..23e1e4b8 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -970,24 +970,3 @@ export function isVariableFromParameter(node, scopeManager) { return variable?.defs[0]?.type === 'Parameter'; } - -export function getSourceCode(context) { - // TODO: remove contet.getSourceCode() when dropping eslint < v9 - return context.sourceCode || context.getSourceCode(); -} - -export function getScope(context) { - // TODO: remove contet.getScope() when dropping eslint < v9 - const sourceCode = context.sourceCode || context.getSourceCode(); - return sourceCode.getScope?.(sourceCode.ast) || context.getScope(); -} - -export function getparserServices(context) { - // TODO: remove context.parserServices when dropping eslint < v9 - return (context.sourceCode || context).parserServices; -} - -export function getFilename(context) { - // TODO: just use context.filename when dropping eslint < v9 - return context.filename || context.getFilename(); -} From 96be60670eb4f94c84d8f853d2ae717c9b8b4ddf Mon Sep 17 00:00:00 2001 From: michael faith Date: Fri, 25 Jul 2025 08:03:07 -0500 Subject: [PATCH 10/15] build: convert eslint-remote-tester config to typescript (#533) * git mv * build: convert eslint-remote-tester config to typescript Supporting the larger typescript migration, this change migrates the config for `eslint-remote-tester` to typescript, which will be necessary to run our plugin from source, once it's typescript. * Bump version of eslint-remote-tester to pick up bug fix --- ...e-tester.config.mjs => eslint-remote-tester.config.ts | 9 ++++++--- package.json | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) rename eslint-remote-tester.config.mjs => eslint-remote-tester.config.ts (89%) diff --git a/eslint-remote-tester.config.mjs b/eslint-remote-tester.config.ts similarity index 89% rename from eslint-remote-tester.config.mjs rename to eslint-remote-tester.config.ts index 313c3349..a22f38c0 100644 --- a/eslint-remote-tester.config.mjs +++ b/eslint-remote-tester.config.ts @@ -1,7 +1,9 @@ -import eslintPlugin from 'eslint-plugin-eslint-plugin'; import tsparser from '@typescript-eslint/parser'; +import type { Config } from 'eslint-remote-tester'; + +// @ts-expect-error - eslint-plugin is not typed yet +import eslintPlugin from './lib/index.js'; -/** @type {import('eslint-remote-tester').Config} */ export default { /** Repositories to scan */ repositories: [ @@ -43,6 +45,7 @@ export default { cache: false, /** ESLint configuration */ + // @ts-expect-error - eslint-plugin is not typed yet eslintConfig: [ { files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], @@ -56,4 +59,4 @@ export default { }, }, ], -}; +} satisfies Config; diff --git a/package.json b/package.json index 6e93218a..aaa146d1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint:package-json": "npmPkgJsonLint .", "release": "release-it", "test": "vitest run --coverage", - "test:remote": "eslint-remote-tester -c ./eslint-remote-tester.config.mjs", + "test:remote": "eslint-remote-tester -c ./eslint-remote-tester.config.ts", "update:eslint-docs": "eslint-doc-generator" }, "files": [ @@ -64,7 +64,7 @@ "eslint-plugin-n": "^17.21.0", "eslint-plugin-prettier": "^5.5.3", "eslint-plugin-unicorn": "^56.0.1", - "eslint-remote-tester": "^4.0.2", + "eslint-remote-tester": "^4.0.3", "eslint-scope": "^8.0.1", "espree": "^10.0.1", "husky": "^9.1.7", From 95b859ab9a263cc623871ac7930c0f83c197163f Mon Sep 17 00:00:00 2001 From: michael faith Date: Sun, 3 Aug 2025 21:02:36 -0500 Subject: [PATCH 11/15] feat: migrate package to TypeScript and publish types (#534) * change test-setup to ts * add build config * add types packages * rename files to ts * migrate fixer-return migrate fixer-return migrate fixer-return * migrate consistent-output migrate consistent-output * migrate meta-property-ordering * migrate no-deprecated-context-methods * migrate no-deprecated-report-api * migrate no-identical-tests * migrate no-meta-replaced-by * migrate no-meta-schema-default * migrate no-missing-message-ids * migrate no-missing-placeholders * migrate no-only-tests * migrate no-property-in-node * migrate no-unused-message-ids * migrate no-unused-placeholders * migrate no-useless-token-range * migrate prefer-message-ids * migrate prefer-object-rule * migrate prefer-output-null * migrate prefer-placeholders * migrate prefer-replace-text * migrate report-message-format * migrate require-meta-default-options * migrate require-meta-docs-description * migrate prefer-replace-text * migrate require-meta-docs-recommended * migrate require-meta-docs-url * migrate require-meta-fixable * migrate require-meta-has-suggestions * migrate require-meta-schema-description * migrate require-meta-schema * migrate require-meta-type * migrate test-case-property-ordering * migrate test-case-shorthand-strings * migrate plugin (index) * migrate indext.ts test * git mv all rule tests * fix type issues in no-meta-replaced-by test * fix type issues with no-missing-placeholders tests * fix type issues with no-unused-placeholders tests * fix type issues with no-useless-token-range tests * fix type issues with report-message-format tests * remove invalid case from valid array * fix type issues with test-case-shorthand-strings * fix type issues in rule-setup tests * Add explicit extensions to imports without them * fix type issues in utils tests * fix merge issue * adjust import order * fix plugin type * fix utils tests * switch to tsup for build * Change import of `package.json` to require, for backwards compatibility * fix utils tests * update rule-setup tests * add build to publish workflow * add slashes to .gitignore * remove jsdoc type annotation from `fixer-return` * remove unnecessary param from `no-indentical-tests` * add early return in `no-missing-placeholders` * removed assert in `no-only-tests` * remove empty param annotation from `no-property-in-node` * remove casting from no-unused-message-ids * add back valid test case to require-meta-type * add explanatory comment to estree.d.ts * remove type annotation from comment in test-case-shorthand-string * removed casting from no-meta-schema-default * remove casts from no-useless-token-range * remove cast from report-message-format * remove cast from require-meta-default-options * remove cast from require-meta-docs-url * remove casts from require-meta-fixables * remove cast from require-meta-type * Adjust PartialRuleInfo types * addressed feedback in utils * ci: add typecheck step to CI workflow * removed unneeded ts-expect-error from eslint.config.ts * Address feedback in utils * remove non-null assertion from require-meta-docs-recommended --- .github/workflows/main.yml | 10 + .github/workflows/release-please.yml | 1 + .gitignore | 5 +- eslint.config.ts | 3 +- lib/{index.js => index.ts} | 89 ++- ...sistent-output.js => consistent-output.ts} | 20 +- .../{fixer-return.js => fixer-return.ts} | 67 +- ...-ordering.js => meta-property-ordering.ts} | 32 +- ...ds.js => no-deprecated-context-methods.ts} | 33 +- ...ort-api.js => no-deprecated-report-api.ts} | 17 +- ...entical-tests.js => no-identical-tests.ts} | 62 +- ...-replaced-by.js => no-meta-replaced-by.ts} | 5 +- ...a-default.js => no-meta-schema-default.ts} | 33 +- ...ssage-ids.js => no-missing-message-ids.ts} | 26 +- ...eholders.js => no-missing-placeholders.ts} | 36 +- .../{no-only-tests.js => no-only-tests.ts} | 16 +- ...erty-in-node.js => no-property-in-node.ts} | 23 +- ...essage-ids.js => no-unused-message-ids.ts} | 48 +- ...ceholders.js => no-unused-placeholders.ts} | 35 +- ...ken-range.js => no-useless-token-range.ts} | 99 +-- ...r-message-ids.js => prefer-message-ids.ts} | 29 +- ...r-object-rule.js => prefer-object-rule.ts} | 12 +- lib/rules/prefer-output-null.js | 77 --- lib/rules/prefer-output-null.ts | 83 +++ ...placeholders.js => prefer-placeholders.ts} | 27 +- ...replace-text.js => prefer-replace-text.ts} | 33 +- ...age-format.js => report-message-format.ts} | 43 +- ...ons.js => require-meta-default-options.ts} | 37 +- ...on.js => require-meta-docs-description.ts} | 12 +- ...ed.js => require-meta-docs-recommended.ts} | 36 +- ...a-docs-url.js => require-meta-docs-url.ts} | 22 +- ...eta-fixable.js => require-meta-fixable.ts} | 25 +- ...ons.js => require-meta-has-suggestions.ts} | 26 +- ....js => require-meta-schema-description.ts} | 25 +- ...-meta-schema.js => require-meta-schema.ts} | 9 +- ...uire-meta-type.js => require-meta-type.ts} | 23 +- ...ring.js => test-case-property-ordering.ts} | 20 +- ...ings.js => test-case-shorthand-strings.ts} | 84 +-- lib/types.ts | 83 +++ lib/{utils.js => utils.ts} | 621 +++++++++++------- package.json | 17 +- tests/lib/fixtures/tsconfig.json | 1 + tests/lib/index.js | 18 - tests/lib/index.ts | 13 + tests/lib/{rule-setup.js => rule-setup.ts} | 27 +- ...sistent-output.js => consistent-output.ts} | 0 .../{fixer-return.js => fixer-return.ts} | 0 ...-ordering.js => meta-property-ordering.ts} | 0 ...ds.js => no-deprecated-context-methods.ts} | 0 ...ort-api.js => no-deprecated-report-api.ts} | 0 ...entical-tests.js => no-identical-tests.ts} | 0 ...-replaced-by.js => no-meta-replaced-by.ts} | 23 +- ...a-default.js => no-meta-schema-default.ts} | 0 ...ssage-ids.js => no-missing-message-ids.ts} | 0 ...eholders.js => no-missing-placeholders.ts} | 10 +- .../{no-only-tests.js => no-only-tests.ts} | 0 ...erty-in-node.js => no-property-in-node.ts} | 0 ...essage-ids.js => no-unused-message-ids.ts} | 0 ...ceholders.js => no-unused-placeholders.ts} | 9 +- ...ken-range.js => no-useless-token-range.ts} | 6 +- ...r-message-ids.js => prefer-message-ids.ts} | 0 ...r-object-rule.js => prefer-object-rule.ts} | 0 ...r-output-null.js => prefer-output-null.ts} | 0 ...placeholders.js => prefer-placeholders.ts} | 0 ...replace-text.js => prefer-replace-text.ts} | 0 ...age-format.js => report-message-format.ts} | 2 +- ...ons.js => require-meta-default-options.ts} | 0 ...on.js => require-meta-docs-description.ts} | 0 ...ed.js => require-meta-docs-recommended.ts} | 0 ...a-docs-url.js => require-meta-docs-url.ts} | 0 ...eta-fixable.js => require-meta-fixable.ts} | 0 ...ons.js => require-meta-has-suggestions.ts} | 0 ....js => require-meta-schema-description.ts} | 0 ...-meta-schema.js => require-meta-schema.ts} | 0 ...uire-meta-type.js => require-meta-type.ts} | 20 +- ...ring.js => test-case-property-ordering.ts} | 0 ...ings.js => test-case-shorthand-strings.ts} | 6 +- tests/lib/{utils.js => utils.ts} | 590 ++++++++++++----- tests/utils/{test-setup.js => test-setup.ts} | 0 tsconfig.json | 9 +- tsup.config.ts | 10 + types/estree.d.ts | 34 + vitest.config.ts | 4 +- 83 files changed, 1757 insertions(+), 1029 deletions(-) rename lib/{index.js => index.ts} (70%) rename lib/rules/{consistent-output.js => consistent-output.ts} (78%) rename lib/rules/{fixer-return.js => fixer-return.ts} (75%) rename lib/rules/{meta-property-ordering.js => meta-property-ordering.ts} (75%) rename lib/rules/{no-deprecated-context-methods.js => no-deprecated-context-methods.ts} (77%) rename lib/rules/{no-deprecated-report-api.js => no-deprecated-report-api.ts} (85%) rename lib/rules/{no-identical-tests.js => no-identical-tests.ts} (51%) rename lib/rules/{no-meta-replaced-by.js => no-meta-replaced-by.ts} (95%) rename lib/rules/{no-meta-schema-default.js => no-meta-schema-default.ts} (74%) rename lib/rules/{no-missing-message-ids.js => no-missing-message-ids.ts} (83%) rename lib/rules/{no-missing-placeholders.js => no-missing-placeholders.ts} (84%) rename lib/rules/{no-only-tests.js => no-only-tests.ts} (88%) rename lib/rules/{no-property-in-node.js => no-property-in-node.ts} (85%) rename lib/rules/{no-unused-message-ids.js => no-unused-message-ids.ts} (74%) rename lib/rules/{no-unused-placeholders.js => no-unused-placeholders.ts} (82%) rename lib/rules/{no-useless-token-range.js => no-useless-token-range.ts} (63%) rename lib/rules/{prefer-message-ids.js => prefer-message-ids.ts} (81%) rename lib/rules/{prefer-object-rule.js => prefer-object-rule.ts} (89%) delete mode 100644 lib/rules/prefer-output-null.js create mode 100644 lib/rules/prefer-output-null.ts rename lib/rules/{prefer-placeholders.js => prefer-placeholders.ts} (83%) rename lib/rules/{prefer-replace-text.js => prefer-replace-text.ts} (77%) rename lib/rules/{report-message-format.js => report-message-format.ts} (78%) rename lib/rules/{require-meta-default-options.js => require-meta-default-options.ts} (75%) rename lib/rules/{require-meta-docs-description.js => require-meta-docs-description.ts} (90%) rename lib/rules/{require-meta-docs-recommended.js => require-meta-docs-recommended.ts} (79%) rename lib/rules/{require-meta-docs-url.js => require-meta-docs-url.ts} (91%) rename lib/rules/{require-meta-fixable.js => require-meta-fixable.ts} (86%) rename lib/rules/{require-meta-has-suggestions.js => require-meta-has-suggestions.ts} (88%) rename lib/rules/{require-meta-schema-description.js => require-meta-schema-description.ts} (79%) rename lib/rules/{require-meta-schema.js => require-meta-schema.ts} (96%) rename lib/rules/{require-meta-type.js => require-meta-type.ts} (83%) rename lib/rules/{test-case-property-ordering.js => test-case-property-ordering.ts} (85%) rename lib/rules/{test-case-shorthand-strings.js => test-case-shorthand-strings.ts} (61%) create mode 100644 lib/types.ts rename lib/{utils.js => utils.ts} (58%) delete mode 100644 tests/lib/index.js create mode 100644 tests/lib/index.ts rename tests/lib/{rule-setup.js => rule-setup.ts} (70%) 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} (83%) 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} (97%) 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} (97%) rename tests/lib/rules/{no-useless-token-range.js => no-useless-token-range.ts} (95%) 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} (99%) 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} (99%) 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} (97%) rename tests/lib/{utils.js => utils.ts} (76%) rename tests/utils/{test-setup.js => test-setup.ts} (100%) create mode 100644 tsup.config.ts create mode 100644 types/estree.d.ts 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 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 }} diff --git a/.gitignore b/.gitignore index 281b1c34..302cc701 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ -.idea -coverage +.idea/ +coverage/ .vscode 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..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)); @@ -17,7 +16,7 @@ const compat = new FlatCompat({ export default defineConfig([ // Global ignores { - ignores: ['node_modules', 'coverage'], + ignores: ['node_modules', 'coverage', 'dist'], }, // Global settings { diff --git a/lib/index.js b/lib/index.ts similarity index 70% rename from lib/index.js rename to lib/index.ts index f512a196..1fbcb0af 100644 --- a/lib/index.js +++ b/lib/index.ts @@ -2,12 +2,10 @@ * @fileoverview An ESLint plugin for linting ESLint plugins * @author Teddy Katz */ +import { createRequire } from 'node:module'; -// ------------------------------------------------------------------------------ -// Requirements -// ------------------------------------------------------------------------------ +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'; @@ -41,20 +39,56 @@ 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', + '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), }; +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 // ------------------------------------------------------------------------------ @@ -93,34 +127,23 @@ 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) => { - return Object.assign(configs, { - [configName]: { - name: `${PLUGIN_NAME}/${configName}`, - plugins: { [PLUGIN_NAME]: plugin }, - rules: Object.fromEntries( - Object.keys(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/lib/rules/consistent-output.js b/lib/rules/consistent-output.ts similarity index 78% rename from lib/rules/consistent-output.js rename to lib/rules/consistent-output.ts index 3c12e0ee..6495fec3 100644 --- a/lib/rules/consistent-output.js +++ 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'; +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,18 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - const always = context.options[0] && context.options[0] === 'always'; + const always: boolean = + 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 ( diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.ts similarity index 75% rename from lib/rules/fixer-return.js rename to lib/rules/fixer-return.ts index 3fb92911..97aedf04 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.ts @@ -2,21 +2,37 @@ * @fileoverview require fixer functions to return a fix * @author 薛定谔的猫 */ - 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.js'; +import type { FunctionInfo } from '../types.js'; + +const DEFAULT_FUNC_INFO: FunctionInfo = { + upper: null, + codePath: null, + hasReturnWithFixer: false, + hasYieldWithFixer: false, + shouldCheck: false, + node: null, +}; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -25,7 +41,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,28 +49,24 @@ 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. * 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, - 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 +82,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,22 +115,22 @@ const rule = { }, // Stacks this function's information. - onCodePathStart(codePath, node) { + onCodePathStart(codePath: Rule.CodePath, node: Node) { funcInfo = { upper: funcInfo, codePath, hasYieldWithFixer: false, hasReturnWithFixer: false, 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; }, // Yield in generators @@ -147,7 +158,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/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.ts similarity index 75% rename from lib/rules/meta-property-ordering.js rename to lib/rules/meta-property-ordering.ts index e66d9320..88125d70 100644 --- a/lib/rules/meta-property-ordering.js +++ 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]; diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.ts similarity index 77% rename from lib/rules/no-deprecated-context-methods.js rename to lib/rules/no-deprecated-context-methods.ts index 490dd096..bd2a126f 100644 --- a/lib/rules/no-deprecated-context-methods.js +++ b/lib/rules/no-deprecated-context-methods.ts @@ -2,6 +2,8 @@ * @fileoverview Disallows usage of deprecated methods on rule context objects * @author Teddy Katz */ +import type { Rule } from 'eslint'; +import type { Identifier, MemberExpression } from 'estree'; import { getContextIdentifiers } from '../utils.js'; @@ -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/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.ts similarity index 85% rename from lib/rules/no-deprecated-report-api.js rename to lib/rules/no-deprecated-report-api.ts index cae105ac..91a3b622 100644 --- a/lib/rules/no-deprecated-report-api.js +++ 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 { Node } 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; // ---------------------------------------------------------------------- // Public @@ -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(', ')}}`, ); diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.ts similarity index 51% rename from lib/rules/no-identical-tests.js rename to lib/rules/no-identical-tests.ts index 5c192e01..d60b5a48 100644 --- a/lib/rules/no-identical-tests.js +++ b/lib/rules/no-identical-tests.ts @@ -2,15 +2,15 @@ * @fileoverview disallow identical tests * @author 薛定谔的猫 */ +import type { Rule } from 'eslint'; +import type { Expression, SpreadElement } from 'estree'; import { getTestInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -27,20 +27,12 @@ const rule = { }, create(context) { - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- const sourceCode = context.sourceCode; - // ---------------------------------------------------------------------- - // Helpers - // ---------------------------------------------------------------------- /** * Create a unique cache key - * @param {object} test - * @returns {string} */ - function toKey(test) { + function toKey(test: Expression | SpreadElement): string { if (test.type !== 'ObjectExpression') { return JSON.stringify([test.type, sourceCode.getText(test)]); } @@ -55,28 +47,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); + } + }); }); }); }, diff --git a/lib/rules/no-meta-replaced-by.js b/lib/rules/no-meta-replaced-by.ts similarity index 95% rename from lib/rules/no-meta-replaced-by.js rename to lib/rules/no-meta-replaced-by.ts index 8ceb6f41..56d04f6f 100644 --- a/lib/rules/no-meta-replaced-by.js +++ b/lib/rules/no-meta-replaced-by.ts @@ -1,15 +1,14 @@ /** * @fileoverview Disallows the usage of `meta.replacedBy` property */ +import type { Rule } from 'eslint'; import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js'; // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { diff --git a/lib/rules/no-meta-schema-default.js b/lib/rules/no-meta-schema-default.ts similarity index 74% rename from lib/rules/no-meta-schema-default.js rename to lib/rules/no-meta-schema-default.ts index 89aafc0b..46a29b0b 100644 --- a/lib/rules/no-meta-schema-default.js +++ b/lib/rules/no-meta-schema-default.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: { @@ -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 (staticKey.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/rules/no-missing-message-ids.js b/lib/rules/no-missing-message-ids.ts similarity index 83% rename from lib/rules/no-missing-message-ids.js rename to lib/rules/no-missing-message-ids.ts index 81dfdf0a..3ece7a2a 100644 --- a/lib/rules/no-missing-message-ids.js +++ b/lib/rules/no-missing-message-ids.ts @@ -1,9 +1,12 @@ +import type { Rule } from 'eslint'; +import type { Identifier, Node } from 'estree'; + import { collectReportViolationAndSuggestionData, findPossibleVariableValues, getContextIdentifiers, - getMessagesNode, getMessageIdNodeById, + getMessagesNode, getReportInfo, getRuleInfo, } from '../utils.js'; @@ -11,9 +14,7 @@ import { // ------------------------------------------------------------------------------ // 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' && @@ -69,13 +70,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, + ); // Look for any possible string values we found for this messageId. values.forEach((val) => { diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.ts similarity index 84% rename from lib/rules/no-missing-placeholders.js rename to lib/rules/no-missing-placeholders.ts index 522ed207..fa39d453 100644 --- a/lib/rules/no-missing-placeholders.js +++ 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 type { Node } from 'estree'; import { collectReportViolationAndSuggestionData, + getContextIdentifiers, getKeyName, + getMessageIdNodeById, + getMessagesNode, getReportInfo, getRuleInfo, - getMessagesNode, - getMessageIdNodeById, - getContextIdentifiers, } from '../utils.js'; // ------------------------------------------------------------------------------ // 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,10 @@ const rule = { messageId, data, } of reportMessagesAndDataArray.filter((obj) => obj.message)) { + if (!message) continue; const messageStaticValue = getStaticValue(message, scope); if ( - ((message.type === 'Literal' && + ((message?.type === 'Literal' && typeof message.value === 'string') || (messageStaticValue && typeof messageStaticValue.value === 'string')) && @@ -107,20 +108,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/rules/no-only-tests.js b/lib/rules/no-only-tests.ts similarity index 88% rename from lib/rules/no-only-tests.js rename to lib/rules/no-only-tests.ts index 87246d94..6ed93ce5 100644 --- a/lib/rules/no-only-tests.js +++ 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'; -/** @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( @@ -56,15 +56,15 @@ const rule = { sourceCode.getTokenBefore(onlyProperty); const tokenAfter = sourceCode.getTokenAfter(onlyProperty); - if ( - (isCommaToken(tokenBefore) && + 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) ) { @@ -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' && diff --git a/lib/rules/no-property-in-node.js b/lib/rules/no-property-in-node.ts similarity index 85% rename from lib/rules/no-property-in-node.js rename to lib/rules/no-property-in-node.ts index 86f2fa41..7ef564a2 100644 --- a/lib/rules/no-property-in-node.js +++ 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,12 @@ const defaultTypedNodeSourceFileTesters = [ * } * ``` * - * @param {import('typescript').Type} type - * @param {RegExp[]} 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 +46,7 @@ function isAstNodeType(type, typedNodeSourceFileTesters) { }); } -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -51,6 +54,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 +79,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 { diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.ts similarity index 74% rename from lib/rules/no-unused-message-ids.js rename to lib/rules/no-unused-message-ids.ts index 8b434d1c..e260e5ce 100644 --- a/lib/rules/no-unused-message-ids.js +++ b/lib/rules/no-unused-message-ids.ts @@ -1,3 +1,6 @@ +import type { Rule } from 'eslint'; +import type { Identifier, Node } from 'estree'; + import { collectReportViolationAndSuggestionData, findPossibleVariableValues, @@ -13,8 +16,7 @@ import { // 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; @@ -54,7 +56,7 @@ const rule = { contextIdentifiers = getContextIdentifiers(scopeManager, ast); }, - 'Program:exit'(ast) { + 'Program:exit'() { if (hasSeenUnknownMessageId || !hasSeenViolationReport) { /* Bail out when the rule is likely to have false positives. @@ -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,15 @@ 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); + : messageId.type === 'Identifier' + ? findPossibleVariableValues(messageId, scopeManager) + : []; if ( values.length === 0 || values.some((val) => val.type !== 'Literal') @@ -113,7 +117,11 @@ 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 + .filter((value) => value.type === 'Literal') + .map((value) => value.value) + .filter((value) => typeof value === 'string') + .forEach((value) => messageIdsUsed.add(value)); } } }, @@ -127,18 +135,26 @@ 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) + (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) => messageIdsUsed.add(val.value)); + values + .filter((val) => val.type === 'Literal') + .map((val) => val.value) + .filter((val) => typeof val === 'string') + .forEach((val) => messageIdsUsed.add(val)); } }, }; diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.ts similarity index 82% rename from lib/rules/no-unused-placeholders.js rename to lib/rules/no-unused-placeholders.ts index 914eba56..a9dc14cd 100644 --- a/lib/rules/no-unused-placeholders.js +++ b/lib/rules/no-unused-placeholders.ts @@ -2,8 +2,9 @@ * @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'; import { collectReportViolationAndSuggestionData, @@ -18,9 +19,7 @@ import { // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'problem', docs: { @@ -29,7 +28,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 +40,7 @@ const rule = { const sourceCode = context.sourceCode; const { scopeManager } = sourceCode; - let contextIdentifiers; + let contextIdentifiers = new Set(); const ruleInfo = getRuleInfo(sourceCode); if (!ruleInfo) { @@ -94,30 +93,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', diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.ts similarity index 63% rename from lib/rules/no-useless-token-range.js rename to lib/rules/no-useless-token-range.ts index 0ae826cb..653ef53d 100644 --- a/lib/rules/no-useless-token-range.js +++ 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,30 +58,29 @@ const rule = { arg.properties.length >= 2 || (arg.properties.length === 1 && (getKeyName(arg.properties[0]) !== 'includeComments' || - arg.properties[0].value.type !== 'Literal')) + (arg.properties[0].type === 'Property' && + arg.properties[0].value.type !== 'Literal'))) ); } /** * 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' @@ -87,6 +93,7 @@ const rule = { (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 0 && + memberExpression.object.type === 'MemberExpression' && isRangeAccess(memberExpression.object)) ); } @@ -94,10 +101,10 @@ 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' @@ -110,6 +117,7 @@ const rule = { (memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 1 && + memberExpression.object.type === 'MemberExpression' && isRangeAccess(memberExpression.object)) ); } @@ -139,32 +147,35 @@ 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 replacementText = - sourceCode.text.slice( - fullRangeAccess.range[0], - identifier.parent.parent.range[0], - ) + - sourceCode.getText(identifier.parent.parent.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.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]), + ); + }, + }); + } }); }, }; diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.ts similarity index 81% rename from lib/rules/prefer-message-ids.js rename to lib/rules/prefer-message-ids.ts index fc5cc51f..1d1ccedb 100644 --- a/lib/rules/prefer-message-ids.js +++ 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/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.ts similarity index 89% rename from lib/rules/prefer-object-rule.js rename to lib/rules/prefer-object-rule.ts index ec0a0470..d07d7cf2 100644 --- a/lib/rules/prefer-object-rule.js +++ 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; } diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js deleted file mode 100644 index 7fa78a66..00000000 --- a/lib/rules/prefer-output-null.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @fileoverview disallows invalid RuleTester test cases where the `output` matches the `code` - * @author 薛定谔的猫 - */ - -import { getTestInfo } from '../utils.js'; - -// ------------------------------------------------------------------------------ -// Rule Definition -// ------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { - meta: { - type: 'suggestion', - docs: { - description: - 'disallow invalid RuleTester test cases where the `output` matches the `code`', - category: 'Tests', - recommended: true, - url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-output-null.md', - }, - fixable: 'code', - schema: [], - messages: { - useOutputNull: - 'Use `output: null` to assert that a test case is not autofixed.', - }, - }, - - 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, - ); - } - return null; - } - - 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'), - }); - } - }); - }); - }, - }; - }, -}; - -export default rule; diff --git a/lib/rules/prefer-output-null.ts b/lib/rules/prefer-output-null.ts new file mode 100644 index 00000000..1b2cc8f6 --- /dev/null +++ b/lib/rules/prefer-output-null.ts @@ -0,0 +1,83 @@ +/** + * @fileoverview disallows invalid RuleTester test cases where the `output` matches the `code` + * @author 薛定谔的猫 + */ + +import type { Rule } from 'eslint'; +import type { Property } from 'estree'; + +import { getTestInfo } from '../utils.js'; + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ +const rule: Rule.RuleModule = { + meta: { + type: 'suggestion', + docs: { + description: + 'disallow invalid RuleTester test cases where the `output` matches the `code`', + category: 'Tests', + recommended: true, + url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-output-null.md', + }, + fixable: 'code', + schema: [], + messages: { + useOutputNull: + 'Use `output: null` to assert that a test case is not autofixed.', + }, + }, + + create(context) { + const sourceCode = context.sourceCode; + + return { + Program(ast) { + getTestInfo(context, ast).forEach((testRun) => { + 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; + } + + const code = getTestInfoProperty('code'); + const output = getTestInfoProperty('output'); + + if ( + output && + code && + sourceCode.getText(output.value) === + sourceCode.getText(code.value) + ) { + context.report({ + node: output, + messageId: 'useOutputNull', + fix: (fixer) => fixer.replaceText(output.value, 'null'), + }); + } + }); + }); + }, + }; + }, +}; + +export default rule; diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.ts similarity index 83% rename from lib/rules/prefer-placeholders.js rename to lib/rules/prefer-placeholders.ts index 18868883..d8a9d217 100644 --- a/lib/rules/prefer-placeholders.js +++ b/lib/rules/prefer-placeholders.ts @@ -4,6 +4,8 @@ */ import { findVariable } from '@eslint-community/eslint-utils'; +import type { Rule } from 'eslint'; +import type { 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, ); diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.ts similarity index 77% rename from lib/rules/prefer-replace-text.js rename to lib/rules/prefer-replace-text.ts index 3d5db946..1a96d65e 100644 --- a/lib/rules/prefer-replace-text.js +++ 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/rules/report-message-format.js b/lib/rules/report-message-format.ts similarity index 78% rename from lib/rules/report-message-format.js rename to lib/rules/report-message-format.ts index 7d6790b5..4ba7506d 100644 --- a/lib/rules/report-message-format.js +++ 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,10 @@ 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 && + typeof staticValue.value === 'string' && + !pattern.test(staticValue.value)) ) { context.report({ node: message, @@ -73,10 +76,6 @@ const rule = { return {}; } - // ---------------------------------------------------------------------- - // Public - // ---------------------------------------------------------------------- - return { Program(ast) { const scope = sourceCode.getScope(ast); @@ -89,10 +88,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 +123,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)); diff --git a/lib/rules/require-meta-default-options.js b/lib/rules/require-meta-default-options.ts similarity index 75% rename from lib/rules/require-meta-default-options.js rename to lib/rules/require-meta-default-options.ts index 24c7e2cc..c8640f55 100644 --- a/lib/rules/require-meta-default-options.js +++ 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' && @@ -67,13 +67,17 @@ const rule = { } 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 {}; } @@ -87,8 +91,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({ diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.ts similarity index 90% rename from lib/rules/require-meta-docs-description.js rename to lib/rules/require-meta-docs-description.ts index 4f1dc6ea..dd0af4eb 100644 --- a/lib/rules/require-meta-docs-description.js +++ 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) }, }); } }, diff --git a/lib/rules/require-meta-docs-recommended.js b/lib/rules/require-meta-docs-recommended.ts similarity index 79% rename from lib/rules/require-meta-docs-recommended.js rename to lib/rules/require-meta-docs-recommended.ts index 3038ea3c..ae3f7d8e 100644 --- a/lib/rules/require-meta-docs-recommended.js +++ 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,26 +8,31 @@ 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, `{ 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}`, ); } -/** @type {import('eslint').Rule.RuleModule} */ -const rule = { +const rule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { @@ -35,7 +42,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 +81,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), }, ] : []; diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.ts similarity index 91% rename from lib/rules/require-meta-docs-url.js rename to lib/rules/require-meta-docs-url.ts index 41e3a350..ae09e8f1 100644 --- a/lib/rules/require-meta-docs-url.js +++ 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,11 @@ const rule = { return; } - if (isExpectedUrl(staticValue && staticValue.value)) { + if ( + staticValue && + typeof staticValue.value === 'string' && + isExpectedUrl(staticValue.value) + ) { return; } diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.ts similarity index 86% rename from lib/rules/require-meta-fixable.js rename to lib/rules/require-meta-fixable.ts index 67607aca..d3d23bf7 100644 --- a/lib/rules/require-meta-fixable.js +++ 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) + staticValue.value && + (typeof staticValue.value !== 'string' || + !['code', 'whitespace'].includes(staticValue.value)) ) { // `fixable` property has an invalid value. context.report({ @@ -111,7 +112,8 @@ const rule = { if ( usesFixFunctions && - !['code', 'whitespace'].includes(staticValue.value) + (typeof staticValue.value !== 'string' || + !['code', 'whitespace'].includes(staticValue.value)) ) { // Rule is fixable but `fixable` property does not have a fixable value. context.report({ @@ -121,6 +123,7 @@ const rule = { } else if ( catchNoFixerButFixableProperty && !usesFixFunctions && + typeof staticValue.value === 'string' && ['code', 'whitespace'].includes(staticValue.value) ) { // Rule is NOT fixable but `fixable` property has a fixable value. diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.ts similarity index 88% rename from lib/rules/require-meta-has-suggestions.js rename to lib/rules/require-meta-has-suggestions.ts index 18b483ec..c031bfcd 100644 --- a/lib/rules/require-meta-has-suggestions.js +++ 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; }, }); } diff --git a/lib/rules/require-meta-schema-description.js b/lib/rules/require-meta-schema-description.ts similarity index 79% rename from lib/rules/require-meta-schema-description.js rename to lib/rules/require-meta-schema-description.ts index b15669ec..dba60771 100644 --- a/lib/rules/require-meta-schema-description.js +++ 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); } } diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.ts similarity index 96% rename from lib/rules/require-meta-schema.js rename to lib/rules/require-meta-schema.ts index da65ad9a..909fdd58 100644 --- a/lib/rules/require-meta-schema.js +++ 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 diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.ts similarity index 83% rename from lib/rules/require-meta-type.js rename to lib/rules/require-meta-type.ts index 31ad1aba..7001936b 100644 --- a/lib/rules/require-meta-type.js +++ 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,10 @@ const rule = { return; } - if (!VALID_TYPES.has(staticValue.value)) { + if ( + typeof staticValue.value !== 'string' || + !VALID_TYPES.has(staticValue.value) + ) { context.report({ node: typeNode.value, messageId: 'unexpected' }); } }, diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.ts similarity index 85% rename from lib/rules/test-case-property-ordering.js rename to lib/rules/test-case-property-ordering.ts index 73728e9a..50eee786 100644 --- a/lib/rules/test-case-property-ordering.js +++ 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; diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.ts similarity index 61% rename from lib/rules/test-case-shorthand-strings.js rename to lib/rules/test-case-shorthand-strings.ts index 2d67dd8e..64352316 100644 --- a/lib/rules/test-case-shorthand-strings.js +++ 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), + ); + }, }); + }); } // ---------------------------------------------------------------------- diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..72c9899a --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,83 @@ +import type { Rule } from 'eslint'; +import type { + ArrayPattern, + ArrowFunctionExpression, + AssignmentPattern, + Expression, + FunctionDeclaration, + FunctionExpression, + MaybeNamedClassDeclaration, + MaybeNamedFunctionDeclaration, + Node, + ObjectPattern, + Pattern, + Property, + RestElement, + 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 + | MaybeNamedFunctionDeclaration + | MaybeNamedClassDeclaration + | null; + isNewStyle?: boolean; + meta?: Expression | Pattern | FunctionDeclaration; +} + +export interface RuleInfo extends PartialRuleInfo { + create: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration; + isNewStyle: boolean; +} + +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.js b/lib/utils.ts similarity index 58% rename from lib/utils.js rename to lib/utils.ts index 23e1e4b8..75355270 100644 --- a/lib/utils.js +++ b/lib/utils.ts @@ -1,27 +1,71 @@ import { getStaticValue, findVariable } from '@eslint-community/eslint-utils'; +import type { Rule, Scope, SourceCode } from 'eslint'; import estraverse from 'estraverse'; +import type { + ArrowFunctionExpression, + AssignmentProperty, + CallExpression, + Directive, + Expression, + FunctionDeclaration, + FunctionExpression, + Identifier, + MaybeNamedClassDeclaration, + MaybeNamedFunctionDeclaration, + MemberExpression, + ModuleDeclaration, + Node, + ObjectExpression, + Pattern, + Program, + Property, + SpreadElement, + Statement, + Super, + TSExportAssignment, + VariableDeclarator, +} from 'estree'; + +import type { + MetaDocsProperty, + PartialRuleInfo, + RuleInfo, + TestInfo, + ViolationAndSuppressionData, +} from './types.js'; const functionTypes = new Set([ 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration', ]); +const isFunctionType = ( + node: + | MaybeNamedClassDeclaration + | MaybeNamedFunctionDeclaration + | 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 +75,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 +133,13 @@ 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 | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, +): 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 +148,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 | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, +): node is CallExpression & { arguments: ObjectExpression[] } { return ( node.type === 'CallExpression' && node.arguments.length === 1 && @@ -116,8 +174,18 @@ 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 + | MaybeNamedClassDeclaration + | Expression + | MaybeNamedFunctionDeclaration + )[] = []; for (const statement of ast.body) { switch (statement.type) { @@ -140,16 +208,16 @@ function getRuleExportsESM(ast, scopeManager) { 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 = { ... };` // 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 +225,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( @@ -166,7 +234,7 @@ function getRuleExportsESM(ast, scopeManager) { ); } 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( @@ -185,7 +253,7 @@ function getRuleExportsESM(ast, scopeManager) { ); } 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( @@ -196,13 +264,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,20 +281,21 @@ 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; + if (leftExpression.type !== 'MemberExpression') return currentExports; 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)) { // 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: {} }` @@ -243,73 +315,82 @@ function getRuleExportsCJS(ast, scopeManager) { ); } 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, + }; } } } 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,15 +400,14 @@ 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 + * @returns the list of elements */ -function collectArrayElements(node) { +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 [ @@ -340,58 +420,66 @@ 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 }); + const firstCreateParam = ruleInfo?.create.params[0]; if ( !ruleInfo || - ruleInfo.create.params.length === 0 || - ruleInfo.create.params[0].type !== 'Identifier' + ruleInfo.create?.params.length === 0 || + firstCreateParam?.type !== 'Identifier' ) { return new Set(); } @@ -399,19 +487,22 @@ 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 === firstCreateParam.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 +511,9 @@ 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 && 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; @@ -434,7 +527,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 +535,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 +557,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 +590,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 +656,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 +680,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 +697,15 @@ 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, +): + | Record + | Record + | null { const reportArgs = node.arguments; // If there is exactly one argument, the API expects an object. @@ -619,21 +719,24 @@ export function getReportInfo(node, context) { 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; - 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 + let keys: string[]; + const sourceCode = context.sourceCode; + const scope = sourceCode.getScope(node); const secondArgStaticValue = getStaticValue(reportArgs[1], scope); if ( @@ -664,11 +767,14 @@ export function getReportInfo(node, context) { /** * Gets a set of all `sourceCode` identifiers. - * @param {ScopeManager} 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 scopeManager + * @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, ast) { +export function getSourceCodeIdentifiers( + scopeManager: Scope.ScopeManager, + ast: Program, +): Set { return new Set( [...getContextIdentifiers(scopeManager, ast)] .filter( @@ -694,27 +800,37 @@ export function getSourceCodeIdentifiers(scopeManager, ast) { /** * 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}`); } + 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}`, ); } /** * 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: NonNullable>, +): ViolationAndSuppressionData[] { return [ // Violation message { @@ -746,29 +862,38 @@ 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, + context: Rule.RuleContext, +): 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, context)?.fix === node ); } /** * Whether the provided node represents a suggestion fixer function. - * @param {Node} node - * @param {Node[]} contextIdentifiers - * @returns {boolean} + * @param node + * @param contextIdentifiers + * @param context */ -export function isSuggestionFixerFunction(node, contextIdentifiers) { +export function isSuggestionFixerFunction( + node: Node, + contextIdentifiers: Set, + context: Rule.RuleContext, +): boolean { const parent = node.parent; return ( (node.type === 'FunctionExpression' || @@ -784,30 +909,38 @@ export function isSuggestionFixerFunction(node, contextIdentifiers) { 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 ); } /** * 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 | undefined, + scopeManager: Scope.ScopeManager, +): (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; } @@ -817,42 +950,53 @@ export function evaluateObjectProperties(objectNode, scopeManager) { }); } -export function getMetaDocsProperty(propertyName, ruleInfo, scopeManager) { - const metaNode = ruleInfo.meta; +export function getMetaDocsProperty( + propertyName: string, + ruleInfo: RuleInfo, + scopeManager: Scope.ScopeManager, +): 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((node) => node.type === 'Property') + .find((p) => getKeyName(p) === 'docs'); const metaPropertyNode = evaluateObjectProperties( docsNode?.value, scopeManager, - ).find((p) => p.type === 'Property' && getKeyName(p) === propertyName); + ) + .filter((node) => node.type === 'Property') + .find((p) => getKeyName(p) === propertyName); return { docsNode, metaNode, metaPropertyNode }; } /** * Get the `meta.messages` node from a rule. - * @param {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @returns {Node|undefined} + * @param ruleInfo + * @param scopeManager */ -export function getMessagesNode(ruleInfo, scopeManager) { +export function getMessagesNode( + ruleInfo: RuleInfo | null, + scopeManager: Scope.ScopeManager, +): 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((node) => node.type === 'Property') + .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; } @@ -861,11 +1005,13 @@ 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 ruleInfo + * @param scopeManager */ -export function getMessageIdNodes(ruleInfo, scopeManager) { +export function getMessageIdNodes( + ruleInfo: RuleInfo, + scopeManager: Scope.ScopeManager, +): (Property | SpreadElement)[] | undefined { const messagesNode = getMessagesNode(ruleInfo, scopeManager); return messagesNode && messagesNode.type === 'ObjectExpression' @@ -875,25 +1021,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 {RuleInfo} ruleInfo - * @param {ScopeManager} scopeManager - * @param {Scope} scope - * @returns {Node|undefined} The matching messageId property from `meta.messages`. + * @param messageId - the messageId to check for + * @param ruleInfo + * @param scopeManager + * @param scope + * @returns The matching messageId property from `meta.messages`. */ -export function getMessageIdNodeById(messageId, ruleInfo, scopeManager, scope) { - return getMessageIdNodes(ruleInfo, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p, scope) === messageId, - ); +export function getMessageIdNodeById( + messageId: string, + ruleInfo: RuleInfo, + scopeManager: Scope.ScopeManager, + scope: Scope.Scope, +): Property | undefined { + return getMessageIdNodes(ruleInfo, scopeManager) + ?.filter((node) => node.type === 'Property') + .find((p) => getKeyName(p, scope) === messageId); } -export function getMetaSchemaNode(metaNode, scopeManager) { - return evaluateObjectProperties(metaNode, scopeManager).find( - (p) => p.type === 'Property' && getKeyName(p) === 'schema', - ); +export function getMetaSchemaNode( + metaNode: Node | undefined, + scopeManager: Scope.ScopeManager, +): Property | undefined { + return evaluateObjectProperties(metaNode, scopeManager) + .filter((node) => node.type === 'Property') + .find((p) => getKeyName(p) === 'schema'); } -export function getMetaSchemaNodeProperty(schemaNode, scopeManager) { +export function getMetaSchemaNodeProperty( + schemaNode: AssignmentProperty | Property | undefined, + scopeManager: Scope.ScopeManager, +): Node | null { if (!schemaNode) { return null; } @@ -901,7 +1058,7 @@ export function getMetaSchemaNodeProperty(schemaNode, scopeManager) { let { value } = schemaNode; if (value.type === 'Identifier' && value.name !== 'undefined') { const variable = findVariable( - scopeManager.acquire(value) || scopeManager.globalScope, + scopeManager.acquire(value) || scopeManager.globalScope!, value, ); @@ -914,10 +1071,10 @@ export function getMetaSchemaNodeProperty(schemaNode, scopeManager) { 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; @@ -925,13 +1082,16 @@ 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: 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) => { @@ -949,22 +1109,25 @@ 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: Identifier, + scopeManager: Scope.ScopeManager, +): boolean { const variable = findVariable( - scopeManager.acquire(node) || scopeManager.globalScope, + scopeManager.acquire(node) || scopeManager.globalScope!, node, ); diff --git a/package.json b/package.json index aaa146d1..cf9f6e1d 100644 --- a/package.json +++ b/package.json @@ -3,28 +3,30 @@ "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": "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 .", "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", @@ -51,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", @@ -75,6 +81,7 @@ "npm-run-all2": "^7.0.1", "prettier": "^3.4.1", "release-it": "^17.2.0", + "tsup": "^8.5.0", "typescript": "^5.8.3", "vitest": "^3.2.4" }, 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/index.js b/tests/lib/index.js deleted file mode 100644 index 96f52e44..00000000 --- a/tests/lib/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { assert, describe, 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$/, - ); - }); - }); - }); -}); diff --git a/tests/lib/index.ts b/tests/lib/index.ts new file mode 100644 index 00000000..0eeda49d --- /dev/null +++ b/tests/lib/index.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import plugin from '../../lib/index.js'; + +describe('exported plugin', () => { + describe('has a meta.docs.url property on each rule', () => { + 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$/, + ), + ); + }); +}); diff --git a/tests/lib/rule-setup.js b/tests/lib/rule-setup.ts similarity index 70% rename from tests/lib/rule-setup.js rename to tests/lib/rule-setup.ts index ab097bc3..dfa0fbba 100644 --- a/tests/lib/rule-setup.js +++ b/tests/lib/rule-setup.ts @@ -1,24 +1,24 @@ import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { assert, describe, it } from 'vitest'; import plugin from '../../lib/index.js'; -const RULE_NAMES = Object.keys(plugin.rules); -const dirname = path.dirname(fileURLToPath(import.meta.url)); +const RULE_NAMES = Object.keys(plugin.rules) as Array< + keyof typeof plugin.rules +>; 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', '')), ); }); @@ -29,25 +29,26 @@ 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', ); }); 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', ); }); }); @@ -55,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( 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 83% rename from tests/lib/rules/no-meta-replaced-by.js rename to tests/lib/rules/no-meta-replaced-by.ts index 9ec810fa..f27fa93d 100644 --- a/tests/lib/rules/no-meta-replaced-by.js +++ 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)), }); 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 97% rename from tests/lib/rules/no-missing-placeholders.js rename to tests/lib/rules/no-missing-placeholders.ts index 2791fee2..f725590e 100644 --- a/tests/lib/rules/no-missing-placeholders.js +++ 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).`, 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 97% rename from tests/lib/rules/no-unused-placeholders.js rename to tests/lib/rules/no-unused-placeholders.ts index b9d55158..9f16166b 100644 --- a/tests/lib/rules/no-unused-placeholders.js +++ 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).`, diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.ts similarity index 95% rename from tests/lib/rules/no-useless-token-range.js rename to tests/lib/rules/no-useless-token-range.ts index 7b6f44e7..72499c80 100644 --- a/tests/lib/rules/no-useless-token-range.js +++ 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) { 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 99% rename from tests/lib/rules/report-message-format.js rename to tests/lib/rules/report-message-format.ts index be260092..dd7f9e61 100644 --- a/tests/lib/rules/report-message-format.js +++ 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. 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 99% rename from tests/lib/rules/require-meta-type.js rename to tests/lib/rules/require-meta-type.ts index 7d44b041..a2326db2 100644 --- a/tests/lib/rules/require-meta-type.js +++ 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' }; @@ -85,6 +75,16 @@ ruleTester.run('require-meta-type', rule, { }; `, 'module.exports = {};', // No rule. + // No `create` function. + { + code: ` + const create = {}; + module.exports = { + meta: {}, + create, + }; + `, + }, ], invalid: [ 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 97% rename from tests/lib/rules/test-case-shorthand-strings.js rename to tests/lib/rules/test-case-shorthand-strings.ts index c65d0c18..0b989397 100644 --- a/tests/lib/rules/test-case-shorthand-strings.js +++ 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 - * @returns {string} Code representing the test cases + * @param cases The code representation of valid test cases + * @returns Code representing the test cases */ -function getTestCases(cases) { +function getTestCases(cases: string[]): string { return ` new RuleTester().run('foo', bar, { valid: [ diff --git a/tests/lib/utils.js b/tests/lib/utils.ts similarity index 76% rename from tests/lib/utils.js rename to tests/lib/utils.ts index 22e2a539..292e392d 100644 --- a/tests/lib/utils.js +++ 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; + } | undefined; + 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,15 +386,13 @@ 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, isNewStyle: true, }, 'module.exports = { create: () => { } };': { create: { type: 'ArrowFunctionExpression' }, - meta: null, isNewStyle: true, }, 'module.exports = { create() {}, meta: { } };': { @@ -380,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 = {};': { @@ -405,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 };': @@ -458,7 +489,7 @@ describe('utils', () => { }, 'const rule = function(context) {return{};}; module.exports = rule;': { create: { type: 'FunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, }; @@ -469,11 +500,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,11 +514,10 @@ describe('utils', () => { }); describe('the file has a valid rule (ESM)', () => { - const CASES = { + const CASES: Record = { // ESM (object style) 'export default { create() {} }': { create: { type: 'FunctionExpression' }, - meta: null, isNewStyle: true, }, 'export default { create() {}, meta: {} }': { @@ -532,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, }, }; @@ -558,11 +587,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 +610,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 +618,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 +628,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,14 +636,18 @@ 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 {}; };', options: { sourceType: 'script' }, expected: { create: { type: 'FunctionExpression' }, - meta: null, + meta: undefined, isNewStyle: false, }, }, @@ -624,7 +657,7 @@ describe('utils', () => { options: { sourceType: 'module' }, expected: { create: { type: 'FunctionDeclaration' }, - meta: null, + meta: undefined, isNewStyle: false, }, }, @@ -635,11 +668,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 +684,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[1] as ExpressionStatement) + .expression as Identifier, + (blockStatement.body[2] 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 +774,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 +800,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 +810,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 +839,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 +852,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 +890,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 +902,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 +913,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 +962,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 +977,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 +1002,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 +1165,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 +1180,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 +1211,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 +1275,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 +1306,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 +1319,9 @@ describe('utils', () => { estraverse.traverse(ast, { enter(node, parent) { - node.parent = parent; + if (parent) { + node.parent = parent; + } }, }); @@ -1218,7 +1334,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 +1476,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 +1501,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 +1559,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 +1620,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 +1653,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 +1686,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 +1722,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 +1736,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 +1744,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 +1773,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 +1784,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 +1822,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 +1853,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 +1867,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 +1893,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 +1909,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 +1936,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 +1968,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 +2013,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 +2034,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, ), ); 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/tsconfig.json b/tsconfig.json index 977142eb..1411ce1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,11 @@ "target": "ES2024", "verbatimModuleSyntax": true, "erasableSyntaxOnly": true, - "forceConsistentCasingInFileNames": true - } + "forceConsistentCasingInFileNames": true, + "paths": { + "eslint-plugin-eslint-plugin": ["./lib/index.ts"] + }, + "types": ["eslint-scope", "espree", "estree", "lodash", "node"] + }, + "include": ["lib/**/*", "tests/**/*", "types/**/*"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 00000000..3542a8da --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + bundle: false, + clean: true, + dts: true, + entry: ['lib/**/*.ts'], + format: ['esm'], + outDir: 'dist', +}); diff --git a/types/estree.d.ts b/types/estree.d.ts new file mode 100644 index 00000000..93339cfa --- /dev/null +++ b/types/estree.d.ts @@ -0,0 +1,34 @@ +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; + } + + interface TSAsExpression extends BaseExpression { + type: 'TSAsExpression'; + expression: Expression | Identifier; + } + + interface TSExportAssignment extends BaseNode { + type: 'TSExportAssignment'; + expression: Expression; + } + + interface ExpressionMap { + TSAsExpression: TSAsExpression; + } + + interface NodeMap { + TSAsExpression: TSAsExpression; + TSExportAssignment: TSExportAssignment; + } +} 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 b353bd1d716c79fc8be458f6de8846c2d5c910f2 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Mon, 4 Aug 2025 09:31:21 -0400 Subject: [PATCH 12/15] feat!: enable `no-meta-replaced-by`, `no-meta-schema-default`, `require-meta-default-options`, `require-meta-schema-description` as `recommended` rules (#530) feat!: enable no-meta-replaced-by, no-meta-schema-default, require-meta-default-options, require-meta-schema-description as recommended rules --- README.md | 8 ++++---- docs/rules/no-meta-replaced-by.md | 2 ++ docs/rules/no-meta-schema-default.md | 2 ++ docs/rules/require-meta-default-options.md | 2 ++ docs/rules/require-meta-schema-description.md | 2 ++ lib/rules/no-meta-replaced-by.ts | 2 +- lib/rules/no-meta-schema-default.ts | 2 +- lib/rules/require-meta-default-options.ts | 2 +- lib/rules/require-meta-schema-description.ts | 2 +- 9 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3b6e5000..5f15127a 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ export default [ | [meta-property-ordering](docs/rules/meta-property-ordering.md) | enforce the order of meta properties | | 🔧 | | | | [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects | ✅ | 🔧 | | | | [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md) | disallow the version of `context.report()` with multiple arguments | ✅ | 🔧 | | | -| [no-meta-replaced-by](docs/rules/no-meta-replaced-by.md) | disallow using the `meta.replacedBy` rule property | | | | | -| [no-meta-schema-default](docs/rules/no-meta-schema-default.md) | disallow rules `meta.schema` properties to include defaults | | | | | +| [no-meta-replaced-by](docs/rules/no-meta-replaced-by.md) | disallow using the `meta.replacedBy` rule property | ✅ | | | | +| [no-meta-schema-default](docs/rules/no-meta-schema-default.md) | disallow rules `meta.schema` properties to include defaults | ✅ | | | | | [no-missing-message-ids](docs/rules/no-missing-message-ids.md) | disallow `messageId`s that are missing from `meta.messages` | ✅ | | | | | [no-missing-placeholders](docs/rules/no-missing-placeholders.md) | disallow missing placeholders in rule report messages | ✅ | | | | | [no-property-in-node](docs/rules/no-property-in-node.md) | disallow using `in` to narrow node types instead of looking at properties | | | | 💭 | @@ -87,14 +87,14 @@ export default [ | [prefer-placeholders](docs/rules/prefer-placeholders.md) | require using placeholders for dynamic report messages | | | | | | [prefer-replace-text](docs/rules/prefer-replace-text.md) | require using `replaceText()` instead of `replaceTextRange()` | | | | | | [report-message-format](docs/rules/report-message-format.md) | enforce a consistent format for rule report messages | | | | | -| [require-meta-default-options](docs/rules/require-meta-default-options.md) | require only rules with options to implement a `meta.defaultOptions` property | | 🔧 | | | +| [require-meta-default-options](docs/rules/require-meta-default-options.md) | require only rules with options to implement a `meta.defaultOptions` property | ✅ | 🔧 | | | | [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format | | | | | | [require-meta-docs-recommended](docs/rules/require-meta-docs-recommended.md) | require rules to implement a `meta.docs.recommended` property | | | 💡 | | | [require-meta-docs-url](docs/rules/require-meta-docs-url.md) | require rules to implement a `meta.docs.url` property | | 🔧 | | | | [require-meta-fixable](docs/rules/require-meta-fixable.md) | require rules to implement a `meta.fixable` property | ✅ | | | | | [require-meta-has-suggestions](docs/rules/require-meta-has-suggestions.md) | require suggestable rules to implement a `meta.hasSuggestions` property | ✅ | 🔧 | | | | [require-meta-schema](docs/rules/require-meta-schema.md) | require rules to implement a `meta.schema` property | ✅ | | 💡 | | -| [require-meta-schema-description](docs/rules/require-meta-schema-description.md) | require rules `meta.schema` properties to include descriptions | | | | | +| [require-meta-schema-description](docs/rules/require-meta-schema-description.md) | require rules `meta.schema` properties to include descriptions | ✅ | | | | | [require-meta-type](docs/rules/require-meta-type.md) | require rules to implement a `meta.type` property | ✅ | | | | ### Tests diff --git a/docs/rules/no-meta-replaced-by.md b/docs/rules/no-meta-replaced-by.md index 070fa5ef..3caf840d 100644 --- a/docs/rules/no-meta-replaced-by.md +++ b/docs/rules/no-meta-replaced-by.md @@ -1,5 +1,7 @@ # Disallow using the `meta.replacedBy` rule property (`eslint-plugin/no-meta-replaced-by`) +💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets). + As of ESLint v9.21.0, the rule property `meta.deprecated` can be either a boolean or an object of type `DeprecatedInfo`. The `DeprecatedInfo` type includes an optional `replacedBy` array that replaces the now-deprecated `meta.replacedBy` property. diff --git a/docs/rules/no-meta-schema-default.md b/docs/rules/no-meta-schema-default.md index 7bd3726e..56bcc3b9 100644 --- a/docs/rules/no-meta-schema-default.md +++ b/docs/rules/no-meta-schema-default.md @@ -1,5 +1,7 @@ # Disallow rules `meta.schema` properties to include defaults (`eslint-plugin/no-meta-schema-default`) +💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets). + Since ESLint v9.15.0, rules' default options are supported using `meta.defaultOptions`. Additionally defining them using the `default` property in `meta.schema` is confusing, error-prone, and can be ambiguous for complex schemas. diff --git a/docs/rules/require-meta-default-options.md b/docs/rules/require-meta-default-options.md index 70dab8ef..7c0488d8 100644 --- a/docs/rules/require-meta-default-options.md +++ b/docs/rules/require-meta-default-options.md @@ -1,5 +1,7 @@ # Require only rules with options to implement a `meta.defaultOptions` property (`eslint-plugin/require-meta-default-options`) +💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets). + 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). diff --git a/docs/rules/require-meta-schema-description.md b/docs/rules/require-meta-schema-description.md index 00a84e39..78e70ee4 100644 --- a/docs/rules/require-meta-schema-description.md +++ b/docs/rules/require-meta-schema-description.md @@ -1,5 +1,7 @@ # Require rules `meta.schema` properties to include descriptions (`eslint-plugin/require-meta-schema-description`) +💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets). + Defining a description in the schema for each rule option helps explain that option to users. diff --git a/lib/rules/no-meta-replaced-by.ts b/lib/rules/no-meta-replaced-by.ts index 56d04f6f..c126a225 100644 --- a/lib/rules/no-meta-replaced-by.ts +++ b/lib/rules/no-meta-replaced-by.ts @@ -14,7 +14,7 @@ const rule: Rule.RuleModule = { docs: { description: 'disallow using the `meta.replacedBy` rule property', category: 'Rules', - recommended: false, + recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-meta-replaced-by.md', }, schema: [], diff --git a/lib/rules/no-meta-schema-default.ts b/lib/rules/no-meta-schema-default.ts index 46a29b0b..4cf540f0 100644 --- a/lib/rules/no-meta-schema-default.ts +++ b/lib/rules/no-meta-schema-default.ts @@ -18,7 +18,7 @@ const rule: Rule.RuleModule = { description: 'disallow rules `meta.schema` properties to include defaults', category: 'Rules', - recommended: false, + recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-meta-schema-default.md', }, schema: [], diff --git a/lib/rules/require-meta-default-options.ts b/lib/rules/require-meta-default-options.ts index c8640f55..a8bc4374 100644 --- a/lib/rules/require-meta-default-options.ts +++ b/lib/rules/require-meta-default-options.ts @@ -15,7 +15,7 @@ const rule: Rule.RuleModule = { description: 'require only rules with options to implement a `meta.defaultOptions` property', category: 'Rules', - recommended: false, + recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-default-options.md', }, fixable: 'code', diff --git a/lib/rules/require-meta-schema-description.ts b/lib/rules/require-meta-schema-description.ts index dba60771..e4167bca 100644 --- a/lib/rules/require-meta-schema-description.ts +++ b/lib/rules/require-meta-schema-description.ts @@ -18,7 +18,7 @@ const rule: Rule.RuleModule = { description: 'require rules `meta.schema` properties to include descriptions', category: 'Rules', - recommended: false, + recommended: true, url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-schema-description.md', }, schema: [], From aa3a4d9303d5c1461fea9c2990d443ba4c4467f9 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:24:21 -0400 Subject: [PATCH 13/15] chore: switch to TypeScript for `.eslint-doc-generatorrc.ts` (#536) chore: switch to TypeScript for .eslint-doc-generatorrc.ts --- .eslint-doc-generatorrc.js => .eslint-doc-generatorrc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .eslint-doc-generatorrc.js => .eslint-doc-generatorrc.ts (84%) diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.ts similarity index 84% rename from .eslint-doc-generatorrc.js rename to .eslint-doc-generatorrc.ts index df9f4712..7f80e39c 100644 --- a/.eslint-doc-generatorrc.js +++ b/.eslint-doc-generatorrc.ts @@ -1,7 +1,7 @@ import prettier from 'prettier'; +import type { GenerateOptions } from 'eslint-doc-generator'; -/** @type {import('eslint-doc-generator').GenerateOptions} */ -const config = { +const config: GenerateOptions = { ignoreConfig: [ 'all', 'all-type-checked', From 6b1347d60b35939b2d7d94bedd4566e1c5a65995 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 4 Aug 2025 09:35:14 -0500 Subject: [PATCH 14/15] build: update typescript to 5.9 (#535) * build: update typescript This change updates `typescript` to 5.9 * caret --------- Co-authored-by: Bryan Mishkin <698306+bmish@users.noreply.github.com> --- .npmpackagejsonlintrc.json | 4 +--- package.json | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json index 89ac0784..da1a90e6 100644 --- a/.npmpackagejsonlintrc.json +++ b/.npmpackagejsonlintrc.json @@ -8,8 +8,6 @@ "prefer-alphabetical-optionalDependencies": "error", "prefer-alphabetical-scripts": "error", "prefer-caret-version-dependencies": "error", - "prefer-caret-version-devDependencies": ["error", { - "exceptions": ["eslint", "eslint-plugin-eslint-plugin"] - }] + "prefer-caret-version-devDependencies": "error" } } diff --git a/package.json b/package.json index cf9f6e1d..a85974ab 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,8 @@ "@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", + "@typescript-eslint/parser": "^8.38.0", + "@typescript-eslint/utils": "^8.38.0", "@vitest/coverage-istanbul": "^3.2.4", "eslint": "^9.31.0", "eslint-config-not-an-aardvark": "^2.1.0", @@ -74,7 +74,7 @@ "eslint-scope": "^8.0.1", "espree": "^10.0.1", "husky": "^9.1.7", - "jiti": "^2.4.2", + "jiti": "^2.5.1", "lodash": "^4.17.21", "markdownlint-cli": "^0.43.0", "npm-package-json-lint": "^8.0.0", @@ -82,7 +82,7 @@ "prettier": "^3.4.1", "release-it": "^17.2.0", "tsup": "^8.5.0", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vitest": "^3.2.4" }, "peerDependencies": { From a923ff40ecdffb99f54eda7c5c853874f6192827 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:58:21 -0400 Subject: [PATCH 15/15] chore(main): release 7.0.0 (#526) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd509eda..d9086d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,26 @@ * update dependency markdownlint-cli to ^0.38.0 ([#410](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/410)) ([6b53c5b](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/6b53c5b7b8bc9e19dcb86796ab29019f89c449fc)) * update dependency markdownlint-cli to ^0.39.0 ([#431](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/431)) ([f005a2c](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/f005a2c0231b8b77f6862dca81b4a6e3099e0493)) +## [7.0.0](https://github.com/eslint-community/eslint-plugin-eslint-plugin/compare/v6.5.0...v7.0.0) (2025-08-04) + + +### ⚠ BREAKING CHANGES + +* enable no-meta-replaced-by, no-meta-schema-default, require-meta-default-options, require-meta-schema-description as recommended rules +* enable `no-meta-replaced-by`, `no-meta-schema-default`, `require-meta-default-options`, `require-meta-schema-description` as `recommended` rules ([#530](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/530)) +* require Node 20, 22, 24+ ([#529](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/529)) +* remove eslint v8 / eslintrc support and remove `flat/` prefix from configs ([#528](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/528)) +* move to ESM only ([#516](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/516)) + +### Features + +* enable `no-meta-replaced-by`, `no-meta-schema-default`, `require-meta-default-options`, `require-meta-schema-description` as `recommended` rules ([#530](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/530)) ([b353bd1](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/b353bd1d716c79fc8be458f6de8846c2d5c910f2)) +* enable no-meta-replaced-by, no-meta-schema-default, require-meta-default-options, require-meta-schema-description as recommended rules ([b353bd1](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/b353bd1d716c79fc8be458f6de8846c2d5c910f2)) +* migrate package to TypeScript and publish types ([#534](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/534)) ([95b859a](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/95b859ab9a263cc623871ac7930c0f83c197163f)) +* move to ESM only ([#516](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/516)) ([9cd5af8](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/9cd5af882bf992a63d05a9b21dd4366c384b5150)) +* remove eslint v8 / eslintrc support and remove `flat/` prefix from configs ([#528](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/528)) ([03cf3d7](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/03cf3d7b857561d24fd0bfdbeeb926cb6bc02d10)) +* require Node 20, 22, 24+ ([#529](https://github.com/eslint-community/eslint-plugin-eslint-plugin/issues/529)) ([b2994c7](https://github.com/eslint-community/eslint-plugin-eslint-plugin/commit/b2994c73ba71576cdd96d95469613b4d573740c2)) + ## [6.5.0](https://github.com/eslint-community/eslint-plugin-eslint-plugin/compare/v6.4.0...v6.5.0) (2025-06-18) diff --git a/package.json b/package.json index a85974ab..0101ff80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-eslint-plugin", - "version": "6.5.0", + "version": "7.0.0", "description": "An ESLint plugin for linting ESLint plugins", "author": "Teddy Katz", "main": "./dist/index.js",