--- /dev/null
+/**
+ * @fileoverview Rule to forbid or enforce dangling commas.
+ * @author Ian Christian Myers
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const lodash = require("lodash");
+const astUtils = require("./utils/ast-utils");
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+const DEFAULT_OPTIONS = Object.freeze({
+ arrays: "never",
+ objects: "never",
+ imports: "never",
+ exports: "never",
+ functions: "never"
+});
+
+/**
+ * Checks whether or not a trailing comma is allowed in a given node.
+ * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
+ * @param {ASTNode} lastItem The node of the last element in the given node.
+ * @returns {boolean} `true` if a trailing comma is allowed.
+ */
+function isTrailingCommaAllowed(lastItem) {
+ return !(
+ lastItem.type === "RestElement" ||
+ lastItem.type === "RestProperty" ||
+ lastItem.type === "ExperimentalRestProperty"
+ );
+}
+
+/**
+ * Normalize option value.
+ * @param {string|Object|undefined} optionValue The 1st option value to normalize.
+ * @param {number} ecmaVersion The normalized ECMAScript version.
+ * @returns {Object} The normalized option value.
+ */
+function normalizeOptions(optionValue, ecmaVersion) {
+ if (typeof optionValue === "string") {
+ return {
+ arrays: optionValue,
+ objects: optionValue,
+ imports: optionValue,
+ exports: optionValue,
+ functions: (!ecmaVersion || ecmaVersion < 8) ? "ignore" : optionValue
+ };
+ }
+ if (typeof optionValue === "object" && optionValue !== null) {
+ return {
+ arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
+ objects: optionValue.objects || DEFAULT_OPTIONS.objects,
+ imports: optionValue.imports || DEFAULT_OPTIONS.imports,
+ exports: optionValue.exports || DEFAULT_OPTIONS.exports,
+ functions: optionValue.functions || DEFAULT_OPTIONS.functions
+ };
+ }
+
+ return DEFAULT_OPTIONS;
+}
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "layout",
+
+ docs: {
+ description: "require or disallow trailing commas",
+ category: "Stylistic Issues",
+ recommended: false,
+ url: "https://eslint.org/docs/rules/comma-dangle"
+ },
+
+ fixable: "code",
+
+ schema: {
+ definitions: {
+ value: {
+ enum: [
+ "always-multiline",
+ "always",
+ "never",
+ "only-multiline"
+ ]
+ },
+ valueWithIgnore: {
+ enum: [
+ "always-multiline",
+ "always",
+ "ignore",
+ "never",
+ "only-multiline"
+ ]
+ }
+ },
+ type: "array",
+ items: [
+ {
+ oneOf: [
+ {
+ $ref: "#/definitions/value"
+ },
+ {
+ type: "object",
+ properties: {
+ arrays: { $ref: "#/definitions/valueWithIgnore" },
+ objects: { $ref: "#/definitions/valueWithIgnore" },
+ imports: { $ref: "#/definitions/valueWithIgnore" },
+ exports: { $ref: "#/definitions/valueWithIgnore" },
+ functions: { $ref: "#/definitions/valueWithIgnore" }
+ },
+ additionalProperties: false
+ }
+ ]
+ }
+ ]
+ },
+
+ messages: {
+ unexpected: "Unexpected trailing comma.",
+ missing: "Missing trailing comma."
+ }
+ },
+
+ create(context) {
+ const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
+
+ const sourceCode = context.getSourceCode();
+
+ /**
+ * Gets the last item of the given node.
+ * @param {ASTNode} node The node to get.
+ * @returns {ASTNode|null} The last node or null.
+ */
+ function getLastItem(node) {
+ switch (node.type) {
+ case "ObjectExpression":
+ case "ObjectPattern":
+ return lodash.last(node.properties);
+ case "ArrayExpression":
+ case "ArrayPattern":
+ return lodash.last(node.elements);
+ case "ImportDeclaration":
+ case "ExportNamedDeclaration":
+ return lodash.last(node.specifiers);
+ case "FunctionDeclaration":
+ case "FunctionExpression":
+ case "ArrowFunctionExpression":
+ return lodash.last(node.params);
+ case "CallExpression":
+ case "NewExpression":
+ return lodash.last(node.arguments);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Gets the trailing comma token of the given node.
+ * If the trailing comma does not exist, this returns the token which is
+ * the insertion point of the trailing comma token.
+ * @param {ASTNode} node The node to get.
+ * @param {ASTNode} lastItem The last item of the node.
+ * @returns {Token} The trailing comma token or the insertion point.
+ */
+ function getTrailingToken(node, lastItem) {
+ switch (node.type) {
+ case "ObjectExpression":
+ case "ArrayExpression":
+ case "CallExpression":
+ case "NewExpression":
+ return sourceCode.getLastToken(node, 1);
+ default: {
+ const nextToken = sourceCode.getTokenAfter(lastItem);
+
+ if (astUtils.isCommaToken(nextToken)) {
+ return nextToken;
+ }
+ return sourceCode.getLastToken(lastItem);
+ }
+ }
+ }
+
+ /**
+ * Checks whether or not a given node is multiline.
+ * This rule handles a given node as multiline when the closing parenthesis
+ * and the last element are not on the same line.
+ * @param {ASTNode} node A node to check.
+ * @returns {boolean} `true` if the node is multiline.
+ */
+ function isMultiline(node) {
+ const lastItem = getLastItem(node);
+
+ if (!lastItem) {
+ return false;
+ }
+
+ const penultimateToken = getTrailingToken(node, lastItem);
+ const lastToken = sourceCode.getTokenAfter(penultimateToken);
+
+ return lastToken.loc.end.line !== penultimateToken.loc.end.line;
+ }
+
+ /**
+ * Reports a trailing comma if it exists.
+ * @param {ASTNode} node A node to check. Its type is one of
+ * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
+ * ImportDeclaration, and ExportNamedDeclaration.
+ * @returns {void}
+ */
+ function forbidTrailingComma(node) {
+ const lastItem = getLastItem(node);
+
+ if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
+ return;
+ }
+
+ const trailingToken = getTrailingToken(node, lastItem);
+
+ if (astUtils.isCommaToken(trailingToken)) {
+ context.report({
+ node: lastItem,
+ loc: trailingToken.loc,
+ messageId: "unexpected",
+ fix(fixer) {
+ return fixer.remove(trailingToken);
+ }
+ });
+ }
+ }
+
+ /**
+ * Reports the last element of a given node if it does not have a trailing
+ * comma.
+ *
+ * If a given node is `ArrayPattern` which has `RestElement`, the trailing
+ * comma is disallowed, so report if it exists.
+ * @param {ASTNode} node A node to check. Its type is one of
+ * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
+ * ImportDeclaration, and ExportNamedDeclaration.
+ * @returns {void}
+ */
+ function forceTrailingComma(node) {
+ const lastItem = getLastItem(node);
+
+ if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
+ return;
+ }
+ if (!isTrailingCommaAllowed(lastItem)) {
+ forbidTrailingComma(node);
+ return;
+ }
+
+ const trailingToken = getTrailingToken(node, lastItem);
+
+ if (trailingToken.value !== ",") {
+ context.report({
+ node: lastItem,
+ loc: {
+ start: trailingToken.loc.end,
+ end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
+ },
+ messageId: "missing",
+ fix(fixer) {
+ return fixer.insertTextAfter(trailingToken, ",");
+ }
+ });
+ }
+ }
+
+ /**
+ * If a given node is multiline, reports the last element of a given node
+ * when it does not have a trailing comma.
+ * Otherwise, reports a trailing comma if it exists.
+ * @param {ASTNode} node A node to check. Its type is one of
+ * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
+ * ImportDeclaration, and ExportNamedDeclaration.
+ * @returns {void}
+ */
+ function forceTrailingCommaIfMultiline(node) {
+ if (isMultiline(node)) {
+ forceTrailingComma(node);
+ } else {
+ forbidTrailingComma(node);
+ }
+ }
+
+ /**
+ * Only if a given node is not multiline, reports the last element of a given node
+ * when it does not have a trailing comma.
+ * Otherwise, reports a trailing comma if it exists.
+ * @param {ASTNode} node A node to check. Its type is one of
+ * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
+ * ImportDeclaration, and ExportNamedDeclaration.
+ * @returns {void}
+ */
+ function allowTrailingCommaIfMultiline(node) {
+ if (!isMultiline(node)) {
+ forbidTrailingComma(node);
+ }
+ }
+
+ const predicate = {
+ always: forceTrailingComma,
+ "always-multiline": forceTrailingCommaIfMultiline,
+ "only-multiline": allowTrailingCommaIfMultiline,
+ never: forbidTrailingComma,
+ ignore: lodash.noop
+ };
+
+ return {
+ ObjectExpression: predicate[options.objects],
+ ObjectPattern: predicate[options.objects],
+
+ ArrayExpression: predicate[options.arrays],
+ ArrayPattern: predicate[options.arrays],
+
+ ImportDeclaration: predicate[options.imports],
+
+ ExportNamedDeclaration: predicate[options.exports],
+
+ FunctionDeclaration: predicate[options.functions],
+ FunctionExpression: predicate[options.functions],
+ ArrowFunctionExpression: predicate[options.functions],
+ CallExpression: predicate[options.functions],
+ NewExpression: predicate[options.functions]
+ };
+ }
+};