Skip to content

feat(eslint-plugin): [no-base-to-string] add support for catching toLocaleString #10138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions packages/eslint-plugin/docs/rules/no-base-to-string.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: 'Require `.toString()` to only be called on objects which provide useful information when stringified.'
description: 'Require `.toString()` and `.toLocaleString()` to only be called on objects which provide useful information when stringified.'
---

import Tabs from '@theme/Tabs';
Expand All @@ -10,10 +10,10 @@ 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.
The default Object `.toString()` uses 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()` method.
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.

> Note that `Function` provides its own `.toString()` that returns the function's code.
> Note that `Function` provides its own `.toString()` and `toLocaleString()` that return the function's code.
> Functions are not flagged by this rule.

## Examples
Expand All @@ -29,20 +29,22 @@ class MyClass {}
const value = new MyClass();
value + '';

// Interpolation and manual .toString() calls too:
// Interpolation and manual .toString() and `toLocaleString()` calls too:
`Value: ${value}`;
({}).toString();
({}).toLocaleString();
```

</TabItem>
<TabItem value="✅ Correct">

```ts
// These types all have useful .toString()s
// These types all have useful .toString() and `toLocaleString()` methods
'Text' + true;
`Value: ${123}`;
`Arrays too: ${[1, 2, 3]}`;
(() => {}).toString();
(() => {}).toLocaleString();

// Defining a custom .toString class is considered acceptable
class CustomToString {
Expand All @@ -68,8 +70,8 @@ const literalWithToString = {

{/* insert option description */}

This is useful for types whose declarations missing `toString()`, but are known to actually have `toString()`.
There are some types missing `toString()` in TypeScript, like `RegExp`, `URL`, `URLSearchParams` etc.
This is useful for types missing `toString()` or `toLocaleString()` (but actually has `toString()` or `toLocaleString()`).
There are some types missing `toString()` or `toLocaleString()` in old versions of TypeScript, like `RegExp`, `URL`, `URLSearchParams` etc.

The following patterns are considered correct with the default options `{ ignoredTypeNames: ["RegExp"] }`:

Expand All @@ -94,4 +96,5 @@ If you don't mind a risk of `"[object Object]"` or incorrect type coercions in y
## Further Reading

- [`Object.prototype.toString()` MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)
- [`Object.prototype.toLocaleString()` MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toLocaleString)
- [Microsoft/TypeScript Add missing toString declarations for base types that have them](https://github.com/microsoft/TypeScript/issues/38347)
8 changes: 5 additions & 3 deletions packages/eslint-plugin/src/rules/no-base-to-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default createRule<Options, MessageIds>({
type: 'suggestion',
docs: {
description:
'Require `.toString()` to only be called on objects which provide useful information when stringified',
'Require `.toString()` and `.toLocaleString()` to only be called on objects which provide useful information when stringified',
recommended: 'recommended',
requiresTypeChecking: true,
},
Expand Down Expand Up @@ -82,7 +82,9 @@ export default createRule<Options, MessageIds>({
}

function collectToStringCertainty(type: ts.Type): Usefulness {
const toString = checker.getPropertyOfType(type, 'toString');
const toString =
checker.getPropertyOfType(type, 'toString') ??
checker.getPropertyOfType(type, 'toLocaleString');
const declarations = toString?.getDeclarations();
if (!toString || !declarations || declarations.length === 0) {
return Usefulness.Always;
Expand Down Expand Up @@ -167,7 +169,7 @@ export default createRule<Options, MessageIds>({
checkExpression(node.left, leftType);
}
},
'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
'CallExpression > MemberExpression.callee > Identifier[name = /^(toLocaleString|toString)$/].property'(
node: TSESTree.Expression,
): void {
const memberExpr = node.parent as TSESTree.MemberExpression;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 57 additions & 1 deletion packages/eslint-plugin/tests/rules/no-base-to-string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,15 @@ ruleTester.run('no-base-to-string', rule, {
`
function someFunction() {}
someFunction.toString();
let text = \`\${someFunction}\`;
`,
`
function someFunction() {}
someFunction.toLocaleString();
let text = \`\${someFunction}\`;
`,
'unknownObject.toString();',
'unknownObject.toLocaleString();',
'unknownObject.someOtherMethod();',
`
class CustomToString {
Expand All @@ -79,14 +85,22 @@ class CustomToString {
const literalWithToString = {
toString: () => 'Hello, world!',
};
'' + literalToString;
'' + literalWithToString;
`,
`
const printer = (inVar: string | number | boolean) => {
inVar.toString();
};
printer('');
printer(1);
printer(true);
`,
`
const printer = (inVar: string | number | boolean) => {
inVar.toLocaleString();
};
printer('');
printer(1);
printer(true);
`,
'let _ = {} * {};',
Expand Down Expand Up @@ -144,6 +158,18 @@ tag\`\${{}}\`;
},
],
},
{
code: '({}).toLocaleString();',
errors: [
{
data: {
certainty: 'will',
name: '{}',
},
messageId: 'baseToString',
},
],
},
{
code: "'' + {};",
errors: [
Expand Down Expand Up @@ -183,6 +209,21 @@ tag\`\${{}}\`;
},
],
},
{
code: `
let someObjectOrString = Math.random() ? { a: true } : 'text';
someObjectOrString.toLocaleString();
`,
errors: [
{
data: {
certainty: 'may',
name: 'someObjectOrString',
},
messageId: 'baseToString',
},
],
},
{
code: `
let someObjectOrString = Math.random() ? { a: true } : 'text';
Expand Down Expand Up @@ -213,6 +254,21 @@ tag\`\${{}}\`;
},
],
},
{
code: `
let someObjectOrObject = Math.random() ? { a: true, b: true } : { a: true };
someObjectOrObject.toLocaleString();
`,
errors: [
{
data: {
certainty: 'will',
name: 'someObjectOrObject',
},
messageId: 'baseToString',
},
],
},
{
code: `
let someObjectOrObject = Math.random() ? { a: true, b: true } : { a: true };
Expand Down
Loading