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