Skip to content

feat(eslint-plugin): [no-unnecessary-type-conversion] add rule #10182

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

Open
wants to merge 41 commits into
base: main
Choose a base branch
from

Conversation

skondrashov
Copy link

@skondrashov skondrashov commented Oct 20, 2024

PR Checklist

Overview

Shows autofixable errors for:

// setup
let str = "string";
const num = 1;
const bool = true;
const big = BigInt(1);

// begin invalid cases

String("hello");
"hello".toString();
"" + "hello";
"hello" + "";

String(1 + "");
(1 + "").toString();
"" + (1 + "");
1 + "" + "";

String(str);
str.toString();
"" + str;
str + "";
str += "";

Number(+"1");
+(+"1");
~~+"1";

Number(num);
+num;
~~num;

Boolean(bool);
!!bool;

BigInt(big);

with the following error messaging:

   9:2  error  Passing a string to String() does not change the type or value of the string            @typescript-eslint/no-unnecessary-coercion
  10:9  error  Using .toString() on a string does not change the type or value of the string           @typescript-eslint/no-unnecessary-coercion
  11:2  error  Concatenating '' with a string does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  12:9  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  14:2  error  Passing a string to String() does not change the type or value of the string            @typescript-eslint/no-unnecessary-coercion
  15:9  error  Using .toString() on a string does not change the type or value of the string           @typescript-eslint/no-unnecessary-coercion
  16:2  error  Concatenating '' with a string does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  17:8  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  19:2  error  Passing a string to String() does not change the type or value of the string            @typescript-eslint/no-unnecessary-coercion
  20:5  error  Using .toString() on a string does not change the type or value of the string           @typescript-eslint/no-unnecessary-coercion
  21:2  error  Concatenating '' with a string does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  22:5  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  23:5  error  Concatenating a string with '' does not change the type or value of the string          @typescript-eslint/no-unnecessary-coercion
  25:2  error  Passing a number to Number() does not change the type or value of the number            @typescript-eslint/no-unnecessary-coercion
  26:2  error  Using the unary + operator on a number does not change the type or value of the number  @typescript-eslint/no-unnecessary-coercion
  27:2  error  Using ~~ on a number does not change the type or value of the number                    @typescript-eslint/no-unnecessary-coercion
  29:2  error  Passing a number to Number() does not change the type or value of the number            @typescript-eslint/no-unnecessary-coercion
  30:2  error  Using the unary + operator on a number does not change the type or value of the number  @typescript-eslint/no-unnecessary-coercion
  31:2  error  Using ~~ on a number does not change the type or value of the number                    @typescript-eslint/no-unnecessary-coercion
  33:2  error  Passing a boolean to Boolean() does not change the type or value of the boolean         @typescript-eslint/no-unnecessary-coercion
  34:2  error  Using !! on a boolean does not change the type or value of the boolean                  @typescript-eslint/no-unnecessary-coercion
  36:2  error  Passing a bigint to BigInt() does not change the type or value of the bigint            @typescript-eslint/no-unnecessary-coercion

Does not (and should not) show errors for:

// setup
let str = Math.random() > 0.5 ? "string" : 1;
const num = Math.random() > 0.5 ? 1 : "1";
const bool = Math.random() > 0.5 ? true : 1;
const big = Math.random() > 0.5 ? BigInt(1) : 1;

// begin valid cases

String(str);
str.toString();
`${str}`;
"" + str;
str + "";
str += "";

new String("asdf");

Number(num);
+num;
~~num;
~1;

Boolean(bool);
!!bool;
!false;

BigInt(big);

Does not do anything with Symbol() because everything weird I tried to do with symbols was already a typescript error.

Does not do anything for some conversions to number which are already forbidden by typescript ("The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type."), including:

"123" * 1;
"123" / 1;
"123" - 0;
"123" | 0;

Does not cover ${"123"}, because that overlaps with the no-unnecessary-template-expression rule. Need feedback on what to do about that.

