2 * @fileoverview Rule to forbid or enforce dangling commas.
3 * @author Ian Christian Myers
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const lodash = require("lodash");
13 const astUtils = require("./utils/ast-utils");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
19 const DEFAULT_OPTIONS = Object.freeze({
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.
33 function isTrailingCommaAllowed(lastItem) {
35 lastItem.type === "RestElement" ||
36 lastItem.type === "RestProperty" ||
37 lastItem.type === "ExperimentalRestProperty"
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.
47 function normalizeOptions(optionValue, ecmaVersion) {
48 if (typeof optionValue === "string") {
54 functions: (!ecmaVersion || ecmaVersion < 8) ? "ignore" : optionValue
57 if (typeof optionValue === "object" && optionValue !== null) {
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
67 return DEFAULT_OPTIONS;
70 //------------------------------------------------------------------------------
72 //------------------------------------------------------------------------------
79 description: "require or disallow trailing commas",
80 category: "Stylistic Issues",
82 url: "https://eslint.org/docs/rules/comma-dangle"
112 $ref: "#/definitions/value"
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" }
123 additionalProperties: false
131 unexpected: "Unexpected trailing comma.",
132 missing: "Missing trailing comma."
137 const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
139 const sourceCode = context.getSourceCode();
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.
146 function getLastItem(node) {
148 case "ObjectExpression":
149 case "ObjectPattern":
150 return lodash.last(node.properties);
151 case "ArrayExpression":
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);
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.
177 function getTrailingToken(node, lastItem) {
179 case "ObjectExpression":
180 case "ArrayExpression":
181 case "CallExpression":
182 case "NewExpression":
183 return sourceCode.getLastToken(node, 1);
185 const nextToken = sourceCode.getTokenAfter(lastItem);
187 if (astUtils.isCommaToken(nextToken)) {
190 return sourceCode.getLastToken(lastItem);
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.
202 function isMultiline(node) {
203 const lastItem = getLastItem(node);
209 const penultimateToken = getTrailingToken(node, lastItem);
210 const lastToken = sourceCode.getTokenAfter(penultimateToken);
212 return lastToken.loc.end.line !== penultimateToken.loc.end.line;
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.
222 function forbidTrailingComma(node) {
223 const lastItem = getLastItem(node);
225 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
229 const trailingToken = getTrailingToken(node, lastItem);
231 if (astUtils.isCommaToken(trailingToken)) {
234 loc: trailingToken.loc,
235 messageId: "unexpected",
237 return fixer.remove(trailingToken);
244 * Reports the last element of a given node if it does not have a trailing
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.
254 function forceTrailingComma(node) {
255 const lastItem = getLastItem(node);
257 if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
260 if (!isTrailingCommaAllowed(lastItem)) {
261 forbidTrailingComma(node);
265 const trailingToken = getTrailingToken(node, lastItem);
267 if (trailingToken.value !== ",") {
271 start: trailingToken.loc.end,
272 end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
274 messageId: "missing",
276 return fixer.insertTextAfter(trailingToken, ",");
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.
291 function forceTrailingCommaIfMultiline(node) {
292 if (isMultiline(node)) {
293 forceTrailingComma(node);
295 forbidTrailingComma(node);
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.
308 function allowTrailingCommaIfMultiline(node) {
309 if (!isMultiline(node)) {
310 forbidTrailingComma(node);
315 always: forceTrailingComma,
316 "always-multiline": forceTrailingCommaIfMultiline,
317 "only-multiline": allowTrailingCommaIfMultiline,
318 never: forbidTrailingComma,
323 ObjectExpression: predicate[options.objects],
324 ObjectPattern: predicate[options.objects],
326 ArrayExpression: predicate[options.arrays],
327 ArrayPattern: predicate[options.arrays],
329 ImportDeclaration: predicate[options.imports],
331 ExportNamedDeclaration: predicate[options.exports],
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]