.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / prefer-template.js
1 /**
2  * @fileoverview A rule to suggest using template literals instead of string concatenation.
3  * @author Toru Nagashima
4  */
5
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
12 const astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Helpers
16 //------------------------------------------------------------------------------
17
18 /**
19  * Checks whether or not a given node is a concatenation.
20  * @param {ASTNode} node A node to check.
21  * @returns {boolean} `true` if the node is a concatenation.
22  */
23 function isConcatenation(node) {
24     return node.type === "BinaryExpression" && node.operator === "+";
25 }
26
27 /**
28  * Gets the top binary expression node for concatenation in parents of a given node.
29  * @param {ASTNode} node A node to get.
30  * @returns {ASTNode} the top binary expression node in parents of a given node.
31  */
32 function getTopConcatBinaryExpression(node) {
33     let currentNode = node;
34
35     while (isConcatenation(currentNode.parent)) {
36         currentNode = currentNode.parent;
37     }
38     return currentNode;
39 }
40
41 /**
42  * Checks whether or not a node contains a string literal with an octal or non-octal decimal escape sequence
43  * @param {ASTNode} node A node to check
44  * @returns {boolean} `true` if at least one string literal within the node contains
45  * an octal or non-octal decimal escape sequence
46  */
47 function hasOctalOrNonOctalDecimalEscapeSequence(node) {
48     if (isConcatenation(node)) {
49         return (
50             hasOctalOrNonOctalDecimalEscapeSequence(node.left) ||
51             hasOctalOrNonOctalDecimalEscapeSequence(node.right)
52         );
53     }
54
55     // No need to check TemplateLiterals – would throw parsing error
56     if (node.type === "Literal" && typeof node.value === "string") {
57         return astUtils.hasOctalOrNonOctalDecimalEscapeSequence(node.raw);
58     }
59
60     return false;
61 }
62
63 /**
64  * Checks whether or not a given binary expression has string literals.
65  * @param {ASTNode} node A node to check.
66  * @returns {boolean} `true` if the node has string literals.
67  */
68 function hasStringLiteral(node) {
69     if (isConcatenation(node)) {
70
71         // `left` is deeper than `right` normally.
72         return hasStringLiteral(node.right) || hasStringLiteral(node.left);
73     }
74     return astUtils.isStringLiteral(node);
75 }
76
77 /**
78  * Checks whether or not a given binary expression has non string literals.
79  * @param {ASTNode} node A node to check.
80  * @returns {boolean} `true` if the node has non string literals.
81  */
82 function hasNonStringLiteral(node) {
83     if (isConcatenation(node)) {
84
85         // `left` is deeper than `right` normally.
86         return hasNonStringLiteral(node.right) || hasNonStringLiteral(node.left);
87     }
88     return !astUtils.isStringLiteral(node);
89 }
90
91 /**
92  * Determines whether a given node will start with a template curly expression (`${}`) when being converted to a template literal.
93  * @param {ASTNode} node The node that will be fixed to a template literal
94  * @returns {boolean} `true` if the node will start with a template curly.
95  */
96 function startsWithTemplateCurly(node) {
97     if (node.type === "BinaryExpression") {
98         return startsWithTemplateCurly(node.left);
99     }
100     if (node.type === "TemplateLiteral") {
101         return node.expressions.length && node.quasis.length && node.quasis[0].range[0] === node.quasis[0].range[1];
102     }
103     return node.type !== "Literal" || typeof node.value !== "string";
104 }
105
106 /**
107  * Determines whether a given node end with a template curly expression (`${}`) when being converted to a template literal.
108  * @param {ASTNode} node The node that will be fixed to a template literal
109  * @returns {boolean} `true` if the node will end with a template curly.
110  */
111 function endsWithTemplateCurly(node) {
112     if (node.type === "BinaryExpression") {
113         return startsWithTemplateCurly(node.right);
114     }
115     if (node.type === "TemplateLiteral") {
116         return node.expressions.length && node.quasis.length && node.quasis[node.quasis.length - 1].range[0] === node.quasis[node.quasis.length - 1].range[1];
117     }
118     return node.type !== "Literal" || typeof node.value !== "string";
119 }
120
121 //------------------------------------------------------------------------------
122 // Rule Definition
123 //------------------------------------------------------------------------------
124
125 module.exports = {
126     meta: {
127         type: "suggestion",
128
129         docs: {
130             description: "require template literals instead of string concatenation",
131             category: "ECMAScript 6",
132             recommended: false,
133             url: "https://eslint.org/docs/rules/prefer-template"
134         },
135
136         schema: [],
137         fixable: "code",
138
139         messages: {
140             unexpectedStringConcatenation: "Unexpected string concatenation."
141         }
142     },
143
144     create(context) {
145         const sourceCode = context.getSourceCode();
146         let done = Object.create(null);
147
148         /**
149          * Gets the non-token text between two nodes, ignoring any other tokens that appear between the two tokens.
150          * @param {ASTNode} node1 The first node
151          * @param {ASTNode} node2 The second node
152          * @returns {string} The text between the nodes, excluding other tokens
153          */
154         function getTextBetween(node1, node2) {
155             const allTokens = [node1].concat(sourceCode.getTokensBetween(node1, node2)).concat(node2);
156             const sourceText = sourceCode.getText();
157
158             return allTokens.slice(0, -1).reduce((accumulator, token, index) => accumulator + sourceText.slice(token.range[1], allTokens[index + 1].range[0]), "");
159         }
160
161         /**
162          * Returns a template literal form of the given node.
163          * @param {ASTNode} currentNode A node that should be converted to a template literal
164          * @param {string} textBeforeNode Text that should appear before the node
165          * @param {string} textAfterNode Text that should appear after the node
166          * @returns {string} A string form of this node, represented as a template literal
167          */
168         function getTemplateLiteral(currentNode, textBeforeNode, textAfterNode) {
169             if (currentNode.type === "Literal" && typeof currentNode.value === "string") {
170
171                 /*
172                  * If the current node is a string literal, escape any instances of ${ or ` to prevent them from being interpreted
173                  * as a template placeholder. However, if the code already contains a backslash before the ${ or `
174                  * for some reason, don't add another backslash, because that would change the meaning of the code (it would cause
175                  * an actual backslash character to appear before the dollar sign).
176                  */
177                 return `\`${currentNode.raw.slice(1, -1).replace(/\\*(\$\{|`)/gu, matched => {
178                     if (matched.lastIndexOf("\\") % 2) {
179                         return `\\${matched}`;
180                     }
181                     return matched;
182
183                 // Unescape any quotes that appear in the original Literal that no longer need to be escaped.
184                 }).replace(new RegExp(`\\\\${currentNode.raw[0]}`, "gu"), currentNode.raw[0])}\``;
185             }
186
187             if (currentNode.type === "TemplateLiteral") {
188                 return sourceCode.getText(currentNode);
189             }
190
191             if (isConcatenation(currentNode) && hasStringLiteral(currentNode) && hasNonStringLiteral(currentNode)) {
192                 const plusSign = sourceCode.getFirstTokenBetween(currentNode.left, currentNode.right, token => token.value === "+");
193                 const textBeforePlus = getTextBetween(currentNode.left, plusSign);
194                 const textAfterPlus = getTextBetween(plusSign, currentNode.right);
195                 const leftEndsWithCurly = endsWithTemplateCurly(currentNode.left);
196                 const rightStartsWithCurly = startsWithTemplateCurly(currentNode.right);
197
198                 if (leftEndsWithCurly) {
199
200                     // If the left side of the expression ends with a template curly, add the extra text to the end of the curly bracket.
201                     // `foo${bar}` /* comment */ + 'baz' --> `foo${bar /* comment */  }${baz}`
202                     return getTemplateLiteral(currentNode.left, textBeforeNode, textBeforePlus + textAfterPlus).slice(0, -1) +
203                         getTemplateLiteral(currentNode.right, null, textAfterNode).slice(1);
204                 }
205                 if (rightStartsWithCurly) {
206
207                     // Otherwise, if the right side of the expression starts with a template curly, add the text there.
208                     // 'foo' /* comment */ + `${bar}baz` --> `foo${ /* comment */  bar}baz`
209                     return getTemplateLiteral(currentNode.left, textBeforeNode, null).slice(0, -1) +
210                         getTemplateLiteral(currentNode.right, textBeforePlus + textAfterPlus, textAfterNode).slice(1);
211                 }
212
213                 /*
214                  * Otherwise, these nodes should not be combined into a template curly, since there is nowhere to put
215                  * the text between them.
216                  */
217                 return `${getTemplateLiteral(currentNode.left, textBeforeNode, null)}${textBeforePlus}+${textAfterPlus}${getTemplateLiteral(currentNode.right, textAfterNode, null)}`;
218             }
219
220             return `\`\${${textBeforeNode || ""}${sourceCode.getText(currentNode)}${textAfterNode || ""}}\``;
221         }
222
223         /**
224          * Returns a fixer object that converts a non-string binary expression to a template literal
225          * @param {SourceCodeFixer} fixer The fixer object
226          * @param {ASTNode} node A node that should be converted to a template literal
227          * @returns {Object} A fix for this binary expression
228          */
229         function fixNonStringBinaryExpression(fixer, node) {
230             const topBinaryExpr = getTopConcatBinaryExpression(node.parent);
231
232             if (hasOctalOrNonOctalDecimalEscapeSequence(topBinaryExpr)) {
233                 return null;
234             }
235
236             return fixer.replaceText(topBinaryExpr, getTemplateLiteral(topBinaryExpr, null, null));
237         }
238
239         /**
240          * Reports if a given node is string concatenation with non string literals.
241          * @param {ASTNode} node A node to check.
242          * @returns {void}
243          */
244         function checkForStringConcat(node) {
245             if (!astUtils.isStringLiteral(node) || !isConcatenation(node.parent)) {
246                 return;
247             }
248
249             const topBinaryExpr = getTopConcatBinaryExpression(node.parent);
250
251             // Checks whether or not this node had been checked already.
252             if (done[topBinaryExpr.range[0]]) {
253                 return;
254             }
255             done[topBinaryExpr.range[0]] = true;
256
257             if (hasNonStringLiteral(topBinaryExpr)) {
258                 context.report({
259                     node: topBinaryExpr,
260                     messageId: "unexpectedStringConcatenation",
261                     fix: fixer => fixNonStringBinaryExpression(fixer, node)
262                 });
263             }
264         }
265
266         return {
267             Program() {
268                 done = Object.create(null);
269             },
270
271             Literal: checkForStringConcat,
272             TemplateLiteral: checkForStringConcat
273         };
274     }
275 };