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