diff --git a/python/ql/src/semmle/python/Exprs.qll b/python/ql/src/semmle/python/Exprs.qll index 81d6989768f3..365f19d8af1f 100644 --- a/python/ql/src/semmle/python/Exprs.qll +++ b/python/ql/src/semmle/python/Exprs.qll @@ -339,7 +339,7 @@ class Ellipsis extends Ellipsis_ { * and numeric literals. */ abstract class ImmutableLiteral extends Expr { - + abstract Object getLiteralObject(); abstract boolean booleanValue(); @@ -380,7 +380,7 @@ class IntegerLiteral extends Num { override Object getLiteralObject() { py_cobjecttypes(result, theIntType()) and py_cobjectnames(result, this.getN()) or - py_cobjecttypes(result, theLongType()) and py_cobjectnames(result, this.getN()) + py_cobjecttypes(result, theLongType()) and py_cobjectnames(result, this.getN()) } override boolean booleanValue() { @@ -454,6 +454,25 @@ class ImaginaryLiteral extends Num { } +class NegativeIntegerLiteral extends ImmutableLiteral, UnaryExpr { + + NegativeIntegerLiteral() { + this.getOp() instanceof USub and + this.getOperand() instanceof IntegerLiteral + } + + override boolean booleanValue() { + result = this.getOperand().(IntegerLiteral).booleanValue() + } + + override Object getLiteralObject() { + (py_cobjecttypes(result, theIntType()) or py_cobjecttypes(result, theLongType())) + and + py_cobjectnames(result, "-" + this.getOperand().(IntegerLiteral).getN()) + } + +} + /** A unicode string expression, such as `u"\u20ac"`. Note that unadorned string constants such as "hello" are treated as Bytes for Python2, but Unicode for Python3. */ class Unicode extends StrConst { diff --git a/python/ql/src/semmle/python/Flow.qll b/python/ql/src/semmle/python/Flow.qll index 2b663323a48e..e0d713a72f16 100755 --- a/python/ql/src/semmle/python/Flow.qll +++ b/python/ql/src/semmle/python/Flow.qll @@ -1,12 +1,12 @@ import python import semmle.python.flow.NameNode private import semmle.python.pointsto.PointsTo - +private import semmle.python.Pruning /* Note about matching parent and child nodes and CFG splitting: * * As a result of CFG splitting a single AST node may have multiple CFG nodes. - * Therefore, when matching CFG nodes to children, we need to make sure that + * Therefore, when matching CFG nodes to children, we need to make sure that * we don't match the child of one CFG node to the wrong parent. * We do this by checking dominance. If the CFG node for the parent precedes that of * the child, then he child node matches the parent node if it is dominated by it. @@ -33,6 +33,10 @@ private AstNode toAst(ControlFlowNode n) { */ class ControlFlowNode extends @py_flow_node { + cached ControlFlowNode() { + not Pruner::unreachable(this) + } + /** Whether this control flow node is a load (including those in augmented assignments) */ predicate isLoad() { exists(Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this)) @@ -219,7 +223,7 @@ class ControlFlowNode extends @py_flow_node { /** Gets what this flow node might "refer-to". Performs a combination of localized (intra-procedural) points-to * analysis and global module-level analysis. This points-to analysis favours precision over recall. It is highly - * precise, but may not provide information for a significant number of flow-nodes. + * precise, but may not provide information for a significant number of flow-nodes. * If the class is unimportant then use `refersTo(value)` or `refersTo(value, origin)` instead. */ predicate refersTo(Object value, ClassObject cls, ControlFlowNode origin) { @@ -238,9 +242,9 @@ class ControlFlowNode extends @py_flow_node { PointsTo::points_to(this, context, value, cls, origin) } - /** Whether this flow node might "refer-to" to `value` which is from `origin` - * Unlike `this.refersTo(value, _, origin)` this predicate includes results - * where the class cannot be inferred. + /** Whether this flow node might "refer-to" to `value` which is from `origin` + * Unlike `this.refersTo(value, _, origin)` this predicate includes results + * where the class cannot be inferred. */ predicate refersTo(Object value, ControlFlowNode origin) { PointsTo::points_to(this, _, value, _, origin) @@ -312,7 +316,7 @@ class ControlFlowNode extends @py_flow_node { exists(BasicBlock b | start_bb_likely_reachable(b) and not end_bb_likely_reachable(b) and - /* If there is an unlikely successor edge earlier in the BB + /* If there is an unlikely successor edge earlier in the BB * than this node, then this node must be unreachable */ exists(ControlFlowNode p, int i, int j | p.(RaisingNode).unlikelySuccessor(_) and @@ -355,7 +359,7 @@ class ControlFlowNode extends @py_flow_node { } /** Whether this dominates other. - * Note that all nodes dominate themselves. + * Note that all nodes dominate themselves. */ pragma [inline] predicate dominates(ControlFlowNode other) { // This predicate is gigantic, so it must be inlined. @@ -446,7 +450,7 @@ class CallNode extends ControlFlowNode { /** Gets the flow node corresponding to the named argument of the call corresponding to this flow node */ ControlFlowNode getArgByName(string name) { - exists(Call c, Keyword k | this.getNode() = c and k = c.getAKeyword() and + exists(Call c, Keyword k | this.getNode() = c and k = c.getAKeyword() and k.getValue() = result.getNode() and k.getArg() = name and result.getBasicBlock().dominates(this.getBasicBlock())) } @@ -487,7 +491,7 @@ class AttrNode extends ControlFlowNode { /** Gets the flow node corresponding to the object of the attribute expression corresponding to this flow node, with the matching name */ ControlFlowNode getObject(string name) { - exists(Attribute a | + exists(Attribute a | this.getNode() = a and a.getObject() = result.getNode() and a.getName() = name and result.getBasicBlock().dominates(this.getBasicBlock())) @@ -558,7 +562,7 @@ class SubscriptNode extends ControlFlowNode { toAst(this) instanceof Subscript } - /** DEPRECATED: Use `getObject()` instead. + /** DEPRECATED: Use `getObject()` instead. * This will be formally deprecated before the end 2018 and removed in 2019.*/ ControlFlowNode getValue() { exists(Subscript s | this.getNode() = s and s.getObject() = result.getNode() and @@ -681,7 +685,7 @@ class UnaryExprNode extends ControlFlowNode { } /** A control flow node corresponding to a definition, that is a control flow node - * where a value is assigned to this node. + * where a value is assigned to this node. * Includes control flow nodes for the targets of assignments, simple or augmented, * and nodes implicitly assigned in class and function definitions and imports. */ @@ -908,7 +912,7 @@ class BasicBlock extends @py_flow_node { /** Whether this basic block dominates the other */ pragma[nomagic] predicate dominates(BasicBlock other) { - this = other + this = other or this.strictlyDominates(other) } @@ -917,8 +921,8 @@ class BasicBlock extends @py_flow_node { this.firstNode().getImmediateDominator().getBasicBlock() = result } - /** Dominance frontier of a node x is the set of all nodes `other` such that `this` dominates a predecessor - * of `other` but does not strictly dominate `other` */ + /** Dominance frontier of a node x is the set of all nodes `other` such that `this` dominates a predecessor + * of `other` but does not strictly dominate `other` */ predicate dominanceFrontier(BasicBlock other) { this.dominates(other.getAPredecessor()) and not this.strictlyDominates(other) } @@ -1059,3 +1063,5 @@ private predicate end_bb_likely_reachable(BasicBlock b) { ) } + + diff --git a/python/ql/src/semmle/python/Pruning.qll b/python/ql/src/semmle/python/Pruning.qll new file mode 100644 index 000000000000..8bcd4c669512 --- /dev/null +++ b/python/ql/src/semmle/python/Pruning.qll @@ -0,0 +1,618 @@ + +private import AST +private import Exprs +private import Stmts +private import Import +private import Operations + +module Pruner { + + /** A control flow node before pruning */ + class UnprunedCfgNode extends @py_flow_node { + + string toString() { none() } + + /** Gets a predecessor of this flow node */ + UnprunedCfgNode getAPredecessor() { + py_successors(result, this) + } + + /** Gets a successor of this flow node */ + UnprunedCfgNode getASuccessor() { + py_successors(this, result) + } + + /** Gets the immediate dominator of this flow node */ + UnprunedCfgNode getImmediateDominator() { + py_idoms(this, result) + } + + /* Holds if this CFG node is a branch */ + predicate isBranch() { + py_true_successors(this, _) or py_false_successors(this, _) + } + + /** Gets the syntactic element corresponding to this flow node */ + AstNode getNode() { + py_flow_bb_node(this, result, _, _) + } + + UnprunedBasicBlock getBasicBlock() { + py_flow_bb_node(this, _, result, _) + } + + /** Gets a successor for this node if the relevant condition is True. */ + UnprunedCfgNode getATrueSuccessor() { + py_true_successors(this, result) + } + + /** Gets a successor for this node if the relevant condition is False. */ + UnprunedCfgNode getAFalseSuccessor() { + py_false_successors(this, result) + } + + } + + /** A control flow node corresponding to a comparison operation, such as `x + if x: + controlled + false_successor + uncontrolled + + false_successor dominates uncontrolled, but not all of its predecessors are this (if x) + or dominated by itself. Whereas in the following code: + + if x: + while controlled: + also_controlled + false_successor + uncontrolled + + the block 'while controlled' is controlled because all of its predecessors are this (if x) + or (in the case of 'also_controlled') dominated by itself. + + The additional constraint on the predecessors of the test successor implies + that `this` strictly dominates `controlled` so that isn't necessary to check + directly. + */ + exists(UnprunedBasicBlock succ | + testIsTrue = true and succ = this.getATrueSuccessor() + or + testIsTrue = false and succ = this.getAFalseSuccessor() + | + succ.dominates(controlled) and + forall(UnprunedBasicBlock pred | pred.getASuccessor() = succ | + pred = this or succ.dominates(pred) + ) + ) + } + + /** Holds if the edge `pred->succ` is reachable only if the test in this block evaluates to `testIsTrue` */ + predicate controlsEdge(UnprunedBasicBlock pred, UnprunedBasicBlock succ, boolean testIsTrue) { + this.controls(pred, testIsTrue) and succ = pred.getASuccessor() + or + pred = this and ( + testIsTrue = true and succ = this.getATrueSuccessor() + or + testIsTrue = false and succ = this.getAFalseSuccessor() + ) + } + + } + + /** A constraint that the variable is truthy `bool(var) is True` or falsey `bool(var) is False` */ + class Truthy extends Constraint, TTruthy { + + private boolean booleanValue() { + this = TTruthy(result) + } + + override string toString() { + result = "Truthy" and this.booleanValue() = true + or + result = "Falsey" and this.booleanValue() = false + } + + override Constraint invert() { + result = TTruthy(this.booleanValue().booleanNot()) + } + + override predicate constrainsVariableToBe(boolean value) { + value = this.booleanValue() + } + + override predicate cannotBeNone() { + this.booleanValue() = true + } + + } + + /** A constraint that the variable is None `(var is None) is True` or not None `(var is None) is False`. + * This includes the `is not` operator, `x is not None` being equivalent to `not x is None` */ + class IsNone extends Constraint, TIsNone { + + private boolean isNone() { + this = TIsNone(result) + } + + override string toString() { + result = "Is None" and this.isNone() = true + or + result = "Is not None" and this.isNone() = false + } + + override Constraint invert() { + result = TIsNone(this.isNone().booleanNot()) + } + + override predicate constrainsVariableToBe(boolean value) { + value = false and this.isNone() = true + } + + override predicate cannotBeNone() { + this = TIsNone(false) + } + + } + + /** A constraint that the variable fulfils some equality or inequality to an integral constant. + * `(var op k) is True` where `op` is an equality or inequality operator and `k` is an integer constant + */ + class ConstrainedByConstant extends Constraint, TConstrainedByConstant { + + private int intValue() { + this = TConstrainedByConstant(_, result) + } + + private CompareOp getOp() { + this = TConstrainedByConstant(result, _) + } + + override string toString() { + result = this.getOp().repr() + " " + this.intValue().toString() + } + + override Constraint invert() { + result = TConstrainedByConstant(this.getOp().invert(), this.intValue()) + } + + override predicate constrainsVariableToBe(boolean value) { + this.getOp() = eq() and this.intValue() = 0 and value = false + or + value = true and ( + this.getOp() = eq() and this.intValue() != 0 + or + this.getOp() = lt() and this.intValue() <= 0 + or + this.getOp() = le() and this.intValue() < 0 + or + this.getOp() = gt() and this.intValue() >= 0 + or + this.getOp() = ge() and this.intValue() > 0 + ) + } + + predicate eq(int val) { + this = TConstrainedByConstant(eq(), val) + } + + predicate ne(int val) { + this = TConstrainedByConstant(ne(), val) + } + + override predicate cannotBeNone() { + this.getOp() = eq() + } + + /** The minimum value that a variable fulfilling this constraint may hold + * within the bounds of a signed 32 bit number. + */ + int minValue() { + this.getOp() = eq() and result = this.intValue() + or + this.getOp() = lt() and result = -2147483648 + or + this.getOp() = le() and result = -2147483648 + or + this.getOp() = gt() and result = this.intValue()+1 + or + this.getOp() = ge() and result = this.intValue() + } + + /** The maximum value that a variable fulfilling this constraint may hold + * within the bounds of a signed 32 bit number. + */ + int maxValue() { + this.getOp() = eq() and result = this.intValue() + or + this.getOp() = gt() and result = 2147483647 + or + this.getOp() = ge() and result = 2147483647 + or + this.getOp() = lt() and result = this.intValue()-1 + or + this.getOp() = le() and result = this.intValue() + } + + } + + /** Holds if the control flow node `n` is unreachable due to + * one or more constraints. + */ + predicate unreachable(UnprunedCfgNode n) { + exists(UnprunedBasicBlock bb | + unreachableBB(bb) and bb.contains(n) + ) + } + + /** Holds if the basic block `bb` is unreachable due to + * one or more constraints. + */ + predicate unreachableBB(UnprunedBasicBlock bb) { + not bb.isEntry() and + forall(UnprunedBasicBlock pred | + pred.getASuccessor() = bb + | + unreachableEdge(pred, bb) + ) + } + + Constraint constraintFromTest(SsaVariable var, UnprunedCfgNode node) { + py_ssa_use(node, var) and result = TTruthy(true) + or + exists(boolean b | + none_test(node, var, b) and result = TIsNone(b) + ) + or + exists(CompareOp op, int k | + int_test(node, var, op, k) and + result = TConstrainedByConstant(op, k) + ) + or + result = constraintFromTest(var, node.(UnprunedNot).getOperand()).invert() + } + + predicate none_test(UnprunedCompareNode test, SsaVariable var, boolean is) { + exists(UnprunedCfgNode left, Cmpop op, UnprunedCfgNode right | + py_ssa_use(left, var) and + test.operands(left, op, right) and + right.getNode() instanceof None + | + op instanceof Is and is = true + or + op instanceof IsNot and is = false + ) + } + + predicate int_test(UnprunedCfgNode test, SsaVariable var, CompareOp op, int k) { + exists(UnprunedCfgNode left, UnprunedCfgNode right, Cmpop cop | + test.(UnprunedCompareNode).operands(left, cop, right) + | + op.forOp(cop) and + py_ssa_use(left, var) and + right.getNode().(IntegerLiteral).getValue() = k + or + op.reverse().forOp(cop) and + py_ssa_use(right, var) and + left.getNode().(IntegerLiteral).getValue() = k + ) + or + int_test(test.(UnprunedNot).getOperand(), var, op.invert(), k) + } + + predicate int_assignment(UnprunedCfgNode asgn, SsaVariable var, CompareOp op, int k) { + exists(Assign a | + a.getATarget() = asgn.getNode() and + py_ssa_use(asgn, var) and + k = a.getValue().(IntegerLiteral).getValue() and + op = eq() + ) + } + + predicate none_assignment(UnprunedCfgNode asgn, SsaVariable var) { + exists(Assign a | + a.getATarget() = asgn.getNode() and + py_ssa_use(asgn, var) and + a.getValue() instanceof None + ) + } + + boolean truthy_assignment(UnprunedCfgNode asgn, SsaVariable var) { + exists(Assign a | + a.getATarget() = asgn.getNode() and + py_ssa_use(asgn, var) + | + a.getValue() instanceof True and result = true + or + a.getValue() instanceof False and result = false + ) + or + module_import(asgn, var) and result = true + } + + /** Gets the constraint on `var` resulting from the assignment in `asgn` */ + Constraint constraintFromAssignment(SsaVariable var, UnprunedBasicBlock asgn) { + exists(CompareOp op, int k | + int_assignment(asgn.getANode(), var, op, k) and + result = TConstrainedByConstant(op, k) + ) + or + none_assignment(asgn.getANode(), var) and result = TIsNone(true) + or + result = TTruthy(truthy_assignment(asgn.getANode(), var)) + } + + /** Holds if the constraint `preval` holds for `var` on edge `pred` -> `succ` as a result of a prior test or assignment */ + pragma [nomagic] + predicate priorConstraint(UnprunedBasicBlock pred, UnprunedBasicBlock succ, Constraint preval, SsaVariable var) { + not (blacklisted(var) and preval = TTruthy(_)) + and + not var.getVariable().escapes() + and + exists(UnprunedBasicBlock first | + not first = pred and + first.(UnprunedConditionBlock).controlsEdge(pred, succ, true) and + preval = constraintFromTest(var, first.last()) + or + not first = pred and + first.(UnprunedConditionBlock).controlsEdge(pred, succ, false) and + preval = constraintFromTest(var, first.last()).invert() + or + preval = constraintFromAssignment(var, first) and + first.dominates(pred) and + (succ = pred.getAFalseSuccessor() or succ = pred.getATrueSuccessor()) + ) + } + + /** Holds if `cond` holds for `var` on conditional edge `pred` -> `succ` as a result of the test for that edge */ + predicate constraintOnBranch(UnprunedBasicBlock pred, UnprunedBasicBlock succ, Constraint cond, SsaVariable var) { + cond = constraintFromTest(var, pred.last()) and + succ = pred.getATrueSuccessor() + or + cond = constraintFromTest(var, pred.last()).invert() and + succ = pred.getAFalseSuccessor() + } + + /** Holds if the pair of constraints (`preval`, `postcond`) holds on the edge `pred` -> `succ` for some SSA variable */ + predicate controllingConditions(UnprunedBasicBlock pred, UnprunedBasicBlock succ, Constraint preval, Constraint postcond) { + exists(SsaVariable var | + priorConstraint(pred, succ, preval, var) and + constraintOnBranch(pred, succ, postcond, var) + ) + } + + /** Holds if the edge `pred` -> `succ` should be pruned as it cannot be reached */ + predicate unreachableEdge(UnprunedBasicBlock pred, UnprunedBasicBlock succ) { + exists(Constraint pre, Constraint cond | + controllingConditions(pred, succ, pre, cond) and + contradicts(pre, cond) + ) + or + unreachableBB(pred) and succ = pred.getASuccessor() + or + simply_dead(pred, succ) + } + + /* Helper for `unreachableEdge`, deal with inequalities here to avoid blow up */ + pragma [inline] + private predicate contradicts(Constraint a, Constraint b) { + a = TIsNone(true) and b.cannotBeNone() + or + a.cannotBeNone() and b = TIsNone(true) + or + a.constrainsVariableToBe(true) and b.constrainsVariableToBe(false) + or + a.constrainsVariableToBe(false) and b.constrainsVariableToBe(true) + or + a.(ConstrainedByConstant).minValue() > b.(ConstrainedByConstant).maxValue() + or + a.(ConstrainedByConstant).maxValue() < b.(ConstrainedByConstant).minValue() + or + exists(int val | + a.(ConstrainedByConstant).eq(val) and b.(ConstrainedByConstant).ne(val) + or + a.(ConstrainedByConstant).ne(val) and b.(ConstrainedByConstant).eq(val) + ) + } + + /** Holds if edge is simply dead. Stuff like `if False: ...` */ + predicate simply_dead(UnprunedBasicBlock pred, UnprunedBasicBlock succ) { + constTest(pred.last()) = true and pred.getAFalseSuccessor() = succ + or + constTest(pred.last()) = false and pred.getATrueSuccessor() = succ + } + + /* Helper for simply_dead */ + private boolean constTest(UnprunedCfgNode node) { + exists(ImmutableLiteral lit | + result = lit.booleanValue() and lit = node.getNode() + ) + or + result = constTest(node.(UnprunedNot).getOperand()).booleanNot() + } + + /** Holds if `var` is blacklisted as having possibly been mutated */ + predicate blacklisted(SsaVariable var) { + possibly_mutated(var) and not whitelisted(var) + } + + predicate possibly_mutated(SsaVariable var) { + exists(Subscript subscr, UnprunedCfgNode node | + subscr.getObject() = node.getNode() and + py_ssa_use(node, var) + ) + or + exists(Attribute attr, UnprunedCfgNode node | + attr.getObject() = node.getNode() and + py_ssa_use(node, var) + ) + } + + /** If SSA variable is defined by an import, then it should + * be whitelisted as taking an attribute cannot change its + * truthiness. + */ + predicate whitelisted(SsaVariable var) { + module_import(_, var) + } + + private predicate module_import(UnprunedCfgNode asgn, SsaVariable var) { + exists(Alias alias | + alias.getValue() instanceof ImportExpr and + py_ssa_defn(var, asgn) and + alias.getAsname() = asgn.getNode() + ) + } +} + diff --git a/python/ql/test/library-tests/ControlFlow/pruning/Constraint.expected b/python/ql/test/library-tests/ControlFlow/pruning/Constraint.expected new file mode 100644 index 000000000000..975f3efdcde1 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/pruning/Constraint.expected @@ -0,0 +1,107 @@ +| 8 | test | test | Truthy | +| 10 | test | test | Truthy | +| 14 | seq | seq | Truthy | +| 16 | seq | seq | Truthy | +| 17 | seq | seq | Truthy | +| 21 | UnaryExpr | t1 | Falsey | +| 21 | t1 | t1 | Truthy | +| 24 | t1 | t1 | Truthy | +| 25 | t1 | t1 | Truthy | +| 26 | t2 | t2 | Truthy | +| 29 | t2 | t2 | Truthy | +| 30 | t2 | t2 | Truthy | +| 31 | t3 | t3 | Truthy | +| 31 | t4 | t4 | Truthy | +| 32 | t3 | t3 | Truthy | +| 33 | t3 | t3 | Truthy | +| 34 | t3 | t3 | Truthy | +| 35 | t4 | t4 | Truthy | +| 36 | t5 | t5 | Truthy | +| 36 | t6 | t6 | Truthy | +| 37 | t5 | t5 | Truthy | +| 38 | t5 | t5 | Truthy | +| 39 | t6 | t6 | Truthy | +| 40 | t6 | t6 | Truthy | +| 43 | t1 | t1 | Truthy | +| 44 | UnaryExpr | t2 | Falsey | +| 44 | t2 | t2 | Truthy | +| 47 | t1 | t1 | Truthy | +| 48 | t2 | t2 | Truthy | +| 49 | t2 | t2 | Truthy | +| 51 | t2 | t2 | Truthy | +| 52 | t2 | t2 | Truthy | +| 55 | seq1 | seq1 | Truthy | +| 57 | UnaryExpr | seq2 | Falsey | +| 57 | seq2 | seq2 | Truthy | +| 60 | seq1 | seq1 | Truthy | +| 62 | seq1 | seq1 | Truthy | +| 63 | seq2 | seq2 | Truthy | +| 65 | seq2 | seq2 | Truthy | +| 66 | seq3 | seq3 | Truthy | +| 68 | UnaryExpr | seq4 | Falsey | +| 68 | seq4 | seq4 | Truthy | +| 71 | seq3 | seq3 | Truthy | +| 73 | var | var | Truthy | +| 74 | seq4 | seq4 | Truthy | +| 76 | var | var | Truthy | +| 78 | seq5 | seq5 | Truthy | +| 80 | seq5 | seq5 | Truthy | +| 81 | seq5 | seq5 | Truthy | +| 83 | var | var | Truthy | +| 88 | UnaryExpr | x | Falsey | +| 88 | x | x | Truthy | +| 89 | Exception | Exception | Truthy | +| 90 | y | y | Truthy | +| 91 | Exception | Exception | Truthy | +| 92 | make_a_call | make_a_call | Truthy | +| 93 | UnaryExpr | x | Falsey | +| 93 | x | x | Truthy | +| 94 | count | count | Truthy | +| 95 | y | y | Truthy | +| 96 | count | count | Truthy | +| 101 | make_a_call | make_a_call | Truthy | +| 102 | UnaryExpr | another_module | Falsey | +| 102 | another_module | another_module | Truthy | +| 103 | count | count | Truthy | +| 107 | UnaryExpr | t1 | Falsey | +| 107 | t1 | t1 | Truthy | +| 109 | t2 | t2 | Truthy | +| 111 | t1 | t1 | Truthy | +| 113 | UnaryExpr | t2 | Falsey | +| 113 | t2 | t2 | Truthy | +| 117 | UnaryExpr | test | Falsey | +| 117 | test | test | Truthy | +| 119 | UnaryExpr | test | Falsey | +| 119 | test | test | Truthy | +| 123 | m | m | Truthy | +| 125 | m | m | Truthy | +| 126 | m | m | Truthy | +| 158 | Compare | ps | Is not None | +| 158 | ps | ps | Truthy | +| 159 | ps | ps | Truthy | +| 160 | Compare | ps | Is None | +| 160 | ps | ps | Truthy | +| 171 | __name__ | __name__ | Truthy | +| 172 | None | None | Truthy | +| 174 | func | func | Truthy | +| 175 | Exception | Exception | Truthy | +| 176 | count | count | Truthy | +| 177 | Compare | escapes | Is None | +| 177 | None | None | Truthy | +| 177 | escapes | escapes | Truthy | +| 178 | count | count | Truthy | +| 180 | count | count | Truthy | +| 188 | true12 | true12 | Truthy | +| 195 | Compare | x | < 4 | +| 195 | x | x | Truthy | +| 197 | Compare | x | < 4 | +| 197 | x | x | Truthy | +| 201 | Compare | x | < 4 | +| 201 | x | x | Truthy | +| 203 | Compare | x | >= 4 | +| 203 | UnaryExpr | x | < 4 | +| 203 | x | x | Truthy | +| 207 | Compare | x | < 4 | +| 207 | x | x | Truthy | +| 209 | Compare | x | < 4 | +| 209 | x | x | Truthy | diff --git a/python/ql/test/library-tests/ControlFlow/pruning/Constraint.ql b/python/ql/test/library-tests/ControlFlow/pruning/Constraint.ql new file mode 100644 index 000000000000..22e10fc51f5f --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/pruning/Constraint.ql @@ -0,0 +1,10 @@ + +import python + +import semmle.python.Pruning + +from Pruner::Constraint c, SsaVariable var, Pruner::UnprunedCfgNode node, int line +where c = Pruner::constraintFromTest(var, node) and line = node.getNode().getLocation().getStartLine() and +line > 0 +select line, node.getNode().toString(), var.getId(), c + diff --git a/python/ql/test/library-tests/ControlFlow/pruning/options b/python/ql/test/library-tests/ControlFlow/pruning/options new file mode 100644 index 000000000000..ebbd9efa4332 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/pruning/options @@ -0,0 +1,2 @@ +semmle-extractor-options: --dont-prune-graph +optimize: true diff --git a/python/ql/test/library-tests/ControlFlow/pruning/test.expected b/python/ql/test/library-tests/ControlFlow/pruning/test.expected index b8cd5b6b98d7..77cfc8e68749 100644 --- a/python/ql/test/library-tests/ControlFlow/pruning/test.expected +++ b/python/ql/test/library-tests/ControlFlow/pruning/test.expected @@ -43,3 +43,13 @@ | 154 | 1 | | 161 | 1 | | 163 | 1 | +| 176 | 1 | +| 178 | 1 | +| 180 | 1 | +| 184 | 1 | +| 186 | 1 | +| 189 | 1 | +| 192 | 1 | +| 198 | 0 | +| 204 | 0 | +| 210 | 0 | diff --git a/python/ql/test/library-tests/ControlFlow/pruning/test.py b/python/ql/test/library-tests/ControlFlow/pruning/test.py index bdc43c414ad5..1e43090e8d7d 100644 --- a/python/ql/test/library-tests/ControlFlow/pruning/test.py +++ b/python/ql/test/library-tests/ControlFlow/pruning/test.py @@ -3,20 +3,20 @@ def dead(): return 0 count - + def conditional_dead(test): if test: return if test: count - + def made_true(seq): if seq: return seq.append(1) if seq: count - + def boolop(t1, t2, t3, t4, t5, t6): if not t1: return @@ -38,7 +38,7 @@ def boolop(t1, t2, t3, t4, t5, t6): t5 and count t6 or count t6 and count - + def with_splitting(t1, t2): if t1: if not t2: @@ -50,7 +50,7 @@ def with_splitting(t1, t2): else: t2 or count t2 and count - + def loops(seq1, seq2, seq3, seq4, seq5): if seq1: return @@ -82,7 +82,7 @@ def loops(seq1, seq2, seq3, seq4, seq5): count print(var) -#Logic does not apply to global variables in calls, +#Logic does not apply to global variables in calls, #as they may be changed from true to false externally. from some_module import x, y if not x: @@ -102,7 +102,7 @@ def loops(seq1, seq2, seq3, seq4, seq5): if not another_module: count - + def negated_conditional_live(t1, t2): if not t1: return @@ -112,13 +112,13 @@ def negated_conditional_live(t1, t2): count if not t2: count - + def negated_conditional_dead(test): if not test: return if not test: count - + def made_true2(m): if m: return @@ -162,3 +162,50 @@ def attribute_lookup_cannot_effect_comparisons_with_immutable_constants(ps): else: count +def func(): + global escapes + so_something() + escapes = True + +#Don't prune on `escapes` as it escapes. +if __name__ == '__main__': + escapes = None # global + try: + func() + except Exception as err: + count + if escapes is None: + count + else: + count + +def func2(): + while 1: + count + if cond12: + count + try: + true12() + count + except IOError: + true12 = 0 + count + +def inequality1(x): + if x < 4: + return + if x < 4: + count + +def inequality2(x): + if x < 4: + return + if not x >= 4: + count + +def reversed_inequality(x): + if x < 4: + return + if 4 > x: + count +