array-element-newline.js 11 KB

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