1/** 2 * @fileoverview This rule shoud require or disallow spaces before or after unary operations. 3 * @author Marcin Kumorek 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const astUtils = require("./utils/ast-utils"); 12 13//------------------------------------------------------------------------------ 14// Rule Definition 15//------------------------------------------------------------------------------ 16 17module.exports = { 18 meta: { 19 type: "layout", 20 21 docs: { 22 description: "enforce consistent spacing before or after unary operators", 23 category: "Stylistic Issues", 24 recommended: false, 25 url: "https://eslint.org/docs/rules/space-unary-ops" 26 }, 27 28 fixable: "whitespace", 29 30 schema: [ 31 { 32 type: "object", 33 properties: { 34 words: { 35 type: "boolean", 36 default: true 37 }, 38 nonwords: { 39 type: "boolean", 40 default: false 41 }, 42 overrides: { 43 type: "object", 44 additionalProperties: { 45 type: "boolean" 46 } 47 } 48 }, 49 additionalProperties: false 50 } 51 ], 52 messages: { 53 unexpectedBefore: "Unexpected space before unary operator '{{operator}}'.", 54 unexpectedAfter: "Unexpected space after unary operator '{{operator}}'.", 55 unexpectedAfterWord: "Unexpected space after unary word operator '{{word}}'.", 56 wordOperator: "Unary word operator '{{word}}' must be followed by whitespace.", 57 operator: "Unary operator '{{operator}}' must be followed by whitespace.", 58 beforeUnaryExpressions: "Space is required before unary expressions '{{token}}'." 59 } 60 }, 61 62 create(context) { 63 const options = context.options[0] || { words: true, nonwords: false }; 64 65 const sourceCode = context.getSourceCode(); 66 67 //-------------------------------------------------------------------------- 68 // Helpers 69 //-------------------------------------------------------------------------- 70 71 /** 72 * Check if the node is the first "!" in a "!!" convert to Boolean expression 73 * @param {ASTnode} node AST node 74 * @returns {boolean} Whether or not the node is first "!" in "!!" 75 */ 76 function isFirstBangInBangBangExpression(node) { 77 return node && node.type === "UnaryExpression" && node.argument.operator === "!" && 78 node.argument && node.argument.type === "UnaryExpression" && node.argument.operator === "!"; 79 } 80 81 /** 82 * Checks if an override exists for a given operator. 83 * @param {string} operator Operator 84 * @returns {boolean} Whether or not an override has been provided for the operator 85 */ 86 function overrideExistsForOperator(operator) { 87 return options.overrides && Object.prototype.hasOwnProperty.call(options.overrides, operator); 88 } 89 90 /** 91 * Gets the value that the override was set to for this operator 92 * @param {string} operator Operator 93 * @returns {boolean} Whether or not an override enforces a space with this operator 94 */ 95 function overrideEnforcesSpaces(operator) { 96 return options.overrides[operator]; 97 } 98 99 /** 100 * Verify Unary Word Operator has spaces after the word operator 101 * @param {ASTnode} node AST node 102 * @param {Object} firstToken first token from the AST node 103 * @param {Object} secondToken second token from the AST node 104 * @param {string} word The word to be used for reporting 105 * @returns {void} 106 */ 107 function verifyWordHasSpaces(node, firstToken, secondToken, word) { 108 if (secondToken.range[0] === firstToken.range[1]) { 109 context.report({ 110 node, 111 messageId: "wordOperator", 112 data: { 113 word 114 }, 115 fix(fixer) { 116 return fixer.insertTextAfter(firstToken, " "); 117 } 118 }); 119 } 120 } 121 122 /** 123 * Verify Unary Word Operator doesn't have spaces after the word operator 124 * @param {ASTnode} node AST node 125 * @param {Object} firstToken first token from the AST node 126 * @param {Object} secondToken second token from the AST node 127 * @param {string} word The word to be used for reporting 128 * @returns {void} 129 */ 130 function verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word) { 131 if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) { 132 if (secondToken.range[0] > firstToken.range[1]) { 133 context.report({ 134 node, 135 messageId: "unexpectedAfterWord", 136 data: { 137 word 138 }, 139 fix(fixer) { 140 return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); 141 } 142 }); 143 } 144 } 145 } 146 147 /** 148 * Check Unary Word Operators for spaces after the word operator 149 * @param {ASTnode} node AST node 150 * @param {Object} firstToken first token from the AST node 151 * @param {Object} secondToken second token from the AST node 152 * @param {string} word The word to be used for reporting 153 * @returns {void} 154 */ 155 function checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, word) { 156 if (overrideExistsForOperator(word)) { 157 if (overrideEnforcesSpaces(word)) { 158 verifyWordHasSpaces(node, firstToken, secondToken, word); 159 } else { 160 verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word); 161 } 162 } else if (options.words) { 163 verifyWordHasSpaces(node, firstToken, secondToken, word); 164 } else { 165 verifyWordDoesntHaveSpaces(node, firstToken, secondToken, word); 166 } 167 } 168 169 /** 170 * Verifies YieldExpressions satisfy spacing requirements 171 * @param {ASTnode} node AST node 172 * @returns {void} 173 */ 174 function checkForSpacesAfterYield(node) { 175 const tokens = sourceCode.getFirstTokens(node, 3), 176 word = "yield"; 177 178 if (!node.argument || node.delegate) { 179 return; 180 } 181 182 checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], word); 183 } 184 185 /** 186 * Verifies AwaitExpressions satisfy spacing requirements 187 * @param {ASTNode} node AwaitExpression AST node 188 * @returns {void} 189 */ 190 function checkForSpacesAfterAwait(node) { 191 const tokens = sourceCode.getFirstTokens(node, 3); 192 193 checkUnaryWordOperatorForSpaces(node, tokens[0], tokens[1], "await"); 194 } 195 196 /** 197 * Verifies UnaryExpression, UpdateExpression and NewExpression have spaces before or after the operator 198 * @param {ASTnode} node AST node 199 * @param {Object} firstToken First token in the expression 200 * @param {Object} secondToken Second token in the expression 201 * @returns {void} 202 */ 203 function verifyNonWordsHaveSpaces(node, firstToken, secondToken) { 204 if (node.prefix) { 205 if (isFirstBangInBangBangExpression(node)) { 206 return; 207 } 208 if (firstToken.range[1] === secondToken.range[0]) { 209 context.report({ 210 node, 211 messageId: "operator", 212 data: { 213 operator: firstToken.value 214 }, 215 fix(fixer) { 216 return fixer.insertTextAfter(firstToken, " "); 217 } 218 }); 219 } 220 } else { 221 if (firstToken.range[1] === secondToken.range[0]) { 222 context.report({ 223 node, 224 messageId: "beforeUnaryExpressions", 225 data: { 226 token: secondToken.value 227 }, 228 fix(fixer) { 229 return fixer.insertTextBefore(secondToken, " "); 230 } 231 }); 232 } 233 } 234 } 235 236 /** 237 * Verifies UnaryExpression, UpdateExpression and NewExpression don't have spaces before or after the operator 238 * @param {ASTnode} node AST node 239 * @param {Object} firstToken First token in the expression 240 * @param {Object} secondToken Second token in the expression 241 * @returns {void} 242 */ 243 function verifyNonWordsDontHaveSpaces(node, firstToken, secondToken) { 244 if (node.prefix) { 245 if (secondToken.range[0] > firstToken.range[1]) { 246 context.report({ 247 node, 248 messageId: "unexpectedAfter", 249 data: { 250 operator: firstToken.value 251 }, 252 fix(fixer) { 253 if (astUtils.canTokensBeAdjacent(firstToken, secondToken)) { 254 return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); 255 } 256 return null; 257 } 258 }); 259 } 260 } else { 261 if (secondToken.range[0] > firstToken.range[1]) { 262 context.report({ 263 node, 264 messageId: "unexpectedBefore", 265 data: { 266 operator: secondToken.value 267 }, 268 fix(fixer) { 269 return fixer.removeRange([firstToken.range[1], secondToken.range[0]]); 270 } 271 }); 272 } 273 } 274 } 275 276 /** 277 * Verifies UnaryExpression, UpdateExpression and NewExpression satisfy spacing requirements 278 * @param {ASTnode} node AST node 279 * @returns {void} 280 */ 281 function checkForSpaces(node) { 282 const tokens = node.type === "UpdateExpression" && !node.prefix 283 ? sourceCode.getLastTokens(node, 2) 284 : sourceCode.getFirstTokens(node, 2); 285 const firstToken = tokens[0]; 286 const secondToken = tokens[1]; 287 288 if ((node.type === "NewExpression" || node.prefix) && firstToken.type === "Keyword") { 289 checkUnaryWordOperatorForSpaces(node, firstToken, secondToken, firstToken.value); 290 return; 291 } 292 293 const operator = node.prefix ? tokens[0].value : tokens[1].value; 294 295 if (overrideExistsForOperator(operator)) { 296 if (overrideEnforcesSpaces(operator)) { 297 verifyNonWordsHaveSpaces(node, firstToken, secondToken); 298 } else { 299 verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); 300 } 301 } else if (options.nonwords) { 302 verifyNonWordsHaveSpaces(node, firstToken, secondToken); 303 } else { 304 verifyNonWordsDontHaveSpaces(node, firstToken, secondToken); 305 } 306 } 307 308 //-------------------------------------------------------------------------- 309 // Public 310 //-------------------------------------------------------------------------- 311 312 return { 313 UnaryExpression: checkForSpaces, 314 UpdateExpression: checkForSpaces, 315 NewExpression: checkForSpaces, 316 YieldExpression: checkForSpacesAfterYield, 317 AwaitExpression: checkForSpacesAfterAwait 318 }; 319 320 } 321}; 322