]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/comma-dangle.js
import eslint 7.28.0
[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 module.exports = {
74 meta: {
75 type: "layout",
76
77 docs: {
78 description: "require or disallow trailing commas",
79 category: "Stylistic Issues",
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 },
128
129 messages: {
130 unexpected: "Unexpected trailing comma.",
131 missing: "Missing trailing comma."
132 }
133 },
134
135 create(context) {
136 const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
137
138 const sourceCode = context.getSourceCode();
139
140 /**
141 * Gets the last item of the given node.
142 * @param {ASTNode} node The node to get.
143 * @returns {ASTNode|null} The last node or null.
144 */
145 function getLastItem(node) {
146
147 /**
148 * Returns the last element of an array
149 * @param {any[]} array The input array
150 * @returns {any} The last element
151 */
152 function last(array) {
153 return array[array.length - 1];
154 }
155
156 switch (node.type) {
157 case "ObjectExpression":
158 case "ObjectPattern":
159 return last(node.properties);
160 case "ArrayExpression":
161 case "ArrayPattern":
162 return last(node.elements);
163 case "ImportDeclaration":
164 case "ExportNamedDeclaration":
165 return last(node.specifiers);
166 case "FunctionDeclaration":
167 case "FunctionExpression":
168 case "ArrowFunctionExpression":
169 return last(node.params);
170 case "CallExpression":
171 case "NewExpression":
172 return last(node.arguments);
173 default:
174 return null;
175 }
176 }
177
178 /**
179 * Gets the trailing comma token of the given node.
180 * If the trailing comma does not exist, this returns the token which is
181 * the insertion point of the trailing comma token.
182 * @param {ASTNode} node The node to get.
183 * @param {ASTNode} lastItem The last item of the node.
184 * @returns {Token} The trailing comma token or the insertion point.
185 */
186 function getTrailingToken(node, lastItem) {
187 switch (node.type) {
188 case "ObjectExpression":
189 case "ArrayExpression":
190 case "CallExpression":
191 case "NewExpression":
192 return sourceCode.getLastToken(node, 1);
193 default: {
194 const nextToken = sourceCode.getTokenAfter(lastItem);
195
196 if (astUtils.isCommaToken(nextToken)) {
197 return nextToken;
198 }
199 return sourceCode.getLastToken(lastItem);
200 }
201 }
202 }
203
204 /**
205 * Checks whether or not a given node is multiline.
206 * This rule handles a given node as multiline when the closing parenthesis
207 * and the last element are not on the same line.
208 * @param {ASTNode} node A node to check.
209 * @returns {boolean} `true` if the node is multiline.
210 */
211 function isMultiline(node) {
212 const lastItem = getLastItem(node);
213
214 if (!lastItem) {
215 return false;
216 }
217
218 const penultimateToken = getTrailingToken(node, lastItem);
219 const lastToken = sourceCode.getTokenAfter(penultimateToken);
220
221 return lastToken.loc.end.line !== penultimateToken.loc.end.line;
222 }
223
224 /**
225 * Reports a trailing comma if it exists.
226 * @param {ASTNode} node A node to check. Its type is one of
227 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
228 * ImportDeclaration, and ExportNamedDeclaration.
229 * @returns {void}
230 */
231 function forbidTrailingComma(node) {
232 const lastItem = getLastItem(node);
233
234 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
235 return;
236 }
237
238 const trailingToken = getTrailingToken(node, lastItem);
239
240 if (astUtils.isCommaToken(trailingToken)) {
241 context.report({
242 node: lastItem,
243 loc: trailingToken.loc,
244 messageId: "unexpected",
245 fix(fixer) {
246 return fixer.remove(trailingToken);
247 }
248 });
249 }
250 }
251
252 /**
253 * Reports the last element of a given node if it does not have a trailing
254 * comma.
255 *
256 * If a given node is `ArrayPattern` which has `RestElement`, the trailing
257 * comma is disallowed, so report if it exists.
258 * @param {ASTNode} node A node to check. Its type is one of
259 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
260 * ImportDeclaration, and ExportNamedDeclaration.
261 * @returns {void}
262 */
263 function forceTrailingComma(node) {
264 const lastItem = getLastItem(node);
265
266 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
267 return;
268 }
269 if (!isTrailingCommaAllowed(lastItem)) {
270 forbidTrailingComma(node);
271 return;
272 }
273
274 const trailingToken = getTrailingToken(node, lastItem);
275
276 if (trailingToken.value !== ",") {
277 context.report({
278 node: lastItem,
279 loc: {
280 start: trailingToken.loc.end,
281 end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
282 },
283 messageId: "missing",
284 fix(fixer) {
285 return fixer.insertTextAfter(trailingToken, ",");
286 }
287 });
288 }
289 }
290
291 /**
292 * If a given node is multiline, reports the last element of a given node
293 * when it does not have a trailing comma.
294 * Otherwise, reports a trailing comma if it exists.
295 * @param {ASTNode} node A node to check. Its type is one of
296 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
297 * ImportDeclaration, and ExportNamedDeclaration.
298 * @returns {void}
299 */
300 function forceTrailingCommaIfMultiline(node) {
301 if (isMultiline(node)) {
302 forceTrailingComma(node);
303 } else {
304 forbidTrailingComma(node);
305 }
306 }
307
308 /**
309 * Only if a given node is not multiline, reports the last element of a given node
310 * when it does not have a trailing comma.
311 * Otherwise, reports a trailing comma if it exists.
312 * @param {ASTNode} node A node to check. Its type is one of
313 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
314 * ImportDeclaration, and ExportNamedDeclaration.
315 * @returns {void}
316 */
317 function allowTrailingCommaIfMultiline(node) {
318 if (!isMultiline(node)) {
319 forbidTrailingComma(node);
320 }
321 }
322
323 const predicate = {
324 always: forceTrailingComma,
325 "always-multiline": forceTrailingCommaIfMultiline,
326 "only-multiline": allowTrailingCommaIfMultiline,
327 never: forbidTrailingComma,
328 ignore: () => {}
329 };
330
331 return {
332 ObjectExpression: predicate[options.objects],
333 ObjectPattern: predicate[options.objects],
334
335 ArrayExpression: predicate[options.arrays],
336 ArrayPattern: predicate[options.arrays],
337
338 ImportDeclaration: predicate[options.imports],
339
340 ExportNamedDeclaration: predicate[options.exports],
341
342 FunctionDeclaration: predicate[options.functions],
343 FunctionExpression: predicate[options.functions],
344 ArrowFunctionExpression: predicate[options.functions],
345 CallExpression: predicate[options.functions],
346 NewExpression: predicate[options.functions]
347 };
348 }
349 };