comma-dangle.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /**
  2. * @fileoverview Rule to forbid or enforce dangling commas.
  3. * @author Ian Christian Myers
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  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 < 2017 ? "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. /** @type {import('../types').Rule.RuleModule} */
  65. module.exports = {
  66. meta: {
  67. deprecated: {
  68. message: "Formatting rules are being moved out of ESLint core.",
  69. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  70. deprecatedSince: "8.53.0",
  71. availableUntil: "11.0.0",
  72. replacedBy: [
  73. {
  74. message:
  75. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  76. url: "https://eslint.style/guide/migration",
  77. plugin: {
  78. name: "@stylistic/eslint-plugin",
  79. url: "https://eslint.style",
  80. },
  81. rule: {
  82. name: "comma-dangle",
  83. url: "https://eslint.style/rules/comma-dangle",
  84. },
  85. },
  86. ],
  87. },
  88. type: "layout",
  89. docs: {
  90. description: "Require or disallow trailing commas",
  91. recommended: false,
  92. url: "https://eslint.org/docs/latest/rules/comma-dangle",
  93. },
  94. fixable: "code",
  95. schema: {
  96. definitions: {
  97. value: {
  98. enum: [
  99. "always-multiline",
  100. "always",
  101. "never",
  102. "only-multiline",
  103. ],
  104. },
  105. valueWithIgnore: {
  106. enum: [
  107. "always-multiline",
  108. "always",
  109. "ignore",
  110. "never",
  111. "only-multiline",
  112. ],
  113. },
  114. },
  115. type: "array",
  116. items: [
  117. {
  118. oneOf: [
  119. {
  120. $ref: "#/definitions/value",
  121. },
  122. {
  123. type: "object",
  124. properties: {
  125. arrays: {
  126. $ref: "#/definitions/valueWithIgnore",
  127. },
  128. objects: {
  129. $ref: "#/definitions/valueWithIgnore",
  130. },
  131. imports: {
  132. $ref: "#/definitions/valueWithIgnore",
  133. },
  134. exports: {
  135. $ref: "#/definitions/valueWithIgnore",
  136. },
  137. functions: {
  138. $ref: "#/definitions/valueWithIgnore",
  139. },
  140. },
  141. additionalProperties: false,
  142. },
  143. ],
  144. },
  145. ],
  146. additionalItems: false,
  147. },
  148. messages: {
  149. unexpected: "Unexpected trailing comma.",
  150. missing: "Missing trailing comma.",
  151. },
  152. },
  153. create(context) {
  154. const options = normalizeOptions(
  155. context.options[0],
  156. context.languageOptions.ecmaVersion,
  157. );
  158. const sourceCode = context.sourceCode;
  159. /**
  160. * Gets the last item of the given node.
  161. * @param {ASTNode} node The node to get.
  162. * @returns {ASTNode|null} The last node or null.
  163. */
  164. function getLastItem(node) {
  165. /**
  166. * Returns the last element of an array
  167. * @param {any[]} array The input array
  168. * @returns {any} The last element
  169. */
  170. function last(array) {
  171. return array.at(-1);
  172. }
  173. switch (node.type) {
  174. case "ObjectExpression":
  175. case "ObjectPattern":
  176. return last(node.properties);
  177. case "ArrayExpression":
  178. case "ArrayPattern":
  179. return last(node.elements);
  180. case "ImportDeclaration":
  181. case "ExportNamedDeclaration":
  182. return last(node.specifiers);
  183. case "FunctionDeclaration":
  184. case "FunctionExpression":
  185. case "ArrowFunctionExpression":
  186. return last(node.params);
  187. case "CallExpression":
  188. case "NewExpression":
  189. return last(node.arguments);
  190. default:
  191. return null;
  192. }
  193. }
  194. /**
  195. * Gets the trailing comma token of the given node.
  196. * If the trailing comma does not exist, this returns the token which is
  197. * the insertion point of the trailing comma token.
  198. * @param {ASTNode} node The node to get.
  199. * @param {ASTNode} lastItem The last item of the node.
  200. * @returns {Token} The trailing comma token or the insertion point.
  201. */
  202. function getTrailingToken(node, lastItem) {
  203. switch (node.type) {
  204. case "ObjectExpression":
  205. case "ArrayExpression":
  206. case "CallExpression":
  207. case "NewExpression":
  208. return sourceCode.getLastToken(node, 1);
  209. default: {
  210. const nextToken = sourceCode.getTokenAfter(lastItem);
  211. if (astUtils.isCommaToken(nextToken)) {
  212. return nextToken;
  213. }
  214. return sourceCode.getLastToken(lastItem);
  215. }
  216. }
  217. }
  218. /**
  219. * Checks whether or not a given node is multiline.
  220. * This rule handles a given node as multiline when the closing parenthesis
  221. * and the last element are not on the same line.
  222. * @param {ASTNode} node A node to check.
  223. * @returns {boolean} `true` if the node is multiline.
  224. */
  225. function isMultiline(node) {
  226. const lastItem = getLastItem(node);
  227. if (!lastItem) {
  228. return false;
  229. }
  230. const penultimateToken = getTrailingToken(node, lastItem);
  231. const lastToken = sourceCode.getTokenAfter(penultimateToken);
  232. return lastToken.loc.end.line !== penultimateToken.loc.end.line;
  233. }
  234. /**
  235. * Reports a trailing comma if it exists.
  236. * @param {ASTNode} node A node to check. Its type is one of
  237. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  238. * ImportDeclaration, and ExportNamedDeclaration.
  239. * @returns {void}
  240. */
  241. function forbidTrailingComma(node) {
  242. const lastItem = getLastItem(node);
  243. if (
  244. !lastItem ||
  245. (node.type === "ImportDeclaration" &&
  246. lastItem.type !== "ImportSpecifier")
  247. ) {
  248. return;
  249. }
  250. const trailingToken = getTrailingToken(node, lastItem);
  251. if (astUtils.isCommaToken(trailingToken)) {
  252. context.report({
  253. node: lastItem,
  254. loc: trailingToken.loc,
  255. messageId: "unexpected",
  256. *fix(fixer) {
  257. yield fixer.remove(trailingToken);
  258. /*
  259. * Extend the range of the fix to include surrounding tokens to ensure
  260. * that the element after which the comma is removed stays _last_.
  261. * This intentionally makes conflicts in fix ranges with rules that may be
  262. * adding or removing elements in the same autofix pass.
  263. * https://github.com/eslint/eslint/issues/15660
  264. */
  265. yield fixer.insertTextBefore(
  266. sourceCode.getTokenBefore(trailingToken),
  267. "",
  268. );
  269. yield fixer.insertTextAfter(
  270. sourceCode.getTokenAfter(trailingToken),
  271. "",
  272. );
  273. },
  274. });
  275. }
  276. }
  277. /**
  278. * Reports the last element of a given node if it does not have a trailing
  279. * comma.
  280. *
  281. * If a given node is `ArrayPattern` which has `RestElement`, the trailing
  282. * comma is disallowed, so report if it exists.
  283. * @param {ASTNode} node A node to check. Its type is one of
  284. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  285. * ImportDeclaration, and ExportNamedDeclaration.
  286. * @returns {void}
  287. */
  288. function forceTrailingComma(node) {
  289. const lastItem = getLastItem(node);
  290. if (
  291. !lastItem ||
  292. (node.type === "ImportDeclaration" &&
  293. lastItem.type !== "ImportSpecifier")
  294. ) {
  295. return;
  296. }
  297. if (!isTrailingCommaAllowed(lastItem)) {
  298. forbidTrailingComma(node);
  299. return;
  300. }
  301. const trailingToken = getTrailingToken(node, lastItem);
  302. if (trailingToken.value !== ",") {
  303. context.report({
  304. node: lastItem,
  305. loc: {
  306. start: trailingToken.loc.end,
  307. end: astUtils.getNextLocation(
  308. sourceCode,
  309. trailingToken.loc.end,
  310. ),
  311. },
  312. messageId: "missing",
  313. *fix(fixer) {
  314. yield fixer.insertTextAfter(trailingToken, ",");
  315. /*
  316. * Extend the range of the fix to include surrounding tokens to ensure
  317. * that the element after which the comma is inserted stays _last_.
  318. * This intentionally makes conflicts in fix ranges with rules that may be
  319. * adding or removing elements in the same autofix pass.
  320. * https://github.com/eslint/eslint/issues/15660
  321. */
  322. yield fixer.insertTextBefore(trailingToken, "");
  323. yield fixer.insertTextAfter(
  324. sourceCode.getTokenAfter(trailingToken),
  325. "",
  326. );
  327. },
  328. });
  329. }
  330. }
  331. /**
  332. * If a given node is multiline, reports the last element of a given node
  333. * when it does not have a trailing comma.
  334. * Otherwise, reports a trailing comma if it exists.
  335. * @param {ASTNode} node A node to check. Its type is one of
  336. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  337. * ImportDeclaration, and ExportNamedDeclaration.
  338. * @returns {void}
  339. */
  340. function forceTrailingCommaIfMultiline(node) {
  341. if (isMultiline(node)) {
  342. forceTrailingComma(node);
  343. } else {
  344. forbidTrailingComma(node);
  345. }
  346. }
  347. /**
  348. * Only if a given node is not multiline, reports the last element of a given node
  349. * when it does not have a trailing comma.
  350. * Otherwise, reports a trailing comma if it exists.
  351. * @param {ASTNode} node A node to check. Its type is one of
  352. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  353. * ImportDeclaration, and ExportNamedDeclaration.
  354. * @returns {void}
  355. */
  356. function allowTrailingCommaIfMultiline(node) {
  357. if (!isMultiline(node)) {
  358. forbidTrailingComma(node);
  359. }
  360. }
  361. const predicate = {
  362. always: forceTrailingComma,
  363. "always-multiline": forceTrailingCommaIfMultiline,
  364. "only-multiline": allowTrailingCommaIfMultiline,
  365. never: forbidTrailingComma,
  366. ignore() {},
  367. };
  368. return {
  369. ObjectExpression: predicate[options.objects],
  370. ObjectPattern: predicate[options.objects],
  371. ArrayExpression: predicate[options.arrays],
  372. ArrayPattern: predicate[options.arrays],
  373. ImportDeclaration: predicate[options.imports],
  374. ExportNamedDeclaration: predicate[options.exports],
  375. FunctionDeclaration: predicate[options.functions],
  376. FunctionExpression: predicate[options.functions],
  377. ArrowFunctionExpression: predicate[options.functions],
  378. CallExpression: predicate[options.functions],
  379. NewExpression: predicate[options.functions],
  380. };
  381. },
  382. };