Skip to content

WIP: add refactoring: string concatenation to template literals #28923

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

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6cc8029
add skeleton
bigaru Dec 5, 2018
50e130a
add test cases
bigaru Dec 5, 2018
9492623
add test cases
bigaru Dec 5, 2018
1046eb6
add visibility tests
bigaru Dec 5, 2018
8510e25
add diagnostic messages
bigaru Dec 5, 2018
f15fcb7
add working conversion to template literal
bigaru Dec 6, 2018
3b99801
add test cases
bigaru Dec 6, 2018
671eb54
complete toTemplate
bigaru Dec 6, 2018
f1d28c9
complete toString
bigaru Dec 6, 2018
9bce643
catch empty head of template literal
bigaru Dec 6, 2018
97cee68
add toString visibility from expression and from middle part
bigaru Dec 7, 2018
5e6e1fb
fix test case
bigaru Dec 7, 2018
91cfca3
combine preceding expressions to one
bigaru Dec 7, 2018
ec69194
do not offer refactoring for tagged templates
bigaru Dec 7, 2018
4d522c3
optimize preceding expression
bigaru Dec 7, 2018
7441c35
treat corner cases
bigaru Dec 7, 2018
7d8fccd
remove parentheses also when expression at ending
bigaru Dec 7, 2018
e567e53
add possibility to invoke from parentheses
bigaru Dec 7, 2018
e0fdf74
only show toString if expression is not binary
bigaru Dec 7, 2018
bea12a1
extract creation of templateHead
bigaru Dec 7, 2018
2a38eef
optimize nodesToTemplate
bigaru Dec 7, 2018
59008eb
extract getEdits for string concatenation
bigaru Dec 7, 2018
997f3e3
optimize getEdits string concatenation
bigaru Dec 7, 2018
81804c2
change from tuple to object literal
bigaru Dec 8, 2018
3f5761f
optimize templateLiteral check
bigaru Dec 8, 2018
8c9903a
extract getEdits for template literal
bigaru Dec 9, 2018
dbd31cc
add test cases
bigaru Dec 9, 2018
85dbb27
add skeleton for handling octal escape
bigaru Dec 9, 2018
2d7a48f
complete handling for octal escape
bigaru Dec 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4799,5 +4799,17 @@
"Add names to all parameters without names": {
"category": "Message",
"code": 95073
},
"Convert string concatenation or template literal": {
"category": "Message",
"code": 95074
},
"Convert to template literal": {
"category": "Message",
"code": 95075
},
"Convert to string concatenation": {
"category": "Message",
"code": 95076
}
}
201 changes: 201 additions & 0 deletions src/services/refactors/convertStringOrTemplateLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/* @internal */
namespace ts.refactor.convertStringOrTemplateLiteral {
const refactorName = "Convert string concatenation or template literal";
const toTemplateLiteralActionName = "Convert to template literal";
const toStringConcatenationActionName = "Convert to string concatenation";

const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_string_concatenation_or_template_literal);
const toTemplateLiteralDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_literal);
const toStringConcatenationDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_string_concatenation);

registerRefactor(refactorName, { getEditsForAction, getAvailableActions });

function getAvailableActions(context: RefactorContext): ReadonlyArray<ApplicableRefactorInfo> {
const { file, startPosition } = context;
const node = getNodeOrParentOfParentheses(file, startPosition);
const maybeBinary = getParentBinaryExpression(node);
const actions: RefactorActionInfo[] = [];

if ((isBinaryExpression(maybeBinary) || isStringLiteral(maybeBinary)) && isStringConcatenationValid(maybeBinary)) {
actions.push({ name: toTemplateLiteralActionName, description: toTemplateLiteralDescription });
}

const templateLiteral = findAncestor(node, n => isTemplateLiteral(n));

if (templateLiteral && !isTaggedTemplateExpression(templateLiteral.parent)) {
actions.push({ name: toStringConcatenationActionName, description: toStringConcatenationDescription });
}

return [{ name: refactorName, description: refactorDescription, actions }];
}

function getNodeOrParentOfParentheses(file: SourceFile, startPosition: number) {
const node = getTokenAtPosition(file, startPosition);
if (isParenthesizedExpression(node.parent) && isBinaryExpression(node.parent.parent)) return node.parent.parent;
return node;
}

function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
const { file, startPosition } = context;
const node = getNodeOrParentOfParentheses(file, startPosition);

