]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to require or disallow line breaks inside braces. | |
3 | * @author Toru Nagashima | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
eb39fafa DC |
13 | |
14 | //------------------------------------------------------------------------------ | |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | // Schema objects. | |
19 | const OPTION_VALUE = { | |
20 | oneOf: [ | |
21 | { | |
22 | enum: ["always", "never"] | |
23 | }, | |
24 | { | |
25 | type: "object", | |
26 | properties: { | |
27 | multiline: { | |
28 | type: "boolean" | |
29 | }, | |
30 | minProperties: { | |
31 | type: "integer", | |
32 | minimum: 0 | |
33 | }, | |
34 | consistent: { | |
35 | type: "boolean" | |
36 | } | |
37 | }, | |
38 | additionalProperties: false, | |
39 | minProperties: 1 | |
40 | } | |
41 | ] | |
42 | }; | |
43 | ||
44 | /** | |
45 | * Normalizes a given option value. | |
46 | * @param {string|Object|undefined} value An option value to parse. | |
47 | * @returns {{multiline: boolean, minProperties: number, consistent: boolean}} Normalized option object. | |
48 | */ | |
49 | function normalizeOptionValue(value) { | |
50 | let multiline = false; | |
51 | let minProperties = Number.POSITIVE_INFINITY; | |
52 | let consistent = false; | |
53 | ||
54 | if (value) { | |
55 | if (value === "always") { | |
56 | minProperties = 0; | |
57 | } else if (value === "never") { | |
58 | minProperties = Number.POSITIVE_INFINITY; | |
59 | } else { | |
60 | multiline = Boolean(value.multiline); | |
61 | minProperties = value.minProperties || Number.POSITIVE_INFINITY; | |
62 | consistent = Boolean(value.consistent); | |
63 | } | |
64 | } else { | |
65 | consistent = true; | |
66 | } | |
67 | ||
68 | return { multiline, minProperties, consistent }; | |
69 | } | |
70 | ||
5422a9cc TL |
71 | /** |
72 | * Checks if a value is an object. | |
73 | * @param {any} value The value to check | |
74 | * @returns {boolean} `true` if the value is an object, otherwise `false` | |
75 | */ | |
76 | function isObject(value) { | |
77 | return typeof value === "object" && value !== null; | |
78 | } | |
79 | ||
80 | /** | |
81 | * Checks if an option is a node-specific option | |
82 | * @param {any} option The option to check | |
83 | * @returns {boolean} `true` if the option is node-specific, otherwise `false` | |
84 | */ | |
85 | function isNodeSpecificOption(option) { | |
86 | return isObject(option) || typeof option === "string"; | |
87 | } | |
88 | ||
eb39fafa DC |
89 | /** |
90 | * Normalizes a given option value. | |
91 | * @param {string|Object|undefined} options An option value to parse. | |
92 | * @returns {{ | |
93 | * ObjectExpression: {multiline: boolean, minProperties: number, consistent: boolean}, | |
94 | * ObjectPattern: {multiline: boolean, minProperties: number, consistent: boolean}, | |
95 | * ImportDeclaration: {multiline: boolean, minProperties: number, consistent: boolean}, | |
96 | * ExportNamedDeclaration : {multiline: boolean, minProperties: number, consistent: boolean} | |
97 | * }} Normalized option object. | |
98 | */ | |
99 | function normalizeOptions(options) { | |
5422a9cc | 100 | if (isObject(options) && Object.values(options).some(isNodeSpecificOption)) { |
eb39fafa DC |
101 | return { |
102 | ObjectExpression: normalizeOptionValue(options.ObjectExpression), | |
103 | ObjectPattern: normalizeOptionValue(options.ObjectPattern), | |
104 | ImportDeclaration: normalizeOptionValue(options.ImportDeclaration), | |
105 | ExportNamedDeclaration: normalizeOptionValue(options.ExportDeclaration) | |
106 | }; | |
107 | } | |
108 | ||
109 | const value = normalizeOptionValue(options); | |
110 | ||
111 | return { ObjectExpression: value, ObjectPattern: value, ImportDeclaration: value, ExportNamedDeclaration: value }; | |
112 | } | |
113 | ||
114 | /** | |
115 | * Determines if ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration | |
116 | * node needs to be checked for missing line breaks | |
117 | * @param {ASTNode} node Node under inspection | |
118 | * @param {Object} options option specific to node type | |
119 | * @param {Token} first First object property | |
120 | * @param {Token} last Last object property | |
121 | * @returns {boolean} `true` if node needs to be checked for missing line breaks | |
122 | */ | |
123 | function areLineBreaksRequired(node, options, first, last) { | |
124 | let objectProperties; | |
125 | ||
126 | if (node.type === "ObjectExpression" || node.type === "ObjectPattern") { | |
127 | objectProperties = node.properties; | |
128 | } else { | |
129 | ||
130 | // is ImportDeclaration or ExportNamedDeclaration | |
131 | objectProperties = node.specifiers | |
132 | .filter(s => s.type === "ImportSpecifier" || s.type === "ExportSpecifier"); | |
133 | } | |
134 | ||
135 | return objectProperties.length >= options.minProperties || | |
136 | ( | |
137 | options.multiline && | |
138 | objectProperties.length > 0 && | |
139 | first.loc.start.line !== last.loc.end.line | |
140 | ); | |
141 | } | |
142 | ||
143 | //------------------------------------------------------------------------------ | |
144 | // Rule Definition | |
145 | //------------------------------------------------------------------------------ | |
146 | ||
34eeec05 | 147 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
148 | module.exports = { |
149 | meta: { | |
150 | type: "layout", | |
151 | ||
152 | docs: { | |
8f9d1d4d | 153 | description: "Enforce consistent line breaks after opening and before closing braces", |
eb39fafa | 154 | recommended: false, |
f2a92ac6 | 155 | url: "https://eslint.org/docs/latest/rules/object-curly-newline" |
eb39fafa DC |
156 | }, |
157 | ||
158 | fixable: "whitespace", | |
159 | ||
160 | schema: [ | |
161 | { | |
162 | oneOf: [ | |
163 | OPTION_VALUE, | |
164 | { | |
165 | type: "object", | |
166 | properties: { | |
167 | ObjectExpression: OPTION_VALUE, | |
168 | ObjectPattern: OPTION_VALUE, | |
169 | ImportDeclaration: OPTION_VALUE, | |
170 | ExportDeclaration: OPTION_VALUE | |
171 | }, | |
172 | additionalProperties: false, | |
173 | minProperties: 1 | |
174 | } | |
175 | ] | |
176 | } | |
177 | ], | |
178 | ||
179 | messages: { | |
180 | unexpectedLinebreakBeforeClosingBrace: "Unexpected line break before this closing brace.", | |
181 | unexpectedLinebreakAfterOpeningBrace: "Unexpected line break after this opening brace.", | |
182 | expectedLinebreakBeforeClosingBrace: "Expected a line break before this closing brace.", | |
183 | expectedLinebreakAfterOpeningBrace: "Expected a line break after this opening brace." | |
184 | } | |
185 | }, | |
186 | ||
187 | create(context) { | |
f2a92ac6 | 188 | const sourceCode = context.sourceCode; |
eb39fafa DC |
189 | const normalizedOptions = normalizeOptions(context.options[0]); |
190 | ||
191 | /** | |
192 | * Reports a given node if it violated this rule. | |
193 | * @param {ASTNode} node A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node. | |
194 | * @returns {void} | |
195 | */ | |
196 | function check(node) { | |
197 | const options = normalizedOptions[node.type]; | |
198 | ||
199 | if ( | |
200 | (node.type === "ImportDeclaration" && | |
201 | !node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) || | |
202 | (node.type === "ExportNamedDeclaration" && | |
203 | !node.specifiers.some(specifier => specifier.type === "ExportSpecifier")) | |
204 | ) { | |
205 | return; | |
206 | } | |
207 | ||
208 | const openBrace = sourceCode.getFirstToken(node, token => token.value === "{"); | |
209 | ||
210 | let closeBrace; | |
211 | ||
212 | if (node.typeAnnotation) { | |
213 | closeBrace = sourceCode.getTokenBefore(node.typeAnnotation); | |
214 | } else { | |
215 | closeBrace = sourceCode.getLastToken(node, token => token.value === "}"); | |
216 | } | |
217 | ||
218 | let first = sourceCode.getTokenAfter(openBrace, { includeComments: true }); | |
219 | let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true }); | |
220 | ||
221 | const needsLineBreaks = areLineBreaksRequired(node, options, first, last); | |
222 | ||
223 | const hasCommentsFirstToken = astUtils.isCommentToken(first); | |
224 | const hasCommentsLastToken = astUtils.isCommentToken(last); | |
225 | ||
226 | /* | |
227 | * Use tokens or comments to check multiline or not. | |
228 | * But use only tokens to check whether line breaks are needed. | |
229 | * This allows: | |
230 | * var obj = { // eslint-disable-line foo | |
231 | * a: 1 | |
232 | * } | |
233 | */ | |
234 | first = sourceCode.getTokenAfter(openBrace); | |
235 | last = sourceCode.getTokenBefore(closeBrace); | |
236 | ||
237 | if (needsLineBreaks) { | |
238 | if (astUtils.isTokenOnSameLine(openBrace, first)) { | |
239 | context.report({ | |
240 | messageId: "expectedLinebreakAfterOpeningBrace", | |
241 | node, | |
6f036462 | 242 | loc: openBrace.loc, |
eb39fafa DC |
243 | fix(fixer) { |
244 | if (hasCommentsFirstToken) { | |
245 | return null; | |
246 | } | |
247 | ||
248 | return fixer.insertTextAfter(openBrace, "\n"); | |
249 | } | |
250 | }); | |
251 | } | |
252 | if (astUtils.isTokenOnSameLine(last, closeBrace)) { | |
253 | context.report({ | |
254 | messageId: "expectedLinebreakBeforeClosingBrace", | |
255 | node, | |
6f036462 | 256 | loc: closeBrace.loc, |
eb39fafa DC |
257 | fix(fixer) { |
258 | if (hasCommentsLastToken) { | |
259 | return null; | |
260 | } | |
261 | ||
262 | return fixer.insertTextBefore(closeBrace, "\n"); | |
263 | } | |
264 | }); | |
265 | } | |
266 | } else { | |
267 | const consistent = options.consistent; | |
268 | const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first); | |
269 | const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace); | |
270 | ||
271 | if ( | |
272 | (!consistent && hasLineBreakBetweenOpenBraceAndFirst) || | |
273 | (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast) | |
274 | ) { | |
275 | context.report({ | |
276 | messageId: "unexpectedLinebreakAfterOpeningBrace", | |
277 | node, | |
6f036462 | 278 | loc: openBrace.loc, |
eb39fafa DC |
279 | fix(fixer) { |
280 | if (hasCommentsFirstToken) { | |
281 | return null; | |
282 | } | |
283 | ||
284 | return fixer.removeRange([ | |
285 | openBrace.range[1], | |
286 | first.range[0] | |
287 | ]); | |
288 | } | |
289 | }); | |
290 | } | |
291 | if ( | |
292 | (!consistent && hasLineBreakBetweenCloseBraceAndLast) || | |
293 | (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast) | |
294 | ) { | |
295 | context.report({ | |
296 | messageId: "unexpectedLinebreakBeforeClosingBrace", | |
297 | node, | |
6f036462 | 298 | loc: closeBrace.loc, |
eb39fafa DC |
299 | fix(fixer) { |
300 | if (hasCommentsLastToken) { | |
301 | return null; | |
302 | } | |
303 | ||
304 | return fixer.removeRange([ | |
305 | last.range[1], | |
306 | closeBrace.range[0] | |
307 | ]); | |
308 | } | |
309 | }); | |
310 | } | |
311 | } | |
312 | } | |
313 | ||
314 | return { | |
315 | ObjectExpression: check, | |
316 | ObjectPattern: check, | |
317 | ImportDeclaration: check, | |
318 | ExportNamedDeclaration: check | |
319 | }; | |
320 | } | |
321 | }; |