]>
git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/prefer-object-spread.js
2 * @fileoverview Prefers object spread property over Object.assign
3 * @author Sharmila Jesupaul
4 * See LICENSE file in root directory for full license.
9 const { CALL
, ReferenceTracker
} = require("eslint-utils");
15 } = require("./utils/ast-utils");
17 const ANY_SPACE
= /\s/u;
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
24 function hasArraySpread(node
) {
25 return node
.arguments
.some(arg
=> arg
.type
=== "SpreadElement");
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.
33 function isAccessorProperty(node
) {
34 return node
.type
=== "Property" &&
35 (node
.kind
=== "get" || node
.kind
=== "set");
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.
43 function hasAccessors(node
) {
44 return node
.properties
.some(isAccessorProperty
);
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.
52 function hasArgumentsWithAccessors(node
) {
54 .filter(arg
=> arg
.type
=== "ObjectExpression")
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
65 function needsParens(node
, sourceCode
) {
66 const parent
= node
.parent
;
68 switch (parent
.type
) {
69 case "VariableDeclarator":
70 case "ArrayExpression":
71 case "ReturnStatement":
72 case "CallExpression":
75 case "AssignmentExpression":
76 return parent
.left
=== node
&& !isParenthesised(sourceCode
, node
);
78 return !isParenthesised(sourceCode
, node
);
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
88 function argNeedsParens(node
, sourceCode
) {
90 case "AssignmentExpression":
91 case "ArrowFunctionExpression":
92 case "ConditionalExpression":
93 return !isParenthesised(sourceCode
, node
);
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.
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
);
112 // Note: don't include the parens of the argument list.
116 leftNext
.range
[0] > leftArgumentListParen
.range
[0] &&
117 isOpeningParenToken(leftNext
) &&
118 isClosingParenToken(rightNext
)
120 parens
.push(leftNext
, rightNext
);
121 leftNext
= sourceCode
.getTokenBefore(leftNext
);
122 rightNext
= sourceCode
.getTokenAfter(rightNext
);
125 return parens
.sort((a
, b
) => a
.range
[0] - b
.range
[0]);
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.
134 function getStartWithSpaces(token
, sourceCode
) {
135 const text
= sourceCode
.text
;
136 let start
= token
.range
[0];
138 // If the previous token is a line comment then skip this step to avoid commenting this token out.
140 const prevToken
= sourceCode
.getTokenBefore(token
, { includeComments
: true });
142 if (prevToken
&& prevToken
.type
=== "Line") {
147 // Detect spaces before the token.
148 while (ANY_SPACE
.test(text
[start
- 1] || "")) {
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.
161 function getEndWithSpaces(token
, sourceCode
) {
162 const text
= sourceCode
.text
;
163 let end
= token
.range
[1];
165 // Detect spaces after the token.
166 while (ANY_SPACE
.test(text
[end
] || "")) {
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.
179 function defineFixer(node
, sourceCode
) {
180 return function *(fixer
) {
181 const leftParen
= sourceCode
.getTokenAfter(node
.callee
, isOpeningParenToken
);
182 const rightParen
= sourceCode
.getLastToken(node
);
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]]);
187 // Replace the parens of argument list to braces.
188 if (needsParens(node
, sourceCode
)) {
189 yield fixer
.replaceText(leftParen
, "({");
190 yield fixer
.replaceText(rightParen
, "})");
192 yield fixer
.replaceText(leftParen
, "{");
193 yield fixer
.replaceText(rightParen
, "}");
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();
202 if (argNode
.type
=== "ObjectExpression") {
203 const maybeTrailingComma
= sourceCode
.getLastToken(argNode
, 1);
204 const maybeArgumentComma
= sourceCode
.getTokenAfter(right
);
207 * Make bare this object literal.
208 * And remove spaces inside of the braces for better formatting.
210 for (const innerParen
of innerParens
) {
211 yield fixer
.remove(innerParen
);
213 const leftRange
= [left
.range
[0], getEndWithSpaces(left
, sourceCode
)];
215 Math
.max(getStartWithSpaces(right
, sourceCode
), leftRange
[1]), // Ensure ranges don't overlap
219 yield fixer
.removeRange(leftRange
);
220 yield fixer
.removeRange(rightRange
);
222 // Remove the comma of this argument if it's duplication.
224 (argNode
.properties
.length
=== 0 || isCommaToken(maybeTrailingComma
)) &&
225 isCommaToken(maybeArgumentComma
)
227 yield fixer
.remove(maybeArgumentComma
);
232 if (argNeedsParens(argNode
, sourceCode
)) {
233 yield fixer
.insertTextBefore(left
, "...(");
234 yield fixer
.insertTextAfter(right
, ")");
236 yield fixer
.insertTextBefore(left
, "...");
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",
252 url
: "https://eslint.org/docs/rules/prefer-object-spread"
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 }`."
265 const sourceCode
= context
.getSourceCode();
269 const scope
= context
.getScope();
270 const tracker
= new ReferenceTracker(scope
);
273 assign
: { [CALL
]: true }
277 // Iterate all calls of `Object.assign` (only of the global variable `Object`).
278 for (const { node
} of tracker
.iterateGlobalReferences(trackMap
)) {
280 node
.arguments
.length
>= 1 &&
281 node
.arguments
[0].type
=== "ObjectExpression" &&
282 !hasArraySpread(node
) &&
284 node
.arguments
.length
> 1 &&
285 hasArgumentsWithAccessors(node
)
288 const messageId
= node
.arguments
.length
=== 1
289 ? "useLiteralMessage"
290 : "useSpreadMessage";
291 const fix
= defineFixer(node
, sourceCode
);
293 context
.report({ node
, messageId
, fix
});