Skip to content

add refactoring: string concatenation to template literals #30565

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

Merged
merged 46 commits into from
Jan 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2db0745
add skeleton
bigaru Dec 5, 2018
7620615
add test cases
bigaru Dec 5, 2018
2bb2a82
add test cases
bigaru Dec 5, 2018
6952b1f
add visibility tests
bigaru Dec 5, 2018
03f0f88
add diagnostic messages
bigaru Dec 5, 2018
b84f95d
add working conversion to template literal
bigaru Dec 6, 2018
fc13b2b
add test cases
bigaru Dec 6, 2018
3d2b552
complete toTemplate
bigaru Dec 6, 2018
2b29994
complete toString
bigaru Dec 6, 2018
576271e
catch empty head of template literal
bigaru Dec 6, 2018
3b28488
add toString visibility from expression and from middle part
bigaru Dec 7, 2018
76ce1c6
fix test case
bigaru Dec 7, 2018
6fe4663
combine preceding expressions to one
bigaru Dec 7, 2018
6de23d7
do not offer refactoring for tagged templates
bigaru Dec 7, 2018
882e616
optimize preceding expression
bigaru Dec 7, 2018
7d9e8f4
treat corner cases
bigaru Dec 7, 2018
74e3cd7
remove parentheses also when expression at ending
bigaru Dec 7, 2018
1594468
add possibility to invoke from parentheses
bigaru Dec 7, 2018
cba0ddc
only show toString if expression is not binary
bigaru Dec 7, 2018
6721966
extract creation of templateHead
bigaru Dec 7, 2018
08ed6cf
optimize nodesToTemplate
bigaru Dec 7, 2018
ad0614a
extract getEdits for string concatenation
bigaru Dec 7, 2018
9b9aa35
optimize getEdits string concatenation
bigaru Dec 7, 2018
2b08bd3
change from tuple to object literal
bigaru Dec 8, 2018
16109df
optimize templateLiteral check
bigaru Dec 8, 2018
3ce2168
extract getEdits for template literal
bigaru Dec 9, 2018
cf25c12
add test cases
bigaru Dec 9, 2018
806eb12
add skeleton for handling octal escape
bigaru Dec 9, 2018
6a1df73
complete handling for octal escape
bigaru Dec 9, 2018
d2ab0bd
support single quotes when decoding raw string
bigaru Mar 22, 2019
1bcd8da
clean test cases
bigaru Mar 24, 2019
29fc8c3
support case when variable is re-assigned
bigaru Mar 24, 2019
2a15acb
refactor creation of template expression
bigaru Mar 24, 2019
935cf04
optimize and add more tests for parenthesized case
bigaru Mar 27, 2019
8ef6990
catch case when there is only single expr and optimize arrayToTree
bigaru Mar 27, 2019
834c5df
add support for hex and unicode escapes
bigaru Mar 27, 2019
9fa112e
test also the output from visibility tests
bigaru Mar 27, 2019
17f3861
copy comments from template literal to string
bigaru Mar 27, 2019
dd89a49
copy comments from string to template literal
bigaru Mar 28, 2019
4b95c1f
optimize treeToArray
bigaru Mar 29, 2019
04f96db
use stringLiteral property text instead of decodeRawString
bigaru Jun 30, 2019
3e0d34c
remove explicit escaping for placeholder opening
bigaru Jun 30, 2019
35a3a5f
Merge remote-tracking branch 'origin' into convert-to-template
DanielRosenwasser Dec 21, 2019
88795e2
Fixed lints.
DanielRosenwasser Dec 21, 2019
ad0f006
Remove refactoring from template expression to string concatenation.
DanielRosenwasser Jan 3, 2020
ff4fa1f
Cleaned up refactoring names, descriptions.
DanielRosenwasser Jan 3, 2020
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
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5341,6 +5341,10 @@
"category": "Message",
"code": 95095
},
"Convert to template string": {
"category": "Message",
"code": 95096
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
186 changes: 186 additions & 0 deletions src/services/refactors/convertStringOrTemplateLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/* @internal */
namespace ts.refactor.convertStringOrTemplateLiteral {
const refactorName = "Convert to template string";
const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string);

registerRefactor(refactorName, { getEditsForAction, getAvailableActions });

function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
const { file, startPosition } = context;
const node = getNodeOrParentOfParentheses(file, startPosition);
const maybeBinary = getParentBinaryExpression(node);
const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] };

if ((isBinaryExpression(maybeBinary) || isStringLiteral(maybeBinary)) && isStringConcatenationValid(maybeBinary)) {
refactorInfo.actions.push({ name: refactorName, description: refactorDescription });
return [refactorInfo];
}
return emptyArray;
}

function getNodeOrParentOfParentheses(file: SourceFile, startPosition: number) {
const node = getTokenAtPosition(file, startPosition);
const nestedBinary = getParentBinaryExpression(node);
const isNonStringBinary = !isStringConcatenationValid(nestedBinary);

if (
isNonStringBinary &&
isParenthesizedExpression(nestedBinary.parent) &&
isBinaryExpression(nestedBinary.parent.parent)
) {
return nestedBinary.parent.parent;
}
return node;
}

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

switch (actionName) {
case refactorDescription:
return { edits: getEditsForToTemplateLiteral(context, node) };
default:
return Debug.fail("invalid action");
}
}

function getEditsForToTemplateLiteral(context: RefactorContext, node: Node) {
const maybeBinary = getParentBinaryExpression(node);
const file = context.file;

const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file);
const trailingCommentRanges = getTrailingCommentRanges(file.text, maybeBinary.end);

