UNPKG

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