UNPKG

11 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 .filter(s => s.type === "ImportSpecifier" || s.type === "ExportSpecifier");
121 }
122
123 return objectProperties.length >= options.minProperties ||
124 (
125 options.multiline &&
126 objectProperties.length > 0 &&
127 first.loc.start.line !== last.loc.end.line
128 );
129}
130
131//------------------------------------------------------------------------------
132// Rule Definition
133//------------------------------------------------------------------------------
134
135module.exports = {
136 meta: {
137 docs: {
138 description: "enforce consistent line breaks inside braces",
139 category: "Stylistic Issues",
140 recommended: false,
141 url: "https://eslint.org/docs/rules/object-curly-newline"
142 },
143 fixable: "whitespace",
144 schema: [
145 {
146 oneOf: [
147 OPTION_VALUE,
148 {
149 type: "object",
150 properties: {
151 ObjectExpression: OPTION_VALUE,
152 ObjectPattern: OPTION_VALUE,
153 ImportDeclaration: OPTION_VALUE,
154 ExportDeclaration: OPTION_VALUE
155 },
156 additionalProperties: false,
157 minProperties: 1
158 }
159 ]
160 }
161 ]
162 },
163
164 create(context) {
165 const sourceCode = context.getSourceCode();
166 const normalizedOptions = normalizeOptions(context.options[0]);
167
168 /**
169 * Reports a given node if it violated this rule.
170 * @param {ASTNode} node - A node to check. This is an ObjectExpression, ObjectPattern, ImportDeclaration or ExportNamedDeclaration node.
171 * @param {{multiline: boolean, minProperties: number, consistent: boolean}} options - An option object.
172 * @returns {void}
173 */
174 function check(node) {
175 const options = normalizedOptions[node.type];
176
177 if (
178 (node.type === "ImportDeclaration" &&
179 !node.specifiers.some(specifier => specifier.type === "ImportSpecifier")) ||
180 (node.type === "ExportNamedDeclaration" &&
181 !node.specifiers.some(specifier => specifier.type === "ExportSpecifier"))
182 ) {
183 return;
184 }
185
186 const openBrace = sourceCode.getFirstToken(node, token => token.value === "{");
187
188 let closeBrace;
189
190 if (node.typeAnnotation) {
191 closeBrace = sourceCode.getTokenBefore(node.typeAnnotation);
192 } else {
193 closeBrace = sourceCode.getLastToken(node, token => token.value === "}");
194 }
195
196 let first = sourceCode.getTokenAfter(openBrace, { includeComments: true });
197 let last = sourceCode.getTokenBefore(closeBrace, { includeComments: true });
198
199 const needsLineBreaks = areLineBreaksRequired(node, options, first, last);
200
201 const hasCommentsFirstToken = astUtils.isCommentToken(first);
202 const hasCommentsLastToken = astUtils.isCommentToken(last);
203
204 /*
205 * Use tokens or comments to check multiline or not.
206 * But use only tokens to check whether line breaks are needed.
207 * This allows:
208 * var obj = { // eslint-disable-line foo
209 * a: 1
210 * }
211 */
212 first = sourceCode.getTokenAfter(openBrace);
213 last = sourceCode.getTokenBefore(closeBrace);
214
215 if (needsLineBreaks) {
216 if (astUtils.isTokenOnSameLine(openBrace, first)) {
217 context.report({
218 message: "Expected a line break after this opening brace.",
219 node,
220 loc: openBrace.loc.start,
221 fix(fixer) {
222 if (hasCommentsFirstToken) {
223 return null;
224 }
225
226 return fixer.insertTextAfter(openBrace, "\n");
227 }
228 });
229 }
230 if (astUtils.isTokenOnSameLine(last, closeBrace)) {
231 context.report({
232 message: "Expected a line break before this closing brace.",
233 node,
234 loc: closeBrace.loc.start,
235 fix(fixer) {
236 if (hasCommentsLastToken) {
237 return null;
238 }
239
240 return fixer.insertTextBefore(closeBrace, "\n");
241 }
242 });
243 }
244 } else {
245 const consistent = options.consistent;
246 const hasLineBreakBetweenOpenBraceAndFirst = !astUtils.isTokenOnSameLine(openBrace, first);
247 const hasLineBreakBetweenCloseBraceAndLast = !astUtils.isTokenOnSameLine(last, closeBrace);
248
249 if (
250 (!consistent && hasLineBreakBetweenOpenBraceAndFirst) ||
251 (consistent && hasLineBreakBetweenOpenBraceAndFirst && !hasLineBreakBetweenCloseBraceAndLast)
252 ) {
253 context.report({
254 message: "Unexpected line break after this opening brace.",
255 node,
256 loc: openBrace.loc.start,
257 fix(fixer) {
258 if (hasCommentsFirstToken) {
259 return null;
260 }
261
262 return fixer.removeRange([
263 openBrace.range[1],
264 first.range[0]
265 ]);
266 }
267 });
268 }
269 if (
270 (!consistent && hasLineBreakBetweenCloseBraceAndLast) ||
271 (consistent && !hasLineBreakBetweenOpenBraceAndFirst && hasLineBreakBetweenCloseBraceAndLast)
272 ) {
273 context.report({
274 message: "Unexpected line break before this closing brace.",
275 node,
276 loc: closeBrace.loc.start,
277 fix(fixer) {
278 if (hasCommentsLastToken) {
279 return null;
280 }
281
282 return fixer.removeRange([
283 last.range[1],
284 closeBrace.range[0]
285 ]);
286 }
287 });
288 }
289 }
290 }
291
292 return {
293 ObjectExpression: check,
294 ObjectPattern: check,
295 ImportDeclaration: check,
296 ExportNamedDeclaration: check
297 };
298 }
299};