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