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 pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy