UNPKG

9.27 kBJavaScriptView Raw
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
9const { CALL, ReferenceTracker } = require("eslint-utils");
10const {
11 isCommaToken,
12 isOpeningParenToken,
13 isClosingParenToken,
14 isParenthesised
15} = require("./utils/ast-utils");
16
17const 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 */
24function 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 */
35function 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 */
58function 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 */
77function 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 */
104function 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 */
131function 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 */
149function 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
213module.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};