I went with the word "coercion" because it was chosen by @Josh-Cena in the original issue but I think it's a poor choice. I think "conversion" or even "type conversion idiom" might be the most precise, because .toString() for example doesn't rely on coercion, and while !!value and !value both coerce the type of value to a boolean, it's the property of the idiom !! that it can be used for type conversion which puts it under the umbrella of this rule. So I think no-unnecessary-type-conversion is a better name for this rule, given the things that I'm currently having it check for.

Catches the following problems in this repository (separate commit):

typescript-eslint\packages\eslint-plugin\src\rules\adjacent-overload-signatures.ts
  79:21  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
  97:21  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\explicit-function-return-type.ts
  170:27  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion
  177:9   error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\member-ordering.ts
  492:14  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-duplicate-enum-values.ts
  50:21  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion
  52:21  error  Passing a number to Number() does not change the type or value of the number  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-empty-interface.ts
  84:44  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-redundant-type-constituents.ts
  176:10  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-unnecessary-template-expression.ts
   22:24  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion
  174:22  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\no-unsafe-assignment.ts
  202:17  error  Passing a string to String() does not change the type or value of the string  @typescript-eslint/no-unnecessary-coercion

typescript-eslint\packages\eslint-plugin\src\rules\prefer-return-this-type.ts
  64:14  error  Using !! on a boolean does not change the type or value of the boolean  @typescript-eslint/no-unnecessary-coercion

Might not have the best placement for the error scope, and might not have the best formatting for autofixes, could use feedback on those too.

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @skondrashov!

typescript-eslint is a 100% community driven project, and we are incredibly grateful that you are contributing to that community.

The core maintainers work on this in their personal time, so please understand that it may not be possible for them to review your work immediately.

Thanks again!


🙏 Please, if you or your company is finding typescript-eslint valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/typescript-eslint.

Copy link

netlify bot commented Oct 20, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 0d16798
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/68010c05d49cb80008638f80
😎 Deploy Preview https://deploy-preview-10182--typescript-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 94 (🔴 down 2 from production)
Accessibility: 100 (no change from production)
Best Practices: 100 (no change from production)
SEO: 98 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link

nx-cloud bot commented Oct 20, 2024

View your CI Pipeline Execution ↗ for commit 0d16798.

Command Status Duration Result
nx test eslint-plugin ✅ Succeeded 6m 5s View ↗
nx run eslint-plugin:test -- --coverage ✅ Succeeded 6m 32s View ↗
nx test eslint-plugin --coverage=false ✅ Succeeded 5m 11s View ↗
nx test type-utils ✅ Succeeded <1s View ↗
nx run types:build ✅ Succeeded 1s View ↗
nx run-many --target=lint --exclude eslint-plugin ✅ Succeeded 1m 26s View ↗
nx typecheck ast-spec ✅ Succeeded 2s View ↗
nx test visitor-keys ✅ Succeeded 1s View ↗
Additional runs (26) ✅ Succeeded ... View ↗

☁️ Nx Cloud last updated this comment at 2025-04-17 14:25:43 UTC

@skondrashov
Copy link
Author

skondrashov commented Oct 20, 2024

Sorry for force-push, wasn't sure how else to resolve the merge conflicts other than redoing the commit cause they didn't show up locally. Doesn't seem like it did anything to the merge conflict though, so I'm out of ideas.

I see I have problems from the perfectionist plugin in my files as well, but the plugin doesn't run locally when I use yarn lint, so I don't know what to do about that either.

Would be happy to address the 2 missing lines of coverage at some point if everything else gets figured out.

Copy link

codecov bot commented Oct 20, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 90.89%. Comparing base (f30a20e) to head (0d16798).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10182      +/-   ##
==========================================
+ Coverage   90.82%   90.89%   +0.06%     
==========================================
  Files         497      498       +1     
  Lines       50204    50552     +348     
  Branches     8274     8306      +32     
==========================================
+ Hits        45600    45948     +348     
  Misses       4589     4589              
  Partials       15       15              
