.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / prefer-object-spread.js
1 /**
2  * @fileoverview Prefers object spread property over Object.assign
3  * @author Sharmila Jesupaul
4  * See LICENSE file in root directory for full license.
5  */
6
7 "use strict";
8
9 const { CALL, ReferenceTracker } = require("eslint-utils");
10 const {
11     isCommaToken,
12     isOpeningParenToken,
13     isClosingParenToken,
14     isParenthesised
15 } = require("./utils/ast-utils");
16
17 const ANY_SPACE = /\s/u;
18
19 /**
20  * Helper that checks if the Object.assign call has array spread
21  * @param {ASTNode} node The node that the rule warns on
22  * @returns {boolean} - Returns true if the Object.assign call has array spread
23  */
24 function hasArraySpread(node) {
25     return node.arguments.some(arg => arg.type === "SpreadElement");
26 }
27
28 /**
29  * Determines whether the given node is an accessor property (getter/setter).
30  * @param {ASTNode} node Node to check.
31  * @returns {boolean} `true` if the node is a getter or a setter.
32  */
33 function isAccessorProperty(node) {
34     return node.type === "Property" &&
35         (node.kind === "get" || node.kind === "set");
36 }
37
38 /**
39  * Determines whether the given object expression node has accessor properties (getters/setters).
40  * @param {ASTNode} node `ObjectExpression` node to check.
41  * @returns {boolean} `true` if the node has at least one getter/setter.
42  */
43 function hasAccessors(node) {
44     return node.properties.some(isAccessorProperty);
45 }
46
47 /**
48  * Determines whether the given call expression node has object expression arguments with accessor properties (getters/setters).
49  * @param {ASTNode} node `CallExpression` node to check.
50  * @returns {boolean} `true` if the node has at least one argument that is an object expression with at least one getter/setter.
51  */
52 function hasArgumentsWithAccessors(node) {
53     return node.arguments
54         .filter(arg => arg.type === "ObjectExpression")
55         .some(hasAccessors);
56 }
57
58 /**
59  * Helper that checks if the node needs parentheses to be valid JS.
60  * The default is to wrap the node in parentheses to avoid parsing errors.
61  * @param {ASTNode} node The node that the rule warns on
62  * @param {Object} sourceCode in context sourcecode object
63  * @returns {boolean} - Returns true if the node needs parentheses
64  */
65 function needsParens(node, sourceCode) {
66     const parent = node.parent;
67
68     switch (parent.type) {
69         case "VariableDeclarator":
70         case "ArrayExpression":
71         case "ReturnStatement":
72         case "CallExpression":
73         case "Property":
74             return false;
75         case "AssignmentExpression":
76             return parent.left === node && !isParenthesised(sourceCode, node);
77         default:
78             return !isParenthesised(sourceCode, node);
79     }
80 }
81
82 /**
83  * Determines if an argument needs parentheses. The default is to not add parens.
84  * @param {ASTNode} node The node to be checked.
85  * @param {Object} sourceCode in context sourcecode object
86  * @returns {boolean} True if the node needs parentheses
87  */
88 function argNeedsParens(node, sourceCode) {
89     switch (node.type) {
90         case "AssignmentExpression":
91         case "ArrowFunctionExpression":
92         case "ConditionalExpression":
93             return !isParenthesised(sourceCode, node);
94         default:
95             return false;
96     }
97 }
98
99 /**
100  * Get the parenthesis tokens of a given ObjectExpression node.
101  * This includes the braces of the object literal and enclosing parentheses.
102  * @param {ASTNode} node The node to get.
103  * @param {Token} leftArgumentListParen The opening paren token of the argument list.
104  * @param {SourceCode} sourceCode The source code object to get tokens.
105  * @returns {Token[]} The parenthesis tokens of the node. This is sorted by the location.
106  */
107 function getParenTokens(node, leftArgumentListParen, sourceCode) {
108     const parens = [sourceCode.getFirstToken(node), sourceCode.getLastToken(node)];
109     let leftNext = sourceCode.getTokenBefore(node);
110     let rightNext = sourceCode.getTokenAfter(node);
111
112     // Note: don't include the parens of the argument list.
113     while (
114         leftNext &&
115         rightNext &&
116         leftNext.range[0] > leftArgumentListParen.range[0] &&
117         isOpeningParenToken(leftNext) &&
118         isClosingParenToken(rightNext)
119     ) {
120         parens.push(leftNext, rightNext);
121         leftNext = sourceCode.getTokenBefore(leftNext);
122         rightNext = sourceCode.getTokenAfter(rightNext);
123     }
124
125     return parens.sort((a, b) => a.range[0] - b.range[0]);
126 }
127
128 /**
129  * Get the range of a given token and around whitespaces.
130  * @param {Token} token The token to get range.
131  * @param {SourceCode} sourceCode The source code object to get tokens.
132  * @returns {number} The end of the range of the token and around whitespaces.
133  */
134 function getStartWithSpaces(token, sourceCode) {
135     const text = sourceCode.text;
136     let start = token.range[0];
137
138     // If the previous token is a line comment then skip this step to avoid commenting this token out.
139     {
140         const prevToken = sourceCode.getTokenBefore(token, { includeComments: true });
141
142         if (prevToken && prevToken.type === "Line") {
143             return start;
144         }
145     }
146
147     // Detect spaces before the token.
148     while (ANY_SPACE.test(text[start - 1] || "")) {
149         start -= 1;
150     }
151
152     return start;
153 }
154
155 /**
156  * Get the range of a given token and around whitespaces.
157  * @param {Token} token The token to get range.
158  * @param {SourceCode} sourceCode The source code object to get tokens.
159  * @returns {number} The start of the range of the token and around whitespaces.
160  */
161 function getEndWithSpaces(token, sourceCode) {
162     const text = sourceCode.text;
163     let end = token.range[1];
164
165     // Detect spaces after the token.
166     while (ANY_SPACE.test(text[end] || "")) {
167         end += 1;
168     }
169
170     return end;
171 }
172
173 /**
174  * Autofixes the Object.assign call to use an object spread instead.
175  * @param {ASTNode|null} node The node that the rule warns on, i.e. the Object.assign call
176  * @param {string} sourceCode sourceCode of the Object.assign call
177  * @returns {Function} autofixer - replaces the Object.assign with a spread object.
178  */
179 function defineFixer(node, sourceCode) {
180     return function *(fixer) {
181         const leftParen = sourceCode.getTokenAfter(node.callee, isOpeningParenToken);
182         const rightParen = sourceCode.getLastToken(node);
183
184         // Remove everything before the opening paren: callee `Object.assign`, type arguments, and whitespace between the callee and the paren.
185         yield fixer.removeRange([node.range[0], leftParen.range[0]]);
186
187         // Replace the parens of argument list to braces.
188         if (needsParens(node, sourceCode)) {
189             yield fixer.replaceText(leftParen, "({");
190             yield fixer.replaceText(rightParen, "})");
191         } else {
192             yield fixer.replaceText(leftParen, "{");
193             yield fixer.replaceText(rightParen, "}");
194         }
195
196         // Process arguments.
197         for (const argNode of node.arguments) {
198             const innerParens = getParenTokens(argNode, leftParen, sourceCode);
199             const left = innerParens.shift();
200             const right = innerParens.pop();
201
202             if (argNode.type === "ObjectExpression") {
203                 const maybeTrailingComma = sourceCode.getLastToken(argNode, 1);
204                 const maybeArgumentComma = sourceCode.getTokenAfter(right);
205
206                 /*
207                  * Make bare this object literal.
208                  * And remove spaces inside of the braces for better formatting.
209                  */
210                 for (const innerParen of innerParens) {
211                     yield fixer.remove(innerParen);
212                 }
213                 const leftRange = [left.range[0], getEndWithSpaces(left, sourceCode)];
214                 const rightRange = [
215                     Math.max(getStartWithSpaces(right, sourceCode), leftRange[1]), // Ensure ranges don't overlap
216                     right.range[1]
217                 ];
218
219                 yield fixer.removeRange(leftRange);
220                 yield fixer.removeRange(rightRange);
221
222                 // Remove the comma of this argument if it's duplication.
223                 if (
224                     (argNode.properties.length === 0 || isCommaToken(maybeTrailingComma)) &&
225                     isCommaToken(maybeArgumentComma)
226                 ) {
227                     yield fixer.remove(maybeArgumentComma);
228                 }
229             } else {
230
231                 // Make spread.
232                 if (argNeedsParens(argNode, sourceCode)) {
233                     yield fixer.insertTextBefore(left, "...(");
234                     yield fixer.insertTextAfter(right, ")");
235                 } else {
236                     yield fixer.insertTextBefore(left, "...");
237                 }
238             }
239         }
240     };
241 }
242
243 module.exports = {
244     meta: {
245         type: "suggestion",
246
247         docs: {
248             description:
249                 "disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead.",
250             category: "Stylistic Issues",
251             recommended: false,
252             url: "https://eslint.org/docs/rules/prefer-object-spread"
253         },
254
255         schema: [],
256         fixable: "code",
257
258         messages: {
259             useSpreadMessage: "Use an object spread instead of `Object.assign` eg: `{ ...foo }`.",
260             useLiteralMessage: "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`."
261         }
262     },
263
264     create(context) {
265         const sourceCode = context.getSourceCode();
266
267         return {
268             Program() {
269                 const scope = context.getScope();
270                 const tracker = new ReferenceTracker(scope);
271                 const trackMap = {
272                     Object: {
273                         assign: { [CALL]: true }
274                     }
275                 };
276
277                 // Iterate all calls of `Object.assign` (only of the global variable `Object`).
278                 for (const { node } of tracker.iterateGlobalReferences(trackMap)) {
279                     if (
280                         node.arguments.length >= 1 &&
281                         node.arguments[0].type === "ObjectExpression" &&
282                         !hasArraySpread(node) &&
283                         !(
284                             node.arguments.length > 1 &&
285                             hasArgumentsWithAccessors(node)
286                         )
287                     ) {
288                         const messageId = node.arguments.length === 1
289                             ? "useLiteralMessage"
290                             : "useSpreadMessage";
291                         const fix = defineFixer(node, sourceCode);
292
293                         context.report({ node, messageId, fix });
294                     }
295                 }
296             }
297         };
298     }
299 };