array-element-newline.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /**
  2. * @fileoverview Rule to enforce line breaks after each array element
  3. * @author Jan Peer Stöcklmair <https://github.com/JPeer264>
  4. */
  5. "use strict";
  6. const astUtils = require("./utils/ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Rule Definition
  9. //------------------------------------------------------------------------------
  10. module.exports = {
  11. meta: {
  12. type: "layout",
  13. docs: {
  14. description: "enforce line breaks after each array element",
  15. category: "Stylistic Issues",
  16. recommended: false,
  17. url: "https://eslint.org/docs/rules/array-element-newline"
  18. },
  19. fixable: "whitespace",
  20. schema: [
  21. {
  22. oneOf: [
  23. {
  24. enum: ["always", "never", "consistent"]
  25. },
  26. {
  27. type: "object",
  28. properties: {
  29. multiline: {
  30. type: "boolean"
  31. },
  32. minItems: {
  33. type: ["integer", "null"],
  34. minimum: 0
  35. }
  36. },
  37. additionalProperties: false
  38. }
  39. ]
  40. }
  41. ],
  42. messages: {
  43. unexpectedLineBreak: "There should be no linebreak here.",
  44. missingLineBreak: "There should be a linebreak after this element."
  45. }
  46. },
  47. create(context) {
  48. const sourceCode = context.getSourceCode();
  49. //----------------------------------------------------------------------
  50. // Helpers
  51. //----------------------------------------------------------------------
  52. /**
  53. * Normalizes a given option value.
  54. * @param {string|Object|undefined} providedOption An option value to parse.
  55. * @returns {{multiline: boolean, minItems: number}} Normalized option object.
  56. */
  57. function normalizeOptionValue(providedOption) {
  58. let consistent = false;
  59. let multiline = false;
  60. let minItems;
  61. const option = providedOption || "always";
  62. if (!option || option === "always" || option.minItems === 0) {
  63. minItems = 0;
  64. } else if (option === "never") {
  65. minItems = Number.POSITIVE_INFINITY;
  66. } else if (option === "consistent") {
  67. consistent = true;
  68. minItems = Number.POSITIVE_INFINITY;
  69. } else {
  70. multiline = Boolean(option.multiline);
  71. minItems = option.minItems || Number.POSITIVE_INFINITY;
  72. }
  73. return { consistent, multiline, minItems };
  74. }
  75. /**
  76. * Normalizes a given option value.
  77. * @param {string|Object|undefined} options An option value to parse.
  78. * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
  79. */
  80. function normalizeOptions(options) {
  81. const value = normalizeOptionValue(options);
  82. return { ArrayExpression: value, ArrayPattern: value };
  83. }
  84. /**
  85. * Reports that there shouldn't be a line break after the first token
  86. * @param {Token} token The token to use for the report.
  87. * @returns {void}
  88. */
  89. function reportNoLineBreak(token) {
  90. const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
  91. context.report({
  92. loc: {
  93. start: tokenBefore.loc.end,
  94. end: token.loc.start
  95. },
  96. messageId: "unexpectedLineBreak",
  97. fix(fixer) {
  98. if (astUtils.isCommentToken(tokenBefore)) {
  99. return null;
  100. }
  101. if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
  102. return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], " ");
  103. }
  104. /*
  105. * This will check if the comma is on the same line as the next element
  106. * Following array:
  107. * [
  108. * 1
  109. * , 2
  110. * , 3
  111. * ]
  112. *
  113. * will be fixed to:
  114. * [
  115. * 1, 2, 3
  116. * ]
  117. */
  118. const twoTokensBefore = sourceCode.getTokenBefore(tokenBefore, { includeComments: true });
  119. if (astUtils.isCommentToken(twoTokensBefore)) {
  120. return null;
  121. }
  122. return fixer.replaceTextRange([twoTokensBefore.range[1], tokenBefore.range[0]], "");
  123. }
  124. });
  125. }
  126. /**
  127. * Reports that there should be a line break after the first token
  128. * @param {Token} token The token to use for the report.
  129. * @returns {void}
  130. */
  131. function reportRequiredLineBreak(token) {
  132. const tokenBefore = sourceCode.getTokenBefore(token, { includeComments: true });
  133. context.report({
  134. loc: {
  135. start: tokenBefore.loc.end,
  136. end: token.loc.start
  137. },
  138. messageId: "missingLineBreak",
  139. fix(fixer) {
  140. return fixer.replaceTextRange([tokenBefore.range[1], token.range[0]], "\n");
  141. }
  142. });
  143. }
  144. /**
  145. * Reports a given node if it violated this rule.
  146. * @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node.
  147. * @returns {void}
  148. */
  149. function check(node) {
  150. const elements = node.elements;
  151. const normalizedOptions = normalizeOptions(context.options[0]);
  152. const options = normalizedOptions[node.type];
  153. let elementBreak = false;
  154. /*
  155. * MULTILINE: true
  156. * loop through every element and check
  157. * if at least one element has linebreaks inside
  158. * this ensures that following is not valid (due to elements are on the same line):
  159. *
  160. * [
  161. * 1,
  162. * 2,
  163. * 3
  164. * ]
  165. */
  166. if (options.multiline) {
  167. elementBreak = elements
  168. .filter(element => element !== null)
  169. .some(element => element.loc.start.line !== element.loc.end.line);
  170. }
  171. const linebreaksCount = node.elements.map((element, i) => {
  172. const previousElement = elements[i - 1];
  173. if (i === 0 || element === null || previousElement === null) {
  174. return false;
  175. }
  176. const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
  177. const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
  178. const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
  179. return !astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement);
  180. }).filter(isBreak => isBreak === true).length;
  181. const needsLinebreaks = (
  182. elements.length >= options.minItems ||
  183. (
  184. options.multiline &&
  185. elementBreak
  186. ) ||
  187. (
  188. options.consistent &&
  189. linebreaksCount > 0 &&
  190. linebreaksCount < node.elements.length
  191. )
  192. );
  193. elements.forEach((element, i) => {
  194. const previousElement = elements[i - 1];
  195. if (i === 0 || element === null || previousElement === null) {
  196. return;
  197. }
  198. const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken);
  199. const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken);
  200. const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken);
  201. if (needsLinebreaks) {
  202. if (astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
  203. reportRequiredLineBreak(firstTokenOfCurrentElement);
  204. }
  205. } else {
  206. if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) {
  207. reportNoLineBreak(firstTokenOfCurrentElement);
  208. }
  209. }
  210. });
  211. }
  212. //----------------------------------------------------------------------
  213. // Public
  214. //----------------------------------------------------------------------
  215. return {
  216. ArrayPattern: check,
  217. ArrayExpression: check
  218. };
  219. }
  220. };