UNPKG

12.2 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("./utils/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: "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 */
33function 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 */
47function 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 || ecmaVersion < 8) ? "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
74module.exports = {
75 meta: {
76 type: "layout",
77
78 docs: {
79 description: "require or disallow trailing commas",
80 category: "Stylistic Issues",
81 recommended: false,
82 url: "https://eslint.org/docs/rules/comma-dangle"
83 },
84
85 fixable: "code",
86
87 schema: {
88 definitions: {
89 value: {
90 enum: [
91 "always-multiline",
92 "always",
93 "never",
94 "only-multiline"
95 ]
96 },
97 valueWithIgnore: {
98 enum: [
99 "always-multiline",
100 "always",
101 "ignore",
102 "never",
103 "only-multiline"
104 ]
105 }
106 },
107 type: "array",
108 items: [
109 {
110 oneOf: [
111 {
112 $ref: "#/definitions/value"
113 },
114 {
115 type: "object",
116 properties: {
117 arrays: { $ref: "#/definitions/valueWithIgnore" },
118 objects: { $ref: "#/definitions/valueWithIgnore" },
119 imports: { $ref: "#/definitions/valueWithIgnore" },
120 exports: { $ref: "#/definitions/valueWithIgnore" },
121 functions: { $ref: "#/definitions/valueWithIgnore" }
122 },
123 additionalProperties: false
124 }
125 ]
126 }
127 ]
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 switch (node.type) {
148 case "ObjectExpression":
149 case "ObjectPattern":
150 return lodash.last(node.properties);
151 case "ArrayExpression":
152 case "ArrayPattern":
153 return lodash.last(node.elements);
154 case "ImportDeclaration":
155 case "ExportNamedDeclaration":
156 return lodash.last(node.specifiers);
157 case "FunctionDeclaration":
158 case "FunctionExpression":
159 case "ArrowFunctionExpression":
160 return lodash.last(node.params);
161 case "CallExpression":
162 case "NewExpression":
163 return lodash.last(node.arguments);
164 default:
165 return null;
166 }
167 }
168
169 /**
170 * Gets the trailing comma token of the given node.
171 * If the trailing comma does not exist, this returns the token which is
172 * the insertion point of the trailing comma token.
173 * @param {ASTNode} node The node to get.
174 * @param {ASTNode} lastItem The last item of the node.
175 * @returns {Token} The trailing comma token or the insertion point.
176 */
177 function getTrailingToken(node, lastItem) {
178 switch (node.type) {
179 case "ObjectExpression":
180 case "ArrayExpression":
181 case "CallExpression":
182 case "NewExpression":
183 return sourceCode.getLastToken(node, 1);
184 default: {
185 const nextToken = sourceCode.getTokenAfter(lastItem);
186
187 if (astUtils.isCommaToken(nextToken)) {
188 return nextToken;
189 }
190 return sourceCode.getLastToken(lastItem);
191 }
192 }
193 }
194
195 /**
196 * Checks whether or not a given node is multiline.
197 * This rule handles a given node as multiline when the closing parenthesis
198 * and the last element are not on the same line.
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 * @param {ASTNode} node A node to check. Its type is one of
218 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
219 * ImportDeclaration, and ExportNamedDeclaration.
220 * @returns {void}
221 */
222 function forbidTrailingComma(node) {
223 const lastItem = getLastItem(node);
224
225 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
226 return;
227 }
228
229 const trailingToken = getTrailingToken(node, lastItem);
230
231 if (astUtils.isCommaToken(trailingToken)) {
232 context.report({
233 node: lastItem,
234 loc: trailingToken.loc,
235 messageId: "unexpected",
236 fix(fixer) {
237 return fixer.remove(trailingToken);
238 }
239 });
240 }
241 }
242
243 /**
244 * Reports the last element of a given node if it does not have a trailing
245 * comma.
246 *
247 * If a given node is `ArrayPattern` which has `RestElement`, the trailing
248 * comma is disallowed, so report if it exists.
249 * @param {ASTNode} node A node to check. Its type is one of
250 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
251 * ImportDeclaration, and ExportNamedDeclaration.
252 * @returns {void}
253 */
254 function forceTrailingComma(node) {
255 const lastItem = getLastItem(node);
256
257 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
258 return;
259 }
260 if (!isTrailingCommaAllowed(lastItem)) {
261 forbidTrailingComma(node);
262 return;
263 }
264
265 const trailingToken = getTrailingToken(node, lastItem);
266
267 if (trailingToken.value !== ",") {
268 context.report({
269 node: lastItem,
270 loc: {
271 start: trailingToken.loc.end,
272 end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
273 },
274 messageId: "missing",
275 fix(fixer) {
276 return fixer.insertTextAfter(trailingToken, ",");
277 }
278 });
279 }
280 }
281
282 /**
283 * If a given node is multiline, reports the last element of a given node
284 * when it does not have a trailing comma.
285 * Otherwise, reports a trailing comma if it exists.
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 * @param {ASTNode} node A node to check. Its type is one of
304 * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
305 * ImportDeclaration, and ExportNamedDeclaration.
306 * @returns {void}
307 */
308 function allowTrailingCommaIfMultiline(node) {
309 if (!isMultiline(node)) {
310 forbidTrailingComma(node);
311 }
312 }
313
314 const predicate = {
315 always: forceTrailingComma,
316 "always-multiline": forceTrailingCommaIfMultiline,
317 "only-multiline": allowTrailingCommaIfMultiline,
318 never: forbidTrailingComma,
319 ignore: lodash.noop
320 };
321
322 return {
323 ObjectExpression: predicate[options.objects],
324 ObjectPattern: predicate[options.objects],
325
326 ArrayExpression: predicate[options.arrays],
327 ArrayPattern: predicate[options.arrays],
328
329 ImportDeclaration: predicate[options.imports],
330
331 ExportNamedDeclaration: predicate[options.exports],
332
333 FunctionDeclaration: predicate[options.functions],
334 FunctionExpression: predicate[options.functions],
335 ArrowFunctionExpression: predicate[options.functions],
336 CallExpression: predicate[options.functions],
337 NewExpression: predicate[options.functions]
338 };
339 }
340};