add var unused in disjunct query

This commit is contained in:
Erik Krogh Kristensen
2021-11-18 12:18:48 +01:00
parent 112cd9d29c
commit 97461d1f11
4 changed files with 388 additions and 0 deletions

View File

@@ -0,0 +1,212 @@
/**
* @name Var only used in one side of disjunct.
* @description Only using a variable on one side of a disjunction can cause a cartesian product.
* @kind problem
* @problem.severity warning
* @id ql/var-unused-in-disjunct
* @tags maintainability
* performance
* @precision high
*/
import ql
/**
* Holds if `node` bind `var` in a (transitive) child node.
* Is a practical approximation that ignores `not` and many other features.
*/
pragma[noinline]
predicate alwaysBindsVar(VarDef var, AstNode node) {
// base case
node.(VarAccess).getDeclaration() = var and
not isSmallType(var.getType()) // <- early pruning
or
// recursive cases
alwaysBindsVar(var, node.getAChild(_)) and // the recursive step, go one step up to the parent.
not node.(FullAggregate).getAnArgument() = var and // except if the parent defines the variable, then we stop.
not node.(Quantifier).getAnArgument() = var and
not node instanceof EffectiveDisjunction // for disjunctions, we need to check both sides.
or
exists(EffectiveDisjunction disj | disj = node |
alwaysBindsVar(var, disj.getLeft()) and
alwaysBindsVar(var, disj.getRight())
)
or
exists(EffectiveDisjunction disj | disj = node |
alwaysBindsVar(var, disj.getAnOperand()) and
disj.getAnOperand() instanceof NoneCall
)
or
exists(IfFormula ifForm | ifForm = node | alwaysBindsVar(var, ifForm.getCondition()))
}
/**
* Holds if we assume `t` is a small type, and
* variables of this type are therefore not an issue in cartesian products.
*/
predicate isSmallType(Type t) {
t.getName() = "string" // DataFlow::Configuration and the like
or
exists(NewType newType | newType = t.getDeclaration() |
forex(NewTypeBranch branch | branch = newType.getABranch() | branch.getArity() = 0)
)
or
t.getName() = "boolean"
or
exists(NewType newType | newType = t.getDeclaration() |
forex(NewTypeBranch branch | branch = newType.getABranch() |
isSmallType(branch.getReturnType())
)
)
or
exists(NewTypeBranch branch | t = branch.getReturnType() |
forall(Type param | param = branch.getParameterType(_) | isSmallType(param))
)
or
isSmallType(t.getASuperType())
}
/**
* Holds if `pred` is inlined.
*/
predicate isInlined(Predicate pred) {
exists(Annotation inline |
inline = pred.getAnAnnotation() and
inline.getName() = "pragma" and
inline.getArgs(0).getValue() = "inline"
)
or
pred.getAnAnnotation().getName() = "bindingset"
}
/**
* An AstNode that acts like a disjunction.
*/
class EffectiveDisjunction extends AstNode {
EffectiveDisjunction() {
this instanceof IfFormula
or
this instanceof Disjunction
}
/** Gets the left operand of this disjunction. */
AstNode getLeft() {
result = this.(IfFormula).getThenPart()
or
result = this.(Disjunction).getLeft()
}
/** Gets the right operand of this disjunction. */
AstNode getRight() {
result = this.(IfFormula).getElsePart()
or
result = this.(Disjunction).getRight()
}
/** Gets any of the operands of this disjunction. */
AstNode getAnOperand() { result = [this.getLeft(), this.getRight()] }
}
/**
* Holds if `disj` only uses `var` in one of its branches.
*/
pragma[noinline]
predicate onlyUseInOneBranch(EffectiveDisjunction disj, VarDef var) {
alwaysBindsVar(var, disj.getLeft()) and
not alwaysBindsVar(var, disj.getRight())
or
not alwaysBindsVar(var, disj.getLeft()) and
alwaysBindsVar(var, disj.getRight())
}
/**
* Holds if `comp` is an equality comparison that has a low number of results.
*/
predicate isTinyAssignment(ComparisonFormula comp) {
comp.getOperator() = "=" and
(
isSmallType(comp.getAnOperand().getType())
or
comp.getAnOperand() instanceof Literal
)
}
/**
* An AstNode that acts like a conjunction.
*/
class EffectiveConjunction extends AstNode {
EffectiveConjunction() {
this instanceof Conjunction
or
this instanceof Exists
}
/** Gets the left operand of this conjunction */
Formula getLeft() {
result = this.(Conjunction).getLeft()
or
result = this.(Exists).getRange()
}
/** Gets the right operand of this conjunction */
Formula getRight() {
result = this.(Conjunction).getRight()
or
result = this.(Exists).getFormula()
}
}
/**
* Holds if `node` is a sub-node of a node that always binds `var`.
*/
predicate varIsAlwaysBound(VarDef var, AstNode node) {
// base case
alwaysBindsVar(var, node) and
onlyUseInOneBranch(_, var) // <- manual magic
or
// recursive cases
exists(AstNode parent | node.getParent() = parent | varIsAlwaysBound(var, parent))
or
exists(EffectiveConjunction parent |
varIsAlwaysBound(var, parent.getLeft()) and
node = parent.getRight()
or
varIsAlwaysBound(var, parent.getRight()) and
node = parent.getLeft()
)
or
exists(IfFormula ifForm | varIsAlwaysBound(var, ifForm.getCondition()) |
node = [ifForm.getThenPart(), ifForm.getElsePart()]
)
or
exists(Forall for | varIsAlwaysBound(var, for.getRange()) | node = for.getFormula())
}
/**
* Holds if `disj` only uses `var` in one of its branches.
* And we should report it as being a bad thing.
*/
predicate badDisjunction(EffectiveDisjunction disj, VarDef var) {
onlyUseInOneBranch(disj, var) and
// it's fine if it's always bound further up
not varIsAlwaysBound(var, disj) and
// none() on one side makes everything fine. (this happens, it's a type-system hack)
not disj.getAnOperand() instanceof NoneCall and
// inlined predicates might bind unused variables in the context they are used in.
not (
isInlined(disj.getEnclosingPredicate()) and
var = disj.getEnclosingPredicate().getParameter(_)
) and
// recursion prevention never runs, it's a compile-time check, so we remove those results here
not disj.getEnclosingPredicate().getParent().(Class).getName().matches("%RecursionPrevention") and // these are by design
// not a small type
not isSmallType(var.getType()) and
// one of the branches is a tiny assignment. These are usually intentional cartesian products (and not too big).
not isTinyAssignment(disj.getAnOperand())
}
from EffectiveDisjunction disj, VarDef var
where
badDisjunction(disj, var) and
not badDisjunction(disj.getParent(), var) // avoid duplicate reporting of the same error
select disj, "The variable " + var.getName() + " is only used in one side of disjunct."

