blob: 28a05b3504308809154b3d238ba1e3fa14a49cfc [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",
Yang Guo4fd355c2019-09-19 10:59:03 +020020 recommended: false,
21 url: "https://eslint.org/docs/rules/array-bracket-newline"
22 },
23
24 fixable: "whitespace",
25
26 schema: [
27 {
28 oneOf: [
29 {
30 enum: ["always", "never", "consistent"]
31 },
32 {
33 type: "object",
34 properties: {
35 multiline: {
36 type: "boolean"
37 },
38 minItems: {
39 type: ["integer", "null"],
40 minimum: 0
41 }
42 },
43 additionalProperties: false
44 }
45 ]
46 }
47 ],
48
49 messages: {
50 unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
51 unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
52 missingOpeningLinebreak: "A linebreak is required after '['.",
53 missingClosingLinebreak: "A linebreak is required before ']'."
54 }
55 },
56
57 create(context) {
58 const sourceCode = context.getSourceCode();
59
60
61 //----------------------------------------------------------------------
62 // Helpers
63 //----------------------------------------------------------------------
64
65 /**
66 * Normalizes a given option value.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010067 * @param {string|Object|undefined} option An option value to parse.
Yang Guo4fd355c2019-09-19 10:59:03 +020068 * @returns {{multiline: boolean, minItems: number}} Normalized option object.
69 */
70 function normalizeOptionValue(option) {
71 let consistent = false;
72 let multiline = false;
73 let minItems = 0;
74
75 if (option) {
76 if (option === "consistent") {
77 consistent = true;
78 minItems = Number.POSITIVE_INFINITY;
79 } else if (option === "always" || option.minItems === 0) {
80 minItems = 0;
81 } else if (option === "never") {
82 minItems = Number.POSITIVE_INFINITY;
83 } else {
84 multiline = Boolean(option.multiline);
85 minItems = option.minItems || Number.POSITIVE_INFINITY;
86 }
87 } else {
88 consistent = false;
89 multiline = true;
90 minItems = Number.POSITIVE_INFINITY;
91 }
92
93 return { consistent, multiline, minItems };
94 }
95
96 /**
97 * Normalizes a given option value.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010098 * @param {string|Object|undefined} options An option value to parse.
Yang Guo4fd355c2019-09-19 10:59:03 +020099 * @returns {{ArrayExpression: {multiline: boolean, minItems: number}, ArrayPattern: {multiline: boolean, minItems: number}}} Normalized option object.
100 */
101 function normalizeOptions(options) {
102 const value = normalizeOptionValue(options);
103
104 return { ArrayExpression: value, ArrayPattern: value };
105 }
106
107 /**
108 * Reports that there shouldn't be a linebreak after the first token
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100109 * @param {ASTNode} node The node to report in the event of an error.
110 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200111 * @returns {void}
112 */
113 function reportNoBeginningLinebreak(node, token) {
114 context.report({
115 node,
116 loc: token.loc,
117 messageId: "unexpectedOpeningLinebreak",
118 fix(fixer) {
119 const nextToken = sourceCode.getTokenAfter(token, { includeComments: true });
120
121 if (astUtils.isCommentToken(nextToken)) {
122 return null;
123 }
124
125 return fixer.removeRange([token.range[1], nextToken.range[0]]);
126 }
127 });
128 }
129
130 /**
131 * Reports that there shouldn't be a linebreak before the last token
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100132 * @param {ASTNode} node The node to report in the event of an error.
133 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200134 * @returns {void}
135 */
136 function reportNoEndingLinebreak(node, token) {
137 context.report({
138 node,
139 loc: token.loc,
140 messageId: "unexpectedClosingLinebreak",
141 fix(fixer) {
142 const previousToken = sourceCode.getTokenBefore(token, { includeComments: true });
143
144 if (astUtils.isCommentToken(previousToken)) {
145 return null;
146 }
147
148 return fixer.removeRange([previousToken.range[1], token.range[0]]);
149 }
150 });
151 }
152
153 /**
154 * Reports that there should be a linebreak after the first token
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100155 * @param {ASTNode} node The node to report in the event of an error.
156 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200157 * @returns {void}
158 */
159 function reportRequiredBeginningLinebreak(node, token) {
160 context.report({
161 node,
162 loc: token.loc,
163 messageId: "missingOpeningLinebreak",
164 fix(fixer) {
165 return fixer.insertTextAfter(token, "\n");
166 }
167 });
168 }
169
170 /**
171 * Reports that there should be a linebreak before the last token
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100172 * @param {ASTNode} node The node to report in the event of an error.
173 * @param {Token} token The token to use for the report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200174 * @returns {void}
175 */
176 function reportRequiredEndingLinebreak(node, token) {
177 context.report({
178 node,
179 loc: token.loc,
180 messageId: "missingClosingLinebreak",
181 fix(fixer) {
182 return fixer.insertTextBefore(token, "\n");
183 }
184 });
185 }
186
187 /**
188 * Reports a given node if it violated this rule.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100189 * @param {ASTNode} node A node to check. This is an ArrayExpression node or an ArrayPattern node.
Yang Guo4fd355c2019-09-19 10:59:03 +0200190 * @returns {void}
191 */
192 function check(node) {
193 const elements = node.elements;
194 const normalizedOptions = normalizeOptions(context.options[0]);
195 const options = normalizedOptions[node.type];
196 const openBracket = sourceCode.getFirstToken(node);
197 const closeBracket = sourceCode.getLastToken(node);
198 const firstIncComment = sourceCode.getTokenAfter(openBracket, { includeComments: true });
199 const lastIncComment = sourceCode.getTokenBefore(closeBracket, { includeComments: true });
200 const first = sourceCode.getTokenAfter(openBracket);
201 const last = sourceCode.getTokenBefore(closeBracket);
202
203 const needsLinebreaks = (
204 elements.length >= options.minItems ||
205 (
206 options.multiline &&
207 elements.length > 0 &&
208 firstIncComment.loc.start.line !== lastIncComment.loc.end.line
209 ) ||
210 (
211 elements.length === 0 &&
212 firstIncComment.type === "Block" &&
213 firstIncComment.loc.start.line !== lastIncComment.loc.end.line &&
214 firstIncComment === lastIncComment
215 ) ||
216 (
217 options.consistent &&
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100218 openBracket.loc.end.line !== first.loc.start.line
Yang Guo4fd355c2019-09-19 10:59:03 +0200219 )
220 );
221
222 /*
223 * Use tokens or comments to check multiline or not.
224 * But use only tokens to check whether linebreaks are needed.
225 * This allows:
226 * var arr = [ // eslint-disable-line foo
227 * 'a'
228 * ]
229 */
230
231 if (needsLinebreaks) {
232 if (astUtils.isTokenOnSameLine(openBracket, first)) {
233 reportRequiredBeginningLinebreak(node, openBracket);
234 }
235 if (astUtils.isTokenOnSameLine(last, closeBracket)) {
236 reportRequiredEndingLinebreak(node, closeBracket);
237 }
238 } else {
239 if (!astUtils.isTokenOnSameLine(openBracket, first)) {
240 reportNoBeginningLinebreak(node, openBracket);
241 }
242 if (!astUtils.isTokenOnSameLine(last, closeBracket)) {
243 reportNoEndingLinebreak(node, closeBracket);
244 }
245 }
246 }
247
248 //----------------------------------------------------------------------
249 // Public
250 //----------------------------------------------------------------------
251
252 return {
253 ArrayPattern: check,
254 ArrayExpression: check
255 };
256 }
257};