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