Skip to content

fix(eslint-plugin): [no-unnecessary-template-expression] add missing parentheses in autofix #8673

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

Conversation

developer-bandi
Copy link
Contributor

PR Checklist

Overview

Fix no consider precedence when fixer function is excution using util function.

@typescript-eslint
Copy link
Contributor

Thanks for the PR, @developer-bandi!

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 Mar 14, 2024

Deploy Preview for typescript-eslint ready!

Name Link
🔨 Latest commit 1d536b6
🔍 Latest deploy log https://app.netlify.com/sites/typescript-eslint/deploys/66c3789a07a1300008113bce
😎 Deploy Preview https://deploy-preview-8673--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: 99 (🔴 down 1 from production)
Accessibility: 100 (no change from production)
Best Practices: 92 (no change from production)
SEO: 90 (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 Mar 14, 2024

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 1d536b6. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this CI Pipeline Execution


✅ Successfully ran 2 targets

Sent with 💌 from NxCloud.

Copy link

codecov bot commented Mar 14, 2024

Codecov Report

Attention: Patch coverage is 84.61538% with 2 lines in your changes missing coverage. Please review.

Project coverage is 88.01%. Comparing base (05d1ddb) to head (1d536b6).
Report is 2 commits behind head on main.

Files Patch % Lines
...ackages/eslint-plugin/src/util/getWrappingFixer.ts 81.81% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8673      +/-   ##
==========================================
- Coverage   88.02%   88.01%   -0.01%     
==========================================
  Files         406      406              
  Lines       13871    13881      +10     
  Branches     4053     4056       +3     
==========================================
+ Hits        12210    12218       +8     
- Misses       1352     1354       +2     
  Partials      309      309              
Flag Coverage Δ
unittest 88.01% <84.61%> (-0.01%) ⬇️

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

Files Coverage Δ
...in/src/rules/no-unnecessary-template-expression.ts 100.00% <100.00%> (ø)
...ackages/eslint-plugin/src/util/getWrappingFixer.ts 93.42% <81.81%> (-2.04%) ⬇️

Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

I like the approach here of using getWrappingFixer! 👏

I also think the code can be refactored a bit to remove the extra layer of indirection & the as. Requesting changes on that please. :)

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Mar 25, 2024
@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Mar 30, 2024
@bradzacher bradzacher added the bug Something isn't working label Apr 4, 2024
Comment on lines 22 to 28
const replaceResult = getWrappingFixer({
sourceCode: context.sourceCode,
node: node.expressions[0],
wrap: (...code: string[]) => {
return code.join('');
},
})(fixer);
Copy link
Member

Choose a reason for hiding this comment

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

[Refactor] Coming from #8673 (comment):

Instead of adding the overhead of the rest of getWrappingFixer(), suggestion: the specific text generation stuff should be extracted out and used here. Like, you could move the text generation from the getWrappingFixer into a separate function, and use that separate helper here.