Flag Coverage Δ
unittest 90.89% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/eslint-plugin/src/configs/eslintrc/all.ts 100.00% <100.00%> (ø)
...lugin/src/configs/eslintrc/disable-type-checked.ts 100.00% <100.00%> (ø)
packages/eslint-plugin/src/configs/flat/all.ts 100.00% <100.00%> (ø)
...nt-plugin/src/configs/flat/disable-type-checked.ts 100.00% <100.00%> (ø)
...t-plugin/src/rules/adjacent-overload-signatures.ts 100.00% <100.00%> (ø)
...-plugin/src/rules/explicit-function-return-type.ts 100.00% <100.00%> (ø)
...ackages/eslint-plugin/src/rules/member-ordering.ts 99.20% <100.00%> (ø)
...slint-plugin/src/rules/no-duplicate-enum-values.ts 100.00% <100.00%> (ø)
...ages/eslint-plugin/src/rules/no-empty-interface.ts 100.00% <100.00%> (ø)
...plugin/src/rules/no-redundant-type-constituents.ts 97.64% <100.00%> (ø)
... and 5 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Oct 21, 2024

// setup
let str = Math.random() > 0.5 ? "string" : 1;
const num = Math.random() > 0.5 ? 1 : "1";
const bool = Math.random() > 0.5 ? true : 1;
const big = Math.random() > 0.5 ? BigInt(1) : 1;

(hint) - We often use declare to do this sort of setup for demo purposes, i.e.

// setup
declare let str: string | number;
declare const num: number | string;
// etc

It's a useful pattern for testing sometimes too, since you may run into surprising behavior when const t = true; has type true rather than type boolean. Nothing specifically requested, just letting you know about another tool for your kit in case it comes in handy during discussion on this PR 🙂

Does not do anything with Symbol() because everything weird I tried to do with symbols was already a typescript error.

Makes sense 👍

Does not do anything for some conversions to number which are already forbidden by typescript ("The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type."), including:

"123" * 1;
"123" / 1;
"123" - 0;
"123" | 0;

Yep, also makes sense. And, even if TS did allow them, we don't need to stress about identifying every possible way for a value to be coerced, just the common patterns, and others can always be added in the future, too. Appreciate your close attention to detail on all these, though!

Does not cover ${"123"}, because that overlaps with the no-unnecessary-template-expression rule. Need feedback on what to do about that.

This is an astute observation. Is there anything this rule would flag that that rule doesn't flag? If not, I'd lean towards letting that separate rule handle this case. If you want you could even put a note in the docs that tells the user that template expressions intentionally aren't handled because they're already dealt with more thoroughly in the no-unnecessary-template-expression rule.

I went with the word "coercion" because it was chosen by @Josh-Cena in the original issue but I think it's a poor choice. I think "conversion" or even "type conversion idiom" might be the most precise, because .toString() for example doesn't rely on coercion, and while !!value and !value both coerce the type of value to a boolean, it's the property of the idiom !! that it can be used for type conversion which puts it under the umbrella of this rule. So I think no-unnecessary-type-conversion is a better name for this rule, given the things that I'm currently having it check for.

Hmm, I hear you. I personally think either one is fine. Maybe someone else will care more strongly 🤷

Might not have the best placement for the error scope, and might not have the best formatting for autofixes, could use feedback on those too.

I think for these reports, reporting the whole node is totally fine

declare const s: string;
s + '';
~~~~~~

!!(longExpressionThatsABoolean);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

But, I really like what you've done with the more granular approach.

!!(longExpressionThatsABoolean);
~~

So I'd say go for it! But if it becomes an implementation hassle, no stress either if you end up falling back to the typical strategy of letting the node dictate the report.

@kirkwaiblinger
Copy link
Member

Sorry for force-push...

No worries! The real pain is just force pushing between reviews, not before a reviewer has looked at it at all 🙂.

..., wasn't sure how else to resolve the merge conflicts other than redoing the commit cause they didn't show up locally.

