source: imaps-frontend/node_modules/eslint/lib/rules/prefer-object-spread.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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