array-element-newline.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /**
  2. * @fileoverview Rule to enforce line breaks after each array element
  3. * @author Jan Peer Stöcklmair <https://github.com/JPeer264>
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. const astUtils = require("./utils/ast-utils");
  8. //------------------------------------------------------------------------------
  9. // Rule Definition
  10. //------------------------------------------------------------------------------
  11. /** @type {import('../types').Rule.RuleModule} */
  12. module.exports = {
  13. meta: {
  14. deprecated: {
  15. message: "Formatting rules are being moved out of ESLint core.",
  16. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  17. deprecatedSince: "8.53.0",
  18. availableUntil: "11.0.0",
  19. replacedBy: [
  20. {
  21. message:
  22. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  23. url: "https://eslint.style/guide/migration",
  24. plugin: {
  25. name: "@stylistic/eslint-plugin",
  26. url: "https://eslint.style",
  27. },
  28. rule: {
  29. name: "array-element-newline",
  30. url: "https://eslint.style/rules/array-element-newline",
  31. },
  32. },
  33. ],
  34. },
  35. type: "layout",
  36. docs: {
  37. description: "Enforce line breaks after each array element",
  38. recommended: false,
  39. url: "https://eslint.org/docs/latest/rules/array-element-newline",
  40. },
  41. fixable: "whitespace",
  42. schema: {
  43. definitions: {
  44. basicConfig: {
  45. oneOf: [
  46. {
  47. enum: ["always", "never", "consistent"],
  48. },
  49. {
  50. type: "object",
  51. properties: {
  52. multiline: {
  53. type: "boolean",
  54. },
  55. minItems: {
  56. type: ["integer", "null"],
  57. minimum: 0,
  58. },
  59. },
  60. additionalProperties: false,
  61. },
  62. ],
  63. },
  64. },
  65. type: "array",
  66. items: [
  67. {
  68. oneOf: [
  69. {
  70. $ref: "#/definitions/basicConfig",
  71. },
  72. {
  73. type: "object",
  74. properties: {
  75. ArrayExpression: {
  76. $ref: "#/definitions/basicConfig",
  77. },
  78. ArrayPattern: {
  79. $ref: "#/definitions/basicConfig",
  80. },
  81. },
  82. additionalProperties: false,
  83. minProperties: 1,
  84. },
  85. ],
  86. },
  87. ],
  88. },
  89. messages: {
  90. unexpectedLineBreak: "There should be no linebreak here.",
  91. missingLineBreak: "There should be a linebreak after this element.",
  92. },
  93. },
  94. create(context) {
  95. const sourceCode = context.sourceCode;
  96. //----------------------------------------------------------------------
  97. // Helpers
  98. //----------------------------------------------------------------------
  99. /**
  100. * Normalizes a given option value.
  101. * @param {string|Object|undefined} providedOption An option value to parse.
  102. * @returns {{multiline: boolean, minItems: number}} Normalized option object.
  103. */
  104. function normalizeOptionValue(providedOption) {
  105. let consistent = false;
  106. let multiline = false;
  107. let minItems;
  108. const option = providedOption || "always";
  109. if (!option || option === "always" || option.minItems === 0) {
  110. minItems = 0;
  111. } else if (option === "never") {
  112. minItems = Number.POSITIVE_INFINITY;
  113. } else if (option === "consistent") {
  114. consistent = true;
  115. minItems = Number.POSITIVE_INFINITY;
  116. } else {
  117. multiline = Boolean(option.multiline);
  118. minItems = option.minItems || Number.POSITIVE_INFINITY;
  119. }
  120. return { consistent, multiline, minItems };
  121. }
  122. /**
  123. * Normalizes a given option value.
  124. * @param {string|Object|undefined} options An option value to parse.
  125. * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
  126. */
  127. function normalizeOptions(options) {
  128. if (options && (options.ArrayExpression || options.ArrayPattern)) {
  129. let expressionOptions, patternOptions;
  130. if (options.ArrayExpression) {
  131. expressionOptions = normalizeOptionValue(
  132. options.ArrayExpression,
  133. );
  134. }
  135. if (options.ArrayPattern) {
  136. patternOptions = normalizeOptionValue(options.ArrayPattern);
  137. }
  138. return {
  139. ArrayExpression: expressionOptions,
  140. ArrayPattern: patternOptions,
  141. };
  142. }
  143. const value = normalizeOptionValue(options);
  144. return { ArrayExpression: value, ArrayPattern: value };
  145. }
  146. /**
  147. * Reports that there shouldn't be a line break after the first token
  148. * @param {Token} token The token to use for the report.
  149. * @returns {void}
  150. */
  151. function reportNoLineBreak(token) {
  152. const tokenBefore = sourceCode.getTokenBefore(token, {
  153. includeComments: true,
  154. });
  155. context.report({
  156. loc: {
  157. start: tokenBefore.loc.end,
  158. end: token.loc.start,
  159. },
  160. messageId: "unexpectedLineBreak",
  161. fix(fixer) {
  162. if (astUtils.isCommentToken(tokenBefore)) {
  163. return null;
  164. }
  165. if (!astUtils.isTokenOnSameLine(tokenBefore, token)) {
  166. return fixer.replaceTextRange(
  167. [tokenBefore.range[1], token.range[0]],
  168. " ",
  169. );
  170. }
  171. /*
  172. * This will check if the comma is on the same line as the next element
  173. * Following array:
  174. * [
  175. * 1
  176. * , 2
  177. * , 3
  178. * ]
  179. *
  180. * will be fixed to:
  181. * [
  182. * 1, 2, 3
  183. * ]
  184. */
  185. const twoTokensBefore = sourceCode.getTokenBefore(
  186. tokenBefore,
  187. { includeComments: true },
  188. );
  189. if (astUtils.isCommentToken(twoTokensBefore)) {
  190. return null;
  191. }
  192. return fixer.replaceTextRange(
  193. [twoTokensBefore.range[1], tokenBefore.range[0]],
  194. "",
  195. );
  196. },
  197. });
  198. }
  199. /**
  200. * Reports that there should be a line break after the first token
  201. * @param {Token} token The token to use for the report.
  202. * @returns {void}
  203. */
  204. function reportRequiredLineBreak(token) {
  205. const tokenBefore = sourceCode.getTokenBefore(token, {
  206. includeComments: true,
  207. });
  208. context.report({
  209. loc: {
  210. start: tokenBefore.loc.end,
  211. end: token.loc.start,
  212. },
  213. messageId: "missingLineBreak",
  214. fix(fixer) {
  215. return fixer.replaceTextRange(
  216. [tokenBefore.range[1], token.range[0]],
  217. "\n",
  218. );
  219. },
  220. });
  221. }
  222. /**
  223. * Reports a given node if it violated this rule.
  224. * @param {ASTNode} node A node to check. This is an ObjectExpression node or an ObjectPattern node.
  225. * @returns {void}
  226. */
  227. function check(node) {
  228. const elements = node.elements;
  229. const normalizedOptions = normalizeOptions(context.options[0]);
  230. const options = normalizedOptions[node.type];
  231. if (!options) {
  232. return;
  233. }
  234. let elementBreak = false;
  235. /*
  236. * MULTILINE: true
  237. * loop through every element and check
  238. * if at least one element has linebreaks inside
  239. * this ensures that following is not valid (due to elements are on the same line):
  240. *
  241. * [
  242. * 1,
  243. * 2,
  244. * 3
  245. * ]
  246. */
  247. if (options.multiline) {
  248. elementBreak = elements
  249. .filter(element => element !== null)
  250. .some(
  251. element =>
  252. element.loc.start.line !== element.loc.end.line,
  253. );
  254. }
  255. let linebreaksCount = 0;
  256. for (let i = 0; i < node.elements.length; i++) {
  257. const element = node.elements[i];
  258. const previousElement = elements[i - 1];
  259. if (i === 0 || element === null || previousElement === null) {
  260. continue;
  261. }
  262. const commaToken = sourceCode.getFirstTokenBetween(
  263. previousElement,
  264. element,
  265. astUtils.isCommaToken,
  266. );
  267. const lastTokenOfPreviousElement =
  268. sourceCode.getTokenBefore(commaToken);
  269. const firstTokenOfCurrentElement =
  270. sourceCode.getTokenAfter(commaToken);
  271. if (
  272. !astUtils.isTokenOnSameLine(
  273. lastTokenOfPreviousElement,
  274. firstTokenOfCurrentElement,
  275. )
  276. ) {
  277. linebreaksCount++;
  278. }
  279. }
  280. const needsLinebreaks =
  281. elements.length >= options.minItems ||
  282. (options.multiline && elementBreak) ||
  283. (options.consistent &&
  284. linebreaksCount > 0 &&
  285. linebreaksCount < node.elements.length);
  286. elements.forEach((element, i) => {
  287. const previousElement = elements[i - 1];
  288. if (i === 0 || element === null || previousElement === null) {
  289. return;
  290. }
  291. const commaToken = sourceCode.getFirstTokenBetween(
  292. previousElement,
  293. element,
  294. astUtils.isCommaToken,
  295. );
  296. const lastTokenOfPreviousElement =
  297. sourceCode.getTokenBefore(commaToken);
  298. const firstTokenOfCurrentElement =
  299. sourceCode.getTokenAfter(commaToken);
  300. if (needsLinebreaks) {
  301. if (
  302. astUtils.isTokenOnSameLine(
  303. lastTokenOfPreviousElement,
  304. firstTokenOfCurrentElement,
  305. )
  306. ) {
  307. reportRequiredLineBreak(firstTokenOfCurrentElement);
  308. }
  309. } else {
  310. if (
  311. !astUtils.isTokenOnSameLine(
  312. lastTokenOfPreviousElement,
  313. firstTokenOfCurrentElement,
  314. )
  315. ) {
  316. reportNoLineBreak(firstTokenOfCurrentElement);
  317. }
  318. }
  319. });
  320. }
  321. //----------------------------------------------------------------------
  322. // Public
  323. //----------------------------------------------------------------------
  324. return {
  325. ArrayPattern: check,
  326. ArrayExpression: check,
  327. };
  328. },
  329. };