Oh, so I think the trouble that you're having is that you've used the main branch on your fork. The "usual" vanilla workflow would be

  1. fork typescript-eslint. The main branch of your fork points to the HEAD of main of the original typescript-eslint at the time the fork was made.
  2. Create a branch for your feature, work on there.
  3. Whenever you need to synchronize upstream changes, use the github UI or CLI to synchronize the changes on typescript-eslint's main with your fork's main. Then merge those into your branch.

But you're in a slightly awkward scenario since your branch is on main. What I personally do (which differs from the above) is to set a second remote on your local repo, i.e. git remote add upstream git@github.com:typescript-eslint/typescript-eslint.git. Then you can git fetch --all && git checkout upstream/main (or similar) to make a branch from there, to merge changes into your PR branch. There's... (too) many ways to address this, good ol git, haha.

LMK if this helps or if you'd like some more help. (I can also push things directly to your branch to bail you out if needed 🤣). And no stress if you end up doing a force push again this time to get yourself in a clean state here!

Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great work! And especially impressive considering it's your first PR to typescript-eslint. Well done!

Requesting a few changes, but nothing major.

🙂

@Josh-Cena
Copy link
Member

I went with the word "coercion" because it was chosen by @Josh-Cena in the original issue but I think it's a poor choice. I think "conversion" or even "type conversion idiom" might be the most precise, because .toString() for example doesn't rely on coercion, and while !!value and !value both coerce the type of value to a boolean, it's the property of the idiom !! that it can be used for type conversion which puts it under the umbrella of this rule. So I think no-unnecessary-type-conversion is a better name for this rule, given the things that I'm currently having it check for.

Agreed; let's go with no-unnecessary-type-conversion instead.

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Nov 11, 2024
@JoshuaKGoldberg
Copy link
Member

👋 Just checking in @skondrashov - is this still something you have time and energy for?

@JoshuaKGoldberg JoshuaKGoldberg added the stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period label Dec 14, 2024
@skondrashov
Copy link
Author

I'll have time to finish up the changes probably early in the new year!

@JoshuaKGoldberg
Copy link
Member

Awesome! I'll move this to draft then in the meantime, so we don't bug you. 🚀

@JoshuaKGoldberg JoshuaKGoldberg marked this pull request as draft December 21, 2024 14:50
@skondrashov
Copy link
Author

After latest round of fixes, I believe there are 3 things I still need to do:

  1. Do scope analysis to see if .toString() has been overwritten before applying the rule in cases involving it.
  2. Move autofixes to suggestion fixes. I would like to understand the motivation for this change, but I just didn't have time to get to it today.
  3. Depending on feedback, do something with the fixes that use getWrappingFixer but don't need to pass a wrap parameter.

I left some comments in the appropriate threads. @kirkwaiblinger do you mind resolving the threads that have been resolved to your satisfaction? I don't want to do it myself in case I'm wrong in resolving something.

@kirkwaiblinger
Copy link
Member

After latest round of fixes, I believe there are 3 things I still need to do:

  1. Do scope analysis to see if .toString() has been overwritten before applying the rule in cases involving it.

Let's not do this, actually. It's complex to detect, a rare situation, the only situation in which it would affect correctness the rule's report is if someone overrides String.prototype.toString in a way that "foo".toString() does not return "foo" - not a situation we're interested in accommodating 🙃

  1. Move autofixes to suggestion fixes. I would like to understand the motivation for this change, but I just didn't have time to get to it today.

