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..d1927bf69b5b 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; +String(x); +``` + ## 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/src/rules/no-base-to-string.ts b/packages/eslint-plugin/src/rules/no-base-to-string.ts index 6617c8d1dea2..26eadce99839 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[]; + checkUnknown?: boolean; }, ]; export type MessageIds = 'baseArrayJoin' | 'baseToString'; @@ -46,6 +47,10 @@ export default createRule({ type: 'object', additionalProperties: false, properties: { + checkUnknown: { + type: 'boolean', + description: 'Whether to also check values of type `unknown`', + }, ignoredTypeNames: { type: 'array', description: @@ -60,6 +65,7 @@ export default createRule({ }, defaultOptions: [ { + checkUnknown: false, ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams'], }, ], @@ -76,6 +82,7 @@ export default createRule({ type ?? services.getTypeAtLocation(node), new Set(), ); + if (certainty === Usefulness.Always) { return; } @@ -213,7 +220,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() @@ -251,8 +258,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/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..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 @@ -56,3 +56,9 @@ let value = /regex/; value.toString(); let text = `${value}`; String(/regex/); + +Options: { "checkUnknown": true } + +declare const x: unknown; +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 f0fe39979c4d..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 @@ -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,202 @@ 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); +} + `, + ` +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: [ + { + 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: [ 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..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 @@ -5,6 +5,10 @@ { "additionalProperties": false, "properties": { + "checkUnknown": { + "description": "Whether to also check values of type `unknown`", + "type": "boolean" + }, "ignoredTypeNames": { "description": "Stringified regular expressions of type names to ignore.", "items": { @@ -22,6 +26,8 @@ type Options = [ { + /** Whether to also check values of type `unknown` */ + checkUnknown?: boolean; /** Stringified regular expressions of type names to ignore. */ ignoredTypeNames?: string[]; },