]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/object-curly-newline.js
import 8.23.1 source
[pve-eslint.git] / eslint / lib / rules / object-curly-newline.js
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");
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
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
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) {
100 if (isObject(options) && Object.values(options).some(isNodeSpecificOption)) {
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
147 /** @type {import('../shared/types').Rule} */
148 module.exports = {
149 meta: {
150 type: "layout",
151
152 docs: {
153 description: "Enforce consistent line breaks after opening and before closing braces",
154 recommended: false,
155 url: "https://eslint.org/docs/rules/object-curly-newline"
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) {
188 const sourceCode = context.getSourceCode();
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,
242 loc: openBrace.loc,
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,
256 loc: closeBrace.loc,
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,
278 loc: openBrace.loc,
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,
298 loc: closeBrace.loc,
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 };