Sounds good 👍 On the motivation, for me:

  • Removing conversions to primitives is a pretty meaningful code change and can cause breakage if the TS types are not accurate at runtime. Can unpack this more, but I'd rather be cautious and demand human intervention rather than run an autofix, which many users have configured to fix on save.
  • We are providing two different approaches (with and without satisfies) for the fix, and the only way to express multiple fix options is with two suggestions, rather than an autofix (or providing a rule option to configure the fix - example - like but that's totally overkill here)
  1. Depending on feedback, do something with the fixes that use getWrappingFixer but don't need to pass a wrap parameter.

responded asking for more detail 👍

I left some comments in the appropriate threads. @kirkwaiblinger do you mind resolving the threads that have been resolved to your satisfaction? I don't want to do it myself in case I'm wrong in resolving something.

Will do! 👍

Copy link
Member

@kirkwaiblinger kirkwaiblinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another good iteration! Looking forward to the next round! 🙂

innerNode: [node.argument],
sourceCode: context.sourceCode,
};
const typeString =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes knowledge of its caller, and doesn't really seem to be necessary..... I was playing around and refactored a couple things, WDYT?

  • removed typeString deduction
  • provide report data explicitly (probably best to do since the report data have very loose types)
  • remove duplication of the isDoubleOperator deduction with outerNode variable (which tbh might be better off coming from the caller as well).
    function handleUnaryOperator(
      node: TSESTree.UnaryExpression,
      typeFlag: ts.TypeFlags,
      isDoubleOperator: boolean, // !! or ~~
      typeString: string,
      violation: string,
    ) {
      const outerNode = isDoubleOperator ? node.parent : node;
      const type = services.getTypeAtLocation(node.argument);
      if (doesUnderlyingTypeMatchFlag(type, typeFlag)) {
        context.report({
          loc: {
            start: outerNode.loc.start,
            end: {
              column: node.loc.start.column + 1,
              line: node.loc.start.line,
            },
          },
          messageId: 'unnecessaryTypeConversion',
          data: {
            type: typeString,
            violation,
          },
          fix: getWrappingFixer({
            node: outerNode,
            innerNode: [node.argument],
            sourceCode: context.sourceCode,
          }),
          suggest: [
            {
              messageId: 'unnecessaryTypeConversionSuggestion',
              data: { type: typeString },
              fix: getWrappingFixer({
                node: outerNode,
                innerNode: [node.argument],
                sourceCode: context.sourceCode,
                wrap: expr => `${expr} satisfies ${typeString}`,
              }),
            },
          ],
        });
      }
    }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lovely, did this almost exactly but with typeString: string, changed to typeString: 'boolean' | 'number', and the order of parameters changed so that isDoubleOperator is last cause it feels like it should be. I feel like you probably don't care about the order of the parameters, but let me know if you think changing the type is a bad move.

getConstrainedTypeAtLocation(services, node.arguments[0]);

const isBuiltInCall = (name: string) => {
if ((node.callee as TSESTree.Identifier).name === name) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can avoid this type assertion by

  1. Define a type predicate for the node type const isIdentifier = isNodeOfType(AST_NODE_TYPES.Identifier)
  2. Alias the node.callee to use it in the closure,
      CallExpression(node: TSESTree.CallExpression): void {
        if (
          isIdentifier(node.callee) &&
          node.arguments.length === 1
        ) {
          const nodeCallee = node.callee;
          const getType = () =>
            getConstrainedTypeAtLocation(services, node.arguments[0]);

          function isBuiltInCall(name: string) {
            if (nodeCallee.name === name) {
              const scope = context.sourceCode.getScope(node);
              const variable = scope.set.get(name);
              return !variable?.defs.length;
            }
            return false;
          }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact, I think it's a bug in TS that you need to alias nodeCallee to preserve the narrowing in the closure, see microsoft/TypeScript#61158

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an advantage of using isNodeOfType over nodeCallee.type === AST_NODE_TYPES.Identifier? Aliasing nodeCallee seemed to solve the problem on its own.

}
}
},
'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today I learned that you could use ESQuery with the .callee like this. Cool!

Question, though, does the .property at the end do anything? I can't think what it would do and no tests fail if I change it to

Suggested change
'CallExpression > MemberExpression.callee > Identifier[name = "toString"].property'(
'CallExpression > MemberExpression.callee > Identifier[name = "toString"]'(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure the selector is correct could you also add the following (valid) test case?

export {};
declare const toString: string;
toString.toUpperCase();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That test fails without the .property, which I guess is what you had in mind? Adding the test, leaving the selector for that reason.

const services = getParserServices(context);
const checker = services.program.getTypeChecker();

const surroundWithParentheses = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind providing a few examples? Would save me quite a bit of time 🙏 In general, we're not too stressed about some extra parens, which most users are likely to fix with a formatter anyway, unlike too few parens, which will actually break people's code.

});

