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