From efb5f30ff880e9d001f77b8f74bd2455343e33f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Mon, 5 May 2025 00:31:32 +0900 Subject: [PATCH 1/8] feat: add type and option --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 6617c8d1dea2..b0b9df6fa1bc 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -21,6 +21,7 @@ enum Usefulness { export type Options = [ { ignoredTypeNames?: string[]; + restrictUnknown?: boolean; }, ]; export type MessageIds = 'baseArrayJoin' | 'baseToString'; @@ -54,6 +55,11 @@ export default createRule({ type: 'string', }, }, + restrictUnknown: { + type: 'boolean', + default: false, + description: 'Restrict applying toString to unknown type', + }, }, }, ], @@ -61,6 +67,7 @@ export default createRule({ defaultOptions: [ { ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams'], + restrictUnknown: false, }, ], create(context, [option]) { From d601fd3c2a81ae807eefa9a56f4d7134ea3c9af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Tue, 6 May 2025 15:15:21 +0900 Subject: [PATCH 2/8] feat: apply checkUnknown option and test case --- .../src/rules/no-base-to-string.ts | 25 ++- .../tests/rules/no-base-to-string.test.ts | 179 +++++++++++++++++- 2 files changed, 186 insertions(+), 18 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 b0b9df6fa1bc..9b9640e40796 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -21,7 +21,7 @@ enum Usefulness { export type Options = [ { ignoredTypeNames?: string[]; - restrictUnknown?: boolean; + checkUnknown?: boolean; }, ]; export type MessageIds = 'baseArrayJoin' | 'baseToString'; @@ -47,6 +47,12 @@ export default createRule({ type: 'object', additionalProperties: false, properties: { + checkUnknown: { + type: 'boolean', + default: false, + description: + 'Checks the case where toString is applied to unknown type', + }, ignoredTypeNames: { type: 'array', description: @@ -55,19 +61,14 @@ export default createRule({ type: 'string', }, }, - restrictUnknown: { - type: 'boolean', - default: false, - description: 'Restrict applying toString to unknown type', - }, }, }, ], }, defaultOptions: [ { + checkUnknown: false, ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams'], - restrictUnknown: false, }, ], create(context, [option]) { @@ -83,6 +84,7 @@ export default createRule({ type ?? services.getTypeAtLocation(node), new Set(), ); + if (certainty === Usefulness.Always) { return; } @@ -220,7 +222,7 @@ export default createRule({ return collectToStringCertainty(constraint, visited); } // unconstrained generic means `unknown` - return Usefulness.Always; + return option.checkUnknown ? Usefulness.Sometimes : Usefulness.Always; } // the Boolean type definition missing toString() @@ -258,8 +260,13 @@ export default createRule({ const toString = checker.getPropertyOfType(type, 'toString') ?? checker.getPropertyOfType(type, 'toLocaleString'); + if (!toString) { - // e.g. any/unknown + // unknown + if (option.checkUnknown && type.flags === ts.TypeFlags.Unknown) { + return Usefulness.Sometimes; + } + // e.g. any 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 f0fe39979c4d..b4685eb73479 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 @@ -475,15 +475,6 @@ declare const bb: ExtendedGuildChannel; bb.toString(); `, ` -function foo(x: T) { - String(x); -} - `, - ` -declare const u: unknown; -String(u); - `, - ` type Value = string | Value[]; declare const v: Value; @@ -511,8 +502,178 @@ String(v); declare const v: ('foo' | 'bar')[][]; String(v); `, + ` +declare const x: unknown; +\`\${x})\`; + `, + ` +declare const x: unknown; +x.toString(); + `, + ` +declare const x: unknown; +x.toLocaleString(); + `, + ` +declare const x: unknown; +'' + x; + `, + ` +declare const x: unknown; +String(x); + `, + ` + declare const x: unknown; + '' += x; + `, + ` + function foo(x: T) { + String(x); + } + `, ], invalid: [ + { + code: ` +declare const x: unknown; +\`\${x})\`; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: unknown; +x.toString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: unknown; +x.toLocaleString(); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: unknown; +'' + x; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: unknown; +String(x); + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: unknown; +'' += x; + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` + function foo(x: T) { + String(x); + } + `, + errors: [ + { + data: { + certainty: 'may', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, { code: '`${{}})`;', errors: [ From 3bdc647ac6a6737826ebd6045a16efea4c01c886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Sun, 18 May 2025 22:37:34 +0900 Subject: [PATCH 3/8] fix: ci error --- .../docs/rules/no-base-to-string.mdx | 11 +++++++ .../no-base-to-string.shot | 6 ++++ .../tests/rules/no-base-to-string.test.ts | 30 +++++++++---------- .../schema-snapshots/no-base-to-string.shot | 7 +++++ 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx index 8097ca8b7f41..24932dad8fd3 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -99,6 +99,17 @@ let text = `${value}`; String(/regex/); ``` +### `checkUnknown` + +{/* insert option description */} + +The following patterns are considered incorrect with the options `{ checkUnknown: true }`: + +```ts option='{ "checkUnknown": true }' showPlaygroundButton +declare const x: unknown; +x.toString(); +``` + ## When Not To Use It If you don't mind a risk of `"[object Object]"` or incorrect type coercions in your values, then you will not need this rule. diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot index ac8bfaecc474..b3ef4c8da549 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot @@ -56,3 +56,9 @@ let value = /regex/; value.toString(); let text = `${value}`; String(/regex/); + +Options: { "checkUnknown": true } + +declare const x: unknown; +x.toString(); +~ 'x' may use Object's default stringification format ('[object Object]') when stringified. 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 b4685eb73479..bfc91aa73e8c 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 @@ -505,32 +505,32 @@ String(v); ` declare const x: unknown; \`\${x})\`; - `, + `, ` declare const x: unknown; x.toString(); - `, + `, ` declare const x: unknown; x.toLocaleString(); - `, + `, ` declare const x: unknown; '' + x; - `, + `, ` declare const x: unknown; String(x); - `, + `, ` - declare const x: unknown; - '' += x; - `, +declare const x: unknown; +'' += x; + `, ` - function foo(x: T) { - String(x); - } - `, +function foo(x: T) { + String(x); +} + `, ], invalid: [ { @@ -655,9 +655,9 @@ declare const x: unknown; }, { code: ` - function foo(x: T) { - String(x); - } +function foo(x: T) { + String(x); +} `, errors: [ { diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot b/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot index fe2577cd6329..4c348f18a6f0 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot @@ -5,6 +5,11 @@ { "additionalProperties": false, "properties": { + "checkUnknown": { + "default": false, + "description": "Checks the case where toString is applied to unknown type", + "type": "boolean" + }, "ignoredTypeNames": { "description": "Stringified regular expressions of type names to ignore.", "items": { @@ -22,6 +27,8 @@ type Options = [ { + /** Checks the case where toString is applied to unknown type */ + checkUnknown?: boolean; /** Stringified regular expressions of type names to ignore. */ ignoredTypeNames?: string[]; }, From 224b3709189dc1952e27cf924836468a03c00332 Mon Sep 17 00:00:00 2001 From: puki4416 Date: Sun, 1 Jun 2025 19:52:39 +0900 Subject: [PATCH 4/8] fix: change example code and add test cases --- .../docs/rules/no-base-to-string.mdx | 2 +- .../tests/rules/no-base-to-string.test.ts | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx index 24932dad8fd3..d1927bf69b5b 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -107,7 +107,7 @@ The following patterns are considered incorrect with the options `{ checkUnknown ```ts option='{ "checkUnknown": true }' showPlaygroundButton declare const x: unknown; -x.toString(); +String(x); ``` ## When Not To Use It 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 bfc91aa73e8c..7c0cebfa0181 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 @@ -674,6 +674,126 @@ function foo(x: T) { }, ], }, + { + code: ` +declare const x: any; +\`\${x})\`; + `, + errors: [ + { + data: { + certainty: 'always', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: any; +x.toString(); + `, + errors: [ + { + data: { + certainty: 'always', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: any; +x.toLocaleString(); + `, + errors: [ + { + data: { + certainty: 'always', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: any; +'' + x; + `, + errors: [ + { + data: { + certainty: 'always', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: any; +String(x); + `, + errors: [ + { + data: { + certainty: 'always', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, + { + code: ` +declare const x: any; +'' += x; + `, + errors: [ + { + data: { + certainty: 'always', + name: 'x', + }, + messageId: 'baseToString', + }, + ], + options: [ + { + checkUnknown: true, + }, + ], + }, { code: '`${{}})`;', errors: [ From 3ca5d85fca860d7b09fd66252ac001b1088d8fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Tue, 3 Jun 2025 22:43:16 +0900 Subject: [PATCH 5/8] fix: no-base-to-string Rule test fix and update schema --- .../no-base-to-string.shot | 4 +- .../tests/rules/no-base-to-string.test.ts | 144 +++--------------- packages/eslint-plugin/vitest.config.mts | 5 +- 3 files changed, 30 insertions(+), 123 deletions(-) diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot index b3ef4c8da549..ae8614ebe762 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-base-to-string.shot @@ -60,5 +60,5 @@ String(/regex/); Options: { "checkUnknown": true } declare const x: unknown; -x.toString(); -~ 'x' may use Object's default stringification format ('[object Object]') when stringified. +String(x); + ~ 'x' may use Object's default stringification format ('[object Object]') when stringified. 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 7c0cebfa0181..5fb889131b67 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 @@ -531,6 +531,30 @@ function foo(x: T) { String(x); } `, + ` +declare const x: any; +\`\${x})\`; + `, + ` +declare const x: any; +x.toString(); + `, + ` +declare const x: any; +x.toLocaleString(); + `, + ` +declare const x: any; +'' + x; + `, + ` +declare const x: any; +String(x); + `, + ` +declare const x: any; +'' += x; + `, ], invalid: [ { @@ -674,126 +698,6 @@ function foo(x: T) { }, ], }, - { - code: ` -declare const x: any; -\`\${x})\`; - `, - errors: [ - { - data: { - certainty: 'always', - name: 'x', - }, - messageId: 'baseToString', - }, - ], - options: [ - { - checkUnknown: true, - }, - ], - }, - { - code: ` -declare const x: any; -x.toString(); - `, - errors: [ - { - data: { - certainty: 'always', - name: 'x', - }, - messageId: 'baseToString', - }, - ], - options: [ - { - checkUnknown: true, - }, - ], - }, - { - code: ` -declare const x: any; -x.toLocaleString(); - `, - errors: [ - { - data: { - certainty: 'always', - name: 'x', - }, - messageId: 'baseToString', - }, - ], - options: [ - { - checkUnknown: true, - }, - ], - }, - { - code: ` -declare const x: any; -'' + x; - `, - errors: [ - { - data: { - certainty: 'always', - name: 'x', - }, - messageId: 'baseToString', - }, - ], - options: [ - { - checkUnknown: true, - }, - ], - }, - { - code: ` -declare const x: any; -String(x); - `, - errors: [ - { - data: { - certainty: 'always', - name: 'x', - }, - messageId: 'baseToString', - }, - ], - options: [ - { - checkUnknown: true, - }, - ], - }, - { - code: ` -declare const x: any; -'' += x; - `, - errors: [ - { - data: { - certainty: 'always', - name: 'x', - }, - messageId: 'baseToString', - }, - ], - options: [ - { - checkUnknown: true, - }, - ], - }, { code: '`${{}})`;', errors: [ diff --git a/packages/eslint-plugin/vitest.config.mts b/packages/eslint-plugin/vitest.config.mts index 44eba50897ef..4d70d7f5230f 100644 --- a/packages/eslint-plugin/vitest.config.mts +++ b/packages/eslint-plugin/vitest.config.mts @@ -11,7 +11,10 @@ export default mergeConfig( root: import.meta.dirname, test: { - dir: path.join(import.meta.dirname, 'tests'), + include: [ + path.join(import.meta.dirname, 'tests/rules/no-base-to-string.test.ts'), + ], + // dir: path.join(import.meta.dirname, 'tests'), name: packageJson.name.replace('@typescript-eslint/', ''), root: import.meta.dirname, }, From 80fd6d86bb4bfd3e832910a9f263ab33237f1e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=80=E1=85=B5=E1=86=B7=E1=84=89=E1=85=A1=E1=86=BC?= =?UTF-8?q?=E1=84=83=E1=85=AE?= Date: Fri, 6 Jun 2025 11:28:35 +0900 Subject: [PATCH 6/8] fix: test config revert --- packages/eslint-plugin/vitest.config.mts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/eslint-plugin/vitest.config.mts b/packages/eslint-plugin/vitest.config.mts index 4d70d7f5230f..44eba50897ef 100644 --- a/packages/eslint-plugin/vitest.config.mts +++ b/packages/eslint-plugin/vitest.config.mts @@ -11,10 +11,7 @@ export default mergeConfig( root: import.meta.dirname, test: { - include: [ - path.join(import.meta.dirname, 'tests/rules/no-base-to-string.test.ts'), - ], - // dir: path.join(import.meta.dirname, 'tests'), + dir: path.join(import.meta.dirname, 'tests'), name: packageJson.name.replace('@typescript-eslint/', ''), root: import.meta.dirname, }, From 4fcfd247350945129e02269fca976c584d8b7f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 23 Jun 2025 12:28:18 -0400 Subject: [PATCH 7/8] Apply suggestions from code review --- 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 9b9640e40796..18959dd65882 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -49,9 +49,8 @@ export default createRule({ properties: { checkUnknown: { type: 'boolean', - default: false, description: - 'Checks the case where toString is applied to unknown type', + 'Whether to also check values of type `unknown`', }, ignoredTypeNames: { type: 'array', From a0981145752d762f73c216f626b59463ce597e83 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 23 Jun 2025 12:56:00 -0400 Subject: [PATCH 8/8] Formatted and updated snapshots --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 3 +-- .../tests/schema-snapshots/no-base-to-string.shot | 5 ++--- 2 files changed, 3 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 18959dd65882..26eadce99839 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -49,8 +49,7 @@ export default createRule({ properties: { checkUnknown: { type: 'boolean', - description: - 'Whether to also check values of type `unknown`', + description: 'Whether to also check values of type `unknown`', }, ignoredTypeNames: { type: 'array', diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot b/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot index 4c348f18a6f0..207398f5c070 100644 --- a/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot +++ b/packages/eslint-plugin/tests/schema-snapshots/no-base-to-string.shot @@ -6,8 +6,7 @@ "additionalProperties": false, "properties": { "checkUnknown": { - "default": false, - "description": "Checks the case where toString is applied to unknown type", + "description": "Whether to also check values of type `unknown`", "type": "boolean" }, "ignoredTypeNames": { @@ -27,7 +26,7 @@ type Options = [ { - /** Checks the case where toString is applied to unknown type */ + /** Whether to also check values of type `unknown` */ checkUnknown?: boolean; /** Stringified regular expressions of type names to ignore. */ ignoredTypeNames?: string[];