• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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