Fix template string refactoring and nodeFactory bug

Instead of letting `createTemplate*` generate a broken raw string from
the cooked one, grab the source code for it.

Also, add a missing bit to `\`-quote `$`s.  As the comment in the code
says, it could just `\`-quote `${` since other `$`s are valid, but I
think that it's less confusing to always quote $s (but the change is in
the comment if minimalism is preferred).

Also, a small-but-confusing bug in `getCookedText()`.

Many tests for all of this.

Fixes #40625
This commit is contained in:
Eli Barzilay 2021-05-28 00:44:17 -04:00
parent 52ca2dd9cf
commit 7e8bba6908
3 changed files with 134 additions and 14 deletions

View File

@ -6148,7 +6148,7 @@ namespace ts {
}
let token = rawTextScanner.scan();
if (token === SyntaxKind.CloseBracketToken) {
if (token === SyntaxKind.CloseBraceToken) {
token = rawTextScanner.reScanTemplateToken(/*isTaggedTemplate*/ false);
}

View File

@ -147,61 +147,85 @@ namespace ts.refactor.convertStringOrTemplateLiteral {
}
};
function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] {
function escapeStringForTemplate(s: string) {
// Escaping for $s in strings that are to be used in template strings
// Naive implementation: replace \x by itself and otherwise $ by \$.
// But to complicate it a bit, this should work for raw strings too.
// And another bit: escape the $ in the replacement for JS's .replace().
return s.replace(/\\.|\$/g, m => m === "$" ? "\\\$" : m);
// Finally, a less-backslash-happy version can work too, doing only ${ instead of all $s:
// s.replace(/\\.|\${/g, m => m === "${" ? "\\\${" : m);
// but `\$${foo}` is likely more clear than the more-confusing-but-still-working `$${foo}`.
}
function getRawTextOfTemplate(node: TemplateHead | TemplateMiddle | TemplateTail) {
// in these cases the right side is ${
const rightShaving = isTemplateHead(node) || isTemplateMiddle(node) ? -2 : -1;
return getTextOfNode(node).slice(1, rightShaving);
}
function concatConsecutiveString(index: number, nodes: readonly Expression[]): [nextIndex: number, text: string, rawText: string, usedIndexes: number[]] {
const indexes = [];
let text = "";
let text = "", rawText = "";
while (index < nodes.length) {
const node = nodes[index];
if (isStringLiteralLike(node)) { // includes isNoSubstitutionTemplateLiteral(node)
text = text + node.text;
text += escapeStringForTemplate(node.text);
rawText += escapeStringForTemplate(getTextOfNode(node).slice(1, -1));
indexes.push(index);
index++;
}
else if (isTemplateExpression(node)) {
text = text + node.head.text;
text += node.head.text;
rawText += getRawTextOfTemplate(node.head);
break;
}
else {
break;
}
}
return [index, text, indexes];
return [index, text, rawText, indexes];
}
function nodesToTemplate({ nodes, operators }: { nodes: readonly Expression[], operators: Token<BinaryOperator>[] }, file: SourceFile) {
const copyOperatorComments = copyTrailingOperatorComments(operators, file);
const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments);
const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes);
const [begin, headText, rawHeadText, headIndexes] = concatConsecutiveString(0, nodes);
if (begin === nodes.length) {
const noSubstitutionTemplateLiteral = factory.createNoSubstitutionTemplateLiteral(headText);
const noSubstitutionTemplateLiteral = factory.createNoSubstitutionTemplateLiteral(headText, rawHeadText);
copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral);
return noSubstitutionTemplateLiteral;
}
const templateSpans: TemplateSpan[] = [];
const templateHead = factory.createTemplateHead(headText);
const templateHead = factory.createTemplateHead(headText, rawHeadText);
copyCommentFromStringLiterals(headIndexes, templateHead);
for (let i = begin; i < nodes.length; i++) {
const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]);
copyOperatorComments(i, currentNode);
const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes);
const [newIndex, subsequentText, rawSubsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes);
i = newIndex - 1;
const isLast = i === nodes.length - 1;
if (isTemplateExpression(currentNode)) {
const spans = map(currentNode.templateSpans, (span, index) => {
copyExpressionComments(span);
const nextSpan = currentNode.templateSpans[index + 1];
const text = span.literal.text + (nextSpan ? "" : subsequentText);
return factory.createTemplateSpan(span.expression, isLast ? factory.createTemplateTail(text) : factory.createTemplateMiddle(text));
const isLastSpan = index === currentNode.templateSpans.length - 1;
const text = span.literal.text + (isLastSpan ? subsequentText : "");
const rawText = getRawTextOfTemplate(span.literal) + (isLastSpan ? rawSubsequentText : "");
return factory.createTemplateSpan(span.expression, isLast
? factory.createTemplateTail(text, rawText)
: factory.createTemplateMiddle(text, rawText));
});
templateSpans.push(...spans);
}
else {
const templatePart = isLast ? factory.createTemplateTail(subsequentText) : factory.createTemplateMiddle(subsequentText);
const templatePart = isLast
? factory.createTemplateTail(subsequentText, rawSubsequentText)
: factory.createTemplateMiddle(subsequentText, rawSubsequentText);
copyCommentFromStringLiterals(stringIndexes, templatePart);
templateSpans.push(factory.createTemplateSpan(currentNode, templatePart));
}

View File

@ -0,0 +1,96 @@
/// <reference path='fourslash.ts' />
// @Filename: /a.ts
////let s = /*a1*/"\0\b\f\t\r\n" + text + "\n"/*a2*/;
goTo.select("a1", "a2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent: 'let s = `\\0\\b\\f\\t\\r\\n${text}\\n`;'
});
// @Filename: /b.ts
////let s = /*b1*/'"' + text + "'"/*b2*/;
goTo.select("b1", "b2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `"${text}'`;
newContent: 'let s = `"${text}\'`;'
});
// @Filename: /c.ts
////let s = /*c1*/'$' + text + "\\"/*c2*/;
goTo.select("c1", "c2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `\$${text}\\`;
newContent: 'let s = `\\$${text}\\\\`;'
});
// @Filename: /d.ts
////let s = /*d1*/`$` + text + `\\`/*d2*/;
goTo.select("d1", "d2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `\$${text}\\`;
newContent: 'let s = `\\$${text}\\\\`;'
});
// @Filename: /e.ts
////let s = /*e1*/'${' + text + "}"/*e2*/;
goTo.select("e1", "e2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `\${${text}}`;
newContent: 'let s = `\\${${text}}`;'
});
// @Filename: /f.ts
////let s = /*f1*/`\${` + text + `}`/*f2*/;
goTo.select("f1", "f2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `\${${text}}`;
newContent: 'let s = `\\${${text}}`;'
});
// @Filename: /g.ts
////let s = /*g1*/'\\$' + text + "\\"/*g2*/;
goTo.select("g1", "g2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `\\\$${text}\\`;
newContent: 'let s = `\\\\\\$${text}\\\\`;'
});
// @Filename: /h.ts
////let s = /*h1*/"\u0041\u0061" + text + "\0\u0000"/*h2*/;
goTo.select("h1", "h2");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
// newContent is: let s = `\u0041\u0061${text}\0\u0000`;
newContent: 'let s = `\\u0041\\u0061${text}\\0\\u0000`;'
});