-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Changes from 33 commits
2db0745
7620615
2bb2a82
6952b1f
03f0f88
b84f95d
fc13b2b
3d2b552
2b29994
576271e
3b28488
76ce1c6
6fe4663
6de23d7
882e616
7d9e8f4
74e3cd7
1594468
cba0ddc
6721966
08ed6cf
ad0614a
9b9aa35
2b08bd3
16109df
3ce2168
cf25c12
806eb12
6a1df73
d2ab0bd
1bcd8da
29fc8c3
2a15acb
935cf04
8ef6990
834c5df
9fa112e
17f3861
dd89a49
4b95c1f
04f96db
3e0d34c
35a3a5f
88795e2
ad0f006
ff4fa1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
/* @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 refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; | ||
|
||
if ((isBinaryExpression(maybeBinary) || isStringLiteral(maybeBinary)) && isStringConcatenationValid(maybeBinary)) { | ||
refactorInfo.actions.push({ name: toTemplateLiteralActionName, description: toTemplateLiteralDescription }); | ||
return [refactorInfo]; | ||
} | ||
|
||
const templateLiteral = findAncestor(node, n => isTemplateLiteral(n)); | ||
|
||
if (templateLiteral && !isTaggedTemplateExpression(templateLiteral.parent)) { | ||
refactorInfo.actions.push({ name: toStringConcatenationActionName, description: toStringConcatenationDescription }); | ||
return [refactorInfo]; | ||
} | ||
|
||
return emptyArray; | ||
} | ||
|
||
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 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 arrayToTree(nodes: ReadonlyArray<Expression>, accumulator?: BinaryExpression): BinaryExpression { | ||
bigaru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (nodes.length === 0) return accumulator!; | ||
|
||
if (!accumulator) { | ||
const left = nodes[0]; | ||
const right = nodes[1]; | ||
bigaru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 currentOperatorValid = node.operatorToken.kind === SyntaxKind.PlusToken; | ||
const areOperatorsValid = leftOperatorValid && currentOperatorValid && rightOperatorValid; | ||
|
||
return { nodes: leftNodes.concat(rightNodes), containsString: true, areOperatorsValid }; | ||
} | ||
|
||
return { nodes: [node as Expression], containsString: isStringLiteral(node), areOperatorsValid: true }; | ||
} | ||
|
||
function concatConsecutiveString(index: number, nodes: ReadonlyArray<Expression>): [number, string] { | ||
let text = ""; | ||
|
||
while (index < nodes.length && isStringLiteral(nodes[index])) { | ||
text = text + decodeRawString(nodes[index].getText()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could cast There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I first used as StringLiteral. If I use // before
const foo = "Unicode \u0023 \u{0023} " + "Hex \x23 " + "Octal \43";
// after
const foo = `Unicode # # Hex # Octal 43`; As you can see the octal escape is not supported with What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I switched back to @RyanCavanaugh // before
const foo = "Unicode \u0023 \u{0023} " + "Hex \x23 " + "Octal \43";
// after
const foo = `Unicode # # Hex # Octal 43`; |
||
index++; | ||
} | ||
|
||
text = escapeText(text); | ||
return [index, text]; | ||
} | ||
|
||
function nodesToTemplate(nodes: ReadonlyArray<Expression>) { | ||
const templateSpans: TemplateSpan[] = []; | ||
const [begin, headText] = concatConsecutiveString(0, nodes); | ||
const templateHead = createTemplateHead(headText); | ||
|
||
if (begin === nodes.length) return createNoSubstitutionTemplateLiteral(headText); | ||
|
||
for (let i = begin; i < nodes.length; i++) { | ||
const expression = isParenthesizedExpression(nodes[i]) ? (nodes[i] as ParenthesizedExpression).expression : nodes[i]; | ||
const [newIndex, subsequentText] = concatConsecutiveString(i + 1, nodes); | ||
i = newIndex - 1; | ||
|
||
const templatePart = i === nodes.length - 1 ? createTemplateTail(subsequentText) : createTemplateMiddle(subsequentText); | ||
templateSpans.push(createTemplateSpan(expression, templatePart)); | ||
} | ||
|
||
return createTemplateExpression(templateHead, templateSpans); | ||
} | ||
|
||
const octalToUnicode = (_match: string, grp: string) => String.fromCharCode(parseInt(grp, 8)); | ||
|
||
function decodeRawString(content: string) { | ||
const outerQuotes = /["']((.|\s)*)["']/; | ||
const octalEscape = /\\((?:[1-7][0-7]{0,2}|[0-7]{2,3}))/g; | ||
bigaru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return content.replace(outerQuotes, (_match, grp) => grp) | ||
.replace(octalEscape, octalToUnicode); | ||
|
||
} | ||
|
||
function escapeText(content: string) { | ||
return content.replace("`", "\`") // back-tick | ||
.replace("${", "$\\{"); // placeholder alike beginning | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think there's an extra backslash here. maybe it should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately if I pass as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that to explain my dilemma // before
const escape = "with ${dollar}"
// after using .replace("${", "$\{")
const escape = `with ${dollar}`
// after using .replace("${", "\${")
const escape = `with ${dollar}`
// after using .replace("${", "$\\{")
const escape = `with $\\{dollar}`
// after using .replace("${", "\\${")
const escape = `with \\${dollar}` using
using
Both approaches change the external behavior. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the external behavior cannot be preserved with one or two backslash, I've decided to remove escaping of opening placeholder. |
||
} | ||
|
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/// <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,30 @@ | ||
/// <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"); | ||
bigaru marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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,18 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// function tag(literals: TemplateStringsArray, ...placeholders: any[]) { 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,13 @@ | ||
/// <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,17 @@ | ||
/// <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,15 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// const age = 42 | ||
//// const foo = `foobar is a ${ age < 18 ? 'child' : /*x*/`/*y*/grown-up ${ age > 40 ? 'who needs probably 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 probably assistance' : '') }\``, | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/// <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" }\``, | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// const age = 42 | ||
//// const foo = `foobar is a ${ /*x*/a/*y*/ge < 18 ? 'child' : `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,15 @@ | ||
/// <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 string concatenation or template literal", | ||
actionName: "Convert to string concatenation", | ||
actionDescription: "Convert to string concatenation", | ||
newContent: | ||
`const age = 42 | ||
const foo = "foobar is " + age + " years old"`, | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// const foo = `/*x*/f/*y*/oobar is ${ 42 * 6 % 4} 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 % 4 + " years old"`, | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you explain why this is necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the idea behind is that
even if your cursor is inside a parentheses, you can still invoke this refactoring (as long as it's not a string binary expression)
both examples were invoked inside the parentheses:
It is not necessary but nice to have