View File

@@ -0,0 +1,167 @@
import ql
class Big = Expr;
class Small extends boolean {
Small() { this = [true, false] }
}
class MyStr extends string {
MyStr() { this = ["foo", "bar"] }
}
predicate bad1(Big b) {
b.toString().matches("%foo")
or
any()
}
int bad2() {
exists(Big big, Small small |
result = big.toString().toInt()
or
result = small.toString().toInt()
)
}
float bad3(Big t) {
result = [1 .. 10].toString().toFloat() or
result = [11 .. 20].toString().toFloat() or
result = t.toString().toFloat() or
result = [21 .. 30].toString().toFloat()
}
string good1(Big t) {
(
result = t.toString()
or
result instanceof MyStr // <- t unused here, but that's ok because of the conjunct that binds t.
) and
t.toString().regexpMatch(".*foo")
}
predicate helper(Big a, Big b) {
a = b and
a.toString().matches("%foo")
}
predicate bad4(Big fromType, Big toType) {
helper(fromType, toType)
or
fromType.toString().matches("%foo")
or
helper(toType, fromType)
}
predicate good2(Big t) {
exists(Small other |
t.toString().matches("%foo")
or
other.toString().matches("%foo") // <- t unused here, but that's ok because of the conjunct (exists) that binds t.
|
t.toString().regexpMatch(".*foo")
)
}
predicate mixed1(Big good, Small small) {
good.toString().matches("%foo")
or
good =
any(Big bad |
small.toString().matches("%foo") and
// the use of good is fine, the comparison futher up binds it.
// the same is not true for bad.
(bad.toString().matches("%foo") or good.toString().regexpMatch("foo.*")) and
small.toString().regexpMatch(".*foo")
)
}
newtype OtherSmall =
Small1() or
Small2(boolean b) { b = true } or
Small3(boolean b, Small o) {
b = true and
o.toString().matches("%foo")
}
predicate good3(OtherSmall small) {
small = Small1()
or
1 = 1
}
predicate good4(Big big, Small small) {
big.toString().matches("%foo")
or
// assignment to small type, intentional cartesian product
small = any(Small s | s.toString().matches("%foo"))
}
predicate good5(Big bb, Big v, boolean certain) {
exists(Big read |
read = bb and
read = v and
certain = true
)
or
v =
any(Big lsv |
lsv.getEnclosingPredicate() = bb.(Expr).getEnclosingPredicate() and
(lsv.toString().matches("%foo") or v.toString().matches("%foo")) and
certain = false
)
}
predicate bad5(Big bb) { if none() then bb.toString().matches("%foo") else any() }
pragma[inline]
predicate good5(Big a, Big b) {
// fine. Assumes it's used somewhere where `a` and `b` are bound.
b = any(Big bb | bb.toString().matches("%foo"))
or
a = any(Big bb | bb.toString().matches("%foo"))
}
predicate bad6(Big a) {
(
a.toString().matches("%foo") // bad
or
any()
) and
(
a.toString().matches("%foo") // also bad
or
any()
)
}
predicate good6(Big a) {
a.toString().matches("%foo") and
(
a.toString().matches("%foo") // good, `a` is bound on the branch of the conjunction.
or
any()
)
}
predicate good7() {
exists(Big l, Big r |
l.toString().matches("%foo1") and
r.toString().matches("%foo2")
or
l.toString().matches("%foo3") and
r.toString().matches("%foo4")
|
not (l.toString().regexpMatch("%foo5") or r.toString().regexpMatch("%foo6")) and
(l.toString().regexpMatch("%foo7") or r.toString().regexpMatch("%foo8"))
)
}
// TOOD: Next test, this one is
string good8(int bitSize) {
if bitSize != 0
then bitSize = 1 and result = bitSize.toString()
else (
if 1 = 0 then result = "foo" else result = "bar"
)
}

View File

@@ -0,0 +1,8 @@
| Test.qll:14:3:16:7 | Disjunction | The variable b is only used in one side of disjunct. |
| Test.qll:21:5:23:37 | Disjunction | The variable big is only used in one side of disjunct. |
| Test.qll:28:3:30:33 | Disjunction | The variable t is only used in one side of disjunct. |
| Test.qll:49:3:53:26 | Disjunction | The variable toType is only used in one side of disjunct. |
| Test.qll:74:8:74:77 | Disjunction | The variable bad is only used in one side of disjunct. |
| Test.qll:115:26:115:80 | IfFormula | The variable bb is only used in one side of disjunct. |
| Test.qll:127:5:129:9 | Disjunction | The variable a is only used in one side of disjunct. |
| Test.qll:132:5:134:9 | Disjunction | The variable a is only used in one side of disjunct. |

View File

@@ -0,0 +1 @@
queries/performance/VarUnusedInDisjunct.ql