comma-dangle.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. /**
  2. * @fileoverview Rule to forbid or enforce dangling commas.
  3. * @author Ian Christian Myers
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const DEFAULT_OPTIONS = Object.freeze({
  14. arrays: "never",
  15. objects: "never",
  16. imports: "never",
  17. exports: "never",
  18. functions: "never"
  19. });
  20. /**
  21. * Checks whether or not a trailing comma is allowed in a given node.
  22. * If the `lastItem` is `RestElement` or `RestProperty`, it disallows trailing commas.
  23. * @param {ASTNode} lastItem The node of the last element in the given node.
  24. * @returns {boolean} `true` if a trailing comma is allowed.
  25. */
  26. function isTrailingCommaAllowed(lastItem) {
  27. return !(
  28. lastItem.type === "RestElement" ||
  29. lastItem.type === "RestProperty" ||
  30. lastItem.type === "ExperimentalRestProperty"
  31. );
  32. }
  33. /**
  34. * Normalize option value.
  35. * @param {string|Object|undefined} optionValue The 1st option value to normalize.
  36. * @param {number} ecmaVersion The normalized ECMAScript version.
  37. * @returns {Object} The normalized option value.
  38. */
  39. function normalizeOptions(optionValue, ecmaVersion) {
  40. if (typeof optionValue === "string") {
  41. return {
  42. arrays: optionValue,
  43. objects: optionValue,
  44. imports: optionValue,
  45. exports: optionValue,
  46. functions: (!ecmaVersion || ecmaVersion < 8) ? "ignore" : optionValue
  47. };
  48. }
  49. if (typeof optionValue === "object" && optionValue !== null) {
  50. return {
  51. arrays: optionValue.arrays || DEFAULT_OPTIONS.arrays,
  52. objects: optionValue.objects || DEFAULT_OPTIONS.objects,
  53. imports: optionValue.imports || DEFAULT_OPTIONS.imports,
  54. exports: optionValue.exports || DEFAULT_OPTIONS.exports,
  55. functions: optionValue.functions || DEFAULT_OPTIONS.functions
  56. };
  57. }
  58. return DEFAULT_OPTIONS;
  59. }
  60. //------------------------------------------------------------------------------
  61. // Rule Definition
  62. //------------------------------------------------------------------------------
  63. module.exports = {
  64. meta: {
  65. type: "layout",
  66. docs: {
  67. description: "require or disallow trailing commas",
  68. category: "Stylistic Issues",
  69. recommended: false,
  70. url: "https://eslint.org/docs/rules/comma-dangle"
  71. },
  72. fixable: "code",
  73. schema: {
  74. definitions: {
  75. value: {
  76. enum: [
  77. "always-multiline",
  78. "always",
  79. "never",
  80. "only-multiline"
  81. ]
  82. },
  83. valueWithIgnore: {
  84. enum: [
  85. "always-multiline",
  86. "always",
  87. "ignore",
  88. "never",
  89. "only-multiline"
  90. ]
  91. }
  92. },
  93. type: "array",
  94. items: [
  95. {
  96. oneOf: [
  97. {
  98. $ref: "#/definitions/value"
  99. },
  100. {
  101. type: "object",
  102. properties: {
  103. arrays: { $ref: "#/definitions/valueWithIgnore" },
  104. objects: { $ref: "#/definitions/valueWithIgnore" },
  105. imports: { $ref: "#/definitions/valueWithIgnore" },
  106. exports: { $ref: "#/definitions/valueWithIgnore" },
  107. functions: { $ref: "#/definitions/valueWithIgnore" }
  108. },
  109. additionalProperties: false
  110. }
  111. ]
  112. }
  113. ]
  114. },
  115. messages: {
  116. unexpected: "Unexpected trailing comma.",
  117. missing: "Missing trailing comma."
  118. }
  119. },
  120. create(context) {
  121. const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
  122. const sourceCode = context.getSourceCode();
  123. /**
  124. * Gets the last item of the given node.
  125. * @param {ASTNode} node The node to get.
  126. * @returns {ASTNode|null} The last node or null.
  127. */
  128. function getLastItem(node) {
  129. /**
  130. * Returns the last element of an array
  131. * @param {any[]} array The input array
  132. * @returns {any} The last element
  133. */
  134. function last(array) {
  135. return array[array.length - 1];
  136. }
  137. switch (node.type) {
  138. case "ObjectExpression":
  139. case "ObjectPattern":
  140. return last(node.properties);
  141. case "ArrayExpression":
  142. case "ArrayPattern":
  143. return last(node.elements);
  144. case "ImportDeclaration":
  145. case "ExportNamedDeclaration":
  146. return last(node.specifiers);
  147. case "FunctionDeclaration":
  148. case "FunctionExpression":
  149. case "ArrowFunctionExpression":
  150. return last(node.params);
  151. case "CallExpression":
  152. case "NewExpression":
  153. return last(node.arguments);
  154. default:
  155. return null;
  156. }
  157. }
  158. /**
  159. * Gets the trailing comma token of the given node.
  160. * If the trailing comma does not exist, this returns the token which is
  161. * the insertion point of the trailing comma token.
  162. * @param {ASTNode} node The node to get.
  163. * @param {ASTNode} lastItem The last item of the node.
  164. * @returns {Token} The trailing comma token or the insertion point.
  165. */
  166. function getTrailingToken(node, lastItem) {
  167. switch (node.type) {
  168. case "ObjectExpression":
  169. case "ArrayExpression":
  170. case "CallExpression":
  171. case "NewExpression":
  172. return sourceCode.getLastToken(node, 1);
  173. default: {
  174. const nextToken = sourceCode.getTokenAfter(lastItem);
  175. if (astUtils.isCommaToken(nextToken)) {
  176. return nextToken;
  177. }
  178. return sourceCode.getLastToken(lastItem);
  179. }
  180. }
  181. }
  182. /**
  183. * Checks whether or not a given node is multiline.
  184. * This rule handles a given node as multiline when the closing parenthesis
  185. * and the last element are not on the same line.
  186. * @param {ASTNode} node A node to check.
  187. * @returns {boolean} `true` if the node is multiline.
  188. */
  189. function isMultiline(node) {
  190. const lastItem = getLastItem(node);
  191. if (!lastItem) {
  192. return false;
  193. }
  194. const penultimateToken = getTrailingToken(node, lastItem);
  195. const lastToken = sourceCode.getTokenAfter(penultimateToken);
  196. return lastToken.loc.end.line !== penultimateToken.loc.end.line;
  197. }
  198. /**
  199. * Reports a trailing comma if it exists.
  200. * @param {ASTNode} node A node to check. Its type is one of
  201. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  202. * ImportDeclaration, and ExportNamedDeclaration.
  203. * @returns {void}
  204. */
  205. function forbidTrailingComma(node) {
  206. const lastItem = getLastItem(node);
  207. if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
  208. return;
  209. }
  210. const trailingToken = getTrailingToken(node, lastItem);
  211. if (astUtils.isCommaToken(trailingToken)) {
  212. context.report({
  213. node: lastItem,
  214. loc: trailingToken.loc,
  215. messageId: "unexpected",
  216. fix(fixer) {
  217. return fixer.remove(trailingToken);
  218. }
  219. });
  220. }
  221. }
  222. /**
  223. * Reports the last element of a given node if it does not have a trailing
  224. * comma.
  225. *
  226. * If a given node is `ArrayPattern` which has `RestElement`, the trailing
  227. * comma is disallowed, so report if it exists.
  228. * @param {ASTNode} node A node to check. Its type is one of
  229. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  230. * ImportDeclaration, and ExportNamedDeclaration.
  231. * @returns {void}
  232. */
  233. function forceTrailingComma(node) {
  234. const lastItem = getLastItem(node);
  235. if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
  236. return;
  237. }
  238. if (!isTrailingCommaAllowed(lastItem)) {
  239. forbidTrailingComma(node);
  240. return;
  241. }
  242. const trailingToken = getTrailingToken(node, lastItem);
  243. if (trailingToken.value !== ",") {
  244. context.report({
  245. node: lastItem,
  246. loc: {
  247. start: trailingToken.loc.end,
  248. end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
  249. },
  250. messageId: "missing",
  251. fix(fixer) {
  252. return fixer.insertTextAfter(trailingToken, ",");
  253. }
  254. });
  255. }
  256. }
  257. /**
  258. * If a given node is multiline, reports the last element of a given node
  259. * when it does not have a trailing comma.
  260. * Otherwise, reports a trailing comma if it exists.
  261. * @param {ASTNode} node A node to check. Its type is one of
  262. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  263. * ImportDeclaration, and ExportNamedDeclaration.
  264. * @returns {void}
  265. */
  266. function forceTrailingCommaIfMultiline(node) {
  267. if (isMultiline(node)) {
  268. forceTrailingComma(node);
  269. } else {
  270. forbidTrailingComma(node);
  271. }
  272. }
  273. /**
  274. * Only if a given node is not multiline, reports the last element of a given node
  275. * when it does not have a trailing comma.
  276. * Otherwise, reports a trailing comma if it exists.
  277. * @param {ASTNode} node A node to check. Its type is one of
  278. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  279. * ImportDeclaration, and ExportNamedDeclaration.
  280. * @returns {void}
  281. */
  282. function allowTrailingCommaIfMultiline(node) {
  283. if (!isMultiline(node)) {
  284. forbidTrailingComma(node);
  285. }
  286. }
  287. const predicate = {
  288. always: forceTrailingComma,
  289. "always-multiline": forceTrailingCommaIfMultiline,
  290. "only-multiline": allowTrailingCommaIfMultiline,
  291. never: forbidTrailingComma,
  292. ignore: () => {}
  293. };
  294. return {
  295. ObjectExpression: predicate[options.objects],
  296. ObjectPattern: predicate[options.objects],
  297. ArrayExpression: predicate[options.arrays],
  298. ArrayPattern: predicate[options.arrays],
  299. ImportDeclaration: predicate[options.imports],
  300. ExportNamedDeclaration: predicate[options.exports],
  301. FunctionDeclaration: predicate[options.functions],
  302. FunctionExpression: predicate[options.functions],
  303. ArrowFunctionExpression: predicate[options.functions],
  304. CallExpression: predicate[options.functions],
  305. NewExpression: predicate[options.functions]
  306. };
  307. }
  308. };