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/3] 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/3] 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/3] 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[]; },