blob: b4b4dd430f65644a5d1b03f69c1bee189f9c6df4 [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
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"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 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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010068 * @param {string|Object|undefined} option An option value to parse.
Yang Guo4fd355c2019-09-19 10:59:03 +020069 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010099 * @param {string|Object|undefined} options An option value to parse.
Yang Guo4fd355c2019-09-19 10:59:03 +0200100 * @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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100110 * @param {ASTNode} node The node to report in the event of an error.
111 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200112 * @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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100133 * @param {ASTNode} node The node to report in the event of an error.
134 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200135 * @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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100156 * @param {ASTNode} node The node to report in the event of an error.
157 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200158 * @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
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100173 * @param {ASTNode} node The node to report in the event of an error.
174 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200175 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100190 * @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node.
Yang Guo4fd355c2019-09-19 10:59:03 +0200191 * @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 &&
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100219 openBracket.loc.end.line !== first.loc.start.line
Yang Guo4fd355c2019-09-19 10:59:03 +0200220 )
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};