array-element-newline.js 8.2 KB

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