diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts index fe648e3bd873..dc15a9bbcd06 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts @@ -5,6 +5,7 @@ import * as ts from 'typescript'; import { createRule, getConstrainedTypeAtLocation, + getMovedNodeCode, getParserServices, isTypeFlagSet, isUndefinedIdentifier, @@ -106,21 +107,14 @@ export default createRule<[], MessageId>({ context.report({ node: node.expressions[0], messageId: 'noUnnecessaryTemplateExpression', - fix(fixer): TSESLint.RuleFix[] { - const [prevQuasi, nextQuasi] = node.quasis; - - // Remove the quasis and backticks. - return [ - fixer.removeRange([ - prevQuasi.range[1] - 3, - node.expressions[0].range[0], - ]), - - fixer.removeRange([ - node.expressions[0].range[1], - nextQuasi.range[0] + 2, - ]), - ]; + fix(fixer): TSESLint.RuleFix | null { + const wrappingCode = getMovedNodeCode({ + sourceCode: context.sourceCode, + nodeToMove: node.expressions[0], + destinationNode: node, + }); + + return fixer.replaceText(node, wrappingCode); }, }); diff --git a/packages/eslint-plugin/src/util/getWrappingFixer.ts b/packages/eslint-plugin/src/util/getWrappingFixer.ts index e6e71168fc41..cb45f1cdfc3d 100644 --- a/packages/eslint-plugin/src/util/getWrappingFixer.ts +++ b/packages/eslint-plugin/src/util/getWrappingFixer.ts @@ -74,6 +74,33 @@ export function getWrappingFixer( return fixer.replaceText(node, code); }; } +/** + * If the node to be moved and the destination node require parentheses, include parentheses in the node to be moved. + * @param sourceCode Source code of current file + * @param nodeToMove Nodes that need to be moved + * @param destinationNode Final destination node with nodeToMove + * @returns If parentheses are required, code for the nodeToMove node is returned with parentheses at both ends of the code. + */ +export function getMovedNodeCode(params: { + sourceCode: Readonly; + 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})`; +} /** * Check if a node will always have the same precedence if it's parent changes. @@ -98,8 +125,10 @@ export function isStrongPrecedenceNode(innerNode: TSESTree.Node): boolean { * Check if a node's parent could have different precedence if the node changes. */ function isWeakPrecedenceParent(node: TSESTree.Node): boolean { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const parent = node.parent!; + const parent = node.parent; + if (!parent) { + return false; + } if ( parent.type === AST_NODE_TYPES.UpdateExpression || diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts index 1dd79982f50f..3902b404d9ec 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts @@ -1127,5 +1127,14 @@ declare const nested: string, interpolation: string; }, ], }, + { + code: "true ? `${'test' || ''}`.trim() : undefined;", + output: "true ? ('test' || '').trim() : undefined;", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, ], });