comma-style.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /**
  2. * @fileoverview Comma style - enforces comma styles of two types: last and first
  3. * @author Vignesh Anand aka vegetableman
  4. */
  5. "use strict";
  6. const astUtils = require("../ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Rule Definition
  9. //------------------------------------------------------------------------------
  10. module.exports = {
  11. meta: {
  12. docs: {
  13. description: "enforce consistent comma style",
  14. category: "Stylistic Issues",
  15. recommended: false,
  16. url: "https://eslint.org/docs/rules/comma-style"
  17. },
  18. fixable: "code",
  19. schema: [
  20. {
  21. enum: ["first", "last"]
  22. },
  23. {
  24. type: "object",
  25. properties: {
  26. exceptions: {
  27. type: "object",
  28. additionalProperties: {
  29. type: "boolean"
  30. }
  31. }
  32. },
  33. additionalProperties: false
  34. }
  35. ],
  36. messages: {
  37. unexpectedLineBeforeAndAfterComma: "Bad line breaking before and after ','.",
  38. expectedCommaFirst: "',' should be placed first.",
  39. expectedCommaLast: "',' should be placed last."
  40. }
  41. },
  42. create(context) {
  43. const style = context.options[0] || "last",
  44. sourceCode = context.getSourceCode();
  45. const exceptions = {
  46. ArrayPattern: true,
  47. ArrowFunctionExpression: true,
  48. CallExpression: true,
  49. FunctionDeclaration: true,
  50. FunctionExpression: true,
  51. ImportDeclaration: true,
  52. ObjectPattern: true,
  53. NewExpression: true
  54. };
  55. if (context.options.length === 2 && context.options[1].hasOwnProperty("exceptions")) {
  56. const keys = Object.keys(context.options[1].exceptions);
  57. for (let i = 0; i < keys.length; i++) {
  58. exceptions[keys[i]] = context.options[1].exceptions[keys[i]];
  59. }
  60. }
  61. //--------------------------------------------------------------------------
  62. // Helpers
  63. //--------------------------------------------------------------------------
  64. /**
  65. * Modified text based on the style
  66. * @param {string} styleType Style type
  67. * @param {string} text Source code text
  68. * @returns {string} modified text
  69. * @private
  70. */
  71. function getReplacedText(styleType, text) {
  72. switch (styleType) {
  73. case "between":
  74. return `,${text.replace("\n", "")}`;
  75. case "first":
  76. return `${text},`;
  77. case "last":
  78. return `,${text}`;
  79. default:
  80. return "";
  81. }
  82. }
  83. /**
  84. * Determines the fixer function for a given style.
  85. * @param {string} styleType comma style
  86. * @param {ASTNode} previousItemToken The token to check.
  87. * @param {ASTNode} commaToken The token to check.
  88. * @param {ASTNode} currentItemToken The token to check.
  89. * @returns {Function} Fixer function
  90. * @private
  91. */
  92. function getFixerFunction(styleType, previousItemToken, commaToken, currentItemToken) {
  93. const text =
  94. sourceCode.text.slice(previousItemToken.range[1], commaToken.range[0]) +
  95. sourceCode.text.slice(commaToken.range[1], currentItemToken.range[0]);
  96. const range = [previousItemToken.range[1], currentItemToken.range[0]];
  97. return function(fixer) {
  98. return fixer.replaceTextRange(range, getReplacedText(styleType, text));
  99. };
  100. }
  101. /**
  102. * Validates the spacing around single items in lists.
  103. * @param {Token} previousItemToken The last token from the previous item.
  104. * @param {Token} commaToken The token representing the comma.
  105. * @param {Token} currentItemToken The first token of the current item.
  106. * @param {Token} reportItem The item to use when reporting an error.
  107. * @returns {void}
  108. * @private
  109. */
  110. function validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem) {
  111. // if single line
  112. if (astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
  113. astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
  114. // do nothing.
  115. } else if (!astUtils.isTokenOnSameLine(commaToken, currentItemToken) &&
  116. !astUtils.isTokenOnSameLine(previousItemToken, commaToken)) {
  117. // lone comma
  118. context.report({
  119. node: reportItem,
  120. loc: {
  121. line: commaToken.loc.end.line,
  122. column: commaToken.loc.start.column
  123. },
  124. messageId: "unexpectedLineBeforeAndAfterComma",
  125. fix: getFixerFunction("between", previousItemToken, commaToken, currentItemToken)
  126. });
  127. } else if (style === "first" && !astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
  128. context.report({
  129. node: reportItem,
  130. messageId: "expectedCommaFirst",
  131. fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
  132. });
  133. } else if (style === "last" && astUtils.isTokenOnSameLine(commaToken, currentItemToken)) {
  134. context.report({
  135. node: reportItem,
  136. loc: {
  137. line: commaToken.loc.end.line,
  138. column: commaToken.loc.end.column
  139. },
  140. messageId: "expectedCommaLast",
  141. fix: getFixerFunction(style, previousItemToken, commaToken, currentItemToken)
  142. });
  143. }
  144. }
  145. /**
  146. * Checks the comma placement with regards to a declaration/property/element
  147. * @param {ASTNode} node The binary expression node to check
  148. * @param {string} property The property of the node containing child nodes.
  149. * @private
  150. * @returns {void}
  151. */
  152. function validateComma(node, property) {
  153. const items = node[property],
  154. arrayLiteral = (node.type === "ArrayExpression" || node.type === "ArrayPattern");
  155. if (items.length > 1 || arrayLiteral) {
  156. // seed as opening [
  157. let previousItemToken = sourceCode.getFirstToken(node);
  158. items.forEach(item => {
  159. const commaToken = item ? sourceCode.getTokenBefore(item) : previousItemToken,
  160. currentItemToken = item ? sourceCode.getFirstToken(item) : sourceCode.getTokenAfter(commaToken),
  161. reportItem = item || currentItemToken,
  162. tokenBeforeComma = sourceCode.getTokenBefore(commaToken);
  163. // Check if previous token is wrapped in parentheses
  164. if (tokenBeforeComma && astUtils.isClosingParenToken(tokenBeforeComma)) {
  165. previousItemToken = tokenBeforeComma;
  166. }
  167. /*
  168. * This works by comparing three token locations:
  169. * - previousItemToken is the last token of the previous item
  170. * - commaToken is the location of the comma before the current item
  171. * - currentItemToken is the first token of the current item
  172. *
  173. * These values get switched around if item is undefined.
  174. * previousItemToken will refer to the last token not belonging
  175. * to the current item, which could be a comma or an opening
  176. * square bracket. currentItemToken could be a comma.
  177. *
  178. * All comparisons are done based on these tokens directly, so
  179. * they are always valid regardless of an undefined item.
  180. */
  181. if (astUtils.isCommaToken(commaToken)) {
  182. validateCommaItemSpacing(previousItemToken, commaToken,
  183. currentItemToken, reportItem);
  184. }
  185. if (item) {
  186. const tokenAfterItem = sourceCode.getTokenAfter(item, astUtils.isNotClosingParenToken);
  187. previousItemToken = tokenAfterItem
  188. ? sourceCode.getTokenBefore(tokenAfterItem)
  189. : sourceCode.ast.tokens[sourceCode.ast.tokens.length - 1];
  190. }
  191. });
  192. /*
  193. * Special case for array literals that have empty last items, such
  194. * as [ 1, 2, ]. These arrays only have two items show up in the
  195. * AST, so we need to look at the token to verify that there's no
  196. * dangling comma.
  197. */
  198. if (arrayLiteral) {
  199. const lastToken = sourceCode.getLastToken(node),
  200. nextToLastToken = sourceCode.getTokenBefore(lastToken);
  201. if (astUtils.isCommaToken(nextToLastToken)) {
  202. validateCommaItemSpacing(
  203. sourceCode.getTokenBefore(nextToLastToken),
  204. nextToLastToken,
  205. lastToken,
  206. lastToken
  207. );
  208. }
  209. }
  210. }
  211. }
  212. //--------------------------------------------------------------------------
  213. // Public
  214. //--------------------------------------------------------------------------
  215. const nodes = {};
  216. if (!exceptions.VariableDeclaration) {
  217. nodes.VariableDeclaration = function(node) {
  218. validateComma(node, "declarations");
  219. };
  220. }
  221. if (!exceptions.ObjectExpression) {
  222. nodes.ObjectExpression = function(node) {
  223. validateComma(node, "properties");
  224. };
  225. }
  226. if (!exceptions.ObjectPattern) {
  227. nodes.ObjectPattern = function(node) {
  228. validateComma(node, "properties");
  229. };
  230. }
  231. if (!exceptions.ArrayExpression) {
  232. nodes.ArrayExpression = function(node) {
  233. validateComma(node, "elements");
  234. };
  235. }
  236. if (!exceptions.ArrayPattern) {
  237. nodes.ArrayPattern = function(node) {
  238. validateComma(node, "elements");
  239. };
  240. }
  241. if (!exceptions.FunctionDeclaration) {
  242. nodes.FunctionDeclaration = function(node) {
  243. validateComma(node, "params");
  244. };
  245. }
  246. if (!exceptions.FunctionExpression) {
  247. nodes.FunctionExpression = function(node) {
  248. validateComma(node, "params");
  249. };
  250. }
  251. if (!exceptions.ArrowFunctionExpression) {
  252. nodes.ArrowFunctionExpression = function(node) {
  253. validateComma(node, "params");
  254. };
  255. }
  256. if (!exceptions.CallExpression) {
  257. nodes.CallExpression = function(node) {
  258. validateComma(node, "arguments");
  259. };
  260. }
  261. if (!exceptions.ImportDeclaration) {
  262. nodes.ImportDeclaration = function(node) {
  263. validateComma(node, "specifiers");
  264. };
  265. }
  266. if (!exceptions.NewExpression) {
  267. nodes.NewExpression = function(node) {
  268. validateComma(node, "arguments");
  269. };
  270. }
  271. return nodes;
  272. }
  273. };