1/** 2 * @fileoverview Disallow parenthesising higher precedence subexpressions. 3 * @author Michael Ficarra 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Rule Definition 9//------------------------------------------------------------------------------ 10 11const { isParenthesized: isParenthesizedRaw } = require("eslint-utils"); 12const astUtils = require("./utils/ast-utils.js"); 13 14module.exports = { 15 meta: { 16 type: "layout", 17 18 docs: { 19 description: "disallow unnecessary parentheses", 20 category: "Possible Errors", 21 recommended: false, 22 url: "https://eslint.org/docs/rules/no-extra-parens" 23 }, 24 25 fixable: "code", 26 27 schema: { 28 anyOf: [ 29 { 30 type: "array", 31 items: [ 32 { 33 enum: ["functions"] 34 } 35 ], 36 minItems: 0, 37 maxItems: 1 38 }, 39 { 40 type: "array", 41 items: [ 42 { 43 enum: ["all"] 44 }, 45 { 46 type: "object", 47 properties: { 48 conditionalAssign: { type: "boolean" }, 49 nestedBinaryExpressions: { type: "boolean" }, 50 returnAssign: { type: "boolean" }, 51 ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] }, 52 enforceForArrowConditionals: { type: "boolean" }, 53 enforceForSequenceExpressions: { type: "boolean" }, 54 enforceForNewInMemberExpressions: { type: "boolean" }, 55 enforceForFunctionPrototypeMethods: { type: "boolean" } 56 }, 57 additionalProperties: false 58 } 59 ], 60 minItems: 0, 61 maxItems: 2 62 } 63 ] 64 }, 65 66 messages: { 67 unexpected: "Unnecessary parentheses around expression." 68 } 69 }, 70 71 create(context) { 72 const sourceCode = context.getSourceCode(); 73 74 const tokensToIgnore = new WeakSet(); 75 const precedence = astUtils.getPrecedence; 76 const ALL_NODES = context.options[0] !== "functions"; 77 const EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false; 78 const NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false; 79 const EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false; 80 const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX; 81 const IGNORE_ARROW_CONDITIONALS = ALL_NODES && context.options[1] && 82 context.options[1].enforceForArrowConditionals === false; 83 const IGNORE_SEQUENCE_EXPRESSIONS = ALL_NODES && context.options[1] && 84 context.options[1].enforceForSequenceExpressions === false; 85 const IGNORE_NEW_IN_MEMBER_EXPR = ALL_NODES && context.options[1] && 86 context.options[1].enforceForNewInMemberExpressions === false; 87 const IGNORE_FUNCTION_PROTOTYPE_METHODS = ALL_NODES && context.options[1] && 88 context.options[1].enforceForFunctionPrototypeMethods === false; 89 90 const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" }); 91 const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" }); 92 93 let reportsBuffer; 94 95 /** 96 * Determines whether the given node is a `call` or `apply` method call, invoked directly on a `FunctionExpression` node. 97 * Example: function(){}.call() 98 * @param {ASTNode} node The node to be checked. 99 * @returns {boolean} True if the node is an immediate `call` or `apply` method call. 100 * @private 101 */ 102 function isImmediateFunctionPrototypeMethodCall(node) { 103 const callNode = astUtils.skipChainExpression(node); 104 105 if (callNode.type !== "CallExpression") { 106 return false; 107 } 108 const callee = astUtils.skipChainExpression(callNode.callee); 109 110 return ( 111 callee.type === "MemberExpression" && 112 callee.object.type === "FunctionExpression" && 113 ["call", "apply"].includes(astUtils.getStaticPropertyName(callee)) 114 ); 115 } 116 117 /** 118 * Determines if this rule should be enforced for a node given the current configuration. 119 * @param {ASTNode} node The node to be checked. 120 * @returns {boolean} True if the rule should be enforced for this node. 121 * @private 122 */ 123 function ruleApplies(node) { 124 if (node.type === "JSXElement" || node.type === "JSXFragment") { 125 const isSingleLine = node.loc.start.line === node.loc.end.line; 126 127 switch (IGNORE_JSX) { 128 129 // Exclude this JSX element from linting 130 case "all": 131 return false; 132 133 // Exclude this JSX element if it is multi-line element 134 case "multi-line": 135 return isSingleLine; 136 137 // Exclude this JSX element if it is single-line element 138 case "single-line": 139 return !isSingleLine; 140 141 // Nothing special to be done for JSX elements 142 case "none": 143 break; 144 145 // no default 146 } 147 } 148 149 if (node.type === "SequenceExpression" && IGNORE_SEQUENCE_EXPRESSIONS) { 150 return false; 151 } 152 153 if (isImmediateFunctionPrototypeMethodCall(node) && IGNORE_FUNCTION_PROTOTYPE_METHODS) { 154 return false; 155 } 156 157 return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression"; 158 } 159 160 /** 161 * Determines if a node is surrounded by parentheses. 162 * @param {ASTNode} node The node to be checked. 163 * @returns {boolean} True if the node is parenthesised. 164 * @private 165 */ 166 function isParenthesised(node) { 167 return isParenthesizedRaw(1, node, sourceCode); 168 } 169 170 /** 171 * Determines if a node is surrounded by parentheses twice. 172 * @param {ASTNode} node The node to be checked. 173 * @returns {boolean} True if the node is doubly parenthesised. 174 * @private 175 */ 176 function isParenthesisedTwice(node) { 177 return isParenthesizedRaw(2, node, sourceCode); 178 } 179 180 /** 181 * Determines if a node is surrounded by (potentially) invalid parentheses. 182 * @param {ASTNode} node The node to be checked. 183 * @returns {boolean} True if the node is incorrectly parenthesised. 184 * @private 185 */ 186 function hasExcessParens(node) { 187 return ruleApplies(node) && isParenthesised(node); 188 } 189 190 /** 191 * Determines if a node that is expected to be parenthesised is surrounded by 192 * (potentially) invalid extra parentheses. 193 * @param {ASTNode} node The node to be checked. 194 * @returns {boolean} True if the node is has an unexpected extra pair of parentheses. 195 * @private 196 */ 197 function hasDoubleExcessParens(node) { 198 return ruleApplies(node) && isParenthesisedTwice(node); 199 } 200 201 /** 202 * Determines if a node that is expected to be parenthesised is surrounded by 203 * (potentially) invalid extra parentheses with considering precedence level of the node. 204 * If the preference level of the node is not higher or equal to precedence lower limit, it also checks 205 * whether the node is surrounded by parentheses twice or not. 206 * @param {ASTNode} node The node to be checked. 207 * @param {number} precedenceLowerLimit The lower limit of precedence. 208 * @returns {boolean} True if the node is has an unexpected extra pair of parentheses. 209 * @private 210 */ 211 function hasExcessParensWithPrecedence(node, precedenceLowerLimit) { 212 if (ruleApplies(node) && isParenthesised(node)) { 213 if ( 214 precedence(node) >= precedenceLowerLimit || 215 isParenthesisedTwice(node) 216 ) { 217 return true; 218 } 219 } 220 return false; 221 } 222 223 /** 224 * Determines if a node test expression is allowed to have a parenthesised assignment 225 * @param {ASTNode} node The node to be checked. 226 * @returns {boolean} True if the assignment can be parenthesised. 227 * @private 228 */ 229 function isCondAssignException(node) { 230 return EXCEPT_COND_ASSIGN && node.test.type === "AssignmentExpression"; 231 } 232 233 /** 234 * Determines if a node is in a return statement 235 * @param {ASTNode} node The node to be checked. 236 * @returns {boolean} True if the node is in a return statement. 237 * @private 238 */ 239 function isInReturnStatement(node) { 240 for (let currentNode = node; currentNode; currentNode = currentNode.parent) { 241 if ( 242 currentNode.type === "ReturnStatement" || 243 (currentNode.type === "ArrowFunctionExpression" && currentNode.body.type !== "BlockStatement") 244 ) { 245 return true; 246 } 247 } 248 249 return false; 250 } 251 252 /** 253 * Determines if a constructor function is newed-up with parens 254 * @param {ASTNode} newExpression The NewExpression node to be checked. 255 * @returns {boolean} True if the constructor is called with parens. 256 * @private 257 */ 258 function isNewExpressionWithParens(newExpression) { 259 const lastToken = sourceCode.getLastToken(newExpression); 260 const penultimateToken = sourceCode.getTokenBefore(lastToken); 261 262 return newExpression.arguments.length > 0 || 263 ( 264 265 // The expression should end with its own parens, e.g., new new foo() is not a new expression with parens 266 astUtils.isOpeningParenToken(penultimateToken) && 267 astUtils.isClosingParenToken(lastToken) && 268 newExpression.callee.range[1] < newExpression.range[1] 269 ); 270 } 271 272 /** 273 * Determines if a node is or contains an assignment expression 274 * @param {ASTNode} node The node to be checked. 275 * @returns {boolean} True if the node is or contains an assignment expression. 276 * @private 277 */ 278 function containsAssignment(node) { 279 if (node.type === "AssignmentExpression") { 280 return true; 281 } 282 if (node.type === "ConditionalExpression" && 283 (node.consequent.type === "AssignmentExpression" || node.alternate.type === "AssignmentExpression")) { 284 return true; 285 } 286 if ((node.left && node.left.type === "AssignmentExpression") || 287 (node.right && node.right.type === "AssignmentExpression")) { 288 return true; 289 } 290 291 return false; 292 } 293 294 /** 295 * Determines if a node is contained by or is itself a return statement and is allowed to have a parenthesised assignment 296 * @param {ASTNode} node The node to be checked. 297 * @returns {boolean} True if the assignment can be parenthesised. 298 * @private 299 */ 300 function isReturnAssignException(node) { 301 if (!EXCEPT_RETURN_ASSIGN || !isInReturnStatement(node)) { 302 return false; 303 } 304 305 if (node.type === "ReturnStatement") { 306 return node.argument && containsAssignment(node.argument); 307 } 308 if (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement") { 309 return containsAssignment(node.body); 310 } 311 return containsAssignment(node); 312 313 } 314 315 /** 316 * Determines if a node following a [no LineTerminator here] restriction is 317 * surrounded by (potentially) invalid extra parentheses. 318 * @param {Token} token The token preceding the [no LineTerminator here] restriction. 319 * @param {ASTNode} node The node to be checked. 320 * @returns {boolean} True if the node is incorrectly parenthesised. 321 * @private 322 */ 323 function hasExcessParensNoLineTerminator(token, node) { 324 if (token.loc.end.line === node.loc.start.line) { 325 return hasExcessParens(node); 326 } 327 328 return hasDoubleExcessParens(node); 329 } 330 331 /** 332 * Determines whether a node should be preceded by an additional space when removing parens 333 * @param {ASTNode} node node to evaluate; must be surrounded by parentheses 334 * @returns {boolean} `true` if a space should be inserted before the node 335 * @private 336 */ 337 function requiresLeadingSpace(node) { 338 const leftParenToken = sourceCode.getTokenBefore(node); 339 const tokenBeforeLeftParen = sourceCode.getTokenBefore(leftParenToken, { includeComments: true }); 340 const tokenAfterLeftParen = sourceCode.getTokenAfter(leftParenToken, { includeComments: true }); 341 342 return tokenBeforeLeftParen && 343 tokenBeforeLeftParen.range[1] === leftParenToken.range[0] && 344 leftParenToken.range[1] === tokenAfterLeftParen.range[0] && 345 !astUtils.canTokensBeAdjacent(tokenBeforeLeftParen, tokenAfterLeftParen); 346 } 347 348 /** 349 * Determines whether a node should be followed by an additional space when removing parens 350 * @param {ASTNode} node node to evaluate; must be surrounded by parentheses 351 * @returns {boolean} `true` if a space should be inserted after the node 352 * @private 353 */ 354 function requiresTrailingSpace(node) { 355 const nextTwoTokens = sourceCode.getTokensAfter(node, { count: 2 }); 356 const rightParenToken = nextTwoTokens[0]; 357 const tokenAfterRightParen = nextTwoTokens[1]; 358 const tokenBeforeRightParen = sourceCode.getLastToken(node); 359 360 return rightParenToken && tokenAfterRightParen && 361 !sourceCode.isSpaceBetweenTokens(rightParenToken, tokenAfterRightParen) && 362 !astUtils.canTokensBeAdjacent(tokenBeforeRightParen, tokenAfterRightParen); 363 } 364 365 /** 366 * Determines if a given expression node is an IIFE 367 * @param {ASTNode} node The node to check 368 * @returns {boolean} `true` if the given node is an IIFE 369 */ 370 function isIIFE(node) { 371 const maybeCallNode = astUtils.skipChainExpression(node); 372 373 return maybeCallNode.type === "CallExpression" && maybeCallNode.callee.type === "FunctionExpression"; 374 } 375 376 /** 377 * Determines if the given node can be the assignment target in destructuring or the LHS of an assignment. 378 * This is to avoid an autofix that could change behavior because parsers mistakenly allow invalid syntax, 379 * such as `(a = b) = c` and `[(a = b) = c] = []`. Ideally, this function shouldn't be necessary. 380 * @param {ASTNode} [node] The node to check 381 * @returns {boolean} `true` if the given node can be a valid assignment target 382 */ 383 function canBeAssignmentTarget(node) { 384 return node && (node.type === "Identifier" || node.type === "MemberExpression"); 385 } 386 387 /** 388 * Report the node 389 * @param {ASTNode} node node to evaluate 390 * @returns {void} 391 * @private 392 */ 393 function report(node) { 394 const leftParenToken = sourceCode.getTokenBefore(node); 395 const rightParenToken = sourceCode.getTokenAfter(node); 396 397 if (!isParenthesisedTwice(node)) { 398 if (tokensToIgnore.has(sourceCode.getFirstToken(node))) { 399 return; 400 } 401 402 if (isIIFE(node) && !isParenthesised(node.callee)) { 403 return; 404 } 405 } 406 407 /** 408 * Finishes reporting 409 * @returns {void} 410 * @private 411 */ 412 function finishReport() { 413 context.report({ 414 node, 415 loc: leftParenToken.loc, 416 messageId: "unexpected", 417 fix(fixer) { 418 const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]); 419 420 return fixer.replaceTextRange([ 421 leftParenToken.range[0], 422 rightParenToken.range[1] 423 ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : "")); 424 } 425 }); 426 } 427 428 if (reportsBuffer) { 429 reportsBuffer.reports.push({ node, finishReport }); 430 return; 431 } 432 433 finishReport(); 434 } 435 436 /** 437 * Evaluate a argument of the node. 438 * @param {ASTNode} node node to evaluate 439 * @returns {void} 440 * @private 441 */ 442 function checkArgumentWithPrecedence(node) { 443 if (hasExcessParensWithPrecedence(node.argument, precedence(node))) { 444 report(node.argument); 445 } 446 } 447 448 /** 449 * Check if a member expression contains a call expression 450 * @param {ASTNode} node MemberExpression node to evaluate 451 * @returns {boolean} true if found, false if not 452 */ 453 function doesMemberExpressionContainCallExpression(node) { 454 let currentNode = node.object; 455 let currentNodeType = node.object.type; 456 457 while (currentNodeType === "MemberExpression") { 458 currentNode = currentNode.object; 459 currentNodeType = currentNode.type; 460 } 461 462 return currentNodeType === "CallExpression"; 463 } 464 465 /** 466 * Evaluate a new call 467 * @param {ASTNode} node node to evaluate 468 * @returns {void} 469 * @private 470 */ 471 function checkCallNew(node) { 472 const callee = node.callee; 473 474 if (hasExcessParensWithPrecedence(callee, precedence(node))) { 475 const hasNewParensException = callee.type === "NewExpression" && !isNewExpressionWithParens(callee); 476 477 if ( 478 hasDoubleExcessParens(callee) || 479 !isIIFE(node) && 480 !hasNewParensException && 481 !( 482 483 // Allow extra parens around a new expression if they are intervening parentheses. 484 node.type === "NewExpression" && 485 callee.type === "MemberExpression" && 486 doesMemberExpressionContainCallExpression(callee) 487 ) && 488 !(!node.optional && callee.type === "ChainExpression") 489 ) { 490 report(node.callee); 491 } 492 } 493 node.arguments 494 .filter(arg => hasExcessParensWithPrecedence(arg, PRECEDENCE_OF_ASSIGNMENT_EXPR)) 495 .forEach(report); 496 } 497 498 /** 499 * Evaluate binary logicals 500 * @param {ASTNode} node node to evaluate 501 * @returns {void} 502 * @private 503 */ 504 function checkBinaryLogical(node) { 505 const prec = precedence(node); 506 const leftPrecedence = precedence(node.left); 507 const rightPrecedence = precedence(node.right); 508 const isExponentiation = node.operator === "**"; 509 const shouldSkipLeft = NESTED_BINARY && (node.left.type === "BinaryExpression" || node.left.type === "LogicalExpression"); 510 const shouldSkipRight = NESTED_BINARY && (node.right.type === "BinaryExpression" || node.right.type === "LogicalExpression"); 511 512 if (!shouldSkipLeft && hasExcessParens(node.left)) { 513 if ( 514 !(node.left.type === "UnaryExpression" && isExponentiation) && 515 !astUtils.isMixedLogicalAndCoalesceExpressions(node.left, node) && 516 (leftPrecedence > prec || (leftPrecedence === prec && !isExponentiation)) || 517 isParenthesisedTwice(node.left) 518 ) { 519 report(node.left); 520 } 521 } 522 523 if (!shouldSkipRight && hasExcessParens(node.right)) { 524 if ( 525 !astUtils.isMixedLogicalAndCoalesceExpressions(node.right, node) && 526 (rightPrecedence > prec || (rightPrecedence === prec && isExponentiation)) || 527 isParenthesisedTwice(node.right) 528 ) { 529 report(node.right); 530 } 531 } 532 } 533 534 /** 535 * Check the parentheses around the super class of the given class definition. 536 * @param {ASTNode} node The node of class declarations to check. 537 * @returns {void} 538 */ 539 function checkClass(node) { 540 if (!node.superClass) { 541 return; 542 } 543 544 /* 545 * If `node.superClass` is a LeftHandSideExpression, parentheses are extra. 546 * Otherwise, parentheses are needed. 547 */ 548 const hasExtraParens = precedence(node.superClass) > PRECEDENCE_OF_UPDATE_EXPR 549 ? hasExcessParens(node.superClass) 550 : hasDoubleExcessParens(node.superClass); 551 552 if (hasExtraParens) { 553 report(node.superClass); 554 } 555 } 556 557 /** 558 * Check the parentheses around the argument of the given spread operator. 559 * @param {ASTNode} node The node of spread elements/properties to check. 560 * @returns {void} 561 */ 562 function checkSpreadOperator(node) { 563 if (hasExcessParensWithPrecedence(node.argument, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { 564 report(node.argument); 565 } 566 } 567 568 /** 569 * Checks the parentheses for an ExpressionStatement or ExportDefaultDeclaration 570 * @param {ASTNode} node The ExpressionStatement.expression or ExportDefaultDeclaration.declaration node 571 * @returns {void} 572 */ 573 function checkExpressionOrExportStatement(node) { 574 const firstToken = isParenthesised(node) ? sourceCode.getTokenBefore(node) : sourceCode.getFirstToken(node); 575 const secondToken = sourceCode.getTokenAfter(firstToken, astUtils.isNotOpeningParenToken); 576 const thirdToken = secondToken ? sourceCode.getTokenAfter(secondToken) : null; 577 const tokenAfterClosingParens = secondToken ? sourceCode.getTokenAfter(secondToken, astUtils.isNotClosingParenToken) : null; 578 579 if ( 580 astUtils.isOpeningParenToken(firstToken) && 581 ( 582 astUtils.isOpeningBraceToken(secondToken) || 583 secondToken.type === "Keyword" && ( 584 secondToken.value === "function" || 585 secondToken.value === "class" || 586 secondToken.value === "let" && 587 tokenAfterClosingParens && 588 ( 589 astUtils.isOpeningBracketToken(tokenAfterClosingParens) || 590 tokenAfterClosingParens.type === "Identifier" 591 ) 592 ) || 593 secondToken && secondToken.type === "Identifier" && secondToken.value === "async" && thirdToken && thirdToken.type === "Keyword" && thirdToken.value === "function" 594 ) 595 ) { 596 tokensToIgnore.add(secondToken); 597 } 598 599 const hasExtraParens = node.parent.type === "ExportDefaultDeclaration" 600 ? hasExcessParensWithPrecedence(node, PRECEDENCE_OF_ASSIGNMENT_EXPR) 601 : hasExcessParens(node); 602 603 if (hasExtraParens) { 604 report(node); 605 } 606 } 607 608 /** 609 * Finds the path from the given node to the specified ancestor. 610 * @param {ASTNode} node First node in the path. 611 * @param {ASTNode} ancestor Last node in the path. 612 * @returns {ASTNode[]} Path, including both nodes. 613 * @throws {Error} If the given node does not have the specified ancestor. 614 */ 615 function pathToAncestor(node, ancestor) { 616 const path = [node]; 617 let currentNode = node; 618 619 while (currentNode !== ancestor) { 620 621 currentNode = currentNode.parent; 622 623 /* istanbul ignore if */ 624 if (currentNode === null) { 625 throw new Error("Nodes are not in the ancestor-descendant relationship."); 626 } 627 628 path.push(currentNode); 629 } 630 631 return path; 632 } 633 634 /** 635 * Finds the path from the given node to the specified descendant. 636 * @param {ASTNode} node First node in the path. 637 * @param {ASTNode} descendant Last node in the path. 638 * @returns {ASTNode[]} Path, including both nodes. 639 * @throws {Error} If the given node does not have the specified descendant. 640 */ 641 function pathToDescendant(node, descendant) { 642 return pathToAncestor(descendant, node).reverse(); 643 } 644 645 /** 646 * Checks whether the syntax of the given ancestor of an 'in' expression inside a for-loop initializer 647 * is preventing the 'in' keyword from being interpreted as a part of an ill-formed for-in loop. 648 * @param {ASTNode} node Ancestor of an 'in' expression. 649 * @param {ASTNode} child Child of the node, ancestor of the same 'in' expression or the 'in' expression itself. 650 * @returns {boolean} True if the keyword 'in' would be interpreted as the 'in' operator, without any parenthesis. 651 */ 652 function isSafelyEnclosingInExpression(node, child) { 653 switch (node.type) { 654 case "ArrayExpression": 655 case "ArrayPattern": 656 case "BlockStatement": 657 case "ObjectExpression": 658 case "ObjectPattern": 659 case "TemplateLiteral": 660 return true; 661 case "ArrowFunctionExpression": 662 case "FunctionExpression": 663 return node.params.includes(child); 664 case "CallExpression": 665 case "NewExpression": 666 return node.arguments.includes(child); 667 case "MemberExpression": 668 return node.computed && node.property === child; 669 case "ConditionalExpression": 670 return node.consequent === child; 671 default: 672 return false; 673 } 674 } 675 676 /** 677 * Starts a new reports buffering. Warnings will be stored in a buffer instead of being reported immediately. 678 * An additional logic that requires multiple nodes (e.g. a whole subtree) may dismiss some of the stored warnings. 679 * @returns {void} 680 */ 681 function startNewReportsBuffering() { 682 reportsBuffer = { 683 upper: reportsBuffer, 684 inExpressionNodes: [], 685 reports: [] 686 }; 687 } 688 689 /** 690 * Ends the current reports buffering. 691 * @returns {void} 692 */ 693 function endCurrentReportsBuffering() { 694 const { upper, inExpressionNodes, reports } = reportsBuffer; 695 696 if (upper) { 697 upper.inExpressionNodes.push(...inExpressionNodes); 698 upper.reports.push(...reports); 699 } else { 700 701 // flush remaining reports 702 reports.forEach(({ finishReport }) => finishReport()); 703 } 704 705 reportsBuffer = upper; 706 } 707 708 /** 709 * Checks whether the given node is in the current reports buffer. 710 * @param {ASTNode} node Node to check. 711 * @returns {boolean} True if the node is in the current buffer, false otherwise. 712 */ 713 function isInCurrentReportsBuffer(node) { 714 return reportsBuffer.reports.some(r => r.node === node); 715 } 716 717 /** 718 * Removes the given node from the current reports buffer. 719 * @param {ASTNode} node Node to remove. 720 * @returns {void} 721 */ 722 function removeFromCurrentReportsBuffer(node) { 723 reportsBuffer.reports = reportsBuffer.reports.filter(r => r.node !== node); 724 } 725 726 return { 727 ArrayExpression(node) { 728 node.elements 729 .filter(e => e && hasExcessParensWithPrecedence(e, PRECEDENCE_OF_ASSIGNMENT_EXPR)) 730 .forEach(report); 731 }, 732 733 ArrayPattern(node) { 734 node.elements 735 .filter(e => canBeAssignmentTarget(e) && hasExcessParens(e)) 736 .forEach(report); 737 }, 738 739 ArrowFunctionExpression(node) { 740 if (isReturnAssignException(node)) { 741 return; 742 } 743 744 if (node.body.type === "ConditionalExpression" && 745 IGNORE_ARROW_CONDITIONALS 746 ) { 747 return; 748 } 749 750 if (node.body.type !== "BlockStatement") { 751 const firstBodyToken = sourceCode.getFirstToken(node.body, astUtils.isNotOpeningParenToken); 752 const tokenBeforeFirst = sourceCode.getTokenBefore(firstBodyToken); 753 754 if (astUtils.isOpeningParenToken(tokenBeforeFirst) && astUtils.isOpeningBraceToken(firstBodyToken)) { 755 tokensToIgnore.add(firstBodyToken); 756 } 757 if (hasExcessParensWithPrecedence(node.body, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { 758 report(node.body); 759 } 760 } 761 }, 762 763 AssignmentExpression(node) { 764 if (canBeAssignmentTarget(node.left) && hasExcessParens(node.left)) { 765 report(node.left); 766 } 767 768 if (!isReturnAssignException(node) && hasExcessParensWithPrecedence(node.right, precedence(node))) { 769 report(node.right); 770 } 771 }, 772 773 BinaryExpression(node) { 774 if (reportsBuffer && node.operator === "in") { 775 reportsBuffer.inExpressionNodes.push(node); 776 } 777 778 checkBinaryLogical(node); 779 }, 780 781 CallExpression: checkCallNew, 782 783 ClassBody(node) { 784 node.body 785 .filter(member => member.type === "MethodDefinition" && member.computed && member.key) 786 .filter(member => hasExcessParensWithPrecedence(member.key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) 787 .forEach(member => report(member.key)); 788 }, 789 790 ConditionalExpression(node) { 791 if (isReturnAssignException(node)) { 792 return; 793 } 794 if ( 795 !isCondAssignException(node) && 796 hasExcessParensWithPrecedence(node.test, precedence({ type: "LogicalExpression", operator: "||" })) 797 ) { 798 report(node.test); 799 } 800 801 if (hasExcessParensWithPrecedence(node.consequent, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { 802 report(node.consequent); 803 } 804 805 if (hasExcessParensWithPrecedence(node.alternate, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { 806 report(node.alternate); 807 } 808 }, 809 810 DoWhileStatement(node) { 811 if (hasExcessParens(node.test) && !isCondAssignException(node)) { 812 report(node.test); 813 } 814 }, 815 816 ExportDefaultDeclaration: node => checkExpressionOrExportStatement(node.declaration), 817 ExpressionStatement: node => checkExpressionOrExportStatement(node.expression), 818 819 "ForInStatement, ForOfStatement"(node) { 820 if (node.left.type !== "VariableDeclarator") { 821 const firstLeftToken = sourceCode.getFirstToken(node.left, astUtils.isNotOpeningParenToken); 822 823 if ( 824 firstLeftToken.value === "let" && ( 825 826 /* 827 * If `let` is the only thing on the left side of the loop, it's the loop variable: `for ((let) of foo);` 828 * Removing it will cause a syntax error, because it will be parsed as the start of a VariableDeclarator. 829 */ 830 (firstLeftToken.range[1] === node.left.range[1] || /* 831 * If `let` is followed by a `[` token, it's a property access on the `let` value: `for ((let[foo]) of bar);` 832 * Removing it will cause the property access to be parsed as a destructuring declaration of `foo` instead. 833 */ 834 astUtils.isOpeningBracketToken( 835 sourceCode.getTokenAfter(firstLeftToken, astUtils.isNotClosingParenToken) 836 )) 837 ) 838 ) { 839 tokensToIgnore.add(firstLeftToken); 840 } 841 } 842 843 if (node.type === "ForOfStatement") { 844 const hasExtraParens = node.right.type === "SequenceExpression" 845 ? hasDoubleExcessParens(node.right) 846 : hasExcessParens(node.right); 847 848 if (hasExtraParens) { 849 report(node.right); 850 } 851 } else if (hasExcessParens(node.right)) { 852 report(node.right); 853 } 854 855 if (hasExcessParens(node.left)) { 856 report(node.left); 857 } 858 }, 859 860 ForStatement(node) { 861 if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) { 862 report(node.test); 863 } 864 865 if (node.update && hasExcessParens(node.update)) { 866 report(node.update); 867 } 868 869 if (node.init) { 870 startNewReportsBuffering(); 871 872 if (hasExcessParens(node.init)) { 873 report(node.init); 874 } 875 } 876 }, 877 878 "ForStatement > *.init:exit"(node) { 879 880 /* 881 * Removing parentheses around `in` expressions might change semantics and cause errors. 882 * 883 * For example, this valid for loop: 884 * for (let a = (b in c); ;); 885 * after removing parentheses would be treated as an invalid for-in loop: 886 * for (let a = b in c; ;); 887 */ 888 889 if (reportsBuffer.reports.length) { 890 reportsBuffer.inExpressionNodes.forEach(inExpressionNode => { 891 const path = pathToDescendant(node, inExpressionNode); 892 let nodeToExclude; 893 894 for (let i = 0; i < path.length; i++) { 895 const pathNode = path[i]; 896 897 if (i < path.length - 1) { 898 const nextPathNode = path[i + 1]; 899 900 if (isSafelyEnclosingInExpression(pathNode, nextPathNode)) { 901 902 // The 'in' expression in safely enclosed by the syntax of its ancestor nodes (e.g. by '{}' or '[]'). 903 return; 904 } 905 } 906 907 if (isParenthesised(pathNode)) { 908 if (isInCurrentReportsBuffer(pathNode)) { 909 910 // This node was supposed to be reported, but parentheses might be necessary. 911 912 if (isParenthesisedTwice(pathNode)) { 913 914 /* 915 * This node is parenthesised twice, it certainly has at least one pair of `extra` parentheses. 916 * If the --fix option is on, the current fixing iteration will remove only one pair of parentheses. 917 * The remaining pair is safely enclosing the 'in' expression. 918 */ 919 return; 920 } 921 922 // Exclude the outermost node only. 923 if (!nodeToExclude) { 924 nodeToExclude = pathNode; 925 } 926 927 // Don't break the loop here, there might be some safe nodes or parentheses that will stay inside. 928 929 } else { 930 931 // This node will stay parenthesised, the 'in' expression in safely enclosed by '()'. 932 return; 933 } 934 } 935 } 936 937 // Exclude the node from the list (i.e. treat parentheses as necessary) 938 removeFromCurrentReportsBuffer(nodeToExclude); 939 }); 940 } 941 942 endCurrentReportsBuffering(); 943 }, 944 945 IfStatement(node) { 946 if (hasExcessParens(node.test) && !isCondAssignException(node)) { 947 report(node.test); 948 } 949 }, 950 951 ImportExpression(node) { 952 const { source } = node; 953 954 if (source.type === "SequenceExpression") { 955 if (hasDoubleExcessParens(source)) { 956 report(source); 957 } 958 } else if (hasExcessParens(source)) { 959 report(source); 960 } 961 }, 962 963 LogicalExpression: checkBinaryLogical, 964 965 MemberExpression(node) { 966 const nodeObjHasExcessParens = hasExcessParens(node.object) && 967 !( 968 isImmediateFunctionPrototypeMethodCall(node.parent) && 969 node.parent.callee === node && 970 IGNORE_FUNCTION_PROTOTYPE_METHODS 971 ); 972 973 if ( 974 nodeObjHasExcessParens && 975 precedence(node.object) >= precedence(node) && 976 ( 977 node.computed || 978 !( 979 astUtils.isDecimalInteger(node.object) || 980 981 // RegExp literal is allowed to have parens (#1589) 982 (node.object.type === "Literal" && node.object.regex) 983 ) 984 ) 985 ) { 986 report(node.object); 987 } 988 989 if (nodeObjHasExcessParens && 990 node.object.type === "CallExpression" && 991 node.parent.type !== "NewExpression") { 992 report(node.object); 993 } 994 995 if (nodeObjHasExcessParens && 996 !IGNORE_NEW_IN_MEMBER_EXPR && 997 node.object.type === "NewExpression" && 998 isNewExpressionWithParens(node.object)) { 999 report(node.object); 1000 } 1001 1002 if (nodeObjHasExcessParens && 1003 node.optional && 1004 node.object.type === "ChainExpression" 1005 ) { 1006 report(node.object); 1007 } 1008 1009 if (node.computed && hasExcessParens(node.property)) { 1010 report(node.property); 1011 } 1012 }, 1013 1014 NewExpression: checkCallNew, 1015 1016 ObjectExpression(node) { 1017 node.properties 1018 .filter(property => property.value && hasExcessParensWithPrecedence(property.value, PRECEDENCE_OF_ASSIGNMENT_EXPR)) 1019 .forEach(property => report(property.value)); 1020 }, 1021 1022 ObjectPattern(node) { 1023 node.properties 1024 .filter(property => { 1025 const value = property.value; 1026 1027 return canBeAssignmentTarget(value) && hasExcessParens(value); 1028 }).forEach(property => report(property.value)); 1029 }, 1030 1031 Property(node) { 1032 if (node.computed) { 1033 const { key } = node; 1034 1035 if (key && hasExcessParensWithPrecedence(key, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { 1036 report(key); 1037 } 1038 } 1039 }, 1040 1041 RestElement(node) { 1042 const argument = node.argument; 1043 1044 if (canBeAssignmentTarget(argument) && hasExcessParens(argument)) { 1045 report(argument); 1046 } 1047 }, 1048 1049 ReturnStatement(node) { 1050 const returnToken = sourceCode.getFirstToken(node); 1051 1052 if (isReturnAssignException(node)) { 1053 return; 1054 } 1055 1056 if (node.argument && 1057 hasExcessParensNoLineTerminator(returnToken, node.argument) && 1058 1059 // RegExp literal is allowed to have parens (#1589) 1060 !(node.argument.type === "Literal" && node.argument.regex)) { 1061 report(node.argument); 1062 } 1063 }, 1064 1065 SequenceExpression(node) { 1066 const precedenceOfNode = precedence(node); 1067 1068 node.expressions 1069 .filter(e => hasExcessParensWithPrecedence(e, precedenceOfNode)) 1070 .forEach(report); 1071 }, 1072 1073 SwitchCase(node) { 1074 if (node.test && hasExcessParens(node.test)) { 1075 report(node.test); 1076 } 1077 }, 1078 1079 SwitchStatement(node) { 1080 if (hasExcessParens(node.discriminant)) { 1081 report(node.discriminant); 1082 } 1083 }, 1084 1085 ThrowStatement(node) { 1086 const throwToken = sourceCode.getFirstToken(node); 1087 1088 if (hasExcessParensNoLineTerminator(throwToken, node.argument)) { 1089 report(node.argument); 1090 } 1091 }, 1092 1093 UnaryExpression: checkArgumentWithPrecedence, 1094 UpdateExpression: checkArgumentWithPrecedence, 1095 AwaitExpression: checkArgumentWithPrecedence, 1096 1097 VariableDeclarator(node) { 1098 if ( 1099 node.init && hasExcessParensWithPrecedence(node.init, PRECEDENCE_OF_ASSIGNMENT_EXPR) && 1100 1101 // RegExp literal is allowed to have parens (#1589) 1102 !(node.init.type === "Literal" && node.init.regex) 1103 ) { 1104 report(node.init); 1105 } 1106 }, 1107 1108 WhileStatement(node) { 1109 if (hasExcessParens(node.test) && !isCondAssignException(node)) { 1110 report(node.test); 1111 } 1112 }, 1113 1114 WithStatement(node) { 1115 if (hasExcessParens(node.object)) { 1116 report(node.object); 1117 } 1118 }, 1119 1120 YieldExpression(node) { 1121 if (node.argument) { 1122 const yieldToken = sourceCode.getFirstToken(node); 1123 1124 if ((precedence(node.argument) >= precedence(node) && 1125 hasExcessParensNoLineTerminator(yieldToken, node.argument)) || 1126 hasDoubleExcessParens(node.argument)) { 1127 report(node.argument); 1128 } 1129 } 1130 }, 1131 1132 ClassDeclaration: checkClass, 1133 ClassExpression: checkClass, 1134 1135 SpreadElement: checkSpreadOperator, 1136 SpreadProperty: checkSpreadOperator, 1137 ExperimentalSpreadProperty: checkSpreadOperator, 1138 1139 TemplateLiteral(node) { 1140 node.expressions 1141 .filter(e => e && hasExcessParens(e)) 1142 .forEach(report); 1143 }, 1144 1145 AssignmentPattern(node) { 1146 const { left, right } = node; 1147 1148 if (canBeAssignmentTarget(left) && hasExcessParens(left)) { 1149 report(left); 1150 } 1151 1152 if (right && hasExcessParensWithPrecedence(right, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { 1153 report(right); 1154 } 1155 } 1156 }; 1157 1158 } 1159}; 1160