Merge pull request #260 from xiemaisi/js/confusing-precedence

Approved by esben-semmle, mc-semmle
This commit is contained in:
semmle-qlci
2018-10-03 09:07:18 +01:00
committed by GitHub
13 changed files with 126 additions and 29 deletions

View File

@@ -0,0 +1,46 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Nested expressions that rely on less well-known operator precedence rules can be
hard to read and understand. They could even indicate a bug where the author of the
code misunderstood the precedence rules.
</p>
</overview>
<recommendation>
<p>
Use parentheses or additional whitespace to clarify grouping.
</p>
</recommendation>
<example>
<p>
Consider the following snippet of code:
</p>
<sample src="examples/UnclearOperatorPrecedence.js" />
<p>
It might look like this tests whether <code>x</code> and <code>y</code> have any bits in
common, but in fact <code>==</code> binds more tightly than <code>&amp;</code>, so the test
is equivalent to <code>x &amp; (y == 0)</code>.
</p>
<p>
If this is the intended interpretation, parentheses should be used to clarify this. You could
also consider adding extra whitespace around <code>&amp;</code> or removing whitespace
around <code>==</code> to make it visually apparent that it binds less tightly:
<code>x &amp; y==0</code>.
</p>
<p>
Probably the best approach in this case, though, would be to use the <code>&amp;&amp;</code>
operator instead to clarify the intended interpretation: <code>x &amp;&amp; y == 0</code>.
</p>
</example>
<references>
<li>Mozilla Developer Network, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence">Operator precedence</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,29 @@
/**
* @name Unclear precedence of nested operators
* @description Nested expressions involving binary bitwise operators and comparisons are easy
* to misunderstand without additional disambiguating parentheses or whitespace.
* @kind problem
* @problem.severity recommendation
* @id js/unclear-operator-precedence
* @tags maintainability
* correctness
* statistical
* non-attributable
* external/cwe/cwe-783
* @precision very-high
*/
import javascript
from BitwiseBinaryExpr bit, Comparison rel, Expr other
where bit.hasOperands(rel, other) and
// only flag if whitespace doesn't clarify the nesting (note that if `bit` has less
// whitespace than `rel`, it will be reported by `js/whitespace-contradicts-precedence`)
bit.getWhitespaceAroundOperator() = rel.getWhitespaceAroundOperator() and
// don't flag if the other operand is itself a comparison,
// since the nesting tends to be visually more obvious in such cases
not other instanceof Comparison and
// don't flag occurrences in minified code
not rel.getTopLevel().isMinified()
select rel, "The '" + rel.getOperator() + "' operator binds more tightly than " +
"'" + bit.getOperator() + "', which may not be obvious in this case."

View File

@@ -54,29 +54,6 @@ class HarmlessNestedExpr extends BinaryExpr {
}
}
/** Holds if the right operand of `expr` starts on line `line`, at column `col`. */
predicate startOfBinaryRhs(BinaryExpr expr, int line, int col) {
exists(Location rloc | rloc = expr.getRightOperand().getLocation() |
rloc.getStartLine() = line and rloc.getStartColumn() = col
)
}
/** Holds if the left operand of `expr` ends on line `line`, at column `col`. */
predicate endOfBinaryLhs(BinaryExpr expr, int line, int col) {
exists(Location lloc | lloc = expr.getLeftOperand().getLocation() |
lloc.getEndLine() = line and lloc.getEndColumn() = col
)
}
/** Gets the number of whitespace characters around the operator of `expr`. */
int operatorWS(BinaryExpr expr) {
exists(int line, int lcol, int rcol |
endOfBinaryLhs(expr, line, lcol) and
startOfBinaryRhs(expr, line, rcol) and
result = rcol - lcol + 1 - expr.getOperator().length()
)
}
/**
* Holds if `inner` is an operand of `outer`, and the relative precedence
* may not be immediately clear, but is important for the semantics of
@@ -88,10 +65,8 @@ predicate interestingNesting(BinaryExpr inner, BinaryExpr outer) {
not inner instanceof HarmlessNestedExpr
}
from BinaryExpr inner, BinaryExpr outer, int wsouter, int wsinner
from BinaryExpr inner, BinaryExpr outer
where interestingNesting(inner, outer) and
wsinner = operatorWS(inner) and wsouter = operatorWS(outer) and
wsinner % 2 = 0 and wsouter % 2 = 0 and
wsinner > wsouter and
inner.getWhitespaceAroundOperator() > outer.getWhitespaceAroundOperator() and
not outer.getTopLevel().isMinified()
select outer, "Whitespace around nested operators contradicts precedence."

View File

@@ -0,0 +1,3 @@
if (x & y == 0) {
// ...
}

View File

@@ -1008,6 +1008,25 @@ class BinaryExpr extends @binaryexpr, Expr {
override ControlFlowNode getFirstControlFlowNode() {
result = getLeftOperand().getFirstControlFlowNode()
}
/**
* Gets the number of whitespace characters around the operator of this expression.
*
* This predicate is only defined if both operands are on the same line, and if the
* amount of whitespace before and after the operator are the same.
*/
int getWhitespaceAroundOperator() {
exists (Token lastLeft, Token operator, Token firstRight, int l, int c1, int c2, int c3, int c4 |
lastLeft = getLeftOperand().getLastToken() and
operator = lastLeft.getNextToken() and
firstRight = operator.getNextToken() and
lastLeft.getLocation().hasLocationInfo(_, _, _, l, c1) and
operator.getLocation().hasLocationInfo(_, l, c2, l, c3) and
firstRight.getLocation().hasLocationInfo(_, l, c4, _, _) and
result = c2-c1-1 and
result = c4-c3-1
)
}
}
/**