ruleTester.run('no-unnecessary-type-conversion', rule, {
valid: [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(testing)
Could you please add some valid cases around ensuring that only primitives cause the rule to flag? In particular, I'm thinking about the boxed primitives, so, things like

String(new String('foo'));

function f(x: Number) {
    return +x;
}

etc

Also it would be good to add some tests around stringy and numbery enums.

Also let's add some coverage around generics, i.e.

function f<T extends string>(x: T) {
    return String(x);
}

Copy link
Author

@skondrashov skondrashov Mar 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added cases for boxed primitives and generics, but enums are elusive since I can't copy the tests for no-unnecessary-template-expression - there are no tests for enums there, and it has what I think is a bug as a result:

enum Foo {
  A = "A",
}

function f(foo: Foo) {
  return `${foo}`;
}

const c = `${Foo.A}`;

This finds autofixes for both the string templates, but applying them changes the type of c or the return type of f from Foo to string if applied. I'm not sure if this is a bug but this PR does the same thing, albeit with String(foo) instead of `${foo}`.

@kirkwaiblinger kirkwaiblinger changed the title feat(eslint-plugin): [no-unnecessary-coercion] add rule feat(eslint-plugin): [no-unnecessary-type-conversion] add rule Feb 10, 2025
@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Feb 10, 2025
@jdufresne

This comment was marked as resolved.

@kirkwaiblinger
Copy link
Member

@jdufresne fun suggestion! Yeah, let's scope this as an enhancement rather than part of the initial implementation 👍

@jdufresne

This comment was marked as resolved.

@skondrashov
Copy link
Author

skondrashov commented Apr 5, 2025

@kirkwaiblinger I think I'm ready for another look.

I left some replies, but I thought I'd reply to the getWrappingFixer questions here. I can only think of one case where there are extra parentheses added by the current code: 'a' + 'b' + ''; -> ('a' + 'b');. Which is annoying but harmless.

I said before in one of my comments that there are cases with double parentheses, but I couldn't find any now. Not sure if I was wrong before, but everything else I tried this time made sense. Nothing with too few parentheses and just one with too many. I found some sets of patterns that might be enlightening to play with anyway -

These patterns (and similar for other types) strip all parentheses, no matter how many you put:

String(((('a'))));    // -> 'a';
((('a'))).toString(); // -> 'a';
((('a'))) + '';       // -> 'a';

If the thing being operated on isn't unary, the output gets a single set of parentheses, again regardless of how many there are. The last case here is the one that feels like it's adding extra parentheses:

String('a' + 'b');      // -> ('a' + 'b');
('a' + 'b').toString(); // -> ('a' + 'b');
'a' + 'b' + '';         // -> ('a' + 'b');

Using the satisfies fix on these is enlightening because it places the right number of pairs of parentheses in the proper places:

'a'.toString();                  // -> 'a' satisfies string;
'a'.toString().length;           // -> ('a' satisfies string).length;
(('a').toString()).length;       // -> ('a' satisfies string).length;
('a' + 'b').toString().length;   // -> (('a' + 'b') satisfies string).length;
(('a' + 'b').toString()).length; // -> (('a' + 'b') satisfies string).length;

Examples like this show that everything somehow works fine with weird double operators, parentheses, and whitespace:

( ~ ( ~ ( 5 ) ) ); // -> ( 5 );

I think my main concern is whether what I did to the code in getWrappingFixer is acceptable, and what I can do to make it better if not.

Still to do:

  1. Depending on feedback, do something with the fixes that use getWrappingFixer but don't need to pass a wrap parameter.
  2. Figure out what to do about the incorrect fixes for enums where all the fields are the same primitive type.
  3. Maybe other feedback from threads?

@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Apr 5, 2025
@fregante
Copy link
Contributor

Excited about this rule! I just saw a lot of Boolean('x' in globalThis) in a repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rule proposal: no-unnecessary-coercion
6 participants