1/** 2 * @fileoverview Disallows or enforces spaces inside of object literals. 3 * @author Jamund Ferguson 4 */ 5"use strict"; 6 7const astUtils = require("./utils/ast-utils"); 8 9//------------------------------------------------------------------------------ 10// Rule Definition 11//------------------------------------------------------------------------------ 12 13module.exports = { 14 meta: { 15 type: "layout", 16 17 docs: { 18 description: "enforce consistent spacing inside braces", 19 category: "Stylistic Issues", 20 recommended: false, 21 url: "https://eslint.org/docs/rules/object-curly-spacing" 22 }, 23 24 fixable: "whitespace", 25 26 schema: [ 27 { 28 enum: ["always", "never"] 29 }, 30 { 31 type: "object", 32 properties: { 33 arraysInObjects: { 34 type: "boolean" 35 }, 36 objectsInObjects: { 37 type: "boolean" 38 } 39 }, 40 additionalProperties: false 41 } 42 ], 43 44 messages: { 45 requireSpaceBefore: "A space is required before '{{token}}'.", 46 requireSpaceAfter: "A space is required after '{{token}}'.", 47 unexpectedSpaceBefore: "There should be no space before '{{token}}'.", 48 unexpectedSpaceAfter: "There should be no space after '{{token}}'." 49 } 50 }, 51 52 create(context) { 53 const spaced = context.options[0] === "always", 54 sourceCode = context.getSourceCode(); 55 56 /** 57 * Determines whether an option is set, relative to the spacing option. 58 * If spaced is "always", then check whether option is set to false. 59 * If spaced is "never", then check whether option is set to true. 60 * @param {Object} option The option to exclude. 61 * @returns {boolean} Whether or not the property is excluded. 62 */ 63 function isOptionSet(option) { 64 return context.options[1] ? context.options[1][option] === !spaced : false; 65 } 66 67 const options = { 68 spaced, 69 arraysInObjectsException: isOptionSet("arraysInObjects"), 70 objectsInObjectsException: isOptionSet("objectsInObjects") 71 }; 72 73 //-------------------------------------------------------------------------- 74 // Helpers 75 //-------------------------------------------------------------------------- 76 77 /** 78 * Reports that there shouldn't be a space after the first token 79 * @param {ASTNode} node The node to report in the event of an error. 80 * @param {Token} token The token to use for the report. 81 * @returns {void} 82 */ 83 function reportNoBeginningSpace(node, token) { 84 const nextToken = context.getSourceCode().getTokenAfter(token, { includeComments: true }); 85 86 context.report({ 87 node, 88 loc: { start: token.loc.end, end: nextToken.loc.start }, 89 messageId: "unexpectedSpaceAfter", 90 data: { 91 token: token.value 92 }, 93 fix(fixer) { 94 return fixer.removeRange([token.range[1], nextToken.range[0]]); 95 } 96 }); 97 } 98 99 /** 100 * Reports that there shouldn't be a space before the last token 101 * @param {ASTNode} node The node to report in the event of an error. 102 * @param {Token} token The token to use for the report. 103 * @returns {void} 104 */ 105 function reportNoEndingSpace(node, token) { 106 const previousToken = context.getSourceCode().getTokenBefore(token, { includeComments: true }); 107 108 context.report({ 109 node, 110 loc: { start: previousToken.loc.end, end: token.loc.start }, 111 messageId: "unexpectedSpaceBefore", 112 data: { 113 token: token.value 114 }, 115 fix(fixer) { 116 return fixer.removeRange([previousToken.range[1], token.range[0]]); 117 } 118 }); 119 } 120 121 /** 122 * Reports that there should be a space after the first token 123 * @param {ASTNode} node The node to report in the event of an error. 124 * @param {Token} token The token to use for the report. 125 * @returns {void} 126 */ 127 function reportRequiredBeginningSpace(node, token) { 128 context.report({ 129 node, 130 loc: token.loc, 131 messageId: "requireSpaceAfter", 132 data: { 133 token: token.value 134 }, 135 fix(fixer) { 136 return fixer.insertTextAfter(token, " "); 137 } 138 }); 139 } 140 141 /** 142 * Reports that there should be a space before the last token 143 * @param {ASTNode} node The node to report in the event of an error. 144 * @param {Token} token The token to use for the report. 145 * @returns {void} 146 */ 147 function reportRequiredEndingSpace(node, token) { 148 context.report({ 149 node, 150 loc: token.loc, 151 messageId: "requireSpaceBefore", 152 data: { 153 token: token.value 154 }, 155 fix(fixer) { 156 return fixer.insertTextBefore(token, " "); 157 } 158 }); 159 } 160 161 /** 162 * Determines if spacing in curly braces is valid. 163 * @param {ASTNode} node The AST node to check. 164 * @param {Token} first The first token to check (should be the opening brace) 165 * @param {Token} second The second token to check (should be first after the opening brace) 166 * @param {Token} penultimate The penultimate token to check (should be last before closing brace) 167 * @param {Token} last The last token to check (should be closing brace) 168 * @returns {void} 169 */ 170 function validateBraceSpacing(node, first, second, penultimate, last) { 171 if (astUtils.isTokenOnSameLine(first, second)) { 172 const firstSpaced = sourceCode.isSpaceBetweenTokens(first, second); 173 174 if (options.spaced && !firstSpaced) { 175 reportRequiredBeginningSpace(node, first); 176 } 177 if (!options.spaced && firstSpaced && second.type !== "Line") { 178 reportNoBeginningSpace(node, first); 179 } 180 } 181 182 if (astUtils.isTokenOnSameLine(penultimate, last)) { 183 const shouldCheckPenultimate = ( 184 options.arraysInObjectsException && astUtils.isClosingBracketToken(penultimate) || 185 options.objectsInObjectsException && astUtils.isClosingBraceToken(penultimate) 186 ); 187 const penultimateType = shouldCheckPenultimate && sourceCode.getNodeByRangeIndex(penultimate.range[0]).type; 188 189 const closingCurlyBraceMustBeSpaced = ( 190 options.arraysInObjectsException && penultimateType === "ArrayExpression" || 191 options.objectsInObjectsException && (penultimateType === "ObjectExpression" || penultimateType === "ObjectPattern") 192 ) ? !options.spaced : options.spaced; 193 194 const lastSpaced = sourceCode.isSpaceBetweenTokens(penultimate, last); 195 196 if (closingCurlyBraceMustBeSpaced && !lastSpaced) { 197 reportRequiredEndingSpace(node, last); 198 } 199 if (!closingCurlyBraceMustBeSpaced && lastSpaced) { 200 reportNoEndingSpace(node, last); 201 } 202 } 203 } 204 205 /** 206 * Gets '}' token of an object node. 207 * 208 * Because the last token of object patterns might be a type annotation, 209 * this traverses tokens preceded by the last property, then returns the 210 * first '}' token. 211 * @param {ASTNode} node The node to get. This node is an 212 * ObjectExpression or an ObjectPattern. And this node has one or 213 * more properties. 214 * @returns {Token} '}' token. 215 */ 216 function getClosingBraceOfObject(node) { 217 const lastProperty = node.properties[node.properties.length - 1]; 218 219 return sourceCode.getTokenAfter(lastProperty, astUtils.isClosingBraceToken); 220 } 221 222 /** 223 * Reports a given object node if spacing in curly braces is invalid. 224 * @param {ASTNode} node An ObjectExpression or ObjectPattern node to check. 225 * @returns {void} 226 */ 227 function checkForObject(node) { 228 if (node.properties.length === 0) { 229 return; 230 } 231 232 const first = sourceCode.getFirstToken(node), 233 last = getClosingBraceOfObject(node), 234 second = sourceCode.getTokenAfter(first, { includeComments: true }), 235 penultimate = sourceCode.getTokenBefore(last, { includeComments: true }); 236 237 validateBraceSpacing(node, first, second, penultimate, last); 238 } 239 240 /** 241 * Reports a given import node if spacing in curly braces is invalid. 242 * @param {ASTNode} node An ImportDeclaration node to check. 243 * @returns {void} 244 */ 245 function checkForImport(node) { 246 if (node.specifiers.length === 0) { 247 return; 248 } 249 250 let firstSpecifier = node.specifiers[0]; 251 const lastSpecifier = node.specifiers[node.specifiers.length - 1]; 252 253 if (lastSpecifier.type !== "ImportSpecifier") { 254 return; 255 } 256 if (firstSpecifier.type !== "ImportSpecifier") { 257 firstSpecifier = node.specifiers[1]; 258 } 259 260 const first = sourceCode.getTokenBefore(firstSpecifier), 261 last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken), 262 second = sourceCode.getTokenAfter(first, { includeComments: true }), 263 penultimate = sourceCode.getTokenBefore(last, { includeComments: true }); 264 265 validateBraceSpacing(node, first, second, penultimate, last); 266 } 267 268 /** 269 * Reports a given export node if spacing in curly braces is invalid. 270 * @param {ASTNode} node An ExportNamedDeclaration node to check. 271 * @returns {void} 272 */ 273 function checkForExport(node) { 274 if (node.specifiers.length === 0) { 275 return; 276 } 277 278 const firstSpecifier = node.specifiers[0], 279 lastSpecifier = node.specifiers[node.specifiers.length - 1], 280 first = sourceCode.getTokenBefore(firstSpecifier), 281 last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken), 282 second = sourceCode.getTokenAfter(first, { includeComments: true }), 283 penultimate = sourceCode.getTokenBefore(last, { includeComments: true }); 284 285 validateBraceSpacing(node, first, second, penultimate, last); 286 } 287 288 //-------------------------------------------------------------------------- 289 // Public 290 //-------------------------------------------------------------------------- 291 292 return { 293 294 // var {x} = y; 295 ObjectPattern: checkForObject, 296 297 // var y = {x: 'y'} 298 ObjectExpression: checkForObject, 299 300 // import {y} from 'x'; 301 ImportDeclaration: checkForImport, 302 303 // export {name} from 'yo'; 304 ExportNamedDeclaration: checkForExport 305 }; 306 307 } 308}; 309