minor adjustment to readme
[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  * Helper that checks if the node needs parentheses to be valid JS.
30  * The default is to wrap the node in parentheses to avoid parsing errors.
31  * @param {ASTNode} node The node that the rule warns on
32  * @param {Object} sourceCode in context sourcecode object
33  * @returns {boolean} - Returns true if the node needs parentheses
34  */
35 function needsParens(node, sourceCode) {
36     const parent = node.parent;
37
38     switch (parent.type) {
39         case "VariableDeclarator":
40         case "ArrayExpression":
41         case "ReturnStatement":
42         case "CallExpression":
43         case "Property":
44             return false;
45         case "AssignmentExpression":
46             return parent.left === node && !isParenthesised(sourceCode, node);
47         default:
48             return !isParenthesised(sourceCode, node);
49     }
50 }
51
52 /**
53  * Determines if an argument needs parentheses. The default is to not add parens.
54  * @param {ASTNode} node The node to be checked.
55  * @param {Object} sourceCode in context sourcecode object
56  * @returns {boolean} True if the node needs parentheses
57  */
58 function argNeedsParens(node, sourceCode) {
59     switch (node.type) {
60         case "AssignmentExpression":
61         case "ArrowFunctionExpression":
62         case "ConditionalExpression":
63             return !isParenthesised(sourceCode, node);
64         default:
65             return false;
66     }
67 }
68
69 /**
70  * Get the parenthesis tokens of a given ObjectExpression node.
71  * This incldues the braces of the object literal and enclosing parentheses.
72  * @param {ASTNode} node The node to get.
73  * @param {Token} leftArgumentListParen The opening paren token of the argument list.
74  * @param {SourceCode} sourceCode The source code object to get tokens.
75  * @returns {Token[]} The parenthesis tokens of the node. This is sorted by the location.
76  */
77 function getParenTokens(node, leftArgumentListParen, sourceCode) {
78     const parens = [sourceCode.getFirstToken(node), sourceCode.getLastToken(node)];
79     let leftNext = sourceCode.getTokenBefore(node);
80     let rightNext = sourceCode.getTokenAfter(node);
81
82     // Note: don't include the parens of the argument list.
83     while (
84         leftNext &&
85         rightNext &&
86         leftNext.range[0] > leftArgumentListParen.range[0] &&
87         isOpeningParenToken(leftNext) &&
88         isClosingParenToken(rightNext)
89     ) {
90         parens.push(leftNext, rightNext);
91         leftNext = sourceCode.getTokenBefore(leftNext);
92         rightNext = sourceCode.getTokenAfter(rightNext);
93     }
94
95     return parens.sort((a, b) => a.range[0] - b.range[0]);
96 }
97
98 /**
99  * Get the range of a given token and around whitespaces.
100  * @param {Token} token The token to get range.
101  * @param {SourceCode} sourceCode The source code object to get tokens.
102  * @returns {number} The end of the range of the token and around whitespaces.
103  */
104 function getStartWithSpaces(token, sourceCode) {
105     const text = sourceCode.text;
106     let start = token.range[0];
107
108     // If the previous token is a line comment then skip this step to avoid commenting this token out.
109     {
110         const prevToken = sourceCode.getTokenBefore(token, { includeComments: true });
111
112         if (prevToken && prevToken.type === "Line") {
113             return start;
114         }
115     }
116
117     // Detect spaces before the token.
118     while (ANY_SPACE.test(text[start - 1] || "")) {
119         start -= 1;
120     }
121
122     return start;
123 }
124
125 /**
126  * Get the range of a given token and around whitespaces.
127  * @param {Token} token The token to get range.
128  * @param {SourceCode} sourceCode The source code object to get tokens.
129  * @returns {number} The start of the range of the token and around whitespaces.
130  */
131 function getEndWithSpaces(token, sourceCode) {
132     const text = sourceCode.text;
133     let end = token.range[1];
134
135     // Detect spaces after the token.
136     while (ANY_SPACE.test(text[end] || "")) {
137         end += 1;
138     }
139
140     return end;
141 }
142
143 /**
144  * Autofixes the Object.assign call to use an object spread instead.
145  * @param {ASTNode|null} node The node that the rule warns on, i.e. the Object.assign call
146  * @param {string} sourceCode sourceCode of the Object.assign call
147  * @returns {Function} autofixer - replaces the Object.assign with a spread object.
148  */
149 function defineFixer(node, sourceCode) {
150     return function *(fixer) {
151         const leftParen = sourceCode.getTokenAfter(node.callee, isOpeningParenToken);
152         const rightParen = sourceCode.getLastToken(node);
153
154         // Remove the callee `Object.assign`
155         yield fixer.remove(node.callee);
156
157         // Replace the parens of argument list to braces.
158         if (needsParens(node, sourceCode)) {
159             yield fixer.replaceText(leftParen, "({");
160             yield fixer.replaceText(rightParen, "})");
161         } else {
162             yield fixer.replaceText(leftParen, "{");
163             yield fixer.replaceText(rightParen, "}");
164         }
165
166         // Process arguments.
167         for (const argNode of node.arguments) {
168             const innerParens = getParenTokens(argNode, leftParen, sourceCode);
169             const left = innerParens.shift();
170             const right = innerParens.pop();
171
172             if (argNode.type === "ObjectExpression") {
173                 const maybeTrailingComma = sourceCode.getLastToken(argNode, 1);
174                 const maybeArgumentComma = sourceCode.getTokenAfter(right);
175
176                 /*
177                  * Make bare this object literal.
178                  * And remove spaces inside of the braces for better formatting.
179                  */
180                 for (const innerParen of innerParens) {
181                     yield fixer.remove(innerParen);
182                 }
183                 const leftRange = [left.range[0], getEndWithSpaces(left, sourceCode)];
184                 const rightRange = [
185                     Math.max(getStartWithSpaces(right, sourceCode), leftRange[1]), // Ensure ranges don't overlap
186                     right.range[1]
187                 ];
188
189                 yield fixer.removeRange(leftRange);
190                 yield fixer.removeRange(rightRange);
191
192                 // Remove the comma of this argument if it's duplication.
193                 if (
194                     (argNode.properties.length === 0 || isCommaToken(maybeTrailingComma)) &&
195                     isCommaToken(maybeArgumentComma)
196                 ) {
197                     yield fixer.remove(maybeArgumentComma);
198                 }
199             } else {
200
201                 // Make spread.
202                 if (argNeedsParens(argNode, sourceCode)) {
203                     yield fixer.insertTextBefore(left, "...(");
204                     yield fixer.insertTextAfter(right, ")");
205                 } else {
206                     yield fixer.insertTextBefore(left, "...");
207                 }
208             }
209         }
210     };
211 }
212
213 module.exports = {
214     meta: {
215         type: "suggestion",
216
217         docs: {
218             description:
219                 "disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead.",
220             category: "Stylistic Issues",
221             recommended: false,
222             url: "https://eslint.org/docs/rules/prefer-object-spread"
223         },
224
225         schema: [],
226         fixable: "code",
227
228         messages: {
229             useSpreadMessage: "Use an object spread instead of `Object.assign` eg: `{ ...foo }`.",
230             useLiteralMessage: "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`."
231         }
232     },
233
234     create(context) {
235         const sourceCode = context.getSourceCode();
236
237         return {
238             Program() {
239                 const scope = context.getScope();
240                 const tracker = new ReferenceTracker(scope);
241                 const trackMap = {
242                     Object: {
243                         assign: { [CALL]: true }
244                     }
245                 };
246
247                 // Iterate all calls of `Object.assign` (only of the global variable `Object`).
248                 for (const { node } of tracker.iterateGlobalReferences(trackMap)) {
249                     if (
250                         node.arguments.length >= 1 &&
251                         node.arguments[0].type === "ObjectExpression" &&
252                         !hasArraySpread(node)
253                     ) {
254                         const messageId = node.arguments.length === 1
255                             ? "useLiteralMessage"
256                             : "useSpreadMessage";
257                         const fix = defineFixer(node, sourceCode);
258
259                         context.report({ node, messageId, fix });
260                     }
261                 }
262             }
263         };
264     }
265 };