UNPKG

12 kBJavaScriptView Raw
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
12const lodash = require("lodash");
13const astUtils = require("../ast-utils");
14
15//------------------------------------------------------------------------------
16// Helpers
17//------------------------------------------------------------------------------
18
19const DEFAULT_OPTIONS = Object.freeze({
20 arrays: "never",
21 objects: "never",
22 imports: "never",
23 exports: "never",
24 functions: "ignore"
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 *
31 * @param {ASTNode} lastItem - The node of the last element in the given node.
32 * @returns {boolean} `true` if a trailing comma is allowed.
33 */
34function isTrailingCommaAllowed(lastItem) {
35 return !(
36 lastItem.type === "RestElement" ||
37 lastItem.type === "RestProperty" ||
38 lastItem.type === "ExperimentalRestProperty"
39 );
40}
41
42/**
43 * Normalize option value.
44 *
45 * @param {string|Object|undefined} optionValue - The 1st option value to normalize.
46 * @returns {Object} The normalized option value.
47 */
48function normalizeOptions(optionValue) {
49 if (typeof optionValue === "string") {
50 return {
51 arrays: optionValue,
52 objects: optionValue,
53 imports: optionValue,
54 exports: optionValue,
55
56 // For backward compatibility, always ignore functions.
57 functions: "ignore"
58 };
59 }
60 if (typeof optionValue === "object" && optionValue !== null) {
61 return {
62 arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
63 objects: optionValue.objects || DEFAULT_OPTIONS.objects,
64 imports: optionValue.imports || DEFAULT_OPTIONS.imports,
65 exports: optionValue.exports || DEFAULT_OPTIONS.exports,
66 functions: optionValue.functions || DEFAULT_OPTIONS.functions
67 };
68 }
69
70 return DEFAULT_OPTIONS;
71}
72
73//------------------------------------------------------------------------------
74// Rule Definition
75//------------------------------------------------------------------------------
76
77module.exports = {
78 meta: {
79 docs: {
80 description: "require or disallow trailing commas",
81 category: "Stylistic Issues",
82 recommended: false,
83 url: "https://eslint.org/docs/rules/comma-dangle"
84 },
85 fixable: "code",
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]);
137 const sourceCode = context.getSourceCode();
138
139 /**
140 * Gets the last item of the given node.
141 * @param {ASTNode} node - The node to get.
142 * @returns {ASTNode|null} The last node or null.
143 */
144 function getLastItem(node) {
145 switch (node.type) {
146 case "ObjectExpression":
147 case "ObjectPattern":
148 return lodash.last(node.properties);
149 case "ArrayExpression":
150 case "ArrayPattern":
151 return lodash.last(node.elements);
152 case "ImportDeclaration":
153 case "ExportNamedDeclaration":
154 return lodash.last(node.specifiers);
155 case "FunctionDeclaration":
156 case "FunctionExpression":
157 case "ArrowFunctionExpression":
158 return lodash.last(node.params);
159 case "CallExpression":
160 case "NewExpression":
161 return lodash.last(node.arguments);
162 default:
163 return null;
164 }
165 }
166
167 /**
168 * Gets the trailing comma token of the given node.
169 * If the trailing comma does not exist, this returns the token which is
170 * the insertion point of the trailing comma token.
171 *
172 * @param {ASTNode} node - The node to get.
173 * @param {ASTNode} lastItem - The last item of the node.
174 * @returns {Token} The trailing comma token or the insertion point.
175 */
176 function getTrailingToken(node, lastItem) {
177 switch (node.type) {
178 case "ObjectExpression":
179 case "ArrayExpression":
180 case "CallExpression":
181 case "NewExpression":
182 return sourceCode.getLastToken(node, 1);
183 default: {
184 const nextToken = sourceCode.getTokenAfter(lastItem);
185
186 if (astUtils.isCommaToken(nextToken)) {
187 return nextToken;
188 }
189 return sourceCode.getLastToken(lastItem);
190 }
191 }
192 }
193
194 /**
195 * Checks whether or not a given node is multiline.
196 * This rule handles a given node as multiline when the closing parenthesis
197 * and the last element are not on the same line.
198 *
199 * @param {ASTNode} node - A node to check.
200 * @returns {boolean} `true` if the node is multiline.
201 */
202 function isMultiline(node) {
203 const lastItem = getLastItem(node);
204
205 if (!lastItem) {
206 return false;
207 }
208
209 const penultimateToken = getTrailingToken(node, lastItem);
210 const lastToken = sourceCode.getTokenAfter(penultimateToken);
211
212 return lastToken.loc.end.line !== penultimateToken.loc.end.line;
213 }
214
215 /**
216 * Reports a trailing comma if it exists.
217 *
218 * @param {ASTNode} node - A node to check. Its type is one of
219 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
220 * ImportDeclaration, and ExportNamedDeclaration.
221 * @returns {void}
222 */
223 function forbidTrailingComma(node) {
224 const lastItem = getLastItem(node);
225
226 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
227 return;
228 }
229
230 const trailingToken = getTrailingToken(node, lastItem);
231
232 if (astUtils.isCommaToken(trailingToken)) {
233 context.report({
234 node: lastItem,
235 loc: trailingToken.loc.start,
236 messageId: "unexpected",
237 fix(fixer) {
238 return fixer.remove(trailingToken);
239 }
240 });
241 }
242 }
243
244 /**
245 * Reports the last element of a given node if it does not have a trailing
246 * comma.
247 *
248 * If a given node is `ArrayPattern` which has `RestElement`, the trailing
249 * comma is disallowed, so report if it exists.
250 *
251 * @param {ASTNode} node - A node to check. Its type is one of
252 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
253 * ImportDeclaration, and ExportNamedDeclaration.
254 * @returns {void}
255 */
256 function forceTrailingComma(node) {
257 const lastItem = getLastItem(node);
258
259 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
260 return;
261 }
262 if (!isTrailingCommaAllowed(lastItem)) {
263 forbidTrailingComma(node);
264 return;
265 }
266
267 const trailingToken = getTrailingToken(node, lastItem);
268
269 if (trailingToken.value !== ",") {
270 context.report({
271 node: lastItem,
272 loc: trailingToken.loc.end,
273 messageId: "missing",
274 fix(fixer) {
275 return fixer.insertTextAfter(trailingToken, ",");
276 }
277 });
278 }
279 }
280
281 /**
282 * If a given node is multiline, reports the last element of a given node
283 * when it does not have a trailing comma.
284 * Otherwise, reports a trailing comma if it exists.
285 *
286 * @param {ASTNode} node - A node to check. Its type is one of
287 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
288 * ImportDeclaration, and ExportNamedDeclaration.
289 * @returns {void}
290 */
291 function forceTrailingCommaIfMultiline(node) {
292 if (isMultiline(node)) {
293 forceTrailingComma(node);
294 } else {
295 forbidTrailingComma(node);
296 }
297 }
298
299 /**
300 * Only if a given node is not multiline, reports the last element of a given node
301 * when it does not have a trailing comma.
302 * Otherwise, reports a trailing comma if it exists.
303 *
304 * @param {ASTNode} node - A node to check. Its type is one of
305 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
306 * ImportDeclaration, and ExportNamedDeclaration.
307 * @returns {void}
308 */
309 function allowTrailingCommaIfMultiline(node) {
310 if (!isMultiline(node)) {
311 forbidTrailingComma(node);
312 }
313 }
314
315 const predicate = {
316 always: forceTrailingComma,
317 "always-multiline": forceTrailingCommaIfMultiline,
318 "only-multiline": allowTrailingCommaIfMultiline,
319 never: forbidTrailingComma,
320 ignore: lodash.noop
321 };
322
323 return {
324 ObjectExpression: predicate[options.objects],
325 ObjectPattern: predicate[options.objects],
326
327 ArrayExpression: predicate[options.arrays],
328 ArrayPattern: predicate[options.arrays],
329
330 ImportDeclaration: predicate[options.imports],
331
332 ExportNamedDeclaration: predicate[options.exports],
333
334 FunctionDeclaration: predicate[options.functions],
335 FunctionExpression: predicate[options.functions],
336 ArrowFunctionExpression: predicate[options.functions],
337 CallExpression: predicate[options.functions],
338 NewExpression: predicate[options.functions]
339 };
340 }
341};