1/** 2 * @fileoverview Disallows or enforces spaces inside of parentheses. 3 * @author Jonathan Rajavuori 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 parentheses", 19 category: "Stylistic Issues", 20 recommended: false, 21 url: "https://eslint.org/docs/rules/space-in-parens" 22 }, 23 24 fixable: "whitespace", 25 26 schema: [ 27 { 28 enum: ["always", "never"] 29 }, 30 { 31 type: "object", 32 properties: { 33 exceptions: { 34 type: "array", 35 items: { 36 enum: ["{}", "[]", "()", "empty"] 37 }, 38 uniqueItems: true 39 } 40 }, 41 additionalProperties: false 42 } 43 ], 44 45 messages: { 46 missingOpeningSpace: "There must be a space after this paren.", 47 missingClosingSpace: "There must be a space before this paren.", 48 rejectedOpeningSpace: "There should be no space after this paren.", 49 rejectedClosingSpace: "There should be no space before this paren." 50 } 51 }, 52 53 create(context) { 54 const ALWAYS = context.options[0] === "always", 55 exceptionsArrayOptions = (context.options[1] && context.options[1].exceptions) || [], 56 options = {}; 57 58 let exceptions; 59 60 if (exceptionsArrayOptions.length) { 61 options.braceException = exceptionsArrayOptions.includes("{}"); 62 options.bracketException = exceptionsArrayOptions.includes("[]"); 63 options.parenException = exceptionsArrayOptions.includes("()"); 64 options.empty = exceptionsArrayOptions.includes("empty"); 65 } 66 67 /** 68 * Produces an object with the opener and closer exception values 69 * @returns {Object} `openers` and `closers` exception values 70 * @private 71 */ 72 function getExceptions() { 73 const openers = [], 74 closers = []; 75 76 if (options.braceException) { 77 openers.push("{"); 78 closers.push("}"); 79 } 80 81 if (options.bracketException) { 82 openers.push("["); 83 closers.push("]"); 84 } 85 86 if (options.parenException) { 87 openers.push("("); 88 closers.push(")"); 89 } 90 91 if (options.empty) { 92 openers.push(")"); 93 closers.push("("); 94 } 95 96 return { 97 openers, 98 closers 99 }; 100 } 101 102 //-------------------------------------------------------------------------- 103 // Helpers 104 //-------------------------------------------------------------------------- 105 const sourceCode = context.getSourceCode(); 106 107 /** 108 * Determines if a token is one of the exceptions for the opener paren 109 * @param {Object} token The token to check 110 * @returns {boolean} True if the token is one of the exceptions for the opener paren 111 */ 112 function isOpenerException(token) { 113 return exceptions.openers.includes(token.value); 114 } 115 116 /** 117 * Determines if a token is one of the exceptions for the closer paren 118 * @param {Object} token The token to check 119 * @returns {boolean} True if the token is one of the exceptions for the closer paren 120 */ 121 function isCloserException(token) { 122 return exceptions.closers.includes(token.value); 123 } 124 125 /** 126 * Determines if an opening paren is immediately followed by a required space 127 * @param {Object} openingParenToken The paren token 128 * @param {Object} tokenAfterOpeningParen The token after it 129 * @returns {boolean} True if the opening paren is missing a required space 130 */ 131 function openerMissingSpace(openingParenToken, tokenAfterOpeningParen) { 132 if (sourceCode.isSpaceBetweenTokens(openingParenToken, tokenAfterOpeningParen)) { 133 return false; 134 } 135 136 if (!options.empty && astUtils.isClosingParenToken(tokenAfterOpeningParen)) { 137 return false; 138 } 139 140 if (ALWAYS) { 141 return !isOpenerException(tokenAfterOpeningParen); 142 } 143 return isOpenerException(tokenAfterOpeningParen); 144 } 145 146 /** 147 * Determines if an opening paren is immediately followed by a disallowed space 148 * @param {Object} openingParenToken The paren token 149 * @param {Object} tokenAfterOpeningParen The token after it 150 * @returns {boolean} True if the opening paren has a disallowed space 151 */ 152 function openerRejectsSpace(openingParenToken, tokenAfterOpeningParen) { 153 if (!astUtils.isTokenOnSameLine(openingParenToken, tokenAfterOpeningParen)) { 154 return false; 155 } 156 157 if (tokenAfterOpeningParen.type === "Line") { 158 return false; 159 } 160 161 if (!sourceCode.isSpaceBetweenTokens(openingParenToken, tokenAfterOpeningParen)) { 162 return false; 163 } 164 165 if (ALWAYS) { 166 return isOpenerException(tokenAfterOpeningParen); 167 } 168 return !isOpenerException(tokenAfterOpeningParen); 169 } 170 171 /** 172 * Determines if a closing paren is immediately preceded by a required space 173 * @param {Object} tokenBeforeClosingParen The token before the paren 174 * @param {Object} closingParenToken The paren token 175 * @returns {boolean} True if the closing paren is missing a required space 176 */ 177 function closerMissingSpace(tokenBeforeClosingParen, closingParenToken) { 178 if (sourceCode.isSpaceBetweenTokens(tokenBeforeClosingParen, closingParenToken)) { 179 return false; 180 } 181 182 if (!options.empty && astUtils.isOpeningParenToken(tokenBeforeClosingParen)) { 183 return false; 184 } 185 186 if (ALWAYS) { 187 return !isCloserException(tokenBeforeClosingParen); 188 } 189 return isCloserException(tokenBeforeClosingParen); 190 } 191 192 /** 193 * Determines if a closer paren is immediately preceded by a disallowed space 194 * @param {Object} tokenBeforeClosingParen The token before the paren 195 * @param {Object} closingParenToken The paren token 196 * @returns {boolean} True if the closing paren has a disallowed space 197 */ 198 function closerRejectsSpace(tokenBeforeClosingParen, closingParenToken) { 199 if (!astUtils.isTokenOnSameLine(tokenBeforeClosingParen, closingParenToken)) { 200 return false; 201 } 202 203 if (!sourceCode.isSpaceBetweenTokens(tokenBeforeClosingParen, closingParenToken)) { 204 return false; 205 } 206 207 if (ALWAYS) { 208 return isCloserException(tokenBeforeClosingParen); 209 } 210 return !isCloserException(tokenBeforeClosingParen); 211 } 212 213 //-------------------------------------------------------------------------- 214 // Public 215 //-------------------------------------------------------------------------- 216 217 return { 218 Program: function checkParenSpaces(node) { 219 exceptions = getExceptions(); 220 const tokens = sourceCode.tokensAndComments; 221 222 tokens.forEach((token, i) => { 223 const prevToken = tokens[i - 1]; 224 const nextToken = tokens[i + 1]; 225 226 // if token is not an opening or closing paren token, do nothing 227 if (!astUtils.isOpeningParenToken(token) && !astUtils.isClosingParenToken(token)) { 228 return; 229 } 230 231 // if token is an opening paren and is not followed by a required space 232 if (token.value === "(" && openerMissingSpace(token, nextToken)) { 233 context.report({ 234 node, 235 loc: token.loc, 236 messageId: "missingOpeningSpace", 237 fix(fixer) { 238 return fixer.insertTextAfter(token, " "); 239 } 240 }); 241 } 242 243 // if token is an opening paren and is followed by a disallowed space 244 if (token.value === "(" && openerRejectsSpace(token, nextToken)) { 245 context.report({ 246 node, 247 loc: { start: token.loc.end, end: nextToken.loc.start }, 248 messageId: "rejectedOpeningSpace", 249 fix(fixer) { 250 return fixer.removeRange([token.range[1], nextToken.range[0]]); 251 } 252 }); 253 } 254 255 // if token is a closing paren and is not preceded by a required space 256 if (token.value === ")" && closerMissingSpace(prevToken, token)) { 257 context.report({ 258 node, 259 loc: token.loc, 260 messageId: "missingClosingSpace", 261 fix(fixer) { 262 return fixer.insertTextBefore(token, " "); 263 } 264 }); 265 } 266 267 // if token is a closing paren and is preceded by a disallowed space 268 if (token.value === ")" && closerRejectsSpace(prevToken, token)) { 269 context.report({ 270 node, 271 loc: { start: prevToken.loc.end, end: token.loc.start }, 272 messageId: "rejectedClosingSpace", 273 fix(fixer) { 274 return fixer.removeRange([prevToken.range[1], token.range[0]]); 275 } 276 }); 277 } 278 }); 279 } 280 }; 281 } 282}; 283