diff --git a/.eslint-doc-generatorrc.js b/.eslint-doc-generatorrc.ts
similarity index 60%
rename from .eslint-doc-generatorrc.js
rename to .eslint-doc-generatorrc.ts
index 5f25c0a6..7f80e39c 100644
--- a/.eslint-doc-generatorrc.js
+++ b/.eslint-doc-generatorrc.ts
@@ -1,9 +1,7 @@
-'use strict';
+import prettier from 'prettier';
+import type { GenerateOptions } from 'eslint-doc-generator';
-const prettier = require('prettier');
-
-/** @type {import('eslint-doc-generator').GenerateOptions} */
-module.exports = {
+const config: GenerateOptions = {
ignoreConfig: [
'all',
'all-type-checked',
@@ -11,13 +9,6 @@ module.exports = {
'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, {
@@ -29,3 +20,5 @@ module.exports = {
urlConfigs:
'https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets',
};
+
+export default config;
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f4f9204f..0636ad67 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,11 +12,9 @@ jobs:
fail-fast: false
matrix:
node-version:
- - 23
+ - 24
- 22
- - 21
- 20
- - 18
os:
- ubuntu-latest
steps:
@@ -33,21 +31,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
@@ -55,6 +42,16 @@ 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
+
+ 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 642ad460..302cc701 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,11 @@
-.idea
-.nyc_output
+.idea/
+coverage/
.vscode
node_modules/
npm-debug.log
yarn.lock
.eslintcache
+dist/
# eslint-remote-tester
eslint-remote-tester-results
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/.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/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/README.md b/README.md
index f6eeb235..5f15127a 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',
@@ -87,8 +74,8 @@ module.exports = [
| [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 | | | | 💭 |
@@ -100,14 +87,14 @@ module.exports = [
| [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
@@ -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/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
deleted file mode 100644
index 59bbf2b3..00000000
--- a/configs/all-type-checked.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * @deprecated use 'flat/all-type-checked' instead
- * @author 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/all-type-checked'];
diff --git a/configs/all.js b/configs/all.js
deleted file mode 100644
index e237d769..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 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/all'];
diff --git a/configs/recommended.js b/configs/recommended.js
deleted file mode 100644
index bb282caa..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 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/recommended'];
diff --git a/configs/rules-recommended.js b/configs/rules-recommended.js
deleted file mode 100644
index 09d8f57a..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 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/rules-recommended'];
diff --git a/configs/rules.js b/configs/rules.js
deleted file mode 100644
index 71ca5852..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 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/rules'];
diff --git a/configs/tests-recommended.js b/configs/tests-recommended.js
deleted file mode 100644
index 0a467239..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 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/tests-recommended'];
diff --git a/configs/tests.js b/configs/tests.js
deleted file mode 100644
index 184d3bab..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 唯然
- */
-
-'use strict';
-
-const plugin = require('../lib/index.js');
-
-module.exports = plugin.configs['flat/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-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/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/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/eslint.config.js b/eslint.config.ts
similarity index 70%
rename from eslint.config.js
rename to eslint.config.ts
index 24f9f4ea..ad8f1362 100644
--- a/eslint.config.js
+++ b/eslint.config.ts
@@ -1,25 +1,34 @@
-'use strict';
-
-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');
+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';
+import eslintPlugin from './lib/index.js';
+const dirname = path.dirname(fileURLToPath(import.meta.url));
const compat = new FlatCompat({
- baseDirectory: __dirname,
+ baseDirectory: dirname,
recommendedConfig: js.configs.recommended,
});
-module.exports = [
+export default defineConfig([
+ // Global ignores
+ {
+ ignores: ['node_modules', 'coverage', 'dist'],
+ },
+ // 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',
@@ -31,16 +40,15 @@ module.exports = [
'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',
},
},
{
// 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': [
@@ -52,10 +60,6 @@ module.exports = [
],
},
},
- {
- files: ['tests/**/*.js'],
- languageOptions: { globals: globals.mocha },
- },
{
files: ['**/*.md'],
plugins: { markdown },
@@ -73,7 +77,9 @@ module.exports = [
'@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
deleted file mode 100644
index d71340a3..00000000
--- a/lib/index.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * @fileoverview An ESLint plugin for linting ESLint plugins
- * @author Teddy Katz
- */
-
-'use strict';
-
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
-const fs = require('fs');
-const path = require('path');
-const packageMetadata = require('../package');
-const PLUGIN_NAME = packageMetadata.name.replace(/^eslint-plugin-/, '');
-
-const configFilters = {
- all: (rule) => !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) =>
- configFilters.recommended(rule) && configFilters.rules(rule),
- 'tests-recommended': (rule) =>
- configFilters.recommended(rule) && configFilters.tests(rule),
-};
-
-// ------------------------------------------------------------------------------
-// Plugin Definition
-// ------------------------------------------------------------------------------
-
-// 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)),
- ]),
-);
-
-/** @type {import("eslint").ESLint.Plugin} */
-const plugin = {
- meta: {
- name: packageMetadata.name,
- version: packageMetadata.version,
- },
- rules: allRules,
- configs: {}, // assigned later
-};
-
-// eslintrc 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 },
- rules: Object.fromEntries(
- Object.keys(allRules)
- .filter((ruleName) => configFilters[configName](allRules[ruleName]))
- .map((ruleName) => [`${PLUGIN_NAME}/${ruleName}`, 'error']),
- ),
- },
- });
- }, {}),
-);
-
-module.exports = plugin;
diff --git a/lib/index.ts b/lib/index.ts
new file mode 100644
index 00000000..1fbcb0af
--- /dev/null
+++ b/lib/index.ts
@@ -0,0 +1,149 @@
+/**
+ * @fileoverview An ESLint plugin for linting ESLint plugins
+ * @author Teddy Katz
+ */
+import { createRequire } from 'node:module';
+
+import type { ESLint, Linter, Rule } from 'eslint';
+
+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 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: 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.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: 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
+// ------------------------------------------------------------------------------
+
+// import all rules in lib/rules
+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,
+} satisfies Record;
+
+const plugin = {
+ meta: {
+ name: packageMetadata.name,
+ version: packageMetadata.version,
+ },
+ rules: allRules,
+ 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 72%
rename from lib/rules/consistent-output.js
rename to lib/rules/consistent-output.ts
index 42cbf3ae..6495fec3 100644
--- a/lib/rules/consistent-output.js
+++ b/lib/rules/consistent-output.ts
@@ -2,17 +2,17 @@
* @fileoverview Enforce consistent use of `output` assertions in rule tests
* @author Teddy Katz
*/
+import type { Rule } from 'eslint';
-'use strict';
+import { getKeyName, getTestInfo } from '../utils.js';
-const utils = require('../utils');
+const keyNameMapper = (property: Parameters[0]) =>
+ getKeyName(property);
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -22,7 +22,7 @@ module.exports = {
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',
@@ -39,20 +39,18 @@ module.exports = {
},
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) {
- utils.getTestInfo(context, ast).forEach((testRun) => {
+ 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(utils.getKeyName).includes('output'),
+ !testCase.properties.map(keyNameMapper).includes('output'),
);
if (
@@ -71,3 +69,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.ts
similarity index 68%
rename from lib/rules/fixer-return.js
rename to lib/rules/fixer-return.ts
index 11e7d57f..97aedf04 100644
--- a/lib/rules/fixer-return.js
+++ b/lib/rules/fixer-return.ts
@@ -2,22 +2,37 @@
* @fileoverview require fixer functions to return a fix
* @author 薛定谔的猫
*/
-
-'use strict';
-
-// ------------------------------------------------------------------------------
-// Requirements
-// ------------------------------------------------------------------------------
-
-const utils = require('../utils');
-const { getStaticValue } = require('@eslint-community/eslint-utils');
+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} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -26,7 +41,7 @@ module.exports = {
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.',
@@ -34,28 +49,24 @@ module.exports = {
},
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
@@ -71,16 +82,14 @@ module.exports = {
/**
* 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}
+ * @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;
}
- 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.
@@ -98,30 +107,30 @@ module.exports = {
return {
Program(ast) {
- const sourceCode = utils.getSourceCode(context);
- contextIdentifiers = utils.getContextIdentifiers(
+ const sourceCode = context.sourceCode;
+ contextIdentifiers = getContextIdentifiers(
sourceCode.scopeManager,
ast,
);
},
// Stacks this function's information.
- onCodePathStart(codePath, node) {
+ onCodePathStart(codePath: Rule.CodePath, node: Node) {
funcInfo = {
upper: funcInfo,
codePath,
hasYieldWithFixer: false,
hasReturnWithFixer: false,
shouldCheck:
- utils.isAutoFixerFunction(node, contextIdentifiers) ||
- utils.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
@@ -148,8 +157,8 @@ module.exports = {
// Ensure the current (arrow) fixer function returned a fix.
'ArrowFunctionExpression:exit'(node) {
if (funcInfo.shouldCheck) {
- const sourceCode = utils.getSourceCode(context);
- const loc = sourceCode.getTokenBefore(node.body).loc; // Show violation on arrow (=>).
+ 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.
if (!isFix(node.body)) {
@@ -167,3 +176,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.ts
similarity index 72%
rename from lib/rules/meta-property-ordering.js
rename to lib/rules/meta-property-ordering.ts
index 27e29218..88125d70 100644
--- a/lib/rules/meta-property-ordering.js
+++ b/lib/rules/meta-property-ordering.ts
@@ -1,11 +1,9 @@
/**
* @fileoverview Enforces the order of meta properties
*/
+import type { Rule } from 'eslint';
-'use strict';
-
-const utils = require('../utils');
-const { getKeyName, getRuleInfo } = utils;
+import { getKeyName, getRuleInfo } from '../utils.js';
const defaultOrder = [
'type',
@@ -19,12 +17,13 @@ const defaultOrder = [
'messages',
];
+const keyNameMapper = (property: Parameters[0]) =>
+ getKeyName(property);
+
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -49,30 +48,35 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
const ruleInfo = getRuleInfo(sourceCode);
if (!ruleInfo) {
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);
});
@@ -83,7 +87,8 @@ module.exports = {
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)),
@@ -94,7 +99,7 @@ module.exports = {
node: violatingProp,
messageId: 'inconsistentOrder',
data: {
- order: knownProps.map(getKeyName).join(', '),
+ order: knownProps.map(keyNameMapper).join(', '),
},
fix(fixer) {
const expectedProps = [...knownProps, ...unknownProps];
@@ -111,3 +116,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.ts
similarity index 71%
rename from lib/rules/no-deprecated-context-methods.js
rename to lib/rules/no-deprecated-context-methods.ts
index c4f6134b..bd2a126f 100644
--- a/lib/rules/no-deprecated-context-methods.js
+++ b/lib/rules/no-deprecated-context-methods.ts
@@ -2,10 +2,10 @@
* @fileoverview Disallows usage of deprecated methods on rule context objects
* @author Teddy Katz
*/
+import type { Rule } from 'eslint';
+import type { Identifier, MemberExpression } from 'estree';
-'use strict';
-
-const utils = require('../utils');
+import { getContextIdentifiers } from '../utils.js';
const DEPRECATED_PASSTHROUGHS = {
getSource: 'getText',
@@ -28,14 +28,12 @@ const DEPRECATED_PASSTHROUGHS = {
getTokensAfter: 'getTokensAfter',
getTokensBefore: 'getTokensBefore',
getTokensBetween: 'getTokensBetween',
-};
+} satisfies Record;
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -54,7 +52,7 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
// ----------------------------------------------------------------------
// Public
@@ -62,37 +60,38 @@ module.exports = {
return {
'Program:exit'(ast) {
- [...utils.getContextIdentifiers(sourceCode.scopeManager, ast)]
+ [...getContextIdentifiers(sourceCode.scopeManager, ast)]
.filter(
(contextId) =>
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],
),
],
- }),
- );
+ });
+ });
},
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.ts
similarity index 76%
rename from lib/rules/no-deprecated-report-api.js
rename to lib/rules/no-deprecated-report-api.ts
index eb0c07c2..91a3b622 100644
--- a/lib/rules/no-deprecated-report-api.js
+++ b/lib/rules/no-deprecated-report-api.ts
@@ -2,17 +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';
-'use strict';
-
-const utils = require('../utils');
+import { getContextIdentifiers, getReportInfo } from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -30,8 +28,8 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
- let contextIdentifiers;
+ const sourceCode = context.sourceCode;
+ let contextIdentifiers: Set;
// ----------------------------------------------------------------------
// Public
@@ -39,7 +37,7 @@ module.exports = {
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(
+ contextIdentifiers = getContextIdentifiers(
sourceCode.scopeManager,
ast,
);
@@ -58,9 +56,11 @@ module.exports = {
node: node.callee.property,
messageId: 'useNewAPI',
fix(fixer) {
- const openingParen = sourceCode.getTokenBefore(node.arguments[0]);
- const closingParen = sourceCode.getLastToken(node);
- const reportInfo = utils.getReportInfo(node, context);
+ const openingParen = sourceCode.getTokenBefore(
+ node.arguments[0],
+ )!;
+ const closingParen = sourceCode.getLastToken(node)!;
+ const reportInfo = getReportInfo(node, context);
if (!reportInfo) {
return null;
@@ -70,7 +70,8 @@ module.exports = {
[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(', ')}}`,
);
@@ -81,3 +82,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.js
deleted file mode 100644
index c7c5867f..00000000
--- a/lib/rules/no-identical-tests.js
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @fileoverview disallow identical tests
- * @author 薛定谔的猫
- */
-
-'use strict';
-
-const utils = require('../utils');
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
- meta: {
- type: 'problem',
- docs: {
- description: 'disallow identical tests',
- category: 'Tests',
- recommended: true,
- url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-identical-tests.md',
- },
- fixable: 'code',
- schema: [],
- messages: {
- identical: 'This test case is identical to another case.',
- },
- },
-
- create(context) {
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
- const sourceCode = utils.getSourceCode(context);
-
- // ----------------------------------------------------------------------
- // Helpers
- // ----------------------------------------------------------------------
- /**
- * Create a unique cache key
- * @param {object} test
- * @returns {string}
- */
- function toKey(test) {
- if (test.type !== 'ObjectExpression') {
- return JSON.stringify([test.type, sourceCode.getText(test)]);
- }
- return JSON.stringify([
- test.type,
- ...test.properties.map((p) => sourceCode.getText(p)).sort(),
- ]);
- }
-
- return {
- Program(ast) {
- utils.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);
- }
- });
- });
- });
- },
- };
- },
-};
diff --git a/lib/rules/no-identical-tests.ts b/lib/rules/no-identical-tests.ts
new file mode 100644
index 00000000..d60b5a48
--- /dev/null
+++ b/lib/rules/no-identical-tests.ts
@@ -0,0 +1,81 @@
+/**
+ * @fileoverview disallow identical tests
+ * @author 薛定谔的猫
+ */
+import type { Rule } from 'eslint';
+import type { Expression, SpreadElement } from 'estree';
+
+import { getTestInfo } from '../utils.js';
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+const rule: Rule.RuleModule = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow identical tests',
+ category: 'Tests',
+ recommended: true,
+ url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-identical-tests.md',
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ identical: 'This test case is identical to another case.',
+ },
+ },
+
+ create(context) {
+ const sourceCode = context.sourceCode;
+
+ /**
+ * Create a unique cache key
+ */
+ function toKey(test: Expression | SpreadElement): string {
+ if (test.type !== 'ObjectExpression') {
+ return JSON.stringify([test.type, sourceCode.getText(test)]);
+ }
+ return JSON.stringify([
+ test.type,
+ ...test.properties.map((p) => sourceCode.getText(p)).sort(),
+ ]);
+ }
+
+ return {
+ Program(ast) {
+ getTestInfo(context, ast).forEach((testRun) => {
+ [testRun.valid, testRun.invalid].forEach((tests) => {
+ const cache = new Set();
+ 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);
+ }
+ });
+ });
+ });
+ },
+ };
+ },
+};
+
+export default rule;
diff --git a/lib/rules/no-meta-replaced-by.js b/lib/rules/no-meta-replaced-by.ts
similarity index 68%
rename from lib/rules/no-meta-replaced-by.js
rename to lib/rules/no-meta-replaced-by.ts
index 36aa1297..c126a225 100644
--- a/lib/rules/no-meta-replaced-by.js
+++ b/lib/rules/no-meta-replaced-by.ts
@@ -1,23 +1,20 @@
/**
* @fileoverview Disallows the usage of `meta.replacedBy` property
*/
+import type { Rule } from 'eslint';
-'use strict';
-
-const utils = require('../utils');
+import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
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: [],
@@ -27,8 +24,8 @@ module.exports = {
},
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
- const ruleInfo = utils.getRuleInfo(sourceCode);
+ const sourceCode = context.sourceCode;
+ const ruleInfo = getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
@@ -42,12 +39,10 @@ module.exports = {
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;
@@ -61,3 +56,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-meta-schema-default.js b/lib/rules/no-meta-schema-default.ts
similarity index 60%
rename from lib/rules/no-meta-schema-default.js
rename to lib/rules/no-meta-schema-default.ts
index ccba978a..4cf540f0 100644
--- a/lib/rules/no-meta-schema-default.js
+++ b/lib/rules/no-meta-schema-default.ts
@@ -1,21 +1,24 @@
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { Expression, SpreadElement } from 'estree';
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
+import {
+ getMetaSchemaNode,
+ getMetaSchemaNodeProperty,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
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: [],
@@ -25,49 +28,47 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
const { scopeManager } = sourceCode;
- const ruleInfo = utils.getRuleInfo(sourceCode);
- if (!ruleInfo) {
+ const ruleInfo = getRuleInfo(sourceCode);
+ if (!ruleInfo || !ruleInfo.meta) {
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);
+ 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 @@ module.exports = {
}
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 @@ module.exports = {
}
case 'elements': {
- checkSchemaElement(value);
-
+ checkSchemaElement(value as Expression | SpreadElement);
break;
}
@@ -111,3 +114,5 @@ module.exports = {
}
},
};
+
+export default rule;
diff --git a/lib/rules/no-missing-message-ids.js b/lib/rules/no-missing-message-ids.ts
similarity index 64%
rename from lib/rules/no-missing-message-ids.js
rename to lib/rules/no-missing-message-ids.ts
index 0bc2d9ed..3ece7a2a 100644
--- a/lib/rules/no-missing-message-ids.js
+++ b/lib/rules/no-missing-message-ids.ts
@@ -1,13 +1,20 @@
-'use strict';
+import type { Rule } from 'eslint';
+import type { Identifier, Node } from 'estree';
-const utils = require('../utils');
+import {
+ collectReportViolationAndSuggestionData,
+ findPossibleVariableValues,
+ getContextIdentifiers,
+ getMessageIdNodeById,
+ getMessagesNode,
+ getReportInfo,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -17,7 +24,7 @@ module.exports = {
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:
@@ -26,16 +33,16 @@ module.exports = {
},
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;
+ let contextIdentifiers: Set;
if (!messagesNode || messagesNode.type !== 'ObjectExpression') {
// If we can't find `meta.messages`, disable the rule.
@@ -44,11 +51,11 @@ module.exports = {
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
+ contextIdentifiers = getContextIdentifiers(scopeManager, ast);
},
CallExpression(node) {
- const scope = utils.getScope(context);
+ const scope = context.sourceCode.getScope(node);
// Check for messageId properties used in known calls to context.report();
if (
node.callee.type === 'MemberExpression' &&
@@ -56,20 +63,23 @@ module.exports = {
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);
- for (const { messageId } of reportMessagesAndDataArray.filter(
- (obj) => obj.messageId,
- )) {
+ collectReportViolationAndSuggestionData(reportInfo);
+ for (const messageId of reportMessagesAndDataArray
+ .map((obj) => obj.messageId)
+ .filter((messageId) => !!messageId)) {
const values =
messageId.type === 'Literal'
? [messageId]
- : utils.findPossibleVariableValues(messageId, scopeManager);
+ : findPossibleVariableValues(
+ messageId as Identifier,
+ scopeManager,
+ );
// Look for any possible string values we found for this messageId.
values.forEach((val) => {
@@ -77,12 +87,7 @@ module.exports = {
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({
@@ -99,3 +104,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.ts
similarity index 69%
rename from lib/rules/no-missing-placeholders.js
rename to lib/rules/no-missing-placeholders.ts
index 996e8004..fa39d453 100644
--- a/lib/rules/no-missing-placeholders.js
+++ b/lib/rules/no-missing-placeholders.ts
@@ -2,18 +2,24 @@
* @fileoverview Disallow missing placeholders in rule report messages
* @author Teddy Katz
*/
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { Node } from 'estree';
-'use strict';
-
-const utils = require('../utils');
-const { getStaticValue } = require('@eslint-community/eslint-utils');
+import {
+ collectReportViolationAndSuggestionData,
+ getContextIdentifiers,
+ getKeyName,
+ getMessageIdNodeById,
+ getMessagesNode,
+ getReportInfo,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -22,7 +28,7 @@ module.exports = {
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:
@@ -31,37 +37,37 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
const { scopeManager } = sourceCode;
- let contextIdentifiers;
+ let contextIdentifiers: Set;
- 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.
@@ -72,7 +78,7 @@ module.exports = {
obj.messageId.type === 'Literal' &&
typeof obj.messageId.value === 'string'
) {
- const correspondingMessage = utils.getMessageIdNodeById(
+ const correspondingMessage = getMessageIdNodeById(
obj.messageId.value,
ruleInfo,
scopeManager,
@@ -90,9 +96,10 @@ module.exports = {
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')) &&
@@ -101,22 +108,21 @@ module.exports = {
// 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) => utils.getKeyName(prop) === match[1],
+ (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] },
});
@@ -129,3 +135,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.ts
similarity index 81%
rename from lib/rules/no-only-tests.js
rename to lib/rules/no-only-tests.ts
index 3aa267d2..6ed93ce5 100644
--- a/lib/rules/no-only-tests.js
+++ b/lib/rules/no-only-tests.ts
@@ -1,14 +1,13 @@
-'use strict';
-
-const utils = require('../utils');
-const {
+import {
isCommaToken,
isOpeningBraceToken,
isClosingBraceToken,
-} = require('@eslint-community/eslint-utils');
+} from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+
+import { getTestInfo } from '../utils.js';
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -29,9 +28,9 @@ module.exports = {
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') {
+ if (test?.type === 'ObjectExpression') {
// Test case object: { code: 'const x = 123;', ... }
const onlyProperty = test.properties.find(
@@ -51,21 +50,21 @@ module.exports = {
{
messageId: 'removeOnly',
*fix(fixer) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
const tokenBefore =
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)
) {
@@ -80,7 +79,7 @@ module.exports = {
});
}
} else if (
- test.type === 'CallExpression' &&
+ test?.type === 'CallExpression' &&
test.callee.type === 'MemberExpression' &&
test.callee.object.type === 'Identifier' &&
test.callee.object.name === 'RuleTester' &&
@@ -96,3 +95,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-property-in-node.js b/lib/rules/no-property-in-node.ts
similarity index 83%
rename from lib/rules/no-property-in-node.js
rename to lib/rules/no-property-in-node.ts
index f21e0060..7ef564a2 100644
--- a/lib/rules/no-property-in-node.js
+++ b/lib/rules/no-property-in-node.ts
@@ -1,6 +1,5 @@
-'use strict';
-
-const utils = require('../utils');
+import type { Rule } from 'eslint';
+import type { Type } from 'typescript';
const defaultTypedNodeSourceFileTesters = [
/@types[/\\]estree[/\\]index\.d\.ts/,
@@ -27,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(
@@ -46,8 +46,7 @@ function isAstNodeType(type, typedNodeSourceFileTesters) {
});
}
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -55,6 +54,7 @@ module.exports = {
'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',
},
@@ -79,18 +79,21 @@ module.exports = {
},
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 {
'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.',
@@ -108,3 +111,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.ts
similarity index 62%
rename from lib/rules/no-unused-message-ids.js
rename to lib/rules/no-unused-message-ids.ts
index 69036140..e260e5ce 100644
--- a/lib/rules/no-unused-message-ids.js
+++ b/lib/rules/no-unused-message-ids.ts
@@ -1,13 +1,22 @@
-'use strict';
-
-const utils = require('../utils');
+import type { Rule } from 'eslint';
+import type { Identifier, Node } from 'estree';
+
+import {
+ collectReportViolationAndSuggestionData,
+ findPossibleVariableValues,
+ getContextIdentifiers,
+ getKeyName,
+ getMessageIdNodes,
+ getReportInfo,
+ getRuleInfo,
+ isVariableFromParameter,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -16,7 +25,7 @@ module.exports = {
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.',
@@ -24,19 +33,19 @@ module.exports = {
},
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 messageIdsUsed = new Set();
- let contextIdentifiers;
+ const messageIdsUsed = new Set();
+ let contextIdentifiers: Set;
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 {};
@@ -44,7 +53,7 @@ module.exports = {
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
+ contextIdentifiers = getContextIdentifiers(scopeManager, ast);
},
'Program:exit'() {
@@ -57,10 +66,10 @@ module.exports = {
return;
}
- const scope = utils.getScope(context);
+ const scope = sourceCode.getScope(sourceCode.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.
@@ -69,7 +78,7 @@ module.exports = {
node: messageIdNode,
messageId: 'unusedMessage',
data: {
- messageId: utils.getKeyName(messageIdNode, scope),
+ messageId: getKeyName(messageIdNode, scope)!,
},
});
}
@@ -83,7 +92,7 @@ module.exports = {
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'report'
) {
- const reportInfo = utils.getReportInfo(node, context);
+ const reportInfo = getReportInfo(node, context);
if (!reportInfo) {
return;
}
@@ -91,14 +100,16 @@ module.exports = {
hasSeenViolationReport = true;
const reportMessagesAndDataArray =
- utils.collectReportViolationAndSuggestionData(reportInfo);
- for (const { messageId } of reportMessagesAndDataArray.filter(
- (obj) => obj.messageId,
- )) {
+ collectReportViolationAndSuggestionData(reportInfo);
+ for (const messageId of reportMessagesAndDataArray
+ .map((obj) => obj.messageId)
+ .filter((messageId) => !!messageId)) {
const values =
messageId.type === 'Literal'
? [messageId]
- : utils.findPossibleVariableValues(messageId, scopeManager);
+ : messageId.type === 'Identifier'
+ ? findPossibleVariableValues(messageId, scopeManager)
+ : [];
if (
values.length === 0 ||
values.some((val) => val.type !== 'Literal')
@@ -106,7 +117,11 @@ module.exports = {
// 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));
}
}
},
@@ -120,20 +135,30 @@ module.exports = {
const values =
node.value.type === 'Literal'
? [node.value]
- : utils.findPossibleVariableValues(node.value, scopeManager);
+ : findPossibleVariableValues(
+ node.value as Identifier,
+ scopeManager,
+ );
if (
values.length === 0 ||
values.some((val) => val.type !== 'Literal') ||
- utils.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));
}
},
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.ts
similarity index 65%
rename from lib/rules/no-unused-placeholders.js
rename to lib/rules/no-unused-placeholders.ts
index d7169c9a..a9dc14cd 100644
--- a/lib/rules/no-unused-placeholders.js
+++ b/lib/rules/no-unused-placeholders.ts
@@ -2,18 +2,24 @@
* @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';
-'use strict';
-
-const utils = require('../utils');
-const { getStaticValue } = require('@eslint-community/eslint-utils');
+import {
+ collectReportViolationAndSuggestionData,
+ getContextIdentifiers,
+ getKeyName,
+ getMessageIdNodeById,
+ getMessagesNode,
+ getReportInfo,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -22,7 +28,7 @@ module.exports = {
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:
@@ -31,36 +37,36 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
const { scopeManager } = sourceCode;
- let contextIdentifiers;
+ let contextIdentifiers = new Set();
- 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.
@@ -71,7 +77,7 @@ module.exports = {
obj.messageId.type === 'Literal' &&
typeof obj.messageId.value === 'string'
) {
- const correspondingMessage = utils.getMessageIdNodeById(
+ const correspondingMessage = getMessageIdNodeById(
obj.messageId.value,
ruleInfo,
scopeManager,
@@ -87,30 +93,32 @@ module.exports = {
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 = utils.getKeyName(prop);
- if (!placeholdersInMessage.has(key)) {
+ const key = getKeyName(prop);
+ if (key && !placeholdersInMessage.has(key)) {
context.report({
node: prop,
messageId: 'placeholderUnused',
@@ -125,3 +133,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.ts
similarity index 60%
rename from lib/rules/no-useless-token-range.js
rename to lib/rules/no-useless-token-range.ts
index 6c6d6214..653ef53d 100644
--- a/lib/rules/no-useless-token-range.js
+++ b/lib/rules/no-useless-token-range.ts
@@ -2,17 +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';
-'use strict';
-
-const utils = require('../utils');
+import { getKeyName, getSourceCodeIdentifiers } from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -30,7 +35,7 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
// ----------------------------------------------------------------------
// Helpers
@@ -39,10 +44,10 @@ module.exports = {
/**
* 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;
}
@@ -52,31 +57,30 @@ module.exports = {
return (
arg.properties.length >= 2 ||
(arg.properties.length === 1 &&
- (utils.getKeyName(arg.properties[0]) !== 'includeComments' ||
- arg.properties[0].value.type !== 'Literal'))
+ (getKeyName(arg.properties[0]) !== 'includeComments' ||
+ (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'
@@ -89,6 +93,7 @@ module.exports = {
(memberExpression.computed &&
memberExpression.property.type === 'Literal' &&
memberExpression.property.value === 0 &&
+ memberExpression.object.type === 'MemberExpression' &&
isRangeAccess(memberExpression.object))
);
}
@@ -96,10 +101,10 @@ module.exports = {
/**
* 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'
@@ -112,6 +117,7 @@ module.exports = {
(memberExpression.computed &&
memberExpression.property.type === 'Literal' &&
memberExpression.property.value === 1 &&
+ memberExpression.object.type === 'MemberExpression' &&
isRangeAccess(memberExpression.object))
);
}
@@ -122,7 +128,7 @@ module.exports = {
return {
'Program:exit'(ast) {
- [...utils.getSourceCodeIdentifiers(sourceCode.scopeManager, ast)]
+ [...getSourceCodeIdentifiers(sourceCode.scopeManager, ast)]
.filter(
(identifier) =>
identifier.parent.type === 'MemberExpression' &&
@@ -141,34 +147,39 @@ module.exports = {
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]),
+ );
+ },
+ });
+ }
});
},
};
},
};
+
+export default rule;
diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.ts
similarity index 68%
rename from lib/rules/prefer-message-ids.js
rename to lib/rules/prefer-message-ids.ts
index cdf66642..1d1ccedb 100644
--- a/lib/rules/prefer-message-ids.js
+++ b/lib/rules/prefer-message-ids.ts
@@ -1,14 +1,19 @@
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { Node } from 'estree';
-const utils = require('../utils');
-const { getStaticValue } = require('@eslint-community/eslint-utils');
+import {
+ collectReportViolationAndSuggestionData,
+ getContextIdentifiers,
+ getKeyName,
+ getReportInfo,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -18,7 +23,7 @@ module.exports = {
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:
@@ -29,13 +34,13 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
- const ruleInfo = utils.getRuleInfo(sourceCode);
+ const sourceCode = context.sourceCode;
+ const ruleInfo = getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}
- let contextIdentifiers;
+ let contextIdentifiers: Set;
// ----------------------------------------------------------------------
// Public
@@ -43,8 +48,8 @@ module.exports = {
return {
Program(ast) {
- const scope = utils.getScope(context);
- contextIdentifiers = utils.getContextIdentifiers(
+ const scope = sourceCode.getScope(ast);
+ contextIdentifiers = getContextIdentifiers(
sourceCode.scopeManager,
ast,
);
@@ -52,10 +57,11 @@ module.exports = {
const metaNode = ruleInfo.meta;
const messagesNode =
metaNode &&
+ metaNode.type === 'ObjectExpression' &&
metaNode.properties &&
- metaNode.properties.find(
- (p) => p.type === 'Property' && utils.getKeyName(p) === 'messages',
- );
+ metaNode.properties
+ .filter((p) => p.type === 'Property')
+ .find((p) => getKeyName(p) === 'messages');
if (!messagesNode) {
context.report({
@@ -71,6 +77,7 @@ module.exports = {
}
if (
+ staticValue.value &&
typeof staticValue.value === 'object' &&
staticValue.value.constructor === Object &&
Object.keys(staticValue.value).length === 0
@@ -88,15 +95,17 @@ module.exports = {
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);
- 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',
@@ -107,3 +116,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.ts
similarity index 83%
rename from lib/rules/prefer-object-rule.js
rename to lib/rules/prefer-object-rule.ts
index 1b0e98f1..d07d7cf2 100644
--- a/lib/rules/prefer-object-rule.js
+++ b/lib/rules/prefer-object-rule.ts
@@ -1,17 +1,14 @@
/**
* @author Brad Zacher
*/
+import type { Rule } from 'eslint';
-'use strict';
-
-const utils = require('../utils');
+import { getRuleInfo } from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -28,12 +25,8 @@ module.exports = {
},
create(context) {
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
-
- const sourceCode = utils.getSourceCode(context);
- const ruleInfo = utils.getRuleInfo(sourceCode);
+ const sourceCode = context.sourceCode;
+ const ruleInfo = getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}
@@ -50,7 +43,6 @@ module.exports = {
*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'
@@ -61,7 +53,7 @@ module.exports = {
);
/* istanbul ignore if */
- if (!openParenToken) {
+ if (!openParenToken || !ruleInfo.create.range) {
// this shouldn't happen, but guarding against crashes just in case
return null;
}
@@ -81,3 +73,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js
deleted file mode 100644
index 4d9e90e5..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 薛定谔的猫
- */
-
-'use strict';
-
-const utils = require('../utils');
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
- 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 = utils.getSourceCode(context);
-
- return {
- Program(ast) {
- utils.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) {
- if (test.type === 'ObjectExpression') {
- return test.properties.find(
- (item) => item.type === 'Property' && item.key.name === key,
- );
- }
- return null;
- }
-
- const code = getTestInfo('code');
- const output = getTestInfo('output');
-
- if (
- output &&
- sourceCode.getText(output.value) ===
- sourceCode.getText(code.value)
- ) {
- context.report({
- node: output,
- messageId: 'useOutputNull',
- fix: (fixer) => fixer.replaceText(output.value, 'null'),
- });
- }
- });
- });
- },
- };
- },
-};
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 72%
rename from lib/rules/prefer-placeholders.js
rename to lib/rules/prefer-placeholders.ts
index f5c88fc1..d8a9d217 100644
--- a/lib/rules/prefer-placeholders.js
+++ b/lib/rules/prefer-placeholders.ts
@@ -3,17 +3,20 @@
* @author Teddy Katz
*/
-'use strict';
+import { findVariable } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { Node } from 'estree';
-const utils = require('../utils');
-const { findVariable } = require('@eslint-community/eslint-utils');
+import {
+ collectReportViolationAndSuggestionData,
+ getContextIdentifiers,
+ getReportInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -22,7 +25,7 @@ module.exports = {
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:
@@ -31,18 +34,14 @@ module.exports = {
},
create(context) {
- let contextIdentifiers;
+ let contextIdentifiers = new Set();
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
const { scopeManager } = sourceCode;
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
-
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
+ contextIdentifiers = getContextIdentifiers(scopeManager, ast);
},
CallExpression(node) {
if (
@@ -51,21 +50,23 @@ module.exports = {
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);
- 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,
);
@@ -100,3 +101,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.ts
similarity index 69%
rename from lib/rules/prefer-replace-text.js
rename to lib/rules/prefer-replace-text.ts
index 173cc509..1a96d65e 100644
--- a/lib/rules/prefer-replace-text.js
+++ b/lib/rules/prefer-replace-text.ts
@@ -2,17 +2,27 @@
* @fileoverview prefer using `replaceText()` instead of `replaceTextRange()`
* @author 薛定谔的猫
*/
+import type { Rule } from 'eslint';
+import type { Identifier, Node } from 'estree';
-'use strict';
+import type { FunctionInfo } from '../types.js';
+import {
+ getContextIdentifiers,
+ isAutoFixerFunction,
+ isSuggestionFixerFunction,
+} from '../utils.js';
-const utils = require('../utils');
+const DEFAULT_FUNC_INFO: FunctionInfo = {
+ upper: null,
+ codePath: null,
+ shouldCheck: false,
+ node: null,
+};
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -22,7 +32,7 @@ module.exports = {
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.',
@@ -30,38 +40,33 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
- let funcInfo = {
- upper: null,
- codePath: null,
- shouldCheck: false,
- node: null,
- };
- let contextIdentifiers;
+ const sourceCode = context.sourceCode;
+ let funcInfo = DEFAULT_FUNC_INFO;
+ let contextIdentifiers: Set;
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(
+ contextIdentifiers = getContextIdentifiers(
sourceCode.scopeManager,
ast,
);
},
// Stacks this function's information.
- onCodePathStart(codePath, node) {
+ onCodePathStart(codePath: Rule.CodePath, node: Node) {
funcInfo = {
upper: funcInfo,
codePath,
shouldCheck:
- utils.isAutoFixerFunction(node, contextIdentifiers) ||
- utils.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.
@@ -89,3 +94,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.ts
similarity index 67%
rename from lib/rules/report-message-format.js
rename to lib/rules/report-message-format.ts
index 4fce24b8..4ba7506d 100644
--- a/lib/rules/report-message-format.js
+++ b/lib/rules/report-message-format.ts
@@ -2,18 +2,21 @@
* @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';
-'use strict';
-
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
+import {
+ getContextIdentifiers,
+ getKeyName,
+ getReportInfo,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -22,7 +25,7 @@ module.exports = {
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.',
@@ -37,14 +40,16 @@ module.exports = {
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' &&
@@ -52,8 +57,10 @@ module.exports = {
!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,
@@ -63,20 +70,16 @@ module.exports = {
}
}
- const sourceCode = utils.getSourceCode(context);
- const ruleInfo = utils.getRuleInfo(sourceCode);
+ const sourceCode = context.sourceCode;
+ const ruleInfo = getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}
- // ----------------------------------------------------------------------
- // Public
- // ----------------------------------------------------------------------
-
return {
Program(ast) {
- const scope = utils.getScope(context);
- contextIdentifiers = utils.getContextIdentifiers(
+ const scope = sourceCode.getScope(ast);
+ contextIdentifiers = getContextIdentifiers(
sourceCode.scopeManager,
ast,
);
@@ -85,10 +88,9 @@ module.exports = {
ruleInfo &&
ruleInfo.meta &&
ruleInfo.meta.type === 'ObjectExpression' &&
- ruleInfo.meta.properties.find(
- (prop) =>
- prop.type === 'Property' && utils.getKeyName(prop) === 'messages',
- );
+ ruleInfo.meta.properties
+ .filter((prop) => prop.type === 'Property')
+ .find((prop) => getKeyName(prop) === 'messages');
if (
!messagesObject ||
@@ -103,14 +105,14 @@ module.exports = {
.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;
@@ -121,13 +123,12 @@ module.exports = {
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));
@@ -137,3 +138,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-default-options.js b/lib/rules/require-meta-default-options.ts
similarity index 62%
rename from lib/rules/require-meta-default-options.js
rename to lib/rules/require-meta-default-options.ts
index 9ac37575..a8bc4374 100644
--- a/lib/rules/require-meta-default-options.js
+++ b/lib/rules/require-meta-default-options.ts
@@ -1,16 +1,21 @@
-'use strict';
+import type { Rule } from 'eslint';
-const utils = require('../utils');
+import {
+ evaluateObjectProperties,
+ getKeyName,
+ getMetaSchemaNode,
+ getMetaSchemaNodeProperty,
+ getRuleInfo,
+} from '../utils.js';
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
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',
@@ -26,30 +31,24 @@ module.exports = {
},
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)
+ .filter((p) => p.type === 'Property')
+ .find((p) => getKeyName(p) === 'defaultOptions');
if (
schemaProperty.type === 'ArrayExpression' &&
@@ -68,13 +67,17 @@ module.exports = {
}
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 {};
}
@@ -88,8 +91,11 @@ module.exports = {
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({
@@ -102,3 +108,5 @@ module.exports = {
return {};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.ts
similarity index 79%
rename from lib/rules/require-meta-docs-description.js
rename to lib/rules/require-meta-docs-description.ts
index b3cf5272..dd0af4eb 100644
--- a/lib/rules/require-meta-docs-description.js
+++ b/lib/rules/require-meta-docs-description.ts
@@ -1,16 +1,11 @@
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
+import { getMetaDocsProperty, getRuleInfo } from '../utils.js';
const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)');
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -20,7 +15,7 @@ module.exports = {
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',
@@ -46,22 +41,22 @@ module.exports = {
},
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({
@@ -95,10 +90,12 @@ module.exports = {
context.report({
node: descriptionNode.value,
messageId: 'mismatch',
- data: { pattern },
+ data: { pattern: String(pattern) },
});
}
},
};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-docs-recommended.js b/lib/rules/require-meta-docs-recommended.ts
similarity index 68%
rename from lib/rules/require-meta-docs-recommended.js
rename to lib/rules/require-meta-docs-recommended.ts
index 67851ffc..ae3f7d8e 100644
--- a/lib/rules/require-meta-docs-recommended.js
+++ b/lib/rules/require-meta-docs-recommended.ts
@@ -1,28 +1,38 @@
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { ObjectExpression } from 'estree';
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
+import {
+ getMetaDocsProperty,
+ getRuleInfo,
+ 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} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -32,7 +42,7 @@ module.exports = {
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: [
{
@@ -57,8 +67,8 @@ module.exports = {
},
create(context) {
- const sourceCode = utils.getSourceCode(context);
- const ruleInfo = utils.getRuleInfo(sourceCode);
+ const sourceCode = context.sourceCode;
+ const ruleInfo = getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}
@@ -68,21 +78,22 @@ module.exports = {
docsNode,
metaNode,
metaPropertyNode: descriptionNode,
- } = utils.getMetaDocsProperty('recommended', ruleInfo, scopeManager);
+ } = 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),
},
]
: [];
@@ -99,7 +110,7 @@ module.exports = {
return {};
}
- const staticValue = utils.isUndefinedIdentifier(descriptionNode.value)
+ const staticValue = isUndefinedIdentifier(descriptionNode.value)
? { value: undefined }
: getStaticValue(descriptionNode.value);
@@ -124,3 +135,5 @@ module.exports = {
return {};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.ts
similarity index 77%
rename from lib/rules/require-meta-docs-url.js
rename to lib/rules/require-meta-docs-url.ts
index cb3e35d3..ae09e8f1 100644
--- a/lib/rules/require-meta-docs-url.js
+++ b/lib/rules/require-meta-docs-url.ts
@@ -1,23 +1,23 @@
/**
* @author Toru Nagashima
*/
+import path from 'node:path';
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
-// -----------------------------------------------------------------------------
-// Requirements
-// -----------------------------------------------------------------------------
-
-const path = require('path');
-const utils = require('../utils');
-const { getStaticValue } = require('@eslint-community/eslint-utils');
+import {
+ getMetaDocsProperty,
+ getRuleInfo,
+ insertProperty,
+ isUndefinedIdentifier,
+} from '../utils.js';
// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -50,12 +50,12 @@ module.exports = {
/**
* 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] || {};
- const filename = utils.getFilename(context);
+ const filename = context.filename;
const ruleName =
filename === ''
? undefined
@@ -67,32 +67,32 @@ module.exports = {
/**
* 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),
);
}
- 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)
@@ -102,7 +102,11 @@ module.exports = {
return;
}
- if (isExpectedUrl(staticValue && staticValue.value)) {
+ if (
+ staticValue &&
+ typeof staticValue.value === 'string' &&
+ isExpectedUrl(staticValue.value)
+ ) {
return;
}
@@ -134,12 +138,12 @@ module.exports = {
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}`,
@@ -150,7 +154,7 @@ module.exports = {
metaNode &&
metaNode.type === 'ObjectExpression'
) {
- return utils.insertProperty(
+ return insertProperty(
fixer,
metaNode,
`docs: {\nurl: ${urlString}\n}`,
@@ -165,3 +169,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.ts
similarity index 74%
rename from lib/rules/require-meta-fixable.js
rename to lib/rules/require-meta-fixable.ts
index e69ca4fc..d3d23bf7 100644
--- a/lib/rules/require-meta-fixable.js
+++ b/lib/rules/require-meta-fixable.ts
@@ -2,18 +2,21 @@
* @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';
-'use strict';
-
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
+import {
+ evaluateObjectProperties,
+ getContextIdentifiers,
+ getKeyName,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -50,11 +53,11 @@ module.exports = {
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);
- let contextIdentifiers;
- let usesFixFunctions;
+ const ruleInfo = getRuleInfo(sourceCode);
+ let contextIdentifiers: Set;
+ let usesFixFunctions = false;
if (!ruleInfo) {
return {};
@@ -62,7 +65,7 @@ module.exports = {
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
+ contextIdentifiers = getContextIdentifiers(scopeManager, ast);
},
CallExpression(node) {
if (
@@ -72,20 +75,20 @@ module.exports = {
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)
+ .filter((prop) => prop.type === 'Property')
+ .find((prop) => getKeyName(prop) === 'fixable');
if (metaFixableProp) {
const staticValue = getStaticValue(metaFixableProp.value, scope);
@@ -95,7 +98,9 @@ module.exports = {
}
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({
@@ -107,7 +112,8 @@ module.exports = {
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({
@@ -117,6 +123,7 @@ module.exports = {
} else if (
catchNoFixerButFixableProperty &&
!usesFixFunctions &&
+ typeof staticValue.value === 'string' &&
['code', 'whitespace'].includes(staticValue.value)
) {
// Rule is NOT fixable but `fixable` property has a fixable value.
@@ -136,3 +143,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.ts
similarity index 76%
rename from lib/rules/require-meta-has-suggestions.js
rename to lib/rules/require-meta-has-suggestions.ts
index c8f75b3f..c031bfcd 100644
--- a/lib/rules/require-meta-has-suggestions.js
+++ b/lib/rules/require-meta-has-suggestions.ts
@@ -1,14 +1,19 @@
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { Node, Property } from 'estree';
-const utils = require('../utils');
-const { getStaticValue } = require('@eslint-community/eslint-utils');
+import {
+ evaluateObjectProperties,
+ getContextIdentifiers,
+ getKeyName,
+ getRuleInfo,
+ isUndefinedIdentifier,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -29,22 +34,22 @@ module.exports = {
},
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 {};
}
- 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) {
- const scope = utils.getScope(context);
+ function doesPropertyContainSuggestions(node: Property): boolean {
+ const scope = sourceCode.getScope(node);
const staticValue = getStaticValue(node.value, scope);
if (
!staticValue ||
@@ -64,7 +69,7 @@ module.exports = {
return {
Program(ast) {
- contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
+ contextIdentifiers = getContextIdentifiers(scopeManager, ast);
},
CallExpression(node) {
if (
@@ -76,9 +81,12 @@ module.exports = {
(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,
+ )
+ .filter((prop) => prop.type === 'Property')
+ .find((prop) => getKeyName(prop) === 'suggest');
if (suggestProp && doesPropertyContainSuggestions(suggestProp)) {
ruleReportsSuggestions = true;
}
@@ -95,12 +103,15 @@ module.exports = {
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,
+ )
+ .filter((prop) => prop.type === 'Property')
+ .find((prop) => getKeyName(prop) === 'hasSuggestions');
const hasSuggestionsStaticValue =
hasSuggestionsProperty &&
getStaticValue(hasSuggestionsProperty.value, scope);
@@ -126,6 +137,7 @@ module.exports = {
'hasSuggestions: true, ',
);
}
+ return null;
},
});
} else if (
@@ -139,13 +151,14 @@ module.exports = {
fix(fixer) {
if (
hasSuggestionsProperty.value.type === 'Literal' ||
- utils.isUndefinedIdentifier(hasSuggestionsProperty.value)
+ isUndefinedIdentifier(hasSuggestionsProperty.value)
) {
return fixer.replaceText(
hasSuggestionsProperty.value,
'true',
);
}
+ return null;
},
});
}
@@ -165,3 +178,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-schema-description.js b/lib/rules/require-meta-schema-description.ts
similarity index 65%
rename from lib/rules/require-meta-schema-description.js
rename to lib/rules/require-meta-schema-description.ts
index 842d576f..e4167bca 100644
--- a/lib/rules/require-meta-schema-description.js
+++ b/lib/rules/require-meta-schema-description.ts
@@ -1,21 +1,24 @@
-'use strict';
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
+import type { Expression, SpreadElement } from 'estree';
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
+import {
+ getMetaSchemaNode,
+ getMetaSchemaNodeProperty,
+ getRuleInfo,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
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: [],
@@ -25,22 +28,19 @@ module.exports = {
},
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 {};
}
@@ -51,15 +51,20 @@ module.exports = {
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 @@ module.exports = {
continue;
}
+ // @ts-expect-error
switch (key.name ?? key.value) {
case 'description': {
hadDescription = true;
@@ -90,9 +96,12 @@ module.exports = {
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);
}
}
@@ -112,3 +121,5 @@ module.exports = {
}
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.ts
similarity index 83%
rename from lib/rules/require-meta-schema.js
rename to lib/rules/require-meta-schema.ts
index 59cf4575..909fdd58 100644
--- a/lib/rules/require-meta-schema.js
+++ b/lib/rules/require-meta-schema.ts
@@ -1,13 +1,19 @@
-'use strict';
+import type { Rule } from 'eslint';
+import type { Node } from 'estree';
-const utils = require('../utils');
+import {
+ getContextIdentifiers,
+ getMetaSchemaNode,
+ getMetaSchemaNodeProperty,
+ getRuleInfo,
+ insertProperty,
+ isUndefinedIdentifier,
+} from '../utils.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -43,14 +49,14 @@ module.exports = {
},
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 {};
}
- let contextIdentifiers;
+ let contextIdentifiers: Set;
const metaNode = ruleInfo.meta;
// Options
@@ -61,15 +67,12 @@ module.exports = {
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;
@@ -87,7 +90,7 @@ module.exports = {
if (
schemaProperty.type === 'Literal' ||
- utils.isUndefinedIdentifier(schemaProperty)
+ isUndefinedIdentifier(schemaProperty)
) {
context.report({ node: schemaProperty, messageId: 'wrongType' });
}
@@ -106,7 +109,7 @@ module.exports = {
{
messageId: 'addEmptySchema',
fix(fixer) {
- return utils.insertProperty(
+ return insertProperty(
fixer,
metaNode,
'schema: []',
@@ -139,3 +142,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.ts
similarity index 67%
rename from lib/rules/require-meta-type.js
rename to lib/rules/require-meta-type.ts
index a6aec027..7001936b 100644
--- a/lib/rules/require-meta-type.js
+++ b/lib/rules/require-meta-type.ts
@@ -2,19 +2,17 @@
* @fileoverview require rules to implement a `meta.type` property
* @author 薛定谔的猫
*/
+import { getStaticValue } from '@eslint-community/eslint-utils';
+import type { Rule } from 'eslint';
-'use strict';
+import { evaluateObjectProperties, getKeyName, getRuleInfo } from '../utils.js';
-const { getStaticValue } = require('@eslint-community/eslint-utils');
-const utils = require('../utils');
const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']);
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'problem',
docs: {
@@ -23,7 +21,7 @@ module.exports = {
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,25 +32,21 @@ module.exports = {
},
create(context) {
- // ----------------------------------------------------------------------
- // 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)
+ .filter((p) => p.type === 'Property')
+ .find((p) => getKeyName(p) === 'type');
if (!typeNode) {
context.report({
@@ -68,10 +62,15 @@ module.exports = {
return;
}
- if (!VALID_TYPES.has(staticValue.value)) {
+ if (
+ typeof staticValue.value !== 'string' ||
+ !VALID_TYPES.has(staticValue.value)
+ ) {
context.report({ node: typeNode.value, messageId: 'unexpected' });
}
},
};
},
};
+
+export default rule;
diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.ts
similarity index 80%
rename from lib/rules/test-case-property-ordering.js
rename to lib/rules/test-case-property-ordering.ts
index f6b4dd2f..50eee786 100644
--- a/lib/rules/test-case-property-ordering.js
+++ b/lib/rules/test-case-property-ordering.ts
@@ -2,10 +2,9 @@
* @fileoverview Requires the properties of a test case to be placed in a consistent order.
* @author 薛定谔的猫
*/
+import type { Rule } from 'eslint';
-'use strict';
-
-const utils = require('../utils');
+import { getKeyName, getTestInfo } from '../utils.js';
const defaultOrder = [
'filename',
@@ -20,12 +19,13 @@ const defaultOrder = [
'errors',
];
+const keyNameMapper = (property: Parameters[0]) =>
+ getKeyName(property);
+
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -54,18 +54,22 @@ module.exports = {
// ----------------------------------------------------------------------
// Public
// ----------------------------------------------------------------------
- const order = context.options[0] || defaultOrder;
- const sourceCode = utils.getSourceCode(context);
+ const order: string[] = context.options[0] || defaultOrder;
+ 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 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;
@@ -109,3 +113,5 @@ module.exports = {
};
},
};
+
+export default rule;
diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.ts
similarity index 56%
rename from lib/rules/test-case-shorthand-strings.js
rename to lib/rules/test-case-shorthand-strings.ts
index 0cf22333..64352316 100644
--- a/lib/rules/test-case-shorthand-strings.js
+++ b/lib/rules/test-case-shorthand-strings.ts
@@ -2,17 +2,15 @@
* @fileoverview Enforce consistent usage of shorthand strings for test cases with no options
* @author Teddy Katz
*/
+import type { Rule } from 'eslint';
-'use strict';
-
-const utils = require('../utils');
+import { getKeyName, getTestInfo } from '../utils.js';
+import type { TestInfo } from '../types.js';
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
-
-/** @type {import('eslint').Rule.RuleModule} */
-module.exports = {
+const rule: Rule.RuleModule = {
meta: {
type: 'suggestion',
docs: {
@@ -39,7 +37,7 @@ module.exports = {
create(context) {
const shorthandOption = context.options[0] || 'as-needed';
- const sourceCode = utils.getSourceCode(context);
+ const sourceCode = context.sourceCode;
// ----------------------------------------------------------------------
// Helpers
@@ -47,11 +45,11 @@ module.exports = {
/**
* 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' ||
@@ -65,13 +63,13 @@ module.exports = {
shorthand: false,
needsLongform: !(
testCase.properties.length === 1 &&
- utils.getKeyName(testCase.properties[0]) === 'code'
+ getKeyName(testCase.properties[0]) === 'code'
),
};
}
return null;
})
- .filter(Boolean);
+ .filter((testCase) => !!testCase);
const isConsistent =
new Set(caseInfoList.map((caseInfo) => caseInfo.shorthand)).size <= 1;
@@ -79,37 +77,47 @@ module.exports = {
(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),
+ );
+ },
});
+ });
}
// ----------------------------------------------------------------------
@@ -118,8 +126,7 @@ module.exports = {
return {
Program(ast) {
- utils
- .getTestInfo(context, ast)
+ getTestInfo(context, ast)
.map((testRun) => testRun.valid)
.filter(Boolean)
.forEach(reportTestCases);
@@ -127,3 +134,5 @@ module.exports = {
};
},
};
+
+export default rule;
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.js
deleted file mode 100644
index bc15d2c0..00000000
--- a/lib/utils.js
+++ /dev/null
@@ -1,1027 +0,0 @@
-'use strict';
-
-const {
- getStaticValue,
- findVariable,
-} = require('@eslint-community/eslint-utils');
-const estraverse = require('estraverse');
-
-const functionTypes = new Set([
- 'FunctionExpression',
- 'ArrowFunctionExpression',
- 'FunctionDeclaration',
-]);
-
-/**
- * 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
- */
-function isNormalFunctionExpression(node) {
- return functionTypes.has(node.type) && !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
- */
-function isRuleTesterConstruction(node) {
- return (
- node.type === 'NewExpression' &&
- ((node.callee.type === 'Identifier' && node.callee.name === 'RuleTester') ||
- (node.callee.type === 'MemberExpression' &&
- node.callee.property.type === 'Identifier' &&
- node.callee.property.name === 'RuleTester'))
- );
-}
-
-const INTERESTING_RULE_KEYS = new Set(['create', 'meta']);
-
-/**
- * Collect properties from an object that have interesting key names into a new object
- * @param {Node[]} properties
- * @param {Set} interestingKeys
- * @returns Object
- */
-function collectInterestingProperties(properties, interestingKeys) {
- return properties.reduce((parsedProps, prop) => {
- const keyValue = module.exports.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;
- }, {});
-}
-
-/**
- * Check if there is a return statement that returns an object somewhere inside the given node.
- * @param {Node} node
- * @returns {boolean}
- */
-function hasObjectReturn(node) {
- let foundMatch = false;
- estraverse.traverse(node, {
- enter(child) {
- if (
- child.type === 'ReturnStatement' &&
- child.argument &&
- child.argument.type === 'ObjectExpression'
- ) {
- foundMatch = true;
- }
- },
- fallback: 'iteration', // Don't crash on unexpected node types.
- });
- return foundMatch;
-}
-
-/**
- * Determine if the given node is likely to be a function-style rule.
- * @param {*} node
- * @returns {boolean}
- */
-function isFunctionRule(node) {
- return (
- 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.
- );
-}
-
-/**
- * Check if the given node is a function call representing a known TypeScript rule creator format.
- * @param {Node} node
- * @returns {boolean}
- */
-function isTypeScriptRuleHelper(node) {
- return (
- node.type === 'CallExpression' &&
- node.arguments.length === 1 &&
- node.arguments[0].type === 'ObjectExpression' &&
- // Check various TypeScript rule helper formats.
- // createESLintRule({ ... })
- (node.callee.type === 'Identifier' ||
- // util.createRule({ ... })
- (node.callee.type === 'MemberExpression' &&
- node.callee.object.type === 'Identifier' &&
- node.callee.property.type === 'Identifier') ||
- // ESLintUtils.RuleCreator(docsUrl)({ ... })
- (node.callee.type === 'CallExpression' &&
- node.callee.callee.type === 'MemberExpression' &&
- node.callee.callee.object.type === 'Identifier' &&
- node.callee.callee.property.type === 'Identifier'))
- );
-}
-
-/**
- * Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
- */
-function getRuleExportsESM(ast, scopeManager) {
- const possibleNodes = [];
-
- for (const statement of ast.body) {
- switch (statement.type) {
- // export default rule;
- case 'ExportDefaultDeclaration': {
- possibleNodes.push(statement.declaration);
- break;
- }
- // export = rule;
- case 'TSExportAssignment': {
- possibleNodes.push(statement.expression);
- break;
- }
- // export const rule = { ... };
- // or export {rule};
- case 'ExportNamedDeclaration': {
- for (const specifier of statement.specifiers) {
- possibleNodes.push(specifier.local);
- }
- if (statement.declaration) {
- const nodes =
- statement.declaration.type === 'VariableDeclaration'
- ? statement.declaration.declarations.map(
- (declarator) => declarator.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)),
- );
- }
- break;
- }
- }
- }
-
- return possibleNodes.reduce((currentExports, node) => {
- if (node.type === 'ObjectExpression') {
- // Check `export default { create() {}, meta: {} }`
- return collectInterestingProperties(
- node.properties,
- INTERESTING_RULE_KEYS,
- );
- } else if (isFunctionRule(node)) {
- // Check `export default function(context) { return { ... }; }`
- return { create: node, meta: null, isNewStyle: false };
- } else if (isTypeScriptRuleHelper(node)) {
- // Check `export default someTypeScriptHelper({ create() {}, meta: {} });
- return collectInterestingProperties(
- node.arguments[0].properties,
- INTERESTING_RULE_KEYS,
- );
- } else if (node.type === 'Identifier') {
- // Rule could be stored in a variable before being exported.
- const possibleRule = findVariableValue(node, scopeManager);
- if (possibleRule) {
- if (possibleRule.type === 'ObjectExpression') {
- // Check `const possibleRule = { ... }; export default possibleRule;
- return collectInterestingProperties(
- possibleRule.properties,
- INTERESTING_RULE_KEYS,
- );
- } else if (isFunctionRule(possibleRule)) {
- // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
- return { create: possibleRule, meta: null, isNewStyle: false };
- } else if (isTypeScriptRuleHelper(possibleRule)) {
- // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
- return collectInterestingProperties(
- possibleRule.arguments[0].properties,
- INTERESTING_RULE_KEYS,
- );
- }
- }
- }
- return currentExports;
- }, {});
-}
-
-/**
- * Helper for `getRuleInfo`. Handles CJS rules.
- */
-function getRuleExportsCJS(ast, scopeManager) {
- let exportsVarOverridden = false;
- let exportsIsFunction = false;
- return ast.body
- .filter((statement) => statement.type === 'ExpressionStatement')
- .map((statement) => statement.expression)
- .filter((expression) => expression.type === 'AssignmentExpression')
- .filter((expression) => expression.left.type === 'MemberExpression')
-
- .reduce((currentExports, node) => {
- if (
- node.left.object.type === 'Identifier' &&
- node.left.object.name === 'module' &&
- node.left.property.type === 'Identifier' &&
- node.left.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 };
- } else if (node.right.type === 'ObjectExpression') {
- // Check `module.exports = { create: function () {}, meta: {} }`
-
- return collectInterestingProperties(
- node.right.properties,
- INTERESTING_RULE_KEYS,
- );
- } else if (node.right.type === 'Identifier') {
- // Rule could be stored in a variable before being exported.
- const possibleRule = findVariableValue(node.right, scopeManager);
- if (possibleRule) {
- if (possibleRule.type === 'ObjectExpression') {
- // Check `const possibleRule = { ... }; module.exports = possibleRule;
- return collectInterestingProperties(
- possibleRule.properties,
- INTERESTING_RULE_KEYS,
- );
- } else if (isFunctionRule(possibleRule)) {
- // Check `const possibleRule = function(context) { return { ... } }; module.exports = possibleRule;`
- return { create: possibleRule, meta: null, 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)
- ) {
- // Check `module.exports.create = () => {}`
-
- currentExports[node.left.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)
- ) {
- // Check `exports.create = () => {}`
-
- currentExports[node.left.property.name] = node.right;
- }
- return currentExports;
- }, {});
-}
-
-/**
- * Find the value of a property in an object by its property key name.
- * @param {Object} obj
- * @param {String} keyName
- * @returns property value
- */
-function findObjectPropertyValueByKeyName(obj, keyName) {
- const property = obj.properties.find(
- (prop) => prop.key.type === 'Identifier' && prop.key.name === keyName,
- );
- 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
- * @returns the first value (or function) that the given variable is initialized to.
- */
-function findVariableValue(node, scopeManager) {
- const variable = findVariable(
- 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
- ) {
- // Given node `x`, get `123` from `const x = 123;`.
- return variable.defs[0].node.init;
- } else if (variable.defs[0].node.type === 'FunctionDeclaration') {
- // Given node `foo`, get `function foo() {}` from `function foo() {}`.
- return variable.defs[0].node;
- }
- }
-}
-
-/**
- * Retrieve all possible elements from an array.
- * 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
- */
-function collectArrayElements(node) {
- if (!node) {
- return [];
- }
- if (node.type === 'ArrayExpression') {
- return node.elements;
- }
- if (node.type === 'ConditionalExpression') {
- return [
- ...collectArrayElements(node.consequent),
- ...collectArrayElements(node.alternate),
- ];
- }
- 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;
- }
-
- // 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;
- }
-
- 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 });
-
- 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 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;
- }
-
- return [node.body];
- }
-
- 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(),
- ) {
- 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;
- }
-
- const extracted = module.exports.extractFunctionBody(declarator.init);
-
- runCalls.push(
- ...module.exports.checkStatementsForTestInfo(
- context,
- 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 === 'FunctionDeclaration') {
- runCalls.push(
- ...module.exports.checkStatementsForTestInfo(
- context,
- statement.body.body,
- variableIdentifiers,
- ),
- );
- }
-
- if (statement.type === 'IfStatement') {
- const body =
- statement.consequent.type === 'BlockStatement'
- ? statement.consequent.body
- : [statement.consequent];
-
- runCalls.push(
- ...module.exports.checkStatementsForTestInfo(
- context,
- body,
- variableIdentifiers,
- ),
- );
-
- continue;
- }
-
- const expression =
- statement.type === 'ExpressionStatement'
- ? statement.expression
- : statement;
-
- if (expression.type !== 'CallExpression') {
- continue;
- }
-
- for (const arg of expression.arguments) {
- const extracted = module.exports.extractFunctionBody(arg);
-
- runCalls.push(
- ...module.exports.checkStatementsForTestInfo(
- context,
- extracted,
- variableIdentifiers,
- ),
- );
- }
-
- 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
- */
- getTestInfo(context, ast) {
- const runCalls = module.exports.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) => module.exports.getKeyName(prop) === 'valid',
- );
- const invalidProperty = run.properties.find(
- (prop) => module.exports.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
- */
- 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 = module.exports.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);
-
- 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.
- */
- 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}`);
- }
- 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 [];
- }
-
- 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];
- });
- },
-
- getMetaDocsProperty(propertyName, ruleInfo, scopeManager) {
- const metaNode = ruleInfo.meta;
-
- const docsNode = module.exports
- .evaluateObjectProperties(metaNode, scopeManager)
- .find(
- (p) => p.type === 'Property' && module.exports.getKeyName(p) === 'docs',
- );
-
- const metaPropertyNode = module.exports
- .evaluateObjectProperties(docsNode?.value, scopeManager)
- .find(
- (p) =>
- p.type === 'Property' &&
- module.exports.getKeyName(p) === propertyName,
- );
-
- 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) {
- return;
- }
-
- const metaNode = ruleInfo.meta;
- const messagesNode = module.exports
- .evaluateObjectProperties(metaNode, scopeManager)
- .find(
- (p) =>
- p.type === 'Property' && module.exports.getKeyName(p) === 'messages',
- );
-
- 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',
- );
- },
-
- getMetaSchemaNodeProperty(schemaNode, scopeManager) {
- if (!schemaNode) {
- return null;
- }
-
- 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;
- }
-
- value = variable.defs[0].node.init;
- }
-
- return value;
- },
-
- /**
- * 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,
- );
-
- 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();
- },
-};
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 00000000..75355270
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,1135 @@
+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 node The node in question
+ * @returns `true` if the node is a normal function expression
+ */
+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 `true` if the node is probably constructing a RuleTester instance
+ */
+function isRuleTesterConstruction(node: Expression | Super): boolean {
+ return (
+ node.type === 'NewExpression' &&
+ ((node.callee.type === 'Identifier' && node.callee.name === 'RuleTester') ||
+ (node.callee.type === 'MemberExpression' &&
+ node.callee.property.type === 'Identifier' &&
+ node.callee.property.name === 'RuleTester'))
+ );
+}
+
+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 properties
+ * @param interestingKeys
+ */
+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.
+ */
+function hasObjectReturn(node: Node): boolean {
+ let foundMatch = false;
+ estraverse.traverse(node, {
+ enter(child) {
+ if (
+ child.type === 'ReturnStatement' &&
+ child.argument &&
+ child.argument.type === 'ObjectExpression'
+ ) {
+ foundMatch = true;
+ }
+ },
+ fallback: 'iteration', // Don't crash on unexpected node types.
+ });
+ return foundMatch;
+}
+
+/**
+ * Determine if the given node is likely to be a function-style rule.
+ * @param 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.
+ );
+}
+
+/**
+ * Check if the given node is a function call representing a known TypeScript rule creator format.
+ */
+function isTypeScriptRuleHelper(
+ node: Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration,
+): node is CallExpression & { arguments: ObjectExpression[] } {
+ return (
+ node.type === 'CallExpression' &&
+ node.arguments.length === 1 &&
+ node.arguments[0].type === 'ObjectExpression' &&
+ // Check various TypeScript rule helper formats.
+ // createESLintRule({ ... })
+ (node.callee.type === 'Identifier' ||
+ // util.createRule({ ... })
+ (node.callee.type === 'MemberExpression' &&
+ node.callee.object.type === 'Identifier' &&
+ node.callee.property.type === 'Identifier') ||
+ // ESLintUtils.RuleCreator(docsUrl)({ ... })
+ (node.callee.type === 'CallExpression' &&
+ node.callee.callee.type === 'MemberExpression' &&
+ node.callee.callee.object.type === 'Identifier' &&
+ node.callee.callee.property.type === 'Identifier'))
+ );
+}
+
+/**
+ * Helper for `getRuleInfo`. Handles ESM and TypeScript rules.
+ */
+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) {
+ // export default rule;
+ case 'ExportDefaultDeclaration': {
+ possibleNodes.push(statement.declaration);
+ break;
+ }
+ // export = rule;
+ case 'TSExportAssignment': {
+ possibleNodes.push(statement.expression);
+ break;
+ }
+ // export const rule = { ... };
+ // or export {rule};
+ case 'ExportNamedDeclaration': {
+ for (const specifier of statement.specifiers) {
+ possibleNodes.push(specifier.local);
+ }
+ if (statement.declaration) {
+ const nodes =
+ statement.declaration.type === 'VariableDeclaration'
+ ? 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 && !isFunctionType(node)),
+ );
+ }
+ break;
+ }
+ }
+ }
+
+ return possibleNodes.reduce((currentExports, node) => {
+ if (node.type === 'ObjectExpression') {
+ // Check `export default { create() {}, meta: {} }`
+ return collectInterestingProperties(
+ node.properties,
+ INTERESTING_RULE_KEYS,
+ );
+ } else if (isFunctionRule(node)) {
+ // Check `export default function(context) { return { ... }; }`
+ return { create: node, meta: undefined, isNewStyle: false };
+ } else if (isTypeScriptRuleHelper(node)) {
+ // Check `export default someTypeScriptHelper({ create() {}, meta: {} });
+ return collectInterestingProperties(
+ node.arguments[0].properties,
+ INTERESTING_RULE_KEYS,
+ );
+ } else if (node.type === 'Identifier') {
+ // Rule could be stored in a variable before being exported.
+ const possibleRule = findVariableValue(node, scopeManager);
+ if (possibleRule) {
+ if (possibleRule.type === 'ObjectExpression') {
+ // Check `const possibleRule = { ... }; export default possibleRule;
+ return collectInterestingProperties(
+ possibleRule.properties,
+ INTERESTING_RULE_KEYS,
+ );
+ } else if (isFunctionRule(possibleRule)) {
+ // Check `const possibleRule = function(context) { return { ... } }; export default possibleRule;`
+ return { create: possibleRule, meta: undefined, isNewStyle: false };
+ } else if (isTypeScriptRuleHelper(possibleRule)) {
+ // Check `const possibleRule = someTypeScriptHelper({ ... }); export default possibleRule;
+ return collectInterestingProperties(
+ possibleRule.arguments[0].properties,
+ INTERESTING_RULE_KEYS,
+ );
+ }
+ }
+ }
+ return currentExports;
+ }, {} as PartialRuleInfo);
+}
+
+/**
+ * Helper for `getRuleInfo`. Handles CJS rules.
+ */
+function getRuleExportsCJS(
+ ast: Program,
+ scopeManager: Scope.ScopeManager,
+): PartialRuleInfo {
+ let exportsVarOverridden = false;
+ let exportsIsFunction = false;
+ return ast.body
+ .filter((statement) => statement.type === 'ExpressionStatement')
+ .map((statement) => statement.expression)
+ .filter((expression) => expression.type === 'AssignmentExpression')
+ .filter((expression) => expression.left.type === 'MemberExpression')
+ .reduce((currentExports, node) => {
+ const leftExpression = node.left;
+ if (leftExpression.type !== 'MemberExpression') return currentExports;
+ if (
+ 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: undefined, isNewStyle: false };
+ } else if (node.right.type === 'ObjectExpression') {
+ // Check `module.exports = { create: function () {}, meta: {} }`
+
+ return collectInterestingProperties(
+ node.right.properties,
+ INTERESTING_RULE_KEYS,
+ );
+ } else if (node.right.type === 'Identifier') {
+ // Rule could be stored in a variable before being exported.
+ const possibleRule = findVariableValue(node.right, scopeManager);
+ if (possibleRule) {
+ if (possibleRule.type === 'ObjectExpression') {
+ // Check `const possibleRule = { ... }; module.exports = possibleRule;
+ return collectInterestingProperties(
+ possibleRule.properties,
+ INTERESTING_RULE_KEYS,
+ );
+ } else if (isFunctionRule(possibleRule)) {
+ // Check `const possibleRule = function(context) { return { ... } }; module.exports = possibleRule;`
+ return {
+ create: possibleRule,
+ meta: undefined,
+ isNewStyle: false,
+ };
+ }
+ }
+ }
+ return {};
+ } else if (
+ !exportsIsFunction &&
+ 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[leftExpression.property.name] = node.right;
+ } else if (
+ !exportsVarOverridden &&
+ leftExpression.object.type === 'Identifier' &&
+ leftExpression.object.name === 'exports' &&
+ leftExpression.property.type === 'Identifier' &&
+ isInterestingRuleKey(leftExpression.property.name)
+ ) {
+ // Check `exports.create = () => {}`
+
+ 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 obj
+ * @returns property value
+ */
+function findObjectPropertyValueByKeyName(
+ obj: ObjectExpression,
+ keyName: String,
+): Property['value'] | undefined {
+ const property = obj.properties.find(
+ (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 - the Identifier node for the variable.
+ * @returns the first value (or function) that the given variable is initialized to.
+ */
+function findVariableValue(
+ node: Identifier,
+ scopeManager: Scope.ScopeManager,
+): Expression | FunctionDeclaration | undefined {
+ const variable = findVariable(
+ scopeManager.acquire(node) || scopeManager.globalScope!,
+ node,
+ );
+ if (variable && variable.defs && variable.defs[0] && variable.defs[0].node) {
+ const variableDefNode: Node = variable.defs[0].node;
+ if (variableDefNode.type === 'VariableDeclarator' && variableDefNode.init) {
+ // Given node `x`, get `123` from `const x = 123;`.
+ return variableDefNode.init;
+ } else if (variableDefNode.type === 'FunctionDeclaration') {
+ // Given node `foo`, get `function foo() {}` from `function foo() {}`.
+ return variableDefNode;
+ }
+ }
+}
+
+/**
+ * Retrieve all possible elements from an array.
+ * 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]
+ * @returns the list of elements
+ */
+function collectArrayElements(node: Node): Node[] {
+ if (!node) {
+ return [];
+ }
+ if (node.type === 'ArrayExpression') {
+ return node.elements.filter((element) => element !== null);
+ }
+ if (node.type === 'ConditionalExpression') {
+ return [
+ ...collectArrayElements(node.consequent),
+ ...collectArrayElements(node.alternate),
+ ];
+ }
+ return [];
+}
+
+/**
+* Performs static analysis on an AST to try to determine the final value of `module.exports`.
+* @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,
+}: {
+ ast: Program;
+ scopeManager: Scope.ScopeManager;
+}): RuleInfo | null {
+ const exportNodes =
+ ast.sourceType === 'module'
+ ? getRuleExportsESM(ast, scopeManager)
+ : getRuleExportsCJS(ast, scopeManager);
+
+ const createExists = 'create' in exportNodes;
+ if (!createExists) {
+ return null;
+ }
+
+ // If create/meta are defined in variables, get their values.
+ 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 { create, ...remainingExportNodes } = exportNodes;
+ if (!(isFunctionType(create) && isNormalFunctionExpression(create))) {
+ return null;
+ }
+
+ 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 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: Scope.ScopeManager,
+ ast: Program,
+): Set {
+ const ruleInfo = getRuleInfo({ ast, scopeManager });
+
+ const firstCreateParam = ruleInfo?.create.params[0];
+ if (
+ !ruleInfo ||
+ ruleInfo.create?.params.length === 0 ||
+ firstCreateParam?.type !== 'Identifier'
+ ) {
+ return new Set();
+ }
+
+ return new Set(
+ scopeManager
+ .getDeclaredVariables(ruleInfo.create)
+ .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 node The `Property` node
+ * @param scope
+ * @returns The key name, or `null` if the name cannot be determined statically.
+ */
+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;
+ }
+ if (property.key.type === 'Identifier') {
+ if (property.computed) {
+ // Variable key: { [myVariable]: 'hello world' }
+ if (scope) {
+ const staticValue = getStaticValue(property.key, scope);
+ 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;
+ }
+ 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 ?? null;
+ }
+ return null;
+}
+
+/**
+ * Extracts the body of a function if the given node is a function
+ *
+ * @param node
+ */
+function extractFunctionBody(
+ node: Expression | SpreadElement,
+): (Statement | Expression)[] {
+ if (
+ node.type === 'ArrowFunctionExpression' ||
+ node.type === 'FunctionExpression'
+ ) {
+ if (node.body.type === 'BlockStatement') {
+ return node.body.body;
+ }
+
+ return [node.body];
+ }
+
+ return [];
+}
+
+/**
+ * Checks the given statements for possible test info
+ *
+ * @param context The `context` variable for the source file itself
+ * @param statements The statements to check
+ * @param variableIdentifiers
+ */
+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) {
+ if (statement.type === 'VariableDeclaration') {
+ for (const declarator of statement.declarations) {
+ if (!declarator.init) {
+ continue;
+ }
+
+ const extracted = extractFunctionBody(declarator.init);
+
+ runCalls.push(
+ ...checkStatementsForTestInfo(
+ context,
+ extracted,
+ variableIdentifiers,
+ ),
+ );
+
+ if (
+ isRuleTesterConstruction(declarator.init) &&
+ declarator.id.type === 'Identifier'
+ ) {
+ const vars = sourceCode.getDeclaredVariables(declarator);
+ vars.forEach((variable) => {
+ variable.references
+ .filter((ref) => ref.isRead())
+ .forEach((ref) => variableIdentifiers.add(ref.identifier));
+ });
+ }
+ }
+ }
+
+ if (statement.type === 'FunctionDeclaration') {
+ runCalls.push(
+ ...checkStatementsForTestInfo(
+ context,
+ statement.body.body,
+ variableIdentifiers,
+ ),
+ );
+ }
+
+ if (statement.type === 'IfStatement') {
+ const body =
+ statement.consequent.type === 'BlockStatement'
+ ? statement.consequent.body
+ : [statement.consequent];
+
+ runCalls.push(
+ ...checkStatementsForTestInfo(context, body, variableIdentifiers),
+ );
+
+ continue;
+ }
+
+ const expression =
+ statement.type === 'ExpressionStatement'
+ ? statement.expression
+ : statement;
+
+ if (expression.type !== 'CallExpression') {
+ continue;
+ }
+
+ for (const arg of expression.arguments) {
+ const extracted = extractFunctionBody(arg);
+
+ runCalls.push(
+ ...checkStatementsForTestInfo(context, extracted, variableIdentifiers),
+ );
+ }
+
+ 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 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: Rule.RuleContext,
+ ast: Program,
+): TestInfo[] {
+ const runCalls = checkStatementsForTestInfo(context, ast.body);
+
+ return runCalls
+ .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',
+ );
+ const invalidProperty = run.properties.find(
+ (prop) => getKeyName(prop) === 'invalid',
+ );
+
+ return {
+ valid:
+ validProperty &&
+ validProperty.type !== 'SpreadElement' &&
+ validProperty.value.type === 'ArrayExpression'
+ ? validProperty.value.elements.filter(Boolean)
+ : [],
+ invalid:
+ invalidProperty &&
+ invalidProperty.type !== 'SpreadElement' &&
+ invalidProperty.value.type === 'ArrayExpression'
+ ? invalidProperty.value.elements.filter(Boolean)
+ : [],
+ };
+ });
+}
+
+/**
+ * Gets information on a report, given the ASTNode of context.report().
+ * @param node The ASTNode of context.report()
+ */
+export function getReportInfo(
+ node: CallExpression,
+ context: Rule.RuleContext,
+):
+ | Record
+ | Record
+ | null {
+ 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 && 'value' in property) {
+ return Object.assign(reportInfo, { [propName]: property.value });
+ }
+ return reportInfo;
+ },
+ {},
+ );
+ }
+ return null;
+ }
+
+ let keys: string[];
+ const sourceCode = context.sourceCode;
+ const scope = sourceCode.getScope(node);
+ const secondArgStaticValue = getStaticValue(reportArgs[1], scope);
+
+ 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
+ * @param ast The AST of the file. This must have `parent` properties.
+ * @returns A set of all identifiers referring to the `SourceCode` object.
+ */
+export function getSourceCodeIdentifiers(
+ scopeManager: Scope.ScopeManager,
+ ast: Program,
+): Set {
+ return new Set(
+ [...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 fixer The fixer.
+ * @param node The ObjectExpression node to insert a property.
+ * @param propertyText The property code to insert.
+ */
+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(lastProperty)!,
+ `,\n${propertyText}`,
+ );
+}
+
+/**
+ * Collect all context.report({...}) violation/suggestion-related nodes into a standardized array for convenience.
+ * @param reportInfo - Result of getReportInfo().
+ * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[]
+ */
+export function collectReportViolationAndSuggestionData(
+ reportInfo: NonNullable>,
+): ViolationAndSuppressionData[] {
+ 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
+ * @param 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' &&
+ 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, context)?.fix === node
+ );
+}
+
+/**
+ * Whether the provided node represents a suggestion fixer function.
+ * @param node
+ * @param contextIdentifiers
+ * @param context
+ */
+export function isSuggestionFixerFunction(
+ node: Node,
+ contextIdentifiers: Set,
+ context: Rule.RuleContext,
+): boolean {
+ const parent = node.parent;
+ return (
+ (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(
+ // @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, context)
+ ?.suggest === parent.parent.parent
+ );
+}
+
+/**
+ * List all properties contained in an object.
+ * Evaluates and includes any properties that may be behind spreads.
+ * @param objectNode
+ * @param scopeManager
+ * @returns the list of all properties that could be found
+ */
+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 as Identifier,
+ scopeManager,
+ );
+ if (value && value.type === 'ObjectExpression') {
+ return value.properties;
+ }
+ return [];
+ }
+ return [property];
+ });
+}
+
+export function getMetaDocsProperty(
+ propertyName: string,
+ ruleInfo: RuleInfo,
+ scopeManager: Scope.ScopeManager,
+): MetaDocsProperty {
+ const metaNode = ruleInfo.meta ?? undefined;
+
+ const docsNode = evaluateObjectProperties(metaNode, scopeManager)
+ .filter((node) => node.type === 'Property')
+ .find((p) => getKeyName(p) === 'docs');
+
+ const metaPropertyNode = evaluateObjectProperties(
+ docsNode?.value,
+ scopeManager,
+ )
+ .filter((node) => node.type === 'Property')
+ .find((p) => getKeyName(p) === propertyName);
+
+ return { docsNode, metaNode, metaPropertyNode };
+}
+
+/**
+ * Get the `meta.messages` node from a rule.
+ * @param ruleInfo
+ * @param scopeManager
+ */
+export function getMessagesNode(
+ ruleInfo: RuleInfo | null,
+ scopeManager: Scope.ScopeManager,
+): ObjectExpression | undefined {
+ if (!ruleInfo) {
+ return;
+ }
+
+ 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 as Identifier,
+ scopeManager,
+ );
+ if (value && value.type === 'ObjectExpression') {
+ return value;
+ }
+ }
+}
+
+/**
+ * Get the list of messageId properties from `meta.messages` for a rule.
+ * @param ruleInfo
+ * @param scopeManager
+ */
+export function getMessageIdNodes(
+ ruleInfo: RuleInfo,
+ scopeManager: Scope.ScopeManager,
+): (Property | SpreadElement)[] | undefined {
+ const messagesNode = getMessagesNode(ruleInfo, scopeManager);
+
+ return messagesNode && messagesNode.type === 'ObjectExpression'
+ ? evaluateObjectProperties(messagesNode, scopeManager)
+ : undefined;
+}
+
+/**
+ * Get the messageId property from a rule's `meta.messages` that matches the given `messageId`.
+ * @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: 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: 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: AssignmentProperty | Property | undefined,
+ scopeManager: Scope.ScopeManager,
+): Node | null {
+ if (!schemaNode) {
+ return null;
+ }
+
+ 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 null;
+ }
+
+ value = (variable.defs[0].node as VariableDeclarator).init! as Expression;
+ }
+
+ return value;
+}
+
+/**
+ * Get the possible values that a variable was initialized to at some point.
+ * @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: Identifier,
+ scopeManager: Scope.ScopeManager,
+): Node[] {
+ 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
+ * @returns Whether the node is an Identifier with name `undefined`.
+ */
+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 - the Identifier node for the variable.
+ * @param scopeManager
+ * @returns whether the variable comes from a function parameter
+ */
+export function isVariableFromParameter(
+ node: Identifier,
+ scopeManager: Scope.ScopeManager,
+): boolean {
+ const variable = findVariable(
+ scopeManager.acquire(node) || scopeManager.globalScope!,
+ node,
+ );
+
+ return variable?.defs[0]?.type === 'Parameter';
+}
diff --git a/package.json b/package.json
index be4d50fc..0101ff80 100644
--- a/package.json
+++ b/package.json
@@ -1,30 +1,32 @@
{
"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": "./lib/index.js",
+ "main": "./dist/index.js",
+ "type": "module",
"exports": {
- ".": "./lib/index.js",
- "./configs/*": "./configs/*.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": "nyc --all --check-coverage --include lib mocha tests --recursive",
- "test:remote": "eslint-remote-tester -c ./eslint-remote-tester.config.mjs",
+ "test": "vitest run --coverage",
+ "test:remote": "eslint-remote-tester",
+ "typecheck": "tsc",
"update:eslint-docs": "eslint-doc-generator"
},
"files": [
- "lib/",
- "configs/"
+ "CHANGELOG.md",
+ "dist/"
],
"keywords": [
"eslint",
@@ -43,53 +45,51 @@
"@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",
"@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/estree": "^1.0.5",
- "@typescript-eslint/parser": "^7.7.0",
- "@typescript-eslint/utils": "^7.7.0",
- "chai": "^4.5.0",
- "eslint": "^9.16.0",
+ "@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.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",
- "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.3",
"eslint-scope": "^8.0.1",
"espree": "^10.0.1",
- "globals": "^15.13.0",
"husky": "^9.1.7",
+ "jiti": "^2.5.1",
"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.7.2"
+ "tsup": "^8.5.0",
+ "typescript": "^5.9.2",
+ "vitest": "^3.2.4"
},
"peerDependencies": {
- "eslint": ">=8.23.0"
+ "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": {
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..baf78709 100644
--- a/tests/lib/fixtures/tsconfig.json
+++ b/tests/lib/fixtures/tsconfig.json
@@ -1,5 +1,7 @@
{
"compilerOptions": {
+ "module": "NodeNext",
"moduleResolution": "NodeNext"
- }
+ },
+ "include": ["*.ts"]
}
diff --git a/tests/lib/index.js b/tests/lib/index.js
deleted file mode 100644
index 794d0d57..00000000
--- a/tests/lib/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-'use strict';
-
-const assert = require('chai').assert;
-const plugin = require('../..');
-
-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 62%
rename from tests/lib/rule-setup.js
rename to tests/lib/rule-setup.ts
index a1c75e82..dfa0fbba 100644
--- a/tests/lib/rule-setup.js
+++ b/tests/lib/rule-setup.ts
@@ -1,22 +1,24 @@
-'use strict';
+import { readdirSync, readFileSync } from 'node:fs';
+import path from 'node:path';
-const { readdirSync, readFileSync } = require('fs');
-const path = require('path');
-const assert = require('chai').assert;
-const plugin = require('../..');
+import { assert, describe, it } from 'vitest';
-const RULE_NAMES = Object.keys(plugin.rules);
+import plugin from '../../lib/index.js';
+
+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', '')),
);
});
@@ -27,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',
);
});
});
@@ -53,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 95%
rename from tests/lib/rules/consistent-output.js
rename to tests/lib/rules/consistent-output.ts
index 75ecabe8..e63d62f7 100644
--- a/tests/lib/rules/consistent-output.js
+++ b/tests/lib/rules/consistent-output.ts
@@ -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 'eslint';
const ERROR = { messageId: 'missingOutput', type: 'ObjectExpression' };
diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.ts
similarity index 99%
rename from tests/lib/rules/fixer-return.js
rename to tests/lib/rules/fixer-return.ts
index 349a9bf4..bff61ee3 100644
--- a/tests/lib/rules/fixer-return.js
+++ b/tests/lib/rules/fixer-return.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.ts
similarity index 96%
rename from tests/lib/rules/meta-property-ordering.js
rename to tests/lib/rules/meta-property-ordering.ts
index 1d4dfec8..632dfe59 100644
--- a/tests/lib/rules/meta-property-ordering.js
+++ b/tests/lib/rules/meta-property-ordering.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-deprecated-context-methods.js b/tests/lib/rules/no-deprecated-context-methods.ts
similarity index 94%
rename from tests/lib/rules/no-deprecated-context-methods.js
rename to tests/lib/rules/no-deprecated-context-methods.ts
index 84770ccd..14eaaccc 100644
--- a/tests/lib/rules/no-deprecated-context-methods.js
+++ b/tests/lib/rules/no-deprecated-context-methods.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.ts
similarity index 98%
rename from tests/lib/rules/no-deprecated-report-api.js
rename to tests/lib/rules/no-deprecated-report-api.ts
index 6483fbde..636b2c11 100644
--- a/tests/lib/rules/no-deprecated-report-api.js
+++ b/tests/lib/rules/no-deprecated-report-api.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.ts
similarity index 97%
rename from tests/lib/rules/no-identical-tests.js
rename to tests/lib/rules/no-identical-tests.ts
index 6f0e444e..169d9140 100644
--- a/tests/lib/rules/no-identical-tests.js
+++ b/tests/lib/rules/no-identical-tests.ts
@@ -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 '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.ts
similarity index 79%
rename from tests/lib/rules/no-meta-replaced-by.js
rename to tests/lib/rules/no-meta-replaced-by.ts
index d72f41ac..f27fa93d 100644
--- a/tests/lib/rules/no-meta-replaced-by.js
+++ b/tests/lib/rules/no-meta-replaced-by.ts
@@ -2,20 +2,18 @@
* @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 'eslint';
// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
-const valid = [
+const valid: string[] = [
'module.exports = {};',
`
module.exports = {
@@ -36,8 +34,7 @@ const valid = [
create(context) {},
};
`,
- {
- code: `
+ `
module.exports = {
meta: {
deprecated: {
@@ -53,11 +50,9 @@ const valid = [
create(context) {},
};
`,
- errors: 0,
- },
];
-const invalid = [
+const invalid: RuleTester.InvalidTestCase[] = [
{
code: `
module.exports = {
@@ -111,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');
}
@@ -122,7 +123,7 @@ const testToESM = (test) => {
...test,
code,
};
-};
+}
new RuleTester({
languageOptions: { sourceType: 'commonjs' },
@@ -134,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 97%
rename from tests/lib/rules/no-meta-schema-default.js
rename to tests/lib/rules/no-meta-schema-default.ts
index 869f4971..07dd50d1 100644
--- a/tests/lib/rules/no-meta-schema-default.js
+++ b/tests/lib/rules/no-meta-schema-default.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-missing-message-ids.js b/tests/lib/rules/no-missing-message-ids.ts
similarity index 98%
rename from tests/lib/rules/no-missing-message-ids.js
rename to tests/lib/rules/no-missing-message-ids.ts
index 78b9e600..aa0045a0 100644
--- a/tests/lib/rules/no-missing-message-ids.js
+++ b/tests/lib/rules/no-missing-message-ids.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.ts
similarity index 96%
rename from tests/lib/rules/no-missing-placeholders.js
rename to tests/lib/rules/no-missing-placeholders.ts
index 43bb137b..f725590e 100644
--- a/tests/lib/rules/no-missing-placeholders.js
+++ b/tests/lib/rules/no-missing-placeholders.ts
@@ -3,21 +3,23 @@
* @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 '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 98%
rename from tests/lib/rules/no-only-tests.js
rename to tests/lib/rules/no-only-tests.ts
index 0df7e600..b7f52125 100644
--- a/tests/lib/rules/no-only-tests.js
+++ b/tests/lib/rules/no-only-tests.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-property-in-node.js b/tests/lib/rules/no-property-in-node.ts
similarity index 90%
rename from tests/lib/rules/no-property-in-node.js
rename to tests/lib/rules/no-property-in-node.ts
index 62181a3b..c21e1cb1 100644
--- a/tests/lib/rules/no-property-in-node.js
+++ b/tests/lib/rules/no-property-in-node.ts
@@ -1,15 +1,18 @@
-'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 'eslint';
+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';
+const dirname = path.dirname(fileURLToPath(import.meta.url));
const ruleTester = new RuleTester({
languageOptions: {
- parser: require('@typescript-eslint/parser'),
+ parser,
parserOptions: {
- project: './tsconfig.json',
- tsconfigRootDir: path.join(__dirname, '../fixtures'),
+ projectService: {
+ defaultProject: 'tsconfig.json',
+ },
+ tsconfigRootDir: path.join(dirname, '../fixtures'),
},
},
});
diff --git a/tests/lib/rules/no-unused-message-ids.js b/tests/lib/rules/no-unused-message-ids.ts
similarity index 98%
rename from tests/lib/rules/no-unused-message-ids.js
rename to tests/lib/rules/no-unused-message-ids.ts
index 8f5f6fa8..244651e1 100644
--- a/tests/lib/rules/no-unused-message-ids.js
+++ b/tests/lib/rules/no-unused-message-ids.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.ts
similarity index 96%
rename from tests/lib/rules/no-unused-placeholders.js
rename to tests/lib/rules/no-unused-placeholders.ts
index fd93b777..9f16166b 100644
--- a/tests/lib/rules/no-unused-placeholders.js
+++ b/tests/lib/rules/no-unused-placeholders.ts
@@ -3,21 +3,22 @@
* @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 '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 91%
rename from tests/lib/rules/no-useless-token-range.js
rename to tests/lib/rules/no-useless-token-range.ts
index c4b13960..72499c80 100644
--- a/tests/lib/rules/no-useless-token-range.js
+++ b/tests/lib/rules/no-useless-token-range.ts
@@ -3,21 +3,19 @@
* @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 '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 98%
rename from tests/lib/rules/prefer-message-ids.js
rename to tests/lib/rules/prefer-message-ids.ts
index ac54184e..9a45cb28 100644
--- a/tests/lib/rules/prefer-message-ids.js
+++ b/tests/lib/rules/prefer-message-ids.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.ts
similarity index 96%
rename from tests/lib/rules/prefer-object-rule.js
rename to tests/lib/rules/prefer-object-rule.ts
index 9c0aae74..4c25013c 100644
--- a/tests/lib/rules/prefer-object-rule.js
+++ b/tests/lib/rules/prefer-object-rule.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/prefer-output-null.js b/tests/lib/rules/prefer-output-null.ts
similarity index 93%
rename from tests/lib/rules/prefer-output-null.js
rename to tests/lib/rules/prefer-output-null.ts
index b7c0eda1..082e41c4 100644
--- a/tests/lib/rules/prefer-output-null.js
+++ b/tests/lib/rules/prefer-output-null.ts
@@ -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 'eslint';
const ERROR = { messageId: 'useOutputNull', type: 'Property' };
diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.ts
similarity index 96%
rename from tests/lib/rules/prefer-placeholders.js
rename to tests/lib/rules/prefer-placeholders.ts
index f7fe49e0..4b2f6447 100644
--- a/tests/lib/rules/prefer-placeholders.js
+++ b/tests/lib/rules/prefer-placeholders.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.ts
similarity index 96%
rename from tests/lib/rules/prefer-replace-text.js
rename to tests/lib/rules/prefer-replace-text.ts
index c83a9559..06117252 100644
--- a/tests/lib/rules/prefer-replace-text.js
+++ b/tests/lib/rules/prefer-replace-text.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.ts
similarity index 97%
rename from tests/lib/rules/report-message-format.js
rename to tests/lib/rules/report-message-format.ts
index 7ca4cf10..dd7f9e61 100644
--- a/tests/lib/rules/report-message-format.js
+++ b/tests/lib/rules/report-message-format.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
@@ -220,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 95%
rename from tests/lib/rules/require-meta-default-options.js
rename to tests/lib/rules/require-meta-default-options.ts
index da2b4209..d6ceb410 100644
--- a/tests/lib/rules/require-meta-default-options.js
+++ b/tests/lib/rules/require-meta-default-options.ts
@@ -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 'eslint';
+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.ts
similarity index 97%
rename from tests/lib/rules/require-meta-docs-description.js
rename to tests/lib/rules/require-meta-docs-description.ts
index 2b4ded37..28d49910 100644
--- a/tests/lib/rules/require-meta-docs-description.js
+++ b/tests/lib/rules/require-meta-docs-description.ts
@@ -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 'eslint';
+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.ts
similarity index 97%
rename from tests/lib/rules/require-meta-docs-recommended.js
rename to tests/lib/rules/require-meta-docs-recommended.ts
index 1a58bacc..60a9d72a 100644
--- a/tests/lib/rules/require-meta-docs-recommended.js
+++ b/tests/lib/rules/require-meta-docs-recommended.ts
@@ -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 'eslint';
+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.ts
similarity index 99%
rename from tests/lib/rules/require-meta-docs-url.js
rename to tests/lib/rules/require-meta-docs-url.ts
index da7a9048..50895f05 100644
--- a/tests/lib/rules/require-meta-docs-url.js
+++ b/tests/lib/rules/require-meta-docs-url.ts
@@ -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 'eslint';
+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.ts
similarity index 98%
rename from tests/lib/rules/require-meta-fixable.js
rename to tests/lib/rules/require-meta-fixable.ts
index 8aef6c04..3bde53c7 100644
--- a/tests/lib/rules/require-meta-fixable.js
+++ b/tests/lib/rules/require-meta-fixable.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.ts
similarity index 98%
rename from tests/lib/rules/require-meta-has-suggestions.js
rename to tests/lib/rules/require-meta-has-suggestions.ts
index 0a1e96ab..8bb7eb84 100644
--- a/tests/lib/rules/require-meta-has-suggestions.js
+++ b/tests/lib/rules/require-meta-has-suggestions.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/require-meta-schema-description.js b/tests/lib/rules/require-meta-schema-description.ts
similarity index 97%
rename from tests/lib/rules/require-meta-schema-description.js
rename to tests/lib/rules/require-meta-schema-description.ts
index 1b3f4256..fa0b414f 100644
--- a/tests/lib/rules/require-meta-schema-description.js
+++ b/tests/lib/rules/require-meta-schema-description.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.ts
similarity index 98%
rename from tests/lib/rules/require-meta-schema.js
rename to tests/lib/rules/require-meta-schema.ts
index dfb1754d..5c6c6af0 100644
--- a/tests/lib/rules/require-meta-schema.js
+++ b/tests/lib/rules/require-meta-schema.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.ts
similarity index 95%
rename from tests/lib/rules/require-meta-type.js
rename to tests/lib/rules/require-meta-type.ts
index 2180e23e..a2326db2 100644
--- a/tests/lib/rules/require-meta-type.js
+++ b/tests/lib/rules/require-meta-type.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
@@ -68,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' };
@@ -87,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 97%
rename from tests/lib/rules/test-case-property-ordering.js
rename to tests/lib/rules/test-case-property-ordering.ts
index 149278d1..05fb8554 100644
--- a/tests/lib/rules/test-case-property-ordering.js
+++ b/tests/lib/rules/test-case-property-ordering.ts
@@ -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 'eslint';
// ------------------------------------------------------------------------------
// Tests
diff --git a/tests/lib/rules/test-case-shorthand-strings.js b/tests/lib/rules/test-case-shorthand-strings.ts
similarity index 95%
rename from tests/lib/rules/test-case-shorthand-strings.js
rename to tests/lib/rules/test-case-shorthand-strings.ts
index 4deddade..0b989397 100644
--- a/tests/lib/rules/test-case-shorthand-strings.js
+++ b/tests/lib/rules/test-case-shorthand-strings.ts
@@ -3,21 +3,19 @@
* @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 '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 d299abcc..292e392d 100644
--- a/tests/lib/utils.js
+++ b/tests/lib/utils.ts
@@ -1,13 +1,46 @@
-'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';
+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.ts b/tests/utils/test-setup.ts
new file mode 100644
index 00000000..342265eb
--- /dev/null
+++ b/tests/utils/test-setup.ts
@@ -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/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..1411ce1d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "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,
+ "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
new file mode 100644
index 00000000..5a9a60c0
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ include: ['tests/lib/**/*.ts'],
+ exclude: ['tests/lib/fixtures/**'],
+ setupFiles: ['tests/utils/test-setup.ts'],
+ clearMocks: true,
+ coverage: {
+ all: true,
+ include: ['lib'],
+ reporter: ['html', 'lcov', 'text'],
+ provider: 'istanbul',
+ thresholds: {
+ statements: 95,
+ branches: 93,
+ functions: 95,
+ lines: 95,
+ },
+ },
+ },
+});