Skip to content

Commit 98aaeb7

Browse files
authored
fix(40610): handle template string concatenation (#40653)
1 parent 45b698b commit 98aaeb7

11 files changed

+186
-16
lines changed

src/services/refactors/convertStringOrTemplateLiteral.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
7878
case SyntaxKind.PropertyAccessExpression:
7979
case SyntaxKind.ElementAccessExpression:
8080
return false;
81+
case SyntaxKind.TemplateExpression:
8182
case SyntaxKind.BinaryExpression:
8283
return !(isBinaryExpression(n.parent) && isNotEqualsOperator(n.parent));
8384
default:
@@ -97,7 +98,7 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
9798
if (isBinaryExpression(current)) {
9899
const { nodes, operators, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(current.left);
99100

100-
if (!leftHasString && !isStringLiteral(current.right)) {
101+
if (!leftHasString && !isStringLiteral(current.right) && !isTemplateExpression(current.right)) {
101102
return { nodes: [current], operators: [], containsString: false, areOperatorsValid: true };
102103
}
103104

@@ -133,20 +134,27 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
133134
};
134135

135136
function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] {
136-
let text = "";
137137
const indexes = [];
138-
139-
while (index < nodes.length && isStringLiteral(nodes[index])) {
140-
const stringNode = nodes[index] as StringLiteral;
141-
text = text + stringNode.text;
142-
indexes.push(index);
143-
index++;
138+
let text = "";
139+
while (index < nodes.length) {
140+
const node = nodes[index];
141+
if (isStringLiteralLike(node)) {
142+
text = text + node.text;
143+
indexes.push(index);
144+
index++;
145+
}
146+
else if (isTemplateExpression(node)) {
147+
text = text + node.head.text;
148+
break;
149+
}
150+
else {
151+
break;
152+
}
144153
}
145-
146154
return [index, text, indexes];
147155
}
148156

149-
function nodesToTemplate({nodes, operators}: {nodes: readonly Expression[], operators: Token<BinaryOperator>[]}, file: SourceFile) {
157+
function nodesToTemplate({ nodes, operators }: { nodes: readonly Expression[], operators: Token<BinaryOperator>[] }, file: SourceFile) {
150158
const copyOperatorComments = copyTrailingOperatorComments(operators, file);
151159
const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments);
152160
const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes);
@@ -167,26 +175,38 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
167175

168176
const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes);
169177
i = newIndex - 1;
170-
171-
const templatePart = i === nodes.length - 1 ? factory.createTemplateTail(subsequentText) : factory.createTemplateMiddle(subsequentText);
172-
copyCommentFromStringLiterals(stringIndexes, templatePart);
173-
templateSpans.push(factory.createTemplateSpan(currentNode, templatePart));
178+
const isLast = i === nodes.length - 1;
179+
180+
if (isTemplateExpression(currentNode)) {
181+
const spans = map(currentNode.templateSpans, (span, index) => {
182+
copyExpressionComments(span);
183+
const nextSpan = currentNode.templateSpans[index + 1];
184+
const text = span.literal.text + (nextSpan ? "" : subsequentText);
185+
return factory.createTemplateSpan(span.expression, isLast ? factory.createTemplateTail(text) : factory.createTemplateMiddle(text));
186+
});
187+
templateSpans.push(...spans);
188+
}
189+
else {
190+
const templatePart = isLast ? factory.createTemplateTail(subsequentText) : factory.createTemplateMiddle(subsequentText);
191+
copyCommentFromStringLiterals(stringIndexes, templatePart);
192+
templateSpans.push(factory.createTemplateSpan(currentNode, templatePart));
193+
}
174194
}
175195

176196
return factory.createTemplateExpression(templateHead, templateSpans);
177197
}
178198

