From 1052e4c0ac7405b42367c684fd5957d67381f4bd Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 1 Dec 2024 22:15:16 +0200 Subject: [PATCH 1/9] initial implementation --- .../src/rules/no-base-to-string.ts | 25 +- .../tests/rules/no-base-to-string.test.ts | 295 ++++++++++++++++++ 2 files changed, 312 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index f7c012eeb21f..8a0cf23daf64 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -94,7 +94,7 @@ export default createRule({ node: TSESTree.Node, type: ts.Type, ): void { - const certainty = collectJoinCertainty(type); + const certainty = collectArrayToStringCertainty(type); if (certainty === Usefulness.Always) { return; @@ -141,13 +141,16 @@ export default createRule({ return Usefulness.Never; } - function collectJoinCertainty(type: ts.Type): Usefulness { + function collectArrayToStringCertainty(type: ts.Type): Usefulness { if (tsutils.isUnionType(type)) { - return collectUnionTypeCertainty(type, collectJoinCertainty); + return collectUnionTypeCertainty(type, collectArrayToStringCertainty); } if (tsutils.isIntersectionType(type)) { - return collectIntersectionTypeCertainty(type, collectJoinCertainty); + return collectIntersectionTypeCertainty( + type, + collectArrayToStringCertainty, + ); } if (checker.isTupleType(type)) { @@ -184,6 +187,10 @@ export default createRule({ return Usefulness.Always; } + if (checker.isArrayType(type) || checker.isTupleType(type)) { + return collectArrayToStringCertainty(type); + } + // Patch for old version TypeScript, the Boolean type definition missing toString() if ( type.flags & ts.TypeFlags.Boolean || @@ -197,10 +204,12 @@ export default createRule({ } if ( - declarations.every( - ({ parent }) => - !ts.isInterfaceDeclaration(parent) || parent.name.text !== 'Object', - ) + declarations.every(({ parent }) => { + return ( + !ts.isInterfaceDeclaration(parent) || + (parent.name.text !== 'Object' && parent.name.text !== 'Array') + ); + }) ) { return Usefulness.Always; } diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts index df1f3d4a6979..ce929bf28ded 100644 --- a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -159,16 +159,38 @@ String({}); ` ['foo', 'bar'].join(''); `, + ` +String(['foo', 'bar']); + `, + ` +\`\${['foo', 'bar']}\`; + `, ` ([{}, 'bar'] as string[]).join(''); `, ` +String([{}, 'bar'] as string[]); + `, + ` +\`\${[{}, 'bar'] as string[]}\`; + `, + ` function foo(array: T[]) { return array.join(); } `, ` +function foo(array: T[]) { + return String(array); +} + `, + ` +function foo(array: T[]) { + return \`\${array}\`; +} + `, + ` class Foo { toString() { return ''; @@ -188,12 +210,21 @@ declare const array: string[]; array.join(''); `, ` +declare const array: string[]; +String(array); + `, + ` class Foo {} declare const array: (string & Foo)[]; array.join(''); `, ` class Foo {} +declare const array: (string & Foo)[]; +String(array); + `, + ` +class Foo {} class Bar {} declare const array: (string & Foo)[] | (string & Bar)[]; array.join(''); @@ -201,20 +232,43 @@ array.join(''); ` class Foo {} class Bar {} +declare const array: (string & Foo)[] | (string & Bar)[]; +String(array); + `, + ` +class Foo {} +class Bar {} declare const array: (string & Foo)[] & (string & Bar)[]; array.join(''); `, ` class Foo {} class Bar {} +declare const array: (string & Foo)[] & (string & Bar)[]; +String(array); + `, + ` +class Foo {} +class Bar {} declare const tuple: [string & Foo, string & Bar]; tuple.join(''); `, ` class Foo {} +class Bar {} +declare const tuple: [string & Foo, string & Bar]; +String(tuple); + `, + ` +class Foo {} declare const tuple: [string] & [Foo]; tuple.join(''); `, + ` +class Foo {} +declare const tuple: [string] & [Foo]; +String(tuple); + `, ], invalid: [ { @@ -476,6 +530,34 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + String([{}, {}]); + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + \`\${[{}, {}]}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` const array = [{}, {}]; @@ -491,6 +573,21 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + const array = [{}, {}]; + String(array); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class A {} @@ -506,6 +603,21 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class A {} + String([new A(), 'str']); + `, + errors: [ + { + data: { + certainty: 'will', + name: "[new A(), 'str']", + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -522,6 +634,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const array: (string | Foo)[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -538,6 +666,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const array: (string & Foo) | (string | Foo)[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -555,6 +699,23 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + class Bar {} + declare const array: Foo[] & Bar[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -571,6 +732,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const array: string[] | Foo[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -587,6 +764,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const tuple: [string, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -603,6 +796,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const tuple: [Foo, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -619,6 +828,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const tuple: [Foo | string, string]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -635,6 +860,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const tuple: [string, string] | [Foo, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} @@ -651,6 +892,22 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class Foo {} + declare const tuple: [Foo, string] & [Foo, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` const array = ['string', { foo: 'bar' }]; @@ -672,6 +929,27 @@ declare const foo: Bar & Foo; }, }, }, + { + code: ` + const array = ['string', { foo: 'bar' }]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootDir, + }, + }, + }, { code: ` type Bar = Record; @@ -689,6 +967,23 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + type Bar = Record; + function foo(array: T[]) { + return String(array); + } + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` type Bar = Record; From 93a83e26d71652ece8b7d79229942ed23d834a43 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 1 Dec 2024 23:15:15 +0200 Subject: [PATCH 2/9] add missing test --- packages/eslint-plugin/tests/rules/no-base-to-string.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts index ce929bf28ded..5a9425e81799 100644 --- a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -269,6 +269,10 @@ class Foo {} declare const tuple: [string] & [Foo]; String(tuple); `, + ` +declare const array: string | string[]; +String(array); + `, ], invalid: [ { From a13cfcf0b92f98b9169de4e4ec934f61403778cd Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 1 Dec 2024 23:43:43 +0200 Subject: [PATCH 3/9] undo unrelated change --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 8a0cf23daf64..e1d57bfb943d 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -204,12 +204,11 @@ export default createRule({ } if ( - declarations.every(({ parent }) => { - return ( + declarations.every( + ({ parent }) => !ts.isInterfaceDeclaration(parent) || - (parent.name.text !== 'Object' && parent.name.text !== 'Array') - ); - }) + (parent.name.text !== 'Object' && parent.name.text !== 'Array'), + ) ) { return Usefulness.Always; } From 9ed7c532b5449b6ded5abebba57ebd398c3a671c Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 2 Dec 2024 00:02:20 +0200 Subject: [PATCH 4/9] also cover [].toString() --- .../tests/rules/no-base-to-string.test.ts | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts index 5a9425e81799..c5126a79ea92 100644 --- a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -165,7 +165,9 @@ String(['foo', 'bar']); ` \`\${['foo', 'bar']}\`; `, - + ` +['foo', 'bar'].toString(); + `, ` ([{}, 'bar'] as string[]).join(''); `, @@ -176,6 +178,9 @@ String([{}, 'bar'] as string[]); \`\${[{}, 'bar'] as string[]}\`; `, ` +([{}, 'bar'] as string[]).toString(); + `, + ` function foo(array: T[]) { return array.join(); } @@ -191,6 +196,11 @@ function foo(array: T[]) { } `, ` +function foo(array: T[]) { + return array.toString(); +} + `, + ` class Foo { toString() { return ''; @@ -562,6 +572,20 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + [{}, {}].toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` const array = [{}, {}]; @@ -622,6 +646,36 @@ declare const foo: Bar & Foo; }, ], }, + { + code: ` + class A {} + \`\${[new A(), 'str']}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: "[new A(), 'str']", + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class A {} + [new A(), 'str'].toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: "[new A(), 'str']", + }, + messageId: 'baseToString', + }, + ], + }, { code: ` class Foo {} From 2b85f4d28ee06c9671aee7fa62d34ab14a29acb1 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 2 Dec 2024 18:05:02 +0200 Subject: [PATCH 5/9] refactor --- .../src/rules/no-base-to-string.ts | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index e1d57bfb943d..2fbadf1db069 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -94,7 +94,7 @@ export default createRule({ node: TSESTree.Node, type: ts.Type, ): void { - const certainty = collectArrayToStringCertainty(type); + const certainty = collectJoinCertainty(type); if (certainty === Usefulness.Always) { return; @@ -141,38 +141,43 @@ export default createRule({ return Usefulness.Never; } - function collectArrayToStringCertainty(type: ts.Type): Usefulness { + function collectTupleCertainty(type: ts.TypeReference): Usefulness { + const typeArgs = checker.getTypeArguments(type); + const certainties = typeArgs.map(t => collectToStringCertainty(t)); + if (certainties.some(certainty => certainty === Usefulness.Never)) { + return Usefulness.Never; + } + + if (certainties.some(certainty => certainty === Usefulness.Sometimes)) { + return Usefulness.Sometimes; + } + + return Usefulness.Always; + } + + function collectArrayCertainty(type: ts.Type): Usefulness { + const elemType = nullThrows( + type.getNumberIndexType(), + 'array should have number index type', + ); + return collectToStringCertainty(elemType); + } + + function collectJoinCertainty(type: ts.Type): Usefulness { if (tsutils.isUnionType(type)) { - return collectUnionTypeCertainty(type, collectArrayToStringCertainty); + return collectUnionTypeCertainty(type, collectJoinCertainty); } if (tsutils.isIntersectionType(type)) { - return collectIntersectionTypeCertainty( - type, - collectArrayToStringCertainty, - ); + return collectIntersectionTypeCertainty(type, collectJoinCertainty); } if (checker.isTupleType(type)) { - const typeArgs = checker.getTypeArguments(type); - const certainties = typeArgs.map(t => collectToStringCertainty(t)); - if (certainties.some(certainty => certainty === Usefulness.Never)) { - return Usefulness.Never; - } - - if (certainties.some(certainty => certainty === Usefulness.Sometimes)) { - return Usefulness.Sometimes; - } - - return Usefulness.Always; + return collectTupleCertainty(type); } if (checker.isArrayType(type)) { - const elemType = nullThrows( - type.getNumberIndexType(), - 'array should have number index type', - ); - return collectToStringCertainty(elemType); + return collectArrayCertainty(type); } return Usefulness.Always; @@ -187,8 +192,12 @@ export default createRule({ return Usefulness.Always; } - if (checker.isArrayType(type) || checker.isTupleType(type)) { - return collectArrayToStringCertainty(type); + if (checker.isTupleType(type)) { + return collectTupleCertainty(type); + } + + if (checker.isArrayType(type)) { + return collectArrayCertainty(type); } // Patch for old version TypeScript, the Boolean type definition missing toString() From 07c59ca030c03718baa3115e434b1b42f0e74803 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Sun, 15 Dec 2024 21:22:30 +0200 Subject: [PATCH 6/9] respect the ignoredTypeNames option --- .../eslint-plugin/src/rules/no-base-to-string.ts | 16 ++++++++-------- .../tests/rules/no-base-to-string.test.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index e7768b0fa8ec..898c319edb7a 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -193,14 +193,6 @@ export default createRule({ return Usefulness.Always; } - if (checker.isTupleType(type)) { - return collectTupleCertainty(type); - } - - if (checker.isArrayType(type)) { - return collectArrayCertainty(type); - } - // the Boolean type definition missing toString() if ( type.flags & ts.TypeFlags.Boolean || @@ -221,6 +213,14 @@ export default createRule({ return collectUnionTypeCertainty(type, collectToStringCertainty); } + if (checker.isTupleType(type)) { + return collectTupleCertainty(type); + } + + if (checker.isArrayType(type)) { + return collectArrayCertainty(type); + } + const toString = checker.getPropertyOfType(type, 'toString') ?? checker.getPropertyOfType(type, 'toLocaleString'); diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts index 795a5f7ee508..ed2f8a23657d 100644 --- a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -178,6 +178,14 @@ String(['foo', 'bar']); ` String([{}, 'bar'] as string[]); `, + { + code: ` +type Foo = { a: string }[]; +declare const foo: Foo; +String(foo); + `, + options: [{ ignoredTypeNames: ['Foo'] }], + }, ` \`\${[{}, 'bar'] as string[]}\`; `, From 0bc1024de1e3505572402c0cdf8d6dd0a3c6aacb Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Wed, 18 Dec 2024 13:51:02 +0200 Subject: [PATCH 7/9] update existing tests to use a helper for checking arrays, and change faulty tests to not check against the {} type --- .../src/rules/no-base-to-string.ts | 4 +- .../tests/rules/no-base-to-string.test.ts | 981 +++++++----------- 2 files changed, 367 insertions(+), 618 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 898c319edb7a..4228dc1371e3 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -18,12 +18,12 @@ enum Usefulness { Sometimes = 'may', } -type Options = [ +export type Options = [ { ignoredTypeNames?: string[]; }, ]; -type MessageIds = 'baseArrayJoin' | 'baseToString'; +export type MessageIds = 'baseArrayJoin' | 'baseToString'; export default createRule({ name: 'no-base-to-string', diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts index ed2f8a23657d..67e6ea0a552c 100644 --- a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -1,5 +1,12 @@ +import type { + InvalidTestCase, + ValidTestCase, +} from '@typescript-eslint/rule-tester'; + import { RuleTester } from '@typescript-eslint/rule-tester'; +import type { MessageIds, Options } from '../../src/rules/no-base-to-string'; + import rule from '../../src/rules/no-base-to-string'; import { getFixturesRootDir } from '../RuleTester'; @@ -38,6 +45,319 @@ const literalListWrapped = [ ...literalListNeedParen.map(i => `(${i})`), ]; +const validArrayOrTupleCases = ( + wrap: (arr: string) => string, +): (string | ValidTestCase)[] => [ + `${wrap("['foo', 'bar']")};`, + `${wrap("([{ a: 'a' }, 'bar'] as string[])")};`, + ` + function foo(array: T[]) { + return ${wrap('array')}; + } + `, + ` + declare const array: string[]; + ${wrap('array')}; + `, + ` + class Foo { + foo: string; + } + declare const array: (string & Foo)[]; + ${wrap('array')}; + `, + ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: (string & Foo)[] | (string & Bar)[]; + ${wrap('array')}; + `, + ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: (string & Foo)[] & (string & Bar)[]; + ${wrap('array')}; + `, + ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const tuple: [string & Foo, string & Bar]; + ${wrap('tuple')}; + `, + ` + class Foo { + foo: string; + } + declare const tuple: [string] & [Foo]; + ${wrap('tuple')}; + `, +]; + +const invalidArrayOrTupleCases = ( + messageId: MessageIds, + wrap: (arr: string) => string, +): InvalidTestCase[] => [ + { + code: wrap('[{}, {}]'), + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId, + }, + ], + }, + { + code: ` + const array = [{}, {}]; + ${wrap('array')}; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId, + }, + ], + }, + { + code: ` + class A { + a: string; + } + ${wrap("[new A(), 'str']")}; + `, + errors: [ + { + data: { + certainty: 'may', + name: "[new A(), 'str']", + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string | Foo)[]; + ${wrap('array')}; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string & Foo) | (string | Foo)[]; + ${wrap('array')}; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: Foo[] & Bar[]; + ${wrap('array')}; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: string[] | Foo[]; + ${wrap('array')}; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, Foo]; + ${wrap('tuple')}; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, Foo]; + ${wrap('tuple')}; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo | string, string]; + ${wrap('tuple')}; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, string] | [Foo, Foo]; + ${wrap('tuple')}; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId, + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, string] & [Foo, Foo]; + ${wrap('tuple')}; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId, + }, + ], + }, + { + code: ` + const array = ['string', { foo: 'bar' }]; + ${wrap('array')}; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId, + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` + type Bar = Record; + function foo(array: T[]) { + return ${wrap('array')}; + } + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId, + }, + ], + }, +]; + ruleTester.run('no-base-to-string', rule, { valid: [ // template @@ -149,35 +469,6 @@ String(foo); `, options: [{ ignoredTypeNames: ['Foo'] }], }, - ` -function String(value) { - return value; -} -declare const myValue: object; -String(myValue); - `, - ` -import { String } from 'foo'; -String({}); - `, - ` -['foo', 'bar'].join(''); - `, - ` -String(['foo', 'bar']); - `, - ` -\`\${['foo', 'bar']}\`; - `, - ` -['foo', 'bar'].toString(); - `, - ` -([{}, 'bar'] as string[]).join(''); - `, - ` -String([{}, 'bar'] as string[]); - `, { code: ` type Foo = { a: string }[]; @@ -186,32 +477,29 @@ String(foo); `, options: [{ ignoredTypeNames: ['Foo'] }], }, + { + code: ` +type Foo = { a: string }[]; +declare const foo: Foo; +\`\${foo}\`; + `, + options: [{ ignoredTypeNames: ['Foo'] }], + }, ` -\`\${[{}, 'bar'] as string[]}\`; - `, - ` -([{}, 'bar'] as string[]).toString(); - `, - ` -function foo(array: T[]) { - return array.join(); -} - `, - ` -function foo(array: T[]) { - return String(array); -} - `, - ` -function foo(array: T[]) { - return \`\${array}\`; +function String(value) { + return value; } +declare const myValue: object; +String(myValue); `, ` -function foo(array: T[]) { - return array.toString(); -} +import { String } from 'foo'; +String({}); `, + ...validArrayOrTupleCases(arr => `${arr}.join('')`), + ...validArrayOrTupleCases(arr => `String(${arr})`), + ...validArrayOrTupleCases(arr => `${arr}.toString()`), + ...validArrayOrTupleCases(arr => `\`\${${arr}}\``), ` class Foo { toString() { @@ -228,73 +516,14 @@ const foo = new Foo(); foo.join(); `, ` -declare const array: string[]; -array.join(''); - `, - ` -declare const array: string[]; -String(array); - `, - ` -class Foo {} -declare const array: (string & Foo)[]; -array.join(''); - `, - ` -class Foo {} -declare const array: (string & Foo)[]; -String(array); - `, - ` -class Foo {} -class Bar {} -declare const array: (string & Foo)[] | (string & Bar)[]; -array.join(''); - `, - ` -class Foo {} -class Bar {} -declare const array: (string & Foo)[] | (string & Bar)[]; -String(array); - `, - ` -class Foo {} -class Bar {} -declare const array: (string & Foo)[] & (string & Bar)[]; -array.join(''); - `, - ` -class Foo {} -class Bar {} -declare const array: (string & Foo)[] & (string & Bar)[]; -String(array); - `, - ` -class Foo {} -class Bar {} -declare const tuple: [string & Foo, string & Bar]; -tuple.join(''); - `, - ` -class Foo {} -class Bar {} -declare const tuple: [string & Foo, string & Bar]; -String(tuple); - `, - ` -class Foo {} -declare const tuple: [string] & [Foo]; -tuple.join(''); - `, - ` -class Foo {} -declare const tuple: [string] & [Foo]; -String(tuple); - `, - ` -declare const array: string | string[]; -String(array); +class Foo { + toString() { + return ''; + } +} +new Foo().toString(); `, + // don't bother trying to interpret spread args. ` let objects = [{}, {}]; @@ -519,7 +748,9 @@ String(u); }, { code: ` -class Foo {} +class Foo { + foo: string; +} declare const foo: string | Foo; \`\${foo}\`; `, @@ -535,8 +766,12 @@ declare const foo: string | Foo; }, { code: ` -class Foo {} -class Bar {} +class Foo { + foo: string; +} +class Bar { + bar: string; +} declare const foo: Bar | Foo; \`\${foo}\`; `, @@ -552,8 +787,12 @@ declare const foo: Bar | Foo; }, { code: ` -class Foo {} -class Bar {} +class Foo { + foo: string; +} +class Bar { + bar: string; +} declare const foo: Bar & Foo; \`\${foo}\`; `, @@ -567,501 +806,11 @@ declare const foo: Bar & Foo; }, ], }, - { - code: ` - [{}, {}].join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: '[{}, {}]', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - String([{}, {}]); - `, - errors: [ - { - data: { - certainty: 'will', - name: '[{}, {}]', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - \`\${[{}, {}]}\`; - `, - errors: [ - { - data: { - certainty: 'will', - name: '[{}, {}]', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - [{}, {}].toString(); - `, - errors: [ - { - data: { - certainty: 'will', - name: '[{}, {}]', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - const array = [{}, {}]; - array.join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - const array = [{}, {}]; - String(array); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'array', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class A {} - [new A(), 'str'].join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: "[new A(), 'str']", - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class A {} - String([new A(), 'str']); - `, - errors: [ - { - data: { - certainty: 'will', - name: "[new A(), 'str']", - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class A {} - \`\${[new A(), 'str']}\`; - `, - errors: [ - { - data: { - certainty: 'will', - name: "[new A(), 'str']", - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class A {} - [new A(), 'str'].toString(); - `, - errors: [ - { - data: { - certainty: 'will', - name: "[new A(), 'str']", - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const array: (string | Foo)[]; - array.join(''); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const array: (string | Foo)[]; - String(array); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const array: (string & Foo) | (string | Foo)[]; - array.join(''); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const array: (string & Foo) | (string | Foo)[]; - String(array); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - class Bar {} - declare const array: Foo[] & Bar[]; - array.join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - class Bar {} - declare const array: Foo[] & Bar[]; - String(array); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'array', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const array: string[] | Foo[]; - array.join(''); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const array: string[] | Foo[]; - String(array); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [string, Foo]; - tuple.join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [string, Foo]; - String(tuple); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [Foo, Foo]; - tuple.join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [Foo, Foo]; - String(tuple); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [Foo | string, string]; - tuple.join(''); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'tuple', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [Foo | string, string]; - String(tuple); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'tuple', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [string, string] | [Foo, Foo]; - tuple.join(''); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'tuple', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [string, string] | [Foo, Foo]; - String(tuple); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'tuple', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [Foo, string] & [Foo, Foo]; - tuple.join(''); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId: 'baseArrayJoin', - }, - ], - }, - { - code: ` - class Foo {} - declare const tuple: [Foo, string] & [Foo, Foo]; - String(tuple); - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId: 'baseToString', - }, - ], - }, - { - code: ` - const array = ['string', { foo: 'bar' }]; - array.join(''); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - languageOptions: { - parserOptions: { - project: './tsconfig.noUncheckedIndexedAccess.json', - tsconfigRootDir: rootDir, - }, - }, - }, - { - code: ` - const array = ['string', { foo: 'bar' }]; - String(array); - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseToString', - }, - ], - languageOptions: { - parserOptions: { - project: './tsconfig.noUncheckedIndexedAccess.json', - tsconfigRootDir: rootDir, - }, - }, - }, - { - code: ` - type Bar = Record; - function foo(array: T[]) { - return array.join(); - } - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId: 'baseArrayJoin', - }, - ], - }, + ...invalidArrayOrTupleCases('baseArrayJoin', arr => `${arr}.join('')`), + ...invalidArrayOrTupleCases('baseToString', arr => `String(${arr})`), + ...invalidArrayOrTupleCases('baseToString', arr => `${arr}.toString()`), + ...invalidArrayOrTupleCases('baseToString', arr => `\`\${${arr}}\``), + { code: ` type Bar = Record; From 2fc6c6c70f4e3a38fe8062e5b6298f90cd71f809 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Thu, 26 Dec 2024 22:11:53 +0200 Subject: [PATCH 8/9] don't use a test helper for creating tests dynamically --- .../tests/rules/no-base-to-string.test.ts | 1670 +++++++++++++---- 1 file changed, 1292 insertions(+), 378 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts index 67e6ea0a552c..6ac4b14bd345 100644 --- a/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts +++ b/packages/eslint-plugin/tests/rules/no-base-to-string.test.ts @@ -1,12 +1,5 @@ -import type { - InvalidTestCase, - ValidTestCase, -} from '@typescript-eslint/rule-tester'; - import { RuleTester } from '@typescript-eslint/rule-tester'; -import type { MessageIds, Options } from '../../src/rules/no-base-to-string'; - import rule from '../../src/rules/no-base-to-string'; import { getFixturesRootDir } from '../RuleTester'; @@ -45,319 +38,6 @@ const literalListWrapped = [ ...literalListNeedParen.map(i => `(${i})`), ]; -const validArrayOrTupleCases = ( - wrap: (arr: string) => string, -): (string | ValidTestCase)[] => [ - `${wrap("['foo', 'bar']")};`, - `${wrap("([{ a: 'a' }, 'bar'] as string[])")};`, - ` - function foo(array: T[]) { - return ${wrap('array')}; - } - `, - ` - declare const array: string[]; - ${wrap('array')}; - `, - ` - class Foo { - foo: string; - } - declare const array: (string & Foo)[]; - ${wrap('array')}; - `, - ` - class Foo { - foo: string; - } - class Bar { - bar: string; - } - declare const array: (string & Foo)[] | (string & Bar)[]; - ${wrap('array')}; - `, - ` - class Foo { - foo: string; - } - class Bar { - bar: string; - } - declare const array: (string & Foo)[] & (string & Bar)[]; - ${wrap('array')}; - `, - ` - class Foo { - foo: string; - } - class Bar { - bar: string; - } - declare const tuple: [string & Foo, string & Bar]; - ${wrap('tuple')}; - `, - ` - class Foo { - foo: string; - } - declare const tuple: [string] & [Foo]; - ${wrap('tuple')}; - `, -]; - -const invalidArrayOrTupleCases = ( - messageId: MessageIds, - wrap: (arr: string) => string, -): InvalidTestCase[] => [ - { - code: wrap('[{}, {}]'), - errors: [ - { - data: { - certainty: 'will', - name: '[{}, {}]', - }, - messageId, - }, - ], - }, - { - code: ` - const array = [{}, {}]; - ${wrap('array')}; - `, - errors: [ - { - data: { - certainty: 'will', - name: 'array', - }, - messageId, - }, - ], - }, - { - code: ` - class A { - a: string; - } - ${wrap("[new A(), 'str']")}; - `, - errors: [ - { - data: { - certainty: 'may', - name: "[new A(), 'str']", - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const array: (string | Foo)[]; - ${wrap('array')}; - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const array: (string & Foo) | (string | Foo)[]; - ${wrap('array')}; - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - class Bar { - bar: string; - } - declare const array: Foo[] & Bar[]; - ${wrap('array')}; - `, - errors: [ - { - data: { - certainty: 'will', - name: 'array', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const array: string[] | Foo[]; - ${wrap('array')}; - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const tuple: [string, Foo]; - ${wrap('tuple')}; - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const tuple: [Foo, Foo]; - ${wrap('tuple')}; - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const tuple: [Foo | string, string]; - ${wrap('tuple')}; - `, - errors: [ - { - data: { - certainty: 'may', - name: 'tuple', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const tuple: [string, string] | [Foo, Foo]; - ${wrap('tuple')}; - `, - errors: [ - { - data: { - certainty: 'may', - name: 'tuple', - }, - messageId, - }, - ], - }, - { - code: ` - class Foo { - foo: string; - } - declare const tuple: [Foo, string] & [Foo, Foo]; - ${wrap('tuple')}; - `, - errors: [ - { - data: { - certainty: 'will', - name: 'tuple', - }, - messageId, - }, - ], - }, - { - code: ` - const array = ['string', { foo: 'bar' }]; - ${wrap('array')}; - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId, - }, - ], - languageOptions: { - parserOptions: { - project: './tsconfig.noUncheckedIndexedAccess.json', - tsconfigRootDir: rootDir, - }, - }, - }, - { - code: ` - type Bar = Record; - function foo(array: T[]) { - return ${wrap('array')}; - } - `, - errors: [ - { - data: { - certainty: 'may', - name: 'array', - }, - messageId, - }, - ], - }, -]; - ruleTester.run('no-base-to-string', rule, { valid: [ // template @@ -469,22 +149,6 @@ String(foo); `, options: [{ ignoredTypeNames: ['Foo'] }], }, - { - code: ` -type Foo = { a: string }[]; -declare const foo: Foo; -String(foo); - `, - options: [{ ignoredTypeNames: ['Foo'] }], - }, - { - code: ` -type Foo = { a: string }[]; -declare const foo: Foo; -\`\${foo}\`; - `, - options: [{ ignoredTypeNames: ['Foo'] }], - }, ` function String(value) { return value; @@ -496,10 +160,18 @@ String(myValue); import { String } from 'foo'; String({}); `, - ...validArrayOrTupleCases(arr => `${arr}.join('')`), - ...validArrayOrTupleCases(arr => `String(${arr})`), - ...validArrayOrTupleCases(arr => `${arr}.toString()`), - ...validArrayOrTupleCases(arr => `\`\${${arr}}\``), + ` +['foo', 'bar'].join(''); + `, + + ` +([{ foo: 'foo' }, 'bar'] as string[]).join(''); + `, + ` +function foo(array: T[]) { + return array.join(); +} + `, ` class Foo { toString() { @@ -516,60 +188,307 @@ const foo = new Foo(); foo.join(); `, ` +declare const array: string[]; +array.join(''); + `, + ` +class Foo { + foo: string; +} +declare const array: (string & Foo)[]; +array.join(''); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] | (string & Bar)[]; +array.join(''); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] & (string & Bar)[]; +array.join(''); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const tuple: [string & Foo, string & Bar]; +tuple.join(''); + `, + ` +class Foo { + foo: string; +} +declare const tuple: [string] & [Foo]; +tuple.join(''); + `, + + ` +String(['foo', 'bar']); + `, + + ` +String([{ foo: 'foo' }, 'bar'] as string[]); + `, + ` +function foo(array: T[]) { + return String(array); +} + `, + ` class Foo { toString() { return ''; } } -new Foo().toString(); +String([new Foo()]); `, - - // don't bother trying to interpret spread args. ` -let objects = [{}, {}]; -String(...objects); +declare const array: string[]; +String(array); `, - // https://github.com/typescript-eslint/typescript-eslint/issues/8585 ` -type Constructable = abstract new (...args: any[]) => Entity; - -interface GuildChannel { - toString(): \`<#\${string}>\`; +class Foo { + foo: string; } - -declare const foo: Constructable; -class ExtendedGuildChannel extends foo {} -declare const bb: ExtendedGuildChannel; -bb.toString(); +declare const array: (string & Foo)[]; +String(array); `, - // https://github.com/typescript-eslint/typescript-eslint/issues/8585 with intersection order reversed. ` -type Constructable = abstract new (...args: any[]) => Entity; - -interface GuildChannel { - toString(): \`<#\${string}>\`; +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] | (string & Bar)[]; +String(array); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] & (string & Bar)[]; +String(array); + `, + ` +class Foo { + foo: string; } +class Bar { + bar: string; +} +declare const tuple: [string & Foo, string & Bar]; +String(tuple); + `, + ` +class Foo { + foo: string; +} +declare const tuple: [string] & [Foo]; +String(tuple); + `, -declare const foo: Constructable<{ bar: 1 } & GuildChannel>; -class ExtendedGuildChannel extends foo {} -declare const bb: ExtendedGuildChannel; -bb.toString(); + ` +['foo', 'bar'].toString(); `, + ` -function foo(x: T) { - String(x); +([{ foo: 'foo' }, 'bar'] as string[]).toString(); + `, + ` +function foo(array: T[]) { + return array.toString(); } `, ` -declare const u: unknown; -String(u); +class Foo { + toString() { + return ''; + } +} +[new Foo()].toString(); `, - ], - invalid: [ - { - code: '`${{}})`;', - errors: [ - { + ` +declare const array: string[]; +array.toString(); + `, + ` +class Foo { + foo: string; +} +declare const array: (string & Foo)[]; +array.toString(); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] | (string & Bar)[]; +array.toString(); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] & (string & Bar)[]; +array.toString(); + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const tuple: [string & Foo, string & Bar]; +tuple.toString(); + `, + ` +class Foo { + foo: string; +} +declare const tuple: [string] & [Foo]; +tuple.toString(); + `, + + ` +\`\${['foo', 'bar']}\`; + `, + + ` +\`\${[{ foo: 'foo' }, 'bar'] as string[]}\`; + `, + ` +function foo(array: T[]) { + return \`\${array}\`; +} + `, + ` +class Foo { + toString() { + return ''; + } +} +\`\${[new Foo()]}\`; + `, + ` +declare const array: string[]; +\`\${array}\`; + `, + ` +class Foo { + foo: string; +} +declare const array: (string & Foo)[]; +\`\${array}\`; + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] | (string & Bar)[]; +\`\${array}\`; + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const array: (string & Foo)[] & (string & Bar)[]; +\`\${array}\`; + `, + ` +class Foo { + foo: string; +} +class Bar { + bar: string; +} +declare const tuple: [string & Foo, string & Bar]; +\`\${tuple}\`; + `, + ` +class Foo { + foo: string; +} +declare const tuple: [string] & [Foo]; +\`\${tuple}\`; + `, + + // don't bother trying to interpret spread args. + ` +let objects = [{}, {}]; +String(...objects); + `, + // https://github.com/typescript-eslint/typescript-eslint/issues/8585 + ` +type Constructable = abstract new (...args: any[]) => Entity; + +interface GuildChannel { + toString(): \`<#\${string}>\`; +} + +declare const foo: Constructable; +class ExtendedGuildChannel extends foo {} +declare const bb: ExtendedGuildChannel; +bb.toString(); + `, + // https://github.com/typescript-eslint/typescript-eslint/issues/8585 with intersection order reversed. + ` +type Constructable = abstract new (...args: any[]) => Entity; + +interface GuildChannel { + toString(): \`<#\${string}>\`; +} + +declare const foo: Constructable<{ bar: 1 } & GuildChannel>; +class ExtendedGuildChannel extends foo {} +declare const bb: ExtendedGuildChannel; +bb.toString(); + `, + ` +function foo(x: T) { + String(x); +} + `, + ` +declare const u: unknown; +String(u); + `, + ], + invalid: [ + { + code: '`${{}})`;', + errors: [ + { data: { certainty: 'will', name: '{}', @@ -806,10 +725,1005 @@ declare const foo: Bar & Foo; }, ], }, - ...invalidArrayOrTupleCases('baseArrayJoin', arr => `${arr}.join('')`), - ...invalidArrayOrTupleCases('baseToString', arr => `String(${arr})`), - ...invalidArrayOrTupleCases('baseToString', arr => `${arr}.toString()`), - ...invalidArrayOrTupleCases('baseToString', arr => `\`\${${arr}}\``), + { + code: ` + [{}, {}].join(''); + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + const array = [{}, {}]; + array.join(''); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class A { + a: string; + } + [new A(), 'str'].join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: "[new A(), 'str']", + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string | Foo)[]; + array.join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string & Foo) | (string | Foo)[]; + array.join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: Foo[] & Bar[]; + array.join(''); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: string[] | Foo[]; + array.join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, Foo]; + tuple.join(''); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, Foo]; + tuple.join(''); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo | string, string]; + tuple.join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, string] | [Foo, Foo]; + tuple.join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, string] & [Foo, Foo]; + tuple.join(''); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + { + code: ` + const array = ['string', { foo: 'bar' }]; + array.join(''); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` + type Bar = Record; + function foo(array: T[]) { + return array.join(); + } + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseArrayJoin', + }, + ], + }, + + { + code: ` + String([{}, {}]); + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + const array = [{}, {}]; + String(array); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class A { + a: string; + } + String([new A(), 'str']); + `, + errors: [ + { + data: { + certainty: 'may', + name: "[new A(), 'str']", + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string | Foo)[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string & Foo) | (string | Foo)[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: Foo[] & Bar[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: string[] | Foo[]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo | string, string]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, string] | [Foo, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, string] & [Foo, Foo]; + String(tuple); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + const array = ['string', { foo: 'bar' }]; + String(array); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` + type Bar = Record; + function foo(array: T[]) { + return String(array); + } + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + + { + code: ` + [{}, {}].toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + const array = [{}, {}]; + array.toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class A { + a: string; + } + [new A(), 'str'].toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: "[new A(), 'str']", + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string | Foo)[]; + array.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string & Foo) | (string | Foo)[]; + array.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: Foo[] & Bar[]; + array.toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: string[] | Foo[]; + array.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, Foo]; + tuple.toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, Foo]; + tuple.toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo | string, string]; + tuple.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, string] | [Foo, Foo]; + tuple.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, string] & [Foo, Foo]; + tuple.toString(); + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + const array = ['string', { foo: 'bar' }]; + array.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` + type Bar = Record; + function foo(array: T[]) { + return array.toString(); + } + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + + { + code: ` + \`\${[{}, {}]}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: '[{}, {}]', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + const array = [{}, {}]; + \`\${array}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class A { + a: string; + } + \`\${[new A(), 'str']}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: "[new A(), 'str']", + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string | Foo)[]; + \`\${array}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: (string & Foo) | (string | Foo)[]; + \`\${array}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + class Bar { + bar: string; + } + declare const array: Foo[] & Bar[]; + \`\${array}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const array: string[] | Foo[]; + \`\${array}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, Foo]; + \`\${tuple}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, Foo]; + \`\${tuple}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo | string, string]; + \`\${tuple}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [string, string] | [Foo, Foo]; + \`\${tuple}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + class Foo { + foo: string; + } + declare const tuple: [Foo, string] & [Foo, Foo]; + \`\${tuple}\`; + `, + errors: [ + { + data: { + certainty: 'will', + name: 'tuple', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` + const array = ['string', { foo: 'bar' }]; + \`\${array}\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + languageOptions: { + parserOptions: { + project: './tsconfig.noUncheckedIndexedAccess.json', + tsconfigRootDir: rootDir, + }, + }, + }, + { + code: ` + type Bar = Record; + function foo(array: T[]) { + return \`\${array}\`; + } + `, + errors: [ + { + data: { + certainty: 'may', + name: 'array', + }, + messageId: 'baseToString', + }, + ], + }, { code: ` From 5ebba433d32a8516a59240690eabe1a2e7d98723 Mon Sep 17 00:00:00 2001 From: Ronen Amiel Date: Mon, 30 Dec 2024 20:09:59 +0200 Subject: [PATCH 9/9] remove unnecessary check --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 4228dc1371e3..62056b5a636e 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -243,8 +243,7 @@ export default createRule({ const declaration = declarations[0]; const isBaseToString = ts.isInterfaceDeclaration(declaration.parent) && - (declaration.parent.name.text === 'Object' || - declaration.parent.name.text === 'Array'); + declaration.parent.name.text === 'Object'; return isBaseToString ? Usefulness.Never : Usefulness.Always; }