1/* @internal */ 2namespace ts.refactor.convertStringOrTemplateLiteral { 3 const refactorName = "Convert to template string"; 4 const refactorDescription = getLocaleSpecificMessage(Diagnostics.Convert_to_template_string); 5 6 const convertStringAction = { 7 name: refactorName, 8 description: refactorDescription, 9 kind: "refactor.rewrite.string" 10 }; 11 registerRefactor(refactorName, { 12 kinds: [convertStringAction.kind], 13 getEditsForAction: getRefactorEditsToConvertToTemplateString, 14 getAvailableActions: getRefactorActionsToConvertToTemplateString 15 }); 16 17 function getRefactorActionsToConvertToTemplateString(context: RefactorContext): readonly ApplicableRefactorInfo[] { 18 const { file, startPosition } = context; 19 const node = getNodeOrParentOfParentheses(file, startPosition); 20 const maybeBinary = getParentBinaryExpression(node); 21 const refactorInfo: ApplicableRefactorInfo = { name: refactorName, description: refactorDescription, actions: [] }; 22 23 if (isBinaryExpression(maybeBinary) && treeToArray(maybeBinary).isValidConcatenation) { 24 refactorInfo.actions.push(convertStringAction); 25 return [refactorInfo]; 26 } 27 else if (context.preferences.provideRefactorNotApplicableReason) { 28 refactorInfo.actions.push({ ...convertStringAction, 29 notApplicableReason: getLocaleSpecificMessage(Diagnostics.Can_only_convert_string_concatenation) 30 }); 31 return [refactorInfo]; 32 } 33 return emptyArray; 34 } 35 36 function getNodeOrParentOfParentheses(file: SourceFile, startPosition: number) { 37 const node = getTokenAtPosition(file, startPosition); 38 const nestedBinary = getParentBinaryExpression(node); 39 const isNonStringBinary = !treeToArray(nestedBinary).isValidConcatenation; 40 41 if ( 42 isNonStringBinary && 43 isParenthesizedExpression(nestedBinary.parent) && 44 isBinaryExpression(nestedBinary.parent.parent) 45 ) { 46 return nestedBinary.parent.parent; 47 } 48 return node; 49 } 50 51 function getRefactorEditsToConvertToTemplateString(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { 52 const { file, startPosition } = context; 53 const node = getNodeOrParentOfParentheses(file, startPosition); 54 55 switch (actionName) { 56 case refactorDescription: 57 return { edits: getEditsForToTemplateLiteral(context, node) }; 58 default: 59 return Debug.fail("invalid action"); 60 } 61 } 62 63 function getEditsForToTemplateLiteral(context: RefactorContext, node: Node) { 64 const maybeBinary = getParentBinaryExpression(node); 65 const file = context.file; 66 67 const templateLiteral = nodesToTemplate(treeToArray(maybeBinary), file); 68 const trailingCommentRanges = getTrailingCommentRanges(file.text, maybeBinary.end); 69 70 if (trailingCommentRanges) { 71 const lastComment = trailingCommentRanges[trailingCommentRanges.length - 1]; 72 const trailingRange = { pos: trailingCommentRanges[0].pos, end: lastComment.end }; 73 74 // since suppressTrailingTrivia(maybeBinary) does not work, the trailing comment is removed manually 75 // otherwise it would have the trailing comment twice 76 return textChanges.ChangeTracker.with(context, t => { 77 t.deleteRange(file, trailingRange); 78 t.replaceNode(file, maybeBinary, templateLiteral); 79 }); 80 } 81 else { 82 return textChanges.ChangeTracker.with(context, t => t.replaceNode(file, maybeBinary, templateLiteral)); 83 } 84 } 85 86 function isNotEqualsOperator(node: BinaryExpression) { 87 return node.operatorToken.kind !== SyntaxKind.EqualsToken; 88 } 89 90 function getParentBinaryExpression(expr: Node) { 91 const container = findAncestor(expr.parent, n => { 92 switch (n.kind) { 93 case SyntaxKind.PropertyAccessExpression: 94 case SyntaxKind.ElementAccessExpression: 95 return false; 96 case SyntaxKind.TemplateExpression: 97 case SyntaxKind.BinaryExpression: 98 return !(isBinaryExpression(n.parent) && isNotEqualsOperator(n.parent)); 99 default: 100 return "quit"; 101 } 102 }); 103 104 return (container || expr) as Expression; 105 } 106 107 function treeToArray(current: Expression) { 108 const loop = (current: Node): { nodes: Expression[], operators: Token<BinaryOperator>[], hasString: boolean, validOperators: boolean} => { 109 if (!isBinaryExpression(current)) { 110 return { nodes: [current as Expression], operators: [], validOperators: true, 111 hasString: isStringLiteral(current) || isNoSubstitutionTemplateLiteral(current) }; 112 } 113 const { nodes, operators, hasString: leftHasString, validOperators: leftOperatorValid } = loop(current.left); 114 115 if (!(leftHasString || isStringLiteral(current.right) || isTemplateExpression(current.right))) { 116 return { nodes: [current], operators: [], hasString: false, validOperators: true }; 117 } 118 119 const currentOperatorValid = current.operatorToken.kind === SyntaxKind.PlusToken; 120 const validOperators = leftOperatorValid && currentOperatorValid; 121 122 nodes.push(current.right); 123 operators.push(current.operatorToken); 124 125 return { nodes, operators, hasString: true, validOperators }; 126 }; 127 const { nodes, operators, validOperators, hasString } = loop(current); 128 return { nodes, operators, isValidConcatenation: validOperators && hasString }; 129 } 130 131 // to copy comments following the operator 132 // "foo" + /* comment */ "bar" 133 const copyTrailingOperatorComments = (operators: Token<BinaryOperator>[], file: SourceFile) => (index: number, targetNode: Node) => { 134 if (index < operators.length) { 135 copyTrailingComments(operators[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 136 } 137 }; 138 139 // to copy comments following the string 140 // "foo" /* comment */ + "bar" /* comment */ + "bar2" 141 const copyCommentFromMultiNode = (nodes: readonly Expression[], file: SourceFile, copyOperatorComments: (index: number, targetNode: Node) => void) => 142 (indexes: number[], targetNode: Node) => { 143 while (indexes.length > 0) { 144 const index = indexes.shift()!; 145 copyTrailingComments(nodes[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 146 copyOperatorComments(index, targetNode); 147 } 148 }; 149 150 function escapeRawStringForTemplate(s: string) { 151 // Escaping for $s in strings that are to be used in template strings 152 // Naive implementation: replace \x by itself and otherwise $ and ` by \$ and \`. 153 // But to complicate it a bit, this should work for raw strings too. 154 return s.replace(/\\.|[$`]/g, m => m[0] === "\\" ? m : "\\" + m); 155 // Finally, a less-backslash-happy version can work too, doing only ${ instead of all $s: 156 // s.replace(/\\.|\${|`/g, m => m[0] === "\\" ? m : "\\" + m); 157 // but `\$${foo}` is likely more clear than the more-confusing-but-still-working `$${foo}`. 158 } 159 160 function getRawTextOfTemplate(node: TemplateHead | TemplateMiddle | TemplateTail) { 161 // in these cases the right side is ${ 162 const rightShaving = isTemplateHead(node) || isTemplateMiddle(node) ? -2 : -1; 163 return getTextOfNode(node).slice(1, rightShaving); 164 } 165 166 function concatConsecutiveString(index: number, nodes: readonly Expression[]): [nextIndex: number, text: string, rawText: string, usedIndexes: number[]] { 167 const indexes = []; 168 let text = "", rawText = ""; 169 while (index < nodes.length) { 170 const node = nodes[index]; 171 if (isStringLiteralLike(node)) { // includes isNoSubstitutionTemplateLiteral(node) 172 text += node.text; 173 rawText += escapeRawStringForTemplate(getTextOfNode(node).slice(1, -1)); 174 indexes.push(index); 175 index++; 176 } 177 else if (isTemplateExpression(node)) { 178 text += node.head.text; 179 rawText += getRawTextOfTemplate(node.head); 180 break; 181 } 182 else { 183 break; 184 } 185 } 186 return [index, text, rawText, indexes]; 187 } 188 189 function nodesToTemplate({ nodes, operators }: { nodes: readonly Expression[], operators: Token<BinaryOperator>[] }, file: SourceFile) { 190 const copyOperatorComments = copyTrailingOperatorComments(operators, file); 191 const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); 192 const [begin, headText, rawHeadText, headIndexes] = concatConsecutiveString(0, nodes); 193 194 if (begin === nodes.length) { 195 const noSubstitutionTemplateLiteral = factory.createNoSubstitutionTemplateLiteral(headText, rawHeadText); 196 copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); 197 return noSubstitutionTemplateLiteral; 198 } 199 200 const templateSpans: TemplateSpan[] = []; 201 const templateHead = factory.createTemplateHead(headText, rawHeadText); 202 copyCommentFromStringLiterals(headIndexes, templateHead); 203 204 for (let i = begin; i < nodes.length; i++) { 205 const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); 206 copyOperatorComments(i, currentNode); 207 208 const [newIndex, subsequentText, rawSubsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); 209 i = newIndex - 1; 210 const isLast = i === nodes.length - 1; 211 212 if (isTemplateExpression(currentNode)) { 213 const spans = map(currentNode.templateSpans, (span, index) => { 214 copyExpressionComments(span); 215 const isLastSpan = index === currentNode.templateSpans.length - 1; 216 const text = span.literal.text + (isLastSpan ? subsequentText : ""); 217 const rawText = getRawTextOfTemplate(span.literal) + (isLastSpan ? rawSubsequentText : ""); 218 return factory.createTemplateSpan(span.expression, isLast && isLastSpan 219 ? factory.createTemplateTail(text, rawText) 220 : factory.createTemplateMiddle(text, rawText)); 221 }); 222 templateSpans.push(...spans); 223 } 224 else { 225 const templatePart = isLast 226 ? factory.createTemplateTail(subsequentText, rawSubsequentText) 227 : factory.createTemplateMiddle(subsequentText, rawSubsequentText); 228 copyCommentFromStringLiterals(stringIndexes, templatePart); 229 templateSpans.push(factory.createTemplateSpan(currentNode, templatePart)); 230 } 231 } 232 233 return factory.createTemplateExpression(templateHead, templateSpans); 234 } 235 236 // to copy comments following the opening & closing parentheses 237 // "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar" 238 function copyExpressionComments(node: ParenthesizedExpression | TemplateSpan) { 239 const file = node.getSourceFile(); 240 copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 241 copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 242 } 243 244 function getExpressionFromParenthesesOrExpression(node: Expression) { 245 if (isParenthesizedExpression(node)) { 246 copyExpressionComments(node); 247 node = node.expression; 248 } 249 return node; 250 } 251} 252