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 + +[![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/rule-tester.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/rule-tester) +[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/rule-tester.svg?style=flat-square)](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. +[![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils) +[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/utils.svg?style=flat-square)](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. + [![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/typescript-estree.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils) [![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/typescript-estree.svg?style=flat-square)](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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy