From 00754ea2dfbdf3be717dcf0ae300df606516f42d Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 12 Oct 2024 19:23:33 +0900 Subject: [PATCH 01/11] feat: add support for toLocaleString method --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 6 ++++-- 1 file changed, 4 insertions(+), 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 df8fa6bb13cf..0bd7e423f060 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -82,7 +82,9 @@ export default createRule({ } 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; @@ -167,7 +169,7 @@ export default createRule({ 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; From 6ef7066780b0c41c6c898aa735f5dd4556a90204 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 12 Oct 2024 19:28:53 +0900 Subject: [PATCH 02/11] test: add Tests --- .../tests/rules/no-base-to-string.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) 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 8cd205a13a9f..89f92f04a5f1 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 @@ -51,6 +51,11 @@ ruleTester.run('no-base-to-string', rule, { // toString() ...literalListWrapped.map(i => `${i === '1' ? `(${i})` : i}.toString();`), + // toLocalString() + ...literalListWrapped.map( + i => `${i === '1' ? `(${i})` : i}.toLocalString();`, + ), + // variable toString() and template ...literalList.map( i => ` @@ -60,12 +65,27 @@ ruleTester.run('no-base-to-string', rule, { `, ), + // variable toLocalString() and template + ...literalList.map( + i => ` + let value = ${i}; + value.toLocalString(); + let text = \`\${value}\`; + `, + ), + ` function someFunction() {} someFunction.toString(); +let text = \`\${someFunction}\`; + `, + ` +function someFunction() {} +someFunction.toLocaleString(); let text = \`\${someFunction}\`; `, 'unknownObject.toString();', + 'unknownObject.toLocaleString();', 'unknownObject.someOtherMethod();', ` class CustomToString { @@ -87,6 +107,14 @@ const printer = (inVar: string | number | boolean) => { }; printer(''); printer(1); +printer(true); + `, + ` +const printer = (inVar: string | number | boolean) => { + inVar.toLocaleString(); +}; +printer(''); +printer(1); printer(true); `, 'let _ = {} * {};', @@ -144,6 +172,18 @@ tag\`\${{}}\`; }, ], }, + { + code: '({}).toLocaleString();', + errors: [ + { + data: { + certainty: 'will', + name: '{}', + }, + messageId: 'baseToString', + }, + ], + }, { code: "'' + {};", errors: [ @@ -183,6 +223,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'; @@ -213,6 +268,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 }; From 3e4bcff508b72194c60dd7106192c61d512d96ba Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 12 Oct 2024 19:29:18 +0900 Subject: [PATCH 03/11] test: fix test --- packages/eslint-plugin/tests/rules/no-base-to-string.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 89f92f04a5f1..a7f2091ba712 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 @@ -99,7 +99,7 @@ class CustomToString { const literalWithToString = { toString: () => 'Hello, world!', }; -'' + literalToString; +'' + literalWithToString; `, ` const printer = (inVar: string | number | boolean) => { From c246f0a8cf46738e6751278f6a7420d7bd973e5b Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 12 Oct 2024 20:00:47 +0900 Subject: [PATCH 04/11] docs: update doc --- .../docs/rules/no-base-to-string.mdx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 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 97767da415b8..5a491ef06aa0 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -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'; @@ -9,11 +9,11 @@ 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. +JavaScript will call `toString()` or `toLocaleString()` 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()` or `toLocaleString()` 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()` or `toLocaleString()` method. -> Note that `Function` provides its own `.toString()` that returns the function's code. +> Note that `Function` provides its own `.toString()` or `toLocaleString()` that returns the function's code. > Functions are not flagged by this rule. ## Examples @@ -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(); ``` ```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 { @@ -67,8 +69,8 @@ const literalWithToString = { ### `ignoredTypeNames` Stringified regular expressions of type names to ignore. -This is useful for types missing `toString()` (but actually has `toString()`). -There are some types missing `toString()` in old version 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 version TypeScript, like `RegExp`, `URL`, `URLSearchParams` etc. The following patterns are considered correct with the default options `{ ignoredTypeNames: ["RegExp"] }`: @@ -93,3 +95,4 @@ 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) From f37af6a6c2edc96979a038b9f04cc1dbed3c16b9 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 12 Oct 2024 21:51:04 +0900 Subject: [PATCH 05/11] test: update snapshot and fix description --- packages/eslint-plugin/src/rules/no-base-to-string.ts | 2 +- .../docs-eslint-output-snapshots/no-base-to-string.shot | 7 +++++-- 2 files changed, 6 insertions(+), 3 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 0bd7e423f060..86f5a3cc1777 100644 --- a/packages/eslint-plugin/src/rules/no-base-to-string.ts +++ b/packages/eslint-plugin/src/rules/no-base-to-string.ts @@ -24,7 +24,7 @@ export default createRule({ 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, }, 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 f5bc9c118ff1..489c9528033d 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 @@ -12,22 +12,25 @@ const value = new MyClass(); value + ''; ~~~~~ 'value' will use Object's default stringification format ('[object Object]') when stringified. -// Interpolation and manual .toString() calls too: +// Interpolation and manual .toString() and .toLocaleString() calls too: \`Value: \${value}\`; ~~~~~ 'value' will use Object's default stringification format ('[object Object]') when stringified. ({}).toString(); ~~ '{}' will use Object's default stringification format ('[object Object]') when stringified. +({}).toLocaleString(); + ~~ '{}' will use Object's default stringification format ('[object Object]') when stringified. " `; exports[`Validating rule docs no-base-to-string.mdx code examples ESLint output 2`] = ` "Correct -// 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 { From ebc6947e66442846a034866946bf8e43d86fa4a7 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 14 Oct 2024 23:48:25 +0900 Subject: [PATCH 06/11] docs: fix typo --- packages/eslint-plugin/docs/rules/no-base-to-string.mdx | 2 +- 1 file changed, 1 insertion(+), 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 5a491ef06aa0..f11ce77d51e4 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -70,7 +70,7 @@ const literalWithToString = { Stringified regular expressions of type names to ignore. 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 version TypeScript, like `RegExp`, `URL`, `URLSearchParams` etc. +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"] }`: From 31a7cf4d164836c8613eb10f2db06dba15f9d16e Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Tue, 15 Oct 2024 14:57:45 +0900 Subject: [PATCH 07/11] test: fix typo and remove test --- .../tests/rules/no-base-to-string.test.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) 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 a7f2091ba712..972c15e2633d 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 @@ -51,9 +51,9 @@ ruleTester.run('no-base-to-string', rule, { // toString() ...literalListWrapped.map(i => `${i === '1' ? `(${i})` : i}.toString();`), - // toLocalString() + // toLocaleString() ...literalListWrapped.map( - i => `${i === '1' ? `(${i})` : i}.toLocalString();`, + i => `${i === '1' ? `(${i})` : i}.toLocaleString();`, ), // variable toString() and template @@ -65,15 +65,6 @@ ruleTester.run('no-base-to-string', rule, { `, ), - // variable toLocalString() and template - ...literalList.map( - i => ` - let value = ${i}; - value.toLocalString(); - let text = \`\${value}\`; - `, - ), - ` function someFunction() {} someFunction.toString(); From 3ffe462fb295502aaf902f482479c5d87c95fda6 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:20:12 +0900 Subject: [PATCH 08/11] Update packages/eslint-plugin/docs/rules/no-base-to-string.mdx Co-authored-by: Flo Edelmann --- packages/eslint-plugin/docs/rules/no-base-to-string.mdx | 2 +- 1 file changed, 1 insertion(+), 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 f11ce77d51e4..38b0e732cd46 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()` or `toLocaleString()` 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 `+` adding to a string or in `${}` template literals. The default Object `.toString()` or `toLocaleString()` 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()` or `toLocaleString()` method. From 1c49fd6cbe3be6a12177b26cfb4613d9661c422b Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:20:44 +0900 Subject: [PATCH 09/11] Update packages/eslint-plugin/docs/rules/no-base-to-string.mdx Co-authored-by: Flo Edelmann --- packages/eslint-plugin/docs/rules/no-base-to-string.mdx | 2 +- 1 file changed, 1 insertion(+), 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 38b0e732cd46..e9581e6b7d98 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -13,7 +13,7 @@ JavaScript will call `toString()` on an object when it is converted to a string, The default Object `.toString()` or `toLocaleString()` 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()` or `toLocaleString()` method. -> Note that `Function` provides its own `.toString()` or `toLocaleString()` 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 From 379dba66ac8082bbddbc44dc6bfb943b97a044aa Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:21:21 +0900 Subject: [PATCH 10/11] =?UTF-8?q?no-base-to-string.mdx=20=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/docs/rules/no-base-to-string.mdx | 2 +- 1 file changed, 1 insertion(+), 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 e9581e6b7d98..28f36b84998d 100644 --- a/packages/eslint-plugin/docs/rules/no-base-to-string.mdx +++ b/packages/eslint-plugin/docs/rules/no-base-to-string.mdx @@ -10,7 +10,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. -The default Object `.toString()` or `toLocaleString()` uses the format `"[object Object]"`, which is often not what was intended. +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()` and `toLocaleString()` that return the function's code. From b4c7216c34fda224fcae2b8b19fab6a754c0f115 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:24:20 +0900 Subject: [PATCH 11/11] Update packages/eslint-plugin/tests/rules/no-base-to-string.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- packages/eslint-plugin/tests/rules/no-base-to-string.test.ts | 5 ----- 1 file changed, 5 deletions(-) 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 972c15e2633d..b57c9d3a78df 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 @@ -51,11 +51,6 @@ ruleTester.run('no-base-to-string', rule, { // toString() ...literalListWrapped.map(i => `${i === '1' ? `(${i})` : i}.toString();`), - // toLocaleString() - ...literalListWrapped.map( - i => `${i === '1' ? `(${i})` : i}.toLocaleString();`, - ), - // variable toString() and template ...literalList.map( i => `