comma-dangle.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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 < 2017 ? "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. /** @type {import('../shared/types').Rule} */
  64. module.exports = {
  65. meta: {
  66. type: "layout",
  67. docs: {
  68. description: "Require or disallow trailing commas",
  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. additionalItems: false
  115. },
  116. messages: {
  117. unexpected: "Unexpected trailing comma.",
  118. missing: "Missing trailing comma."
  119. }
  120. },
  121. create(context) {
  122. const options = normalizeOptions(context.options[0], context.languageOptions.ecmaVersion);
  123. const sourceCode = context.getSourceCode();
  124. /**
  125. * Gets the last item of the given node.
  126. * @param {ASTNode} node The node to get.
  127. * @returns {ASTNode|null} The last node or null.
  128. */
  129. function getLastItem(node) {
  130. /**
  131. * Returns the last element of an array
  132. * @param {any[]} array The input array
  133. * @returns {any} The last element
  134. */
  135. function last(array) {
  136. return array[array.length - 1];
  137. }
  138. switch (node.type) {
  139. case "ObjectExpression":
  140. case "ObjectPattern":
  141. return last(node.properties);
  142. case "ArrayExpression":
  143. case "ArrayPattern":
  144. return last(node.elements);
  145. case "ImportDeclaration":
  146. case "ExportNamedDeclaration":
  147. return last(node.specifiers);
  148. case "FunctionDeclaration":
  149. case "FunctionExpression":
  150. case "ArrowFunctionExpression":
  151. return last(node.params);
  152. case "CallExpression":
  153. case "NewExpression":
  154. return last(node.arguments);
  155. default:
  156. return null;
  157. }
  158. }
  159. /**
  160. * Gets the trailing comma token of the given node.
  161. * If the trailing comma does not exist, this returns the token which is
  162. * the insertion point of the trailing comma token.
  163. * @param {ASTNode} node The node to get.
  164. * @param {ASTNode} lastItem The last item of the node.
  165. * @returns {Token} The trailing comma token or the insertion point.
  166. */
  167. function getTrailingToken(node, lastItem) {
  168. switch (node.type) {
  169. case "ObjectExpression":
  170. case "ArrayExpression":
  171. case "CallExpression":
  172. case "NewExpression":
  173. return sourceCode.getLastToken(node, 1);
  174. default: {
  175. const nextToken = sourceCode.getTokenAfter(lastItem);
  176. if (astUtils.isCommaToken(nextToken)) {
  177. return nextToken;
  178. }
  179. return sourceCode.getLastToken(lastItem);
  180. }
  181. }
  182. }
  183. /**
  184. * Checks whether or not a given node is multiline.
  185. * This rule handles a given node as multiline when the closing parenthesis
  186. * and the last element are not on the same line.
  187. * @param {ASTNode} node A node to check.
  188. * @returns {boolean} `true` if the node is multiline.
  189. */
  190. function isMultiline(node) {
  191. const lastItem = getLastItem(node);
  192. if (!lastItem) {
  193. return false;
  194. }
  195. const penultimateToken = getTrailingToken(node, lastItem);
  196. const lastToken = sourceCode.getTokenAfter(penultimateToken);
  197. return lastToken.loc.end.line !== penultimateToken.loc.end.line;
  198. }
  199. /**
  200. * Reports a trailing comma if it exists.
  201. * @param {ASTNode} node A node to check. Its type is one of
  202. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  203. * ImportDeclaration, and ExportNamedDeclaration.
  204. * @returns {void}
  205. */
  206. function forbidTrailingComma(node) {
  207. const lastItem = getLastItem(node);
  208. if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
  209. return;
  210. }
  211. const trailingToken = getTrailingToken(node, lastItem);
  212. if (astUtils.isCommaToken(trailingToken)) {
  213. context.report({
  214. node: lastItem,
  215. loc: trailingToken.loc,
  216. messageId: "unexpected",
  217. *fix(fixer) {
  218. yield fixer.remove(trailingToken);
  219. /*
  220. * Extend the range of the fix to include surrounding tokens to ensure
  221. * that the element after which the comma is removed stays _last_.
  222. * This intentionally makes conflicts in fix ranges with rules that may be
  223. * adding or removing elements in the same autofix pass.
  224. * https://github.com/eslint/eslint/issues/15660
  225. */
  226. yield fixer.insertTextBefore(sourceCode.getTokenBefore(trailingToken), "");
  227. yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
  228. }
  229. });
  230. }
  231. }
  232. /**
  233. * Reports the last element of a given node if it does not have a trailing
  234. * comma.
  235. *
  236. * If a given node is `ArrayPattern` which has `RestElement`, the trailing
  237. * comma is disallowed, so report if it exists.
  238. * @param {ASTNode} node A node to check. Its type is one of
  239. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  240. * ImportDeclaration, and ExportNamedDeclaration.
  241. * @returns {void}
  242. */
  243. function forceTrailingComma(node) {
  244. const lastItem = getLastItem(node);
  245. if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
  246. return;
  247. }
  248. if (!isTrailingCommaAllowed(lastItem)) {
  249. forbidTrailingComma(node);
  250. return;
  251. }
  252. const trailingToken = getTrailingToken(node, lastItem);
  253. if (trailingToken.value !== ",") {
  254. context.report({
  255. node: lastItem,
  256. loc: {
  257. start: trailingToken.loc.end,
  258. end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
  259. },
  260. messageId: "missing",
  261. *fix(fixer) {
  262. yield fixer.insertTextAfter(trailingToken, ",");
  263. /*
  264. * Extend the range of the fix to include surrounding tokens to ensure
  265. * that the element after which the comma is inserted stays _last_.
  266. * This intentionally makes conflicts in fix ranges with rules that may be
  267. * adding or removing elements in the same autofix pass.
  268. * https://github.com/eslint/eslint/issues/15660
  269. */
  270. yield fixer.insertTextBefore(trailingToken, "");
  271. yield fixer.insertTextAfter(sourceCode.getTokenAfter(trailingToken), "");
  272. }
  273. });
  274. }
  275. }
  276. /**
  277. * If a given node is multiline, reports the last element of a given node
  278. * when it does not have a trailing comma.
  279. * Otherwise, reports a trailing comma if it exists.
  280. * @param {ASTNode} node A node to check. Its type is one of
  281. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  282. * ImportDeclaration, and ExportNamedDeclaration.
  283. * @returns {void}
  284. */
  285. function forceTrailingCommaIfMultiline(node) {
  286. if (isMultiline(node)) {
  287. forceTrailingComma(node);
  288. } else {
  289. forbidTrailingComma(node);
  290. }
  291. }
  292. /**
  293. * Only if a given node is not multiline, reports the last element of a given node
  294. * when it does not have a trailing comma.
  295. * Otherwise, reports a trailing comma if it exists.
  296. * @param {ASTNode} node A node to check. Its type is one of
  297. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  298. * ImportDeclaration, and ExportNamedDeclaration.
  299. * @returns {void}
  300. */
  301. function allowTrailingCommaIfMultiline(node) {
  302. if (!isMultiline(node)) {
  303. forbidTrailingComma(node);
  304. }
  305. }
  306. const predicate = {
  307. always: forceTrailingComma,
  308. "always-multiline": forceTrailingCommaIfMultiline,
  309. "only-multiline": allowTrailingCommaIfMultiline,
  310. never: forbidTrailingComma,
  311. ignore() {}
  312. };
  313. return {
  314. ObjectExpression: predicate[options.objects],
  315. ObjectPattern: predicate[options.objects],
  316. ArrayExpression: predicate[options.arrays],
  317. ArrayPattern: predicate[options.arrays],
  318. ImportDeclaration: predicate[options.imports],
  319. ExportNamedDeclaration: predicate[options.exports],
  320. FunctionDeclaration: predicate[options.functions],
  321. FunctionExpression: predicate[options.functions],
  322. ArrowFunctionExpression: predicate[options.functions],
  323. CallExpression: predicate[options.functions],
  324. NewExpression: predicate[options.functions]
  325. };
  326. }
  327. };