array-element-newline.js 11 KB

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