You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

341 lines
12 KiB

4 years ago
  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. additionalItems: false
  116. },
  117. messages: {
  118. unexpected: "Unexpected trailing comma.",
  119. missing: "Missing trailing comma."
  120. }
  121. },
  122. create(context) {
  123. const options = normalizeOptions(context.options[0], context.parserOptions.ecmaVersion);
  124. const sourceCode = context.getSourceCode();
  125. /**
  126. * Gets the last item of the given node.
  127. * @param {ASTNode} node The node to get.
  128. * @returns {ASTNode|null} The last node or null.
  129. */
  130. function getLastItem(node) {
  131. switch (node.type) {
  132. case "ObjectExpression":
  133. case "ObjectPattern":
  134. return lodash.last(node.properties);
  135. case "ArrayExpression":
  136. case "ArrayPattern":
  137. return lodash.last(node.elements);
  138. case "ImportDeclaration":
  139. case "ExportNamedDeclaration":
  140. return lodash.last(node.specifiers);
  141. case "FunctionDeclaration":
  142. case "FunctionExpression":
  143. case "ArrowFunctionExpression":
  144. return lodash.last(node.params);
  145. case "CallExpression":
  146. case "NewExpression":
  147. return lodash.last(node.arguments);
  148. default:
  149. return null;
  150. }
  151. }
  152. /**
  153. * Gets the trailing comma token of the given node.
  154. * If the trailing comma does not exist, this returns the token which is
  155. * the insertion point of the trailing comma token.
  156. * @param {ASTNode} node The node to get.
  157. * @param {ASTNode} lastItem The last item of the node.
  158. * @returns {Token} The trailing comma token or the insertion point.
  159. */
  160. function getTrailingToken(node, lastItem) {
  161. switch (node.type) {
  162. case "ObjectExpression":
  163. case "ArrayExpression":
  164. case "CallExpression":
  165. case "NewExpression":
  166. return sourceCode.getLastToken(node, 1);
  167. default: {
  168. const nextToken = sourceCode.getTokenAfter(lastItem);
  169. if (astUtils.isCommaToken(nextToken)) {
  170. return nextToken;
  171. }
  172. return sourceCode.getLastToken(lastItem);
  173. }
  174. }
  175. }
  176. /**
  177. * Checks whether or not a given node is multiline.
  178. * This rule handles a given node as multiline when the closing parenthesis
  179. * and the last element are not on the same line.
  180. * @param {ASTNode} node A node to check.
  181. * @returns {boolean} `true` if the node is multiline.
  182. */
  183. function isMultiline(node) {
  184. const lastItem = getLastItem(node);
  185. if (!lastItem) {
  186. return false;
  187. }
  188. const penultimateToken = getTrailingToken(node, lastItem);
  189. const lastToken = sourceCode.getTokenAfter(penultimateToken);
  190. return lastToken.loc.end.line !== penultimateToken.loc.end.line;
  191. }
  192. /**
  193. * Reports a trailing comma if it exists.
  194. * @param {ASTNode} node A node to check. Its type is one of
  195. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  196. * ImportDeclaration, and ExportNamedDeclaration.
  197. * @returns {void}
  198. */
  199. function forbidTrailingComma(node) {
  200. const lastItem = getLastItem(node);
  201. if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
  202. return;
  203. }
  204. const trailingToken = getTrailingToken(node, lastItem);
  205. if (astUtils.isCommaToken(trailingToken)) {
  206. context.report({
  207. node: lastItem,
  208. loc: trailingToken.loc,
  209. messageId: "unexpected",
  210. fix(fixer) {
  211. return fixer.remove(trailingToken);
  212. }
  213. });
  214. }
  215. }
  216. /**
  217. * Reports the last element of a given node if it does not have a trailing
  218. * comma.
  219. *
  220. * If a given node is `ArrayPattern` which has `RestElement`, the trailing
  221. * comma is disallowed, so report if it exists.
  222. * @param {ASTNode} node A node to check. Its type is one of
  223. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  224. * ImportDeclaration, and ExportNamedDeclaration.
  225. * @returns {void}
  226. */
  227. function forceTrailingComma(node) {
  228. const lastItem = getLastItem(node);
  229. if (!lastItem || (node.type === "ImportDeclaration" && lastItem.type !== "ImportSpecifier")) {
  230. return;
  231. }
  232. if (!isTrailingCommaAllowed(lastItem)) {
  233. forbidTrailingComma(node);
  234. return;
  235. }
  236. const trailingToken = getTrailingToken(node, lastItem);
  237. if (trailingToken.value !== ",") {
  238. context.report({
  239. node: lastItem,
  240. loc: {
  241. start: trailingToken.loc.end,
  242. end: astUtils.getNextLocation(sourceCode, trailingToken.loc.end)
  243. },
  244. messageId: "missing",
  245. fix(fixer) {
  246. return fixer.insertTextAfter(trailingToken, ",");
  247. }
  248. });
  249. }
  250. }
  251. /**
  252. * If a given node is multiline, reports the last element of a given node
  253. * when it does not have a trailing comma.
  254. * Otherwise, reports a trailing comma if it exists.
  255. * @param {ASTNode} node A node to check. Its type is one of
  256. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  257. * ImportDeclaration, and ExportNamedDeclaration.
  258. * @returns {void}
  259. */
  260. function forceTrailingCommaIfMultiline(node) {
  261. if (isMultiline(node)) {
  262. forceTrailingComma(node);
  263. } else {
  264. forbidTrailingComma(node);
  265. }
  266. }
  267. /**
  268. * Only if a given node is not multiline, reports the last element of a given node
  269. * when it does not have a trailing comma.
  270. * Otherwise, reports a trailing comma if it exists.
  271. * @param {ASTNode} node A node to check. Its type is one of
  272. * ObjectExpression, ObjectPattern, ArrayExpression, ArrayPattern,
  273. * ImportDeclaration, and ExportNamedDeclaration.
  274. * @returns {void}
  275. */
  276. function allowTrailingCommaIfMultiline(node) {
  277. if (!isMultiline(node)) {
  278. forbidTrailingComma(node);
  279. }
  280. }
  281. const predicate = {
  282. always: forceTrailingComma,
  283. "always-multiline": forceTrailingCommaIfMultiline,
  284. "only-multiline": allowTrailingCommaIfMultiline,
  285. never: forbidTrailingComma,
  286. ignore: lodash.noop
  287. };
  288. return {
  289. ObjectExpression: predicate[options.objects],
  290. ObjectPattern: predicate[options.objects],
  291. ArrayExpression: predicate[options.arrays],
  292. ArrayPattern: predicate[options.arrays],
  293. ImportDeclaration: predicate[options.imports],
  294. ExportNamedDeclaration: predicate[options.exports],
  295. FunctionDeclaration: predicate[options.functions],
  296. FunctionExpression: predicate[options.functions],
  297. ArrowFunctionExpression: predicate[options.functions],
  298. CallExpression: predicate[options.functions],
  299. NewExpression: predicate[options.functions]
  300. };
  301. }
  302. };