comma-dangle.js 12 KB

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