diff --git a/eslint.config.mjs b/eslint.config.mjs index 51d591a29d2b..aeb867a15f22 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -574,6 +574,7 @@ export default tseslint.config( 'packages/ast-spec/{src,tests,typings}/**/*.ts', 'packages/integration-tests/{tests,tools,typing}/**/*.ts', 'packages/parser/{src,tests}/**/*.ts', + 'packages/rule-tester/{src,tests,typings}/**/*.ts', 'packages/utils/src/**/*.ts', 'packages/visitor-keys/src/**/*.ts', 'packages/website*/src/**/*.ts', diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index ab429606bc8b..6a1bd8fb0c42 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -1,14 +1,8 @@ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ // 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 * as parser from '@typescript-eslint/parser'; import type { TSESTree } from '@typescript-eslint/utils'; -import { deepMerge } from '@typescript-eslint/utils/eslint-utils'; import type { AnyRuleCreateFunction, AnyRuleModule, @@ -16,14 +10,19 @@ import type { RuleListener, RuleModule, } from '@typescript-eslint/utils/ts-eslint'; + +import * as parser from '@typescript-eslint/parser'; +import { deepMerge } from '@typescript-eslint/utils/eslint-utils'; import { Linter } from '@typescript-eslint/utils/ts-eslint'; +import assert from 'node:assert'; +import path from 'node:path'; +import util from 'node:util'; // 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 stringify from 'json-stable-stringify-without-jsonify'; import merge from 'lodash.merge'; -import { TestFramework } from './TestFramework'; import type { InvalidTestCase, NormalizedRunTests, @@ -33,6 +32,8 @@ import type { TesterConfigWithDefaults, ValidTestCase, } from './types'; + +import { TestFramework } from './TestFramework'; import { ajvBuilder } from './utils/ajv'; import { cloneDeeplyExcludesParent } from './utils/cloneDeeplyExcludesParent'; import { validate } from './utils/config-validator'; @@ -165,9 +166,9 @@ function getUnsubstitutedMessagePlaceholders( } export class RuleTester extends TestFramework { - readonly #testerConfig: TesterConfigWithDefaults; - readonly #rules: Record = {}; readonly #linter: Linter; + readonly #rules: Record = {}; + readonly #testerConfig: TesterConfigWithDefaults; /** * Creates a new instance of RuleTester. @@ -260,20 +261,6 @@ export class RuleTester extends TestFramework { /** * Define a rule for one particular run of tests. */ - defineRule(name: string, rule: AnyRuleModule): void { - this.#rules[name] = { - ...rule, - // Create a wrapper rule that freezes the `context` properties. - create(context): RuleListener { - freezeDeeply(context.options); - freezeDeeply(context.settings); - freezeDeeply(context.parserOptions); - - return (typeof rule === 'function' ? rule : rule.create)(context); - }, - }; - } - #normalizeTests< MessageIds extends string, Options extends readonly unknown[], @@ -335,6 +322,7 @@ export class RuleTester extends TestFramework { }; const normalizedTests = { + invalid: rawTests.invalid.map(normalizeTest), valid: rawTests.valid .map(test => { if (typeof test === 'string') { @@ -343,7 +331,6 @@ export class RuleTester extends TestFramework { return test; }) .map(normalizeTest), - invalid: rawTests.invalid.map(normalizeTest), }; // convenience iterator to make it easy to loop all tests without a concat @@ -409,6 +396,20 @@ export class RuleTester extends TestFramework { return normalizedTests; } + defineRule(name: string, rule: AnyRuleModule): void { + this.#rules[name] = { + ...rule, + // Create a wrapper rule that freezes the `context` properties. + create(context): RuleListener { + freezeDeeply(context.options); + freezeDeeply(context.settings); + freezeDeeply(context.parserOptions); + + return (typeof rule === 'function' ? rule : rule.create)(context); + }, + }; + } + /** * Adds a new rule test to execute. */ @@ -543,12 +544,12 @@ export class RuleTester extends TestFramework { rule: RuleModule, item: InvalidTestCase | ValidTestCase, ): { - messages: Linter.LintMessage[]; - outputs: string[]; - beforeAST: TSESTree.Program; afterAST: TSESTree.Program; + beforeAST: TSESTree.Program; config: RuleTesterConfig; filename?: string; + messages: Linter.LintMessage[]; + outputs: string[]; } { this.defineRule(ruleName, rule); @@ -692,7 +693,7 @@ export class RuleTester extends TestFramework { do { passNumber++; - const { applyLanguageOptions, applyInlineConfig, finalize } = + const { applyInlineConfig, applyLanguageOptions, finalize } = SourceCode.prototype; try { @@ -704,7 +705,6 @@ export class RuleTester extends TestFramework { }); const actualConfig = merge(configWithoutCustomKeys, { - linterOptions: { reportUnusedDisableDirectives: 1 }, languageOptions: { ...configWithoutCustomKeys.languageOptions, parserOptions: { @@ -713,6 +713,7 @@ export class RuleTester extends TestFramework { ...configWithoutCustomKeys.languageOptions?.parserOptions, }, }, + linterOptions: { reportUnusedDisableDirectives: 1 }, }); messages = this.#linter.verify(code, actualConfig, filename); } finally { diff --git a/packages/rule-tester/src/TestFramework.ts b/packages/rule-tester/src/TestFramework.ts index f7ed21b51e17..ada6b963c570 100644 --- a/packages/rule-tester/src/TestFramework.ts +++ b/packages/rule-tester/src/TestFramework.ts @@ -6,24 +6,22 @@ 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; - }; +export type RuleTesterTestFrameworkFunction = { + /** + * Skips running the tests inside this `describe` for the current file + */ + skip?: RuleTesterTestFrameworkFunctionBase; +} & RuleTesterTestFrameworkFunctionBase; +export type RuleTesterTestFrameworkItFunction = { + /** + * Only runs this test in the current file. + */ + only?: RuleTesterTestFrameworkFunctionBase; + /** + * Skips running this test in the current file. + */ + skip?: RuleTesterTestFrameworkFunctionBase; +} & RuleTesterTestFrameworkFunctionBase; type Maybe = T | null | undefined; diff --git a/packages/rule-tester/src/index.ts b/packages/rule-tester/src/index.ts index 6ea08fc5addb..ef918ca3d6d3 100644 --- a/packages/rule-tester/src/index.ts +++ b/packages/rule-tester/src/index.ts @@ -1,5 +1,5 @@ -export { RuleTester } from './RuleTester'; export { noFormat } from './noFormat'; +export { RuleTester } from './RuleTester'; export type { InvalidTestCase, RuleTesterConfig, diff --git a/packages/rule-tester/src/types/DependencyConstraint.ts b/packages/rule-tester/src/types/DependencyConstraint.ts index f2b38dbf2245..2e9138cd401d 100644 --- a/packages/rule-tester/src/types/DependencyConstraint.ts +++ b/packages/rule-tester/src/types/DependencyConstraint.ts @@ -6,12 +6,12 @@ export interface RangeOptions { } export interface SemverVersionConstraint { - readonly range: string; readonly options?: RangeOptions | boolean; + readonly range: string; } export type AtLeastVersionConstraint = - | `${number}.${number}.${number}-${string}` | `${number}.${number}.${number}` + | `${number}.${number}.${number}-${string}` | `${number}.${number}` | `${number}`; export type VersionConstraint = diff --git a/packages/rule-tester/src/types/InvalidTestCase.ts b/packages/rule-tester/src/types/InvalidTestCase.ts index 9936754f6afb..bcd9439f5544 100644 --- a/packages/rule-tester/src/types/InvalidTestCase.ts +++ b/packages/rule-tester/src/types/InvalidTestCase.ts @@ -5,14 +5,14 @@ import type { DependencyConstraint } from './DependencyConstraint'; import type { ValidTestCase } from './ValidTestCase'; export interface SuggestionOutput { - /** - * Reported message ID. - */ - readonly messageId: MessageIds; /** * The data used to fill the message template. */ readonly data?: ReportDescriptorMessageData; + /** + * Reported message ID. + */ + readonly messageId: MessageIds; /** * 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. @@ -65,6 +65,10 @@ export interface InvalidTestCase< MessageIds extends string, Options extends readonly unknown[], > extends ValidTestCase { + /** + * Constraints that must pass in the current environment for the test to run + */ + readonly dependencyConstraints?: DependencyConstraint; /** * Expected errors. */ @@ -72,9 +76,5 @@ export interface InvalidTestCase< /** * The expected code after autofixes are applied. If set to `null`, the test runner will assert that no autofix is suggested. */ - readonly output?: string | string[] | null; - /** - * Constraints that must pass in the current environment for the test to run - */ - readonly dependencyConstraints?: DependencyConstraint; + readonly output?: string[] | string | null; } diff --git a/packages/rule-tester/src/types/ValidTestCase.ts b/packages/rule-tester/src/types/ValidTestCase.ts index 4f1633b32fef..c2d95c772a84 100644 --- a/packages/rule-tester/src/types/ValidTestCase.ts +++ b/packages/rule-tester/src/types/ValidTestCase.ts @@ -27,14 +27,14 @@ export interface TestLanguageOptions { } export interface ValidTestCase { - /** - * Name for the test case. - */ - readonly name?: string; /** * Code for the test case. */ readonly code: string; + /** + * Constraints that must pass in the current environment for the test to run + */ + readonly dependencyConstraints?: DependencyConstraint; /** * The fake filename for the test case. Useful for rules that make assertion about filenames. */ @@ -43,6 +43,14 @@ export interface ValidTestCase { * Language options for the test case. */ readonly languageOptions?: TestLanguageOptions; + /** + * Name for the test case. + */ + readonly name?: string; + /** + * Run this case exclusively for debugging in supported test frameworks. + */ + readonly only?: boolean; /** * Options for the test case. */ @@ -51,16 +59,8 @@ export interface ValidTestCase { * 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 index bd04d11e050c..6d89f7808fde 100644 --- a/packages/rule-tester/src/types/index.ts +++ b/packages/rule-tester/src/types/index.ts @@ -16,23 +16,23 @@ export interface RunTests< MessageIds extends string, Options extends readonly unknown[], > { + readonly invalid: readonly InvalidTestCase[]; // RuleTester.run also accepts strings for valid cases readonly valid: readonly (ValidTestCase | string)[]; - readonly invalid: readonly InvalidTestCase[]; } export interface NormalizedRunTests< MessageIds extends string, Options extends readonly unknown[], > { - readonly valid: readonly ValidTestCase[]; readonly invalid: readonly InvalidTestCase[]; + readonly valid: readonly ValidTestCase[]; } -export type { ValidTestCase } from './ValidTestCase'; export type { InvalidTestCase, SuggestionOutput, TestCaseError, } from './InvalidTestCase'; export type { RuleTesterConfig } from './RuleTesterConfig'; +export type { ValidTestCase } from './ValidTestCase'; diff --git a/packages/rule-tester/src/utils/ajv.ts b/packages/rule-tester/src/utils/ajv.ts index f3dcacc641d4..feb9fe998d05 100644 --- a/packages/rule-tester/src/utils/ajv.ts +++ b/packages/rule-tester/src/utils/ajv.ts @@ -6,11 +6,11 @@ import metaSchema from 'ajv/lib/refs/json-schema-draft-04.json'; export function ajvBuilder(additionalOptions = {}): Ajv.Ajv { const ajv = new Ajv({ meta: false, + missingRefs: 'ignore', + schemaId: 'auto', useDefaults: true, validateSchema: false, - missingRefs: 'ignore', verbose: true, - schemaId: 'auto', ...additionalOptions, }); diff --git a/packages/rule-tester/src/utils/config-validator.ts b/packages/rule-tester/src/utils/config-validator.ts index 66805e9d6fd0..b8b740915aba 100644 --- a/packages/rule-tester/src/utils/config-validator.ts +++ b/packages/rule-tester/src/utils/config-validator.ts @@ -1,16 +1,17 @@ // Forked from https://github.com/eslint/eslint/blob/ad9dd6a933fd098a0d99c6a9aa059850535c23ee/lib/shared/config-validator.js -import util from 'node:util'; - 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 util from 'node:util'; import type { TesterConfigWithDefaults } from '../types'; + import { ajvBuilder } from './ajv'; import { emitDeprecationWarning } from './deprecation-warnings'; import { flatConfigSchema } from './flat-config-schema'; @@ -25,8 +26,8 @@ const ruleValidators = new WeakMap(); let validateSchema: ValidateFunction | undefined; const severityMap = { error: 2, - warn: 1, off: 0, + warn: 1, } as const; /** diff --git a/packages/rule-tester/src/utils/deprecation-warnings.ts b/packages/rule-tester/src/utils/deprecation-warnings.ts index 9f264a412964..2453be707f45 100644 --- a/packages/rule-tester/src/utils/deprecation-warnings.ts +++ b/packages/rule-tester/src/utils/deprecation-warnings.ts @@ -21,7 +21,7 @@ export function emitDeprecationWarning( source: string, errorCode: keyof typeof deprecationWarningMessages, ): void { - const cacheKey = JSON.stringify({ source, errorCode }); + const cacheKey = JSON.stringify({ errorCode, source }); if (sourceFileErrorCache.has(cacheKey)) { return; diff --git a/packages/rule-tester/src/utils/flat-config-schema.ts b/packages/rule-tester/src/utils/flat-config-schema.ts index 89a1e18fad71..0f46b9ef549e 100644 --- a/packages/rule-tester/src/utils/flat-config-schema.ts +++ b/packages/rule-tester/src/utils/flat-config-schema.ts @@ -10,17 +10,17 @@ import { normalizeSeverityToNumber } from './severity'; type PluginMemberName = `${string}/${string}`; interface ObjectPropertySchema { - merge: string | ((a: T, b: T) => T); - validate: string | ((value: unknown) => asserts value is T); + merge: ((a: T, b: T) => T) | string; + validate: ((value: unknown) => asserts value is T) | string; } const ruleSeverities = new Map([ - [0, 0], + ['error', 2], ['off', 0], - [1, 1], ['warn', 1], + [0, 0], + [1, 1], [2, 2], - ['error', 2], ]); /** @@ -77,7 +77,7 @@ function deepMerge( const result = { ...first, ...second, - } as First & Second & ObjectLike; + } as First & ObjectLike & Second; delete (result as ObjectLike).__proto__; // don't merge own property "__proto__" @@ -151,8 +151,8 @@ function hasMethod(object: Record): boolean { * The error type when a rule's options are configured with an invalid type. */ class InvalidRuleOptionsError extends Error { - readonly messageTemplate: string; readonly messageData: { ruleId: string; value: unknown }; + readonly messageTemplate: string; constructor(ruleId: string, value: unknown) { super( @@ -183,8 +183,8 @@ function assertIsRuleOptions(ruleId: string, value: unknown): void { * The error type when a rule's severity is invalid. */ class InvalidRuleSeverityError extends Error { - readonly messageTemplate: string; readonly messageData: { ruleId: string; value: unknown }; + readonly messageTemplate: string; constructor(ruleId: string, value: unknown) { super( @@ -241,8 +241,8 @@ function assertIsObject(value: unknown): void { * The error type when there's an eslintrc-style options in a flat config. */ class IncompatibleKeyError extends Error { - readonly messageTemplate: string; readonly messageData: { key: string }; + readonly messageTemplate: string; /** * @param key The invalid key. @@ -260,8 +260,8 @@ class IncompatibleKeyError extends Error { * The error type when there's an eslintrc-style plugins array found. */ class IncompatiblePluginsError extends Error { - readonly messageTemplate: string; readonly messageData: { plugins: string[] }; + readonly messageTemplate: string; constructor(plugins: string[]) { super( @@ -297,7 +297,7 @@ const disableDirectiveSeveritySchema: ObjectPropertySchema( obj: Obj, key: K, -) => obj is Obj & { [key in K]-?: Obj[key] }; +) => obj is { [key in K]-?: Obj[key] } & Obj; diff --git a/packages/rule-tester/src/utils/validationHelpers.ts b/packages/rule-tester/src/utils/validationHelpers.ts index aa468910e671..fddf0f6f687c 100644 --- a/packages/rule-tester/src/utils/validationHelpers.ts +++ b/packages/rule-tester/src/utils/validationHelpers.ts @@ -1,7 +1,8 @@ -import { simpleTraverse } from '@typescript-eslint/typescript-estree'; import type { TSESTree } from '@typescript-eslint/utils'; import type { Parser, SourceCode } from '@typescript-eslint/utils/ts-eslint'; +import { simpleTraverse } from '@typescript-eslint/typescript-estree'; + /* * List every parameters possible on a test case that are not related to eslint * configuration @@ -91,30 +92,30 @@ export function wrapParser( */ function defineStartEndAsError(objName: string, node: unknown): void { Object.defineProperties(node, { - start: { + end: { + configurable: true, + enumerable: false, get() { throw new Error( - `Use ${objName}.range[0] instead of ${objName}.start`, + `Use ${objName}.range[1] instead of ${objName}.end`, ); }, + }, + start: { configurable: true, enumerable: false, - }, - end: { get() { throw new Error( - `Use ${objName}.range[1] instead of ${objName}.end`, + `Use ${objName}.range[0] instead of ${objName}.start`, ); }, - configurable: true, - enumerable: false, }, }); } simpleTraverse(ast, { - visitorKeys, enter: node => defineStartEndAsError('node', node), + visitorKeys, }); ast.tokens?.forEach(token => defineStartEndAsError('token', token)); ast.comments?.forEach(comment => defineStartEndAsError('token', comment)); @@ -122,26 +123,28 @@ export function wrapParser( if ('parseForESLint' in parser) { return { - // @ts-expect-error -- see above - [parserSymbol]: parser, parseForESLint(...args): Parser.ParseResult { const parsed = parser.parseForESLint(...args) as Parser.ParseResult; defineStartEndAsErrorInTree(parsed.ast, parsed.visitorKeys); return parsed; }, + + // @ts-expect-error -- see above + [parserSymbol]: parser, }; } return { - // @ts-expect-error -- see above - [parserSymbol]: parser, parse(...args): TSESTree.Program { const ast = parser.parse(...args) as TSESTree.Program; defineStartEndAsErrorInTree(ast); return ast; }, + + // @ts-expect-error -- see above + [parserSymbol]: parser, }; } diff --git a/packages/rule-tester/tests/RuleTester.test.ts b/packages/rule-tester/tests/RuleTester.test.ts index 06bfd09d5abc..daf69a1a373b 100644 --- a/packages/rule-tester/tests/RuleTester.test.ts +++ b/packages/rule-tester/tests/RuleTester.test.ts @@ -1,10 +1,12 @@ -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 * as parser from '@typescript-eslint/parser'; +import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; + import type { RuleTesterTestFrameworkFunctionBase } from '../src/TestFramework'; + +import { RuleTester } from '../src/RuleTester'; 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 @@ -79,26 +81,26 @@ const _mockedItSkip = jest.mocked(RuleTester.itSkip); 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 } }, + range: [0, 0], sourceType: 'module', tokens: [], - range: [0, 0], + type: AST_NODE_TYPES.Program, }; const NOOP_RULE: RuleModule<'error'> = { + create() { + return {}; + }, + defaultOptions: [], meta: { messages: { error: 'error', }, - type: 'problem', schema: [], - }, - defaultOptions: [], - create() { - return {}; + type: 'problem', }, }; @@ -113,6 +115,9 @@ describe('RuleTester', () => { }); runRuleForItemSpy.mockImplementation((_1, _2, testCase) => { return { + afterAST: EMPTY_PROGRAM, + beforeAST: EMPTY_PROGRAM, + config: {}, messages: 'errors' in testCase ? [ @@ -129,9 +134,6 @@ describe('RuleTester', () => { ] : [], outputs: [testCase.code], - afterAST: EMPTY_PROGRAM, - beforeAST: EMPTY_PROGRAM, - config: {}, }; }); @@ -154,6 +156,12 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { + invalid: [ + { + code: 'invalid tests should work as well', + errors: [{ messageId: 'error' }], + }, + ], valid: [ 'string based valid test', { @@ -183,12 +191,6 @@ describe('RuleTester', () => { }, }, ], - invalid: [ - { - code: 'invalid tests should work as well', - errors: [{ messageId: 'error' }], - }, - ], }); expect(getTestConfigFromCall()).toMatchInlineSnapshot(` @@ -263,6 +265,10 @@ describe('RuleTester', () => { it('allows the automated filenames to be overridden in the constructor', () => { const ruleTester = new RuleTester({ + defaultFilenames: { + ts: 'set-in-constructor.ts', + tsx: 'react-set-in-constructor.tsx', + }, languageOptions: { parser, parserOptions: { @@ -270,13 +276,10 @@ describe('RuleTester', () => { tsconfigRootDir: '/some/path/that/totally/exists/', }, }, - defaultFilenames: { - ts: 'set-in-constructor.ts', - tsx: 'react-set-in-constructor.tsx', - }, }); ruleTester.run('my-rule', NOOP_RULE, { + invalid: [], valid: [ { code: 'normal', @@ -292,7 +295,6 @@ describe('RuleTester', () => { }, }, ], - invalid: [], }); expect(getTestConfigFromCall()).toMatchInlineSnapshot(` @@ -358,14 +360,14 @@ describe('RuleTester', () => { expect(() => ruleTester.run('my-rule', NOOP_RULE, { + invalid: [], + valid: [ { code: 'object based valid test', languageOptions: { parser }, }, ], - - invalid: [], }), ).toThrowErrorMatchingInlineSnapshot( `"Do not set the parser at the test level unless you want to use a parser other than "@typescript-eslint/parser""`, @@ -379,13 +381,13 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { + invalid: [], valid: [ 'const x = 1;', { code: 'const x = 2;' }, // empty object is ignored { code: 'const x = 3;', dependencyConstraints: {} }, ], - invalid: [], }); expect(satisfiesAllDependencyConstraintsMock).not.toHaveBeenCalled(); @@ -398,6 +400,7 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { + invalid: [], valid: [ 'const x = 1;', { code: 'const x = 2;' }, @@ -412,7 +415,6 @@ describe('RuleTester', () => { }, }, ], - invalid: [], }); expect(satisfiesAllDependencyConstraintsMock).not.toHaveBeenCalled(); @@ -424,6 +426,13 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { + invalid: [ + { + code: 'const x = 3;', + errors: [{ messageId: 'error' }], + only: true, + }, + ], valid: [ 'const x = 1;', { code: 'const x = 2;' }, @@ -434,13 +443,6 @@ describe('RuleTester', () => { }, }, ], - invalid: [ - { - code: 'const x = 3;', - errors: [{ messageId: 'error' }], - only: true, - }, - ], }); expect(satisfiesAllDependencyConstraintsMock).not.toHaveBeenCalled(); @@ -453,46 +455,46 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { - valid: [ + invalid: [ { - code: 'passing - major', + code: 'failing - major', dependencyConstraints: { - 'totally-real-dependency': '10', + 'totally-real-dependency': '999', }, + errors: [{ messageId: 'error' }], }, { - code: 'passing - major.minor', + code: 'failing - major.minor', dependencyConstraints: { - 'totally-real-dependency': '10.0', + 'totally-real-dependency': '999.0', }, + errors: [{ messageId: 'error' }], }, { - code: 'passing - major.minor.patch', + code: 'failing - major.minor.patch', dependencyConstraints: { - 'totally-real-dependency': '10.0.0', + 'totally-real-dependency': '999.0.0', }, + errors: [{ messageId: 'error' }], }, ], - invalid: [ + valid: [ { - code: 'failing - major', - errors: [{ messageId: 'error' }], + code: 'passing - major', dependencyConstraints: { - 'totally-real-dependency': '999', + 'totally-real-dependency': '10', }, }, { - code: 'failing - major.minor', - errors: [{ messageId: 'error' }], + code: 'passing - major.minor', dependencyConstraints: { - 'totally-real-dependency': '999.0', + 'totally-real-dependency': '10.0', }, }, { - code: 'failing - major.minor.patch', - errors: [{ messageId: 'error' }], + code: 'passing - major.minor.patch', dependencyConstraints: { - 'totally-real-dependency': '999.0.0', + 'totally-real-dependency': '10.0.0', }, }, ], @@ -603,53 +605,53 @@ describe('RuleTester', () => { }); 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', }, }, + errors: [{ messageId: 'error' }], }, { code: 'failing - major.minor', - errors: [{ messageId: 'error' }], dependencyConstraints: { 'totally-real-dependency': { range: '>=999.0', }, }, + errors: [{ messageId: 'error' }], }, { code: 'failing with options', - errors: [{ messageId: 'error' }], dependencyConstraints: { 'totally-real-dependency-prerelease': { - range: '^10', options: { includePrerelease: false, }, + range: '^10', + }, + }, + errors: [{ messageId: 'error' }], + }, + ], + valid: [ + { + code: 'passing - major', + dependencyConstraints: { + 'totally-real-dependency': { + range: '^10', + }, + }, + }, + { + code: 'passing - major.minor', + dependencyConstraints: { + 'totally-real-dependency': { + range: '<999', }, }, }, @@ -761,37 +763,37 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { - valid: [ - 'string based is always run', + invalid: [ { code: 'no constraints is always run', + errors: [{ messageId: 'error' }], }, { code: 'empty object is always run', dependencyConstraints: {}, + errors: [{ messageId: 'error' }], }, { - code: 'passing constraint', + code: 'failing constraint', dependencyConstraints: { - 'totally-real-dependency': '10', + 'totally-real-dependency': '99999', }, + errors: [{ messageId: 'error' }], }, ], - invalid: [ + valid: [ + 'string based is always run', { 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' }], + code: 'passing constraint', dependencyConstraints: { - 'totally-real-dependency': '99999', + 'totally-real-dependency': '10', }, }, ], @@ -906,17 +908,17 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { - valid: [ - { - code: 'passing - major', - }, - ], invalid: [ { code: 'failing - major', errors: [{ messageId: 'error' }], }, ], + valid: [ + { + code: 'passing - major', + }, + ], }); // trigger the describe block @@ -938,17 +940,17 @@ describe('RuleTester', () => { }); ruleTester.run('my-rule', NOOP_RULE, { - valid: [ - { - code: 'valid', - }, - ], invalid: [ { code: 'invalid', errors: [{ messageId: 'error' }], }, ], + valid: [ + { + code: 'valid', + }, + ], }); // trigger the describe block @@ -961,13 +963,13 @@ describe('RuleTester', () => { const ruleTester = new RuleTester(); ruleTester.run('my-rule', NOOP_RULE, { - valid: [], invalid: [ { code: 'invalid', errors: [{ messageId: 'error' }], }, ], + valid: [], }); expect(mockedDescribe.mock.calls).toMatchInlineSnapshot(` @@ -988,12 +990,12 @@ describe('RuleTester', () => { const ruleTester = new RuleTester(); ruleTester.run('my-rule', NOOP_RULE, { + invalid: [], valid: [ { code: 'valid', }, ], - invalid: [], }); expect(mockedDescribe.mock.calls).toMatchInlineSnapshot(` @@ -1021,36 +1023,36 @@ describe('RuleTester - multipass fixer', () => { describe('without fixes', () => { const ruleTester = new RuleTester(); const rule: RuleModule<'error'> = { - meta: { - messages: { - error: 'error', - }, - type: 'problem', - schema: [], - }, - defaultOptions: [], create(context) { return { 'Identifier[name=foo]'(node): void { context.report({ - node, messageId: 'error', + node, }); }, }; }, + defaultOptions: [], + meta: { + messages: { + error: 'error', + }, + schema: [], + type: 'problem', + }, }; it('passes with no output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', errors: [{ messageId: 'error' }], }, ], + valid: [], }); }).not.toThrow(); }); @@ -1058,13 +1060,13 @@ describe('RuleTester - multipass fixer', () => { it('passes with null output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', errors: [{ messageId: 'error' }], }, ], + valid: [], }); }).not.toThrow(); }); @@ -1072,14 +1074,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with string output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: 'bar', errors: [{ messageId: 'error' }], + output: 'bar', }, ], + valid: [], }); }).toThrow('Expected autofix to be suggested.'); }); @@ -1087,14 +1089,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with array output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: ['bar', 'baz'], errors: [{ messageId: 'error' }], + output: ['bar', 'baz'], }, ], + valid: [], }); }).toThrow('Expected autofix to be suggested.'); }); @@ -1103,39 +1105,39 @@ describe('RuleTester - multipass fixer', () => { describe('with single fix', () => { const ruleTester = new RuleTester(); const rule: RuleModule<'error'> = { - meta: { - messages: { - error: 'error', - }, - type: 'problem', - fixable: 'code', - schema: [], - }, - defaultOptions: [], create(context) { return { 'Identifier[name=foo]'(node): void { context.report({ - node, - messageId: 'error', fix: fixer => fixer.replaceText(node, 'bar'), + messageId: 'error', + node, }); }, }; }, + defaultOptions: [], + meta: { + fixable: 'code', + messages: { + error: 'error', + }, + schema: [], + type: 'problem', + }, }; it('passes with correct string output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: 'bar', errors: [{ messageId: 'error' }], + output: 'bar', }, ], + valid: [], }); }).not.toThrow(); }); @@ -1143,14 +1145,14 @@ describe('RuleTester - multipass fixer', () => { it('passes with correct array output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: ['bar'], errors: [{ messageId: 'error' }], + output: ['bar'], }, ], + valid: [], }); }).not.toThrow(); }); @@ -1158,13 +1160,13 @@ describe('RuleTester - multipass fixer', () => { it('throws with no output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', errors: [{ messageId: 'error' }], }, ], + valid: [], }); }).toThrow("The rule fixed the code. Please add 'output' property."); }); @@ -1172,14 +1174,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with null output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: null, errors: [{ messageId: 'error' }], + output: null, }, ], + valid: [], }); }).toThrow('Expected no autofixes to be suggested.'); }); @@ -1187,14 +1189,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with incorrect array output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: ['bar', 'baz'], errors: [{ messageId: 'error' }], + output: ['bar', 'baz'], }, ], + valid: [], }); }).toThrow('Outputs do not match.'); }); @@ -1202,14 +1204,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with incorrect string output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: 'baz', errors: [{ messageId: 'error' }], + output: 'baz', }, ], + valid: [], }); }).toThrow('Output is incorrect.'); }); @@ -1218,46 +1220,46 @@ describe('RuleTester - multipass fixer', () => { describe('with multiple fixes', () => { const ruleTester = new RuleTester(); const rule: RuleModule<'error'> = { - meta: { - messages: { - error: 'error', - }, - type: 'problem', - fixable: 'code', - schema: [], - }, - defaultOptions: [], create(context) { return { - 'Identifier[name=foo]'(node): void { + 'Identifier[name=bar]'(node): void { context.report({ - node, + fix: fixer => fixer.replaceText(node, 'baz'), messageId: 'error', - fix: fixer => fixer.replaceText(node, 'bar'), + node, }); }, - 'Identifier[name=bar]'(node): void { + 'Identifier[name=foo]'(node): void { context.report({ - node, + fix: fixer => fixer.replaceText(node, 'bar'), messageId: 'error', - fix: fixer => fixer.replaceText(node, 'baz'), + node, }); }, }; }, + defaultOptions: [], + meta: { + fixable: 'code', + messages: { + error: 'error', + }, + schema: [], + type: 'problem', + }, }; it('passes with correct array output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: ['bar', 'baz'], errors: [{ messageId: 'error' }], + output: ['bar', 'baz'], }, ], + valid: [], }); }).not.toThrow(); }); @@ -1265,14 +1267,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with string output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: 'bar', errors: [{ messageId: 'error' }], + output: 'bar', }, ], + valid: [], }); }).toThrow( 'Multiple autofixes are required due to overlapping fix ranges - please use the array form of output to declare all of the expected autofix passes.', @@ -1282,14 +1284,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with incorrect array output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: ['bar'], errors: [{ messageId: 'error' }], + output: ['bar'], }, ], + valid: [], }); }).toThrow('Outputs do not match.'); }); @@ -1297,14 +1299,14 @@ describe('RuleTester - multipass fixer', () => { it('throws with incorrectly ordered array output', () => { expect(() => { ruleTester.run('my-rule', rule, { - valid: [], invalid: [ { code: 'foo', - output: ['baz', 'bar'], errors: [{ messageId: 'error' }], + output: ['baz', 'bar'], }, ], + valid: [], }); }).toThrow('Outputs do not match.'); }); diff --git a/packages/rule-tester/tests/flat-config-schema.test.ts b/packages/rule-tester/tests/flat-config-schema.test.ts index b52a2b8cc31d..b1f5ccb35cec 100644 --- a/packages/rule-tester/tests/flat-config-schema.test.ts +++ b/packages/rule-tester/tests/flat-config-schema.test.ts @@ -2,6 +2,7 @@ // Forked from: https://github.com/eslint/eslint/blob/f182114144ae0bb7187de34a1661f31fb70f1357/tests/lib/config/flat-config-schema.js import type { ObjectLike } from '../src/utils/flat-config-schema'; + import { flatConfigSchema } from '../src/utils/flat-config-schema'; describe('merge', () => { @@ -22,7 +23,7 @@ describe('merge', () => { }); it('returns an object equal to the first one if the second one is undefined', () => { - const first = { foo: 42, bar: 'baz' }; + const first = { bar: 'baz', foo: 42 }; const result = merge(first, undefined); expect(result).toEqual(first); @@ -30,7 +31,7 @@ describe('merge', () => { }); it('returns an object equal to the second one if the first one is undefined', () => { - const second = { foo: 42, bar: 'baz' }; + const second = { bar: 'baz', foo: 42 }; const result = merge(undefined, second); expect(result).toEqual(second); @@ -94,7 +95,7 @@ describe('merge', () => { it('does not change the prototype of a merged object', () => { const first = { foo: 42 }; - const second = { bar: 'baz', ['__proto__']: { qux: true } }; + const second = { ['__proto__']: { qux: true }, bar: 'baz' }; const result = merge(first, second); expect(Object.getPrototypeOf(result)).toBe(Object.prototype); @@ -154,11 +155,11 @@ describe('merge', () => { }); it('sets properties to undefined', () => { - const first = { foo: undefined, bar: undefined }; - const second = { foo: undefined, baz: undefined }; + const first = { bar: undefined, foo: undefined }; + const second = { baz: undefined, foo: undefined }; const result = merge(first, second); - expect(result).toEqual({ foo: undefined, bar: undefined, baz: undefined }); + expect(result).toEqual({ bar: undefined, baz: undefined, foo: undefined }); }); it('considers only own enumerable properties', () => { @@ -205,7 +206,7 @@ describe('merge', () => { expect(result.first).toEqual(first); expect(result.second).toEqual(second); - const expected: ObjectLike = { foo: 42, bar: 'baz' }; + const expected: ObjectLike = { bar: 'baz', foo: 42 }; expected.first = first; expected.second = second; @@ -224,7 +225,7 @@ describe('merge', () => { expect(result.reference).toEqual(result); - const expected: ObjectLike = { foo: 42, bar: 'baz' }; + const expected: ObjectLike = { bar: 'baz', foo: 42 }; expected.reference = expected; expect(result).toEqual(expected); @@ -242,7 +243,7 @@ describe('merge', () => { expect(result.first).toEqual(first); expect(result.second).toEqual(second); - const expected: ObjectLike = { foo: 42, bar: 'baz' }; + const expected: ObjectLike = { bar: 'baz', foo: 42 }; expected.first = first; expected.second = second; @@ -261,9 +262,9 @@ describe('merge', () => { expect(result).toEqual((result.reference as ObjectLike).reference); const expected = { - foo: 42, bar: 'baz', - reference: { foo: 42, bar: 'baz' }, + foo: 42, + reference: { bar: 'baz', foo: 42 }, }; (expected.reference as ObjectLike).reference = expected; @@ -290,10 +291,10 @@ describe('merge', () => { expect(result.a).toEqual(result.d); const expected = { - a: { foo: 42, bar: 'baz' }, - b: { foo: 42, bar: 'something else' }, - c: { foo: 'different', bar: 'baz' }, - d: { foo: 42, bar: 'baz' }, + a: { bar: 'baz', foo: 42 }, + b: { bar: 'something else', foo: 42 }, + c: { bar: 'baz', foo: 'different' }, + d: { bar: 'baz', foo: 42 }, }; expect(result).toEqual(expected);