|
|
|
/**
|
|
|
|
* @fileoverview Disallow parenthesising higher precedence subexpressions.
|
|
|
|
* @author Michael Ficarra
|
|
|
|
* @copyright 2014 Michael Ficarra. All rights reserved.
|
|
|
|
* See LICENSE file in root directory for full license.
|
|
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Rule Definition
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
module.exports = function(context) {
|
|
|
|
|
|
|
|
var ALL_NODES = context.options[0] !== "functions";
|
|
|
|
var EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false;
|
|
|
|
var sourceCode = context.getSourceCode();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if this rule should be enforced for a node given the current configuration.
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the rule should be enforced for this node.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function ruleApplies(node) {
|
|
|
|
return ALL_NODES || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a node is surrounded by parentheses.
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the node is parenthesised.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function isParenthesised(node) {
|
|
|
|
var previousToken = context.getTokenBefore(node),
|
|
|
|
nextToken = context.getTokenAfter(node);
|
|
|
|
|
|
|
|
return previousToken && nextToken &&
|
|
|
|
previousToken.value === "(" && previousToken.range[1] <= node.range[0] &&
|
|
|
|
nextToken.value === ")" && nextToken.range[0] >= node.range[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a node is surrounded by parentheses twice.
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the node is doubly parenthesised.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function isParenthesisedTwice(node) {
|
|
|
|
var previousToken = context.getTokenBefore(node, 1),
|
|
|
|
nextToken = context.getTokenAfter(node, 1);
|
|
|
|
|
|
|
|
return isParenthesised(node) && previousToken && nextToken &&
|
|
|
|
previousToken.value === "(" && previousToken.range[1] <= node.range[0] &&
|
|
|
|
nextToken.value === ")" && nextToken.range[0] >= node.range[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a node is surrounded by (potentially) invalid parentheses.
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the node is incorrectly parenthesised.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function hasExcessParens(node) {
|
|
|
|
return ruleApplies(node) && isParenthesised(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a node that is expected to be parenthesised is surrounded by
|
|
|
|
* (potentially) invalid extra parentheses.
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the node is has an unexpected extra pair of parentheses.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function hasDoubleExcessParens(node) {
|
|
|
|
return ruleApplies(node) && isParenthesisedTwice(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a node test expression is allowed to have a parenthesised assignment
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the assignment can be parenthesised.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function isCondAssignException(node) {
|
|
|
|
return EXCEPT_COND_ASSIGN && node.test.type === "AssignmentExpression";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a node following a [no LineTerminator here] restriction is
|
|
|
|
* surrounded by (potentially) invalid extra parentheses.
|
|
|
|
* @param {Token} token - The token preceding the [no LineTerminator here] restriction.
|
|
|
|
* @param {ASTNode} node - The node to be checked.
|
|
|
|
* @returns {boolean} True if the node is incorrectly parenthesised.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function hasExcessParensNoLineTerminator(token, node) {
|
|
|
|
if (token.loc.end.line === node.loc.start.line) {
|
|
|
|
return hasExcessParens(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
return hasDoubleExcessParens(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether or not a given node is located at the head of ExpressionStatement.
|
|
|
|
* @param {ASTNode} node - A node to check.
|
|
|
|
* @returns {boolean} `true` if the node is located at the head of ExpressionStatement.
|
|
|
|
*/
|
|
|
|
function isHeadOfExpressionStatement(node) {
|
|
|
|
var parent = node.parent;
|
|
|
|
while (parent) {
|
|
|
|
switch (parent.type) {
|
|
|
|
case "SequenceExpression":
|
|
|
|
if (parent.expressions[0] !== node || isParenthesised(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "UnaryExpression":
|
|
|
|
case "UpdateExpression":
|
|
|
|
if (parent.prefix || isParenthesised(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "BinaryExpression":
|
|
|
|
case "LogicalExpression":
|
|
|
|
if (parent.left !== node || isParenthesised(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "ConditionalExpression":
|
|
|
|
if (parent.test !== node || isParenthesised(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "CallExpression":
|
|
|
|
if (parent.callee !== node || isParenthesised(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "MemberExpression":
|
|
|
|
if (parent.object !== node || isParenthesised(node)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "ExpressionStatement":
|
|
|
|
return true;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
node = parent;
|
|
|
|
parent = parent.parent;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* istanbul ignore next */
|
|
|
|
throw new Error("unreachable");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the precedence level based on the node type
|
|
|
|
* @param {ASTNode} node node to evaluate
|
|
|
|
* @returns {int} precedence level
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function precedence(node) {
|
|
|
|
|
|
|
|
switch (node.type) {
|
|
|
|
case "SequenceExpression":
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case "AssignmentExpression":
|
|
|
|
case "ArrowFunctionExpression":
|
|
|
|
case "YieldExpression":
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
case "ConditionalExpression":
|
|
|
|
return 3;
|
|
|
|
|
|
|
|
case "LogicalExpression":
|
|
|
|
switch (node.operator) {
|
|
|
|
case "||":
|
|
|
|
return 4;
|
|
|
|
case "&&":
|
|
|
|
return 5;
|
|
|
|
// no default
|
|
|
|
}
|
|
|
|
|
|
|
|
/* falls through */
|
|
|
|
case "BinaryExpression":
|
|
|
|
switch (node.operator) {
|
|
|
|
case "|":
|
|
|
|
return 6;
|
|
|
|
case "^":
|
|
|
|
return 7;
|
|
|
|
case "&":
|
|
|
|
return 8;
|
|
|
|
case "==":
|
|
|
|
case "!=":
|
|
|
|
case "===":
|
|
|
|
case "!==":
|
|
|
|
return 9;
|
|
|
|
case "<":
|
|
|
|
case "<=":
|
|
|
|
case ">":
|
|
|
|
case ">=":
|
|
|
|
case "in":
|
|
|
|
case "instanceof":
|
|
|
|
return 10;
|
|
|
|
case "<<":
|
|
|
|
case ">>":
|
|
|
|
case ">>>":
|
|
|
|
return 11;
|
|
|
|
case "+":
|
|
|
|
case "-":
|
|
|
|
return 12;
|
|
|
|
case "*":
|
|
|
|
case "/":
|
|
|
|
case "%":
|
|
|
|
return 13;
|
|
|
|
// no default
|
|
|
|
}
|
|
|
|
/* falls through */
|
|
|
|
case "UnaryExpression":
|
|
|
|
return 14;
|
|
|
|
case "UpdateExpression":
|
|
|
|
return 15;
|
|
|
|
case "CallExpression":
|
|
|
|
// IIFE is allowed to have parens in any position (#655)
|
|
|
|
if (node.callee.type === "FunctionExpression") {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return 16;
|
|
|
|
case "NewExpression":
|
|
|
|
return 17;
|
|
|
|
// no default
|
|
|
|
}
|
|
|
|
return 18;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Report the node
|
|
|
|
* @param {ASTNode} node node to evaluate
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function report(node) {
|
|
|
|
var previousToken = context.getTokenBefore(node);
|
|
|
|
context.report(node, previousToken.loc.start, "Gratuitous parentheses around expression.");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Evaluate Unary update
|
|
|
|
* @param {ASTNode} node node to evaluate
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function dryUnaryUpdate(node) {
|
|
|
|
if (hasExcessParens(node.argument) && precedence(node.argument) >= precedence(node)) {
|
|
|
|
report(node.argument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Evaluate a new call
|
|
|
|
* @param {ASTNode} node node to evaluate
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function dryCallNew(node) {
|
|
|
|
if (hasExcessParens(node.callee) && precedence(node.callee) >= precedence(node) && !(
|
|
|
|
node.type === "CallExpression" &&
|
|
|
|
node.callee.type === "FunctionExpression" &&
|
|
|
|
// One set of parentheses are allowed for a function expression
|
|
|
|
!hasDoubleExcessParens(node.callee)
|
|
|
|
)) {
|
|
|
|
report(node.callee);
|
|
|
|
}
|
|
|
|
if (node.arguments.length === 1) {
|
|
|
|
if (hasDoubleExcessParens(node.arguments[0]) && precedence(node.arguments[0]) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(node.arguments[0]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
[].forEach.call(node.arguments, function(arg) {
|
|
|
|
if (hasExcessParens(arg) && precedence(arg) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(arg);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Evaluate binary logicals
|
|
|
|
* @param {ASTNode} node node to evaluate
|
|
|
|
* @returns {void}
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
function dryBinaryLogical(node) {
|
|
|
|
var prec = precedence(node);
|
|
|
|
if (hasExcessParens(node.left) && precedence(node.left) >= prec) {
|
|
|
|
report(node.left);
|
|
|
|
}
|
|
|
|
if (hasExcessParens(node.right) && precedence(node.right) > prec) {
|
|
|
|
report(node.right);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
"ArrayExpression": function(node) {
|
|
|
|
[].forEach.call(node.elements, function(e) {
|
|
|
|
if (e && hasExcessParens(e) && precedence(e) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
"ArrowFunctionExpression": function(node) {
|
|
|
|
if (node.body.type !== "BlockStatement") {
|
|
|
|
if (node.body.type !== "ObjectExpression" && hasExcessParens(node.body) && precedence(node.body) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(node.body);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Object literals *must* be parenthesised
|
|
|
|
if (node.body.type === "ObjectExpression" && hasDoubleExcessParens(node.body)) {
|
|
|
|
report(node.body);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"AssignmentExpression": function(node) {
|
|
|
|
if (hasExcessParens(node.right) && precedence(node.right) >= precedence(node)) {
|
|
|
|
report(node.right);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"BinaryExpression": dryBinaryLogical,
|
|
|
|
"CallExpression": dryCallNew,
|
|
|
|
"ConditionalExpression": function(node) {
|
|
|
|
if (hasExcessParens(node.test) && precedence(node.test) >= precedence({type: "LogicalExpression", operator: "||"})) {
|
|
|
|
report(node.test);
|
|
|
|
}
|
|
|
|
if (hasExcessParens(node.consequent) && precedence(node.consequent) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(node.consequent);
|
|
|
|
}
|
|
|
|
if (hasExcessParens(node.alternate) && precedence(node.alternate) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(node.alternate);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"DoWhileStatement": function(node) {
|
|
|
|
if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
|
|
|
|
report(node.test);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ExpressionStatement": function(node) {
|
|
|
|
var firstToken, secondToken, firstTokens;
|
|
|
|
if (hasExcessParens(node.expression)) {
|
|
|
|
firstTokens = context.getFirstTokens(node.expression, 2);
|
|
|
|
firstToken = firstTokens[0];
|
|
|
|
secondToken = firstTokens[1];
|
|
|
|
|
|
|
|
if (
|
|
|
|
!firstToken ||
|
|
|
|
firstToken.value !== "{" &&
|
|
|
|
firstToken.value !== "function" &&
|
|
|
|
firstToken.value !== "class" &&
|
|
|
|
(
|
|
|
|
firstToken.value !== "let" ||
|
|
|
|
!secondToken ||
|
|
|
|
secondToken.value !== "["
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
report(node.expression);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ForInStatement": function(node) {
|
|
|
|
if (hasExcessParens(node.right)) {
|
|
|
|
report(node.right);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ForOfStatement": function(node) {
|
|
|
|
if (hasExcessParens(node.right)) {
|
|
|
|
report(node.right);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ForStatement": function(node) {
|
|
|
|
if (node.init && hasExcessParens(node.init)) {
|
|
|
|
report(node.init);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.test && hasExcessParens(node.test) && !isCondAssignException(node)) {
|
|
|
|
report(node.test);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.update && hasExcessParens(node.update)) {
|
|
|
|
report(node.update);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"IfStatement": function(node) {
|
|
|
|
if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
|
|
|
|
report(node.test);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"LogicalExpression": dryBinaryLogical,
|
|
|
|
"MemberExpression": function(node) {
|
|
|
|
if (
|
|
|
|
hasExcessParens(node.object) &&
|
|
|
|
precedence(node.object) >= precedence(node) &&
|
|
|
|
(
|
|
|
|
node.computed ||
|
|
|
|
!(
|
|
|
|
(node.object.type === "Literal" &&
|
|
|
|
typeof node.object.value === "number" &&
|
|
|
|
/^[0-9]+$/.test(context.getFirstToken(node.object).value))
|
|
|
|
||
|
|
|
|
// RegExp literal is allowed to have parens (#1589)
|
|
|
|
(node.object.type === "Literal" && node.object.regex)
|
|
|
|
)
|
|
|
|
) &&
|
|
|
|
!(
|
|
|
|
(node.object.type === "FunctionExpression" || node.object.type === "ClassExpression") &&
|
|
|
|
isHeadOfExpressionStatement(node) &&
|
|
|
|
!hasDoubleExcessParens(node.object)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
report(node.object);
|
|
|
|
}
|
|
|
|
if (node.computed && hasExcessParens(node.property)) {
|
|
|
|
report(node.property);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"NewExpression": dryCallNew,
|
|
|
|
"ObjectExpression": function(node) {
|
|
|
|
[].forEach.call(node.properties, function(e) {
|
|
|
|
var v = e.value;
|
|
|
|
if (v && hasExcessParens(v) && precedence(v) >= precedence({type: "AssignmentExpression"})) {
|
|
|
|
report(v);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
"ReturnStatement": function(node) {
|
|
|
|
var returnToken = sourceCode.getFirstToken(node);
|
|
|
|
|
|
|
|
if (node.argument &&
|
|
|
|
hasExcessParensNoLineTerminator(returnToken, node.argument) &&
|
|
|
|
// RegExp literal is allowed to have parens (#1589)
|
|
|
|
!(node.argument.type === "Literal" && node.argument.regex)) {
|
|
|
|
report(node.argument);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"SequenceExpression": function(node) {
|
|
|
|
[].forEach.call(node.expressions, function(e) {
|
|
|
|
if (hasExcessParens(e) && precedence(e) >= precedence(node)) {
|
|
|
|
report(e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
"SwitchCase": function(node) {
|
|
|
|
if (node.test && hasExcessParens(node.test)) {
|
|
|
|
report(node.test);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"SwitchStatement": function(node) {
|
|
|
|
if (hasDoubleExcessParens(node.discriminant)) {
|
|
|
|
report(node.discriminant);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"ThrowStatement": function(node) {
|
|
|
|
var throwToken = sourceCode.getFirstToken(node);
|
|
|
|
|
|
|
|
if (hasExcessParensNoLineTerminator(throwToken, node.argument)) {
|
|
|
|
report(node.argument);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"UnaryExpression": dryUnaryUpdate,
|
|
|
|
"UpdateExpression": dryUnaryUpdate,
|
|
|
|
"VariableDeclarator": function(node) {
|
|
|
|
if (node.init && hasExcessParens(node.init) &&
|
|
|
|
precedence(node.init) >= precedence({type: "AssignmentExpression"}) &&
|
|
|
|
// RegExp literal is allowed to have parens (#1589)
|
|
|
|
!(node.init.type === "Literal" && node.init.regex)) {
|
|
|
|
report(node.init);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"WhileStatement": function(node) {
|
|
|
|
if (hasDoubleExcessParens(node.test) && !isCondAssignException(node)) {
|
|
|
|
report(node.test);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"WithStatement": function(node) {
|
|
|
|
if (hasDoubleExcessParens(node.object)) {
|
|
|
|
report(node.object);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"YieldExpression": function(node) {
|
|
|
|
var yieldToken;
|
|
|
|
|
|
|
|
if (node.argument) {
|
|
|
|
yieldToken = sourceCode.getFirstToken(node);
|
|
|
|
|
|
|
|
if ((precedence(node.argument) >= precedence(node) &&
|
|
|
|
hasExcessParensNoLineTerminator(yieldToken, node.argument)) ||
|
|
|
|
hasDoubleExcessParens(node.argument)) {
|
|
|
|
report(node.argument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports.schema = {
|
|
|
|
"anyOf": [
|
|
|
|
{
|
|
|
|
"type": "array",
|
|
|
|
"items": [
|
|
|
|
{
|
|
|
|
"enum": ["functions"]
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"minItems": 0,
|
|
|
|
"maxItems": 1
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"type": "array",
|
|
|
|
"items": [
|
|
|
|
{
|
|
|
|
"enum": ["all"]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"type": "object",
|
|
|
|
"properties": {
|
|
|
|
"conditionalAssign": {"type": "boolean"}
|
|
|
|
},
|
|
|
|
"additionalProperties": false
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"minItems": 0,
|
|
|
|
"maxItems": 2
|
|
|
|
}
|
|
|
|
]
|
|
|
|
};
|