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