if (trailingCommentRanges) {
const lastComment = trailingCommentRanges[trailingCommentRanges.length - 1];
const trailingRange = { pos: trailingCommentRanges[0].pos, end: lastComment.end };

// since suppressTrailingTrivia(maybeBinary) does not work, the trailing comment is removed manually
// otherwise it would have the trailing comment twice
return textChanges.ChangeTracker.with(context, t => {
t.deleteRange(file, trailingRange);
t.replaceNode(file, maybeBinary, templateLiteral);
});
}
else {
return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral));
}
}

function isNotEqualsOperator(node: BinaryExpression) {
return node.operatorToken.kind !== SyntaxKind.EqualsToken;
}

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

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

function treeToArray(current: Node): { nodes: Expression[], operators: Token<BinaryOperator>[], containsString: boolean, areOperatorsValid: boolean} {
if (isBinaryExpression(current)) {
const { nodes, operators, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(current.left);

if (!leftHasString && !isStringLiteral(current.right)) {
return { nodes: [current], operators: [], containsString: false, areOperatorsValid: true };
}

const currentOperatorValid = current.operatorToken.kind === SyntaxKind.PlusToken;
const areOperatorsValid = leftOperatorValid && currentOperatorValid;

nodes.push(current.right);
operators.push(current.operatorToken);

return { nodes, operators, containsString: true, areOperatorsValid };
}

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

// to copy comments following the operator
// "foo" + /* comment */ "bar"
const copyTrailingOperatorComments = (operators: Token<BinaryOperator>[], file: SourceFile) => (index: number, targetNode: Node) => {
if (index < operators.length) {
copyTrailingComments(operators[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
}
};

// to copy comments following the string
// "foo" /* comment */ + "bar" /* comment */ + "bar2"
const copyCommentFromMultiNode = (nodes: readonly Expression[], file: SourceFile, copyOperatorComments: (index: number, targetNode: Node) => void) =>
(indexes: number[], targetNode: Node) => {
while (indexes.length > 0) {
const index = indexes.shift()!;
copyTrailingComments(nodes[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
copyOperatorComments(index, targetNode);
}
};

function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] {
let text = "";
const indexes = [];

while (index < nodes.length && isStringLiteral(nodes[index])) {
const stringNode = nodes[index] as StringLiteral;
text = text + stringNode.text;
indexes.push(index);
index++;
}

text = escapeString(text);
return [index, text, 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);

if (begin === nodes.length) {
const noSubstitutionTemplateLiteral = createNoSubstitutionTemplateLiteral(headText);
copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral);
return noSubstitutionTemplateLiteral;
}

const templateSpans: TemplateSpan[] = [];
const templateHead = createTemplateHead(headText);
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);
i = newIndex - 1;

const templatePart = i === nodes.length - 1 ? createTemplateTail(subsequentText) : createTemplateMiddle(subsequentText);
copyCommentFromStringLiterals(stringIndexes, templatePart);
templateSpans.push(createTemplateSpan(currentNode, templatePart));
}

return createTemplateExpression(templateHead, templateSpans);
}

// to copy comments following the opening & closing parentheses
// "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar"
function copyCommentsWhenParenthesized(node: ParenthesizedExpression) {
const file = node.getSourceFile();
copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false);
}

function getExpressionFromParenthesesOrExpression(node: Expression) {
if (isParenthesizedExpression(node)) {
copyCommentsWhenParenthesized(node);
node = node.expression;
}
return node;
}
}
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"refactors/moveToNewFile.ts",
"refactors/addOrRemoveBracesToArrowFunction.ts",
"refactors/convertParamsToDestructuredObject.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 to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
`console.log(\`foobar is \${32} 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 back`tick"

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
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 to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
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*/f/*y*/oobar is " + (42 + 6)

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

//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is" /* C2 */ + 42 /* C3 */ + " and bar" /* C4 */ + " is" /* C5 */ + 52/* C6 */

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42 /* C3 */} and bar is\${ /* C4 */ /* C5 */52 /* C6 */}`",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = /* C0 */ /*x*/"/*y*/foo" + /* C1 */ " is" + /* C2 */ 42 + /* C3 */ " and bar" + /* C4 */ " is" + /* C5 */ 52/* C6 */

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42 /* C3 */} and bar is\${ /* C4 */ /* C5 */52 /* C6 */}`",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is"/* C2 */ /* C3 */

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
"const foo = /* C0 */ `foo is` /* C1 */ /* C2 */ /* C3 */",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = /*x*/"/*y*/foobar is" + ( /* C1 */ 42 ) /* C2 */ + /* C3 */ " years old"

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

//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is" /* C2 */ + 42/* C3 */

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42 /* C3 */}`",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path='fourslash.ts' />

//// const foo = /* C0 */ /*x*/"/*y*/foo" /* C1 */ + " is" /* C2 */ + 42 + " years old"/* C3 */

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
"const foo = /* C0 */ `foo is\${ /* C1 */ /* C2 */42} years old` /* C3 */",
});
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 + " years" + " old" + " and " + 6 + " cars" + " are" + " missing"

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
`const foo = \`foobar is \${42} years old and \${6} cars are missing\``,
});
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*/n/*y*/ame + " is " + age + " years old"

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
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 = "/*x*/f/*y*/oobar is " + age + " years old"

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

//// const foo = "/*x*/f/*y*/oobar " + "rocks" + " fantastically"

goTo.select("x", "y");
edit.applyRefactor({
refactorName: "Convert to template string",
actionName: "Convert to template string",
actionDescription: ts.Diagnostics.Convert_to_template_string.message,
newContent:
"const foo = `foobar rocks fantastically`",
});
Loading