I think this means that we should extract codegen part from getWrappingFixer implementation to the separate utility function, not extract whole getWrappingFixer call to the separate function. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I misunderstood the explanation. thank you The code generation part that generates parentheses was separated and made into a function.

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Apr 8, 2024
@@ -207,7 +207,7 @@ ruleTester.run('no-useless-template-literals', rule, {

{
code: noFormat`\`\${ 'a' + 'b' }\`;`,
output: `'a' + 'b';`,
output: `('a' + 'b');`,
Copy link
Member

Choose a reason for hiding this comment

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

How come this test case gains parens? Is that just the getWrappingFixer being overly cautions?

Copy link
Member

Choose a reason for hiding this comment

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

I also have this question. Feels like a bug to me?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first, I thought it would be good for the expression to be wrapped, but as you said, I think it's excessive and looks like a bug.

However, in order to restore the test code, the getWrappingFixer function must be modified. Would it be a good idea to modify this function?

export function getWrappingFixer(
params: WrappingFixerParams,
): TSESLint.ReportFixFunction {
export function getWrappingCode(params: WrappingFixerParams): string {
Copy link
Member

Choose a reason for hiding this comment

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

One thing worth noting is that this uses the parent node to infer whether parens are needed (see if (isWeakPrecedenceParent(node))). So I'm wondering if that could give mistaken results for a scenario where we're reparenting code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand that you are using the above node to infer whether parentheses are needed, but I don't understand this part (So I'm wondering if that could give mistaken results for a scenario where we're reparenting code?). Can you tell me in more detail what the scenario is for reparenting the code?

Copy link
Member

Choose a reason for hiding this comment

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

Sorry for taking a bit to get back to you - took me a while to figure out what I meant 😆 . Basically, getWrappingFixer() allows you to transform a node whose parent did not change.

// input
<stuff before> nodeToBeModified <stuff after>
// output
<stuff before> modifiedNode <stuff after>

But, now that getWrappingCode has been extracted, it can be used instead like

// input
<stuff before> nodeToBeModified <stuff after>
// getWrappingCode output
modifiedNode
// used in fixer as
<DIFFERENT stuff before> modifiedNode <DIFFERENT stuff after>

In fact, that's the whole reason that you've extracted it in this PR.

// input
condition ? `${'left' || 'right'}`.trim() : 'lol';
// getWrappingCodeOutput
('left' || 'right')
// used in fixer as 
condition ? ('left' || 'right').trim() : 'lol';

So, getWrappingCode needs to deduce whether to wrap the expression in parentheses based on the NEW parent, not the existing one.

Used within the getWrappingFixer those were the one and the same, so the line if (isWeakPrecedenceParent(node)) was justified, but now that getWrappingCode may be used indepdently, we'd need something like if (isWeakPrecedenceNewParent(node, newParent)), in order for the function to give expected results in general.

I'd have to have a longer think to come up with test cases where this discrepancy would manifest, but hopefully this gets you started? If it's still unclear let me know and I can think up some test cases. The bottom line is that we need to be aware of whether parens are needed for the new parent, not the existing parent, in order for getWrappingCode to be safe for general use outside of getWrappingFixer.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I apologize for not clear question. But the answers were the best!

If my understanding is correct, the parent that the isWeakPrecedenceNewParent function checks must be the new parent. In fact, the previous code was checking the parent of the expression, so it was always checking the templete literal node as the parent, so it was not getting normal results.

However, if i slightly modify the code to satisfy the above conditions, i will get the following results depending on the conditions of innerNode and parents condition.

// before
true ? `${'test' || ''}`.trim() : undefined;
// after
true ? (('test' || '')).trim() : undefined;

In my opinion, it seems better to use parentheses when both the logic of applying parentheses to innerNode and the logic of examining the state of the parent are satisfied. so i revert getWrappingFixer and make new getWrappingCode function

I think we're just getting started, so i appreciate your feedback.

@JoshuaKGoldberg JoshuaKGoldberg added the awaiting response Issues waiting for a reply from the OP or another party label Apr 23, 2024
@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Apr 28, 2024
export function getWrappingFixer(
params: WrappingFixerParams,
): TSESLint.ReportFixFunction {
export function getWrappingCode(params: WrappingFixerParams): string {
Copy link
Member

Choose a reason for hiding this comment

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

Sorry for taking a bit to get back to you - took me a while to figure out what I meant 😆 . Basically, getWrappingFixer() allows you to transform a node whose parent did not change.

// input
<stuff before> nodeToBeModified <stuff after>
// output
<stuff before> modifiedNode <stuff after>

But, now that getWrappingCode has been extracted, it can be used instead like

// input
<stuff before> nodeToBeModified <stuff after>
// getWrappingCode output
modifiedNode
// used in fixer as
<DIFFERENT stuff before> modifiedNode <DIFFERENT stuff after>

In fact, that's the whole reason that you've extracted it in this PR.

// input
condition ? `${'left' || 'right'}`.trim() : 'lol';
// getWrappingCodeOutput
('left' || 'right')
// used in fixer as 
condition ? ('left' || 'right').trim() : 'lol';

So, getWrappingCode needs to deduce whether to wrap the expression in parentheses based on the NEW parent, not the existing one.

Used within the getWrappingFixer those were the one and the same, so the line if (isWeakPrecedenceParent(node)) was justified, but now that getWrappingCode may be used indepdently, we'd need something like if (isWeakPrecedenceNewParent(node, newParent)), in order for the function to give expected results in general.

I'd have to have a longer think to come up with test cases where this discrepancy would manifest, but hopefully this gets you started? If it's still unclear let me know and I can think up some test cases. The bottom line is that we need to be aware of whether parens are needed for the new parent, not the existing parent, in order for getWrappingCode to be safe for general use outside of getWrappingFixer.

@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label May 8, 2024
@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label May 12, 2024
@kirkwaiblinger
Copy link
Member

kirkwaiblinger commented Jun 3, 2024

FYI @developer-bandi - This will have a semantic merge conflict with #8821, which has now been merged. When this is ready to go, you'll want to duplicate the fix to the no-unnecessary-template-expression copy of the rule

Also, I'm sorry for taking so long to do another review pass. I'm setting an intention for myself to get another pass in this week. Please bug me if I don't get to it within that time!

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.

I spent a long time playing around with this on my machine.... this is a difficult area of the code. Requesting a few changes for clarity, but I think the essential logic is in place! 🙂

Also, would like to tag in another member of @typescript-eslint/triage-team to help make sure we're on the right track.

const parent = node.parent!;
function isWeakPrecedenceParent(
node: TSESTree.Node,
parent = node.parent,
Copy link
Member

Choose a reason for hiding this comment

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

Let's keep the parent deduced by the node... so,

function isWeakPrecedenceParent(node: TSESTree.Node): boolean {
  const parent = node.parent;
  if (!parent) {
    return false;
  }

@@ -75,6 +75,23 @@ export function getWrappingFixer(
};
}

export function getWrappingCode(params: {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think of reframing this function to look something like this?

/**
 * Useful jsdoc here
 */
export function getMovedNodeCode(params: {
  sourceCode: Readonly<TSESLint.SourceCode>;
  nodeToMove: TSESTree.Node;
  destinationNode: TSESTree.Node;
}): string {
  const { sourceCode, nodeToMove: existingNode, destinationNode } = params;
  const code = sourceCode.getText(existingNode);
  if (isStrongPrecedenceNode(existingNode)) {
    // Moved node never needs parens
    return code;
  }

  if (!isWeakPrecedenceParent(destinationNode)) {
    // Destination would never needs parens, regardless what node moves there
    return code;
  }
  
  // Parens may be necessary
  return `(${code})`;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks much more intuitive and good. We will reflect it appropriately

@kirkwaiblinger kirkwaiblinger requested a review from a team June 21, 2024 05:06
@kirkwaiblinger kirkwaiblinger added the awaiting response Issues waiting for a reply from the OP or another party label Jun 21, 2024
Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

I agree with @kirkwaiblinger 🙂

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.

teamwork

]),
];
fix(fixer): TSESLint.RuleFix | null {
const wrappingCode = getMovedNodeCode({
Copy link
Member

Choose a reason for hiding this comment

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

(nit) the name wrappingCode is slightly stale with the rename, but I don't have many good ideas for a better name 🙃. Any ideas? codeToReplaceTemplate? Up to you if you want to change this or leave it 🤷‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the function name is getMovedNodeCode, how about changing the variable name to movedNodeCode? If you don't think it's appropriate, it might be a good idea to keep the current name.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense to me 👍

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.

Ah - reminder to duplicate these changes to the no-unnecessary-template-expressions copy of the rule. Otherwise I think I'm happy!

@kirkwaiblinger kirkwaiblinger changed the title fix(eslint-plugin): [no-useless-template-literals] add parentheses consider precedence fix(eslint-plugin): [no-unnecessary-template-expression] add parentheses consider precedence Jul 14, 2024
@kirkwaiblinger kirkwaiblinger changed the title fix(eslint-plugin): [no-unnecessary-template-expression] add parentheses consider precedence fix(eslint-plugin): [no-unnecessary-template-expression] add missing parentheses in autofix Jul 14, 2024
@JoshuaKGoldberg
Copy link
Member

👋 @developer-bandi just checking in, is this something you still 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 Aug 12, 2024
@developer-bandi
Copy link
Contributor Author

@JoshuaKGoldberg I haven't been able to proceed with the work recently due to lack of time. �I plan to start working on it this weekend.

@github-actions github-actions bot removed the awaiting response Issues waiting for a reply from the OP or another party label Aug 18, 2024
@developer-bandi
Copy link
Contributor Author

copy work to [no-unnecessary-template-expression] and delete no-useless-template-literals to resolve conflict

@JoshuaKGoldberg JoshuaKGoldberg removed the stale PRs or Issues that are at risk of being or have been closed due to inactivity for a prolonged period label Aug 19, 2024
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.

ship it

@kirkwaiblinger kirkwaiblinger added the 1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge label Aug 19, 2024
return code;
}

if (!isWeakPrecedenceParent(destinationNode)) {
Copy link
Member

Choose a reason for hiding this comment

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

[Praise] Great reuse of the existing functions!

Copy link
Member

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

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

Nice!!

@JoshuaKGoldberg JoshuaKGoldberg merged commit 90655d1 into typescript-eslint:main Aug 19, 2024
59 of 61 checks passed
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 27, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
1 approval >=1 team member has approved this PR; we're now leaving it open for more reviews before we merge bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: [no-unnecessary-template-expression] Auto fix will change meaning of code
5 participants