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, 14 getAvailableActions 15 }); 16 17 function getAvailableActions(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) && isStringConcatenationValid(maybeBinary)) { 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 = !isStringConcatenationValid(nestedBinary); 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 getEditsForAction(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; 105 } 106 107 function isStringConcatenationValid(node: Node): boolean { 108 const { containsString, areOperatorsValid } = treeToArray(node); 109 return containsString && areOperatorsValid; 110 } 111 112 function treeToArray(current: Node): { nodes: Expression[], operators: Token<BinaryOperator>[], containsString: boolean, areOperatorsValid: boolean} { 113 if (isBinaryExpression(current)) { 114 const { nodes, operators, containsString: leftHasString, areOperatorsValid: leftOperatorValid } = treeToArray(current.left); 115 116 if (!leftHasString && !isStringLiteral(current.right) && !isTemplateExpression(current.right)) { 117 return { nodes: [current], operators: [], containsString: false, areOperatorsValid: true }; 118 } 119 120 const currentOperatorValid = current.operatorToken.kind === SyntaxKind.PlusToken; 121 const areOperatorsValid = leftOperatorValid && currentOperatorValid; 122 123 nodes.push(current.right); 124 operators.push(current.operatorToken); 125 126 return { nodes, operators, containsString: true, areOperatorsValid }; 127 } 128 129 return { nodes: [current as Expression], operators: [], containsString: isStringLiteral(current), areOperatorsValid: true }; 130 } 131 132 // to copy comments following the operator 133 // "foo" + /* comment */ "bar" 134 const copyTrailingOperatorComments = (operators: Token<BinaryOperator>[], file: SourceFile) => (index: number, targetNode: Node) => { 135 if (index < operators.length) { 136 copyTrailingComments(operators[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 137 } 138 }; 139 140 // to copy comments following the string 141 // "foo" /* comment */ + "bar" /* comment */ + "bar2" 142 const copyCommentFromMultiNode = (nodes: readonly Expression[], file: SourceFile, copyOperatorComments: (index: number, targetNode: Node) => void) => 143 (indexes: number[], targetNode: Node) => { 144 while (indexes.length > 0) { 145 const index = indexes.shift()!; 146 copyTrailingComments(nodes[index], targetNode, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 147 copyOperatorComments(index, targetNode); 148 } 149 }; 150 151 function concatConsecutiveString(index: number, nodes: readonly Expression[]): [number, string, number[]] { 152 const indexes = []; 153 let text = ""; 154 while (index < nodes.length) { 155 const node = nodes[index]; 156 if (isStringLiteralLike(node)) { 157 text = text + node.text; 158 indexes.push(index); 159 index++; 160 } 161 else if (isTemplateExpression(node)) { 162 text = text + node.head.text; 163 break; 164 } 165 else { 166 break; 167 } 168 } 169 return [index, text, indexes]; 170 } 171 172 function nodesToTemplate({ nodes, operators }: { nodes: readonly Expression[], operators: Token<BinaryOperator>[] }, file: SourceFile) { 173 const copyOperatorComments = copyTrailingOperatorComments(operators, file); 174 const copyCommentFromStringLiterals = copyCommentFromMultiNode(nodes, file, copyOperatorComments); 175 const [begin, headText, headIndexes] = concatConsecutiveString(0, nodes); 176 177 if (begin === nodes.length) { 178 const noSubstitutionTemplateLiteral = factory.createNoSubstitutionTemplateLiteral(headText); 179 copyCommentFromStringLiterals(headIndexes, noSubstitutionTemplateLiteral); 180 return noSubstitutionTemplateLiteral; 181 } 182 183 const templateSpans: TemplateSpan[] = []; 184 const templateHead = factory.createTemplateHead(headText); 185 copyCommentFromStringLiterals(headIndexes, templateHead); 186 187 for (let i = begin; i < nodes.length; i++) { 188 const currentNode = getExpressionFromParenthesesOrExpression(nodes[i]); 189 copyOperatorComments(i, currentNode); 190 191 const [newIndex, subsequentText, stringIndexes] = concatConsecutiveString(i + 1, nodes); 192 i = newIndex - 1; 193 const isLast = i === nodes.length - 1; 194 195 if (isTemplateExpression(currentNode)) { 196 const spans = map(currentNode.templateSpans, (span, index) => { 197 copyExpressionComments(span); 198 const nextSpan = currentNode.templateSpans[index + 1]; 199 const text = span.literal.text + (nextSpan ? "" : subsequentText); 200 return factory.createTemplateSpan(span.expression, isLast ? factory.createTemplateTail(text) : factory.createTemplateMiddle(text)); 201 }); 202 templateSpans.push(...spans); 203 } 204 else { 205 const templatePart = isLast ? factory.createTemplateTail(subsequentText) : factory.createTemplateMiddle(subsequentText); 206 copyCommentFromStringLiterals(stringIndexes, templatePart); 207 templateSpans.push(factory.createTemplateSpan(currentNode, templatePart)); 208 } 209 } 210 211 return factory.createTemplateExpression(templateHead, templateSpans); 212 } 213 214 // to copy comments following the opening & closing parentheses 215 // "foo" + ( /* comment */ 5 + 5 ) /* comment */ + "bar" 216 function copyExpressionComments(node: ParenthesizedExpression | TemplateSpan) { 217 const file = node.getSourceFile(); 218 copyTrailingComments(node, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 219 copyTrailingAsLeadingComments(node.expression, node.expression, file, SyntaxKind.MultiLineCommentTrivia, /* hasTrailingNewLine */ false); 220 } 221 222 function getExpressionFromParenthesesOrExpression(node: Expression) { 223 if (isParenthesizedExpression(node)) { 224 copyExpressionComments(node); 225 node = node.expression; 226 } 227 return node; 228 } 229} 230