diff --git a/.cspell.json b/.cspell.json
index 62dd3ad1bfc4..044fb2b43420 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -1,6 +1,14 @@
{
- "version": "0.1",
+ "version": "0.2",
"language": "en",
+ "enableFiletypes": [
+ "markdown",
+ "mdx",
+ "typescript",
+ "typescriptreact",
+ "javascript",
+ "javascriptreact"
+ ],
"ignorePaths": [
".cspell.json",
".github/workflows/**",
diff --git a/.eslintignore b/.eslintignore
index d372e0ba7c1b..431bc8030293 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -12,3 +12,6 @@ packages/types/src/generated/**/*.ts
# Playground types downloaded from the web
packages/website/src/vendor
+
+# see the file header in eslint-base.test.js for more info
+packages/rule-tester/tests/eslint-base
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index b8d7426f2cff..feda2165fe25 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -1,6 +1,8 @@
{
enabledManagers: ['github-actions', 'npm'],
ignoreDeps: [
+ // AJV is out-of-date, but it's intentionally synced with ESLint - https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/package.json#L70
+ 'ajv',
// globby is ESM so we can't go any higher right now
'globby',
// this dep now uses package.json exports - we will be removing it next major
diff --git a/.prettierignore b/.prettierignore
index dd9551fb811d..b807ea259fe5 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,3 +18,6 @@ CHANGELOG.md
packages/website/.docusaurus
packages/website/build
+
+# see the file header in eslint-base.test.js for more info
+packages/rule-tester/tests/eslint-base
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 27b3fb977408..4cee04bec4a0 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -105,6 +105,42 @@
"${workspaceFolder}/packages/scope-manager/dist/index.js",
],
},
+ {
+ "type": "node",
+ "request": "launch",
+ "name": "Run currently opened rule-tester test",
+ "cwd": "${workspaceFolder}/packages/rule-tester/",
+ "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
+ "args": [
+ "--runInBand",
+ "--no-cache",
+ "--no-coverage",
+ "${fileBasename}"
+ ],
+ "sourceMaps": true,
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+ "skipFiles": [
+ "${workspaceFolder}/packages/utils/src/index.ts",
+ "${workspaceFolder}/packages/utils/dist/index.js",
+ "${workspaceFolder}/packages/utils/src/ts-estree.ts",
+ "${workspaceFolder}/packages/utils/dist/ts-estree.js",
+ "${workspaceFolder}/packages/type-utils/src/ts-estree.ts",
+ "${workspaceFolder}/packages/type-utils/dist/ts-estree.js",
+ "${workspaceFolder}/packages/parser/src/index.ts",
+ "${workspaceFolder}/packages/parser/dist/index.js",
+ "${workspaceFolder}/packages/rule-tester/src/index.ts",
+ "${workspaceFolder}/packages/rule-tester/dist/index.js",
+ "${workspaceFolder}/packages/typescript-estree/src/index.ts",
+ "${workspaceFolder}/packages/typescript-estree/dist/index.js",
+ "${workspaceFolder}/packages/types/src/index.ts",
+ "${workspaceFolder}/packages/types/dist/index.js",
+ "${workspaceFolder}/packages/visitor-keys/src/index.ts",
+ "${workspaceFolder}/packages/visitor-keys/dist/index.js",
+ "${workspaceFolder}/packages/scope-manager/dist/index.js",
+ "${workspaceFolder}/packages/scope-manager/dist/index.js",
+ ],
+ },
{
"type": "node",
"request": "launch",
diff --git a/docs/Architecture.mdx b/docs/Architecture.mdx
index ee224c1e90ae..847473a659d8 100644
--- a/docs/Architecture.mdx
+++ b/docs/Architecture.mdx
@@ -12,6 +12,7 @@ They are:
- [`@typescript-eslint/eslint-plugin`](./architecture/ESLint_Plugin.mdx): An ESLint plugin which provides lint rules for TypeScript codebases.
- [`@typescript-eslint/eslint-plugin-tslint`](./architecture/ESLint_Plugin_TSLint.mdx): ESLint plugin that allows running TSLint rules within ESLint to help you migrate from TSLint to ESLint.
- [`@typescript-eslint/parser`](./architecture/Parser.mdx): An ESLint parser which allows for ESLint to lint TypeScript source code.
+- [`@typescript-eslint/rule-tester`](./architecture/Rule_Tester.mdx): A utility for testing ESLint rules.
- [`@typescript-eslint/scope-manager`](./architecture/Scope_Manager.mdx): A fork of [`eslint-scope`](https://github.com/eslint/eslint-scope), enhanced to support TypeScript functionality.
-- [`@typescript-eslint/typescript-estree`](./architecture/TypeScript-ESTree.mdx): The underlying code used by [`@typescript-eslint/parser`](./architecture/Parser.mdx) that converts TypeScript source code into an ESTree-compatible form.
+- [`@typescript-eslint/typescript-estree`](./architecture/TypeScript_ESTree.mdx): The underlying code used by [`@typescript-eslint/parser`](./architecture/Parser.mdx) that converts TypeScript source code into an ESTree-compatible form.
- [`@typescript-eslint/utils`](./architecture/Utils.mdx): Utilities for working with TypeScript + ESLint together.
diff --git a/docs/Custom_Rules.mdx b/docs/Custom_Rules.mdx
index d636a9931120..e245e2fb0585 100644
--- a/docs/Custom_Rules.mdx
+++ b/docs/Custom_Rules.mdx
@@ -274,18 +274,20 @@ This can be necessary for TypeScript APIs not wrapped by the parser services.
## Testing
-`@typescript-eslint/utils` exports a `RuleTester` with a similar API to the built-in [ESLint `RuleTester`](https://eslint.org/docs/developer-guide/nodejs-api#ruletester).
+`@typescript-eslint/rule-tester` exports a `RuleTester` with a similar API to the built-in ESLint `RuleTester`.
It should be provided with the same `parser` and `parserOptions` you would use in your ESLint configuration.
+Below is a quick-start guide. For more in-depth docs and examples [see the `@typescript-eslint/rule-tester` package documentation](./architecture/Rule_Tester.mdx).
+
### Testing Untyped Rules
For rules that don't need type information, passing just the `parser` will do:
```ts
-import { ESLintUtils } from '@typescript-eslint/utils';
+import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-rule';
-const ruleTester = new ESLintUtils.RuleTester({
+const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});
@@ -305,10 +307,10 @@ For rules that do need type information, `parserOptions` must be passed in as we
Tests must have at least an absolute `tsconfigRootDir` path provided as well as a relative `project` path from that directory:
```ts
-import { ESLintUtils } from '@typescript-eslint/utils';
+import { RuleTester } from '@typescript-eslint/rule-tester';
import rule from './my-typed-rule';
-const ruleTester = new ESLintUtils.RuleTester({
+const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
@@ -327,11 +329,11 @@ ruleTester.run('my-typed-rule', rule, {
```
:::note
-For now, `ESLintUtils.RuleTester` requires the following physical files be present on disk for typed rules:
+For now, `RuleTester` requires the following physical files be present on disk for typed rules:
- `tsconfig.json`: tsconfig used as the test "project"
- One of the following two files:
- `file.ts`: blank test file used for normal TS tests
- - `file.tsx`: blank test file used for tests with `parserOptions: { ecmaFeatures: { jsx: true } }`
+ - `react.tsx`: blank test file used for tests with `parserOptions: { ecmaFeatures: { jsx: true } }`
:::
diff --git a/docs/architecture/Rule_Tester.mdx b/docs/architecture/Rule_Tester.mdx
new file mode 100644
index 000000000000..8caec346d771
--- /dev/null
+++ b/docs/architecture/Rule_Tester.mdx
@@ -0,0 +1,218 @@
+---
+id: rule-tester
+sidebar_label: rule-tester
+---
+
+import CodeBlock from '@theme/CodeBlock';
+
+# `@typescript-eslint/rule-tester`
+
+> A utility for testing ESLint rules
+
+This is a fork of ESLint's built-in `RuleTester` to provide some better types and additional features for testing TypeScript rules.
+
+## Usage
+
+For non-type-aware rules you can test them as follows:
+
+```ts
+import { RuleTester } from '@typescript-eslint/rule-tester';
+import rule from '../src/rules/my-rule.ts';
+
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+});
+
+ruleTester.run('my-rule', rule, {
+ valid: [
+ // valid tests can be a raw string,
+ 'const x = 1;',
+ // or they can be an object
+ {
+ code: 'const y = 2;',
+ options: [{ ruleOption: true }],
+ },
+
+ // you can enable JSX parsing by passing parserOptions.ecmaFeatures.jsx = true
+ {
+ code: 'const z =
;',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ ],
+ invalid: [
+ // invalid tests must always be an object
+ {
+ code: 'const a = 1;',
+ // invalid tests must always specify the expected errors
+ errors: [
+ {
+ messageId: 'ruleMessage',
+ // If applicable - it's recommended that you also assert the data in
+ // addition to the messageId so that you can ensure the correct message
+ // is generated
+ data: {
+ placeholder1: 'a',
+ },
+ },
+ ],
+ },
+
+ // fixers can be tested using the output parameter
+ {
+ code: 'const b = 1;',
+ output: 'const c = 1;',
+ errors: [
+ /* ... */
+ ],
+ },
+ // passing `output = null` will enforce the code is NOT changed
+ {
+ code: 'const c = 1;',
+ output: null,
+ errors: [
+ /* ... */
+ ],
+ },
+
+ // suggestions can be tested via errors
+ {
+ code: 'const d = 1;',
+ output: null,
+ errors: [
+ {
+ messageId: 'suggestionError',
+ suggestions: [
+ {
+ messageId: 'suggestionOne',
+ output: 'const e = 1;',
+ },
+ ],
+ },
+ ],
+ },
+ // passing `suggestions = null` will enforce there are NO suggestions
+ {
+ code: 'const d = 1;',
+ output: null,
+ errors: [
+ {
+ messageId: 'noSuggestionError',
+ suggestions: null,
+ },
+ ],
+ },
+ ],
+});
+```
+
+### Type-Aware Testing
+
+Type-aware rules can be tested in almost exactly the same way, except you need to create some files on disk.
+We require files on disk due to a limitation with TypeScript in that it requires physical files on disk to initialize the project.
+We suggest creating a `fixture` folder nearby that contains three files:
+
+1. `file.ts` - this should be an empty file.
+2. `react.tsx` - this should be an empty file.
+3. `tsconfig.json` - this should be the config to use for your test, for example:
+ ```json
+ {
+ "compilerOptions": {
+ "strict": true
+ },
+ "include": ["file.ts", "react.tsx"]
+ }
+ ```
+
+:::caution
+It's important to note that both `file.ts` and `react.tsx` must both be empty files!
+The rule tester will automatically use the string content from your tests - the empty files are just there for initialization.
+:::
+
+You can then test your rule by providing the type-aware config:
+
+```ts
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ // Added lines start
+ parserOptions: {
+ tsconfigRootDir: './path/to/your/folder/fixture',
+ project: './tsconfig.json',
+ },
+ // Added lines end
+});
+```
+
+With that config the parser will automatically run in type-aware mode and you can write tests just like before.
+
+### Test Dependency Constraints
+
+Sometimes it's desirable to test your rule against multiple versions of a dependency to ensure backwards and forwards compatibility.
+With backwards-compatibility testing there comes a complication in that some tests may not be compatible with an older version of a dependency.
+For example - if you're testing against an older version of TypeScript, certain features might cause a parser error!
+
+import DependencyConstraint from '!!raw-loader!../../packages/rule-tester/src/types/DependencyConstraint.ts';
+
+{DependencyConstraint}
+
+The `RuleTester` allows you to apply dependency constraints at either an individual test or constructor level.
+
+```ts
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ // Added lines start
+ dependencyConstraints: {
+ // none of the tests will run unless `my-dependency` matches the semver range `>=1.2.3`
+ 'my-dependency': '1.2.3',
+ // you can also provide granular semver ranges
+ 'my-granular-dep': {
+ // none of the tests will run unless `my-granular-dep` matches the semver range `~3.2.1`
+ range: '~3.2.1',
+ },
+ },
+ // Added lines end
+});
+
+ruleTester.run('my-rule', rule, {
+ valid: [
+ {
+ code: 'const y = 2;',
+ // Added lines start
+ dependencyConstraints: {
+ // this test won't run unless BOTH dependencies match the given ranges
+ first: '1.2.3',
+ second: '3.2.1',
+ },
+ // Added lines end
+ },
+ ],
+ invalid: [
+ /* ... */
+ ],
+});
+```
+
+All dependencies provided in the `dependencyConstraints` object must match their given ranges in order for a test to not be skipped.
+
+## Options
+
+### `RuleTester` constructor options
+
+import RuleTesterConfig from '!!raw-loader!../../packages/rule-tester/src/types/RuleTesterConfig.ts';
+
+{RuleTesterConfig}
+
+### Valid test case options
+
+import ValidTestCase from '!!raw-loader!../../packages/rule-tester/src/types/ValidTestCase.ts';
+
+{ValidTestCase}
+
+### Invalid test case options
+
+import InvalidTestCase from '!!raw-loader!../../packages/rule-tester/src/types/InvalidTestCase.ts';
+
+{InvalidTestCase}
diff --git a/docs/architecture/TypeScript-ESTree.mdx b/docs/architecture/TypeScript_ESTree.mdx
similarity index 100%
rename from docs/architecture/TypeScript-ESTree.mdx
rename to docs/architecture/TypeScript_ESTree.mdx
diff --git a/packages/eslint-plugin-tslint/README.md b/packages/eslint-plugin-tslint/README.md
index 57c414230ec8..dfe8ffecb367 100644
--- a/packages/eslint-plugin-tslint/README.md
+++ b/packages/eslint-plugin-tslint/README.md
@@ -8,3 +8,5 @@
👉 See **https://typescript-eslint.io/architecture/eslint-plugin-tslint** for documentation on this package.
> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
+
+
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 9c98f8c7d4b6..e8e09e03eb6a 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -8,3 +8,5 @@ An ESLint plugin which provides lint rules for TypeScript codebases.
👉 See **https://typescript-eslint.io/getting-started** for our Getting Started docs.
> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
+
+
diff --git a/packages/parser/README.md b/packages/parser/README.md
index f057b97f8680..5f92225c3829 100644
--- a/packages/parser/README.md
+++ b/packages/parser/README.md
@@ -8,3 +8,5 @@
👉 See **https://typescript-eslint.io/architecture/parser** for documentation on this package.
> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
+
+
diff --git a/packages/rule-tester/LICENSE b/packages/rule-tester/LICENSE
new file mode 100644
index 000000000000..d68c4a4557ee
--- /dev/null
+++ b/packages/rule-tester/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 typescript-eslint and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/rule-tester/README.md b/packages/rule-tester/README.md
new file mode 100644
index 000000000000..2464e5ac626f
--- /dev/null
+++ b/packages/rule-tester/README.md
@@ -0,0 +1,10 @@
+# `@typescript-eslint/rule-tester`
+
+> Tooling to test ESLint rules
+
+[](https://www.npmjs.com/package/@typescript-eslint/rule-tester)
+[](https://www.npmjs.com/package/@typescript-eslint/rule-tester)
+
+👉 See **https://typescript-eslint.io/architecture/rule-tester** for documentation on this package.
+
+
diff --git a/packages/rule-tester/jest.config.js b/packages/rule-tester/jest.config.js
new file mode 100644
index 000000000000..910991b20cff
--- /dev/null
+++ b/packages/rule-tester/jest.config.js
@@ -0,0 +1,7 @@
+'use strict';
+
+// @ts-check
+/** @type {import('@jest/types').Config.InitialOptions} */
+module.exports = {
+ ...require('../../jest.config.base.js'),
+};
diff --git a/packages/rule-tester/package.json b/packages/rule-tester/package.json
new file mode 100644
index 000000000000..5b38b4895e4f
--- /dev/null
+++ b/packages/rule-tester/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "@typescript-eslint/rule-tester",
+ "version": "5.59.1",
+ "description": "Tooling to test ESLint rules",
+ "files": [
+ "dist",
+ "_ts4.2",
+ "README.md",
+ "LICENSE"
+ ],
+ "type": "commonjs",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "engines": {
+ "node": "^14.18.0 || ^16.0.0 || >=18.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/typescript-eslint/typescript-eslint.git",
+ "directory": "packages/rule-tester"
+ },
+ "bugs": {
+ "url": "https://github.com/typescript-eslint/typescript-eslint/issues"
+ },
+ "license": "MIT",
+ "keywords": [
+ "eslint",
+ "typescript",
+ "estree"
+ ],
+ "scripts": {
+ "build": "tsc -b tsconfig.build.json",
+ "postbuild": "downlevel-dts dist _ts4.2/dist --to=4.2",
+ "clean": "tsc -b tsconfig.build.json --clean",
+ "postclean": "rimraf dist && rimraf _ts3.4 && rimraf coverage",
+ "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore",
+ "lint": "nx lint",
+ "pretest-eslint-base": "tsc -b tsconfig.build.json",
+ "test-eslint-base": "mocha --require source-map-support/register ./tests/eslint-base/eslint-base.test.js",
+ "test": "jest --coverage",
+ "typecheck": "tsc -p tsconfig.json --noEmit"
+ },
+ "//": "NOTE - AJV is out-of-date, but it's intentionally synced with ESLint - https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/package.json#L70",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.59.1",
+ "@typescript-eslint/utils": "5.59.1",
+ "lodash.merge": "4.6.2",
+ "semver": "^7.3.7",
+ "ajv": "^6.10.0"
+ },
+ "peerDependencies": {
+ "@eslint/eslintrc": ">=2",
+ "eslint": ">=8"
+ },
+ "devDependencies": {
+ "@typescript-eslint/parser": "5.59.1",
+ "@types/lodash.merge": "4.6.7",
+ "chai": "^4.0.1",
+ "mocha": "^8.3.2",
+ "sinon": "^11.0.0",
+ "source-map-support": "^0.5.21"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "typesVersions": {
+ "<3.8": {
+ "*": [
+ "_ts3.4/*"
+ ]
+ }
+ }
+}
diff --git a/packages/rule-tester/project.json b/packages/rule-tester/project.json
new file mode 100644
index 000000000000..148a81c84c88
--- /dev/null
+++ b/packages/rule-tester/project.json
@@ -0,0 +1,15 @@
+{
+ "name": "rule-tester",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "type": "library",
+ "implicitDependencies": [],
+ "targets": {
+ "lint": {
+ "executor": "@nrwl/linter:eslint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "lintFilePatterns": ["packages/rule-tester/**/*.ts"]
+ }
+ }
+ }
+}
diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts
new file mode 100644
index 000000000000..0ff658adfbdb
--- /dev/null
+++ b/packages/rule-tester/src/RuleTester.ts
@@ -0,0 +1,1028 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/rule-tester/rule-tester.js
+
+import assert from 'node:assert';
+import path from 'node:path';
+import util from 'node:util';
+
+import type * as ParserType from '@typescript-eslint/parser';
+import type { TSESTree } from '@typescript-eslint/utils';
+import { deepMerge } from '@typescript-eslint/utils/eslint-utils';
+import type {
+ AnyRuleCreateFunction,
+ AnyRuleModule,
+ ParserOptions,
+ RuleContext,
+ RuleModule,
+} from '@typescript-eslint/utils/ts-eslint';
+import { Linter } from '@typescript-eslint/utils/ts-eslint';
+// we intentionally import from eslint here because we need to use the same class
+// that ESLint uses, not our custom override typed version
+import { SourceCode } from 'eslint';
+import merge from 'lodash.merge';
+
+import { TestFramework } from './TestFramework';
+import type {
+ InvalidTestCase,
+ NormalizedRunTests,
+ RuleTesterConfig,
+ RunTests,
+ TesterConfigWithDefaults,
+ ValidTestCase,
+} from './types';
+import { ajvBuilder } from './utils/ajv';
+import { cloneDeeplyExcludesParent } from './utils/cloneDeeplyExcludesParent';
+import { validate } from './utils/config-validator';
+import { satisfiesAllDependencyConstraints } from './utils/dependencyConstraints';
+import { freezeDeeply } from './utils/freezeDeeply';
+import { getRuleOptionsSchema } from './utils/getRuleOptionsSchema';
+import { hasOwnProperty } from './utils/hasOwnProperty';
+import { interpolate } from './utils/interpolate';
+import { isReadonlyArray } from './utils/isReadonlyArray';
+import * as SourceCodeFixer from './utils/SourceCodeFixer';
+import {
+ emitLegacyRuleAPIWarning,
+ emitMissingSchemaWarning,
+ ERROR_OBJECT_PARAMETERS,
+ FRIENDLY_ERROR_OBJECT_PARAMETER_LIST,
+ FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST,
+ getCommentsDeprecation,
+ REQUIRED_SCENARIOS,
+ RULE_TESTER_PARAMETERS,
+ sanitize,
+ SUGGESTION_OBJECT_PARAMETERS,
+ wrapParser,
+} from './utils/validationHelpers';
+
+const ajv = ajvBuilder({ strictDefaults: true });
+const TYPESCRIPT_ESLINT_PARSER = '@typescript-eslint/parser';
+const DUPLICATE_PARSER_ERROR_MESSAGE = `Do not set the parser at the test level unless you want to use a parser other than "${TYPESCRIPT_ESLINT_PARSER}"`;
+
+/*
+ * testerDefaultConfig must not be modified as it allows to reset the tester to
+ * the initial default configuration
+ */
+const testerDefaultConfig: Readonly = {
+ parser: TYPESCRIPT_ESLINT_PARSER,
+ rules: {},
+ defaultFilenames: { ts: 'file.ts', tsx: 'react.tsx' },
+};
+let defaultConfig = deepMerge(
+ {},
+ testerDefaultConfig,
+) as TesterConfigWithDefaults;
+
+export class RuleTester extends TestFramework {
+ readonly #testerConfig: TesterConfigWithDefaults;
+ readonly #rules: Record = {};
+ readonly #linter: Linter = new Linter();
+
+ /**
+ * Creates a new instance of RuleTester.
+ */
+ constructor(testerConfig?: RuleTesterConfig) {
+ super();
+
+ /**
+ * The configuration to use for this tester. Combination of the tester
+ * configuration and the default configuration.
+ */
+ this.#testerConfig = merge({}, defaultConfig, testerConfig, {
+ rules: { 'rule-tester/validate-ast': 'error' },
+ // as of eslint 6 you have to provide an absolute path to the parser
+ // but that's not as clean to type, this saves us trying to manually enforce
+ // that contributors require.resolve everything
+ parser: require.resolve((testerConfig ?? defaultConfig).parser),
+ });
+
+ // make sure that the parser doesn't hold onto file handles between tests
+ // on linux (i.e. our CI env), there can be very a limited number of watch handles available
+ const constructor = this.constructor as typeof RuleTester;
+ constructor.afterAll(() => {
+ try {
+ // instead of creating a hard dependency, just use a soft require
+ // a bit weird, but if they're using this tooling, it'll be installed
+ const parser = require(TYPESCRIPT_ESLINT_PARSER) as typeof ParserType;
+ parser.clearCaches();
+ } catch {
+ // ignored on purpose
+ }
+ });
+ }
+
+ /**
+ * Set the configuration to use for all future tests
+ */
+ static setDefaultConfig(config: RuleTesterConfig): void {
+ if (typeof config !== 'object' || config == null) {
+ throw new TypeError(
+ 'RuleTester.setDefaultConfig: config must be an object',
+ );
+ }
+ // Make sure the rules object exists since it is assumed to exist later
+ defaultConfig = deepMerge(
+ defaultConfig,
+ // @ts-expect-error -- no index signature
+ config,
+ ) as TesterConfigWithDefaults;
+ }
+
+ /**
+ * Get the current configuration used for all tests
+ */
+ static getDefaultConfig(): Readonly {
+ return defaultConfig;
+ }
+
+ /**
+ * Reset the configuration to the initial configuration of the tester removing
+ * any changes made until now.
+ */
+ static resetDefaultConfig(): void {
+ defaultConfig = merge({}, testerDefaultConfig);
+ }
+
+ /**
+ * Adds the `only` property to a test to run it in isolation.
+ */
+ static only>(
+ item: string | ValidTestCase,
+ ): ValidTestCase;
+ /**
+ * Adds the `only` property to a test to run it in isolation.
+ */
+ static only>(
+ item: InvalidTestCase,
+ ): InvalidTestCase;
+ static only>(
+ item:
+ | string
+ | ValidTestCase
+ | InvalidTestCase,
+ ): ValidTestCase | InvalidTestCase {
+ if (typeof item === 'string') {
+ return { code: item, only: true };
+ }
+
+ return { ...item, only: true };
+ }
+
+ /**
+ * Define a rule for one particular run of tests.
+ */
+ defineRule(name: string, rule: AnyRuleModule | AnyRuleCreateFunction): void {
+ this.#rules[name] = rule;
+ }
+
+ #normalizeTests<
+ TMessageIds extends string,
+ TOptions extends readonly unknown[],
+ >(
+ rawTests: RunTests,
+ ): NormalizedRunTests {
+ /*
+ Automatically add a filename to the tests to enable type-aware tests to "just work".
+ This saves users having to verbosely and manually add the filename to every
+ single test case.
+ Hugely helps with the string-based valid test cases as it means they don't
+ need to be made objects!
+ */
+ const getFilename = (testOptions?: ParserOptions): string => {
+ const resolvedOptions = deepMerge(
+ this.#testerConfig.parserOptions,
+ testOptions,
+ ) as ParserOptions;
+ const filename = resolvedOptions.ecmaFeatures?.jsx
+ ? this.#testerConfig.defaultFilenames.tsx
+ : this.#testerConfig.defaultFilenames.ts;
+ if (resolvedOptions.project) {
+ return path.join(
+ resolvedOptions.tsconfigRootDir != null
+ ? resolvedOptions.tsconfigRootDir
+ : process.cwd(),
+ filename,
+ );
+ }
+ return filename;
+ };
+ const normalizeTest = <
+ TMessageIds extends string,
+ TOptions extends readonly unknown[],
+ T extends
+ | ValidTestCase
+ | InvalidTestCase,
+ >(
+ test: T,
+ ): T => {
+ if (test.parser === TYPESCRIPT_ESLINT_PARSER) {
+ throw new Error(DUPLICATE_PARSER_ERROR_MESSAGE);
+ }
+ if (!test.filename) {
+ return {
+ ...test,
+ filename: getFilename(test.parserOptions),
+ };
+ }
+ return test;
+ };
+
+ const normalizedTests = {
+ valid: rawTests.valid
+ .map(test => {
+ if (typeof test === 'string') {
+ return { code: test };
+ }
+ return test;
+ })
+ .map(normalizeTest),
+ invalid: rawTests.invalid.map(normalizeTest),
+ };
+
+ // convenience iterator to make it easy to loop all tests without a concat
+ const allTestsIterator = {
+ *[Symbol.iterator](): Generator, void, unknown> {
+ for (const testCase of normalizedTests.valid) {
+ yield testCase;
+ }
+ for (const testCase of normalizedTests.invalid) {
+ yield testCase;
+ }
+ },
+ };
+
+ const hasOnly = ((): boolean => {
+ for (const test of allTestsIterator) {
+ if (test.only) {
+ return true;
+ }
+ }
+ return false;
+ })();
+ if (hasOnly) {
+ // if there is an `only: true` - don't try apply constraints - assume that
+ // we are in "local development" mode rather than "CI validation" mode
+ return normalizedTests;
+ }
+
+ const hasConstraints = ((): boolean => {
+ for (const test of allTestsIterator) {
+ if (
+ test.dependencyConstraints &&
+ Object.keys(test.dependencyConstraints).length > 0
+ ) {
+ return true;
+ }
+ }
+ return false;
+ })();
+ if (!hasConstraints) {
+ return normalizedTests;
+ }
+
+ /*
+ Mark all unsatisfactory tests as `skip: true`.
+ We do this instead of just omitting the tests entirely because it gives the
+ test framework the opportunity to log the test as skipped rather than the test
+ just disappearing without a trace.
+ */
+ const maybeMarkAsOnly = <
+ T extends
+ | ValidTestCase
+ | InvalidTestCase,
+ >(
+ test: T,
+ ): T => {
+ return {
+ ...test,
+ skip: !satisfiesAllDependencyConstraints(test.dependencyConstraints),
+ };
+ };
+ normalizedTests.valid = normalizedTests.valid.map(maybeMarkAsOnly);
+ normalizedTests.invalid = normalizedTests.invalid.map(maybeMarkAsOnly);
+
+ return normalizedTests;
+ }
+
+ /**
+ * Adds a new rule test to execute.
+ */
+ run(
+ ruleName: string,
+ rule: RuleModule,
+ test: RunTests,
+ ): void {
+ const constructor = this.constructor as typeof RuleTester;
+
+ if (
+ this.#testerConfig.dependencyConstraints &&
+ !satisfiesAllDependencyConstraints(
+ this.#testerConfig.dependencyConstraints,
+ )
+ ) {
+ // for frameworks like mocha or jest that have a "skip" version of their function
+ // we can provide a nice skipped test!
+ constructor.describeSkip(ruleName, () => {
+ constructor.it(
+ 'All tests skipped due to unsatisfied constructor dependency constraints',
+ () => {
+ // some frameworks error if there are no assertions
+ assert.equal(true, true);
+ },
+ );
+ });
+
+ // don't run any tests because we don't match the base constraint
+ return;
+ }
+
+ if (!test || typeof test !== 'object') {
+ throw new TypeError(
+ `Test Scenarios for rule ${ruleName} : Could not find test scenario object`,
+ );
+ }
+
+ const scenarioErrors: string[] = [];
+ REQUIRED_SCENARIOS.forEach(scenarioType => {
+ if (!test[scenarioType]) {
+ scenarioErrors.push(
+ `Could not find any ${scenarioType} test scenarios`,
+ );
+ }
+ });
+
+ if (scenarioErrors.length > 0) {
+ throw new Error(
+ [
+ `Test Scenarios for rule ${ruleName} is invalid:`,
+ ...scenarioErrors,
+ ].join('\n'),
+ );
+ }
+
+ if (typeof rule === 'function') {
+ emitLegacyRuleAPIWarning(ruleName);
+ }
+
+ this.#linter.defineRule(
+ ruleName,
+ Object.assign({}, rule, {
+ // Create a wrapper rule that freezes the `context` properties.
+ create(context: RuleContext) {
+ freezeDeeply(context.options);
+ freezeDeeply(context.settings);
+ freezeDeeply(context.parserOptions);
+
+ return (typeof rule === 'function' ? rule : rule.create)(context);
+ },
+ }),
+ );
+
+ this.#linter.defineRules(this.#rules);
+
+ const normalizedTests = this.#normalizeTests(test);
+
+ function getTestMethod(
+ test: ValidTestCase,
+ ): 'it' | 'itOnly' | 'itSkip' {
+ if (test.skip) {
+ return 'itSkip';
+ }
+ if (test.only) {
+ return 'itOnly';
+ }
+ return 'it';
+ }
+
+ /*
+ * This creates a test suite and pipes all supplied info through
+ * one of the templates above.
+ */
+ constructor.describe(ruleName, () => {
+ constructor.describe('valid', () => {
+ normalizedTests.valid.forEach(valid => {
+ const testName = ((): string => {
+ if (valid.name == null || valid.name.length === 0) {
+ return valid.code;
+ }
+ return valid.name;
+ })();
+ constructor[getTestMethod(valid)](sanitize(testName), () => {
+ this.#testValidTemplate(ruleName, rule, valid);
+ });
+ });
+ });
+
+ constructor.describe('invalid', () => {
+ normalizedTests.invalid.forEach(invalid => {
+ const name = ((): string => {
+ if (invalid.name == null || invalid.name.length === 0) {
+ return invalid.code;
+ }
+ return invalid.name;
+ })();
+ constructor[getTestMethod(invalid)](sanitize(name), () => {
+ this.#testInvalidTemplate(ruleName, rule, invalid);
+ });
+ });
+ });
+ });
+ }
+
+ /**
+ * Run the rule for the given item
+ * @throws {Error} If an invalid schema.
+ * Use @private instead of #private to expose it for testing purposes
+ */
+ private runRuleForItem<
+ TMessageIds extends string,
+ TOptions extends readonly unknown[],
+ >(
+ ruleName: string,
+ rule: RuleModule,
+ item: ValidTestCase | InvalidTestCase,
+ ): {
+ messages: Linter.LintMessage[];
+ output: string;
+ beforeAST: TSESTree.Program;
+ afterAST: TSESTree.Program;
+ } {
+ let config: TesterConfigWithDefaults = merge({}, this.#testerConfig);
+ let code;
+ let filename;
+ let output;
+ let beforeAST: TSESTree.Program;
+ let afterAST: TSESTree.Program;
+
+ if (typeof item === 'string') {
+ code = item;
+ } else {
+ code = item.code;
+
+ /*
+ * Assumes everything on the item is a config except for the
+ * parameters used by this tester
+ */
+ const itemConfig: Record = { ...item };
+
+ for (const parameter of RULE_TESTER_PARAMETERS) {
+ delete itemConfig[parameter];
+ }
+
+ /*
+ * Create the config object from the tester config and this item
+ * specific configurations.
+ */
+ config = merge(config, itemConfig);
+ }
+
+ if (item.filename) {
+ filename = item.filename;
+ }
+
+ if (hasOwnProperty(item, 'options')) {
+ assert(Array.isArray(item.options), 'options must be an array');
+ if (
+ item.options.length > 0 &&
+ typeof rule === 'object' &&
+ (!rule.meta || (rule.meta && rule.meta.schema == null))
+ ) {
+ emitMissingSchemaWarning(ruleName);
+ }
+ config.rules[ruleName] = ['error', ...item.options];
+ } else {
+ config.rules[ruleName] = 'error';
+ }
+
+ const schema = getRuleOptionsSchema(rule);
+
+ /*
+ * Setup AST getters.
+ * The goal is to check whether or not AST was modified when
+ * running the rule under test.
+ */
+ this.#linter.defineRule('rule-tester/validate-ast', {
+ create() {
+ return {
+ Program(node): void {
+ beforeAST = cloneDeeplyExcludesParent(node);
+ },
+ 'Program:exit'(node): void {
+ afterAST = node;
+ },
+ };
+ },
+ });
+
+ if (typeof config.parser === 'string') {
+ assert(
+ path.isAbsolute(config.parser),
+ 'Parsers provided as strings to RuleTester must be absolute paths',
+ );
+ } else {
+ config.parser = require.resolve(TYPESCRIPT_ESLINT_PARSER);
+ }
+
+ this.#linter.defineParser(
+ config.parser,
+ wrapParser(require(config.parser) as Linter.ParserModule),
+ );
+
+ if (schema) {
+ ajv.validateSchema(schema);
+
+ if (ajv.errors) {
+ const errors = ajv.errors
+ .map(error => {
+ const field =
+ error.dataPath[0] === '.'
+ ? error.dataPath.slice(1)
+ : error.dataPath;
+
+ return `\t${field}: ${error.message}`;
+ })
+ .join('\n');
+
+ throw new Error(
+ [`Schema for rule ${ruleName} is invalid:`, errors].join(
+ // no space after comma to match eslint core
+ ',',
+ ),
+ );
+ }
+
+ /*
+ * `ajv.validateSchema` checks for errors in the structure of the schema (by comparing the schema against a "meta-schema"),
+ * and it reports those errors individually. However, there are other types of schema errors that only occur when compiling
+ * the schema (e.g. using invalid defaults in a schema), and only one of these errors can be reported at a time. As a result,
+ * the schema is compiled here separately from checking for `validateSchema` errors.
+ */
+ try {
+ ajv.compile(schema);
+ } catch (err) {
+ throw new Error(
+ `Schema for rule ${ruleName} is invalid: ${(err as Error).message}`,
+ );
+ }
+ }
+
+ validate(config, 'rule-tester', id => (id === ruleName ? rule : null));
+
+ // Verify the code.
+ // @ts-expect-error -- we don't define deprecated members on our types
+ const { getComments } = SourceCode.prototype as { getComments: unknown };
+ let messages;
+
+ try {
+ // @ts-expect-error -- we don't define deprecated members on our types
+ SourceCode.prototype.getComments = getCommentsDeprecation;
+ messages = this.#linter.verify(code, config, filename);
+ } finally {
+ // @ts-expect-error -- we don't define deprecated members on our types
+ SourceCode.prototype.getComments = getComments;
+ }
+
+ const fatalErrorMessage = messages.find(m => m.fatal);
+
+ assert(
+ !fatalErrorMessage,
+ `A fatal parsing error occurred: ${fatalErrorMessage?.message}`,
+ );
+
+ // Verify if autofix makes a syntax error or not.
+ if (messages.some(m => m.fix)) {
+ output = SourceCodeFixer.applyFixes(code, messages).output;
+ const errorMessageInFix = this.#linter
+ .verify(output, config, filename)
+ .find(m => m.fatal);
+
+ assert(
+ !errorMessageInFix,
+ [
+ 'A fatal parsing error occurred in autofix.',
+ `Error: ${errorMessageInFix?.message}`,
+ 'Autofix output:',
+ output,
+ ].join('\n'),
+ );
+ } else {
+ output = code;
+ }
+
+ return {
+ messages,
+ output,
+ // is definitely assigned within the `rule-tester/validate-ast` rule
+ beforeAST: beforeAST!,
+ // is definitely assigned within the `rule-tester/validate-ast` rule
+ afterAST: cloneDeeplyExcludesParent(afterAST!),
+ };
+ }
+
+ /**
+ * Check if the template is valid or not
+ * all valid cases go through this
+ */
+ #testValidTemplate<
+ TMessageIds extends string,
+ TOptions extends readonly unknown[],
+ >(
+ ruleName: string,
+ rule: RuleModule,
+ itemIn: string | ValidTestCase,
+ ): void {
+ const item: ValidTestCase =
+ typeof itemIn === 'object' ? itemIn : { code: itemIn };
+
+ assert.ok(
+ typeof item.code === 'string',
+ "Test case must specify a string value for 'code'",
+ );
+ if (item.name) {
+ assert.ok(
+ typeof item.name === 'string',
+ "Optional test case property 'name' must be a string",
+ );
+ }
+
+ const result = this.runRuleForItem(ruleName, rule, item);
+ const messages = result.messages;
+
+ assert.strictEqual(
+ messages.length,
+ 0,
+ util.format(
+ 'Should have no errors but had %d: %s',
+ messages.length,
+ util.inspect(messages),
+ ),
+ );
+
+ assertASTDidntChange(result.beforeAST, result.afterAST);
+ }
+
+ /**
+ * Check if the template is invalid or not
+ * all invalid cases go through this.
+ */
+ #testInvalidTemplate<
+ TMessageIds extends string,
+ TOptions extends readonly unknown[],
+ >(
+ ruleName: string,
+ rule: RuleModule,
+ item: InvalidTestCase,
+ ): void {
+ assert.ok(
+ typeof item.code === 'string',
+ "Test case must specify a string value for 'code'",
+ );
+ if (item.name) {
+ assert.ok(
+ typeof item.name === 'string',
+ "Optional test case property 'name' must be a string",
+ );
+ }
+ assert.ok(
+ item.errors || item.errors === 0,
+ `Did not specify errors for an invalid test of ${ruleName}`,
+ );
+
+ if (Array.isArray(item.errors) && item.errors.length === 0) {
+ assert.fail('Invalid cases must have at least one error');
+ }
+
+ const ruleHasMetaMessages =
+ hasOwnProperty(rule, 'meta') && hasOwnProperty(rule.meta, 'messages');
+ const friendlyIDList = ruleHasMetaMessages
+ ? `[${Object.keys(rule.meta.messages)
+ .map(key => `'${key}'`)
+ .join(', ')}]`
+ : null;
+
+ const result = this.runRuleForItem(ruleName, rule, item);
+ const messages = result.messages;
+
+ if (typeof item.errors === 'number') {
+ if (item.errors === 0) {
+ assert.fail("Invalid cases must have 'error' value greater than 0");
+ }
+
+ assert.strictEqual(
+ messages.length,
+ item.errors,
+ util.format(
+ 'Should have %d error%s but had %d: %s',
+ item.errors,
+ item.errors === 1 ? '' : 's',
+ messages.length,
+ util.inspect(messages),
+ ),
+ );
+ } else {
+ assert.strictEqual(
+ messages.length,
+ item.errors.length,
+ util.format(
+ 'Should have %d error%s but had %d: %s',
+ item.errors.length,
+ item.errors.length === 1 ? '' : 's',
+ messages.length,
+ util.inspect(messages),
+ ),
+ );
+
+ const hasMessageOfThisRule = messages.some(m => m.ruleId === ruleName);
+
+ for (let i = 0, l = item.errors.length; i < l; i++) {
+ const error = item.errors[i];
+ const message = messages[i];
+
+ assert(
+ hasMessageOfThisRule,
+ 'Error rule name should be the same as the name of the rule being tested',
+ );
+
+ if (typeof error === 'string' || error instanceof RegExp) {
+ // Just an error message.
+ assertMessageMatches(message.message, error);
+ } else if (typeof error === 'object' && error != null) {
+ /*
+ * Error object.
+ * This may have a message, messageId, data, node type, line, and/or
+ * column.
+ */
+
+ Object.keys(error).forEach(propertyName => {
+ assert.ok(
+ ERROR_OBJECT_PARAMETERS.has(propertyName),
+ `Invalid error property name '${propertyName}'. Expected one of ${FRIENDLY_ERROR_OBJECT_PARAMETER_LIST}.`,
+ );
+ });
+
+ // @ts-expect-error -- we purposely don't define `message` on our types as the current standard is `messageId`
+ if (hasOwnProperty(error, 'message')) {
+ assert.ok(
+ !hasOwnProperty(error, 'messageId'),
+ "Error should not specify both 'message' and a 'messageId'.",
+ );
+ assert.ok(
+ !hasOwnProperty(error, 'data'),
+ "Error should not specify both 'data' and 'message'.",
+ );
+ assertMessageMatches(
+ message.message,
+ // @ts-expect-error -- we purposely don't define `message` on our types as the current standard is `messageId`
+ error.message as unknown,
+ );
+ } else if (hasOwnProperty(error, 'messageId')) {
+ assert.ok(
+ ruleHasMetaMessages,
+ "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'.",
+ );
+ if (!hasOwnProperty(rule.meta.messages, error.messageId)) {
+ assert(
+ false,
+ `Invalid messageId '${error.messageId}'. Expected one of ${friendlyIDList}.`,
+ );
+ }
+ assert.strictEqual(
+ message.messageId,
+ error.messageId,
+ `messageId '${message.messageId}' does not match expected messageId '${error.messageId}'.`,
+ );
+ if (hasOwnProperty(error, 'data')) {
+ /*
+ * if data was provided, then directly compare the returned message to a synthetic
+ * interpolated message using the same message ID and data provided in the test.
+ * See https://github.com/eslint/eslint/issues/9890 for context.
+ */
+ const unformattedOriginalMessage =
+ rule.meta.messages[error.messageId];
+ const rehydratedMessage = interpolate(
+ unformattedOriginalMessage,
+ error.data,
+ );
+
+ assert.strictEqual(
+ message.message,
+ rehydratedMessage,
+ `Hydrated message "${rehydratedMessage}" does not match "${message.message}"`,
+ );
+ }
+ }
+
+ assert.ok(
+ hasOwnProperty(error, 'data')
+ ? hasOwnProperty(error, 'messageId')
+ : true,
+ "Error must specify 'messageId' if 'data' is used.",
+ );
+
+ if (error.type) {
+ assert.strictEqual(
+ message.nodeType,
+ error.type,
+ `Error type should be ${error.type}, found ${message.nodeType}`,
+ );
+ }
+
+ if (hasOwnProperty(error, 'line')) {
+ assert.strictEqual(
+ message.line,
+ error.line,
+ `Error line should be ${error.line}`,
+ );
+ }
+
+ if (hasOwnProperty(error, 'column')) {
+ assert.strictEqual(
+ message.column,
+ error.column,
+ `Error column should be ${error.column}`,
+ );
+ }
+
+ if (hasOwnProperty(error, 'endLine')) {
+ assert.strictEqual(
+ message.endLine,
+ error.endLine,
+ `Error endLine should be ${error.endLine}`,
+ );
+ }
+
+ if (hasOwnProperty(error, 'endColumn')) {
+ assert.strictEqual(
+ message.endColumn,
+ error.endColumn,
+ `Error endColumn should be ${error.endColumn}`,
+ );
+ }
+
+ if (hasOwnProperty(error, 'suggestions')) {
+ // Support asserting there are no suggestions
+ if (
+ !error.suggestions ||
+ (isReadonlyArray(error.suggestions) &&
+ error.suggestions.length === 0)
+ ) {
+ if (
+ Array.isArray(message.suggestions) &&
+ message.suggestions.length > 0
+ ) {
+ assert.fail(
+ `Error should have no suggestions on error with message: "${message.message}"`,
+ );
+ }
+ } else {
+ assert(
+ Array.isArray(message.suggestions),
+ `Error should have an array of suggestions. Instead received "${String(
+ message.suggestions,
+ )}" on error with message: "${message.message}"`,
+ );
+ const messageSuggestions = message.suggestions;
+ assert.strictEqual(
+ messageSuggestions.length,
+ error.suggestions.length,
+ `Error should have ${error.suggestions.length} suggestions. Instead found ${messageSuggestions.length} suggestions`,
+ );
+
+ error.suggestions.forEach((expectedSuggestion, index) => {
+ assert.ok(
+ typeof expectedSuggestion === 'object' &&
+ expectedSuggestion != null,
+ "Test suggestion in 'suggestions' array must be an object.",
+ );
+ Object.keys(expectedSuggestion).forEach(propertyName => {
+ assert.ok(
+ SUGGESTION_OBJECT_PARAMETERS.has(propertyName),
+ `Invalid suggestion property name '${propertyName}'. Expected one of ${FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST}.`,
+ );
+ });
+
+ const actualSuggestion = messageSuggestions[index];
+ const suggestionPrefix = `Error Suggestion at index ${index} :`;
+
+ // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId`
+ if (hasOwnProperty(expectedSuggestion, 'desc')) {
+ assert.ok(
+ !hasOwnProperty(expectedSuggestion, 'data'),
+ `${suggestionPrefix} Test should not specify both 'desc' and 'data'.`,
+ );
+ // @ts-expect-error -- we purposely don't define `desc` on our types as the current standard is `messageId`
+ const expectedDesc = expectedSuggestion.desc as string;
+ assert.strictEqual(
+ actualSuggestion.desc,
+ expectedDesc,
+ `${suggestionPrefix} desc should be "${expectedDesc}" but got "${actualSuggestion.desc}" instead.`,
+ );
+ }
+
+ if (hasOwnProperty(expectedSuggestion, 'messageId')) {
+ assert.ok(
+ ruleHasMetaMessages,
+ `${suggestionPrefix} Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.`,
+ );
+ assert.ok(
+ hasOwnProperty(
+ rule.meta.messages,
+ expectedSuggestion.messageId,
+ ),
+ `${suggestionPrefix} Test has invalid messageId '${expectedSuggestion.messageId}', the rule under test allows only one of ${friendlyIDList}.`,
+ );
+ assert.strictEqual(
+ actualSuggestion.messageId,
+ expectedSuggestion.messageId,
+ `${suggestionPrefix} messageId should be '${expectedSuggestion.messageId}' but got '${actualSuggestion.messageId}' instead.`,
+ );
+ if (hasOwnProperty(expectedSuggestion, 'data')) {
+ const unformattedMetaMessage =
+ rule.meta.messages[expectedSuggestion.messageId];
+ const rehydratedDesc = interpolate(
+ unformattedMetaMessage,
+ expectedSuggestion.data,
+ );
+
+ assert.strictEqual(
+ actualSuggestion.desc,
+ rehydratedDesc,
+ `${suggestionPrefix} Hydrated test desc "${rehydratedDesc}" does not match received desc "${actualSuggestion.desc}".`,
+ );
+ }
+ } else {
+ assert.ok(
+ !hasOwnProperty(expectedSuggestion, 'data'),
+ `${suggestionPrefix} Test must specify 'messageId' if 'data' is used.`,
+ );
+ }
+
+ if (hasOwnProperty(expectedSuggestion, 'output')) {
+ const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(
+ item.code,
+ [actualSuggestion],
+ ).output;
+
+ assert.strictEqual(
+ codeWithAppliedSuggestion,
+ expectedSuggestion.output,
+ `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`,
+ );
+ }
+ });
+ }
+ }
+ } else {
+ // Message was an unexpected type
+ assert.fail(
+ `Error should be a string, object, or RegExp, but found (${util.inspect(
+ message,
+ )})`,
+ );
+ }
+ }
+ }
+
+ if (hasOwnProperty(item, 'output')) {
+ if (item.output == null) {
+ assert.strictEqual(
+ result.output,
+ item.code,
+ 'Expected no autofixes to be suggested',
+ );
+ } else {
+ assert.strictEqual(result.output, item.output, 'Output is incorrect.');
+ }
+ } else {
+ assert.strictEqual(
+ result.output,
+ item.code,
+ "The rule fixed the code. Please add 'output' property.",
+ );
+ }
+
+ assertASTDidntChange(result.beforeAST, result.afterAST);
+ }
+}
+
+/**
+ * Check if the AST was changed
+ */
+function assertASTDidntChange(beforeAST: unknown, afterAST: unknown): void {
+ assert.deepStrictEqual(beforeAST, afterAST, 'Rule should not modify AST.');
+}
+
+/**
+ * Asserts that the message matches its expected value. If the expected
+ * value is a regular expression, it is checked against the actual
+ * value.
+ */
+function assertMessageMatches(actual: string, expected: string | RegExp): void {
+ if (expected instanceof RegExp) {
+ // assert.js doesn't have a built-in RegExp match function
+ assert.ok(
+ expected.test(actual),
+ `Expected '${actual}' to match ${expected}`,
+ );
+ } else {
+ assert.strictEqual(actual, expected);
+ }
+}
diff --git a/packages/rule-tester/src/TestFramework.ts b/packages/rule-tester/src/TestFramework.ts
new file mode 100644
index 000000000000..dea77d746249
--- /dev/null
+++ b/packages/rule-tester/src/TestFramework.ts
@@ -0,0 +1,220 @@
+/**
+ * @param text a string describing the rule
+ * @param callback the test callback
+ */
+export type RuleTesterTestFrameworkFunctionBase = (
+ text: string,
+ callback: () => void,
+) => void;
+export type RuleTesterTestFrameworkFunction =
+ RuleTesterTestFrameworkFunctionBase & {
+ /**
+ * Skips running the tests inside this `describe` for the current file
+ */
+ skip?: RuleTesterTestFrameworkFunctionBase;
+ };
+export type RuleTesterTestFrameworkItFunction =
+ RuleTesterTestFrameworkFunctionBase & {
+ /**
+ * Only runs this test in the current file.
+ */
+ only?: RuleTesterTestFrameworkFunctionBase;
+ /**
+ * Skips running this test in the current file.
+ */
+ skip?: RuleTesterTestFrameworkFunctionBase;
+ };
+
+type Maybe = T | null | undefined;
+
+/**
+ * @param fn a callback called after all the tests are done
+ */
+type AfterAll = (fn: () => void) => void;
+
+let OVERRIDE_AFTER_ALL: Maybe = null;
+let OVERRIDE_DESCRIBE: Maybe = null;
+let OVERRIDE_DESCRIBE_SKIP: Maybe = null;
+let OVERRIDE_IT: Maybe = null;
+let OVERRIDE_IT_ONLY: Maybe = null;
+let OVERRIDE_IT_SKIP: Maybe = null;
+
+/*
+ * NOTE - If people use `mocha test.js --watch` command, the test function
+ * instances are different for each execution.
+ * This is why the getters get fresh instance always.
+ */
+
+/**
+ * Defines a test framework used by the rule tester
+ * This class defaults to using functions defined on the global scope, but also
+ * allows the user to manually supply functions in case they want to roll their
+ * own tooling
+ */
+export abstract class TestFramework {
+ /**
+ * Runs a function after all the tests in this file have completed.
+ */
+ static get afterAll(): AfterAll {
+ if (OVERRIDE_AFTER_ALL != null) {
+ return OVERRIDE_AFTER_ALL;
+ }
+ if (typeof afterAll === 'function') {
+ return afterAll;
+ }
+ throw new Error(
+ 'Missing definition for `afterAll` - you must set one using `RuleTester.afterAll` or there must be one defined globally as `afterAll`.',
+ );
+ }
+ static set afterAll(value: Maybe) {
+ OVERRIDE_AFTER_ALL = value;
+ }
+
+ /**
+ * Creates a test grouping
+ */
+ static get describe(): RuleTesterTestFrameworkFunction {
+ if (OVERRIDE_DESCRIBE != null) {
+ return OVERRIDE_DESCRIBE;
+ }
+ if (typeof describe === 'function') {
+ return describe;
+ }
+ throw new Error(
+ 'Missing definition for `describe` - you must set one using `RuleTester.describe` or there must be one defined globally as `describe`.',
+ );
+ }
+ static set describe(value: Maybe) {
+ OVERRIDE_DESCRIBE = value;
+ }
+
+ /**
+ * Skips running the tests inside this `describe` for the current file
+ */
+ static get describeSkip(): RuleTesterTestFrameworkFunctionBase {
+ if (OVERRIDE_DESCRIBE_SKIP != null) {
+ return OVERRIDE_DESCRIBE_SKIP;
+ }
+ if (
+ typeof OVERRIDE_DESCRIBE === 'function' &&
+ typeof OVERRIDE_DESCRIBE.skip === 'function'
+ ) {
+ return OVERRIDE_DESCRIBE.skip.bind(OVERRIDE_DESCRIBE);
+ }
+ if (typeof describe === 'function' && typeof describe.skip === 'function') {
+ return describe.skip.bind(describe);
+ }
+ if (
+ typeof OVERRIDE_DESCRIBE === 'function' ||
+ typeof OVERRIDE_IT === 'function'
+ ) {
+ throw new Error(
+ 'Set `RuleTester.describeSkip` to use `dependencyConstraints` with a custom test framework.',
+ );
+ }
+ if (typeof describe === 'function') {
+ throw new Error(
+ 'The current test framework does not support skipping tests tests with `dependencyConstraints`.',
+ );
+ }
+ throw new Error(
+ 'Missing definition for `describeSkip` - you must set one using `RuleTester.describeSkip` or there must be one defined globally as `describe.skip`.',
+ );
+ }
+ static set describeSkip(value: Maybe) {
+ OVERRIDE_DESCRIBE_SKIP = value;
+ }
+
+ /**
+ * Creates a test closure
+ */
+ static get it(): RuleTesterTestFrameworkItFunction {
+ if (OVERRIDE_IT != null) {
+ return OVERRIDE_IT;
+ }
+ if (typeof it === 'function') {
+ return it;
+ }
+ throw new Error(
+ 'Missing definition for `it` - you must set one using `RuleTester.it` or there must be one defined globally as `it`.',
+ );
+ }
+ static set it(value: Maybe) {
+ OVERRIDE_IT = value;
+ }
+
+ /**
+ * Only runs this test in the current file.
+ */
+ static get itOnly(): RuleTesterTestFrameworkFunctionBase {
+ if (OVERRIDE_IT_ONLY != null) {
+ return OVERRIDE_IT_ONLY;
+ }
+ if (
+ typeof OVERRIDE_IT === 'function' &&
+ typeof OVERRIDE_IT.only === 'function'
+ ) {
+ return OVERRIDE_IT.only.bind(OVERRIDE_IT);
+ }
+ if (typeof it === 'function' && typeof it.only === 'function') {
+ return it.only.bind(it);
+ }
+ if (
+ typeof OVERRIDE_DESCRIBE === 'function' ||
+ typeof OVERRIDE_IT === 'function'
+ ) {
+ throw new Error(
+ 'Set `RuleTester.itOnly` to use `only` with a custom test framework.\n' +
+ 'See https://eslint.org/docs/latest/integrate/nodejs-api#customizing-ruletester for more.',
+ );
+ }
+ if (typeof it === 'function') {
+ throw new Error(
+ 'The current test framework does not support exclusive tests with `only`.',
+ );
+ }
+ throw new Error(
+ 'Missing definition for `itOnly` - you must set one using `RuleTester.itOnly` or there must be one defined globally as `it.only`.',
+ );
+ }
+ static set itOnly(value: Maybe) {
+ OVERRIDE_IT_ONLY = value;
+ }
+
+ /**
+ * Skips running this test in the current file.
+ */
+ static get itSkip(): RuleTesterTestFrameworkFunctionBase {
+ if (OVERRIDE_IT_SKIP != null) {
+ return OVERRIDE_IT_SKIP;
+ }
+ if (
+ typeof OVERRIDE_IT === 'function' &&
+ typeof OVERRIDE_IT.skip === 'function'
+ ) {
+ return OVERRIDE_IT.skip.bind(OVERRIDE_IT);
+ }
+ if (typeof it === 'function' && typeof it.skip === 'function') {
+ return it.skip.bind(it);
+ }
+ if (
+ typeof OVERRIDE_DESCRIBE === 'function' ||
+ typeof OVERRIDE_IT === 'function'
+ ) {
+ throw new Error(
+ 'Set `RuleTester.itSkip` to use `only` with a custom test framework.',
+ );
+ }
+ if (typeof it === 'function') {
+ throw new Error(
+ 'The current test framework does not support exclusive tests with `only`.',
+ );
+ }
+ throw new Error(
+ 'Missing definition for `itSkip` - you must set one using `RuleTester.itSkip` or there must be one defined globally as `it.only`.',
+ );
+ }
+ static set itSkip(value: Maybe) {
+ OVERRIDE_IT_SKIP = value;
+ }
+}
diff --git a/packages/rule-tester/src/index.ts b/packages/rule-tester/src/index.ts
new file mode 100644
index 000000000000..6ea08fc5addb
--- /dev/null
+++ b/packages/rule-tester/src/index.ts
@@ -0,0 +1,16 @@
+export { RuleTester } from './RuleTester';
+export { noFormat } from './noFormat';
+export type {
+ InvalidTestCase,
+ RuleTesterConfig,
+ RunTests,
+ SuggestionOutput,
+ TestCaseError,
+ ValidTestCase,
+} from './types';
+export type {
+ AtLeastVersionConstraint,
+ DependencyConstraint,
+ SemverVersionConstraint,
+ VersionConstraint,
+} from './types/DependencyConstraint';
diff --git a/packages/rule-tester/src/noFormat.ts b/packages/rule-tester/src/noFormat.ts
new file mode 100644
index 000000000000..e52001cdb662
--- /dev/null
+++ b/packages/rule-tester/src/noFormat.ts
@@ -0,0 +1,7 @@
+/**
+ * Simple no-op tag to mark code samples as "should not format with prettier"
+ * for the plugin-test-formatting lint rule
+ */
+export function noFormat(raw: TemplateStringsArray, ...keys: string[]): string {
+ return String.raw({ raw }, ...keys);
+}
diff --git a/packages/rule-tester/src/types/DependencyConstraint.ts b/packages/rule-tester/src/types/DependencyConstraint.ts
new file mode 100644
index 000000000000..37cb3d5a1805
--- /dev/null
+++ b/packages/rule-tester/src/types/DependencyConstraint.ts
@@ -0,0 +1,20 @@
+import type { RangeOptions } from 'semver';
+
+export interface SemverVersionConstraint {
+ readonly range: string;
+ readonly options?: boolean | RangeOptions;
+}
+export type AtLeastVersionConstraint =
+ | `${number}`
+ | `${number}.${number}`
+ | `${number}.${number}.${number}`
+ | `${number}.${number}.${number}-${string}`;
+export type VersionConstraint =
+ | SemverVersionConstraint
+ | AtLeastVersionConstraint;
+export interface DependencyConstraint {
+ /**
+ * Passing a string for the value is shorthand for a '>=' constraint
+ */
+ readonly [packageName: string]: VersionConstraint;
+}
diff --git a/packages/rule-tester/src/types/InvalidTestCase.ts b/packages/rule-tester/src/types/InvalidTestCase.ts
new file mode 100644
index 000000000000..1bef9e2b89fb
--- /dev/null
+++ b/packages/rule-tester/src/types/InvalidTestCase.ts
@@ -0,0 +1,80 @@
+import type { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';
+import type { ReportDescriptorMessageData } from '@typescript-eslint/utils/ts-eslint';
+
+import type { DependencyConstraint } from './DependencyConstraint';
+import type { ValidTestCase } from './ValidTestCase';
+
+export interface SuggestionOutput {
+ /**
+ * Reported message ID.
+ */
+ readonly messageId: TMessageIds;
+ /**
+ * The data used to fill the message template.
+ */
+ readonly data?: ReportDescriptorMessageData;
+ /**
+ * NOTE: Suggestions will be applied as a stand-alone change, without triggering multi-pass fixes.
+ * Each individual error has its own suggestion, so you have to show the correct, _isolated_ output for each suggestion.
+ */
+ readonly output: string;
+
+ // we disallow this because it's much better to use messageIds for reusable errors that are easily testable
+ // readonly desc?: string;
+}
+
+export interface TestCaseError {
+ /**
+ * The 1-based column number of the reported start location.
+ */
+ readonly column?: number;
+ /**
+ * The data used to fill the message template.
+ */
+ readonly data?: ReportDescriptorMessageData;
+ /**
+ * The 1-based column number of the reported end location.
+ */
+ readonly endColumn?: number;
+ /**
+ * The 1-based line number of the reported end location.
+ */
+ readonly endLine?: number;
+ /**
+ * The 1-based line number of the reported start location.
+ */
+ readonly line?: number;
+ /**
+ * Reported message ID.
+ */
+ readonly messageId: TMessageIds;
+ /**
+ * Reported suggestions.
+ */
+ readonly suggestions?: readonly SuggestionOutput[] | null;
+ /**
+ * The type of the reported AST node.
+ */
+ readonly type?: AST_NODE_TYPES | AST_TOKEN_TYPES;
+
+ // we disallow this because it's much better to use messageIds for reusable errors that are easily testable
+ // readonly message?: string | RegExp;
+}
+
+export interface InvalidTestCase<
+ TMessageIds extends string,
+ TOptions extends Readonly,
+> extends ValidTestCase {
+ /**
+ * Expected errors.
+ */
+ readonly errors: readonly TestCaseError[];
+ /**
+ * The expected code after autofixes are applied. If set to `null`, the test runner will assert that no autofix is suggested.
+ */
+ readonly output?: string | null;
+ /**
+ * Constraints that must pass in the current environment for the test to run
+ */
+ readonly dependencyConstraints?: DependencyConstraint;
+}
diff --git a/packages/rule-tester/src/types/RuleTesterConfig.ts b/packages/rule-tester/src/types/RuleTesterConfig.ts
new file mode 100644
index 000000000000..c722c5be074e
--- /dev/null
+++ b/packages/rule-tester/src/types/RuleTesterConfig.ts
@@ -0,0 +1,27 @@
+import type { Linter, ParserOptions } from '@typescript-eslint/utils/ts-eslint';
+
+import type { DependencyConstraint } from './DependencyConstraint';
+
+export interface RuleTesterConfig extends Linter.Config {
+ /**
+ * The default parser to use for tests.
+ * @default '@typescript-eslint/parser'
+ */
+ readonly parser: string;
+ /**
+ * The default parser options to use for tests.
+ */
+ readonly parserOptions?: Readonly;
+ /**
+ * Constraints that must pass in the current environment for any tests to run.
+ */
+ readonly dependencyConstraints?: DependencyConstraint;
+ /**
+ * The default filenames to use for type-aware tests.
+ * @default { ts: 'file.ts', tsx: 'react.tsx' }
+ */
+ readonly defaultFilenames?: Readonly<{
+ ts: string;
+ tsx: string;
+ }>;
+}
diff --git a/packages/rule-tester/src/types/ValidTestCase.ts b/packages/rule-tester/src/types/ValidTestCase.ts
new file mode 100644
index 000000000000..4aa2ef0aa6c4
--- /dev/null
+++ b/packages/rule-tester/src/types/ValidTestCase.ts
@@ -0,0 +1,57 @@
+import type {
+ ParserOptions,
+ SharedConfigurationSettings,
+} from '@typescript-eslint/utils/ts-eslint';
+
+import type { DependencyConstraint } from './DependencyConstraint';
+
+export interface ValidTestCase> {
+ /**
+ * Name for the test case.
+ */
+ readonly name?: string;
+ /**
+ * Code for the test case.
+ */
+ readonly code: string;
+ /**
+ * Environments for the test case.
+ */
+ readonly env?: Readonly>;
+ /**
+ * The fake filename for the test case. Useful for rules that make assertion about filenames.
+ */
+ readonly filename?: string;
+ /**
+ * The additional global variables.
+ */
+ readonly globals?: Record;
+ /**
+ * Options for the test case.
+ */
+ readonly options?: Readonly;
+ /**
+ * The absolute path for the parser.
+ */
+ readonly parser?: string;
+ /**
+ * Options for the parser.
+ */
+ readonly parserOptions?: Readonly;
+ /**
+ * Settings for the test case.
+ */
+ readonly settings?: Readonly;
+ /**
+ * Run this case exclusively for debugging in supported test frameworks.
+ */
+ readonly only?: boolean;
+ /**
+ * Skip this case in supported test frameworks.
+ */
+ readonly skip?: boolean;
+ /**
+ * Constraints that must pass in the current environment for the test to run
+ */
+ readonly dependencyConstraints?: DependencyConstraint;
+}
diff --git a/packages/rule-tester/src/types/index.ts b/packages/rule-tester/src/types/index.ts
new file mode 100644
index 000000000000..bebb6786af89
--- /dev/null
+++ b/packages/rule-tester/src/types/index.ts
@@ -0,0 +1,36 @@
+import type { InvalidTestCase } from './InvalidTestCase';
+import type { RuleTesterConfig } from './RuleTesterConfig';
+import type { ValidTestCase } from './ValidTestCase';
+
+type Mutable = {
+ -readonly [P in keyof T]: T[P];
+};
+export type TesterConfigWithDefaults = Mutable<
+ RuleTesterConfig &
+ Required>
+>;
+
+export interface RunTests<
+ TMessageIds extends string,
+ TOptions extends Readonly,
+> {
+ // RuleTester.run also accepts strings for valid cases
+ readonly valid: readonly (ValidTestCase | string)[];
+ readonly invalid: readonly InvalidTestCase[];
+}
+
+export interface NormalizedRunTests<
+ TMessageIds extends string,
+ TOptions extends Readonly,
+> {
+ readonly valid: readonly ValidTestCase[];
+ readonly invalid: readonly InvalidTestCase[];
+}
+
+export type { ValidTestCase } from './ValidTestCase';
+export type {
+ InvalidTestCase,
+ SuggestionOutput,
+ TestCaseError,
+} from './InvalidTestCase';
+export type { RuleTesterConfig } from './RuleTesterConfig';
diff --git a/packages/rule-tester/src/utils/SourceCodeFixer.ts b/packages/rule-tester/src/utils/SourceCodeFixer.ts
new file mode 100644
index 000000000000..9346b42b0803
--- /dev/null
+++ b/packages/rule-tester/src/utils/SourceCodeFixer.ts
@@ -0,0 +1,117 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/linter/source-code-fixer.js
+
+import type { Linter } from '@typescript-eslint/utils/ts-eslint';
+
+import { hasOwnProperty } from './hasOwnProperty';
+
+type LintMessage = Linter.LintMessage | Linter.LintSuggestion;
+type LintMessageWithFix = LintMessage & Required>;
+
+const BOM = '\uFEFF';
+
+/**
+ * Compares items in a messages array by range.
+ * @returns -1 if a comes before b, 1 if a comes after b, 0 if equal.
+ */
+function compareMessagesByFixRange(
+ a: LintMessageWithFix,
+ b: LintMessageWithFix,
+): number {
+ return a.fix.range[0] - b.fix.range[0] || a.fix.range[1] - b.fix.range[1];
+}
+
+/**
+ * Compares items in a messages array by line and column.
+ * @returns -1 if a comes before b, 1 if a comes after b, 0 if equal.
+ */
+function compareMessagesByLocation(a: LintMessage, b: LintMessage): number {
+ // @ts-expect-error -- it's not possible for suggestions to reach this location
+ return a.line - b.line || a.column - b.column;
+}
+
+/**
+ * Applies the fixes specified by the messages to the given text. Tries to be
+ * smart about the fixes and won't apply fixes over the same area in the text.
+ * @param sourceText The text to apply the changes to.
+ * @param messages The array of messages reported by ESLint.
+ * @returns {Object} An object containing the fixed text and any unfixed messages.
+ */
+export function applyFixes(
+ sourceText: string,
+ messages: readonly LintMessage[],
+): {
+ fixed: boolean;
+ messages: readonly LintMessage[];
+ output: string;
+} {
+ // clone the array
+ const remainingMessages: LintMessage[] = [];
+ const fixes: LintMessageWithFix[] = [];
+ const bom = sourceText.startsWith(BOM) ? BOM : '';
+ const text = bom ? sourceText.slice(1) : sourceText;
+ let lastPos = Number.NEGATIVE_INFINITY;
+ let output = bom;
+
+ /**
+ * Try to use the 'fix' from a problem.
+ * @param {Message} problem The message object to apply fixes from
+ * @returns {boolean} Whether fix was successfully applied
+ */
+ function attemptFix(problem: LintMessageWithFix): boolean {
+ const fix = problem.fix;
+ const start = fix.range[0];
+ const end = fix.range[1];
+
+ // Remain it as a problem if it's overlapped or it's a negative range
+ if (lastPos >= start || start > end) {
+ remainingMessages.push(problem);
+ return false;
+ }
+
+ // Remove BOM.
+ if ((start < 0 && end >= 0) || (start === 0 && fix.text.startsWith(BOM))) {
+ output = '';
+ }
+
+ // Make output to this fix.
+ output += text.slice(Math.max(0, lastPos), Math.max(0, start));
+ output += fix.text;
+ lastPos = end;
+ return true;
+ }
+
+ messages.forEach(problem => {
+ if (hasOwnProperty(problem, 'fix')) {
+ fixes.push(problem);
+ } else {
+ remainingMessages.push(problem);
+ }
+ });
+
+ if (fixes.length) {
+ let fixesWereApplied = false;
+
+ for (const problem of fixes.sort(compareMessagesByFixRange)) {
+ attemptFix(problem);
+
+ /*
+ * The only time attemptFix will fail is if a previous fix was
+ * applied which conflicts with it. So we can mark this as true.
+ */
+ fixesWereApplied = true;
+ }
+ output += text.slice(Math.max(0, lastPos));
+
+ return {
+ fixed: fixesWereApplied,
+ messages: remainingMessages.sort(compareMessagesByLocation),
+ output,
+ };
+ }
+
+ return {
+ fixed: false,
+ messages,
+ output: bom + text,
+ };
+}
diff --git a/packages/rule-tester/src/utils/ajv.ts b/packages/rule-tester/src/utils/ajv.ts
new file mode 100644
index 000000000000..f3dcacc641d4
--- /dev/null
+++ b/packages/rule-tester/src/utils/ajv.ts
@@ -0,0 +1,23 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/shared/ajv.js
+
+import Ajv from 'ajv';
+import metaSchema from 'ajv/lib/refs/json-schema-draft-04.json';
+
+export function ajvBuilder(additionalOptions = {}): Ajv.Ajv {
+ const ajv = new Ajv({
+ meta: false,
+ useDefaults: true,
+ validateSchema: false,
+ missingRefs: 'ignore',
+ verbose: true,
+ schemaId: 'auto',
+ ...additionalOptions,
+ });
+
+ ajv.addMetaSchema(metaSchema);
+
+ // @ts-expect-error -- this is an untyped part of the ajv API
+ ajv._opts.defaultMeta = metaSchema.id;
+
+ return ajv;
+}
diff --git a/packages/rule-tester/src/utils/cloneDeeplyExcludesParent.ts b/packages/rule-tester/src/utils/cloneDeeplyExcludesParent.ts
new file mode 100644
index 000000000000..b3bb23869a75
--- /dev/null
+++ b/packages/rule-tester/src/utils/cloneDeeplyExcludesParent.ts
@@ -0,0 +1,23 @@
+/**
+ * Clones a given value deeply.
+ * Note: This ignores `parent` property.
+ */
+export function cloneDeeplyExcludesParent(x: T): T {
+ if (typeof x === 'object' && x != null) {
+ if (Array.isArray(x)) {
+ return x.map(cloneDeeplyExcludesParent) as T;
+ }
+
+ const retv = {} as typeof x;
+
+ for (const key in x) {
+ if (key !== 'parent' && Object.prototype.hasOwnProperty.call(x, key)) {
+ retv[key] = cloneDeeplyExcludesParent(x[key]);
+ }
+ }
+
+ return retv;
+ }
+
+ return x;
+}
diff --git a/packages/rule-tester/src/utils/config-schema.ts b/packages/rule-tester/src/utils/config-schema.ts
new file mode 100644
index 000000000000..8261ac8749c8
--- /dev/null
+++ b/packages/rule-tester/src/utils/config-schema.ts
@@ -0,0 +1,91 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/conf/config-schema.js
+
+import type { JSONSchema } from '@typescript-eslint/utils';
+
+const baseConfigProperties: JSONSchema.JSONSchema4['properties'] = {
+ $schema: { type: 'string' },
+ defaultFilenames: {
+ type: 'object',
+ properties: {
+ ts: { type: 'string' },
+ tsx: { type: 'string' },
+ },
+ required: ['ts', 'tsx'],
+ additionalProperties: false,
+ },
+ dependencyConstraints: {
+ type: 'object',
+ additionalProperties: {
+ type: 'string',
+ },
+ },
+ env: { type: 'object' },
+ extends: { $ref: '#/definitions/stringOrStrings' },
+ globals: { type: 'object' },
+ noInlineConfig: { type: 'boolean' },
+ overrides: {
+ type: 'array',
+ items: { $ref: '#/definitions/overrideConfig' },
+ additionalItems: false,
+ },
+ parser: { type: ['string', 'null'] },
+ parserOptions: { type: 'object' },
+ plugins: { type: 'array' },
+ processor: { type: 'string' },
+ reportUnusedDisableDirectives: { type: 'boolean' },
+ rules: { type: 'object' },
+ settings: { type: 'object' },
+
+ ecmaFeatures: { type: 'object' }, // deprecated; logs a warning when used
+};
+
+export const configSchema: JSONSchema.JSONSchema4 = {
+ definitions: {
+ stringOrStrings: {
+ oneOf: [
+ { type: 'string' },
+ {
+ type: 'array',
+ items: { type: 'string' },
+ additionalItems: false,
+ },
+ ],
+ },
+ stringOrStringsRequired: {
+ oneOf: [
+ { type: 'string' },
+ {
+ type: 'array',
+ items: { type: 'string' },
+ additionalItems: false,
+ minItems: 1,
+ },
+ ],
+ },
+
+ // Config at top-level.
+ objectConfig: {
+ type: 'object',
+ properties: {
+ root: { type: 'boolean' },
+ ignorePatterns: { $ref: '#/definitions/stringOrStrings' },
+ ...baseConfigProperties,
+ },
+ additionalProperties: false,
+ },
+
+ // Config in `overrides`.
+ overrideConfig: {
+ type: 'object',
+ properties: {
+ excludedFiles: { $ref: '#/definitions/stringOrStrings' },
+ files: { $ref: '#/definitions/stringOrStringsRequired' },
+ ...baseConfigProperties,
+ },
+ required: ['files'],
+ additionalProperties: false,
+ },
+ },
+
+ $ref: '#/definitions/objectConfig',
+};
diff --git a/packages/rule-tester/src/utils/config-validator.ts b/packages/rule-tester/src/utils/config-validator.ts
new file mode 100644
index 000000000000..ef88f7e664eb
--- /dev/null
+++ b/packages/rule-tester/src/utils/config-validator.ts
@@ -0,0 +1,288 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/shared/config-validator.js
+
+import util from 'node:util';
+
+import { Legacy } from '@eslint/eslintrc';
+import type { AnyRuleModule, Linter } from '@typescript-eslint/utils/ts-eslint';
+import type {
+ AdditionalPropertiesParams,
+ ErrorObject as AjvErrorObject,
+ ValidateFunction,
+} from 'ajv';
+import { builtinRules } from 'eslint/use-at-your-own-risk';
+
+import type { TesterConfigWithDefaults } from '../types';
+import { ajvBuilder } from './ajv';
+import { configSchema } from './config-schema';
+import { emitDeprecationWarning } from './deprecation-warnings';
+import { getRuleOptionsSchema } from './getRuleOptionsSchema';
+import { hasOwnProperty } from './hasOwnProperty';
+
+type GetAdditionalRule = (ruleId: string) => AnyRuleModule | null;
+
+const { ConfigOps, environments: BuiltInEnvironments } = Legacy;
+const ajv = ajvBuilder();
+const ruleValidators = new WeakMap();
+
+let validateSchema: ValidateFunction;
+const severityMap = {
+ error: 2,
+ warn: 1,
+ off: 0,
+} as const;
+
+/**
+ * Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid.
+ * @param options The given options for the rule.
+ * @throws {Error} Wrong severity value.
+ */
+function validateRuleSeverity(options: Linter.RuleEntry): number | string {
+ const severity = Array.isArray(options) ? options[0] : options;
+ const normSeverity =
+ typeof severity === 'string'
+ ? severityMap[severity.toLowerCase() as Linter.SeverityString]
+ : severity;
+
+ if (normSeverity === 0 || normSeverity === 1 || normSeverity === 2) {
+ return normSeverity;
+ }
+
+ throw new Error(
+ `\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '${util
+ .inspect(severity)
+ .replace(/'/gu, '"')
+ .replace(/\n/gu, '')}').\n`,
+ );
+}
+
+/**
+ * Validates the non-severity options passed to a rule, based on its schema.
+ * @param rule The rule to validate
+ * @param localOptions The options for the rule, excluding severity
+ * @throws {Error} Any rule validation errors.
+ */
+function validateRuleSchema(
+ rule: AnyRuleModule,
+ localOptions: unknown[],
+): void {
+ if (!ruleValidators.has(rule)) {
+ const schema = getRuleOptionsSchema(rule);
+
+ if (schema) {
+ ruleValidators.set(rule, ajv.compile(schema));
+ }
+ }
+
+ const validateRule = ruleValidators.get(rule);
+
+ if (validateRule) {
+ validateRule(localOptions);
+ if (validateRule.errors) {
+ throw new Error(
+ validateRule.errors
+ .map(
+ error =>
+ `\tValue ${JSON.stringify(error.data)} ${error.message}.\n`,
+ )
+ .join(''),
+ );
+ }
+ }
+}
+
+/**
+ * Validates a rule's options against its schema.
+ * @param rule The rule that the config is being validated for
+ * @param ruleId The rule's unique name.
+ * @param {Array|number} options The given options for the rule.
+ * @param source The name of the configuration source to report in any errors. If null or undefined,
+ * no source is prepended to the message.
+ * @throws {Error} Upon any bad rule configuration.
+ */
+function validateRuleOptions(
+ rule: AnyRuleModule,
+ ruleId: string,
+ options: Linter.RuleEntry,
+ source: string | null = null,
+): void {
+ try {
+ const severity = validateRuleSeverity(options);
+
+ if (severity !== 0) {
+ validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []);
+ }
+ } catch (err) {
+ const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${
+ (err as Error).message
+ }`;
+
+ if (typeof source === 'string') {
+ throw new Error(`${source}:\n\t${enhancedMessage}`);
+ } else {
+ throw new Error(enhancedMessage);
+ }
+ }
+}
+
+/**
+ * Validates an environment object
+ * @param environment The environment config object to validate.
+ * @param source The name of the configuration source to report in any errors.
+ */
+function validateEnvironment(
+ environment: Linter.EnvironmentConfig | undefined,
+ source: string,
+): void {
+ // not having an environment is ok
+ if (!environment) {
+ return;
+ }
+
+ Object.keys(environment).forEach(id => {
+ const env = BuiltInEnvironments.get(id) ?? null;
+
+ if (!env) {
+ const message = `${source}:\n\tEnvironment key "${id}" is unknown\n`;
+
+ throw new Error(message);
+ }
+ });
+}
+
+/**
+ * Validates a rules config object
+ * @param rulesConfig The rules config object to validate.
+ * @param source The name of the configuration source to report in any errors.
+ * @param getAdditionalRule A map from strings to loaded rules
+ */
+function validateRules(
+ rulesConfig: Linter.RulesRecord | undefined,
+ source: string,
+ getAdditionalRule: GetAdditionalRule,
+): void {
+ if (!rulesConfig) {
+ return;
+ }
+
+ Object.keys(rulesConfig).forEach(id => {
+ const rule = getAdditionalRule(id) ?? builtinRules.get(id) ?? null;
+ if (rule == null) {
+ return;
+ }
+
+ validateRuleOptions(rule, id, rulesConfig[id]!, source);
+ });
+}
+
+/**
+ * Validates a `globals` section of a config file
+ * @param globalsConfig The `globals` section
+ * @param source The name of the configuration source to report in the event of an error.
+ */
+function validateGlobals(
+ globalsConfig: Linter.GlobalsConfig | undefined,
+ source: string | null = null,
+): void {
+ if (!globalsConfig) {
+ return;
+ }
+
+ Object.entries(globalsConfig).forEach(
+ ([configuredGlobal, configuredValue]) => {
+ try {
+ ConfigOps.normalizeConfigGlobal(configuredValue);
+ } catch (err) {
+ throw new Error(
+ `ESLint configuration of global '${configuredGlobal}' in ${source} is invalid:\n${
+ (err as Error).message
+ }`,
+ );
+ }
+ },
+ );
+}
+
+/**
+ * Formats an array of schema validation errors.
+ */
+function formatErrors(errors: AjvErrorObject[]): string {
+ return errors
+ .map(error => {
+ if (error.keyword === 'additionalProperties') {
+ const params = error.params as AdditionalPropertiesParams;
+ const formattedPropertyPath = error.dataPath.length
+ ? `${error.dataPath.slice(1)}.${params.additionalProperty}`
+ : params.additionalProperty;
+
+ return `Unexpected top-level property "${formattedPropertyPath}"`;
+ }
+ if (error.keyword === 'type') {
+ const formattedField = error.dataPath.slice(1);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const formattedExpectedType = Array.isArray(error.schema)
+ ? error.schema.join('/')
+ : error.schema;
+ const formattedValue = JSON.stringify(error.data);
+
+ return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
+ }
+
+ const field =
+ error.dataPath[0] === '.' ? error.dataPath.slice(1) : error.dataPath;
+
+ return `"${field}" ${error.message}. Value: ${JSON.stringify(
+ error.data,
+ )}`;
+ })
+ .map(message => `\t- ${message}.\n`)
+ .join('');
+}
+
+/**
+ * Validates the top level properties of the config object.
+ * @param config The config object to validate.
+ * @param source The name of the configuration source to report in any errors.
+ * @throws {Error} For any config invalid per the schema.
+ */
+function validateConfigSchema(
+ config: TesterConfigWithDefaults,
+ source: string,
+): void {
+ validateSchema = validateSchema || ajv.compile(configSchema);
+
+ if (!validateSchema(config)) {
+ throw new Error(
+ `ESLint configuration in ${source} is invalid:\n${formatErrors(
+ validateSchema.errors!,
+ )}`,
+ );
+ }
+
+ // @ts-expect-error -- intentional deprecated check
+ if (hasOwnProperty(config, 'ecmaFeatures')) {
+ emitDeprecationWarning(source, 'ESLINT_LEGACY_ECMAFEATURES');
+ }
+}
+
+/**
+ * Validates an entire config object.
+ * @param config The config object to validate.
+ * @param source The name of the configuration source to report in any errors.
+ * @param getAdditionalRule A map from strings to loaded rules.
+ */
+export function validate(
+ config: TesterConfigWithDefaults,
+ source: string,
+ getAdditionalRule: GetAdditionalRule,
+): void {
+ validateConfigSchema(config, source);
+ validateRules(config.rules, source, getAdditionalRule);
+ validateEnvironment(config.env, source);
+ validateGlobals(config.globals, source);
+
+ for (const override of config.overrides ?? []) {
+ validateRules(override.rules, source, getAdditionalRule);
+ validateEnvironment(override.env, source);
+ validateGlobals(config.globals, source);
+ }
+}
diff --git a/packages/rule-tester/src/utils/dependencyConstraints.ts b/packages/rule-tester/src/utils/dependencyConstraints.ts
new file mode 100644
index 000000000000..e651356587a1
--- /dev/null
+++ b/packages/rule-tester/src/utils/dependencyConstraints.ts
@@ -0,0 +1,48 @@
+import * as semver from 'semver';
+
+import type {
+ DependencyConstraint,
+ SemverVersionConstraint,
+} from '../types/DependencyConstraint';
+
+const BASE_SATISFIES_OPTIONS: semver.RangeOptions = {
+ includePrerelease: true,
+};
+
+function satisfiesDependencyConstraint(
+ packageName: string,
+ constraintIn: DependencyConstraint[string],
+): boolean {
+ const constraint: SemverVersionConstraint =
+ typeof constraintIn === 'string'
+ ? {
+ range: `>=${constraintIn}`,
+ }
+ : constraintIn;
+
+ return semver.satisfies(
+ (require(`${packageName}/package.json`) as { version: string }).version,
+ constraint.range,
+ typeof constraint.options === 'object'
+ ? { ...BASE_SATISFIES_OPTIONS, ...constraint.options }
+ : constraint.options,
+ );
+}
+
+export function satisfiesAllDependencyConstraints(
+ dependencyConstraints: DependencyConstraint | undefined,
+): boolean {
+ if (dependencyConstraints == null) {
+ return true;
+ }
+
+ for (const [packageName, constraint] of Object.entries(
+ dependencyConstraints,
+ )) {
+ if (!satisfiesDependencyConstraint(packageName, constraint)) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/packages/rule-tester/src/utils/deprecation-warnings.ts b/packages/rule-tester/src/utils/deprecation-warnings.ts
new file mode 100644
index 000000000000..9f264a412964
--- /dev/null
+++ b/packages/rule-tester/src/utils/deprecation-warnings.ts
@@ -0,0 +1,40 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/shared/deprecation-warnings.js
+
+import path from 'node:path';
+
+// Definitions for deprecation warnings.
+const deprecationWarningMessages = {
+ ESLINT_LEGACY_ECMAFEATURES:
+ "The 'ecmaFeatures' config file property is deprecated and has no effect.",
+} as const;
+
+const sourceFileErrorCache = new Set();
+
+/**
+ * Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
+ * for each unique file path, but repeated invocations with the same file path have no effect.
+ * No warnings are emitted if the `--no-deprecation` or `--no-warnings` Node runtime flags are active.
+ * @param source The name of the configuration source to report the warning for.
+ * @param errorCode The warning message to show.
+ */
+export function emitDeprecationWarning(
+ source: string,
+ errorCode: keyof typeof deprecationWarningMessages,
+): void {
+ const cacheKey = JSON.stringify({ source, errorCode });
+
+ if (sourceFileErrorCache.has(cacheKey)) {
+ return;
+ }
+
+ sourceFileErrorCache.add(cacheKey);
+
+ const rel = path.relative(process.cwd(), source);
+ const message = deprecationWarningMessages[errorCode];
+
+ process.emitWarning(
+ `${message} (found in "${rel}")`,
+ 'DeprecationWarning',
+ errorCode,
+ );
+}
diff --git a/packages/rule-tester/src/utils/freezeDeeply.ts b/packages/rule-tester/src/utils/freezeDeeply.ts
new file mode 100644
index 000000000000..36c375063ae7
--- /dev/null
+++ b/packages/rule-tester/src/utils/freezeDeeply.ts
@@ -0,0 +1,17 @@
+/**
+ * Freezes a given value deeply.
+ */
+export function freezeDeeply(x: unknown): void {
+ if (typeof x === 'object' && x != null) {
+ if (Array.isArray(x)) {
+ x.forEach(freezeDeeply);
+ } else {
+ for (const key in x) {
+ if (key !== 'parent' && Object.prototype.hasOwnProperty.call(x, key)) {
+ freezeDeeply((x as Record)[key]);
+ }
+ }
+ }
+ Object.freeze(x);
+ }
+}
diff --git a/packages/rule-tester/src/utils/getRuleOptionsSchema.ts b/packages/rule-tester/src/utils/getRuleOptionsSchema.ts
new file mode 100644
index 000000000000..51f94711f701
--- /dev/null
+++ b/packages/rule-tester/src/utils/getRuleOptionsSchema.ts
@@ -0,0 +1,35 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/shared/config-validator.js#LL50-L82C2
+
+import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema';
+import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
+
+import { isReadonlyArray } from './isReadonlyArray';
+
+/**
+ * Gets a complete options schema for a rule.
+ * @param rule A new-style rule object
+ * @returns JSON Schema for the rule's options.
+ */
+export function getRuleOptionsSchema(rule: AnyRuleModule): JSONSchema4 | null {
+ const schema = rule.meta?.schema;
+
+ // Given a tuple of schemas, insert warning level at the beginning
+ if (isReadonlyArray(schema)) {
+ if (schema.length) {
+ return {
+ type: 'array',
+ items: schema as JSONSchema4[],
+ minItems: 0,
+ maxItems: schema.length,
+ };
+ }
+ return {
+ type: 'array',
+ minItems: 0,
+ maxItems: 0,
+ };
+ }
+
+ // Given a full schema, leave it alone
+ return schema || null;
+}
diff --git a/packages/rule-tester/src/utils/hasOwnProperty.ts b/packages/rule-tester/src/utils/hasOwnProperty.ts
new file mode 100644
index 000000000000..a8816f89fe85
--- /dev/null
+++ b/packages/rule-tester/src/utils/hasOwnProperty.ts
@@ -0,0 +1,8 @@
+// typed so that TS can remove optionality
+export const hasOwnProperty = Function.call.bind(Object.hasOwnProperty) as <
+ TObj extends object,
+ TK extends keyof TObj,
+>(
+ obj: TObj,
+ key: TK,
+) => obj is TObj & { [key in TK]-?: TObj[key] };
diff --git a/packages/rule-tester/src/utils/interpolate.ts b/packages/rule-tester/src/utils/interpolate.ts
new file mode 100644
index 000000000000..0b3266fefb9b
--- /dev/null
+++ b/packages/rule-tester/src/utils/interpolate.ts
@@ -0,0 +1,27 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/linter/interpolate.js
+
+import type { ReportDescriptorMessageData } from '@typescript-eslint/utils/ts-eslint';
+
+export function interpolate(
+ text: string,
+ data: ReportDescriptorMessageData,
+): string {
+ if (!data) {
+ return text;
+ }
+
+ // Substitution content for any {{ }} markers.
+ return text.replace(
+ /\{\{([^{}]+?)\}\}/gu,
+ (fullMatch, termWithWhitespace: string) => {
+ const term = termWithWhitespace.trim();
+
+ if (term in data) {
+ return String(data[term]);
+ }
+
+ // Preserve old behavior: If parameter name not provided, don't replace it.
+ return fullMatch;
+ },
+ );
+}
diff --git a/packages/rule-tester/src/utils/isReadonlyArray.ts b/packages/rule-tester/src/utils/isReadonlyArray.ts
new file mode 100644
index 000000000000..b16a9f31f011
--- /dev/null
+++ b/packages/rule-tester/src/utils/isReadonlyArray.ts
@@ -0,0 +1,4 @@
+// working around https://github.com/microsoft/TypeScript/issues/17002
+export function isReadonlyArray(arg: unknown): arg is readonly unknown[] {
+ return Array.isArray(arg);
+}
diff --git a/packages/rule-tester/src/utils/validationHelpers.ts b/packages/rule-tester/src/utils/validationHelpers.ts
new file mode 100644
index 000000000000..33fd0c234de8
--- /dev/null
+++ b/packages/rule-tester/src/utils/validationHelpers.ts
@@ -0,0 +1,182 @@
+import { simpleTraverse } from '@typescript-eslint/typescript-estree';
+import type { TSESTree } from '@typescript-eslint/utils';
+import type { Linter, SourceCode } from '@typescript-eslint/utils/ts-eslint';
+
+/*
+ * List every parameters possible on a test case that are not related to eslint
+ * configuration
+ */
+export const RULE_TESTER_PARAMETERS = [
+ 'code',
+ 'defaultFilenames',
+ 'dependencyConstraints',
+ 'errors',
+ 'filename',
+ 'name',
+ 'only',
+ 'options',
+ 'output',
+ 'skip',
+] as const;
+
+/*
+ * All allowed property names in error objects.
+ */
+export const ERROR_OBJECT_PARAMETERS: ReadonlySet = new Set([
+ 'column',
+ 'data',
+ 'endColumn',
+ 'endLine',
+ 'line',
+ 'message',
+ 'messageId',
+ 'suggestions',
+ 'type',
+]);
+export const FRIENDLY_ERROR_OBJECT_PARAMETER_LIST = `[${[
+ ...ERROR_OBJECT_PARAMETERS,
+]
+ .map(key => `'${key}'`)
+ .join(', ')}]`;
+
+/*
+ * All allowed property names in suggestion objects.
+ */
+export const SUGGESTION_OBJECT_PARAMETERS: ReadonlySet = new Set([
+ 'data',
+ 'desc',
+ 'messageId',
+ 'output',
+]);
+export const FRIENDLY_SUGGESTION_OBJECT_PARAMETER_LIST = `[${[
+ ...SUGGESTION_OBJECT_PARAMETERS,
+]
+ .map(key => `'${key}'`)
+ .join(', ')}]`;
+
+/**
+ * Replace control characters by `\u00xx` form.
+ */
+export function sanitize(text: string): string {
+ if (typeof text !== 'string') {
+ return '';
+ }
+ return text.replace(
+ // eslint-disable-next-line no-control-regex
+ /[\u0000-\u0009\u000b-\u001a]/gu,
+ c => `\\u${c.codePointAt(0)!.toString(16).padStart(4, '0')}`,
+ );
+}
+
+// this symbol is used internally by ESLint to unwrap the wrapped parser
+// https://github.com/eslint/eslint/blob/129e252132c7c476d7de17f40b54a333ddb2e6bb/lib/linter/linter.js#L139-L146
+const parserSymbol = Symbol.for('eslint.RuleTester.parser');
+/**
+ * Wraps the given parser in order to intercept and modify return values from the `parse` and `parseForESLint` methods, for test purposes.
+ * In particular, to modify ast nodes, tokens and comments to throw on access to their `start` and `end` properties.
+ */
+export function wrapParser(parser: Linter.ParserModule): Linter.ParserModule {
+ /**
+ * Define `start`/`end` properties of all nodes of the given AST as throwing error.
+ */
+ function defineStartEndAsErrorInTree(
+ ast: TSESTree.Program,
+ visitorKeys?: Readonly,
+ ): void {
+ /**
+ * Define `start`/`end` properties as throwing error.
+ */
+ function defineStartEndAsError(objName: string, node: unknown): void {
+ Object.defineProperties(node, {
+ start: {
+ get() {
+ throw new Error(
+ `Use ${objName}.range[0] instead of ${objName}.start`,
+ );
+ },
+ configurable: true,
+ enumerable: false,
+ },
+ end: {
+ get() {
+ throw new Error(
+ `Use ${objName}.range[1] instead of ${objName}.end`,
+ );
+ },
+ configurable: true,
+ enumerable: false,
+ },
+ });
+ }
+
+ simpleTraverse(ast, {
+ visitorKeys: visitorKeys,
+ enter: node => defineStartEndAsError('node', node),
+ });
+ ast.tokens?.forEach(token => defineStartEndAsError('token', token));
+ ast.comments?.forEach(comment => defineStartEndAsError('token', comment));
+ }
+
+ if ('parseForESLint' in parser) {
+ return {
+ // @ts-expect-error -- see above
+ [parserSymbol]: parser,
+ parseForESLint(...args): Linter.ESLintParseResult {
+ const ret = parser.parseForESLint(...args);
+
+ defineStartEndAsErrorInTree(ret.ast, ret.visitorKeys);
+ return ret;
+ },
+ };
+ }
+
+ return {
+ // @ts-expect-error -- see above
+ [parserSymbol]: parser,
+ parse(...args): TSESTree.Program {
+ const ast = parser.parse(...args);
+
+ defineStartEndAsErrorInTree(ast);
+ return ast;
+ },
+ };
+}
+
+/**
+ * Function to replace `SourceCode.prototype.getComments`.
+ */
+export function getCommentsDeprecation(): never {
+ throw new Error(
+ '`SourceCode#getComments()` is deprecated and will be removed in a future major version. Use `getCommentsBefore()`, `getCommentsAfter()`, and `getCommentsInside()` instead.',
+ );
+}
+
+const EMIT_LEGACY_RULE_API_WARNING: Record = {};
+/**
+ * Emit a deprecation warning if function-style format is being used.
+ */
+export function emitLegacyRuleAPIWarning(ruleName: string): void {
+ if (!EMIT_LEGACY_RULE_API_WARNING[`warned-${ruleName}`]) {
+ EMIT_LEGACY_RULE_API_WARNING[`warned-${ruleName}`] = true;
+ process.emitWarning(
+ `"${ruleName}" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules`,
+ 'DeprecationWarning',
+ );
+ }
+}
+
+const EMIT_MISSING_SCHEMA_WARNING: Record = {};
+/**
+ * Emit a deprecation warning if rule has options but is missing the "meta.schema" property
+ */
+export function emitMissingSchemaWarning(ruleName: string): void {
+ if (!EMIT_MISSING_SCHEMA_WARNING[`warned-${ruleName}`]) {
+ EMIT_MISSING_SCHEMA_WARNING[`warned-${ruleName}`] = true;
+ process.emitWarning(
+ `"${ruleName}" rule has options but is missing the "meta.schema" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas`,
+ 'DeprecationWarning',
+ );
+ }
+}
+
+export const REQUIRED_SCENARIOS = ['valid', 'invalid'] as const;
diff --git a/packages/rule-tester/tests/RuleTester.test.ts b/packages/rule-tester/tests/RuleTester.test.ts
new file mode 100644
index 000000000000..6ed3edc23f35
--- /dev/null
+++ b/packages/rule-tester/tests/RuleTester.test.ts
@@ -0,0 +1,822 @@
+import * as parser from '@typescript-eslint/parser';
+import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
+import type { TSESTree } from '@typescript-eslint/utils';
+import type { RuleModule } from '@typescript-eslint/utils/ts-eslint';
+
+import { RuleTester } from '../src/RuleTester';
+import type { RuleTesterTestFrameworkFunctionBase } from '../src/TestFramework';
+import * as dependencyConstraintsModule from '../src/utils/dependencyConstraints';
+
+// we can't spy on the exports of an ES module - so we instead have to mock the entire module
+jest.mock('../src/dependencyConstraints', () => {
+ const dependencyConstraints = jest.requireActual<
+ typeof dependencyConstraintsModule
+ >('../src/dependencyConstraints');
+
+ return {
+ ...dependencyConstraints,
+ __esModule: true,
+ satisfiesAllDependencyConstraints: jest.fn(
+ dependencyConstraints.satisfiesAllDependencyConstraints,
+ ),
+ };
+});
+const satisfiesAllDependencyConstraintsMock = jest.mocked(
+ dependencyConstraintsModule.satisfiesAllDependencyConstraints,
+);
+
+jest.mock(
+ 'totally-real-dependency/package.json',
+ () => ({
+ version: '10.0.0',
+ }),
+ {
+ // this is not a real module that will exist
+ virtual: true,
+ },
+);
+jest.mock(
+ 'totally-real-dependency-prerelease/package.json',
+ () => ({
+ version: '10.0.0-rc.1',
+ }),
+ {
+ // this is not a real module that will exist
+ virtual: true,
+ },
+);
+
+jest.mock('@typescript-eslint/parser', () => {
+ const actualParser = jest.requireActual(
+ '@typescript-eslint/parser',
+ );
+ return {
+ ...actualParser,
+ __esModule: true,
+ clearCaches: jest.fn(),
+ };
+});
+
+/* eslint-disable jest/prefer-spy-on --
+ we need to specifically assign to the properties or else it will use the
+ global value and register actual tests! */
+const IMMEDIATE_CALLBACK: RuleTesterTestFrameworkFunctionBase = (_, cb) => cb();
+RuleTester.afterAll =
+ jest.fn(/* intentionally don't immediate callback here */);
+RuleTester.describe = jest.fn(IMMEDIATE_CALLBACK);
+RuleTester.describeSkip = jest.fn(IMMEDIATE_CALLBACK);
+RuleTester.it = jest.fn(IMMEDIATE_CALLBACK);
+RuleTester.itOnly = jest.fn(IMMEDIATE_CALLBACK);
+RuleTester.itSkip = jest.fn(IMMEDIATE_CALLBACK);
+/* eslint-enable jest/prefer-spy-on */
+
+const mockedAfterAll = jest.mocked(RuleTester.afterAll);
+const mockedDescribe = jest.mocked(RuleTester.describe);
+const mockedDescribeSkip = jest.mocked(RuleTester.describeSkip);
+const mockedIt = jest.mocked(RuleTester.it);
+const _mockedItOnly = jest.mocked(RuleTester.itOnly);
+const _mockedItSkip = jest.mocked(RuleTester.itSkip);
+const runRuleForItemSpy = jest.spyOn(
+ RuleTester.prototype,
+ // @ts-expect-error -- method is private
+ 'runRuleForItem',
+) as jest.SpiedFunction;
+const mockedParserClearCaches = jest.mocked(parser.clearCaches);
+
+const EMPTY_PROGRAM: TSESTree.Program = {
+ type: AST_NODE_TYPES.Program,
+ body: [],
+ comments: [],
+ loc: { end: { column: 0, line: 0 }, start: { column: 0, line: 0 } },
+ sourceType: 'module',
+ tokens: [],
+ range: [0, 0],
+};
+runRuleForItemSpy.mockImplementation((_1, _2, testCase) => {
+ return {
+ messages:
+ 'errors' in testCase
+ ? [
+ {
+ column: 0,
+ line: 0,
+ message: 'error',
+ messageId: 'error',
+ nodeType: AST_NODE_TYPES.Program,
+ ruleId: 'my-rule',
+ severity: 2,
+ source: null,
+ },
+ ]
+ : [],
+ output: testCase.code,
+ afterAST: EMPTY_PROGRAM,
+ beforeAST: EMPTY_PROGRAM,
+ };
+});
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+const NOOP_RULE: RuleModule<'error', []> = {
+ meta: {
+ messages: {
+ error: 'error',
+ },
+ type: 'problem',
+ schema: {},
+ },
+ defaultOptions: [],
+ create() {
+ return {};
+ },
+};
+
+function getTestConfigFromCall(): unknown[] {
+ return runRuleForItemSpy.mock.calls.map(c => c[2]);
+}
+
+describe('RuleTester', () => {
+ describe('filenames', () => {
+ it('automatically sets the filename for tests', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir: '/some/path/that/totally/exists/',
+ },
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ 'string based valid test',
+ {
+ code: 'object based valid test',
+ },
+ {
+ code: "explicit filename shouldn't be overwritten",
+ filename: '/set/in/the/test.ts',
+ },
+ {
+ code: 'jsx should have the correct filename',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ {
+ code: 'type-aware parser options should override the constructor config',
+ parserOptions: {
+ project: 'tsconfig.test-specific.json',
+ tsconfigRootDir: '/set/in/the/test/',
+ },
+ },
+ ],
+ invalid: [
+ {
+ code: 'invalid tests should work as well',
+ errors: [{ messageId: 'error' }],
+ },
+ ],
+ });
+
+ expect(getTestConfigFromCall()).toMatchInlineSnapshot(`
+ [
+ {
+ "code": "string based valid test",
+ "filename": "/some/path/that/totally/exists/file.ts",
+ },
+ {
+ "code": "object based valid test",
+ "filename": "/some/path/that/totally/exists/file.ts",
+ },
+ {
+ "code": "explicit filename shouldn't be overwritten",
+ "filename": "/set/in/the/test.ts",
+ },
+ {
+ "code": "jsx should have the correct filename",
+ "filename": "/some/path/that/totally/exists/react.tsx",
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true,
+ },
+ },
+ },
+ {
+ "code": "type-aware parser options should override the constructor config",
+ "filename": "/set/in/the/test/file.ts",
+ "parserOptions": {
+ "project": "tsconfig.test-specific.json",
+ "tsconfigRootDir": "/set/in/the/test/",
+ },
+ },
+ {
+ "code": "invalid tests should work as well",
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "/some/path/that/totally/exists/file.ts",
+ },
+ ]
+ `);
+ });
+
+ it('allows the automated filenames to be overridden in the constructor', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir: '/some/path/that/totally/exists/',
+ },
+ defaultFilenames: {
+ ts: 'set-in-constructor.ts',
+ tsx: 'react-set-in-constructor.tsx',
+ },
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ {
+ code: 'normal',
+ },
+ {
+ code: 'jsx',
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ ],
+ invalid: [],
+ });
+
+ expect(getTestConfigFromCall()).toMatchInlineSnapshot(`
+ [
+ {
+ "code": "normal",
+ "filename": "/some/path/that/totally/exists/set-in-constructor.ts",
+ },
+ {
+ "code": "jsx",
+ "filename": "/some/path/that/totally/exists/react-set-in-constructor.tsx",
+ "parserOptions": {
+ "ecmaFeatures": {
+ "jsx": true,
+ },
+ },
+ },
+ ]
+ `);
+ });
+ });
+
+ it('schedules the parser caches to be cleared afterAll', () => {
+ // it should schedule the afterAll
+ expect(mockedAfterAll).toHaveBeenCalledTimes(0);
+ const _ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir: '/some/path/that/totally/exists/',
+ },
+ });
+ expect(mockedAfterAll).toHaveBeenCalledTimes(1);
+
+ // the provided callback should clear the caches
+ const callback = mockedAfterAll.mock.calls[0][0];
+ expect(typeof callback).toBe('function');
+ expect(mockedParserClearCaches).not.toHaveBeenCalled();
+ callback();
+ expect(mockedParserClearCaches).toHaveBeenCalledTimes(1);
+ });
+
+ it('throws an error if you attempt to set the parser to ts-eslint at the test level', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: 'tsconfig.json',
+ tsconfigRootDir: '/some/path/that/totally/exists/',
+ },
+ });
+
+ expect(() =>
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ {
+ code: 'object based valid test',
+ parser: '@typescript-eslint/parser',
+ },
+ ],
+
+ invalid: [],
+ }),
+ ).toThrowErrorMatchingInlineSnapshot(
+ `"Do not set the parser at the test level unless you want to use a parser other than "@typescript-eslint/parser""`,
+ );
+ });
+
+ describe('checks dependencies as specified', () => {
+ it('does not check dependencies if there are no dependency constraints', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ 'const x = 1;',
+ { code: 'const x = 2;' },
+ // empty object is ignored
+ { code: 'const x = 3;', dependencyConstraints: {} },
+ ],
+ invalid: [],
+ });
+
+ expect(satisfiesAllDependencyConstraintsMock).not.toHaveBeenCalled();
+ });
+
+ describe('does not check dependencies if is an "only" manually set', () => {
+ it('in the valid section', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ 'const x = 1;',
+ { code: 'const x = 2;' },
+ {
+ code: 'const x = 3;',
+ // eslint-disable-next-line eslint-plugin/no-only-tests -- intentional only for test purposes
+ only: true,
+ },
+ {
+ code: 'const x = 4;',
+ dependencyConstraints: {
+ 'totally-real-dependency': '999',
+ },
+ },
+ ],
+ invalid: [],
+ });
+
+ expect(satisfiesAllDependencyConstraintsMock).not.toHaveBeenCalled();
+ });
+
+ it('in the invalid section', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ 'const x = 1;',
+ { code: 'const x = 2;' },
+ {
+ code: 'const x = 4;',
+ dependencyConstraints: {
+ 'totally-real-dependency': '999',
+ },
+ },
+ ],
+ invalid: [
+ {
+ code: 'const x = 3;',
+ errors: [{ messageId: 'error' }],
+ // eslint-disable-next-line eslint-plugin/no-only-tests -- intentional only for test purposes
+ only: true,
+ },
+ ],
+ });
+
+ expect(satisfiesAllDependencyConstraintsMock).not.toHaveBeenCalled();
+ });
+ });
+
+ it('correctly handles string-based at-least', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ {
+ code: 'passing - major',
+ dependencyConstraints: {
+ 'totally-real-dependency': '10',
+ },
+ },
+ {
+ code: 'passing - major.minor',
+ dependencyConstraints: {
+ 'totally-real-dependency': '10.0',
+ },
+ },
+ {
+ code: 'passing - major.minor.patch',
+ dependencyConstraints: {
+ 'totally-real-dependency': '10.0.0',
+ },
+ },
+ ],
+ invalid: [
+ {
+ code: 'failing - major',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency': '999',
+ },
+ },
+ {
+ code: 'failing - major.minor',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency': '999.0',
+ },
+ },
+ {
+ code: 'failing - major.minor.patch',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency': '999.0.0',
+ },
+ },
+ ],
+ });
+
+ expect(getTestConfigFromCall()).toMatchInlineSnapshot(`
+ [
+ {
+ "code": "passing - major",
+ "dependencyConstraints": {
+ "totally-real-dependency": "10",
+ },
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "passing - major.minor",
+ "dependencyConstraints": {
+ "totally-real-dependency": "10.0",
+ },
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "passing - major.minor.patch",
+ "dependencyConstraints": {
+ "totally-real-dependency": "10.0.0",
+ },
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "failing - major",
+ "dependencyConstraints": {
+ "totally-real-dependency": "999",
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ {
+ "code": "failing - major.minor",
+ "dependencyConstraints": {
+ "totally-real-dependency": "999.0",
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ {
+ "code": "failing - major.minor.patch",
+ "dependencyConstraints": {
+ "totally-real-dependency": "999.0.0",
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ ]
+ `);
+ });
+
+ it('correctly handles object-based semver', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ {
+ code: 'passing - major',
+ dependencyConstraints: {
+ 'totally-real-dependency': {
+ range: '^10',
+ },
+ },
+ },
+ {
+ code: 'passing - major.minor',
+ dependencyConstraints: {
+ 'totally-real-dependency': {
+ range: '<999',
+ },
+ },
+ },
+ ],
+ invalid: [
+ {
+ code: 'failing - major',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency': {
+ range: '^999',
+ },
+ },
+ },
+ {
+ code: 'failing - major.minor',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency': {
+ range: '>=999.0',
+ },
+ },
+ },
+
+ {
+ code: 'failing with options',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency-prerelease': {
+ range: '^10',
+ options: {
+ includePrerelease: false,
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ expect(getTestConfigFromCall()).toMatchInlineSnapshot(`
+ [
+ {
+ "code": "passing - major",
+ "dependencyConstraints": {
+ "totally-real-dependency": {
+ "range": "^10",
+ },
+ },
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "passing - major.minor",
+ "dependencyConstraints": {
+ "totally-real-dependency": {
+ "range": "<999",
+ },
+ },
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "failing - major",
+ "dependencyConstraints": {
+ "totally-real-dependency": {
+ "range": "^999",
+ },
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ {
+ "code": "failing - major.minor",
+ "dependencyConstraints": {
+ "totally-real-dependency": {
+ "range": ">=999.0",
+ },
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ {
+ "code": "failing with options",
+ "dependencyConstraints": {
+ "totally-real-dependency-prerelease": {
+ "options": {
+ "includePrerelease": false,
+ },
+ "range": "^10",
+ },
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ ]
+ `);
+ });
+
+ it('tests without versions should always be run', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ 'string based is always run',
+ {
+ code: 'no constraints is always run',
+ },
+ {
+ code: 'empty object is always run',
+ dependencyConstraints: {},
+ },
+ {
+ code: 'passing constraint',
+ dependencyConstraints: {
+ 'totally-real-dependency': '10',
+ },
+ },
+ ],
+ invalid: [
+ {
+ code: 'no constraints is always run',
+ errors: [{ messageId: 'error' }],
+ },
+ {
+ code: 'empty object is always run',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {},
+ },
+ {
+ code: 'failing constraint',
+ errors: [{ messageId: 'error' }],
+ dependencyConstraints: {
+ 'totally-real-dependency': '99999',
+ },
+ },
+ ],
+ });
+
+ expect(getTestConfigFromCall()).toMatchInlineSnapshot(`
+ [
+ {
+ "code": "string based is always run",
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "no constraints is always run",
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "empty object is always run",
+ "dependencyConstraints": {},
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "passing constraint",
+ "dependencyConstraints": {
+ "totally-real-dependency": "10",
+ },
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "no constraints is always run",
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "empty object is always run",
+ "dependencyConstraints": {},
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": false,
+ },
+ {
+ "code": "failing constraint",
+ "dependencyConstraints": {
+ "totally-real-dependency": "99999",
+ },
+ "errors": [
+ {
+ "messageId": "error",
+ },
+ ],
+ "filename": "file.ts",
+ "skip": true,
+ },
+ ]
+ `);
+ });
+
+ describe('constructor constraints', () => {
+ it('skips all tests if a constructor constraint is not satisifed', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ dependencyConstraints: {
+ 'totally-real-dependency': '999',
+ },
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ {
+ code: 'passing - major',
+ },
+ ],
+ invalid: [
+ {
+ code: 'failing - major',
+ errors: [{ messageId: 'error' }],
+ },
+ ],
+ });
+
+ // trigger the describe block
+ expect(mockedDescribeSkip.mock.calls).toHaveLength(1);
+ expect(mockedIt.mock.lastCall).toMatchInlineSnapshot(`
+ [
+ "All tests skipped due to unsatisfied constructor dependency constraints",
+ [Function],
+ ]
+ `);
+ });
+
+ it('does not skip all tests if a constructor constraint is satisifed', () => {
+ const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+ dependencyConstraints: {
+ 'totally-real-dependency': '10',
+ },
+ });
+
+ ruleTester.run('my-rule', NOOP_RULE, {
+ valid: [
+ {
+ code: 'valid',
+ },
+ ],
+ invalid: [
+ {
+ code: 'invalid',
+ errors: [{ messageId: 'error' }],
+ },
+ ],
+ });
+
+ // trigger the describe block
+ expect(mockedDescribe.mock.calls).toHaveLength(3);
+ expect(mockedDescribeSkip.mock.calls).toHaveLength(0);
+ // expect(mockedIt.mock.lastCall).toMatchInlineSnapshot(`undefined`);
+ });
+ });
+ });
+});
diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js
new file mode 100644
index 000000000000..03d5f1d8f8c6
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js
@@ -0,0 +1,2882 @@
+/**
+ * This file intentionally does not match the standards in the rest of our codebase.
+ * It's intended to exactly match the test in ESLint core so we can ensure we
+ * have compatibility.
+ * It's tempting to switch this to be strictly typed in TS and to use jest - but
+ * it's too easy to introduce subtle changes into the test by doing that. It also
+ * makes it much harder to merge upstream changes into this test.
+ *
+ * The only edits we have made are to update the paths for our rep
+ *
+ * Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/lib/rule-tester/rule-tester.js
+ *
+ * @noformat
+ */
+/* eslint-disable */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+const sinon = require("sinon"),
+ EventEmitter = require("events"),
+ { RuleTester } = require("../../dist/RuleTester"),
+ assert = require("chai").assert,
+ nodeAssert = require("assert"),
+ espree = require("espree");
+
+const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => {
+ try {
+ nodeAssert.strictEqual(1, 2);
+ } catch (err) {
+ return err.operator;
+ }
+ throw new Error("unexpected successful assertion");
+})();
+
+/**
+ * Do nothing.
+ * @returns {void}
+ */
+function noop() {
+
+ // do nothing.
+}
+
+//------------------------------------------------------------------------------
+// Rewire Things
+//------------------------------------------------------------------------------
+
+/*
+ * So here's the situation. Because RuleTester uses it() and describe() from
+ * Mocha, any failures would show up in the output of this test file. That means
+ * when we tested that a failure is thrown, that would also count as a failure
+ * in the testing for RuleTester. In order to remove those results from the
+ * results of this file, we need to overwrite it() and describe() just in
+ * RuleTester to do nothing but run code. Effectively, it() and describe()
+ * just become regular functions inside of index.js, not at all related to Mocha.
+ * That allows the results of this file to be untainted and therefore accurate.
+ *
+ * To assert that the right arguments are passed to RuleTester.describe/it, an
+ * event emitter is used which emits the arguments.
+ */
+
+const ruleTesterTestEmitter = new EventEmitter();
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("RuleTester", () => {
+
+ // Stub `describe()` and `it()` while this test suite.
+ before(() => {
+ RuleTester.describe = function(text, method) {
+ ruleTesterTestEmitter.emit("describe", text, method);
+ return method.call(this);
+ };
+ RuleTester.it = function(text, method) {
+ ruleTesterTestEmitter.emit("it", text, method);
+ return method.call(this);
+ };
+ });
+ after(() => {
+ RuleTester.describe = null;
+ RuleTester.it = null;
+ });
+
+ let ruleTester;
+
+ /**
+ * A helper function to verify Node.js core error messages.
+ * @param {string} actual The actual input
+ * @param {string} expected The expected input
+ * @returns {Function} Error callback to verify that the message is correct
+ * for the actual and expected input.
+ */
+ function assertErrorMatches(actual, expected) {
+ const err = new nodeAssert.AssertionError({
+ actual,
+ expected,
+ operator: NODE_ASSERT_STRICT_EQUAL_OPERATOR
+ });
+
+ return err.message;
+ }
+
+ beforeEach(() => {
+ RuleTester.resetDefaultConfig();
+ ruleTester = new RuleTester();
+ });
+
+ describe("only", () => {
+ describe("`itOnly` accessor", () => {
+ describe("when `itOnly` is set", () => {
+ before(() => {
+ RuleTester.itOnly = sinon.spy();
+ });
+ after(() => {
+ RuleTester.itOnly = void 0;
+ });
+ beforeEach(() => {
+ RuleTester.itOnly.resetHistory();
+ ruleTester = new RuleTester();
+ });
+
+ it("is called by exclusive tests", () => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [{
+ code: "const notVar = 42;",
+ only: true
+ }],
+ invalid: []
+ });
+
+ sinon.assert.calledWith(RuleTester.itOnly, "const notVar = 42;");
+ });
+ });
+
+ describe("when `it` is set and has an `only()` method", () => {
+ before(() => {
+ RuleTester.it.only = () => {};
+ sinon.spy(RuleTester.it, "only");
+ });
+ after(() => {
+ RuleTester.it.only = void 0;
+ });
+ beforeEach(() => {
+ RuleTester.it.only.resetHistory();
+ ruleTester = new RuleTester();
+ });
+
+ it("is called by tests with `only` set", () => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [{
+ code: "const notVar = 42;",
+ only: true
+ }],
+ invalid: []
+ });
+
+ sinon.assert.calledWith(RuleTester.it.only, "const notVar = 42;");
+ });
+ });
+
+ describe("when global `it` is a function that has an `only()` method", () => {
+ let originalGlobalItOnly;
+
+ before(() => {
+
+ /*
+ * We run tests with `--forbid-only`, so we have to override
+ * `it.only` to prevent the real one from being called.
+ */
+ originalGlobalItOnly = it.only;
+ it.only = () => {};
+ sinon.spy(it, "only");
+ });
+ after(() => {
+ it.only = originalGlobalItOnly;
+ });
+ beforeEach(() => {
+ it.only.resetHistory();
+ ruleTester = new RuleTester();
+ });
+
+ it("is called by tests with `only` set", () => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [{
+ code: "const notVar = 42;",
+ only: true
+ }],
+ invalid: []
+ });
+
+ sinon.assert.calledWith(it.only, "const notVar = 42;");
+ });
+ });
+
+ describe("when `describe` and `it` are overridden without `itOnly`", () => {
+ let originalGlobalItOnly;
+
+ before(() => {
+
+ /*
+ * These tests override `describe` and `it` already, so we
+ * don't need to override them here. We do, however, need to
+ * remove `only` from the global `it` to prevent it from
+ * being used instead.
+ */
+ originalGlobalItOnly = it.only;
+ it.only = void 0;
+ });
+ after(() => {
+ it.only = originalGlobalItOnly;
+ });
+ beforeEach(() => {
+ ruleTester = new RuleTester();
+ });
+
+ it("throws an error recommending overriding `itOnly`", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [{
+ code: "const notVar = 42;",
+ only: true
+ }],
+ invalid: []
+ });
+ }, "Set `RuleTester.itOnly` to use `only` with a custom test framework.");
+ });
+ });
+
+ describe("when global `it` is a function that does not have an `only()` method", () => {
+ let originalGlobalIt;
+ let originalRuleTesterDescribe;
+ let originalRuleTesterIt;
+
+ before(() => {
+ originalGlobalIt = global.it;
+
+ // eslint-disable-next-line no-global-assign -- Temporarily override Mocha global
+ it = () => {};
+
+ /*
+ * These tests override `describe` and `it`, so we need to
+ * un-override them here so they won't interfere.
+ */
+ originalRuleTesterDescribe = RuleTester.describe;
+ RuleTester.describe = void 0;
+ originalRuleTesterIt = RuleTester.it;
+ RuleTester.it = void 0;
+ });
+ after(() => {
+
+ // eslint-disable-next-line no-global-assign -- Restore Mocha global
+ it = originalGlobalIt;
+ RuleTester.describe = originalRuleTesterDescribe;
+ RuleTester.it = originalRuleTesterIt;
+ });
+ beforeEach(() => {
+ ruleTester = new RuleTester();
+ });
+
+ it("throws an error explaining that the current test framework does not support `only`", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [{
+ code: "const notVar = 42;",
+ only: true
+ }],
+ invalid: []
+ });
+ }, "The current test framework does not support exclusive tests with `only`.");
+ });
+ });
+ });
+
+ describe("test cases", () => {
+ const ruleName = "no-var";
+ const rule = require("./fixtures/no-var");
+
+ let originalRuleTesterIt;
+ let spyRuleTesterIt;
+ let originalRuleTesterItOnly;
+ let spyRuleTesterItOnly;
+
+ before(() => {
+ originalRuleTesterIt = RuleTester.it;
+ spyRuleTesterIt = sinon.spy();
+ RuleTester.it = spyRuleTesterIt;
+ originalRuleTesterItOnly = RuleTester.itOnly;
+ spyRuleTesterItOnly = sinon.spy();
+ RuleTester.itOnly = spyRuleTesterItOnly;
+ });
+ after(() => {
+ RuleTester.it = originalRuleTesterIt;
+ RuleTester.itOnly = originalRuleTesterItOnly;
+ });
+ beforeEach(() => {
+ spyRuleTesterIt.resetHistory();
+ spyRuleTesterItOnly.resetHistory();
+ ruleTester = new RuleTester();
+ });
+
+ it("isn't called for normal tests", () => {
+ ruleTester.run(ruleName, rule, {
+ valid: ["const notVar = 42;"],
+ invalid: []
+ });
+ sinon.assert.calledWith(spyRuleTesterIt, "const notVar = 42;");
+ sinon.assert.notCalled(spyRuleTesterItOnly);
+ });
+
+ it("calls it or itOnly for every test case", () => {
+
+ /*
+ * `RuleTester` doesn't implement test case exclusivity itself.
+ * Setting `only: true` just causes `RuleTester` to call
+ * whatever `only()` function is provided by the test framework
+ * instead of the regular `it()` function.
+ */
+
+ ruleTester.run(ruleName, rule, {
+ valid: [
+ "const valid = 42;",
+ {
+ code: "const onlyValid = 42;",
+ only: true
+ }
+ ],
+ invalid: [
+ {
+ code: "var invalid = 42;",
+ errors: [/^Bad var/u]
+ },
+ {
+ code: "var onlyInvalid = 42;",
+ errors: [/^Bad var/u],
+ only: true
+ }
+ ]
+ });
+
+ sinon.assert.calledWith(spyRuleTesterIt, "const valid = 42;");
+ sinon.assert.calledWith(spyRuleTesterItOnly, "const onlyValid = 42;");
+ sinon.assert.calledWith(spyRuleTesterIt, "var invalid = 42;");
+ sinon.assert.calledWith(spyRuleTesterItOnly, "var onlyInvalid = 42;");
+ });
+ });
+
+ describe("static helper wrapper", () => {
+ it("adds `only` to string test cases", () => {
+ const test = RuleTester.only("const valid = 42;");
+
+ assert.deepStrictEqual(test, {
+ code: "const valid = 42;",
+ only: true
+ });
+ });
+
+ it("adds `only` to object test cases", () => {
+ const test = RuleTester.only({ code: "const valid = 42;" });
+
+ assert.deepStrictEqual(test, {
+ code: "const valid = 42;",
+ only: true
+ });
+ });
+ });
+ });
+
+ it("should not throw an error when everything passes", () => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] }
+ ]
+ });
+ });
+
+ it("should throw an error when valid code is invalid", () => {
+
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "eval(foo)"
+ ],
+ invalid: [
+ { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] }
+ ]
+ });
+ }, /Should have no errors but had 1/u);
+ });
+
+ it("should throw an error when valid code is invalid", () => {
+
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ { code: "eval(foo)" }
+ ],
+ invalid: [
+ { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] }
+ ]
+ });
+ }, /Should have no errors but had 1/u);
+ });
+
+ it("should throw an error if invalid code is valid", () => {
+
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "Eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression" }] }
+ ]
+ });
+ }, /Should have 1 error but had 0/u);
+ });
+
+ it("should throw an error when the error message is wrong", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+
+ // Only the invalid test matters here
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", errors: [{ message: "Bad error message." }] }
+ ]
+ });
+ }, assertErrorMatches("Bad var.", "Bad error message."));
+ });
+
+ it("should throw an error when the error message regex does not match", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", errors: [{ message: /Bad error message/u }] }
+ ]
+ });
+ }, /Expected 'Bad var.' to match \/Bad error message\//u);
+ });
+
+ it("should throw an error when the error is not a supported type", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+
+ // Only the invalid test matters here
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", errors: [42] }
+ ]
+ });
+ }, /Error should be a string, object, or RegExp/u);
+ });
+
+ it("should throw an error when any of the errors is not a supported type", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+
+ // Only the invalid test matters here
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar; var baz = quux", errors: [{ type: "VariableDeclaration" }, null] }
+ ]
+ });
+ }, /Error should be a string, object, or RegExp/u);
+ });
+
+ it("should throw an error when the error is a string and it does not match error message", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+
+ // Only the invalid test matters here
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", errors: ["Bad error message."] }
+ ]
+ });
+ }, assertErrorMatches("Bad var.", "Bad error message."));
+ });
+
+ it("should throw an error when the error is a string and it does not match error message", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+
+ valid: [
+ ],
+ invalid: [
+ { code: "var foo = bar;", errors: [/Bad error message/u] }
+ ]
+ });
+ }, /Expected 'Bad var.' to match \/Bad error message\//u);
+ });
+
+ it("should not throw an error when the error is a string and it matches error message", () => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+
+ // Only the invalid test matters here
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", output: " foo = bar;", errors: ["Bad var."] }
+ ]
+ });
+ });
+
+ it("should not throw an error when the error is a regex and it matches error message", () => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", output: " foo = bar;", errors: [/^Bad var/u] }
+ ]
+ });
+ });
+
+ it("should throw an error when the error is an object with an unknown property name", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", errors: [{ Message: "Bad var." }] }
+ ]
+ });
+ }, /Invalid error property name 'Message'/u);
+ });
+
+ it("should throw an error when any of the errors is an object with an unknown property name", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ {
+ code: "var foo = bar; var baz = quux",
+ errors: [
+ { message: "Bad var.", type: "VariableDeclaration" },
+ { message: "Bad var.", typo: "VariableDeclaration" }
+ ]
+ }
+ ]
+ });
+ }, /Invalid error property name 'typo'/u);
+ });
+
+ it("should not throw an error when the error is a regex in an object and it matches error message", () => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", output: " foo = bar;", errors: [{ message: /^Bad var/u }] }
+ ]
+ });
+ });
+
+ it("should throw an error when the expected output doesn't match", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration" }] }
+ ]
+ });
+ }, /Output is incorrect/u);
+ });
+
+ it("should use strict equality to compare output", () => {
+ const replaceProgramWith5Rule = {
+ meta: {
+ fixable: "code"
+ },
+
+ create: context => ({
+ Program(node) {
+ context.report({ node, message: "bad", fix: fixer => fixer.replaceText(node, "5") });
+ }
+ })
+ };
+
+ // Should not throw.
+ ruleTester.run("foo", replaceProgramWith5Rule, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", output: "5", errors: 1 }
+ ]
+ });
+
+ assert.throws(() => {
+ ruleTester.run("foo", replaceProgramWith5Rule, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", output: 5, errors: 1 }
+ ]
+ });
+ }, /Output is incorrect/u);
+ });
+
+ it("should throw an error when the expected output doesn't match and errors is just a number", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", output: "foo = bar", errors: 1 }
+ ]
+ });
+ }, /Output is incorrect/u);
+ });
+
+ it("should not throw an error when the expected output is null and no errors produce output", () => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "eval(x)", errors: 1, output: null },
+ { code: "eval(x); eval(y);", errors: 2, output: null }
+ ]
+ });
+ });
+
+ it("should throw an error when the expected output is null and problems produce output", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", output: null, errors: 1 }
+ ]
+ });
+ }, /Expected no autofixes to be suggested/u);
+
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ {
+ code: "var foo = bar; var qux = boop;",
+ output: null,
+ errors: 2
+ }
+ ]
+ });
+ }, /Expected no autofixes to be suggested/u);
+ });
+
+ it("should throw an error when the expected output is null and only some problems produce output", () => {
+ assert.throws(() => {
+ ruleTester.run("fixes-one-problem", require("./fixtures/fixes-one-problem"), {
+ valid: [],
+ invalid: [
+ { code: "foo", output: null, errors: 2 }
+ ]
+ });
+ }, /Expected no autofixes to be suggested/u);
+ });
+
+ it("should throw an error when the expected output isn't specified and problems produce output", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", errors: 1 }
+ ]
+ });
+ }, "The rule fixed the code. Please add 'output' property.");
+ });
+
+ it("should throw an error if invalid code specifies wrong type", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression2" }] }
+ ]
+ });
+ }, /Error type should be CallExpression2, found CallExpression/u);
+ });
+
+ it("should throw an error if invalid code specifies wrong line", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "eval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression", line: 5 }] }
+ ]
+ });
+ }, /Error line should be 5/u);
+ });
+
+ it("should not skip line assertion if line is a falsy value", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "\neval(foo)", errors: [{ message: "eval sucks.", type: "CallExpression", line: 0 }] }
+ ]
+ });
+ }, /Error line should be 0/u);
+ });
+
+ it("should throw an error if invalid code specifies wrong column", () => {
+ const wrongColumn = 10,
+ expectedErrorMessage = "Error column should be 1";
+
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: ["Eval(foo)"],
+ invalid: [{
+ code: "eval(foo)",
+ errors: [{
+ message: "eval sucks.",
+ column: wrongColumn
+ }]
+ }]
+ });
+ }, expectedErrorMessage);
+ });
+
+ it("should throw error for empty error array", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: []
+ }]
+ });
+ }, /Invalid cases must have at least one error/u);
+ });
+
+ it("should throw error for errors : 0", () => {
+ assert.throws(() => {
+ ruleTester.run(
+ "suggestions-messageIds",
+ require("./fixtures/suggestions")
+ .withMessageIds,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: "var foo;",
+ errors: 0
+ }
+ ]
+ }
+ );
+ }, /Invalid cases must have 'error' value greater than 0/u);
+ });
+
+ it("should not skip column assertion if column is a falsy value", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: ["Eval(foo)"],
+ invalid: [{
+ code: "var foo; eval(foo)",
+ errors: [{ message: "eval sucks.", column: 0 }]
+ }]
+ });
+ }, /Error column should be 0/u);
+ });
+
+ it("should throw an error if invalid code specifies wrong endLine", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration", endLine: 10 }] }
+ ]
+ });
+ }, "Error endLine should be 10");
+ });
+
+ it("should throw an error if invalid code specifies wrong endColumn", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [
+ "bar = baz;"
+ ],
+ invalid: [
+ { code: "var foo = bar;", output: "foo = bar", errors: [{ message: "Bad var.", type: "VariableDeclaration", endColumn: 10 }] }
+ ]
+ });
+ }, "Error endColumn should be 10");
+ });
+
+ it("should throw an error if invalid code has the wrong number of errors", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ {
+ code: "eval(foo)",
+ errors: [
+ { message: "eval sucks.", type: "CallExpression" },
+ { message: "eval sucks.", type: "CallExpression" }
+ ]
+ }
+ ]
+ });
+ }, /Should have 2 errors but had 1/u);
+ });
+
+ it("should throw an error if invalid code does not have errors", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "eval(foo)" }
+ ]
+ });
+ }, /Did not specify errors for an invalid test of no-eval/u);
+ });
+
+ it("should throw an error if invalid code has the wrong explicit number of errors", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "Eval(foo)"
+ ],
+ invalid: [
+ { code: "eval(foo)", errors: 2 }
+ ]
+ });
+ }, /Should have 2 errors but had 1/u);
+ });
+
+ it("should throw an error if there's a parsing error in a valid test", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "1eval('foo')"
+ ],
+ invalid: [
+ { code: "eval('foo')", errors: [{}] }
+ ]
+ });
+ }, /fatal parsing error/iu);
+ });
+
+ it("should throw an error if there's a parsing error in an invalid test", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "noeval('foo')"
+ ],
+ invalid: [
+ { code: "1eval('foo')", errors: [{}] }
+ ]
+ });
+ }, /fatal parsing error/iu);
+ });
+
+ it("should throw an error if there's a parsing error in an invalid test and errors is just a number", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ "noeval('foo')"
+ ],
+ invalid: [
+ { code: "1eval('foo')", errors: 1 }
+ ]
+ });
+ }, /fatal parsing error/iu);
+ });
+
+ // https://github.com/eslint/eslint/issues/4779
+ it("should throw an error if there's a parsing error and output doesn't match", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [],
+ invalid: [
+ { code: "obvious parser error", output: "string that doesnt match", errors: [{}] }
+ ]
+ });
+ }, /fatal parsing error/iu);
+ });
+
+ it("should not throw an error if invalid code has at least an expected empty error object", () => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: ["Eval(foo)"],
+ invalid: [{
+ code: "eval(foo)",
+ errors: [{}]
+ }]
+ });
+ });
+
+ it("should pass-through the globals config of valid tests to the to rule", () => {
+ ruleTester.run("no-test-global", require("./fixtures/no-test-global"), {
+ valid: [
+ "var test = 'foo'",
+ {
+ code: "var test2 = 'bar'",
+ globals: { test: true }
+ }
+ ],
+ invalid: [{ code: "bar", errors: 1 }]
+ });
+ });
+
+ it("should pass-through the globals config of invalid tests to the to rule", () => {
+ ruleTester.run("no-test-global", require("./fixtures/no-test-global"), {
+ valid: ["var test = 'foo'"],
+ invalid: [
+ {
+ code: "var test = 'foo'; var foo = 'bar'",
+ errors: 1
+ },
+ {
+ code: "var test = 'foo'",
+ globals: { foo: true },
+ errors: [{ message: "Global variable foo should not be used." }]
+ }
+ ]
+ });
+ });
+
+ it("should pass-through the settings config to rules", () => {
+ ruleTester.run("no-test-settings", require("./fixtures/no-test-settings"), {
+ valid: [
+ {
+ code: "var test = 'bar'", settings: { test: 1 }
+ }
+ ],
+ invalid: [
+ {
+ code: "var test = 'bar'", settings: { "no-test": 22 }, errors: 1
+ }
+ ]
+ });
+ });
+
+ it("should pass-through the filename to the rule", () => {
+ (function() {
+ ruleTester.run("", require("./fixtures/no-test-filename"), {
+ valid: [
+ {
+ code: "var foo = 'bar'",
+ filename: "somefile.js"
+ }
+ ],
+ invalid: [
+ {
+ code: "var foo = 'bar'",
+ errors: [
+ { message: "Filename test was not defined." }
+ ]
+ }
+ ]
+ });
+ }());
+ });
+
+ it("should pass-through the options to the rule", () => {
+ ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), {
+ valid: [
+ {
+ code: "var foo = 'bar'",
+ options: [false]
+ }
+ ],
+ invalid: [
+ {
+ code: "var foo = 'bar'",
+ options: [true],
+ errors: [{ message: "Invalid args" }]
+ }
+ ]
+ });
+ });
+
+ it("should throw an error if the options are an object", () => {
+ assert.throws(() => {
+ ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), {
+ valid: [
+ {
+ code: "foo",
+ options: { ok: true }
+ }
+ ],
+ invalid: []
+ });
+ }, /options must be an array/u);
+ });
+
+ it("should throw an error if the options are a number", () => {
+ assert.throws(() => {
+ ruleTester.run("no-invalid-args", require("./fixtures/no-invalid-args"), {
+ valid: [
+ {
+ code: "foo",
+ options: 0
+ }
+ ],
+ invalid: []
+ });
+ }, /options must be an array/u);
+ });
+
+ it("should pass-through the parser to the rule", () => {
+ const spy = sinon.spy(ruleTester.linter, "verify");
+
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ {
+ code: "Eval(foo)"
+ }
+ ],
+ invalid: [
+ {
+ code: "eval(foo)",
+ parser: require.resolve("esprima"),
+ errors: [{ line: 1 }]
+ }
+ ]
+ });
+ assert.strictEqual(spy.args[1][1].parser, require.resolve("esprima"));
+ });
+
+ // skipping because it's not something our parser cares about
+ it.skip("should pass normalized ecmaVersion to the rule", () => {
+ const reportEcmaVersionRule = {
+ meta: {
+ messages: {
+ ecmaVersionMessage: "context.parserOptions.ecmaVersion is {{type}} {{ecmaVersion}}."
+ }
+ },
+ create: context => ({
+ Program(node) {
+ const { ecmaVersion } = context.parserOptions;
+
+ context.report({
+ node,
+ messageId: "ecmaVersionMessage",
+ data: { type: typeof ecmaVersion, ecmaVersion }
+ });
+ }
+ })
+ };
+
+ const notEspree = require.resolve("./fixtures/empty-program-parser");
+
+ ruleTester.run("report-ecma-version", reportEcmaVersionRule, {
+ valid: [],
+ invalid: [
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }]
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ parserOptions: {}
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ parserOptions: { ecmaFeatures: { jsx: true } }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ parser: require.resolve("espree")
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }],
+ parserOptions: { ecmaVersion: 6 }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }],
+ parserOptions: { ecmaVersion: 2015 }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ env: { browser: true }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ env: { es6: false }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }],
+ env: { es6: true }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }],
+ env: { es6: false, es2017: true }
+ },
+ {
+ code: "let x",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }],
+ env: { es6: "truthy" }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "8" } }],
+ env: { es2017: true }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "11" } }],
+ env: { es2020: true }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "12" } }],
+ env: { es2021: true }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }],
+ parserOptions: { ecmaVersion: "latest" }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }],
+ parser: require.resolve("espree"),
+ parserOptions: { ecmaVersion: "latest" }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }],
+ parserOptions: { ecmaVersion: "latest", ecmaFeatures: { jsx: true } }
+ },
+ {
+ code: "import 'foo'",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }],
+ parserOptions: { ecmaVersion: "latest", sourceType: "module" }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }],
+ parserOptions: { ecmaVersion: "latest" },
+ env: { es6: true }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: String(espree.latestEcmaVersion) } }],
+ parserOptions: { ecmaVersion: "latest" },
+ env: { es2020: true }
+ },
+
+ // Non-Espree parsers normalize ecmaVersion if it's not "latest"
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ parser: notEspree
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }],
+ parser: notEspree,
+ parserOptions: {}
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "5" } }],
+ parser: notEspree,
+ parserOptions: { ecmaVersion: 5 }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }],
+ parser: notEspree,
+ parserOptions: { ecmaVersion: 6 }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: 6 } }],
+ parser: notEspree,
+ parserOptions: { ecmaVersion: 2015 }
+ },
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }],
+ parser: notEspree,
+ parserOptions: { ecmaVersion: "latest" }
+ }
+ ]
+ });
+
+ [{ parserOptions: { ecmaVersion: 6 } }, { env: { es6: true } }].forEach(options => {
+ new RuleTester(options).run("report-ecma-version", reportEcmaVersionRule, {
+ valid: [],
+ invalid: [
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }]
+ },
+ {
+ code: "",
+ parserOptions: {},
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "number", ecmaVersion: "6" } }]
+ }
+ ]
+ });
+ });
+
+ new RuleTester({ parser: notEspree }).run("report-ecma-version", reportEcmaVersionRule, {
+ valid: [],
+ invalid: [
+ {
+ code: "",
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "undefined", ecmaVersion: "undefined" } }]
+ },
+ {
+ code: "",
+ parserOptions: { ecmaVersion: "latest" },
+ errors: [{ messageId: "ecmaVersionMessage", data: { type: "string", ecmaVersion: "latest" } }]
+ }
+ ]
+ });
+ });
+
+ it("should pass-through services from parseForESLint to the rule", () => {
+ const enhancedParserPath = require.resolve("./fixtures/enhanced-parser");
+ const disallowHiRule = {
+ create: context => ({
+ Literal(node) {
+ const disallowed = context.parserServices.test.getMessage(); // returns "Hi!"
+
+ if (node.value === disallowed) {
+ context.report({ node, message: `Don't use '${disallowed}'` });
+ }
+ }
+ })
+ };
+
+ ruleTester.run("no-hi", disallowHiRule, {
+ valid: [
+ {
+ code: "'Hello!'",
+ parser: enhancedParserPath
+ }
+ ],
+ invalid: [
+ {
+ code: "'Hi!'",
+ parser: enhancedParserPath,
+ errors: [{ message: "Don't use 'Hi!'" }]
+ }
+ ]
+ });
+ });
+
+ it("should prevent invalid options schemas", () => {
+ assert.throws(() => {
+ ruleTester.run("no-invalid-schema", require("./fixtures/no-invalid-schema"), {
+ valid: [
+ "var answer = 6 * 7;",
+ { code: "var answer = 6 * 7;", options: [] }
+ ],
+ invalid: [
+ { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected nothing." }] }
+ ]
+ });
+ }, "Schema for rule no-invalid-schema is invalid:,\titems: should be object\n\titems[0].enum: should NOT have fewer than 1 items\n\titems: should match some schema in anyOf");
+
+ });
+
+ it("should prevent schema violations in options", () => {
+ assert.throws(() => {
+ ruleTester.run("no-schema-violation", require("./fixtures/no-schema-violation"), {
+ valid: [
+ "var answer = 6 * 7;",
+ { code: "var answer = 6 * 7;", options: ["foo"] }
+ ],
+ invalid: [
+ { code: "var answer = 6 * 7;", options: ["bar"], errors: [{ message: "Expected foo." }] }
+ ]
+ });
+ }, /Value "bar" should be equal to one of the allowed values./u);
+
+ });
+
+ it("should disallow invalid defaults in rules", () => {
+ const ruleWithInvalidDefaults = {
+ meta: {
+ schema: [
+ {
+ oneOf: [
+ { enum: ["foo"] },
+ {
+ type: "object",
+ properties: {
+ foo: {
+ enum: ["foo", "bar"],
+ default: "foo"
+ }
+ },
+ additionalProperties: false
+ }
+ ]
+ }
+ ]
+ },
+ create: () => ({})
+ };
+
+ assert.throws(() => {
+ ruleTester.run("invalid-defaults", ruleWithInvalidDefaults, {
+ valid: [
+ {
+ code: "foo",
+ options: [{}]
+ }
+ ],
+ invalid: []
+ });
+ }, /Schema for rule invalid-defaults is invalid: default is ignored for: data1\.foo/u);
+ });
+
+ it("throw an error when an unknown config option is included", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ { code: "Eval(foo)", foo: "bar" }
+ ],
+ invalid: []
+ });
+ }, /ESLint configuration in rule-tester is invalid./u);
+ });
+
+ it("throw an error when an invalid config value is included", () => {
+ assert.throws(() => {
+ ruleTester.run("no-eval", require("./fixtures/no-eval"), {
+ valid: [
+ { code: "Eval(foo)", env: ["es6"] }
+ ],
+ invalid: []
+ });
+ }, /Property "env" is the wrong type./u);
+ });
+
+ it("should pass-through the tester config to the rule", () => {
+ ruleTester = new RuleTester({
+ globals: { test: true }
+ });
+
+ ruleTester.run("no-test-global", require("./fixtures/no-test-global"), {
+ valid: [
+ "var test = 'foo'",
+ "var test2 = test"
+ ],
+ invalid: [{ code: "bar", errors: 1, globals: { foo: true } }]
+ });
+ });
+
+ it("should correctly set the globals configuration", () => {
+ const config = { globals: { test: true } };
+
+ RuleTester.setDefaultConfig(config);
+ assert(
+ RuleTester.getDefaultConfig().globals.test,
+ "The default config object is incorrect"
+ );
+ });
+
+ it("should correctly reset the global configuration", () => {
+ const config = { globals: { test: true } };
+
+ RuleTester.setDefaultConfig(config);
+ RuleTester.resetDefaultConfig();
+ assert.deepStrictEqual(
+ RuleTester.getDefaultConfig(),
+ { parser: require.resolve('@typescript-eslint/parser'), rules: {} },
+ "The default configuration has not reset correctly"
+ );
+ });
+
+ it("should enforce the global configuration to be an object", () => {
+
+ /**
+ * Set the default config for the rules tester
+ * @param {Object} config configuration object
+ * @returns {Function} Function to be executed
+ * @private
+ */
+ function setConfig(config) {
+ return function() {
+ RuleTester.setDefaultConfig(config);
+ };
+ }
+ assert.throw(setConfig());
+ assert.throw(setConfig(1));
+ assert.throw(setConfig(3.14));
+ assert.throw(setConfig("foo"));
+ assert.throw(setConfig(null));
+ assert.throw(setConfig(true));
+ });
+
+ it("should pass-through the globals config to the tester then to the to rule", () => {
+ const config = { globals: { test: true } };
+
+ RuleTester.setDefaultConfig(config);
+ ruleTester = new RuleTester();
+
+ ruleTester.run("no-test-global", require("./fixtures/no-test-global"), {
+ valid: [
+ "var test = 'foo'",
+ "var test2 = test"
+ ],
+ invalid: [{ code: "bar", errors: 1, globals: { foo: true } }]
+ });
+ });
+
+ it("should throw an error if AST was modified", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast"), {
+ valid: [
+ "var foo = 0;"
+ ],
+ invalid: []
+ });
+ }, "Rule should not modify AST.");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast"), {
+ valid: [],
+ invalid: [
+ { code: "var bar = 0;", errors: ["error"] }
+ ]
+ });
+ }, "Rule should not modify AST.");
+ });
+
+ it("should throw an error if AST was modified (at Program)", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-first"), {
+ valid: [
+ "var foo = 0;"
+ ],
+ invalid: []
+ });
+ }, "Rule should not modify AST.");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-first"), {
+ valid: [],
+ invalid: [
+ { code: "var bar = 0;", errors: ["error"] }
+ ]
+ });
+ }, "Rule should not modify AST.");
+ });
+
+ it("should throw an error if AST was modified (at Program:exit)", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {
+ valid: [
+ "var foo = 0;"
+ ],
+ invalid: []
+ });
+ }, "Rule should not modify AST.");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {
+ valid: [],
+ invalid: [
+ { code: "var bar = 0;", errors: ["error"] }
+ ]
+ });
+ }, "Rule should not modify AST.");
+ });
+
+ it("should throw an error if rule uses start and end properties on nodes, tokens or comments", () => {
+ const usesStartEndRule = {
+ create(context) {
+
+ const sourceCode = context.getSourceCode();
+
+ return {
+ CallExpression(node) {
+ noop(node.arguments[1].start);
+ },
+ "BinaryExpression[operator='+']"(node) {
+ noop(node.end);
+ },
+ "UnaryExpression[operator='-']"(node) {
+ noop(sourceCode.getFirstToken(node).start);
+ },
+ ConditionalExpression(node) {
+ noop(sourceCode.getFirstToken(node).end);
+ },
+ BlockStatement(node) {
+ noop(sourceCode.getCommentsInside(node)[0].start);
+ },
+ ObjectExpression(node) {
+ noop(sourceCode.getCommentsInside(node)[0].end);
+ },
+ Decorator(node) {
+ noop(node.start);
+ }
+ };
+ }
+ };
+
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: ["foo(a, b)"],
+ invalid: []
+ });
+ }, "Use node.range[0] instead of node.start");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [],
+ invalid: [{ code: "var a = b * (c + d) / e;", errors: 1 }]
+ });
+ }, "Use node.range[1] instead of node.end");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [],
+ invalid: [{ code: "var a = -b * c;", errors: 1 }]
+ });
+ }, "Use token.range[0] instead of token.start");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: ["var a = b ? c : d;"],
+ invalid: []
+ });
+ }, "Use token.range[1] instead of token.end");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: ["function f() { /* comment */ }"],
+ invalid: []
+ });
+ }, "Use token.range[0] instead of token.start");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [],
+ invalid: [{ code: "var x = //\n {\n //comment\n //\n}", errors: 1 }]
+ });
+ }, "Use token.range[1] instead of token.end");
+
+ const enhancedParserPath = require.resolve("./fixtures/enhanced-parser");
+
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [{ code: "foo(a, b)", parser: enhancedParserPath }],
+ invalid: []
+ });
+ }, "Use node.range[0] instead of node.start");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [],
+ invalid: [{ code: "var a = b * (c + d) / e;", parser: enhancedParserPath, errors: 1 }]
+ });
+ }, "Use node.range[1] instead of node.end");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [],
+ invalid: [{ code: "var a = -b * c;", parser: enhancedParserPath, errors: 1 }]
+ });
+ }, "Use token.range[0] instead of token.start");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [{ code: "var a = b ? c : d;", parser: enhancedParserPath }],
+ invalid: []
+ });
+ }, "Use token.range[1] instead of token.end");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [{ code: "function f() { /* comment */ }", parser: enhancedParserPath }],
+ invalid: []
+ });
+ }, "Use token.range[0] instead of token.start");
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [],
+ invalid: [{ code: "var x = //\n {\n //comment\n //\n}", parser: enhancedParserPath, errors: 1 }]
+ });
+ }, "Use token.range[1] instead of token.end");
+
+ assert.throws(() => {
+ ruleTester.run("uses-start-end", usesStartEndRule, {
+ valid: [{ code: "@foo class A {}", parser: require.resolve("./fixtures/enhanced-parser2") }],
+ invalid: []
+ });
+ }, "Use node.range[0] instead of node.start");
+ });
+
+ it("should throw an error if no test scenarios given", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"));
+ }, "Test Scenarios for rule foo : Could not find test scenario object");
+ });
+
+ it("should throw an error if no acceptable test scenario object is given", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), []);
+ }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), "");
+ }, "Test Scenarios for rule foo : Could not find test scenario object");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), 2);
+ }, "Test Scenarios for rule foo : Could not find test scenario object");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {});
+ }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios\nCould not find any invalid test scenarios");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {
+ valid: []
+ });
+ }, "Test Scenarios for rule foo is invalid:\nCould not find any invalid test scenarios");
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/modify-ast-at-last"), {
+ invalid: []
+ });
+ }, "Test Scenarios for rule foo is invalid:\nCould not find any valid test scenarios");
+ });
+
+ // Nominal message/messageId use cases
+ it("should assert match if message provided in both test and result.", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ message: "something" }] }]
+ });
+ }, /Avoid using variables named/u);
+
+ ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ message: "Avoid using variables named 'foo'." }] }]
+ });
+ });
+
+ it("should assert match between messageId if provided in both test and result.", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ messageId: "unused" }] }]
+ });
+ }, "messageId 'avoidFoo' does not match expected messageId 'unused'.");
+
+ ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
+ });
+ });
+ it("should assert match between resulting message output if messageId and data provided in both test and result", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo", data: { name: "notFoo" } }] }]
+ });
+ }, "Hydrated message \"Avoid using variables named 'notFoo'.\" does not match \"Avoid using variables named 'foo'.\"");
+ });
+
+ // messageId/message misconfiguration cases
+ it("should throw if user tests for both message and messageId", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ message: "something", messageId: "avoidFoo" }] }]
+ });
+ }, "Error should not specify both 'message' and a 'messageId'.");
+ });
+ it("should throw if user tests for messageId but the rule doesn't use the messageId meta syntax.", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMessageOnly, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ messageId: "avoidFoo" }] }]
+ });
+ }, "Error can not use 'messageId' if rule under test doesn't define 'meta.messages'");
+ });
+ it("should throw if user tests for messageId not listed in the rule's meta syntax.", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ messageId: "useFoo" }] }]
+ });
+ }, /Invalid messageId 'useFoo'/u);
+ });
+ it("should throw if data provided without messageId.", () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/messageId").withMetaWithData, {
+ valid: [],
+ invalid: [{ code: "foo", errors: [{ data: "something" }] }]
+ });
+ }, "Error must specify 'messageId' if 'data' is used.");
+ });
+
+ describe("suggestions", () => {
+ it("should pass with valid suggestions (tested using desc)", () => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [
+ "var boo;"
+ ],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ output: "var bar;"
+ }]
+ }]
+ }]
+ });
+ });
+
+ it("should pass with suggestions on multiple lines", () => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [
+ {
+ code: "function foo() {\n var foo = 1;\n}",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ output: "function bar() {\n var foo = 1;\n}"
+ }]
+ }, {
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ output: "function foo() {\n var bar = 1;\n}"
+ }]
+ }]
+ }
+ ]
+ });
+ });
+
+ it("should pass with valid suggestions (tested using messageIds)", () => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ });
+
+ it("should pass with valid suggestions (one tested using messageIds, the other using desc)", () => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ desc: "Rename identifier 'foo' to 'baz'",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ });
+
+ it("should pass with valid suggestions (tested using both desc and messageIds for the same suggestion)", () => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ desc: "Rename identifier 'foo' to 'baz'",
+ messageId: "renameFoo",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ });
+
+ it("should pass with valid suggestions (tested using only desc on a rule that utilizes meta.messages)", () => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ output: "var bar;"
+ }, {
+ desc: "Rename identifier 'foo' to 'baz'",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ });
+
+ it("should pass with valid suggestions (tested using messageIds and data)", () => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ data: { newName: "bar" },
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ data: { newName: "baz" },
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ });
+
+
+ it("should pass when tested using empty suggestion test objects if the array length is correct", () => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{}, {}]
+ }]
+ }]
+ });
+ });
+
+ it("should support explicitly expecting no suggestions", () => {
+ [void 0, null, false, []].forEach(suggestions => {
+ ruleTester.run("suggestions-basic", require("./fixtures/no-eval"), {
+ valid: [],
+ invalid: [{
+ code: "eval('var foo');",
+ errors: [{
+ suggestions
+ }]
+ }]
+ });
+ });
+ });
+
+ it("should fail when expecting no suggestions and there are suggestions", () => {
+ [void 0, null, false, []].forEach(suggestions => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions
+ }]
+ }]
+ });
+ }, "Error should have no suggestions on error with message: \"Avoid using identifiers named 'foo'.\"");
+ });
+ });
+
+ it("should fail when testing for suggestions that don't exist", () => {
+ assert.throws(() => {
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "this-does-not-exist"
+ }]
+ }]
+ }]
+ });
+ }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\"");
+ });
+
+ it("should fail when there are a different number of suggestions", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ output: "var bar;"
+ }, {
+ desc: "Rename identifier 'foo' to 'baz'",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error should have 2 suggestions. Instead found 1 suggestions");
+ });
+
+ it("should throw if the suggestion description doesn't match", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "not right",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 0 : desc should be \"not right\" but got \"Rename identifier 'foo' to 'bar'\" instead.");
+ });
+
+ it("should throw if the suggestion description doesn't match (although messageIds match)", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ desc: "Rename id 'foo' to 'baz'",
+ messageId: "renameFoo",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 1 : desc should be \"Rename id 'foo' to 'baz'\" but got \"Rename identifier 'foo' to 'baz'\" instead.");
+ });
+
+ it("should throw if the suggestion messageId doesn't match", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "unused",
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 0 : messageId should be 'unused' but got 'renameFoo' instead.");
+ });
+
+ it("should throw if the suggestion messageId doesn't match (although descriptions match)", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ desc: "Rename identifier 'foo' to 'baz'",
+ messageId: "avoidFoo",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 1 : messageId should be 'avoidFoo' but got 'renameFoo' instead.");
+ });
+
+ it("should throw if test specifies messageId for a rule that doesn't have meta.messages", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ output: "var bar;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 0 : Test can not use 'messageId' if rule under test doesn't define 'meta.messages'.");
+ });
+
+ it("should throw if test specifies messageId that doesn't exist in the rule's meta.messages", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ messageId: "removeFoo",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 1 : Test has invalid messageId 'removeFoo', the rule under test allows only one of ['avoidFoo', 'unused', 'renameFoo'].");
+ });
+
+ it("should throw if hydrated desc doesn't match (wrong data value)", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ data: { newName: "car" },
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ data: { newName: "baz" },
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 0 : Hydrated test desc \"Rename identifier 'foo' to 'car'\" does not match received desc \"Rename identifier 'foo' to 'bar'\".");
+ });
+
+ it("should throw if hydrated desc doesn't match (wrong data key)", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ data: { newName: "bar" },
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ data: { name: "baz" },
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 1 : Hydrated test desc \"Rename identifier 'foo' to '{{ newName }}'\" does not match received desc \"Rename identifier 'foo' to 'baz'\".");
+ });
+
+ it("should throw if test specifies both desc and data", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ messageId: "renameFoo",
+ data: { newName: "bar" },
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ data: { newName: "baz" },
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 0 : Test should not specify both 'desc' and 'data'.");
+ });
+
+ it("should throw if test uses data but doesn't specify messageId", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ data: { newName: "bar" },
+ output: "var bar;"
+ }, {
+ data: { newName: "baz" },
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Error Suggestion at index 1 : Test must specify 'messageId' if 'data' is used.");
+ });
+
+ it("should throw if the resulting suggestion output doesn't match", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ output: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, "Expected the applied suggestion fix to match the test suggestion output");
+ });
+
+ it("should fail when specified suggestion isn't an object", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [null]
+ }]
+ }]
+ });
+ }, "Test suggestion in 'suggestions' array must be an object.");
+
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [
+ {
+ messageId: "renameFoo",
+ output: "var bar;"
+ },
+ "Rename identifier 'foo' to 'baz'"
+ ]
+ }]
+ }]
+ });
+ }, "Test suggestion in 'suggestions' array must be an object.");
+ });
+
+ it("should fail when the suggestion is an object with an unknown property name", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-basic", require("./fixtures/suggestions").basic, {
+ valid: [
+ "var boo;"
+ ],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ message: "Rename identifier 'foo' to 'bar'"
+ }]
+ }]
+ }]
+ });
+ }, /Invalid suggestion property name 'message'/u);
+ });
+
+ it("should fail when any of the suggestions is an object with an unknown property name", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-messageIds", require("./fixtures/suggestions").withMessageIds, {
+ valid: [],
+ invalid: [{
+ code: "var foo;",
+ errors: [{
+ suggestions: [{
+ messageId: "renameFoo",
+ output: "var bar;"
+ }, {
+ messageId: "renameFoo",
+ outpt: "var baz;"
+ }]
+ }]
+ }]
+ });
+ }, /Invalid suggestion property name 'outpt'/u);
+ });
+
+ it("should throw an error if a rule that doesn't have `meta.hasSuggestions` enabled produces suggestions", () => {
+ assert.throws(() => {
+ ruleTester.run("suggestions-missing-hasSuggestions-property", require("./fixtures/suggestions").withoutHasSuggestionsProperty, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", output: "5", errors: 1 }
+ ]
+ });
+ }, "Rules with suggestions must set the `meta.hasSuggestions` property to `true`.");
+ });
+ });
+
+ describe("deprecations", () => {
+ let processStub;
+ const ruleWithNoSchema = {
+ meta: {
+ type: "suggestion"
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+ };
+ const ruleWithNoMeta = {
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+ };
+
+ beforeEach(() => {
+ processStub = sinon.stub(process, "emitWarning");
+ });
+
+ afterEach(() => {
+ processStub.restore();
+ });
+
+ it("should log a deprecation warning when using the legacy function-style API for rule", () => {
+
+ /**
+ * Legacy-format rule (a function instead of an object with `create` method).
+ * @param {RuleContext} context The ESLint rule context object.
+ * @returns {Object} Listeners.
+ */
+ function functionStyleRule(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+
+ ruleTester.run("function-style-rule", functionStyleRule, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once");
+ assert.deepStrictEqual(
+ processStub.getCall(0).args,
+ [
+ "\"function-style-rule\" rule is using the deprecated function-style format and will stop working in ESLint v9. Please use object-style format: https://eslint.org/docs/latest/extend/custom-rules",
+ "DeprecationWarning"
+ ]
+ );
+ });
+
+ it("should log a deprecation warning when meta is not defined for the rule", () => {
+ ruleTester.run("rule-with-no-meta-1", ruleWithNoMeta, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once");
+ assert.deepStrictEqual(
+ processStub.getCall(0).args,
+ [
+ "\"rule-with-no-meta-1\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas",
+ "DeprecationWarning"
+ ]
+ );
+ });
+
+ it("should log a deprecation warning when schema is not defined for the rule", () => {
+ ruleTester.run("rule-with-no-schema-1", ruleWithNoSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once");
+ assert.deepStrictEqual(
+ processStub.getCall(0).args,
+ [
+ "\"rule-with-no-schema-1\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas",
+ "DeprecationWarning"
+ ]
+ );
+ });
+
+ it("should log a deprecation warning when schema is `undefined`", () => {
+ const ruleWithUndefinedSchema = {
+ meta: {
+ type: "problem",
+ // eslint-disable-next-line no-undefined -- intentionally added for test case
+ schema: undefined
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+ };
+
+ ruleTester.run("rule-with-undefined-schema", ruleWithUndefinedSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once");
+ assert.deepStrictEqual(
+ processStub.getCall(0).args,
+ [
+ "\"rule-with-undefined-schema\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas",
+ "DeprecationWarning"
+ ]
+ );
+ });
+
+ it("should log a deprecation warning when schema is `null`", () => {
+ const ruleWithNullSchema = {
+ meta: {
+ type: "problem",
+ schema: null
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+ };
+
+ ruleTester.run("rule-with-null-schema", ruleWithNullSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", options: [{ foo: true }], errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once");
+ assert.deepStrictEqual(
+ processStub.getCall(0).args,
+ [
+ "\"rule-with-null-schema\" rule has options but is missing the \"meta.schema\" property and will stop working in ESLint v9. Please add a schema: https://eslint.org/docs/latest/extend/custom-rules#options-schemas",
+ "DeprecationWarning"
+ ]
+ );
+ });
+
+ it("should not log a deprecation warning when schema is an empty array", () => {
+ const ruleWithEmptySchema = {
+ meta: {
+ type: "suggestion",
+ schema: []
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+ };
+
+ ruleTester.run("rule-with-no-options", ruleWithEmptySchema, {
+ valid: [],
+ invalid: [{ code: "var foo = bar;", errors: 1 }]
+ });
+
+ assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`");
+ });
+
+ it("When the rule is an object-style rule, the legacy rule API warning is not emitted", () => {
+ ruleTester.run("rule-with-no-schema-2", ruleWithNoSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`");
+ });
+
+ it("When the rule has meta.schema and there are test cases with options, the missing schema warning is not emitted", () => {
+ const ruleWithSchema = {
+ meta: {
+ type: "suggestion",
+ schema: [{
+ type: "boolean"
+ }]
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({ node, message: "bad" });
+ }
+ };
+ }
+ };
+
+ ruleTester.run("rule-with-schema", ruleWithSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", options: [true], errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`");
+ });
+
+ it("When the rule does not have meta, but there are no test cases with options, the missing schema warning is not emitted", () => {
+ ruleTester.run("rule-with-no-meta-2", ruleWithNoMeta, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`");
+ });
+
+ it("When the rule has meta without meta.schema, but there are no test cases with options, the missing schema warning is not emitted", () => {
+ ruleTester.run("rule-with-no-schema-3", ruleWithNoSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`");
+ });
+ it("When the rule has meta without meta.schema, and some test cases have options property but it's an empty array, the missing schema warning is not emitted", () => {
+ ruleTester.run("rule-with-no-schema-4", ruleWithNoSchema, {
+ valid: [],
+ invalid: [
+ { code: "var foo = bar;", options: [], errors: 1 }
+ ]
+ });
+
+ assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`");
+ });
+ });
+
+ /**
+ * Asserts that a particular value will be emitted from an EventEmitter.
+ * @param {EventEmitter} emitter The emitter that should emit a value
+ * @param {string} emitType The type of emission to listen for
+ * @param {any} expectedValue The value that should be emitted
+ * @returns {Promise} A Promise that fulfills if the value is emitted, and rejects if something else is emitted.
+ * The Promise will be indefinitely pending if no value is emitted.
+ */
+ function assertEmitted(emitter, emitType, expectedValue) {
+ return new Promise((resolve, reject) => {
+ emitter.once(emitType, emittedValue => {
+ if (emittedValue === expectedValue) {
+ resolve();
+ } else {
+ reject(new Error(`Expected ${expectedValue} to be emitted but ${emittedValue} was emitted instead.`));
+ }
+ });
+ });
+ }
+
+ describe("naming test cases", () => {
+
+ it("should use the first argument as the name of the test suite", () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "describe", "this-is-a-rule-name");
+
+ ruleTester.run("this-is-a-rule-name", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: []
+ });
+
+ return assertion;
+ });
+
+ it("should use the test code as the name of the tests for valid code (string form)", () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "valid(code);");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [
+ "valid(code);"
+ ],
+ invalid: []
+ });
+
+ return assertion;
+ });
+
+ it("should use the test code as the name of the tests for valid code (object form)", () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "valid(code);");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [
+ {
+ code: "valid(code);"
+ }
+ ],
+ invalid: []
+ });
+
+ return assertion;
+ });
+
+ it("should use the test code as the name of the tests for invalid code", () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "var x = invalid(code);");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ {
+ code: "var x = invalid(code);",
+ output: " x = invalid(code);",
+ errors: 1
+ }
+ ]
+ });
+
+ return assertion;
+ });
+
+ // https://github.com/eslint/eslint/issues/8142
+ it("should use the empty string as the name of the test if the test case is an empty string", () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [
+ {
+ code: ""
+ }
+ ],
+ invalid: []
+ });
+
+ return assertion;
+ });
+
+ it('should use the "name" property if set to a non-empty string', () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "my test");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ {
+ name: "my test",
+ code: "var x = invalid(code);",
+ output: " x = invalid(code);",
+ errors: 1
+ }
+ ]
+ });
+
+ return assertion;
+ });
+
+ it('should use the "name" property if set to a non-empty string for valid cases too', () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "my test");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [
+ {
+ name: "my test",
+ code: "valid(code);"
+ }
+ ],
+ invalid: []
+ });
+
+ return assertion;
+ });
+
+
+ it('should use the test code as the name if the "name" property is set to an empty string', () => {
+ const assertion = assertEmitted(ruleTesterTestEmitter, "it", "var x = invalid(code);");
+
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ {
+ name: "",
+ code: "var x = invalid(code);",
+ output: " x = invalid(code);",
+ errors: 1
+ }
+ ]
+ });
+
+ return assertion;
+ });
+
+ it('should throw if "name" property is not a string', () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [{ code: "foo", name: 123 }],
+ invalid: [{ code: "foo" }]
+
+ });
+ }, /Optional test case property 'name' must be a string/u);
+
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: ["foo"],
+ invalid: [{ code: "foo", name: 123 }]
+ });
+ }, /Optional test case property 'name' must be a string/u);
+ });
+
+ it('should throw if "code" property is not a string', () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [{ code: 123 }],
+ invalid: [{ code: "foo" }]
+
+ });
+ }, /Test case must specify a string value for 'code'/u);
+
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [123],
+ invalid: [{ code: "foo" }]
+
+ });
+ }, /Test case must specify a string value for 'code'/u);
+
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: ["foo"],
+ invalid: [{ code: 123 }]
+ });
+ }, /Test case must specify a string value for 'code'/u);
+ });
+
+ it('should throw if "code" property is missing', () => {
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: [{ }],
+ invalid: [{ code: "foo" }]
+
+ });
+ }, /Test case must specify a string value for 'code'/u);
+
+ assert.throws(() => {
+ ruleTester.run("foo", require("./fixtures/no-var"), {
+ valid: ["foo"],
+ invalid: [{ }]
+ });
+ }, /Test case must specify a string value for 'code'/u);
+ });
+ });
+
+ // https://github.com/eslint/eslint/issues/11615
+ it("should fail the case if autofix made a syntax error.", () => {
+ assert.throw(() => {
+ ruleTester.run(
+ "foo",
+ {
+ meta: {
+ fixable: "code"
+ },
+ create(context) {
+ return {
+ Identifier(node) {
+ context.report({
+ node,
+ message: "make a syntax error",
+ fix(fixer) {
+ return fixer.replaceText(node, "one two");
+ }
+ });
+ }
+ };
+ }
+ },
+ {
+ valid: ["one()"],
+ invalid: []
+ }
+ );
+ }, /A fatal parsing error occurred in autofix.\nError: .+\nAutofix output:\n.+/u);
+ });
+
+ describe("sanitize test cases", () => {
+ let originalRuleTesterIt;
+ let spyRuleTesterIt;
+
+ before(() => {
+ originalRuleTesterIt = RuleTester.it;
+ spyRuleTesterIt = sinon.spy();
+ RuleTester.it = spyRuleTesterIt;
+ });
+ after(() => {
+ RuleTester.it = originalRuleTesterIt;
+ });
+ beforeEach(() => {
+ spyRuleTesterIt.resetHistory();
+ ruleTester = new RuleTester();
+ });
+ it("should present newline when using back-tick as new line", () => {
+ const code = `
+ var foo = bar;`;
+
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ {
+ code,
+ errors: [/^Bad var/u]
+ }
+ ]
+ });
+ sinon.assert.calledWith(spyRuleTesterIt, code);
+ });
+ it("should present \\u0000 as a string", () => {
+ const code = "\u0000";
+
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ {
+ code,
+ errors: [/^Bad var/u]
+ }
+ ]
+ });
+ sinon.assert.calledWith(spyRuleTesterIt, "\\u0000");
+ });
+ it("should present the pipe character correctly", () => {
+ const code = "var foo = bar || baz;";
+
+ ruleTester.run("no-var", require("./fixtures/no-var"), {
+ valid: [],
+ invalid: [
+ {
+ code,
+ errors: [/^Bad var/u]
+ }
+ ]
+ });
+ sinon.assert.calledWith(spyRuleTesterIt, code);
+ });
+
+ });
+
+ describe("SourceCode#getComments()", () => {
+ const useGetCommentsRule = {
+ create: context => ({
+ Program(node) {
+ const sourceCode = context.getSourceCode();
+
+ sourceCode.getComments(node);
+ }
+ })
+ };
+
+ it("should throw if called from a valid test case", () => {
+ assert.throws(() => {
+ ruleTester.run("use-get-comments", useGetCommentsRule, {
+ valid: [""],
+ invalid: []
+ });
+ }, /`SourceCode#getComments\(\)` is deprecated/u);
+ });
+
+ it("should throw if called from an invalid test case", () => {
+ assert.throws(() => {
+ ruleTester.run("use-get-comments", useGetCommentsRule, {
+ valid: [],
+ invalid: [{
+ code: "",
+ errors: [{}]
+ }]
+ });
+ }, /`SourceCode#getComments\(\)` is deprecated/u);
+ });
+ });
+
+ describe("Subclassing", () => {
+
+ it("should allow subclasses to set the describe/it/itOnly statics and should correctly use those values", () => {
+ const assertionDescribe = assertEmitted(ruleTesterTestEmitter, "custom describe", "this-is-a-rule-name");
+ const assertionIt = assertEmitted(ruleTesterTestEmitter, "custom it", "valid(code);");
+ const assertionItOnly = assertEmitted(ruleTesterTestEmitter, "custom itOnly", "validOnly(code);");
+
+ /**
+ * Subclass for testing
+ */
+ class RuleTesterSubclass extends RuleTester { }
+ RuleTesterSubclass.describe = function(text, method) {
+ ruleTesterTestEmitter.emit("custom describe", text, method);
+ return method.call(this);
+ };
+ RuleTesterSubclass.it = function(text, method) {
+ ruleTesterTestEmitter.emit("custom it", text, method);
+ return method.call(this);
+ };
+ RuleTesterSubclass.itOnly = function(text, method) {
+ ruleTesterTestEmitter.emit("custom itOnly", text, method);
+ return method.call(this);
+ };
+
+ const ruleTesterSubclass = new RuleTesterSubclass();
+
+ ruleTesterSubclass.run("this-is-a-rule-name", require("./fixtures/no-var"), {
+ valid: [
+ "valid(code);",
+ {
+ code: "validOnly(code);",
+ only: true
+ }
+ ],
+ invalid: []
+ });
+
+ return Promise.all([
+ assertionDescribe,
+ assertionIt,
+ assertionItOnly
+ ]);
+ });
+
+ });
+
+});
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/empty-program-parser.js b/packages/rule-tester/tests/eslint-base/fixtures/empty-program-parser.js
new file mode 100644
index 000000000000..06a87c90dcde
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/empty-program-parser.js
@@ -0,0 +1,29 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/parsers/empty-program-parser.js
+
+"use strict";
+
+exports.parse = function (text, parserOptions) {
+ return {
+ "type": "Program",
+ "start": 0,
+ "end": 0,
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 1,
+ "column": 0
+ }
+ },
+ "range": [
+ 0,
+ 0
+ ],
+ "body": [],
+ "sourceType": "script",
+ "comments": [],
+ "tokens": []
+ };
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/enhanced-parser.js b/packages/rule-tester/tests/eslint-base/fixtures/enhanced-parser.js
new file mode 100644
index 000000000000..9aef71554d3d
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/enhanced-parser.js
@@ -0,0 +1,20 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/parsers/enhanced-parser.js
+
+var espree = require("espree");
+
+exports.parseForESLint = function(code, options) {
+ return {
+ ast: espree.parse(code, options),
+ services: {
+ test: {
+ getMessage() {
+ return "Hi!";
+ }
+ }
+ }
+ };
+};
+
+exports.parse = function() {
+ throw new Error("Use parseForESLint() instead.");
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/enhanced-parser2.js b/packages/rule-tester/tests/eslint-base/fixtures/enhanced-parser2.js
new file mode 100644
index 000000000000..bd91282a38c6
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/enhanced-parser2.js
@@ -0,0 +1,25 @@
+// Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/parsers/enhanced-parser2.js
+
+"use strict";
+
+const assert = require("assert");
+const vk = require("eslint-visitor-keys");
+const KEYS = vk.unionWith({
+ ClassDeclaration: ["experimentalDecorators"],
+ ClassExpression: ["experimentalDecorators"]
+})
+
+exports.parseForESLint = (code, options) => {
+ assert(code === "@foo class A {}");
+ assert(options.eslintVisitorKeys === true);
+ assert(options.eslintScopeManager === true);
+
+ return {
+ ast: { type: "Program", start: 0, end: 15, loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 15 } }, comments: [], tokens: [{ type: "Punctuator", value: "@", start: 0, end: 1, loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }, range: [0, 1] }, { type: "Identifier", value: "foo", start: 1, end: 4, loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 } }, range: [1, 4] }, { type: "Keyword", value: "class", start: 5, end: 10, loc: { start: { line: 1, column: 5 }, end: { line: 1, column: 10 } }, range: [5, 10] }, { type: "Identifier", value: "A", start: 11, end: 12, loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }, range: [11, 12] }, { type: "Punctuator", value: "{", start: 13, end: 14, loc: { start: { line: 1, column: 13 }, end: { line: 1, column: 14 } }, range: [13, 14] }, { type: "Punctuator", value: "}", start: 14, end: 15, loc: { start: { line: 1, column: 14 }, end: { line: 1, column: 15 } }, range: [14, 15] }], range: [5, 15], sourceType: "module", body: [{ type: "ClassDeclaration", start: 5, end: 15, loc: { start: { line: 1, column: 5 }, end: { line: 1, column: 15 } }, experimentalDecorators: [{ type: "Decorator", start: 0, end: 4, loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 4 } }, expression: { type: "Identifier", start: 1, end: 4, loc: { start: { line: 1, column: 1 }, end: { line: 1, column: 4 }, identifierName: "foo" }, name: "foo", range: [1, 4], _babelType: "Identifier" }, range: [0, 4], _babelType: "Decorator" }], id: { type: "Identifier", start: 11, end: 12, loc: { start: { line: 1, column: 11 }, end: { line: 1, column: 12 }, identifierName: "A" }, name: "A", range: [11, 12], _babelType: "Identifier" }, superClass: null, body: { type: "ClassBody", start: 13, end: 15, loc: { start: { line: 1, column: 13 }, end: { line: 1, column: 15 } }, body: [], range: [13, 15], _babelType: "ClassBody" }, range: [5, 15], _babelType: "ClassDeclaration" }] },
+ visitorKeys: KEYS
+ };
+};
+
+exports.parse = function () {
+ throw new Error("Use parseForESLint() instead.");
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/fixes-one-problem.js b/packages/rule-tester/tests/eslint-base/fixtures/fixes-one-problem.js
new file mode 100644
index 000000000000..adde04196c95
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/fixes-one-problem.js
@@ -0,0 +1,25 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/fixes-one-problem.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ fixable: "code"
+ },
+ create(context) {
+ return {
+ Program(node) {
+ context.report({
+ node,
+ message: "No programs allowed."
+ });
+
+ context.report({
+ node,
+ message: "Seriously, no programs allowed.",
+ fix: fixer => fixer.remove(node)
+ });
+ }
+ }
+ }
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/messageId.js b/packages/rule-tester/tests/eslint-base/fixtures/messageId.js
new file mode 100644
index 000000000000..8f2bb2a246f3
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/messageId.js
@@ -0,0 +1,39 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/messageId.js
+
+"use strict";
+
+module.exports.withMetaWithData = {
+ meta: {
+ messages: {
+ avoidFoo: "Avoid using variables named '{{ name }}'.",
+ unused: "An unused key"
+ }
+ },
+ create(context) {
+ return {
+ Identifier(node) {
+ if (node.name === "foo") {
+ context.report({
+ node,
+ messageId: "avoidFoo",
+ data: {
+ name: "foo"
+ }
+ });
+ }
+ }
+ };
+ }
+};
+
+module.exports.withMessageOnly = {
+ create(context) {
+ return {
+ Identifier(node) {
+ if (node.name === "foo") {
+ context.report({ node, message: "Avoid using variables named 'foo'."});
+ }
+ }
+ };
+ }
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/modify-ast-at-first.js b/packages/rule-tester/tests/eslint-base/fixtures/modify-ast-at-first.js
new file mode 100644
index 000000000000..53ddcd28847e
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/modify-ast-at-first.js
@@ -0,0 +1,37 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/modify-ast-at-first.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: []
+ },
+ create(context) {
+ return {
+ "Program": function(node) {
+ node.body.push({
+ "type": "Identifier",
+ "name": "modified",
+ "range": [0, 8],
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 1,
+ "column": 8
+ }
+ }
+ });
+ },
+
+ "Identifier": function(node) {
+ if (node.name === "bar") {
+ context.report({message: "error", node: node});
+ }
+ }
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/modify-ast-at-last.js b/packages/rule-tester/tests/eslint-base/fixtures/modify-ast-at-last.js
new file mode 100644
index 000000000000..9fda56d02609
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/modify-ast-at-last.js
@@ -0,0 +1,37 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/modify-ast-at-last.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: []
+ },
+ create(context) {
+ return {
+ "Program:exit": function(node) {
+ node.body.push({
+ "type": "Identifier",
+ "name": "modified",
+ "range": [0, 8],
+ "loc": {
+ "start": {
+ "line": 1,
+ "column": 0
+ },
+ "end": {
+ "line": 1,
+ "column": 8
+ }
+ }
+ });
+ },
+
+ "Identifier": function(node) {
+ if (node.name === "bar") {
+ context.report({message: "error", node: node});
+ }
+ }
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/modify-ast.js b/packages/rule-tester/tests/eslint-base/fixtures/modify-ast.js
new file mode 100644
index 000000000000..3b8a879920aa
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/modify-ast.js
@@ -0,0 +1,21 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/modify-ast.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: []
+ },
+ create(context) {
+ return {
+ "Identifier": function(node) {
+ node.name += "!";
+
+ if (node.name === "bar!") {
+ context.report({message: "error", node: node});
+ }
+ }
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-eval.js b/packages/rule-tester/tests/eslint-base/fixtures/no-eval.js
new file mode 100644
index 000000000000..a7cba23f6128
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-eval.js
@@ -0,0 +1,19 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-eval.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: [],
+ },
+ create(context) {
+ return {
+ CallExpression: function (node) {
+ if (node.callee.name === "eval") {
+ context.report(node, "eval sucks.");
+ }
+ },
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-invalid-args.js b/packages/rule-tester/tests/eslint-base/fixtures/no-invalid-args.js
new file mode 100644
index 000000000000..7f9a1683aa07
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-invalid-args.js
@@ -0,0 +1,23 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-invalid-args.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: [{
+ type: "boolean"
+ }]
+ },
+ create(context) {
+ var config = context.options[0];
+
+ return {
+ "Program": function(node) {
+ if (config === true) {
+ context.report(node, "Invalid args");
+ }
+ }
+ };
+ }
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-invalid-schema.js b/packages/rule-tester/tests/eslint-base/fixtures/no-invalid-schema.js
new file mode 100644
index 000000000000..fd691f11a961
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-invalid-schema.js
@@ -0,0 +1,21 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-invalid-schema.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: [{
+ "enum": []
+ }]
+ },
+ create(context) {
+ return {
+ "Program": function(node) {
+ if (config) {
+ context.report(node, "Expected nothing.");
+ }
+ }
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-schema-violation.js b/packages/rule-tester/tests/eslint-base/fixtures/no-schema-violation.js
new file mode 100644
index 000000000000..1e12913c2287
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-schema-violation.js
@@ -0,0 +1,22 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-schema-violation.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: [{
+ "enum": ["foo"]
+ }]
+ },
+ create(context) {
+ const config = context.options[0];
+ return {
+ "Program": function(node) {
+ if (config && config !== "foo") {
+ context.report(node, "Expected foo.");
+ }
+ }
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-test-filename b/packages/rule-tester/tests/eslint-base/fixtures/no-test-filename
new file mode 100644
index 000000000000..795bd0ac7af3
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-test-filename
@@ -0,0 +1,19 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-test-filename
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: []
+ },
+ create(context) {
+ return {
+ "Program": function(node) {
+ if (context.getFilename() === '') {
+ context.report(node, "Filename test was not defined.");
+ }
+ }
+ };
+ }
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-test-global.js b/packages/rule-tester/tests/eslint-base/fixtures/no-test-global.js
new file mode 100644
index 000000000000..94834189376a
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-test-global.js
@@ -0,0 +1,26 @@
+// Forked from https://github.com/eslint/eslint/tree/1665c029acb92bf8812267f1647ad1a7054cbcb4/tests/fixtures/testers/rule-tester/no-test-global.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: [],
+ },
+ create(context) {
+ return {
+ "Program": function(node) {
+ var globals = context.getScope().variables.map(function (variable) {
+ return variable.name;
+ });
+
+ if (globals.indexOf("test") === -1) {
+ context.report(node, "Global variable test was not defined.");
+ }
+ if (globals.indexOf("foo") !== -1) {
+ context.report(node, "Global variable foo should not be used.");
+ }
+ }
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-test-settings.js b/packages/rule-tester/tests/eslint-base/fixtures/no-test-settings.js
new file mode 100644
index 000000000000..291b81b105f5
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-test-settings.js
@@ -0,0 +1,22 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-test-settings.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ type: "problem",
+ schema: [],
+ },
+ create(context) {
+ return {
+ Program: function (node) {
+ if (!context.settings || !context.settings.test) {
+ context.report(
+ node,
+ "Global settings test was not defined."
+ );
+ }
+ },
+ };
+ },
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/no-var.js b/packages/rule-tester/tests/eslint-base/fixtures/no-var.js
new file mode 100644
index 000000000000..26f0382536d9
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/no-var.js
@@ -0,0 +1,28 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/no-var.js
+
+"use strict";
+
+module.exports = {
+ meta: {
+ fixable: "code",
+ schema: []
+ },
+ create(context) {
+ var sourceCode = context.getSourceCode();
+
+ return {
+ "VariableDeclaration": function(node) {
+ if (node.kind === "var") {
+ context.report({
+ node: node,
+ loc: sourceCode.getFirstToken(node).loc,
+ message: "Bad var.",
+ fix: function(fixer) {
+ return fixer.remove(sourceCode.getFirstToken(node));
+ }
+ })
+ }
+ }
+ };
+ }
+};
diff --git a/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js
new file mode 100644
index 000000000000..4638ac2cacbf
--- /dev/null
+++ b/packages/rule-tester/tests/eslint-base/fixtures/suggestions.js
@@ -0,0 +1,76 @@
+// Forked from https://github.com/eslint/eslint/tree/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/tests/fixtures/testers/rule-tester/suggestions.js
+
+"use strict";
+
+module.exports.basic = {
+ meta: { hasSuggestions: true },
+ create(context) {
+ return {
+ Identifier(node) {
+ if (node.name === "foo") {
+ context.report({
+ node,
+ message: "Avoid using identifiers named 'foo'.",
+ suggest: [{
+ desc: "Rename identifier 'foo' to 'bar'",
+ fix: fixer => fixer.replaceText(node, 'bar')
+ }]
+ });
+ }
+ }
+ };
+ }
+};
+
+module.exports.withMessageIds = {
+ meta: {
+ messages: {
+ avoidFoo: "Avoid using identifiers named '{{ name }}'.",
+ unused: "An unused key",
+ renameFoo: "Rename identifier 'foo' to '{{ newName }}'"
+ },
+ hasSuggestions: true
+ },
+ create(context) {
+ return {
+ Identifier(node) {
+ if (node.name === "foo") {
+ context.report({
+ node,
+ messageId: "avoidFoo",
+ data: {
+ name: "foo"
+ },
+ suggest: [{
+ messageId: "renameFoo",
+ data: {
+ newName: "bar"
+ },
+ fix: fixer => fixer.replaceText(node, "bar")
+ }, {
+ messageId: "renameFoo",
+ data: {
+ newName: "baz"
+ },
+ fix: fixer => fixer.replaceText(node, "baz")
+ }]
+ });
+ }
+ }
+ };
+ }
+};
+
+module.exports.withoutHasSuggestionsProperty = {
+ create(context) {
+ return {
+ Identifier(node) {
+ context.report({
+ node,
+ message: "some message",
+ suggest: [{ desc: "some suggestion", fix: fixer => fixer.replaceText(node, 'bar') }]
+ });
+ }
+ };
+ }
+};
diff --git a/packages/rule-tester/tsconfig.build.json b/packages/rule-tester/tsconfig.build.json
new file mode 100644
index 000000000000..782f14402ae4
--- /dev/null
+++ b/packages/rule-tester/tsconfig.build.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "resolveJsonModule": true
+ },
+ "include": ["src", "typings"],
+ "references": [{ "path": "../utils/tsconfig.build.json" }]
+}
diff --git a/packages/rule-tester/tsconfig.json b/packages/rule-tester/tsconfig.json
new file mode 100644
index 000000000000..9cea515ba6b2
--- /dev/null
+++ b/packages/rule-tester/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.build.json",
+ "compilerOptions": {
+ "composite": false,
+ "rootDir": "."
+ },
+ "include": ["src", "typings", "tests", "tools"]
+}
diff --git a/packages/rule-tester/typings/eslint.d.ts b/packages/rule-tester/typings/eslint.d.ts
new file mode 100644
index 000000000000..6341a84533a8
--- /dev/null
+++ b/packages/rule-tester/typings/eslint.d.ts
@@ -0,0 +1,24 @@
+declare module 'eslint/use-at-your-own-risk' {
+ import type { AnyRuleModule } from '@typescript-eslint/utils/ts-eslint';
+
+ export const builtinRules: ReadonlyMap;
+}
+
+declare module '@eslint/eslintrc' {
+ import type { Linter } from '@typescript-eslint/utils/ts-eslint';
+
+ export const Legacy: {
+ ConfigOps: {
+ normalizeConfigGlobal: (
+ configuredValue: boolean | string | null,
+ ) => Linter.GlobalVariableOptionBase;
+ // ...
+ };
+ environments: Map;
+ // ...
+ };
+}
+
+declare module 'eslint' {
+ export { SourceCode } from '@typescript-eslint/utils/ts-eslint';
+}
diff --git a/packages/scope-manager/README.md b/packages/scope-manager/README.md
index 0258932e390a..233997bcbb6f 100644
--- a/packages/scope-manager/README.md
+++ b/packages/scope-manager/README.md
@@ -6,3 +6,5 @@
👉 See **https://typescript-eslint.io/architecture/scope-manager** for documentation on this package.
> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
+
+
diff --git a/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts b/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts
index 82611eb5147e..5a19f01debf0 100644
--- a/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts
+++ b/packages/scope-manager/tests/eslint-scope/get-declared-variables.test.ts
@@ -16,18 +16,20 @@ describe('ScopeManager.prototype.getDeclaredVariables', () => {
});
simpleTraverse(ast, {
- [type](node) {
- const expected = expectedNamesList.shift()!;
- const actual = scopeManager.getDeclaredVariables(node);
-
- expect(actual).toHaveLength(expected.length);
- if (actual.length > 0) {
- const end = actual.length - 1;
-
- for (let i = 0; i <= end; i++) {
- expect(actual[i].name).toBe(expected[i]);
+ visitors: {
+ [type](node) {
+ const expected = expectedNamesList.shift()!;
+ const actual = scopeManager.getDeclaredVariables(node);
+
+ expect(actual).toHaveLength(expected.length);
+ if (actual.length > 0) {
+ const end = actual.length - 1;
+
+ for (let i = 0; i <= end; i++) {
+ expect(actual[i].name).toBe(expected[i]);
+ }
}
- }
+ },
},
});
diff --git a/packages/scope-manager/tests/util/getSpecificNode.ts b/packages/scope-manager/tests/util/getSpecificNode.ts
index a6d356156125..e9d23cf90c97 100644
--- a/packages/scope-manager/tests/util/getSpecificNode.ts
+++ b/packages/scope-manager/tests/util/getSpecificNode.ts
@@ -32,13 +32,15 @@ function getSpecificNode(
simpleTraverse(
ast,
{
- [selector](n) {
- const res = cb ? cb(n) : n;
- if (res) {
- // the callback shouldn't match multiple nodes or else tests may behave weirdly
- expect(node).toBeFalsy();
- node = typeof res === 'boolean' ? n : res;
- }
+ visitors: {
+ [selector](n) {
+ const res = cb ? cb(n) : n;
+ if (res) {
+ // the callback shouldn't match multiple nodes or else tests may behave weirdly
+ expect(node).toBeFalsy();
+ node = typeof res === 'boolean' ? n : res;
+ }
+ },
},
},
true,
diff --git a/packages/type-utils/README.md b/packages/type-utils/README.md
index 2f842e803cc5..09a28aea18c5 100644
--- a/packages/type-utils/README.md
+++ b/packages/type-utils/README.md
@@ -2,11 +2,13 @@
> Type utilities for working with TypeScript within ESLint rules.
+[](https://www.npmjs.com/package/@typescript-eslint/utils)
+[](https://www.npmjs.com/package/@typescript-eslint/utils)
+
The utilities in this package are separated from `@typescript-eslint/utils` so that that package does not require a dependency on `typescript`.
-## ✋ Internal Package
+👉 See **https://typescript-eslint.io/architecture/type-utils** for documentation on this package.
-This is an _internal package_ to the [typescript-eslint monorepo](https://github.com/typescript-eslint/typescript-eslint).
-You likely don't want to use it directly.
+> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
-👉 See **https://typescript-eslint.io** for docs on typescript-eslint.
+
diff --git a/packages/typescript-estree/README.md b/packages/typescript-estree/README.md
index 4ce7f1e7cf4e..9d7ec247ebfc 100644
--- a/packages/typescript-estree/README.md
+++ b/packages/typescript-estree/README.md
@@ -1,5 +1,7 @@
# `@typescript-eslint/typescript-estree`
+> A parser that produces an ESTree-compatible AST for TypeScript code.
+
[](https://www.npmjs.com/package/@typescript-eslint/utils)
[](https://www.npmjs.com/package/@typescript-eslint/utils)
@@ -8,3 +10,5 @@
👉 See **https://typescript-eslint.io/architecture/typescript-estree** for documentation on this package.
> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
+
+
diff --git a/packages/typescript-estree/src/simple-traverse.ts b/packages/typescript-estree/src/simple-traverse.ts
index 2d51cdbe4fa1..67b56d02c380 100644
--- a/packages/typescript-estree/src/simple-traverse.ts
+++ b/packages/typescript-estree/src/simple-traverse.ts
@@ -1,3 +1,4 @@
+import type { VisitorKeys } from '@typescript-eslint/visitor-keys';
import { visitorKeys } from '@typescript-eslint/visitor-keys';
import type { TSESTree } from './ts-estree';
@@ -16,25 +17,33 @@ function getVisitorKeysForNode(
return (keys ?? []) as never;
}
-type SimpleTraverseOptions =
+type SimpleTraverseOptions = Readonly<
| {
+ visitorKeys?: Readonly;
enter: (node: TSESTree.Node, parent: TSESTree.Node | undefined) => void;
}
| {
- [key: string]: (
- node: TSESTree.Node,
- parent: TSESTree.Node | undefined,
- ) => void;
- };
+ visitorKeys?: Readonly;
+ visitors: {
+ [key: string]: (
+ node: TSESTree.Node,
+ parent: TSESTree.Node | undefined,
+ ) => void;
+ };
+ }
+>;
class SimpleTraverser {
- private readonly allVisitorKeys = visitorKeys;
+ private readonly allVisitorKeys: Readonly = visitorKeys;
private readonly selectors: SimpleTraverseOptions;
private readonly setParentPointers: boolean;
constructor(selectors: SimpleTraverseOptions, setParentPointers = false) {
this.selectors = selectors;
this.setParentPointers = setParentPointers;
+ if (selectors.visitorKeys) {
+ this.allVisitorKeys = selectors.visitorKeys;
+ }
}
traverse(node: unknown, parent: TSESTree.Node | undefined): void {
@@ -48,8 +57,8 @@ class SimpleTraverser {
if ('enter' in this.selectors) {
this.selectors.enter(node, parent);
- } else if (node.type in this.selectors) {
- this.selectors[node.type](node, parent);
+ } else if (node.type in this.selectors.visitors) {
+ this.selectors.visitors[node.type](node, parent);
}
const keys = getVisitorKeysForNode(this.allVisitorKeys, node);
diff --git a/packages/utils/README.md b/packages/utils/README.md
index 8013675d9625..171393b51ca9 100644
--- a/packages/utils/README.md
+++ b/packages/utils/README.md
@@ -8,3 +8,5 @@
👉 See **https://typescript-eslint.io/architecture/utils** for documentation on this package.
> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code.
+
+
diff --git a/packages/utils/package.json b/packages/utils/package.json
index a852cc6e0261..ed063710ad90 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -67,7 +67,6 @@
"dependencies": {
"@eslint-community/eslint-utils": "^4.3.0",
"@types/json-schema": "^7.0.9",
- "@types/semver": "^7.3.12",
"@typescript-eslint/scope-manager": "5.59.1",
"@typescript-eslint/types": "5.59.1",
"@typescript-eslint/typescript-estree": "5.59.1",
diff --git a/packages/utils/src/json-schema.ts b/packages/utils/src/json-schema.ts
index 8e11b8b3caa2..c6b1ccacd717 100644
--- a/packages/utils/src/json-schema.ts
+++ b/packages/utils/src/json-schema.ts
@@ -1,24 +1,2 @@
-// Note - @types/json-schema@7.0.4 added some function declarations to the type package
-// If we do export *, then it will also export these function declarations.
-// This will cause typescript to not scrub the require from the build, breaking anyone who doesn't have it as a dependency
-
// eslint-disable-next-line import/no-extraneous-dependencies
-export {
- JSONSchema4,
- JSONSchema4Type,
- JSONSchema4TypeName,
- JSONSchema4Version,
- JSONSchema6,
- JSONSchema6Definition,
- JSONSchema6Type,
- JSONSchema6TypeName,
- JSONSchema6Version,
- JSONSchema7,
- JSONSchema7Array,
- JSONSchema7Definition,
- JSONSchema7Type,
- JSONSchema7TypeName,
- JSONSchema7Version,
- ValidationError,
- ValidationResult,
-} from 'json-schema';
+export type * from 'json-schema';
diff --git a/packages/utils/src/ts-eslint/Linter.ts b/packages/utils/src/ts-eslint/Linter.ts
index 20ec02893a50..171335c5dd8c 100644
--- a/packages/utils/src/ts-eslint/Linter.ts
+++ b/packages/utils/src/ts-eslint/Linter.ts
@@ -13,6 +13,12 @@ import type {
import type { Scope } from './Scope';
import type { SourceCode } from './SourceCode';
+export type MinimalRuleModule<
+ TMessageIds extends string = string,
+ TOptions extends readonly unknown[] = [],
+> = Pick, 'create'> &
+ Partial, 'create'>>;
+
declare class LinterBase {
/**
* Initialize the Linter.
@@ -34,7 +40,7 @@ declare class LinterBase {
*/
defineRule(
ruleId: string,
- ruleModule: RuleModule | RuleCreateFunction,
+ ruleModule: MinimalRuleModule | RuleCreateFunction,
): void;
/**
@@ -44,7 +50,8 @@ declare class LinterBase {
defineRules(
rulesToDefine: Record<
string,
- RuleModule | RuleCreateFunction
+ | MinimalRuleModule
+ | RuleCreateFunction
>,
): void;
@@ -52,7 +59,7 @@ declare class LinterBase {
* Gets an object with all loaded rules.
* @returns All loaded rules
*/
- getRules(): Map>;
+ getRules(): Map>;
/**
* Gets the `SourceCode` object representing the parsed source.
@@ -120,7 +127,15 @@ namespace Linter {
export type RuleEntry = RuleLevel | RuleLevelAndOptions;
export type RulesRecord = Partial>;
- export type GlobalVariableOption = 'readonly' | 'writable' | 'off' | boolean;
+ export type GlobalVariableOptionBase = 'readonly' | 'writable' | 'off';
+ export type GlobalVariableOption = GlobalVariableOptionBase | boolean;
+
+ export interface GlobalsConfig {
+ [name: string]: GlobalVariableOption;
+ }
+ export interface EnvironmentConfig {
+ [name: string]: boolean;
+ }
// https://github.com/eslint/eslint/blob/v6.8.0/conf/config-schema.js
interface BaseConfig {
@@ -128,7 +143,7 @@ namespace Linter {
/**
* The environment settings.
*/
- env?: { [name: string]: boolean };
+ env?: EnvironmentConfig;
/**
* The path to other config files or the package name of shareable configs.
*/
@@ -136,7 +151,7 @@ namespace Linter {
/**
* The global variable settings.
*/
- globals?: { [name: string]: GlobalVariableOption };
+ globals?: GlobalsConfig;
/**
* The flag that disables directive comments.
*/
diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts
index b0705cc5c473..f798e6e8cce2 100644
--- a/packages/utils/src/ts-eslint/Rule.ts
+++ b/packages/utils/src/ts-eslint/Rule.ts
@@ -117,11 +117,13 @@ type ReportFixFunction = (
type ReportSuggestionArray =
SuggestionReportDescriptor[];
+type ReportDescriptorMessageData = Readonly>;
+
interface ReportDescriptorBase {
/**
* The parameters for the message string associated with `messageId`.
*/
- readonly data?: Readonly>;
+ readonly data?: ReportDescriptorMessageData;
/**
* The fixer function.
*/
@@ -264,8 +266,7 @@ type RuleFunction = (
node: T,
) => void;
-interface RuleListener {
- [nodeSelector: string]: RuleFunction | undefined;
+interface RuleListenerBaseSelectors {
ArrayExpression?: RuleFunction;
ArrayPattern?: RuleFunction;
ArrowFunctionExpression?: RuleFunction;
@@ -424,6 +425,18 @@ interface RuleListener {
WithStatement?: RuleFunction;
YieldExpression?: RuleFunction;
}
+type RuleListenerExitSelectors = {
+ [K in keyof RuleListenerBaseSelectors as `${K}:exit`]: RuleListenerBaseSelectors[K];
+};
+interface RuleListenerCatchAllBaseCase {
+ [nodeSelector: string]: RuleFunction | undefined;
+}
+// Interface to merge into for anyone that wants to add more selectors
+interface RuleListenerExtension {}
+
+type RuleListener = RuleListenerBaseSelectors &
+ RuleListenerExitSelectors &
+ RuleListenerCatchAllBaseCase;
interface RuleModule<
TMessageIds extends string,
@@ -447,14 +460,19 @@ interface RuleModule<
*/
create(context: Readonly>): TRuleListener;
}
+type AnyRuleModule = RuleModule;
type RuleCreateFunction<
TMessageIds extends string = never,
TOptions extends readonly unknown[] = unknown[],
> = (context: Readonly>) => RuleListener;
+type AnyRuleCreateFunction = RuleCreateFunction;
export {
+ AnyRuleCreateFunction,
+ AnyRuleModule,
ReportDescriptor,
+ ReportDescriptorMessageData,
ReportFixFunction,
ReportSuggestionArray,
RuleContext,
@@ -463,6 +481,7 @@ export {
RuleFixer,
RuleFunction,
RuleListener,
+ RuleListenerExtension,
RuleMetaData,
RuleMetaDataDocs,
RuleModule,
diff --git a/packages/utils/src/ts-eslint/RuleTester.ts b/packages/utils/src/ts-eslint/RuleTester.ts
index 6c0b98b795f2..51f7840fc363 100644
--- a/packages/utils/src/ts-eslint/RuleTester.ts
+++ b/packages/utils/src/ts-eslint/RuleTester.ts
@@ -4,6 +4,7 @@ import type { AST_NODE_TYPES, AST_TOKEN_TYPES } from '../ts-estree';
import type { Linter } from './Linter';
import type { ParserOptions } from './ParserOptions';
import type {
+ ReportDescriptorMessageData,
RuleCreateFunction,
RuleModule,
SharedConfigurationSettings,
@@ -62,7 +63,7 @@ interface SuggestionOutput {
/**
* The data used to fill the message template.
*/
- readonly data?: Readonly>;
+ readonly data?: ReportDescriptorMessageData;
/**
* NOTE: Suggestions will be applied as a stand-alone change, without triggering multi-pass fixes.
* Each individual error has its own suggestion, so you have to show the correct, _isolated_ output for each suggestion.
@@ -95,7 +96,7 @@ interface TestCaseError {
/**
* The data used to fill the message template.
*/
- readonly data?: Readonly>;
+ readonly data?: ReportDescriptorMessageData;
/**
* The 1-based column number of the reported end location.
*/
diff --git a/packages/website/package.json b/packages/website/package.json
index abe0c02eb8d4..01ea34e9cd5c 100644
--- a/packages/website/package.json
+++ b/packages/website/package.json
@@ -59,6 +59,7 @@
"globby": "^11.1.0",
"make-dir": "*",
"monaco-editor": "^0.37.0",
+ "raw-loader": "^4.0.2",
"rimraf": "*",
"stylelint": "^15.3.0",
"stylelint-config-recommended": "^11.0.0",
diff --git a/packages/website/sidebars/sidebar.base.js b/packages/website/sidebars/sidebar.base.js
index 2a097b02a703..c1485c6007a8 100644
--- a/packages/website/sidebars/sidebar.base.js
+++ b/packages/website/sidebars/sidebar.base.js
@@ -42,6 +42,7 @@ module.exports = {
'architecture/eslint-plugin',
'architecture/eslint-plugin-tslint',
'architecture/parser',
+ 'architecture/rule-tester',
'architecture/scope-manager',
'architecture/typescript-estree',
'architecture/utils',
diff --git a/patches/ajv+6.12.6.patch b/patches/ajv+6.12.6.patch
new file mode 100644
index 000000000000..43b89b3f40a1
--- /dev/null
+++ b/patches/ajv+6.12.6.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/ajv/lib/ajv.d.ts b/node_modules/ajv/lib/ajv.d.ts
+index 078364d..21cf7bf 100644
+--- a/node_modules/ajv/lib/ajv.d.ts
++++ b/node_modules/ajv/lib/ajv.d.ts
+@@ -153,7 +153,7 @@ declare namespace ajv {
+ parentData?: object | Array,
+ parentDataProperty?: string | number,
+ rootData?: object | Array
+- ): boolean | PromiseLike;
++ ): boolean;
+ schema?: object | boolean;
+ errors?: null | Array;
+ refs?: object;
diff --git a/yarn.lock b/yarn.lock
index 8b3c889b137f..667000b4fcba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2202,6 +2202,11 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
+"@eslint/js@8.38.0":
+ version "8.38.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.38.0.tgz#73a8a0d8aa8a8e6fe270431c5e72ae91b5337892"
+ integrity sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==
+
"@eslint/js@8.39.0":
version "8.39.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b"
@@ -3325,6 +3330,13 @@
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
+"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3":
+ version "1.8.6"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9"
+ integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==
+ dependencies:
+ type-detect "4.0.8"
+
"@sinonjs/commons@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
@@ -3339,6 +3351,27 @@
dependencies:
"@sinonjs/commons" "^2.0.0"
+"@sinonjs/fake-timers@^7.1.2":
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
+ integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+
+"@sinonjs/samsam@^6.0.2":
+ version "6.1.3"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.3.tgz#4e30bcd4700336363302a7d72cbec9b9ab87b104"
+ integrity sha512-nhOb2dWPeb1sd3IQXL/dVPnKHDOAFfvichtBf4xV00/rU1QbPCQqKMbvIheIjqwVjh7qIgf2AHTHi391yMOMpQ==
+ dependencies:
+ "@sinonjs/commons" "^1.6.0"
+ lodash.get "^4.4.2"
+ type-detect "^4.0.8"
+
+"@sinonjs/text-encoding@^0.7.1":
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918"
+ integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==
+
"@slorber/static-site-generator-webpack-plugin@^4.0.7":
version "4.0.7"
resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz#fc1678bddefab014e2145cbe25b3ce4e1cfc36f3"
@@ -3823,6 +3856,18 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/lodash.merge@4.6.7":
+ version "4.6.7"
+ resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.7.tgz#0af6555dd8bc6568ef73e5e0d820a027362946b1"
+ integrity sha512-OwxUJ9E50gw3LnAefSHJPHaBLGEKmQBQ7CZe/xflHkyy/wH2zVyEIAKReHvVrrn7zKdF58p16We9kMfh7v0RRQ==
+ dependencies:
+ "@types/lodash" "*"
+
+"@types/lodash@*":
+ version "4.14.192"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285"
+ integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==
+
"@types/marked@*", "@types/marked@^4.0.3":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.8.tgz#b316887ab3499d0a8f4c70b7bd8508f92d477955"
@@ -3986,7 +4031,7 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
-"@types/semver@*", "@types/semver@^7.3.12", "@types/semver@^7.3.9":
+"@types/semver@*", "@types/semver@^7.3.9":
version "7.3.13"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
@@ -4059,6 +4104,11 @@
dependencies:
"@types/yargs-parser" "*"
+"@ungap/promise-all-settled@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
+ integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
+
"@webassemblyjs/ast@1.11.5", "@webassemblyjs/ast@^1.11.5":
version "1.11.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.5.tgz#6e818036b94548c1fb53b754b5cae3c9b208281c"
@@ -4370,7 +4420,7 @@ ansi-align@^3.0.0, ansi-align@^3.0.1:
dependencies:
string-width "^4.1.0"
-ansi-colors@^4.1.1:
+ansi-colors@4.1.1, ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
@@ -4387,6 +4437,11 @@ ansi-html-community@^0.0.8:
resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
+ansi-regex@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.1.tgz#123d6479e92ad45ad897d4054e3c7ca7db4944e1"
+ integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==
+
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@@ -4421,7 +4476,7 @@ ansi-styles@^6.0.0, ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
-anymatch@^3.0.3, anymatch@~3.1.2:
+anymatch@^3.0.3, anymatch@~3.1.1, anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
@@ -4574,6 +4629,11 @@ asap@~2.0.3:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+assertion-error@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+ integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+
ast-types-flow@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
@@ -4929,6 +4989,11 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
+browser-stdout@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+ integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.3, browserslist@^4.21.3, browserslist@^4.21.4:
version "4.21.5"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7"
@@ -5100,7 +5165,7 @@ camelcase@^5.3.1:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-camelcase@^6.2.0:
+camelcase@^6.0.0, camelcase@^6.2.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
@@ -5125,6 +5190,19 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==
+chai@^4.0.1:
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"
+ integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==
+ dependencies:
+ assertion-error "^1.1.0"
+ check-error "^1.0.2"
+ deep-eql "^4.1.2"
+ get-func-name "^2.0.0"
+ loupe "^2.3.1"
+ pathval "^1.1.1"
+ type-detect "^4.0.5"
+
chalk@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
@@ -5180,6 +5258,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
+check-error@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
+ integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
+
cheerio-select@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
@@ -5205,6 +5288,21 @@ cheerio@^1.0.0-rc.12:
parse5 "^7.0.0"
parse5-htmlparser2-tree-adapter "^7.0.0"
+chokidar@3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
+ integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
+ dependencies:
+ anymatch "~3.1.1"
+ braces "~3.0.2"
+ glob-parent "~5.1.0"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.5.0"
+ optionalDependencies:
+ fsevents "~2.3.1"
+
chokidar@^3.4.2, chokidar@^3.5.1, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@@ -6154,6 +6252,13 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
dependencies:
ms "2.1.2"
+debug@4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+ integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+ dependencies:
+ ms "2.1.2"
+
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@@ -6174,6 +6279,11 @@ decamelize@^1.1.0, decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+decamelize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+ integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
@@ -6186,6 +6296,13 @@ dedent@0.7.0, dedent@^0.7.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==
+deep-eql@^4.1.2:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d"
+ integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==
+ dependencies:
+ type-detect "^4.0.0"
+
deep-equal@^2.0.5:
version "2.2.0"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6"
@@ -6343,11 +6460,21 @@ diff-sequences@^29.4.3:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
+diff@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+ integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
+diff@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
+ integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
+
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -6740,6 +6867,11 @@ escape-html@^1.0.3, escape-html@~1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -6750,11 +6882,6 @@ escape-string-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
-escape-string-regexp@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
- integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
eslint-import-resolver-node@^0.3.7:
version "0.3.7"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz#83b375187d412324a1963d84fa664377a23eb4d7"
@@ -6907,7 +7034,7 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
-eslint-scope@^7.2.0:
+eslint-scope@^7.1.1, eslint-scope@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
@@ -7583,7 +7710,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
-fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
+fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -7651,6 +7778,11 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+get-func-name@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
+ integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==
+
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
@@ -7786,7 +7918,7 @@ github-slugger@^1.4.0:
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==
-glob-parent@5.1.2, glob-parent@^5.1.2, glob-parent@~5.1.2:
+glob-parent@5.1.2, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -7827,6 +7959,18 @@ glob@7.1.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
+glob@7.1.6:
+ version "7.1.6"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
+ integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -7987,6 +8131,11 @@ gray-matter@^4.0.3:
section-matter "^1.0.0"
strip-bom-string "^1.0.0"
+growl@1.10.5:
+ version "1.10.5"
+ resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
+ integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
+
gzip-size@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
@@ -8150,7 +8299,7 @@ hastscript@^6.0.0:
property-information "^5.0.0"
space-separated-tokens "^1.0.0"
-he@^1.2.0:
+he@1.2.0, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -8717,6 +8866,11 @@ is-extglob@^2.1.1:
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+ integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==
+
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@@ -8819,7 +8973,7 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
-is-plain-obj@^2.0.0:
+is-plain-obj@^2.0.0, is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
@@ -9466,6 +9620,13 @@ js-sdsl@^4.1.4:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+js-yaml@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
+ integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
+ dependencies:
+ argparse "^2.0.1"
+
js-yaml@4.1.0, js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
@@ -9607,6 +9768,11 @@ just-diff@^6.0.0:
resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285"
integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==
+just-extend@^4.0.2:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744"
+ integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==
+
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@@ -9948,7 +10114,7 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-lodash.merge@^4.6.2:
+lodash.merge@4.6.2, lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
@@ -9973,6 +10139,13 @@ lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@~4.17
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+log-symbols@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
+ integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
+ dependencies:
+ chalk "^4.0.0"
+
log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
@@ -9998,6 +10171,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
+loupe@^2.3.1:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53"
+ integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==
+ dependencies:
+ get-func-name "^2.0.0"
+
lower-case@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@@ -10538,6 +10718,37 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+mocha@^8.3.2:
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff"
+ integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==
+ dependencies:
+ "@ungap/promise-all-settled" "1.1.2"
+ ansi-colors "4.1.1"
+ browser-stdout "1.3.1"
+ chokidar "3.5.1"
+ debug "4.3.1"
+ diff "5.0.0"
+ escape-string-regexp "4.0.0"
+ find-up "5.0.0"
+ glob "7.1.6"
+ growl "1.10.5"
+ he "1.2.0"
+ js-yaml "4.0.0"
+ log-symbols "4.0.0"
+ minimatch "3.0.4"
+ ms "2.1.3"
+ nanoid "3.1.20"
+ serialize-javascript "5.0.1"
+ strip-json-comments "3.1.1"
+ supports-color "8.1.1"
+ which "2.0.2"
+ wide-align "1.1.3"
+ workerpool "6.1.0"
+ yargs "16.2.0"
+ yargs-parser "20.2.4"
+ yargs-unparser "2.0.0"
+
modify-values@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
@@ -10587,6 +10798,11 @@ mute-stream@0.0.8, mute-stream@~0.0.4:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
+nanoid@3.1.20:
+ version "3.1.20"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
+ integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
+
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
@@ -10617,6 +10833,17 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+nise@^5.1.0:
+ version "5.1.4"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0"
+ integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==
+ dependencies:
+ "@sinonjs/commons" "^2.0.0"
+ "@sinonjs/fake-timers" "^10.0.2"
+ "@sinonjs/text-encoding" "^0.7.1"
+ just-extend "^4.0.2"
+ path-to-regexp "^1.7.0"
+
no-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
@@ -11549,6 +11776,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+pathval@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
+ integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
+
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -12170,6 +12402,14 @@ raw-body@2.5.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
+raw-loader@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
+ integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
+ dependencies:
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+
rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -12470,6 +12710,13 @@ readable-stream@^4.1.0:
events "^3.3.0"
process "^0.11.10"
+readdirp@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
+ integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
+ dependencies:
+ picomatch "^2.2.1"
+
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -13032,6 +13279,13 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
+serialize-javascript@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
+ integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+ dependencies:
+ randombytes "^2.1.0"
+
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@@ -13176,6 +13430,18 @@ sigstore@^1.0.0:
make-fetch-happen "^11.0.1"
tuf-js "^1.0.0"
+sinon@^11.0.0:
+ version "11.1.2"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-11.1.2.tgz#9e78850c747241d5c59d1614d8f9cbe8840e8674"
+ integrity sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==
+ dependencies:
+ "@sinonjs/commons" "^1.8.3"
+ "@sinonjs/fake-timers" "^7.1.2"
+ "@sinonjs/samsam" "^6.0.2"
+ diff "^5.0.0"
+ nise "^5.1.0"
+ supports-color "^7.2.0"
+
sirv@^1.0.7:
version "1.0.18"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.18.tgz#105fab52fb656ce8a2bebbf36b11052005952899"
@@ -13475,6 +13741,14 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
+"string-width@^1.0.2 || 2":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -13557,6 +13831,13 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==
+ dependencies:
+ ansi-regex "^3.0.0"
+
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -13608,7 +13889,7 @@ strip-indent@^3.0.0:
dependencies:
min-indent "^1.0.0"
-strip-json-comments@^3.1.0, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1:
+strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1, strip-json-comments@~3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -13715,6 +13996,13 @@ stylelint@^15.3.0:
v8-compile-cache "^2.3.0"
write-file-atomic "^5.0.0"
+supports-color@8.1.1, supports-color@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
@@ -13722,20 +14010,13 @@ supports-color@^5.3.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^7.0.0, supports-color@^7.1.0:
+supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
-supports-color@^8.0.0:
- version "8.1.1"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
- integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
- dependencies:
- has-flag "^4.0.0"
-
supports-hyperlinks@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b"
@@ -14148,7 +14429,7 @@ type-check@^0.4.0, type-check@~0.4.0:
dependencies:
prelude-ls "^1.2.1"
-type-detect@4.0.8:
+type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
@@ -14864,6 +15145,13 @@ which-typed-array@^1.1.9:
has-tostringtag "^1.0.0"
is-typed-array "^1.1.10"
+which@2.0.2, which@^2.0.1, which@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@@ -14871,13 +15159,6 @@ which@^1.2.9, which@^1.3.1:
dependencies:
isexe "^2.0.0"
-which@^2.0.1, which@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
which@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/which/-/which-3.0.0.tgz#a9efd016db59728758a390d23f1687b6e8f59f8e"
@@ -14885,6 +15166,13 @@ which@^3.0.0:
dependencies:
isexe "^2.0.0"
+wide-align@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+ dependencies:
+ string-width "^1.0.2 || 2"
+
wide-align@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
@@ -15079,6 +15367,11 @@ workbox-window@6.5.4, workbox-window@^6.5.3:
"@types/trusted-types" "^2.0.2"
workbox-core "6.5.4"
+workerpool@6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
+ integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
+
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -15242,6 +15535,16 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.3:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+yargs-unparser@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+ integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+ dependencies:
+ camelcase "^6.0.0"
+ decamelize "^4.0.0"
+ flat "^5.0.2"
+ is-plain-obj "^2.1.0"
+
yargs@16.2.0, yargs@^16.2.0:
version "16.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"