179199
// to copy comments following the opening & closing parentheses
180200
// "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar"
181-
function copyCommentsWhenParenthesized(node: ParenthesizedExpression) {
201+
function copyExpressionComments(node: ParenthesizedExpression | TemplateSpan) {
182202
const file = node.getSourceFile();
183203
copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
184204
copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
185205
}
186206

187207
function getExpressionFromParenthesesOrExpression(node: Expression) {
188208
if (isParenthesizedExpression(node)) {
189-
copyCommentsWhenParenthesized(node);
209+
copyExpressionComments(node);
190210
node = node.expression;
191211
}
192212
return node;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = /*start*/"" + ``/*end*/;
4+
5+
goTo.select("start", "end");
6+
edit.applyRefactor({
7+
refactorName: "Convert to template string",
8+
actionName: "Convert to template string",
9+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
10+
newContent: "const foo = ``;",
11+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const a = /*x*/`${1}`/*y*/ + "a" + "b" + ` ${2}.`;
4+
5+
goTo.select("x", "y");
6+
edit.applyRefactor({
7+
refactorName: "Convert to template string",
8+
actionName: "Convert to template string",
9+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
10+
newContent: "const a = `${1}ab ${2}.`;"
11+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = 1;
4+
////const bar = /*start*/"a" + `${foo}`/*end*/;
5+
6+
goTo.select("start", "end");
7+
edit.applyRefactor({
8+
refactorName: "Convert to template string",
9+
actionName: "Convert to template string",
10+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
11+
newContent: [
12+
"const foo = 1;",
13+
"const bar = `a${foo}`;"
14+
].join("\n")
15+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = /*start*/`` + ""/*end*/;
4+
5+
goTo.select("start", "end");
6+
edit.applyRefactor({
7+
refactorName: "Convert to template string",
8+
actionName: "Convert to template string",
9+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
10+
newContent: "const foo = ``;"
11+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = 1;
4+
////const bar = /*start*/`` + "1" + `` + `${foo}`/*end*/;
5+
6+
goTo.select("start", "end");
7+
edit.applyRefactor({
8+
refactorName: "Convert to template string",
9+
actionName: "Convert to template string",
10+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
11+
newContent: [
12+
"const foo = 1;",
13+
"const bar = `1${foo}`;"
14+
].join("\n")
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = 1;
4+
////const bar = /*start*/`1` + "2" + `3` + `${foo}`/*end*/;
5+
6+
goTo.select("start", "end");
7+
edit.applyRefactor({
8+
refactorName: "Convert to template string",
9+
actionName: "Convert to template string",
10+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
11+
newContent: [
12+
"const foo = 1;",
13+
"const bar = `123${foo}`;"
14+
].join("\n")
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = 1;
4+
////const bar = /*start*/"a" + `${foo}` + "b"/*end*/;
5+
6+
goTo.select("start", "end");
7+
edit.applyRefactor({
8+
refactorName: "Convert to template string",
9+
actionName: "Convert to template string",
10+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
11+
newContent: [
12+
"const foo = 1;",
13+
"const bar = `a${foo}b`;"
14+
].join("\n")
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const foo = 1;
4+
////const bar = /*start*/"a " + `${foo}` + " b " + " c"/*end*/;
5+
6+
goTo.select("start", "end");
7+
edit.applyRefactor({
8+
refactorName: "Convert to template string",
9+
actionName: "Convert to template string",
10+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
11+
newContent: [
12+
"const foo = 1;",
13+
"const bar = `a ${foo} b c`;"
14+
].join("\n")
15+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const a = 1;
4+
////const b = 1;
5+
////const c = 1;
6+
////const d = 1;
7+
////const d = `a${/* a */ a /* a */} ${b}` + /*start*/" other "/*end*/ + c + `${/* d */ d /* d */}`;
8+
9+
goTo.select("start", "end");
10+
edit.applyRefactor({
11+
refactorName: "Convert to template string",
12+
actionName: "Convert to template string",
13+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
14+
newContent: [
15+
"const a = 1;",
16+
"const b = 1;",
17+
"const c = 1;",
18+
"const d = 1;",
19+
"const d = `a${/* a */ a /* a */} ${b} other ${c}${/* d */ d /* d */}`;"
20+
].join("\n")
21+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////const a = 1;
4+
////const b = 1;
5+
////const c = 1;
6+
////const d = 1;
7+
////const d = /*start*/"start" + ` / a start ${a} a end / b start ${b} b end / ` + c + ` / d start ${d} d end / ` + "end"/*end*/;
8+
9+
goTo.select("start", "end");
10+
edit.applyRefactor({
11+
refactorName: "Convert to template string",
12+
actionName: "Convert to template string",
13+
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
14+
newContent: [
15+
"const a = 1;",
16+
"const b = 1;",
17+
"const c = 1;",
18+
"const d = 1;",
19+
"const d = `start / a start ${a} a end / b start ${b} b end / ${c} / d start ${d} d end / end`;"
20+
].join("\n")
21+
});

0 commit comments

Comments
 (0)