1 | /**
|
---|
2 | * @fileoverview Rule to forbid or enforce dangling commas.
|
---|
3 | * @author Ian Christian Myers
|
---|
4 | * @deprecated in ESLint v8.53.0
|
---|
5 | */
|
---|
6 |
|
---|
7 | "use strict";
|
---|
8 |
|
---|
9 | //------------------------------------------------------------------------------
|
---|
10 | // Requirements
|
---|
11 | //------------------------------------------------------------------------------
|
---|
12 |
|
---|
13 | const astUtils = require("./utils/ast-utils");
|
---|
14 |
|
---|
15 | //------------------------------------------------------------------------------
|
---|
16 | // Helpers
|
---|
17 | //------------------------------------------------------------------------------
|
---|
18 |
|
---|
19 | const DEFAULT_OPTIONS = Object.freeze({
|
---|
20 | arrays: "never",
|
---|
21 | objects: "never",
|
---|
22 | imports: "never",
|
---|
23 | exports: "never",
|
---|
24 | functions: "never"
|
---|
25 | });
|
---|
26 |
|
---|
27 | /**
|
---|
28 | * Checks whether or not a trailing comma is allowed in a given node.
|
---|
29 | * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
|
---|
30 | * @param {ASTNode} lastItem The node of the last element in the given node.
|
---|
31 | * @returns {boolean} `true` if a trailing comma is allowed.
|
---|
32 | */
|
---|
33 | function isTrailingCommaAllowed(lastItem) {
|
---|
34 | return !(
|
---|
35 | lastItem.type === "RestElement" ||
|
---|
36 | lastItem.type === "RestProperty" ||
|
---|
37 | lastItem.type === "ExperimentalRestProperty"
|
---|
38 | );
|
---|
39 | }
|
---|
40 |
|
---|
41 | /**
|
---|
42 | * Normalize option value.
|
---|
43 | * @param {string|Object|undefined} optionValue The 1st option value to normalize.
|
---|
44 | * @param {number} ecmaVersion The normalized ECMAScript version.
|
---|
45 | * @returns {Object} The normalized option value.
|
---|
46 | */
|
---|
47 | function normalizeOptions(optionValue, ecmaVersion) {
|
---|
48 | if (typeof optionValue === "string") {
|
---|
49 | return {
|
---|
50 | arrays: optionValue,
|
---|
51 | objects: optionValue,
|
---|
52 | imports: optionValue,
|
---|
53 | exports: optionValue,
|
---|
54 | functions: ecmaVersion < 2017 ? "ignore" : optionValue
|
---|
55 | };
|
---|
56 | }
|
---|
57 | if (typeof optionValue === "object" && optionValue !== null) {
|
---|
58 | return {
|
---|
59 | arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
|
---|
60 | objects: optionValue.objects || DEFAULT_OPTIONS.objects,
|
---|
61 | imports: optionValue.imports || DEFAULT_OPTIONS.imports,
|
---|
62 | exports: optionValue.exports || DEFAULT_OPTIONS.exports,
|
---|
63 | functions: optionValue.functions || DEFAULT_OPTIONS.functions
|
---|
64 | };
|
---|
65 | }
|
---|
66 |
|
---|
67 | return DEFAULT_OPTIONS;
|
---|
68 | }
|
---|
69 |
|
---|
70 | //------------------------------------------------------------------------------
|
---|
71 | // Rule Definition
|
---|
72 | //------------------------------------------------------------------------------
|
---|
73 |
|
---|
74 | /** @type {import('../shared/types').Rule} */
|
---|
75 | module.exports = {
|
---|
76 | meta: {
|
---|
77 | deprecated: true,
|
---|
78 | replacedBy: [],
|
---|
79 | type: "layout",
|
---|
80 |
|
---|
81 | docs: {
|
---|
82 | description: "Require or disallow trailing commas",
|
---|
83 | recommended: false,
|
---|
84 | url: "https://eslint.org/docs/latest/rules/comma-dangle"
|
---|
85 | },
|
---|
86 |
|
---|
87 | fixable: "code",
|
---|
88 |
|
---|
89 | schema: {
|
---|
90 | definitions: {
|
---|
91 | value: {
|
---|
92 | enum: [
|
---|
93 | "always-multiline",
|
---|
94 | "always",
|
---|
95 | "never",
|
---|
96 | "only-multiline"
|
---|
97 | ]
|
---|
98 | },
|
---|
99 | valueWithIgnore: {
|
---|
100 | enum: [
|
---|
101 | "always-multiline",
|
---|
102 | "always",
|
---|
103 | "ignore",
|
---|
104 | "never",
|
---|
105 | "only-multiline"
|
---|
106 | ]
|
---|
107 | }
|
---|
108 | },
|
---|
109 | type: "array",
|
---|
110 | items: [
|
---|
111 | {
|
---|
112 | oneOf: [
|
---|
113 | {
|
---|
114 | $ref: "#/definitions/value"
|
---|
115 | },
|
---|
116 | {
|
---|
117 | type: "object",
|
---|
118 | properties: {
|
---|
119 | arrays: { $ref: "#/definitions/valueWithIgnore" },
|
---|
120 | objects: { $ref: "#/definitions/valueWithIgnore" },
|
---|
121 | imports: { $ref: "#/definitions/valueWithIgnore" },
|
---|
122 | exports: { $ref: "#/definitions/valueWithIgnore" },
|
---|
123 | functions: { $ref: "#/definitions/valueWithIgnore" }
|
---|
124 | },
|
---|
125 | additionalProperties: false
|
---|
126 | }
|
---|
127 | ]
|
---|
128 | }
|
---|
129 | ],
|
---|
130 | additionalItems: false
|
---|
131 | },
|
---|
132 |
|
---|
133 | messages: {
|
---|
134 | unexpected: "Unexpected trailing comma.",
|
---|
135 | missing: "Missing trailing comma."
|
---|
136 | }
|
---|
137 | },
|
---|
138 |
|
---|
139 | create(context) {
|
---|
140 | const options = normalizeOptions(context.options[0], context.languageOptions.ecmaVersion);
|
---|
141 |
|
---|
142 | const sourceCode = context.sourceCode;
|
---|
143 |
|
---|
144 | /**
|
---|
145 | * Gets the last item of the given node.
|
---|
146 | * @param {ASTNode} node The node to get.
|
---|
147 | * @returns {ASTNode|null} The last node or null.
|
---|
148 | */
|
---|
149 | function getLastItem(node) {
|
---|
150 |
|
---|
151 | /**
|
---|
152 | * Returns the last element of an array
|
---|
153 | * @param {any[]} array The input array
|
---|
154 | * @returns {any} The last element
|
---|
155 | */
|
---|
156 | function last(array) {
|
---|
157 | return array[array.length - 1];
|
---|
158 | }
|
---|
159 |
|
---|
160 | switch (node.type) {
|
---|
161 | case "ObjectExpression":
|
---|
162 | case "ObjectPattern":
|
---|
163 | return last(node.properties);
|
---|
164 | case "ArrayExpression":
|
---|
165 | case "ArrayPattern":
|
---|
166 | return last(node.elements);
|
---|
167 | case "ImportDeclaration":
|
---|
168 | case "ExportNamedDeclaration":
|
---|
169 | return last(node.specifiers);
|
---|
170 | case "FunctionDeclaration":
|
---|
171 | case "FunctionExpression":
|
---|
172 | case "ArrowFunctionExpression":
|
---|
173 | return last(node.params);
|
---|
174 | case "CallExpression":
|
---|
175 | case "NewExpression":
|
---|
176 | return last(node.arguments);
|
---|
177 | default:
|
---|
178 | return null;
|
---|
179 | }
|
---|
180 | }
|
---|
181 |
|
---|
182 | /**
|
---|
183 | * Gets the trailing comma token of the given node.
|
---|
184 | * If the trailing comma does not exist, this returns the token which is
|
---|
185 | * the insertion point of the trailing comma token.
|
---|
186 | * @param {ASTNode} node The node to get.
|
---|
187 | * @param {ASTNode} lastItem The last item of the node.
|
---|
188 | * @returns {Token} The trailing comma token or the insertion point.
|
---|
189 | */
|
---|
190 | function getTrailingToken(node, lastItem) {
|
---|
191 | switch (node.type) {
|
---|
192 | case "ObjectExpression":
|
---|
193 | case "ArrayExpression":
|
---|
194 | case "CallExpression":
|
---|
195 | case "NewExpression":
|
---|
196 | return sourceCode.getLastToken(node, 1);
|
---|
197 | default: {
|
---|
198 | const nextToken = sourceCode.getTokenAfter(lastItem);
|
---|
199 |
|
---|
200 | if (astUtils.isCommaToken(nextToken)) {
|
---|
201 | return nextToken;
|
---|
202 | }
|
---|
203 | return sourceCode.getLastToken(lastItem);
|
---|
204 | }
|
---|
205 | }
|
---|
206 | }
|
---|
207 |
|
---|
208 | /**
|
---|
209 | * Checks whether or not a given node is multiline.
|
---|
210 | * This rule handles a given node as multiline when the closing parenthesis
|
---|
211 | * and the last element are not on the same line.
|
---|
212 | * @param {ASTNode} node A node to check.
|
---|
213 | * @returns {boolean} `true` if the node is multiline.
|
---|
214 | */
|
---|
215 | function isMultiline(node) {
|
---|
216 | const lastItem = getLastItem(node);
|
---|
217 |
|
---|
218 | if (!lastItem) {
|
---|
219 | return false;
|
---|
220 | }
|
---|
221 |
|
---|
222 | const penultimateToken = getTrailingToken(node, lastItem);
|
---|
223 | const lastToken = sourceCode.getTokenAfter(penultimateToken);
|
---|
224 |
|
---|
225 | return lastToken.loc.end.line !== penultimateToken.loc.end.line;
|
---|
226 | }
|
---|
227 |
|
---|
228 | /**
|
---|
229 | * Reports a trailing comma if it exists.
|
---|
230 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
231 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
232 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
233 | * @returns {void}
|
---|
234 | */
|
---|
235 | function forbidTrailingComma(node) {
|
---|
236 | const lastItem = getLastItem(node);
|
---|
237 |
|
---|
238 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
---|
239 | return;
|
---|
240 | }
|
---|
241 |
|
---|
242 | const trailingToken = getTrailingToken(node, lastItem);
|
---|
243 |
|
---|
244 | if (astUtils.isCommaToken(trailingToken)) {
|
---|
245 | context.report({
|
---|
246 | node: lastItem,
|
---|
247 | loc: trailingToken.loc,
|
---|
248 | messageId: "unexpected",
|
---|
249 | *fix(fixer) {
|
---|
250 | yield fixer.remove(trailingToken);
|
---|
251 |
|
---|
252 | /*
|
---|
253 | * Extend the range of the fix to include surrounding tokens to ensure
|
---|
254 | * that the element after which the comma is removed stays _last_.
|
---|
255 | * This intentionally makes conflicts in fix ranges with rules that may be
|
---|
256 | * adding or removing elements in the same autofix pass.
|
---|
257 | * https://github.com/eslint/eslint/issues/15660
|
---|
258 | */
|
---|
259 | yield fixer.insertTextBefore(sourceCode.getTokenBefore(trailingToken), "");
|
---|
260 | yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
|
---|
261 | }
|
---|
262 | });
|
---|
263 | }
|
---|
264 | }
|
---|
265 |
|
---|
266 | /**
|
---|
267 | * Reports the last element of a given node if it does not have a trailing
|
---|
268 | * comma.
|
---|
269 | *
|
---|
270 | * If a given node is `ArrayPattern` which has `RestElement`, the trailing
|
---|
271 | * comma is disallowed, so report if it exists.
|
---|
272 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
273 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
274 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
275 | * @returns {void}
|
---|
276 | */
|
---|
277 | function forceTrailingComma(node) {
|
---|
278 | const lastItem = getLastItem(node);
|
---|
279 |
|
---|
280 | if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
|
---|
281 | return;
|
---|
282 | }
|
---|
283 | if (!isTrailingCommaAllowed(lastItem)) {
|
---|
284 | forbidTrailingComma(node);
|
---|
285 | return;
|
---|
286 | }
|
---|
287 |
|
---|
288 | const trailingToken = getTrailingToken(node, lastItem);
|
---|
289 |
|
---|
290 | if (trailingToken.value !== ",") {
|
---|
291 | context.report({
|
---|
292 | node: lastItem,
|
---|
293 | loc: {
|
---|
294 | start: trailingToken.loc.end,
|
---|
295 | end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
|
---|
296 | },
|
---|
297 | messageId: "missing",
|
---|
298 | *fix(fixer) {
|
---|
299 | yield fixer.insertTextAfter(trailingToken, ",");
|
---|
300 |
|
---|
301 | /*
|
---|
302 | * Extend the range of the fix to include surrounding tokens to ensure
|
---|
303 | * that the element after which the comma is inserted stays _last_.
|
---|
304 | * This intentionally makes conflicts in fix ranges with rules that may be
|
---|
305 | * adding or removing elements in the same autofix pass.
|
---|
306 | * https://github.com/eslint/eslint/issues/15660
|
---|
307 | */
|
---|
308 | yield fixer.insertTextBefore(trailingToken, "");
|
---|
309 | yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
|
---|
310 | }
|
---|
311 | });
|
---|
312 | }
|
---|
313 | }
|
---|
314 |
|
---|
315 | /**
|
---|
316 | * If a given node is multiline, reports the last element of a given node
|
---|
317 | * when it does not have a trailing comma.
|
---|
318 | * Otherwise, reports a trailing comma if it exists.
|
---|
319 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
320 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
321 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
322 | * @returns {void}
|
---|
323 | */
|
---|
324 | function forceTrailingCommaIfMultiline(node) {
|
---|
325 | if (isMultiline(node)) {
|
---|
326 | forceTrailingComma(node);
|
---|
327 | } else {
|
---|
328 | forbidTrailingComma(node);
|
---|
329 | }
|
---|
330 | }
|
---|
331 |
|
---|
332 | /**
|
---|
333 | * Only if a given node is not multiline, reports the last element of a given node
|
---|
334 | * when it does not have a trailing comma.
|
---|
335 | * Otherwise, reports a trailing comma if it exists.
|
---|
336 | * @param {ASTNode} node A node to check. Its type is one of
|
---|
337 | * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
|
---|
338 | * ImportDeclaration, and ExportNamedDeclaration.
|
---|
339 | * @returns {void}
|
---|
340 | */
|
---|
341 | function allowTrailingCommaIfMultiline(node) {
|
---|
342 | if (!isMultiline(node)) {
|
---|
343 | forbidTrailingComma(node);
|
---|
344 | }
|
---|
345 | }
|
---|
346 |
|
---|
347 | const predicate = {
|
---|
348 | always: forceTrailingComma,
|
---|
349 | "always-multiline": forceTrailingCommaIfMultiline,
|
---|
350 | "only-multiline": allowTrailingCommaIfMultiline,
|
---|
351 | never: forbidTrailingComma,
|
---|
352 | ignore() {}
|
---|
353 | };
|
---|
354 |
|
---|
355 | return {
|
---|
356 | ObjectExpression: predicate[options.objects],
|
---|
357 | ObjectPattern: predicate[options.objects],
|
---|
358 |
|
---|
359 | ArrayExpression: predicate[options.arrays],
|
---|
360 | ArrayPattern: predicate[options.arrays],
|
---|
361 |
|
---|
362 | ImportDeclaration: predicate[options.imports],
|
---|
363 |
|
---|
364 | ExportNamedDeclaration: predicate[options.exports],
|
---|
365 |
|
---|
366 | FunctionDeclaration: predicate[options.functions],
|
---|
367 | FunctionExpression: predicate[options.functions],
|
---|
368 | ArrowFunctionExpression: predicate[options.functions],
|
---|
369 | CallExpression: predicate[options.functions],
|
---|
370 | NewExpression: predicate[options.functions]
|
---|
371 | };
|
---|
372 | }
|
---|
373 | };
|
---|