• 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,
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