switch (actionName) {
case toTemplateLiteralActionName:
return { edits: getEditsForToTemplateLiteral(context, node) };

case toStringConcatenationActionName:
return { edits: getEditsForToStringConcatenation(context, node) };

default:
return Debug.fail("invalid action");
}
}

function getEditsForToTemplateLiteral(context: RefactorContext, node: Node) {
const maybeBinary = getParentBinaryExpression(node);
const arrayOfNodes = transformTreeToArray(maybeBinary);
const templateLiteral = nodesToTemplate(arrayOfNodes);
return textChanges.ChangeTracker.with(context, t => t.replaceNode(context.file, maybeBinary, templateLiteral));
}

function getEditsForToStringConcatenation(context: RefactorContext, node: Node) {
const templateLiteral = findAncestor(node, n => isTemplateLiteral(n))! as TemplateLiteral;

if (isTemplateExpression(templateLiteral)) {
const { head, templateSpans } = templateLiteral;
const arrayOfNodes = templateSpans.map(templateSpanToExpressions)
.reduce((accumulator, nextArray) => accumulator.concat(nextArray));

if (head.text.length !== 0) arrayOfNodes.unshift(createStringLiteral(head.text));

const binaryExpression = arrayToTree(arrayOfNodes);
return textChanges.ChangeTracker.with(context, t => t.replaceNode(context.file, templateLiteral, binaryExpression));
}
else {
const stringLiteral = createStringLiteral(templateLiteral.text);
return textChanges.ChangeTracker.with(context, t => t.replaceNode(context.file, node, stringLiteral));
}
}

function templateSpanToExpressions(templateSpan: TemplateSpan): Expression[] {
const { expression, literal } = templateSpan;
const text = literal.text;
return text.length === 0 ? [expression] : [expression, createStringLiteral(text)];
}

function getParentBinaryExpression(expr: Node) {
while (isBinaryExpression(expr.parent)) {
expr = expr.parent;
}
return expr;
}

function arrayToTree(nodes: ReadonlyArray<Expression>, accumulator?: BinaryExpression): BinaryExpression {
if (nodes.length === 0) return accumulator!;

if (!accumulator) {
const left = nodes[0];
const right = nodes[1];

const binary = createBinary(left, SyntaxKind.PlusToken, right);
return arrayToTree(nodes.slice(2), binary);
}

const right = nodes[0];
const binary = createBinary(accumulator, SyntaxKind.PlusToken, right);
return arrayToTree(nodes.slice(1), binary);
}

function isStringConcatenationValid(node: Node): boolean {
const { containsString, areOperatorsValid } = treeToArray(node);
return containsString && areOperatorsValid;
}

function transformTreeToArray(node: Node): ReadonlyArray<Expression> {
return treeToArray(node).nodes;
}

