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 1b2d21320bcf..1da882c1ebe4 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/no-base-to-string** for documentation. -JavaScript will call `toString()` on an object when it is converted to a string, such as when `+` adding to a string or in `${}` template literals. +JavaScript will call `toString()` on an object when it is converted to a string, such as when concatenated with a string (`expr + ''`), when interpolated into template literals (`${expr}`), or when passed as an argument to the String constructor (`String(expr)`). The default Object `.toString()` and `toLocaleString()` use the format `"[object Object]"`, which is often not what was intended. This rule reports on stringified values that aren't primitives and don't define a more useful `.toString()` or `toLocaleString()` method. @@ -31,6 +31,7 @@ value + ''; // Interpolation and manual .toString() and `toLocaleString()` calls too: `Value: ${value}`; +String({}); ({}).toString(); ({}).toLocaleString(); ``` @@ -44,6 +45,7 @@ value + ''; `Value: ${123}`; `Arrays too: ${[1, 2, 3]}`; (() => {}).toString(); +String(42); (() => {}).toLocaleString(); // Defining a custom .toString class is considered acceptable @@ -64,6 +66,15 @@ const literalWithToString = { +## Alternatives + +Consider using `JSON.stringify` when you want to convert non-primitive things to string for logging, debugging, etc. + +```typescript +declare const o: object; +const errorMessage = 'Found unexpected value: ' + JSON.stringify(o); +``` + ## Options ### `ignoredTypeNames` @@ -82,6 +93,7 @@ The following patterns are considered correct with the default options `{ ignore let value = /regex/; value.toString(); let text = `${value}`; +String(/regex/); ``` ## When Not To Use It 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 86f5a3cc1777..7aad839a584a 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/internal/prefer-ast-types-enum */ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -59,7 +60,7 @@ export default createRule({ const checker = services.program.getTypeChecker(); const ignoredTypeNames = option.ignoredTypeNames ?? []; - function checkExpression(node: TSESTree.Expression, type?: ts.Type): void { + function checkExpression(node: TSESTree.Node, type?: ts.Type): void { if (node.type === AST_NODE_TYPES.Literal) { return; } @@ -153,6 +154,19 @@ export default createRule({ return Usefulness.Never; } + function isBuiltInStringCall(node: TSESTree.CallExpression): boolean { + if ( + node.callee.type === AST_NODE_TYPES.Identifier && + node.callee.name === 'String' && + node.arguments[0] + ) { + const scope = context.sourceCode.getScope(node); + const variable = scope.set.get('String'); + return !variable?.defs.length; + } + return false; + } + return { 'AssignmentExpression[operator = "+="], BinaryExpression[operator = "+"]'( node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression, @@ -169,6 +183,11 @@ export default createRule({ checkExpression(node.left, leftType); } }, + CallExpression(node: TSESTree.CallExpression): void { + if (isBuiltInStringCall(node)) { + checkExpression(node.arguments[0]); + } + }, 'CallExpression > MemberExpression.callee > Identifier[name = /^(toLocaleString|toString)$/].property'( node: TSESTree.Expression, ): void { 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 afc8aa555457..1226f0a5f7ff 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 @@ -15,6 +15,8 @@ value + ''; // Interpolation and manual .toString() and \`toLocaleString()\` calls too: \`Value: \${value}\`; ~~~~~ 'value' will use Object's default stringification format ('[object Object]') when stringified. +String({}); + ~~ '{}' will use Object's default stringification format ('[object Object]') when stringified. ({}).toString(); ~~ '{}' will use Object's default stringification format ('[object Object]') when stringified. ({}).toLocaleString(); @@ -30,6 +32,7 @@ exports[`Validating rule docs no-base-to-string.mdx code examples ESLint output \`Value: \${123}\`; \`Arrays too: \${[1, 2, 3]}\`; (() => {}).toString(); +String(42); (() => {}).toLocaleString(); // Defining a custom .toString class is considered acceptable @@ -57,5 +60,6 @@ exports[`Validating rule docs no-base-to-string.mdx code examples ESLint output let value = /regex/; value.toString(); let text = \`\${value}\`; +String(/regex/); " `; 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 b57c9d3a78df..5aebfa112f9a 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 @@ -60,6 +60,8 @@ ruleTester.run('no-base-to-string', rule, { `, ), + // String() + ...literalList.map(i => `String(${i});`), ` function someFunction() {} someFunction.toString(); @@ -132,6 +134,28 @@ tag\`\${{}}\`; "'' += new Error();", "'' += new URL();", "'' += new URLSearchParams();", + ` +let numbers = [1, 2, 3]; +String(...a); + `, + ` +Number(1); + `, + { + code: 'String(/regex/);', + options: [{ ignoredTypeNames: ['RegExp'] }], + }, + ` +function String(value) { + return value; +} +declare const myValue: object; +String(myValue); + `, + ` +import { String } from 'foo'; +String({}); + `, ], invalid: [ { @@ -182,6 +206,33 @@ tag\`\${{}}\`; }, ], }, + { + code: 'String({});', + errors: [ + { + data: { + certainty: 'will', + name: '{}', + }, + messageId: 'baseToString', + }, + ], + }, + { + code: ` +let objects = [{}, {}]; +String(...objects); + `, + errors: [ + { + data: { + certainty: 'will', + name: '...objects', + }, + messageId: 'baseToString', + }, + ], + }, { code: "'' += {};", errors: [