1 | /**
|
2 | * @fileoverview Rule to enforce linebreaks after open and before close array brackets
|
3 | * @author Jan Peer Stöcklmair <https://github.com/JPeer264>
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | const astUtils = require("./utils/ast-utils");
|
9 |
|
10 | //------------------------------------------------------------------------------
|
11 | // Rule Definition
|
12 | //------------------------------------------------------------------------------
|
13 |
|
14 | module.exports = {
|
15 | meta: {
|
16 | type: "layout",
|
17 |
|
18 | docs: {
|
19 | description: "enforce linebreaks after opening and before closing array brackets",
|
20 | category: "Stylistic Issues",
|
21 | recommended: false,
|
22 | url: "https://eslint.org/docs/rules/array-bracket-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 | unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
|
52 | unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
|
53 | missingOpeningLinebreak: "A linebreak is required after '['.",
|
54 | missingClosingLinebreak: "A linebreak is required before ']'."
|
55 | }
|
56 | },
|
57 |
|
58 | create(context) {
|
59 | const sourceCode = context.getSourceCode();
|
60 |
|
61 |
|
62 | //----------------------------------------------------------------------
|
63 | // Helpers
|
64 | //----------------------------------------------------------------------
|
65 |
|
66 | /**
|
67 | * Normalizes a given option value.
|
68 | * @param {string|Object|undefined} option An option value to parse.
|
69 | * @returns {{multiline: boolean, minItems: number}} Normalized option object.
|
70 | */
|
71 | function normalizeOptionValue(option) {
|
72 | let consistent = false;
|
73 | let multiline = false;
|
74 | let minItems = 0;
|
75 |
|
76 | if (option) {
|
77 | if (option === "consistent") {
|
78 | consistent = true;
|
79 | minItems = Number.POSITIVE_INFINITY;
|
80 | } else if (option === "always" || option.minItems === 0) {
|
81 | minItems = 0;
|
82 | } else if (option === "never") {
|
83 | minItems = Number.POSITIVE_INFINITY;
|
84 | } else {
|
85 | multiline = Boolean(option.multiline);
|
86 | minItems = option.minItems || Number.POSITIVE_INFINITY;
|
87 | }
|
88 | } else {
|
89 | consistent = false;
|
90 | multiline = true;
|
91 | minItems = Number.POSITIVE_INFINITY;
|
92 | }
|
93 |
|
94 | return { consistent, multiline, minItems };
|
95 | }
|
96 |
|
97 | /**
|
98 | * Normalizes a given option value.
|
99 | * @param {string|Object|undefined} options An option value to parse.
|
100 | * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
|
101 | */
|
102 | function normalizeOptions(options) {
|
103 | const value = normalizeOptionValue(options);
|
104 |
|
105 | return { ArrayExpression: value, ArrayPattern: value };
|
106 | }
|
107 |
|
108 | /**
|
109 | * Reports that there shouldn't be a linebreak after the first token
|
110 | * @param {ASTNode} node The node to report in the event of an error.
|
111 | * @param {Token} token The token to use for the report.
|
112 | * @returns {void}
|
113 | */
|
114 | function reportNoBeginningLinebreak(node, token) {
|
115 | context.report({
|
116 | node,
|
117 | loc: token.loc,
|
118 | messageId: "unexpectedOpeningLinebreak",
|
119 | fix(fixer) {
|
120 | const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
|
121 |
|
122 | if (astUtils.isCommentToken(nextToken)) {
|
123 | return null;
|
124 | }
|
125 |
|
126 | return fixer.removeRange([token.range[1], nextToken.range[0]]);
|
127 | }
|
128 | });
|
129 | }
|
130 |
|
131 | /**
|
132 | * Reports that there shouldn't be a linebreak before the last token
|
133 | * @param {ASTNode} node The node to report in the event of an error.
|
134 | * @param {Token} token The token to use for the report.
|
135 | * @returns {void}
|
136 | */
|
137 | function reportNoEndingLinebreak(node, token) {
|
138 | context.report({
|
139 | node,
|
140 | loc: token.loc,
|
141 | messageId: "unexpectedClosingLinebreak",
|
142 | fix(fixer) {
|
143 | const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
|
144 |
|
145 | if (astUtils.isCommentToken(previousToken)) {
|
146 | return null;
|
147 | }
|
148 |
|
149 | return fixer.removeRange([previousToken.range[1], token.range[0]]);
|
150 | }
|
151 | });
|
152 | }
|
153 |
|
154 | /**
|
155 | * Reports that there should be a linebreak after the first token
|
156 | * @param {ASTNode} node The node to report in the event of an error.
|
157 | * @param {Token} token The token to use for the report.
|
158 | * @returns {void}
|
159 | */
|
160 | function reportRequiredBeginningLinebreak(node, token) {
|
161 | context.report({
|
162 | node,
|
163 | loc: token.loc,
|
164 | messageId: "missingOpeningLinebreak",
|
165 | fix(fixer) {
|
166 | return fixer.insertTextAfter(token, "\n");
|
167 | }
|
168 | });
|
169 | }
|
170 |
|
171 | /**
|
172 | * Reports that there should be a linebreak before the last token
|
173 | * @param {ASTNode} node The node to report in the event of an error.
|
174 | * @param {Token} token The token to use for the report.
|
175 | * @returns {void}
|
176 | */
|
177 | function reportRequiredEndingLinebreak(node, token) {
|
178 | context.report({
|
179 | node,
|
180 | loc: token.loc,
|
181 | messageId: "missingClosingLinebreak",
|
182 | fix(fixer) {
|
183 | return fixer.insertTextBefore(token, "\n");
|
184 | }
|
185 | });
|
186 | }
|
187 |
|
188 | /**
|
189 | * Reports a given node if it violated this rule.
|
190 | * @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node.
|
191 | * @returns {void}
|
192 | */
|
193 | function check(node) {
|
194 | const elements = node.elements;
|
195 | const normalizedOptions = normalizeOptions(context.options[0]);
|
196 | const options = normalizedOptions[node.type];
|
197 | const openBracket = sourceCode.getFirstToken(node);
|
198 | const closeBracket = sourceCode.getLastToken(node);
|
199 | const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true });
|
200 | const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true });
|
201 | const first = sourceCode.getTokenAfter(openBracket);
|
202 | const last = sourceCode.getTokenBefore(closeBracket);
|
203 |
|
204 | const needsLinebreaks = (
|
205 | elements.length >= options.minItems ||
|
206 | (
|
207 | options.multiline &&
|
208 | elements.length > 0 &&
|
209 | firstIncComment.loc.start.line !== lastIncComment.loc.end.line
|
210 | ) ||
|
211 | (
|
212 | elements.length === 0 &&
|
213 | firstIncComment.type === "Block" &&
|
214 | firstIncComment.loc.start.line !== lastIncComment.loc.end.line &&
|
215 | firstIncComment === lastIncComment
|
216 | ) ||
|
217 | (
|
218 | options.consistent &&
|
219 | openBracket.loc.end.line !== first.loc.start.line
|
220 | )
|
221 | );
|
222 |
|
223 | /*
|
224 | * Use tokens or comments to check multiline or not.
|
225 | * But use only tokens to check whether linebreaks are needed.
|
226 | * This allows:
|
227 | * var arr = [ // eslint-disable-line foo
|
228 | * 'a'
|
229 | * ]
|
230 | */
|
231 |
|
232 | if (needsLinebreaks) {
|
233 | if (astUtils.isTokenOnSameLine(openBracket, first)) {
|
234 | reportRequiredBeginningLinebreak(node, openBracket);
|
235 | }
|
236 | if (astUtils.isTokenOnSameLine(last, closeBracket)) {
|
237 | reportRequiredEndingLinebreak(node, closeBracket);
|
238 | }
|
239 | } else {
|
240 | if (!astUtils.isTokenOnSameLine(openBracket, first)) {
|
241 | reportNoBeginningLinebreak(node, openBracket);
|
242 | }
|
243 | if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
|
244 | reportNoEndingLinebreak(node, closeBracket);
|
245 | }
|
246 | }
|
247 | }
|
248 |
|
249 | //----------------------------------------------------------------------
|
250 | // Public
|
251 | //----------------------------------------------------------------------
|
252 |
|
253 | return {
|
254 | ArrayPattern: check,
|
255 | ArrayExpression: check
|
256 | };
|
257 | }
|
258 | };
|