diff --git a/packages/eslint-plugin/tests/dedupeTestCases.ts b/packages/eslint-plugin/tests/dedupeTestCases.ts new file mode 100644 index 000000000000..e4a197218fcf --- /dev/null +++ b/packages/eslint-plugin/tests/dedupeTestCases.ts @@ -0,0 +1,14 @@ +export const dedupeTestCases = (...caseArrays: (readonly T[])[]): T[] => { + const cases = caseArrays.flat(); + const dedupedCases = Object.values( + Object.fromEntries( + cases.map(testCase => [JSON.stringify(testCase), testCase]), + ), + ); + if (cases.length === dedupedCases.length) { + throw new Error( + '`dedupeTestCases` is not necessary — no duplicate test cases detected!', + ); + } + return dedupedCases; +}; diff --git a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts index b365c66b32fa..fde1ed0d0dd5 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-assertions.test.ts @@ -7,11 +7,10 @@ import type { Options, } from '../../src/rules/consistent-type-assertions'; import rule from '../../src/rules/consistent-type-assertions'; +import { dedupeTestCases } from '../dedupeTestCases'; import { batchedSingleLineTests } from '../RuleTester'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', -}); +const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser' }); const ANGLE_BRACKET_TESTS_EXCEPT_CONST_CASE = ` const x = new Generic(); @@ -33,6 +32,7 @@ const ANGLE_BRACKET_TESTS = `${ANGLE_BRACKET_TESTS_EXCEPT_CONST_CASE} const x = { key: 'value' }; `; +// Intentionally contains a duplicate in order to mirror ANGLE_BRACKET_TESTS_EXCEPT_CONST_CASE const AS_TESTS_EXCEPT_CONST_CASE = ` const x = new Generic() as Foo; const x = b as A; @@ -84,15 +84,14 @@ print\`\${{ bar: 5 }}\` ruleTester.run('consistent-type-assertions', rule, { valid: [ - ...batchedSingleLineTests({ - code: AS_TESTS, - options: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'allow', - }, - ], - }), + ...dedupeTestCases( + batchedSingleLineTests({ + code: AS_TESTS, + options: [ + { assertionStyle: 'as', objectLiteralTypeAssertions: 'allow' }, + ], + }), + ), ...batchedSingleLineTests({ code: ANGLE_BRACKET_TESTS, options: [ @@ -104,12 +103,7 @@ ruleTester.run('consistent-type-assertions', rule, { }), ...batchedSingleLineTests({ code: `${OBJECT_LITERAL_AS_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, - options: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'allow', - }, - ], + options: [{ assertionStyle: 'as', objectLiteralTypeAssertions: 'allow' }], }), ...batchedSingleLineTests({ code: `${OBJECT_LITERAL_ANGLE_BRACKET_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_ANGLE_BRACKET_CASTS}`, @@ -138,29 +132,11 @@ ruleTester.run('consistent-type-assertions', rule, { }, ], }), - { - code: 'const x = [1];', - options: [ - { - assertionStyle: 'never', - }, - ], - }, - { - code: 'const x = [1] as const;', - options: [ - { - assertionStyle: 'never', - }, - ], - }, + { code: 'const x = [1];', options: [{ assertionStyle: 'never' }] }, + { code: 'const x = [1] as const;', options: [{ assertionStyle: 'never' }] }, { code: 'const bar = ;', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, + parserOptions: { ecmaFeatures: { jsx: true } }, options: [ { assertionStyle: 'as', @@ -170,279 +146,25 @@ ruleTester.run('consistent-type-assertions', rule, { }, ], invalid: [ - ...batchedSingleLineTests({ - code: AS_TESTS, - options: [ - { - assertionStyle: 'angle-bracket', - }, - ], - errors: [ - { - messageId: 'angle-bracket', - line: 2, - }, - { - messageId: 'angle-bracket', - line: 3, - }, - { - messageId: 'angle-bracket', - line: 4, - }, - { - messageId: 'angle-bracket', - line: 5, - }, - { - messageId: 'angle-bracket', - line: 6, - }, - { - messageId: 'angle-bracket', - line: 7, - }, - { - messageId: 'angle-bracket', - line: 8, - }, - { - messageId: 'angle-bracket', - line: 9, - }, - { - messageId: 'angle-bracket', - line: 10, - }, - { - messageId: 'angle-bracket', - line: 11, - }, - { - messageId: 'angle-bracket', - line: 12, - }, - { - messageId: 'angle-bracket', - line: 13, - }, - { - messageId: 'angle-bracket', - line: 14, - }, - { - messageId: 'angle-bracket', - line: 15, - }, - { - messageId: 'angle-bracket', - line: 16, - }, - ], - }), - ...batchedSingleLineTests({ - code: ANGLE_BRACKET_TESTS, - options: [ - { - assertionStyle: 'as', - }, - ], - errors: [ - { - messageId: 'as', - line: 2, - }, - { - messageId: 'as', - line: 3, - }, - { - messageId: 'as', - line: 4, - }, - { - messageId: 'as', - line: 5, - }, - { - messageId: 'as', - line: 6, - }, - { - messageId: 'as', - line: 7, - }, - { - messageId: 'as', - line: 8, - }, - { - messageId: 'as', - line: 9, - }, - { - messageId: 'as', - line: 10, - }, - { - messageId: 'as', - line: 11, - }, - { - messageId: 'as', - line: 12, - }, - { - messageId: 'as', - line: 13, - }, - { - messageId: 'as', - line: 14, - }, - { - messageId: 'as', - line: 15, - }, - { - messageId: 'as', - line: 16, - }, - ], - output: AS_TESTS, - }), - ...batchedSingleLineTests({ - code: AS_TESTS_EXCEPT_CONST_CASE, - options: [ - { - assertionStyle: 'never', - }, - ], - errors: [ - { - messageId: 'never', - line: 2, - }, - { - messageId: 'never', - line: 3, - }, - { - messageId: 'never', - line: 4, - }, - { - messageId: 'never', - line: 5, - }, - { - messageId: 'never', - line: 6, - }, - { - messageId: 'never', - line: 7, - }, - { - messageId: 'never', - line: 8, - }, - { - messageId: 'never', - line: 9, - }, - { - messageId: 'never', - line: 10, - }, - { - messageId: 'never', - line: 11, - }, - { - messageId: 'never', - line: 12, - }, - { - messageId: 'never', - line: 13, - }, - { - messageId: 'never', - line: 14, - }, - { - messageId: 'never', - line: 15, - }, - ], - }), - ...batchedSingleLineTests({ - code: ANGLE_BRACKET_TESTS_EXCEPT_CONST_CASE, - options: [ - { - assertionStyle: 'never', - }, - ], - errors: [ - { - messageId: 'never', - line: 2, - }, - { - messageId: 'never', - line: 3, - }, - { - messageId: 'never', - line: 4, - }, - { - messageId: 'never', - line: 5, - }, - { - messageId: 'never', - line: 6, - }, - { - messageId: 'never', - line: 7, - }, - { - messageId: 'never', - line: 8, - }, - { - messageId: 'never', - line: 9, - }, - { - messageId: 'never', - line: 10, - }, - { - messageId: 'never', - line: 11, - }, - { - messageId: 'never', - line: 12, - }, - { - messageId: 'never', - line: 13, - }, - { - messageId: 'never', - line: 14, - }, - { - messageId: 'never', - line: 15, - }, - ], - }), + ...dedupeTestCases( + ( + [ + ['angle-bracket', AS_TESTS], + ['as', ANGLE_BRACKET_TESTS, AS_TESTS], + ['never', AS_TESTS_EXCEPT_CONST_CASE], + ['never', ANGLE_BRACKET_TESTS_EXCEPT_CONST_CASE], + ] as const + ).flatMap(([assertionStyle, code, output]) => + batchedSingleLineTests({ + code, + options: [{ assertionStyle }], + errors: code + .split(`\n`) + .map((_, i) => ({ messageId: assertionStyle, line: i + 1 })), + output, + }), + ), + ), ...batchedSingleLineTests({ code: OBJECT_LITERAL_AS_CASTS, options: [ @@ -553,12 +275,7 @@ ruleTester.run('consistent-type-assertions', rule, { }), ...batchedSingleLineTests({ code: `${OBJECT_LITERAL_AS_CASTS.trimEnd()}${OBJECT_LITERAL_ARGUMENT_AS_CASTS}`, - options: [ - { - assertionStyle: 'as', - objectLiteralTypeAssertions: 'never', - }, - ], + options: [{ assertionStyle: 'as', objectLiteralTypeAssertions: 'never' }], errors: [ { messageId: 'unexpectedObjectTypeAssertion', @@ -816,22 +533,9 @@ ruleTester.run('consistent-type-assertions', rule, { { code: 'const foo = ;', output: null, - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - options: [ - { - assertionStyle: 'never', - }, - ], - errors: [ - { - messageId: 'never', - line: 1, - }, - ], + parserOptions: { ecmaFeatures: { jsx: true } }, + options: [{ assertionStyle: 'never' }], + errors: [{ messageId: 'never', line: 1 }], }, { code: 'const a = (b, c);', diff --git a/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts b/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts index 443e4e92f19d..e67567ada705 100644 --- a/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/func-call-spacing.test.ts @@ -9,9 +9,7 @@ import type { TSESLint } from '@typescript-eslint/utils'; import type { MessageIds, Options } from '../../src/rules/func-call-spacing'; import rule from '../../src/rules/func-call-spacing'; -const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', -}); +const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser' }); ruleTester.run('func-call-spacing', rule, { valid: [ @@ -69,7 +67,7 @@ ruleTester.run('func-call-spacing', rule, { 'f?.b(b, b)', 'f?.b?.(b, b)', '(function() {}?.())', - '((function() {})())', + '((function() {})?.())', '( f )?.( 0 )', '( (f) )?.( (0) )', '( f()() )?.(0)', @@ -133,64 +131,29 @@ ruleTester.run('func-call-spacing', rule, { invalid: [ // "never" ...[ - { - code: 'f ();', - output: 'f();', - }, - { - code: 'f (a, b);', - output: 'f(a, b);', - }, + { code: 'f ();', output: 'f();' }, + { code: 'f (a, b);', output: 'f(a, b);' }, { code: 'f.b ();', output: 'f.b();', - errors: [ - { - messageId: 'unexpectedWhitespace' as const, - column: 3, - }, - ], + errors: [{ messageId: 'unexpectedWhitespace' as const, column: 3 }], }, { code: 'f.b().c ();', output: 'f.b().c();', - errors: [ - { - messageId: 'unexpectedWhitespace' as const, - column: 7, - }, - ], - }, - { - code: 'f() ()', - output: 'f()()', - }, - { - code: '(function() {} ())', - output: '(function() {}())', - }, - { - code: 'var f = new Foo ()', - output: 'var f = new Foo()', - }, - { - code: 'f ( (0) )', - output: 'f( (0) )', - }, - { - code: 'f(0) (1)', - output: 'f(0)(1)', + errors: [{ messageId: 'unexpectedWhitespace' as const, column: 7 }], }, + { code: 'f() ()', output: 'f()()' }, + { code: '(function() {} ())', output: '(function() {}())' }, + { code: 'var f = new Foo ()', output: 'var f = new Foo()' }, + { code: 'f ( (0) )', output: 'f( (0) )' }, + { code: 'f(0) (1)', output: 'f(0)(1)' }, { code: 'f ();\n t ();', output: 'f();\n t();', errors: [ - { - messageId: 'unexpectedWhitespace' as const, - }, - { - messageId: 'unexpectedWhitespace' as const, - }, + { messageId: 'unexpectedWhitespace' as const }, + { messageId: 'unexpectedWhitespace' as const }, ], }, @@ -207,11 +170,7 @@ this.decrement(request) `, output: null, // no change errors: [ - { - messageId: 'unexpectedWhitespace' as const, - line: 3, - column: 23, - }, + { messageId: 'unexpectedWhitespace' as const, line: 3, column: 23 }, ], }, { @@ -221,11 +180,7 @@ var a = foo `, output: null, // no change errors: [ - { - messageId: 'unexpectedWhitespace' as const, - line: 2, - column: 9, - }, + { messageId: 'unexpectedWhitespace' as const, line: 2, column: 9 }, ], }, { @@ -235,11 +190,7 @@ var a = foo `, output: null, // no change errors: [ - { - messageId: 'unexpectedWhitespace' as const, - line: 2, - column: 9, - }, + { messageId: 'unexpectedWhitespace' as const, line: 2, column: 9 }, ], }, { @@ -260,149 +211,72 @@ var a = foo }, ].map>(code => ({ options: ['never'], - errors: [ - { - messageId: 'unexpectedWhitespace', - }, - ], + errors: [{ messageId: 'unexpectedWhitespace' }], ...code, })), // "always" ...[ - { - code: 'f();', - output: 'f ();', - }, - { - code: 'f(a, b);', - output: 'f (a, b);', - }, - { - code: 'f() ()', - output: 'f () ()', - }, - { - code: 'var f = new Foo()', - output: 'var f = new Foo ()', - }, - { - code: 'f( (0) )', - output: 'f ( (0) )', - }, - { - code: 'f(0) (1)', - output: 'f (0) (1)', - }, + { code: 'f();', output: 'f ();' }, + { code: 'f(a, b);', output: 'f (a, b);' }, + { code: 'f() ()', output: 'f () ()' }, + { code: 'var f = new Foo()', output: 'var f = new Foo ()' }, + { code: 'f( (0) )', output: 'f ( (0) )' }, + { code: 'f(0) (1)', output: 'f (0) (1)' }, ].map>(code => ({ options: ['always'], - errors: [ - { - messageId: 'missing', - }, - ], + errors: [{ messageId: 'missing' }], ...code, })), ...[ - { - code: 'f\n();', - output: 'f ();', - }, - { - code: 'f\n(a, b);', - output: 'f (a, b);', - }, + { code: 'f\n();', output: 'f ();' }, + { code: 'f\n(a, b);', output: 'f (a, b);' }, { code: 'f.b();', output: 'f.b ();', - errors: [ - { - messageId: 'missing' as const, - column: 3, - }, - ], - }, - { - code: 'f.b\n();', - output: 'f.b ();', + errors: [{ messageId: 'missing' as const, column: 3 }], }, + { code: 'f.b\n();', output: 'f.b ();' }, { code: 'f.b().c ();', output: 'f.b ().c ();', - errors: [ - { - messageId: 'missing' as const, - column: 3, - }, - ], - }, - { - code: 'f.b\n().c ();', - output: 'f.b ().c ();', - }, - { - code: 'f\n() ()', - output: 'f () ()', + errors: [{ messageId: 'missing' as const, column: 3 }], }, + { code: 'f.b\n().c ();', output: 'f.b ().c ();' }, + { code: 'f\n() ()', output: 'f () ()' }, { code: 'f\n()()', output: 'f () ()', errors: [ - { - messageId: 'unexpectedNewline' as const, - }, - { - messageId: 'missing' as const, - }, + { messageId: 'unexpectedNewline' as const }, + { messageId: 'missing' as const }, ], }, { code: '(function() {}())', output: '(function() {} ())', - errors: [ - { - messageId: 'missing' as const, - }, - ], + errors: [{ messageId: 'missing' as const }], }, { code: 'f();\n t();', output: 'f ();\n t ();', errors: [ - { - messageId: 'missing' as const, - }, - { - messageId: 'missing' as const, - }, + { messageId: 'missing' as const }, + { messageId: 'missing' as const }, ], }, - { - code: 'f\r();', - output: 'f ();', - }, + { code: 'f\r();', output: 'f ();' }, { code: 'f\u2028();', output: 'f ();', - errors: [ - { - messageId: 'unexpectedNewline' as const, - }, - ], + errors: [{ messageId: 'unexpectedNewline' as const }], }, { code: 'f\u2029();', output: 'f ();', - errors: [ - { - messageId: 'unexpectedNewline' as const, - }, - ], - }, - { - code: 'f\r\n();', - output: 'f ();', + errors: [{ messageId: 'unexpectedNewline' as const }], }, + { code: 'f\r\n();', output: 'f ();' }, ].map>(code => ({ options: ['always'], errors: [ @@ -418,67 +292,30 @@ var a = foo // "always", "allowNewlines": true ...[ - { - code: 'f();', - output: 'f ();', - }, - { - code: 'f(a, b);', - output: 'f (a, b);', - }, + { code: 'f();', output: 'f ();' }, + { code: 'f(a, b);', output: 'f (a, b);' }, { code: 'f.b();', output: 'f.b ();', - errors: [ - { - messageId: 'missing' as const, - column: 3, - }, - ], - }, - { - code: 'f.b().c ();', - output: 'f.b ().c ();', - }, - { - code: 'f() ()', - output: 'f () ()', - }, - { - code: '(function() {}())', - output: '(function() {} ())', - }, - { - code: 'var f = new Foo()', - output: 'var f = new Foo ()', - }, - { - code: 'f( (0) )', - output: 'f ( (0) )', - }, - { - code: 'f(0) (1)', - output: 'f (0) (1)', + errors: [{ messageId: 'missing' as const, column: 3 }], }, + { code: 'f.b().c ();', output: 'f.b ().c ();' }, + { code: 'f() ()', output: 'f () ()' }, + { code: '(function() {}())', output: '(function() {} ())' }, + { code: 'var f = new Foo()', output: 'var f = new Foo ()' }, + { code: 'f( (0) )', output: 'f ( (0) )' }, + { code: 'f(0) (1)', output: 'f (0) (1)' }, { code: 'f();\n t();', output: 'f ();\n t ();', errors: [ - { - messageId: 'missing' as const, - }, - { - messageId: 'missing' as const, - }, + { messageId: 'missing' as const }, + { messageId: 'missing' as const }, ], }, ].map>(code => ({ options: ['always', { allowNewlines: true }], - errors: [ - { - messageId: 'missing', - }, - ], + errors: [{ messageId: 'missing' }], ...code, })), @@ -494,33 +331,21 @@ var a = foo acc.push( { options: ['always', { allowNewlines: true }], - errors: [ - { - messageId: 'unexpectedWhitespace', - }, - ], + errors: [{ messageId: 'unexpectedWhitespace' }], code, // apply no fixers to it output: null, }, { options: ['always'], - errors: [ - { - messageId: 'unexpectedWhitespace', - }, - ], + errors: [{ messageId: 'unexpectedWhitespace' }], code, // apply no fixers to it output: null, }, { options: ['never'], - errors: [ - { - messageId: 'unexpectedWhitespace', - }, - ], + errors: [{ messageId: 'unexpectedWhitespace' }], code, // apply no fixers to it output: null, diff --git a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts index a2f5adba6703..9146084f2017 100644 --- a/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts +++ b/packages/eslint-plugin/tests/rules/member-ordering/member-ordering-alphabetically-order.test.ts @@ -1829,156 +1829,6 @@ class FooTestGetter { }, ], }, - - // default option + interface + wrong order within group and wrong group order + alphabetically - { - code: ` -interface Foo { - [a: string]: number; - - a: x; - b: x; - c: x; - - c(): void; - b(): void; - a(): void; - - (): Baz; - - new (): Bar; -} - `, - options: [ - { default: { memberTypes: defaultOrder, order: 'alphabetically' } }, - ], - errors: [ - { - messageId: 'incorrectOrder', - data: { - member: 'b', - beforeMember: 'c', - }, - }, - { - messageId: 'incorrectOrder', - data: { - member: 'a', - beforeMember: 'b', - }, - }, - { - messageId: 'incorrectGroupOrder', - data: { - name: 'call', - rank: 'field', - }, - }, - { - messageId: 'incorrectGroupOrder', - data: { - name: 'new', - rank: 'method', - }, - }, - ], - }, - - // default option + type literal + wrong order within group and wrong group order + alphabetically - { - code: ` -type Foo = { - [a: string]: number; - - a: x; - b: x; - c: x; - - c(): void; - b(): void; - a(): void; - - (): Baz; - - new (): Bar; -}; - `, - options: [ - { default: { memberTypes: defaultOrder, order: 'alphabetically' } }, - ], - errors: [ - { - messageId: 'incorrectOrder', - data: { - member: 'b', - beforeMember: 'c', - }, - }, - { - messageId: 'incorrectOrder', - data: { - member: 'a', - beforeMember: 'b', - }, - }, - { - messageId: 'incorrectGroupOrder', - data: { - name: 'call', - rank: 'field', - }, - }, - { - messageId: 'incorrectGroupOrder', - data: { - name: 'new', - rank: 'method', - }, - }, - ], - }, - - // default option + class + wrong order within group and wrong group order + alphabetically - { - code: ` -class Foo { - public static c: string = ''; - public static b: string = ''; - public static a: string; - - constructor() {} - - public d: string = ''; -} - `, - options: [ - { default: { memberTypes: defaultOrder, order: 'alphabetically' } }, - ], - errors: [ - { - messageId: 'incorrectOrder', - data: { - member: 'b', - beforeMember: 'c', - }, - }, - { - messageId: 'incorrectOrder', - data: { - member: 'a', - beforeMember: 'b', - }, - }, - { - messageId: 'incorrectGroupOrder', - data: { - name: 'd', - rank: 'public constructor', - }, - }, - ], - }, - // default option + class expression + wrong order within group and wrong group order + alphabetically { code: ` diff --git a/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts b/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts index 327bc77ab71e..7e1207a222d2 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention/cases/createTestCases.ts @@ -77,48 +77,26 @@ const IGNORED_FILTER = { regex: /.gnored/.source, }; -type Cases = { - code: string[]; - options: Omit; -}[]; +type Cases = { code: string[]; options: Omit }[]; export function createTestCases(cases: Cases): void { - const ruleTester = new RuleTester({ - parser: '@typescript-eslint/parser', - }); - - ruleTester.run('naming-convention', rule, { - invalid: createInvalidTestCases(), - valid: createValidTestCases(), - }); - - function createValidTestCases(): TSESLint.ValidTestCase[] { - const newCases: TSESLint.ValidTestCase[] = []; - - for (const test of cases) { - for (const [formatLoose, names] of Object.entries(formatTestNames)) { - const format = [formatLoose as PredefinedFormatsString]; - for (const name of names.valid) { + const createValidTestCases = (): TSESLint.ValidTestCase[] => + cases.flatMap(test => + Object.entries(formatTestNames).flatMap(([formatLoose, names]) => + names.valid.flatMap(name => { + const format = [formatLoose as PredefinedFormatsString]; const createCase = ( preparedName: string, options: Selector, ): TSESLint.ValidTestCase => ({ - options: [ - { - ...options, - filter: IGNORED_FILTER, - }, - ], + options: [{ ...options, filter: IGNORED_FILTER }], code: `// ${JSON.stringify(options)}\n${test.code .map(code => code.replace(REPLACE_REGEX, preparedName)) .join('\n')}`, }); - newCases.push( - createCase(name, { - ...test.options, - format, - }), + return [ + createCase(name, { ...test.options, format }), // leadingUnderscore createCase(name, { @@ -171,11 +149,6 @@ export function createTestCases(cases: Cases): void { format, leadingUnderscore: 'allowSingleOrDouble', }), - createCase(name, { - ...test.options, - format, - leadingUnderscore: 'allowSingleOrDouble', - }), // trailingUnderscore createCase(name, { @@ -228,11 +201,6 @@ export function createTestCases(cases: Cases): void { format, trailingUnderscore: 'allowSingleOrDouble', }), - createCase(name, { - ...test.options, - format, - trailingUnderscore: 'allowSingleOrDouble', - }), // prefix createCase(`MyPrefix${name}`, { @@ -257,13 +225,10 @@ export function createTestCases(cases: Cases): void { format, suffix: ['MySuffix1', 'MySuffix2'], }), - ); - } - } - } - - return newCases; - } + ]; + }), + ), + ); function createInvalidTestCases(): TSESLint.InvalidTestCase< MessageIds, @@ -480,4 +445,13 @@ export function createTestCases(cases: Cases): void { return newCases; } + + const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + }); + + ruleTester.run('naming-convention', rule, { + invalid: createInvalidTestCases(), + valid: createValidTestCases(), + }); } diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 16f0c035c27a..951e82f944d9 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -130,10 +130,6 @@ x === null ? x : y; `, ` declare const x: string | null | unknown; -x === null ? x : y; - `, - ` -declare const x: string | undefined; x === null ? x : y; `, ].map(code => ({ @@ -371,7 +367,7 @@ undefined !== x ? x : y; `, ` declare const x: string | undefined; -undefined === x ? y : x; +x === undefined ? y : x; `, ` declare const x: string | undefined; @@ -387,7 +383,7 @@ null !== x ? x : y; `, ` declare const x: string | null; -null === x ? y : x; +x === null ? y : x; `, ` declare const x: string | null; diff --git a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts index fbab86a20910..a55b54572f55 100644 --- a/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-optional-chain/prefer-optional-chain.test.ts @@ -1,6 +1,7 @@ import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../../src/rules/prefer-optional-chain'; +import { dedupeTestCases } from '../../dedupeTestCases'; import { getFixturesRootDir } from '../../RuleTester'; import { BaseCases, identity } from './base-cases'; @@ -67,10 +68,7 @@ describe('|| {}', () => { column: 1, endColumn: 16, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'foo?.bar;', - }, + { messageId: 'optionalChainSuggest', output: 'foo?.bar;' }, ], }, ], @@ -83,10 +81,7 @@ describe('|| {}', () => { column: 1, endColumn: 18, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'foo?.bar;', - }, + { messageId: 'optionalChainSuggest', output: 'foo?.bar;' }, ], }, ], @@ -163,10 +158,7 @@ describe('|| {}', () => { column: 1, endColumn: 21, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'foo.bar?.[baz];', - }, + { messageId: 'optionalChainSuggest', output: 'foo.bar?.[baz];' }, ], }, ], @@ -316,10 +308,7 @@ describe('|| {}', () => { column: 1, endColumn: 16, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'foo?.bar;', - }, + { messageId: 'optionalChainSuggest', output: 'foo?.bar;' }, ], }, ], @@ -332,10 +321,7 @@ describe('|| {}', () => { column: 1, endColumn: 18, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'foo?.bar;', - }, + { messageId: 'optionalChainSuggest', output: 'foo?.bar;' }, ], }, ], @@ -412,10 +398,7 @@ describe('|| {}', () => { column: 1, endColumn: 21, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'foo.bar?.[baz];', - }, + { messageId: 'optionalChainSuggest', output: 'foo.bar?.[baz];' }, ], }, ], @@ -549,10 +532,7 @@ describe('|| {}', () => { column: 1, endColumn: 18, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '(a > b)?.bar;', - }, + { messageId: 'optionalChainSuggest', output: '(a > b)?.bar;' }, ], }, ], @@ -629,10 +609,7 @@ describe('|| {}', () => { column: 1, endColumn: 21, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '(a << b)?.bar;', - }, + { messageId: 'optionalChainSuggest', output: '(a << b)?.bar;' }, ], }, ], @@ -645,10 +622,7 @@ describe('|| {}', () => { column: 1, endColumn: 23, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '(foo ** 2)?.bar;', - }, + { messageId: 'optionalChainSuggest', output: '(foo ** 2)?.bar;' }, ], }, ], @@ -661,10 +635,7 @@ describe('|| {}', () => { column: 1, endColumn: 21, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '(foo ** 2)?.bar;', - }, + { messageId: 'optionalChainSuggest', output: '(foo ** 2)?.bar;' }, ], }, ], @@ -677,10 +648,7 @@ describe('|| {}', () => { column: 1, endColumn: 18, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '(foo++)?.bar;', - }, + { messageId: 'optionalChainSuggest', output: '(foo++)?.bar;' }, ], }, ], @@ -693,10 +661,7 @@ describe('|| {}', () => { column: 1, endColumn: 17, suggestions: [ - { - messageId: 'optionalChainSuggest', - output: '(+foo)?.bar;', - }, + { messageId: 'optionalChainSuggest', output: '(+foo)?.bar;' }, ], }, ], @@ -707,10 +672,7 @@ describe('|| {}', () => { { messageId: 'preferOptionalChain', suggestions: [ - { - messageId: 'optionalChainSuggest', - output: 'this?.foo;', - }, + { messageId: 'optionalChainSuggest', output: 'this?.foo;' }, ], }, ], @@ -881,66 +843,42 @@ describe('hand-crafted cases', () => { declare const x: any; x && x.length; `, - options: [ - { - checkAny: false, - }, - ], + options: [{ checkAny: false }], }, { code: ` declare const x: bigint; x && x.length; `, - options: [ - { - checkBigInt: false, - }, - ], + options: [{ checkBigInt: false }], }, { code: ` declare const x: boolean; x && x.length; `, - options: [ - { - checkBoolean: false, - }, - ], + options: [{ checkBoolean: false }], }, { code: ` declare const x: number; x && x.length; `, - options: [ - { - checkNumber: false, - }, - ], + options: [{ checkNumber: false }], }, { code: ` declare const x: string; x && x.length; `, - options: [ - { - checkString: false, - }, - ], + options: [{ checkString: false }], }, { code: ` declare const x: unknown; x && x.length; `, - options: [ - { - checkUnknown: false, - }, - ], + options: [{ checkUnknown: false }], }, '(x = {}) && (x.y = true) != null && x.y.toString();', "('x' as `${'x'}`) && ('x' as `${'x'}`).length;", @@ -988,14 +926,8 @@ describe('hand-crafted cases', () => { code: noFormat`foo && foo.bar && foo.bar.baz || baz && baz.bar && baz.bar.foo`, output: 'foo?.bar?.baz || baz?.bar?.foo', errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - { - messageId: 'preferOptionalChain', - suggestions: null, - }, + { messageId: 'preferOptionalChain', suggestions: null }, + { messageId: 'preferOptionalChain', suggestions: null }, ], }, // case with inconsistent checks should "break" the chain @@ -1003,12 +935,7 @@ describe('hand-crafted cases', () => { code: 'foo && foo.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', output: 'foo?.bar != null && foo.bar.baz !== undefined && foo.bar.baz.buzz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1022,141 +949,72 @@ describe('hand-crafted cases', () => { foo.bar.baz.qux !== undefined && foo.bar.baz.qux.buzz; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // ensure essential whitespace isn't removed { code: 'foo && foo.bar(baz => );', output: 'foo?.bar(baz => );', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], + parserOptions: { ecmaFeatures: { jsx: true } }, filename: 'react.tsx', }, { code: 'foo && foo.bar(baz => typeof baz);', output: 'foo?.bar(baz => typeof baz);', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: "foo && foo['some long string'] && foo['some long string'].baz;", output: "foo?.['some long string']?.baz;", - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo[`some long string`] && foo[`some long string`].baz;', output: 'foo?.[`some long string`]?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo[`some ${long} string`] && foo[`some ${long} string`].baz;', output: 'foo?.[`some ${long} string`]?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // complex computed properties should be handled correctly { code: 'foo && foo[bar as string] && foo[bar as string].baz;', output: 'foo?.[bar as string]?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo[1 + 2] && foo[1 + 2].baz;', output: 'foo?.[1 + 2]?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo[typeof bar] && foo[typeof bar].baz;', output: 'foo?.[typeof bar]?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo.bar(a) && foo.bar(a, b).baz;', output: 'foo?.bar(a) && foo.bar(a, b).baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo() && foo()(bar);', output: 'foo()?.(bar);', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // type parameters are considered { code: 'foo && foo() && foo().bar;', output: 'foo?.()?.bar;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo() && foo().bar;', output: 'foo?.() && foo().bar;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // should preserve comments in a call expression { @@ -1170,76 +1028,41 @@ describe('hand-crafted cases', () => { // comment2 b, ); `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // ensure binary expressions that are the last expression do not get removed // these get autofixers because the trailing binary means the type doesn't matter { code: 'foo && foo.bar != null;', output: 'foo?.bar != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo.bar != undefined;', output: 'foo?.bar != undefined;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo.bar != null && baz;', output: 'foo?.bar != null && baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // case with this keyword at the start of expression { code: 'this.bar && this.bar.baz;', output: 'this.bar?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // other weird cases { code: 'foo && foo?.();', output: 'foo?.();', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo.bar && foo.bar?.();', output: 'foo.bar?.();', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo && foo.bar(baz => );', @@ -1252,77 +1075,42 @@ describe('hand-crafted cases', () => { suggestions: null, }, ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, + parserOptions: { ecmaFeatures: { jsx: true } }, filename: 'react.tsx', }, // case with this keyword at the start of expression { code: '!this.bar || !this.bar.baz;', output: '!this.bar?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!a.b || !a.b();', output: '!a.b?.();', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!foo.bar || !foo.bar.baz;', output: '!foo.bar?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!foo[bar] || !foo[bar]?.[baz];', output: '!foo[bar]?.[baz];', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!foo || !foo?.bar.baz;', output: '!foo?.bar.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // two errors { code: '(!foo || !foo.bar || !foo.bar.baz) && (!baz || !baz.bar || !baz.bar.foo);', output: '(!foo?.bar?.baz) && (!baz?.bar?.foo);', errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - { - messageId: 'preferOptionalChain', - suggestions: null, - }, + { messageId: 'preferOptionalChain', suggestions: null }, + { messageId: 'preferOptionalChain', suggestions: null }, ], }, { @@ -1355,73 +1143,38 @@ describe('hand-crafted cases', () => { { code: 'import.meta && import.meta?.baz;', output: 'import.meta?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!import.meta || !import.meta?.baz;', output: '!import.meta?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'import.meta && import.meta?.() && import.meta?.().baz;', output: 'import.meta?.()?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // non-null expressions { code: '!foo() || !foo().bar;', output: '!foo()?.bar;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!foo!.bar || !foo!.bar.baz;', output: '!foo!.bar?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!foo!.bar!.baz || !foo!.bar!.baz!.paz;', output: '!foo!.bar!.baz?.paz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: '!foo.bar!.baz || !foo.bar!.baz!.paz;', output: '!foo.bar!.baz?.paz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1447,12 +1200,7 @@ describe('hand-crafted cases', () => { { code: 'foo != null && foo.bar != null;', output: 'foo?.bar != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1484,124 +1232,67 @@ describe('hand-crafted cases', () => { declare const foo: { bar: string | null } | null; foo?.bar != null; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // https://github.com/typescript-eslint/typescript-eslint/issues/6332 { code: 'unrelated != null && foo != null && foo.bar != null;', output: 'unrelated != null && foo?.bar != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'unrelated1 != null && unrelated2 != null && foo != null && foo.bar != null;', output: 'unrelated1 != null && unrelated2 != null && foo?.bar != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // https://github.com/typescript-eslint/typescript-eslint/issues/1461 { code: 'foo1 != null && foo1.bar != null && foo2 != null && foo2.bar != null;', output: 'foo1?.bar != null && foo2?.bar != null;', errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - { - messageId: 'preferOptionalChain', - suggestions: null, - }, + { messageId: 'preferOptionalChain', suggestions: null }, + { messageId: 'preferOptionalChain', suggestions: null }, ], }, { code: 'foo && foo.a && bar && bar.a;', output: 'foo?.a && bar?.a;', errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - { - messageId: 'preferOptionalChain', - suggestions: null, - }, + { messageId: 'preferOptionalChain', suggestions: null }, + { messageId: 'preferOptionalChain', suggestions: null }, ], }, // randomly placed optional chain tokens are ignored { code: 'foo.bar.baz != null && foo?.bar?.baz.bam != null;', output: 'foo.bar.baz?.bam != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo?.bar.baz != null && foo.bar?.baz.bam != null;', output: 'foo?.bar.baz?.bam != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo?.bar?.baz != null && foo.bar.baz.bam != null;', output: 'foo?.bar?.baz?.bam != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // randomly placed non-null assertions are retained as long as they're in an earlier operand { code: 'foo.bar.baz != null && foo!.bar!.baz.bam != null;', output: 'foo.bar.baz?.bam != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo!.bar.baz != null && foo.bar!.baz.bam != null;', output: 'foo!.bar.baz?.bam != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: 'foo!.bar!.baz != null && foo.bar.baz.bam != null;', output: 'foo!.bar!.baz?.bam != null;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // mixed binary checks are followed and flagged { @@ -1621,12 +1312,7 @@ describe('hand-crafted cases', () => { output: ` a?.b?.c?.d?.e?.f?.g?.h; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1645,12 +1331,7 @@ describe('hand-crafted cases', () => { output: ` !a?.b?.c?.d?.e?.f?.g?.h; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1669,23 +1350,13 @@ describe('hand-crafted cases', () => { output: ` !a?.b?.c?.d?.e?.f?.g?.h; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // yoda checks are flagged { code: 'undefined !== foo && null !== foo && null != foo.bar && foo.bar.baz;', output: 'foo?.bar?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1697,12 +1368,7 @@ describe('hand-crafted cases', () => { output: ` foo?.bar?.baz; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1714,12 +1380,7 @@ describe('hand-crafted cases', () => { output: ` null != foo?.bar?.baz; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // We should retain the split strict equals check if it's the last operand { @@ -1830,12 +1491,7 @@ describe('hand-crafted cases', () => { undefined !== foo?.bar?.baz && null !== foo.bar.baz; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1849,23 +1505,13 @@ describe('hand-crafted cases', () => { foo?.bar?.baz !== undefined && foo.bar.baz !== null; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // await { code: '(await foo).bar && (await foo).bar.baz;', output: '(await foo).bar?.baz;', - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // TODO - should we handle this case and expand the range, or should we leave this as is? { @@ -1885,12 +1531,7 @@ describe('hand-crafted cases', () => { a?.b?.c?.d?.e?.f?.g == null || a.b.c.d.e.f.g.h; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { @@ -1902,12 +1543,7 @@ describe('hand-crafted cases', () => { declare const foo: { bar: number } | null | undefined; foo?.bar != null; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1918,12 +1554,7 @@ describe('hand-crafted cases', () => { declare const foo: { bar: number } | undefined; typeof foo?.bar !== 'undefined'; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -1934,12 +1565,7 @@ describe('hand-crafted cases', () => { declare const foo: { bar: number } | undefined; 'undefined' !== typeof foo?.bar; `, - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, // requireNullish @@ -2116,12 +1742,7 @@ describe('hand-crafted cases', () => { true, }, ], - errors: [ - { - messageId: 'preferOptionalChain', - suggestions: null, - }, - ], + errors: [{ messageId: 'preferOptionalChain', suggestions: null }], }, { code: ` @@ -2180,11 +1801,7 @@ describe('hand-crafted cases', () => { globalThis?.Array(); } `, - errors: [ - { - messageId: 'preferOptionalChain', - }, - ], + errors: [{ messageId: 'preferOptionalChain' }], }, { code: ` @@ -2215,9 +1832,7 @@ describe('base cases', () => { ruleTester.run('prefer-optional-chain', rule, { valid: [], invalid: [ - ...BaseCases({ - operator: '&&', - }), + ...BaseCases({ operator: '&&' }), // it should ignore parts of the expression that aren't part of the expression chain ...BaseCases({ operator: '&&', @@ -2401,24 +2016,25 @@ describe('base cases', () => { describe('should ignore spacing sanity checks', () => { ruleTester.run('prefer-optional-chain', rule, { valid: [], - invalid: [ + // One base case does not match the mutator, so we have to dedupe it + invalid: dedupeTestCases( // it should ignore whitespace in the expressions - ...BaseCases({ + BaseCases({ operator: '&&', mutateCode: c => c.replace(/\./g, '. '), // note - the rule will use raw text for computed expressions - so we // need to ensure that the spacing for the computed member // expressions is retained for correct fixer matching mutateOutput: c => - c.replace(/(\[.+\])/g, m => m.replace(/\./g, '. ')), + c.replace(/(\[.+])/g, m => m.replace(/\./g, '. ')), }), - ...BaseCases({ + BaseCases({ operator: '&&', mutateCode: c => c.replace(/\./g, '.\n'), mutateOutput: c => - c.replace(/(\[.+\])/g, m => m.replace(/\./g, '.\n')), + c.replace(/(\[.+])/g, m => m.replace(/\./g, '.\n')), }), - ], + ), }); }); }); diff --git a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts index d392a5232fd6..4a563263d51b 100644 --- a/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-readonly-parameter-types.test.ts @@ -7,6 +7,7 @@ import type { InferOptionsTypeFromRule, } from '../../src/util'; import { readonlynessOptionsDefaults } from '../../src/util'; +import { dedupeTestCases } from '../dedupeTestCases'; import { getFixturesRootDir } from '../RuleTester'; type MessageIds = InferMessageIdsTypeFromRule; @@ -256,11 +257,7 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { ) {} } `, - options: [ - { - checkParameterProperties: true, - }, - ], + options: [{ checkParameterProperties: true }], }, { code: ` @@ -273,11 +270,7 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { ) {} } `, - options: [ - { - checkParameterProperties: false, - }, - ], + options: [{ checkParameterProperties: false }], }, // type functions @@ -482,22 +475,25 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { ], invalid: [ // arrays - ...arrays.map>(baseType => { - const type = baseType - .replace(/readonly /g, '') - .replace(/Readonly<(.+?)>/g, '$1') - .replace(/ReadonlyArray/g, 'Array'); - return { - code: `function foo(arg: ${type}) {}`, - errors: [ - { - messageId: 'shouldBeReadonly', - column: 14, - endColumn: 19 + type.length, - }, - ], - }; - }), + // Removing readonly causes duplicates + ...dedupeTestCases( + arrays.map>(baseType => { + const type = baseType + .replace(/readonly /g, '') + .replace(/Readonly<(.+?)>/g, '$1') + .replace(/ReadonlyArray/g, 'Array'); + return { + code: `function foo(arg: ${type}) {}`, + errors: [ + { + messageId: 'shouldBeReadonly', + column: 14, + endColumn: 19 + type.length, + }, + ], + }; + }), + ), // nested arrays { code: 'function foo(arg: readonly string[][]) {}', @@ -648,11 +644,7 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { ) {} } `, - options: [ - { - checkParameterProperties: true, - }, - ], + options: [{ checkParameterProperties: true }], errors: [ { messageId: 'shouldBeReadonly', @@ -696,11 +688,7 @@ ruleTester.run('prefer-readonly-parameter-types', rule, { ) {} } `, - options: [ - { - checkParameterProperties: false, - }, - ], + options: [{ checkParameterProperties: false }], errors: [ { messageId: 'shouldBeReadonly', diff --git a/packages/rule-tester/package.json b/packages/rule-tester/package.json index dcb82d9a056e..76c51382d89b 100644 --- a/packages/rule-tester/package.json +++ b/packages/rule-tester/package.json @@ -51,6 +51,7 @@ "@typescript-eslint/typescript-estree": "7.11.0", "@typescript-eslint/utils": "7.11.0", "ajv": "^6.12.6", + "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", "semver": "^7.6.0" }, @@ -60,6 +61,7 @@ }, "devDependencies": { "@jest/types": "29.6.3", + "@types/json-stable-stringify-without-jsonify": "^1.0.2", "@types/lodash.merge": "4.6.9", "@typescript-eslint/parser": "7.11.0", "chai": "^4.4.1", diff --git a/packages/rule-tester/src/RuleTester.ts b/packages/rule-tester/src/RuleTester.ts index b4911ef6febc..2f3ee8798722 100644 --- a/packages/rule-tester/src/RuleTester.ts +++ b/packages/rule-tester/src/RuleTester.ts @@ -20,6 +20,7 @@ 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 stringify from 'json-stable-stringify-without-jsonify'; import merge from 'lodash.merge'; import { TestFramework } from './TestFramework'; @@ -40,6 +41,7 @@ import { getRuleOptionsSchema } from './utils/getRuleOptionsSchema'; import { hasOwnProperty } from './utils/hasOwnProperty'; import { getPlaceholderMatcher, interpolate } from './utils/interpolate'; import { isReadonlyArray } from './utils/isReadonlyArray'; +import { isSerializable } from './utils/serialization'; import * as SourceCodeFixer from './utils/SourceCodeFixer'; import { emitLegacyRuleAPIWarning, @@ -390,6 +392,9 @@ export class RuleTester extends TestFramework { ); } + const seenValidTestCases = new Set(); + const seenInvalidTestCases = new Set(); + if (typeof rule === 'function') { emitLegacyRuleAPIWarning(ruleName); } @@ -439,7 +444,12 @@ export class RuleTester extends TestFramework { return valid.name; })(); constructor[getTestMethod(valid)](sanitize(testName), () => { - this.#testValidTemplate(ruleName, rule, valid); + this.#testValidTemplate( + ruleName, + rule, + valid, + seenValidTestCases, + ); }); }); }); @@ -455,7 +465,12 @@ export class RuleTester extends TestFramework { return invalid.name; })(); constructor[getTestMethod(invalid)](sanitize(name), () => { - this.#testInvalidTemplate(ruleName, rule, invalid); + this.#testInvalidTemplate( + ruleName, + rule, + invalid, + seenInvalidTestCases, + ); }); }); }); @@ -671,6 +686,7 @@ export class RuleTester extends TestFramework { ruleName: string, rule: RuleModule, itemIn: ValidTestCase | string, + seenValidTestCases: Set, ): void { const item: ValidTestCase = typeof itemIn === 'object' ? itemIn : { code: itemIn }; @@ -686,6 +702,8 @@ export class RuleTester extends TestFramework { ); } + checkDuplicateTestCase(item, seenValidTestCases); + const result = this.runRuleForItem(ruleName, rule, item); const messages = result.messages; @@ -713,6 +731,7 @@ export class RuleTester extends TestFramework { ruleName: string, rule: RuleModule, item: InvalidTestCase, + seenInvalidTestCases: Set, ): void { assert.ok( typeof item.code === 'string', @@ -733,6 +752,8 @@ export class RuleTester extends TestFramework { assert.fail('Invalid cases must have at least one error'); } + checkDuplicateTestCase(item, seenInvalidTestCases); + const ruleHasMetaMessages = hasOwnProperty(rule, 'meta') && hasOwnProperty(rule.meta, 'messages'); const friendlyIDList = ruleHasMetaMessages @@ -1115,6 +1136,30 @@ function assertASTDidntChange(beforeAST: unknown, afterAST: unknown): void { assert.deepStrictEqual(beforeAST, afterAST, 'Rule should not modify AST.'); } +/** + * Check if this test case is a duplicate of one we have seen before. + */ +function checkDuplicateTestCase( + item: unknown, + seenTestCases: Set, +): void { + if (!isSerializable(item)) { + /* + * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check. + * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions. + */ + return; + } + + const serializedTestCase = stringify(item); + + assert( + !seenTestCases.has(serializedTestCase), + 'detected duplicate test case', + ); + seenTestCases.add(serializedTestCase); +} + /** * Asserts that the message matches its expected value. If the expected * value is a regular expression, it is checked against the actual diff --git a/packages/rule-tester/src/utils/serialization.ts b/packages/rule-tester/src/utils/serialization.ts new file mode 100644 index 000000000000..92fe3f6b0eb3 --- /dev/null +++ b/packages/rule-tester/src/utils/serialization.ts @@ -0,0 +1,42 @@ +/** + * Check if a value is a primitive or plain object created by the Object constructor. + */ +function isSerializablePrimitiveOrPlainObject(val: unknown): boolean { + return ( + // eslint-disable-next-line eqeqeq + val === null || + typeof val === 'string' || + typeof val === 'boolean' || + typeof val === 'number' || + (typeof val === 'object' && val.constructor === Object) || + Array.isArray(val) + ); +} + +/** + * Check if a value is serializable. + * Functions or objects like RegExp cannot be serialized by JSON.stringify(). + * Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript + */ +export function isSerializable(val: unknown): boolean { + if (!isSerializablePrimitiveOrPlainObject(val)) { + return false; + } + if (typeof val === 'object') { + const valAsObj = val as Record; + for (const property in valAsObj) { + // TODO(#9028): use `Object.hasOwn` (used in eslint@9) once we upgrade to eslint@9 + if (Object.prototype.hasOwnProperty.call(valAsObj, property)) { + if (!isSerializablePrimitiveOrPlainObject(valAsObj[property])) { + return false; + } + if (typeof valAsObj[property] === 'object') { + if (!isSerializable(valAsObj[property])) { + return false; + } + } + } + } + } + return true; +} diff --git a/packages/rule-tester/tests/eslint-base/eslint-base.test.js b/packages/rule-tester/tests/eslint-base/eslint-base.test.js index 260dcc542970..a50654f922aa 100644 --- a/packages/rule-tester/tests/eslint-base/eslint-base.test.js +++ b/packages/rule-tester/tests/eslint-base/eslint-base.test.js @@ -3063,4 +3063,299 @@ describe("RuleTester", () => { }); + describe("duplicate test cases", () => { + describe("valid test cases", () => { + it("throws with duplicate string test cases", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: {}, + create() { + return {}; + } + }, { + valid: ["foo", "foo"], + invalid: [] + }); + }, "detected duplicate test case"); + }); + + it("throws with duplicate object test cases", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: {}, + create() { + return {}; + } + }, { + valid: [{ code: "foo" }, { code: "foo" }], + invalid: [] + }); + }, "detected duplicate test case"); + }); + + it("throws with string and object test cases", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: {}, + create() { + return {}; + } + }, { + valid: ["foo", { code: "foo" }], + invalid: [] + }); + }, "detected duplicate test case"); + }); + + it("ignores the name property", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: {}, + create() { + return {}; + } + }, { + valid: [{ code: "foo" }, { name: "bar", code: "foo" }], + invalid: [] + }); + }, "detected duplicate test case"); + }); + + it("does not ignore top level test case properties nested in other test case properties", () => { + ruleTester.run("foo", { + meta: { schema: [{ type: "object" }] }, + create() { + return {}; + } + }, { + valid: [{ options: [{ name: "foo" }], name: "foo", code: "same" }, { options: [{ name: "bar" }], name: "bar", code: "same" }], + invalid: [] + }); + }); + + it("does not throw an error for defining the same test case in different run calls", () => { + const rule = { + meta: {}, + create() { + return {}; + } + }; + + ruleTester.run("foo", rule, { + valid: ["foo"], + invalid: [] + }); + + ruleTester.run("foo", rule, { + valid: ["foo"], + invalid: [] + }); + }); + }); + + describe("invalid test cases", () => { + it("throws with duplicate object test cases", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: {}, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }] }, + { code: "const x = 123;", errors: [{ message: "foo bar" }] } + ] + }); + }, "detected duplicate test case"); + }); + + it("throws with duplicate object test cases when options is a primitive", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: { schema: false }, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }], options: ["abc"] }, + { code: "const x = 123;", errors: [{ message: "foo bar" }], options: ["abc"] } + ] + }); + }, "detected duplicate test case"); + }); + + it("throws with duplicate object test cases when options is a nested serializable object", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: { schema: false }, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }], options: [{ foo: [{ a: true, b: [1, 2, 3] }] }] }, + { code: "const x = 123;", errors: [{ message: "foo bar" }], options: [{ foo: [{ a: true, b: [1, 2, 3] }] }] } + ] + }); + }, "detected duplicate test case"); + }); + + it("throws with duplicate object test cases even when property order differs", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: {}, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }] }, + { errors: [{ message: "foo bar" }], code: "const x = 123;" } + ] + }); + }, "detected duplicate test case"); + }); + + it("ignores duplicate test case when non-serializable property present (settings)", () => { + ruleTester.run("foo", { + meta: {}, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }], settings: { foo: /abc/u } }, + { code: "const x = 123;", errors: [{ message: "foo bar" }], settings: { foo: /abc/u } } + ] + }); + }); + + it("ignores duplicate test case when non-serializable property present (languageOptions.parserOptions)", () => { + ruleTester.run("foo", { + meta: {}, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }], languageOptions: { parserOptions: { foo: /abc/u } } }, + { code: "const x = 123;", errors: [{ message: "foo bar" }], languageOptions: { parserOptions: { foo: /abc/u } } } + ] + }); + }); + + it("ignores duplicate test case when non-serializable property present (plugins)", () => { + ruleTester.run("foo", { + meta: {}, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }], plugins: { foo: /abc/u } }, + { code: "const x = 123;", errors: [{ message: "foo bar" }], plugins: { foo: /abc/u } } + ] + }); + }); + + it("ignores duplicate test case when non-serializable property present (options)", () => { + ruleTester.run("foo", { + meta: { schema: false }, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: ["foo"], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }], options: [{ foo: /abc/u }] }, + { code: "const x = 123;", errors: [{ message: "foo bar" }], options: [{ foo: /abc/u }] } + ] + }); + }); + + it("detects duplicate test cases even if the error matchers differ", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: { schema: false }, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: [], + invalid: [ + { code: "const x = 123;", errors: [{ message: "foo bar" }] }, + { code: "const x = 123;", errors: 1 } + ] + }); + }, "detected duplicate test case"); + }); + + it("detects duplicate test cases even if the presence of the output property differs", () => { + assert.throws(() => { + ruleTester.run("foo", { + meta: { schema: false }, + create(context) { + return { + VariableDeclaration(node) { + context.report(node, "foo bar"); + } + }; + } + }, { + valid: [], + invalid: [ + { code: "const x = 123;", errors: 1 }, + { code: "const x = 123;", errors: 1, output: null } + ] + }); + }, "detected duplicate test case"); + }); + }); + }); + }); diff --git a/yarn.lock b/yarn.lock index 4c5db221cdfe..8dddf2e9270f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5231,6 +5231,13 @@ __metadata: languageName: node linkType: hard +"@types/json-stable-stringify-without-jsonify@npm:^1.0.2": + version: 1.0.2 + resolution: "@types/json-stable-stringify-without-jsonify@npm:1.0.2" + checksum: b8822ef38b1e845cca8151ef2baf5c99bc935364e94317b91eb1ffabb9280a0debd791b3b450f99e15bd121c0ecbecae926095b9f6b169e95a4659b4eb59f90f + languageName: node + linkType: hard + "@types/json5@npm:^0.0.29": version: 0.0.29 resolution: "@types/json5@npm:0.0.29" @@ -5694,6 +5701,7 @@ __metadata: resolution: "@typescript-eslint/rule-tester@workspace:packages/rule-tester" dependencies: "@jest/types": 29.6.3 + "@types/json-stable-stringify-without-jsonify": ^1.0.2 "@types/lodash.merge": 4.6.9 "@typescript-eslint/parser": 7.11.0 "@typescript-eslint/typescript-estree": 7.11.0 @@ -5703,6 +5711,7 @@ __metadata: eslint-visitor-keys: ^4.0.0 espree: ^10.0.1 esprima: ^4.0.1 + json-stable-stringify-without-jsonify: ^1.0.1 lodash.merge: 4.6.2 mocha: ^10.4.0 semver: ^7.6.0