UNPKG

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