2 * @fileoverview Rule to forbid or enforce dangling commas.
3 * @author Ian Christian Myers
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
18 const DEFAULT_OPTIONS = Object.freeze({
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.
32 function isTrailingCommaAllowed(lastItem) {
34 lastItem.type === "RestElement" ||
35 lastItem.type === "RestProperty" ||
36 lastItem.type === "ExperimentalRestProperty"
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.
46 function normalizeOptions(optionValue, ecmaVersion) {
47 if (typeof optionValue === "string") {
53 functions: (!ecmaVersion || ecmaVersion < 8) ? "ignore" : optionValue
56 if (typeof optionValue === "object" && optionValue !== null) {
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
66 return DEFAULT_OPTIONS;
69 //------------------------------------------------------------------------------
71 //------------------------------------------------------------------------------
78 description: "require or disallow trailing commas",
79 category: "Stylistic Issues",
81 url: "https://eslint.org/docs/rules/comma-dangle"
111 $ref: "#/definitions/value"
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" }
122 additionalProperties: false
130 unexpected: "Unexpected trailing comma.",
131 missing: "Missing trailing comma."
136 const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
138 const sourceCode = context.getSourceCode();
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.
145 function getLastItem(node) {
148 * Returns the last element of an array
149 * @param {any[]} array The input array
150 * @returns {any} The last element
152 function last(array) {
153 return array[array.length - 1];
157 case "ObjectExpression":
158 case "ObjectPattern":
159 return last(node.properties);
160 case "ArrayExpression":
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);
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.
186 function getTrailingToken(node, lastItem) {
188 case "ObjectExpression":
189 case "ArrayExpression":
190 case "CallExpression":
191 case "NewExpression":
192 return sourceCode.getLastToken(node, 1);
194 const nextToken = sourceCode.getTokenAfter(lastItem);
196 if (astUtils.isCommaToken(nextToken)) {
199 return sourceCode.getLastToken(lastItem);
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.
211 function isMultiline(node) {
212 const lastItem = getLastItem(node);
218 const penultimateToken = getTrailingToken(node, lastItem);
219 const lastToken = sourceCode.getTokenAfter(penultimateToken);
221 return lastToken.loc.end.line !== penultimateToken.loc.end.line;
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.
231 function forbidTrailingComma(node) {
232 const lastItem = getLastItem(node);
234 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
238 const trailingToken = getTrailingToken(node, lastItem);
240 if (astUtils.isCommaToken(trailingToken)) {
243 loc: trailingToken.loc,
244 messageId: "unexpected",
246 return fixer.remove(trailingToken);
253 * Reports the last element of a given node if it does not have a trailing
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.
263 function forceTrailingComma(node) {
264 const lastItem = getLastItem(node);
266 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
269 if (!isTrailingCommaAllowed(lastItem)) {
270 forbidTrailingComma(node);
274 const trailingToken = getTrailingToken(node, lastItem);
276 if (trailingToken.value !== ",") {
280 start: trailingToken.loc.end,
281 end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
283 messageId: "missing",
285 return fixer.insertTextAfter(trailingToken, ",");
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.
300 function forceTrailingCommaIfMultiline(node) {
301 if (isMultiline(node)) {
302 forceTrailingComma(node);
304 forbidTrailingComma(node);
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.
317 function allowTrailingCommaIfMultiline(node) {
318 if (!isMultiline(node)) {
319 forbidTrailingComma(node);
324 always: forceTrailingComma,
325 "always-multiline": forceTrailingCommaIfMultiline,
326 "only-multiline": allowTrailingCommaIfMultiline,
327 never: forbidTrailingComma,
332 ObjectExpression: predicate[options.objects],
333 ObjectPattern: predicate[options.objects],
335 ArrayExpression: predicate[options.arrays],
336 ArrayPattern: predicate[options.arrays],
338 ImportDeclaration: predicate[options.imports],
340 ExportNamedDeclaration: predicate[options.exports],
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]