1 |
|
2 |
|
3 |
|
4 |
|
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 |
|
21 |
|
22 |
|
23 |
|
24 | function hasArraySpread(node) {
|
25 | return node.arguments.some(arg => arg.type === "SpreadElement");
|
26 | }
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
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 |
|
54 |
|
55 |
|
56 |
|
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 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
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 |
|
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 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | function getStartWithSpaces(token, sourceCode) {
|
105 | const text = sourceCode.text;
|
106 | let start = token.range[0];
|
107 |
|
108 |
|
109 | {
|
110 | const prevToken = sourceCode.getTokenBefore(token, { includeComments: true });
|
111 |
|
112 | if (prevToken && prevToken.type === "Line") {
|
113 | return start;
|
114 | }
|
115 | }
|
116 |
|
117 |
|
118 | while (ANY_SPACE.test(text[start - 1] || "")) {
|
119 | start -= 1;
|
120 | }
|
121 |
|
122 | return start;
|
123 | }
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 | function getEndWithSpaces(token, sourceCode) {
|
132 | const text = sourceCode.text;
|
133 | let end = token.range[1];
|
134 |
|
135 |
|
136 | while (ANY_SPACE.test(text[end] || "")) {
|
137 | end += 1;
|
138 | }
|
139 |
|
140 | return end;
|
141 | }
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
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 |
|
155 | yield fixer.remove(node.callee);
|
156 |
|
157 |
|
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 |
|
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 |
|
178 |
|
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]),
|
186 | right.range[1]
|
187 | ];
|
188 |
|
189 | yield fixer.removeRange(leftRange);
|
190 | yield fixer.removeRange(rightRange);
|
191 |
|
192 |
|
193 | if (
|
194 | (argNode.properties.length === 0 || isCommaToken(maybeTrailingComma)) &&
|
195 | isCommaToken(maybeArgumentComma)
|
196 | ) {
|
197 | yield fixer.remove(maybeArgumentComma);
|
198 | }
|
199 | } else {
|
200 |
|
201 |
|
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 |
|
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 | };
|