function treeToArray(node: Node): { nodes: ReadonlyArray<Expression>, containsString: boolean, areOperatorsValid: boolean} {
if (isBinaryExpression(node)) {
const { nodes: leftNodes, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(node.left);
const { nodes: rightNodes, containsString: rightHasString, areOperatorsValid: rightOperatorValid } = treeToArray(node.right);

if (!leftHasString && !rightHasString) {
return { nodes: [node], containsString: false, areOperatorsValid: true };
}

const nodeOperatorValid = node.operatorToken.kind === SyntaxKind.PlusToken;
const isPlus = leftOperatorValid && nodeOperatorValid && rightOperatorValid;

return { nodes: leftNodes.concat(rightNodes), containsString: true, areOperatorsValid: isPlus };
}

return { nodes: [node as Expression], containsString: isStringLiteral(node), areOperatorsValid: true };
}

function createHead(nodes: ReadonlyArray<Expression>): [number, TemplateHead] {
let begin = 0;
let text = "";

while (begin < nodes.length && isStringLiteral(nodes[begin])) {
const next = nodes[begin] as StringLiteral;
text = text + decodeRawString(next.getText());
Copy link
Contributor

Choose a reason for hiding this comment

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

node.getText() is the correct solution to get the raw string content. To avoid unnecessary work, consider passing the SourceFile as argument to it.

begin++;
}

text = escapeText(text);
return [begin, createTemplateHead(text)];
}

function nodesToTemplate(nodes: ReadonlyArray<Expression>) {
const templateSpans: TemplateSpan[] = [];
const [begin, head] = createHead(nodes);

if (begin === nodes.length) {
return createNoSubstitutionTemplateLiteral(head.text);
}

for (let i = begin; i < nodes.length; i++) {
let current = nodes[i];
let text = "";

while (i + 1 < nodes.length && isStringLiteral(nodes[i + 1])) {
const next = nodes[i + 1] as StringLiteral;
text = text + decodeRawString(next.getText());
i++;
}

text = escapeText(text);
const templatePart = i === nodes.length - 1 ? createTemplateTail(text) : createTemplateMiddle(text);

if (isParenthesizedExpression(current)) current = current.expression;
templateSpans.push(createTemplateSpan(current, templatePart));
}

return createTemplateExpression(head, templateSpans);
}

const hexToUnicode = (_match: string, grp: string) => String.fromCharCode(parseInt(grp, 16));
const octalToUnicode = (_match: string, grp: string) => String.fromCharCode(parseInt(grp, 8));

function decodeRawString(content: string) {
const outerQuotes = /"((.|\s)*)"/;
const unicodeEscape = /\\u([\d\w]+)/gi;
const unicodeEscapeWithBraces = /\\u\{([\d\w]+\})/gi;
const hexEscape = /\\x([\d\w]+)/gi;
const octalEscape = /\\([0-7]+)/g;

return content.replace(outerQuotes, (_match, grp) => grp)
.replace(unicodeEscape, hexToUnicode)
.replace(unicodeEscapeWithBraces, hexToUnicode)
.replace(hexEscape, hexToUnicode)
.replace(octalEscape, octalToUnicode);

}

function escapeText(content: string) {
return content.replace("`", "\`") // back-tick
.replace("\${", `$\\{`); // placeholder alike beginning
}

}
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"refactors/generateGetAccessorAndSetAccessor.ts",
"refactors/moveToNewFile.ts",
"refactors/addOrRemoveBracesToArrowFunction.ts",
"refactors/convertStringOrTemplateLiteral.ts",
"services.ts",
"breakpoints.ts",
"transform.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// console.log(`/*x*/f/*y*/oobar is ${ 32 } years old`)

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
`console.log("foobar is " + 32 + " years old")`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/// <reference path='fourslash.ts' />

//// const age = 22
//// const name = "Eddy"
//// const /*z*/f/*y*/oo = /*x*/`/*w*/M/*v*/r/*u*/ /*t*/$/*s*/{ /*r*/n/*q*/ame } is ${ /*p*/a/*o*/ge + 34 } years old`

goTo.select("z", "y");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("x", "w");
verify.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("v", "u");
verify.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("t", "s");
verify.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("r", "q");
verify.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("p", "o");
verify.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />

//// function tag(literals: TemplateStringsArray, ...placeholders: string[]) { return "tagged" }
//// const alpha = tag/*z*/`/*y*/foobar`
//// const beta = tag/*x*/`/*w*/foobar ${/*v*/4/*u*/2}`

goTo.select("z", "y");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("x", "w");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");

goTo.select("v", "u");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to string concatenation");
verify.not.refactorAvailable("Convert string concatenation or template literal", "Convert to template literal");
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = `/*x*/w/*y*/ith back\`tick`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
"const foo = \"with back`tick\"",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = `/*x*/f/*y*/oobar is ${ 42 + 6 } years old`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
`const foo = "foobar is " + (42 + 6) + " years old"`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = `/*x*/w/*y*/ith \${dollar}`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
"const foo = \"with \${dollar}\"",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = `/*x*/f/*y*/oobar is ${ 42 }${ 6 } years old`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
`const foo = "foobar is " + 42 + 6 + " years old"`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />

//// const age = 22
//// const name = "Eddy"
//// const foo = `/*x*/$/*y*/{ name } is ${ age } years old`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
`const age = 22
const name = "Eddy"
const foo = name + " is " + age + " years old"`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

//// const age = 42
//// const foo = `foobar is a ${ age < 18 ? 'child' : /*x*/`/*y*/grown-up ${ age > 40 ? 'who needs probaply assistance' : ''}` }`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
`const age = 42
const foo = \`foobar is a \${ age < 18 ? 'child' : "grown-up " + (age > 40 ? 'who needs probaply assistance' : '') }\``,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />

//// const age = 42
//// const foo = `foobar is a ${ `/*x*/3/*y*/4` }`

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert string concatenation or template literal",
actionName: "Convert to string concatenation",
actionDescription: "Convert to string concatenation",
newContent:
`const age = 42
const foo = \`foobar is a \${ "34" }\``,
});
Loading