mirror of
https://github.com/github/codeql.git
synced 2026-07-28 22:36:50 +02:00
Merge pull request #21921 from github/yoff/python-add-new-cfg-library
Python: add new shared-CFG-backed control flow graph (additive)
This commit is contained in:
2
python/ql/consistency-queries/CfgConsistency.ql
Normal file
2
python/ql/consistency-queries/CfgConsistency.ql
Normal file
@@ -0,0 +1,2 @@
|
||||
import semmle.python.controlflow.internal.AstNodeImpl
|
||||
import ControlFlow::Consistency
|
||||
41
python/ql/lib/ide-contextual-queries/printCfg.ql
Normal file
41
python/ql/lib/ide-contextual-queries/printCfg.ql
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @name Print CFG
|
||||
* @description Produces a representation of a file's Control Flow Graph.
|
||||
* This query is used by the VS Code extension.
|
||||
* @id py/print-cfg
|
||||
* @kind graph
|
||||
* @tags ide-contextual-queries/print-cfg
|
||||
*/
|
||||
|
||||
import semmle.python.Files as Files
|
||||
import semmle.python.controlflow.internal.AstNodeImpl
|
||||
|
||||
external string selectedSourceFile();
|
||||
|
||||
private predicate selectedSourceFileAlias = selectedSourceFile/0;
|
||||
|
||||
external int selectedSourceLine();
|
||||
|
||||
private predicate selectedSourceLineAlias = selectedSourceLine/0;
|
||||
|
||||
external int selectedSourceColumn();
|
||||
|
||||
private predicate selectedSourceColumnAlias = selectedSourceColumn/0;
|
||||
|
||||
module ViewCfgQueryInput implements ControlFlow::ViewCfgQueryInputSig<Files::File> {
|
||||
predicate selectedSourceFile = selectedSourceFileAlias/0;
|
||||
|
||||
predicate selectedSourceLine = selectedSourceLineAlias/0;
|
||||
|
||||
predicate selectedSourceColumn = selectedSourceColumnAlias/0;
|
||||
|
||||
predicate cfgScopeSpan(
|
||||
Ast::Callable scope, Files::File file, int startLine, int startColumn, int endLine,
|
||||
int endColumn
|
||||
) {
|
||||
file = scope.getLocation().getFile() and
|
||||
scope.getLocation().hasLocationInfo(_, startLine, startColumn, endLine, endColumn)
|
||||
}
|
||||
}
|
||||
|
||||
import ControlFlow::ViewCfgQuery<Files::File, ViewCfgQueryInput>
|
||||
1772
python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll
Normal file
1772
python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll
Normal file
File diff suppressed because it is too large
Load Diff
926
python/ql/lib/semmle/python/controlflow/internal/Cfg.qll
Normal file
926
python/ql/lib/semmle/python/controlflow/internal/Cfg.qll
Normal file
@@ -0,0 +1,926 @@
|
||||
/**
|
||||
* Provides a Python control flow graph facade backed by the shared
|
||||
* `codeql.controlflow.ControlFlowGraph` library (via `AstNodeImpl.qll`).
|
||||
*
|
||||
* This module re-exposes the same API surface as `semmle/python/Flow.qll`
|
||||
* (the legacy CFG), but is implemented on the new shared CFG. It is
|
||||
* intended as a drop-in replacement for use by the Python dataflow library
|
||||
* and other downstream code.
|
||||
*
|
||||
* Layering follows the Java pattern (`java/ql/lib/semmle/code/java/Expr.qll`
|
||||
* and `SsaImpl.qll`): variable identity and similar AST-level semantics
|
||||
* live on the Python AST classes (`Name.defines(v)`, `Name.uses(v)`, ...);
|
||||
* the CFG layer is purely positional, with `toAst` / `getNode` bridging
|
||||
* back to the AST. The shared SSA library can then be parameterized on
|
||||
* (`BasicBlock`, `int`) directly, with no CFG-level variable predicates.
|
||||
*/
|
||||
overlay[local?]
|
||||
module;
|
||||
|
||||
private import python as Py
|
||||
private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
|
||||
private import codeql.controlflow.SuccessorType
|
||||
|
||||
/**
|
||||
* A control flow node.
|
||||
*
|
||||
* This is the full set of CFG nodes from the shared library — it includes
|
||||
* before-nodes, in-order/post-order nodes, after-value-split nodes, and
|
||||
* entry/exit nodes. This enables full control-flow-level reasoning and
|
||||
* compatibility with the shared control-flow reachability library.
|
||||
*
|
||||
* AST-level semantics (`getNode()`, `isLoad()`, typed wrappers, etc.)
|
||||
* are available only on the `injects` (canonical) node for each AST node.
|
||||
* Non-injects nodes are purely positional CFG nodes with no AST mapping.
|
||||
*/
|
||||
class ControlFlowNode extends CfgImpl::ControlFlowNode {
|
||||
/** Gets the syntactic element corresponding to this flow node, if any. */
|
||||
Py::AstNode getNode() {
|
||||
exists(CfgImpl::Ast::AstNode n | this.injects(n) | result = CfgImpl::astNodeToPyNode(n))
|
||||
}
|
||||
|
||||
Py::Expr asPyExpr() { result = this.getNode() }
|
||||
|
||||
/** Gets a predecessor of this flow node. */
|
||||
ControlFlowNode getAPredecessor() { this = result.getASuccessor() }
|
||||
|
||||
/** Gets a successor of this flow node. */
|
||||
ControlFlowNode getASuccessor() { result = super.getASuccessor() }
|
||||
|
||||
/** Gets a successor for this node if the relevant condition is True. */
|
||||
ControlFlowNode getATrueSuccessor() {
|
||||
result = super.getASuccessor(any(BooleanSuccessor t | t.getValue() = true))
|
||||
}
|
||||
|
||||
/** Gets a successor for this node if the relevant condition is False. */
|
||||
ControlFlowNode getAFalseSuccessor() {
|
||||
result = super.getASuccessor(any(BooleanSuccessor t | t.getValue() = false))
|
||||
}
|
||||
|
||||
/** Gets a successor for this node if an exception is raised. */
|
||||
ControlFlowNode getAnExceptionalSuccessor() { result = super.getAnExceptionSuccessor() }
|
||||
|
||||
/** Gets a successor for this node if no exception is raised. */
|
||||
ControlFlowNode getANormalSuccessor() { result = super.getANormalSuccessor() }
|
||||
|
||||
/** Gets the basic block containing this flow node. */
|
||||
BasicBlock getBasicBlock() { result = super.getBasicBlock() }
|
||||
|
||||
/** Gets the scope containing this flow node. */
|
||||
Py::Scope getScope() { result = super.getEnclosingCallable().asScope() }
|
||||
|
||||
/** Gets the enclosing module. */
|
||||
Py::Module getEnclosingModule() { result = this.getScope().getEnclosingModule() }
|
||||
|
||||
/** Gets the immediate dominator of this flow node. */
|
||||
ControlFlowNode getImmediateDominator() {
|
||||
// Defined positionally via the basic-block dominance tree.
|
||||
exists(BasicBlock bb, int i | bb.getNode(i) = this |
|
||||
// Predecessor within the same basic block.
|
||||
i > 0 and result = bb.getNode(i - 1)
|
||||
or
|
||||
// First node of `bb`: dominator is the last node of the immediate dominator block.
|
||||
i = 0 and result = bb.getImmediateDominator().getLastNode()
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if this strictly dominates `other`. */
|
||||
overlay[caller?]
|
||||
pragma[inline]
|
||||
predicate strictlyDominates(ControlFlowNode other) { super.strictlyDominates(other) }
|
||||
|
||||
/** Holds if this dominates `other` (reflexively). */
|
||||
overlay[caller?]
|
||||
pragma[inline]
|
||||
predicate dominates(ControlFlowNode other) { super.dominates(other) }
|
||||
|
||||
/** Holds if this is the first node in its enclosing scope. */
|
||||
predicate isEntryNode() { this instanceof CfgImpl::ControlFlow::EntryNode }
|
||||
|
||||
/** Holds if this is the first node of a module. */
|
||||
predicate isModuleEntry() {
|
||||
this.isEntryNode() and super.getAstNode().asScope() instanceof Py::Module
|
||||
}
|
||||
|
||||
/** Holds if this node may exit its scope by raising an exception. */
|
||||
predicate isExceptionalExit(Py::Scope s) {
|
||||
this instanceof CfgImpl::ControlFlow::ExceptionalExitNode and
|
||||
super.getEnclosingCallable().asScope() = s
|
||||
}
|
||||
|
||||
/** Holds if this node is a normal (non-exceptional) exit. */
|
||||
predicate isNormalExit() { this instanceof CfgImpl::ControlFlow::NormalExitNode }
|
||||
|
||||
// ===== AST-shape predicates (bridges to the wrapped Python AST) =====
|
||||
/**
|
||||
* Holds if this flow node is a load (including those in augmented
|
||||
* assignments).
|
||||
*
|
||||
* Note: an augmented-assignment target (`x[i]` in `x[i] += 1`) is
|
||||
* both a load and a store — `isLoad` and `isStore` both hold on the
|
||||
* canonical CFG node. This mirrors Java's `VarAccess.isVarRead`,
|
||||
* which holds on the destination of compound and unary assignments
|
||||
* even though the destination is also a write.
|
||||
*/
|
||||
predicate isLoad() { py_expr_contexts(_, 3, this.asPyExpr()) }
|
||||
|
||||
/** Holds if this flow node is a store (including those in augmented assignments). */
|
||||
predicate isStore() { py_expr_contexts(_, 5, this.asPyExpr()) or augstore(_, this) }
|
||||
|
||||
/** Holds if this flow node is a delete. */
|
||||
predicate isDelete() { py_expr_contexts(_, 2, this.asPyExpr()) }
|
||||
|
||||
/** Holds if this flow node is a parameter. */
|
||||
predicate isParameter() { py_expr_contexts(_, 4, this.asPyExpr()) }
|
||||
|
||||
/** Holds if this flow node is a store in an augmented assignment. */
|
||||
predicate isAugStore() { augstore(_, this) }
|
||||
|
||||
/** Holds if this flow node is a load in an augmented assignment. */
|
||||
predicate isAugLoad() { augstore(this, _) }
|
||||
|
||||
/** Holds if this flow node corresponds to a literal. */
|
||||
predicate isLiteral() {
|
||||
this.getNode() instanceof Py::Bytes or
|
||||
this.getNode() instanceof Py::Dict or
|
||||
this.getNode() instanceof Py::DictComp or
|
||||
this.getNode() instanceof Py::Set or
|
||||
this.getNode() instanceof Py::SetComp or
|
||||
this.getNode() instanceof Py::Ellipsis or
|
||||
this.getNode() instanceof Py::GeneratorExp or
|
||||
this.getNode() instanceof Py::Lambda or
|
||||
this.getNode() instanceof Py::ListComp or
|
||||
this.getNode() instanceof Py::List or
|
||||
this.getNode() instanceof Py::Num or
|
||||
this.getNode() instanceof Py::Tuple or
|
||||
this.getNode() instanceof Py::Unicode or
|
||||
this.getNode() instanceof Py::NameConstant
|
||||
}
|
||||
|
||||
/** Holds if this flow node corresponds to an attribute expression. */
|
||||
predicate isAttribute() { this.getNode() instanceof Py::Attribute }
|
||||
|
||||
/** Holds if this flow node corresponds to a subscript expression. */
|
||||
predicate isSubscript() { this.getNode() instanceof Py::Subscript }
|
||||
|
||||
/** Holds if this flow node corresponds to an import member. */
|
||||
predicate isImportMember() { this.getNode() instanceof Py::ImportMember }
|
||||
|
||||
/** Holds if this flow node corresponds to a call. */
|
||||
predicate isCall() { this.getNode() instanceof Py::Call }
|
||||
|
||||
/** Holds if this flow node corresponds to an import. */
|
||||
predicate isImport() { this.getNode() instanceof Py::ImportExpr }
|
||||
|
||||
/** Holds if this flow node corresponds to a conditional expression. */
|
||||
predicate isIfExp() { this.getNode() instanceof Py::IfExp }
|
||||
|
||||
/** Holds if this flow node corresponds to a function definition expression. */
|
||||
predicate isFunction() { this.getNode() instanceof Py::FunctionExpr }
|
||||
|
||||
/** Holds if this flow node corresponds to a class definition expression. */
|
||||
predicate isClass() { this.getNode() instanceof Py::ClassExpr }
|
||||
|
||||
/**
|
||||
* Holds if this flow node is a branch (i.e. has both a true and a
|
||||
* false successor).
|
||||
*/
|
||||
predicate isBranch() { exists(this.getATrueSuccessor()) or exists(this.getAFalseSuccessor()) }
|
||||
|
||||
/**
|
||||
* Gets a CFG child of this node, defined as a CFG node whose AST node
|
||||
* is a child of this CFG node's AST node, restricted to nodes that
|
||||
* dominate this one (so the child has been evaluated by the time we
|
||||
* reach this node).
|
||||
*
|
||||
* Mirrors `Flow.qll`'s `getAChild`. UnaryExprNode is excluded because
|
||||
* its operand is its CFG predecessor (handled separately).
|
||||
*/
|
||||
pragma[nomagic]
|
||||
ControlFlowNode getAChild() {
|
||||
this.getNode().(Py::Expr).getAChildNode() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock()) and
|
||||
not this instanceof UnaryExprNode
|
||||
}
|
||||
|
||||
/** Holds if this flow node strictly reaches `other`. */
|
||||
predicate strictlyReaches(ControlFlowNode other) { this.getASuccessor+() = other }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `load` is the load half of an augmented-assignment target,
|
||||
* and `store` is the corresponding store half.
|
||||
*
|
||||
* In the legacy CFG (`Flow.qll`) the same Python `Name` had two
|
||||
* distinct CFG nodes — a load node (context 3) earlier in the BB, and
|
||||
* a store node (context 5) later. The legacy `augstore` related the
|
||||
* pair via dominance.
|
||||
*
|
||||
* In the new (shared) CFG, the canonical node for an AST expression is
|
||||
* unique, so `load` and `store` collapse onto the same CFG node. The
|
||||
* predicate is therefore reflexive on the augmented-assignment
|
||||
* target's canonical node.
|
||||
*/
|
||||
private predicate augstore(ControlFlowNode load, ControlFlowNode store) {
|
||||
exists(Py::AugAssign aa | aa.getTarget() = load.getNode()) and
|
||||
load = store
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic block — a maximal-length sequence of control flow nodes such
|
||||
* that no node except the first has a predecessor outside the sequence,
|
||||
* and no node except the last has a successor outside the sequence.
|
||||
*/
|
||||
class BasicBlock extends CfgImpl::BasicBlock {
|
||||
/** Gets the `n`th node in this basic block. */
|
||||
ControlFlowNode getNode(int n) { result = super.getNode(n) }
|
||||
|
||||
/** Gets a node in this basic block. */
|
||||
ControlFlowNode getANode() { result = super.getNode(_) }
|
||||
|
||||
/** Gets the first node in this basic block. */
|
||||
ControlFlowNode firstNode() { result = this.getNode(0) }
|
||||
|
||||
/** Gets the last node in this basic block. */
|
||||
ControlFlowNode getLastNode() { result = super.getLastNode() }
|
||||
|
||||
/** Holds if this basic block contains `node`. */
|
||||
predicate contains(ControlFlowNode node) { node = this.getANode() }
|
||||
|
||||
// Inherited from the shared library's `BasicBlock`:
|
||||
// getASuccessor(), getASuccessor(SuccessorType), getAPredecessor(),
|
||||
// strictlyDominates(), dominates(), getImmediateDominator(),
|
||||
// length(), inLoop().
|
||||
// We shadow `getNode(int)` etc. to return `ControlFlowNode` (this
|
||||
// facade's type) and add Python-style helpers below.
|
||||
/** Gets a true successor to this basic block. */
|
||||
BasicBlock getATrueSuccessor() {
|
||||
result = super.getASuccessor(any(BooleanSuccessor t | t.getValue() = true))
|
||||
}
|
||||
|
||||
/** Gets a false successor to this basic block. */
|
||||
BasicBlock getAFalseSuccessor() {
|
||||
result = super.getASuccessor(any(BooleanSuccessor t | t.getValue() = false))
|
||||
}
|
||||
|
||||
/** Gets an unconditional successor to this basic block. */
|
||||
BasicBlock getAnUnconditionalSuccessor() {
|
||||
result = super.getASuccessor() and
|
||||
not result = this.getATrueSuccessor() and
|
||||
not result = this.getAFalseSuccessor()
|
||||
}
|
||||
|
||||
/** Gets an exceptional successor to this basic block. */
|
||||
BasicBlock getAnExceptionalSuccessor() { result = super.getASuccessor(any(ExceptionSuccessor t)) }
|
||||
|
||||
/**
|
||||
* Holds if this basic block is in the dominance frontier of `df`.
|
||||
*
|
||||
* Note: implemented locally rather than via the shared lib, which
|
||||
* doesn't currently expose a `dominanceFrontier` predicate at this
|
||||
* level.
|
||||
*/
|
||||
predicate inDominanceFrontier(BasicBlock df) { super.inDominanceFrontier(df) }
|
||||
|
||||
/** Holds if this basic block strictly reaches `other`. */
|
||||
predicate strictlyReaches(BasicBlock other) { super.getASuccessor+() = other }
|
||||
|
||||
/** Holds if this basic block reaches `other` (reflexively). */
|
||||
predicate reaches(BasicBlock other) { this = other or this.strictlyReaches(other) }
|
||||
|
||||
/** Holds if flow from this basic block reaches a normal exit from its scope. */
|
||||
predicate reachesExit() {
|
||||
this.getANode() instanceof CfgImpl::ControlFlow::NormalExitNode
|
||||
or
|
||||
exists(BasicBlock succ | succ = super.getASuccessor() and succ.reachesExit())
|
||||
}
|
||||
|
||||
/** Gets the scope of this basic block. */
|
||||
Py::Scope getScope() { exists(ControlFlowNode n | n = this.getANode() | result = n.getScope()) }
|
||||
|
||||
/** Holds if flow from this BasicBlock always reaches `succ`. */
|
||||
predicate alwaysReaches(BasicBlock succ) {
|
||||
succ = this
|
||||
or
|
||||
strictcount(BasicBlock s | s = super.getASuccessor()) = 1 and
|
||||
succ = super.getASuccessor()
|
||||
or
|
||||
forex(BasicBlock immsucc | immsucc = super.getASuccessor() | immsucc.alwaysReaches(succ))
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this basic block ends in a node that branches on a boolean
|
||||
* outcome, and `other` is dominated by the corresponding successor
|
||||
* for `branch` while not being reachable from the other branch
|
||||
* without going through this BB.
|
||||
*
|
||||
* In other words: any execution that reaches `other` must have just
|
||||
* evaluated the last node of this BB and taken the `branch` outcome.
|
||||
* This mirrors the legacy `ConditionBlock.controls(BB, branch)`.
|
||||
*/
|
||||
predicate controls(BasicBlock other, boolean branch) {
|
||||
super.edgeDominates(other, any(BooleanSuccessor t | t.getValue() = branch))
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Re-exports for SSA / dominance consumers
|
||||
//
|
||||
// The shared `BB::CfgSig` requires `EntryBasicBlock` and `dominatingEdge` in
|
||||
// addition to the BasicBlock class we already expose. They are provided by
|
||||
// the shared CFG library on the `BB::Make` instantiation produced by
|
||||
// `AstNodeImpl.qll`.
|
||||
// ===========================================================================
|
||||
/** An entry basic block, that is, a basic block whose first node is an entry node. */
|
||||
class EntryBasicBlock = CfgImpl::Cfg::EntryBasicBlock;
|
||||
|
||||
/**
|
||||
* Holds if `bb1` has `bb2` as a direct successor and the edge between `bb1`
|
||||
* and `bb2` is a dominating edge.
|
||||
*/
|
||||
predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2;
|
||||
|
||||
// ===========================================================================
|
||||
// AST-shape subclasses of ControlFlowNode
|
||||
//
|
||||
// Each class is a thin wrapper around the canonical CFG node for a given
|
||||
// kind of Python AST node. Methods that take/return CFG nodes look up
|
||||
// related CFG nodes by AST identity (via `getNode()`), and the dominance
|
||||
// constraint from the old CFG (`result.getBasicBlock().dominates(this.getBasicBlock())`)
|
||||
// is preserved.
|
||||
// ===========================================================================
|
||||
/** Gets the canonical `ControlFlowNode` for AST expression `e`. */
|
||||
ControlFlowNode astExprToCfg(Py::Expr e) { result.getNode() = e }
|
||||
|
||||
/** A control flow node corresponding to a `Name` or `PlaceHolder` expression. */
|
||||
class NameNode extends ControlFlowNode {
|
||||
NameNode() {
|
||||
this.getNode() instanceof Py::Name
|
||||
or
|
||||
this.getNode() instanceof Py::PlaceHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this flow node defines the variable `v`.
|
||||
*
|
||||
* This includes augmented-assignment targets — `n += 1` is both a
|
||||
* read and a write of `n`, so `defines(n)` and `uses(n)` both hold
|
||||
* on the same canonical CFG node. Mirrors Java's `VariableUpdate`
|
||||
* semantics where compound assignments register both a write
|
||||
* (`VarWrite`) and a read (`VarRead`) on the destination.
|
||||
*/
|
||||
predicate defines(Py::Variable v) { exists(Py::Name n | n = this.getNode() and n.defines(v)) }
|
||||
|
||||
/** Holds if this flow node deletes the variable `v`. */
|
||||
predicate deletes(Py::Variable v) { exists(Py::Name n | n = this.getNode() and n.deletes(v)) }
|
||||
|
||||
/** Holds if this flow node uses the variable `v`. */
|
||||
predicate uses(Py::Variable v) {
|
||||
this.isLoad() and
|
||||
exists(Py::Name u | u = this.getNode() and u.uses(v))
|
||||
or
|
||||
exists(Py::PlaceHolder u |
|
||||
u = this.getNode() and u.getVariable() = v and u.getCtx() instanceof Py::Load
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the identifier of this name node. */
|
||||
string getId() {
|
||||
result = this.getNode().(Py::Name).getId()
|
||||
or
|
||||
result = this.getNode().(Py::PlaceHolder).getId()
|
||||
}
|
||||
|
||||
/** Holds if this is a use of a local variable. */
|
||||
predicate isLocal() { exists(Py::Variable v | this.uses(v) and v instanceof Py::LocalVariable) }
|
||||
|
||||
/** Holds if this is a use of a non-local variable. */
|
||||
predicate isNonLocal() {
|
||||
exists(Py::Variable v | this.uses(v) and v.getScope() != this.getScope())
|
||||
}
|
||||
|
||||
/** Holds if this is a use of a global (including builtin) variable. */
|
||||
predicate isGlobal() { exists(Py::Variable v | this.uses(v) and v instanceof Py::GlobalVariable) }
|
||||
|
||||
/**
|
||||
* Holds if this is a use of `self` — the first parameter of an
|
||||
* enclosing method.
|
||||
*
|
||||
* AST-level approximation: matches when the Name uses a `Variable`
|
||||
* that is the first parameter of an enclosing `Function` defined
|
||||
* inside a `Class`.
|
||||
*/
|
||||
predicate isSelf() {
|
||||
exists(Py::Variable v, Py::Function f, Py::Class c |
|
||||
this.uses(v) and
|
||||
f = c.getAMethod() and
|
||||
v.getScope() = f and
|
||||
v = f.getArg(0).(Py::Name).getVariable()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a named constant (`None`, `True`, `False`). */
|
||||
class NameConstantNode extends NameNode {
|
||||
NameConstantNode() { this.getNode() instanceof Py::NameConstant }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a call. */
|
||||
class CallNode extends ControlFlowNode {
|
||||
CallNode() { super.getNode() instanceof Py::Call }
|
||||
|
||||
override Py::Call getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the underlying Python `Call`. */
|
||||
Py::Call getCall() { result = this.getNode() }
|
||||
|
||||
/** Gets the flow node for the function component of this call. */
|
||||
ControlFlowNode getFunction() {
|
||||
this.getCall().getFunc() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Gets the flow node for the `n`th positional argument. */
|
||||
ControlFlowNode getArg(int n) {
|
||||
this.getCall().getArg(n) = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Gets the flow node for the named argument with name `name`. */
|
||||
ControlFlowNode getArgByName(string name) {
|
||||
exists(Py::Keyword k |
|
||||
k = this.getCall().getANamedArg() and
|
||||
k.getValue() = result.getNode() and
|
||||
k.getArg() = name and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets a flow node corresponding to any argument. */
|
||||
ControlFlowNode getAnArg() { result = this.getArg(_) or result = this.getArgByName(_) }
|
||||
|
||||
/** Gets the first tuple (`*args`) argument, if any. */
|
||||
ControlFlowNode getStarArg() {
|
||||
this.getCall().getStarArg() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Gets a dictionary (`**kwargs`) argument, if any. */
|
||||
ControlFlowNode getKwargs() {
|
||||
this.getCall().getKwargs() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Holds if this call is a decorator call applied to a class or a function. */
|
||||
predicate isDecoratorCall() { this.isClassDecoratorCall() or this.isFunctionDecoratorCall() }
|
||||
|
||||
/** Holds if this call is a decorator call applied to a class. */
|
||||
predicate isClassDecoratorCall() {
|
||||
exists(Py::ClassExpr cls | this.getNode() = cls.getADecoratorCall())
|
||||
}
|
||||
|
||||
/** Holds if this call is a decorator call applied to a function. */
|
||||
predicate isFunctionDecoratorCall() {
|
||||
exists(Py::FunctionExpr func | this.getNode() = func.getADecoratorCall())
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to an attribute expression. */
|
||||
class AttrNode extends ControlFlowNode {
|
||||
AttrNode() { super.getNode() instanceof Py::Attribute }
|
||||
|
||||
override Py::Attribute getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the flow node for the object of the attribute expression. */
|
||||
ControlFlowNode getObject() {
|
||||
this.getNode().getObject() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Gets the flow node for the object of this attribute expression, with the matching name. */
|
||||
ControlFlowNode getObject(string name) {
|
||||
this.getName() = name and
|
||||
result = this.getObject()
|
||||
}
|
||||
|
||||
/** Gets the attribute name. */
|
||||
string getName() { result = this.getNode().getName() }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to an import statement (`import x`). */
|
||||
class ImportExprNode extends ControlFlowNode {
|
||||
ImportExprNode() { super.getNode() instanceof Py::ImportExpr }
|
||||
|
||||
override Py::ImportExpr getNode() { result = super.getNode() }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a `from ... import name` expression. */
|
||||
class ImportMemberNode extends ControlFlowNode {
|
||||
ImportMemberNode() { super.getNode() instanceof Py::ImportMember }
|
||||
|
||||
override Py::ImportMember getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the flow node for the module being imported from, with the matching name. */
|
||||
ControlFlowNode getModule(string name) {
|
||||
exists(Py::ImportMember i |
|
||||
i = this.getNode() and
|
||||
i.getModule() = result.getNode() and
|
||||
i.getName() = name and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a `from ... import *` statement. */
|
||||
class ImportStarNode extends ControlFlowNode {
|
||||
ImportStarNode() { super.getNode() instanceof Py::ImportStar }
|
||||
|
||||
override Py::ImportStar getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the flow node for the module being imported from. */
|
||||
ControlFlowNode getModule() {
|
||||
this.getNode().getModuleExpr() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a subscript expression. */
|
||||
class SubscriptNode extends ControlFlowNode {
|
||||
SubscriptNode() { super.getNode() instanceof Py::Subscript }
|
||||
|
||||
override Py::Subscript getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the flow node for the value being subscripted. */
|
||||
ControlFlowNode getObject() {
|
||||
this.getNode().getObject() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Gets the flow node for the index expression. */
|
||||
ControlFlowNode getIndex() {
|
||||
this.getNode().getIndex() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a comparison operation. */
|
||||
class CompareNode extends ControlFlowNode {
|
||||
CompareNode() { super.getNode() instanceof Py::Compare }
|
||||
|
||||
override Py::Compare getNode() { result = super.getNode() }
|
||||
|
||||
/** Holds if `left` and `right` are a pair of operands for this comparison. */
|
||||
predicate operands(ControlFlowNode left, Py::Cmpop op, ControlFlowNode right) {
|
||||
exists(Py::Compare c, Py::Expr eleft, Py::Expr eright |
|
||||
c = this.getNode() and eleft = left.getNode() and eright = right.getNode()
|
||||
|
|
||||
eleft = c.getLeft() and eright = c.getComparator(0) and op = c.getOp(0)
|
||||
or
|
||||
exists(int i |
|
||||
eleft = c.getComparator(i - 1) and eright = c.getComparator(i) and op = c.getOp(i)
|
||||
)
|
||||
) and
|
||||
left.getBasicBlock().dominates(this.getBasicBlock()) and
|
||||
right.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a conditional expression (`x if c else y`). */
|
||||
class IfExprNode extends ControlFlowNode {
|
||||
IfExprNode() { super.getNode() instanceof Py::IfExp }
|
||||
|
||||
override Py::IfExp getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the flow node for one of the value operands (true-branch or false-branch). */
|
||||
ControlFlowNode getAnOperand() {
|
||||
exists(Py::IfExp ie |
|
||||
ie = this.getNode() and
|
||||
(result.getNode() = ie.getBody() or result.getNode() = ie.getOrelse())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to an assignment expression (walrus `:=`). */
|
||||
class AssignmentExprNode extends ControlFlowNode {
|
||||
AssignmentExprNode() { super.getNode() instanceof Py::AssignExpr }
|
||||
|
||||
override Py::AssignExpr getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the flow node for the left-hand side. */
|
||||
ControlFlowNode getTarget() {
|
||||
this.getNode().getTarget() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
/** Gets the flow node for the right-hand side. */
|
||||
ControlFlowNode getValue() {
|
||||
this.getNode().getValue() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a binary expression (`a + b` etc.). */
|
||||
class BinaryExprNode extends ControlFlowNode {
|
||||
BinaryExprNode() { super.getNode() instanceof Py::BinaryExpr }
|
||||
|
||||
override Py::BinaryExpr getNode() { result = super.getNode() }
|
||||
|
||||
ControlFlowNode getLeft() {
|
||||
this.getNode().getLeft() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
ControlFlowNode getRight() {
|
||||
this.getNode().getRight() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
Py::Operator getOp() { result = this.getNode().(Py::BinaryExpr).getOp() }
|
||||
|
||||
/** Holds if `left` and `right` are the operands and `op` is the operator. */
|
||||
predicate operands(ControlFlowNode left, Py::Operator op, ControlFlowNode right) {
|
||||
left = this.getLeft() and right = this.getRight() and op = this.getOp()
|
||||
}
|
||||
|
||||
/** Gets either operand. */
|
||||
ControlFlowNode getAnOperand() { result = this.getLeft() or result = this.getRight() }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a boolean expression (`a and b`, `a or b`). */
|
||||
class BoolExprNode extends ControlFlowNode {
|
||||
BoolExprNode() { super.getNode() instanceof Py::BoolExpr }
|
||||
|
||||
override Py::BoolExpr getNode() { result = super.getNode() }
|
||||
|
||||
Py::Boolop getOp() { result = this.getNode().(Py::BoolExpr).getOp() }
|
||||
|
||||
/** Gets any operand of this boolean expression. */
|
||||
ControlFlowNode getAnOperand() { this.getNode().getAValue() = result.getNode() }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a unary expression (`-x`, `not x`, etc.). */
|
||||
class UnaryExprNode extends ControlFlowNode {
|
||||
UnaryExprNode() { super.getNode() instanceof Py::UnaryExpr }
|
||||
|
||||
override Py::UnaryExpr getNode() { result = super.getNode() }
|
||||
|
||||
ControlFlowNode getOperand() {
|
||||
this.getNode().getOperand() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
|
||||
Py::Unaryop getOp() { result = this.getNode().(Py::UnaryExpr).getOp() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A control flow node that is a definition: it appears in a context that
|
||||
* binds a variable (assignment target, parameter, etc.).
|
||||
*/
|
||||
class DefinitionNode extends ControlFlowNode {
|
||||
DefinitionNode() { this.isStore() or this.isParameter() }
|
||||
|
||||
/** Gets the value assigned, if any. */
|
||||
ControlFlowNode getValue() {
|
||||
// For-target: the value is the for-loop's iter expression (which
|
||||
// is also where `Cfg::ForNode` lives — its `getNode()` returns the
|
||||
// enclosing `Py::For` statement). Treated specially because there
|
||||
// is no AST node holding the result of `iter(next(seq))`; we use
|
||||
// the iter expression's CFG node as the stand-in.
|
||||
exists(Py::For f |
|
||||
f.getTarget() = this.getNode() and
|
||||
result.getNode() = f.getIter()
|
||||
)
|
||||
or
|
||||
exists(Py::AstNode value | value = assignedValue(this.getNode()) |
|
||||
result.getNode() = value and
|
||||
(
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
or
|
||||
result.isImport()
|
||||
or
|
||||
// The default value for a parameter is evaluated in the same basic block as
|
||||
// the function definition, but the parameter belongs to the basic block of the
|
||||
// function, so there is no dominance relationship between the two.
|
||||
exists(Py::Parameter param | this.getNode() = param.asName())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AST node that holds the value assigned to `lhs` in a binding
|
||||
* context. Mirrors `Flow.qll::assigned_value`.
|
||||
*/
|
||||
private Py::AstNode assignedValue(Py::Expr lhs) {
|
||||
// lhs = result
|
||||
exists(Py::Assign a | a.getATarget() = lhs and result = a.getValue())
|
||||
or
|
||||
// lhs := result
|
||||
exists(Py::AssignExpr a | a.getTarget() = lhs and result = a.getValue())
|
||||
or
|
||||
// lhs: annotation = result
|
||||
exists(Py::AnnAssign a | a.getTarget() = lhs and result = a.getValue())
|
||||
or
|
||||
// import result as lhs (also covers plain `import lhs`, where alias.getAsname() = lhs)
|
||||
exists(Py::Alias a | a.getAsname() = lhs and result = a.getValue())
|
||||
or
|
||||
// lhs += x -> result is the (lhs + x) binary expression
|
||||
exists(Py::AugAssign a, Py::BinaryExpr b |
|
||||
b = a.getOperation() and result = b and lhs = b.getLeft()
|
||||
)
|
||||
or
|
||||
// Nested sequence assign: ..., lhs, ... = ..., result, ...
|
||||
exists(Py::Assign a | nestedSequenceAssign(a.getATarget(), a.getValue(), lhs, result))
|
||||
or
|
||||
// Parameter default
|
||||
exists(Py::Parameter param | lhs = param.asName() and result = param.getDefault())
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for nested sequence assignments such as `(a, b), c = (1, 2), 3`.
|
||||
*/
|
||||
private predicate nestedSequenceAssign(
|
||||
Py::Expr leftParent, Py::Expr rightParent, Py::Expr left, Py::Expr right
|
||||
) {
|
||||
exists(int i |
|
||||
leftParent.(Py::Tuple).getElt(i) = left and rightParent.(Py::Tuple).getElt(i) = right
|
||||
or
|
||||
leftParent.(Py::List).getElt(i) = left and rightParent.(Py::List).getElt(i) = right
|
||||
)
|
||||
or
|
||||
exists(Py::Expr leftMid, Py::Expr rightMid |
|
||||
nestedSequenceAssign(leftParent, rightParent, leftMid, rightMid) and
|
||||
nestedSequenceAssign(leftMid, rightMid, left, right)
|
||||
)
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a deletion (`del x`). */
|
||||
class DeletionNode extends ControlFlowNode {
|
||||
DeletionNode() { this.isDelete() }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a `for` loop target. */
|
||||
class ForNode extends ControlFlowNode {
|
||||
ForNode() { exists(Py::For f | this.getNode() = f.getIter()) }
|
||||
|
||||
/** Gets the iterable expression. */
|
||||
ControlFlowNode getIter() {
|
||||
result = this and result = result // canonical "after" of the iterable
|
||||
}
|
||||
|
||||
/** Gets the sequence expression (alias for `getIter()`, matches legacy Flow naming). */
|
||||
ControlFlowNode getSequence() { result = this.getIter() }
|
||||
|
||||
/** Gets the target (loop variable) of the `for` loop. */
|
||||
ControlFlowNode getTarget() {
|
||||
exists(Py::For f |
|
||||
f.getIter() = this.getNode() and
|
||||
f.getTarget() = result.getNode()
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if `target` is the loop variable and `sequence` is the iterable. */
|
||||
predicate iterates(ControlFlowNode target, ControlFlowNode sequence) {
|
||||
target = this.getTarget() and sequence = this.getSequence()
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a `raise` statement. */
|
||||
class RaiseStmtNode extends ControlFlowNode {
|
||||
RaiseStmtNode() { super.getNode() instanceof Py::Raise }
|
||||
|
||||
override Py::Raise getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the exception expression, if any. */
|
||||
ControlFlowNode getException() {
|
||||
this.getNode().getException() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a starred expression (`*x`). */
|
||||
class StarredNode extends ControlFlowNode {
|
||||
StarredNode() { this.getNode() instanceof Py::Starred }
|
||||
|
||||
/** Gets the value being starred. */
|
||||
ControlFlowNode getValue() {
|
||||
exists(Py::Starred s |
|
||||
s = this.getNode() and
|
||||
s.getValue() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to an `except` clause's name binding. */
|
||||
class ExceptFlowNode extends ControlFlowNode {
|
||||
ExceptFlowNode() { exists(Py::ExceptStmt e | this.getNode() = e.getName()) }
|
||||
|
||||
/** Gets the CFG node for the bound `as`-name itself. */
|
||||
ControlFlowNode getName() { result = this }
|
||||
|
||||
/** Gets the type expression of this exception handler. */
|
||||
ControlFlowNode getType() {
|
||||
exists(Py::ExceptStmt e |
|
||||
e.getName() = this.getNode() and
|
||||
e.getType() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to an `except*` clause's name binding. */
|
||||
class ExceptGroupFlowNode extends ControlFlowNode {
|
||||
ExceptGroupFlowNode() { exists(Py::ExceptGroupStmt e | this.getNode() = e.getName()) }
|
||||
|
||||
/** Gets the CFG node for the bound `as`-name itself. */
|
||||
ControlFlowNode getName() { result = this }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a sequence (tuple, list). */
|
||||
abstract class SequenceNode extends ControlFlowNode {
|
||||
/** Gets the `n`th element of this sequence. */
|
||||
abstract ControlFlowNode getElement(int n);
|
||||
|
||||
/** Gets any element of this sequence. */
|
||||
ControlFlowNode getAnElement() { result = this.getElement(_) }
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a tuple literal. */
|
||||
class TupleNode extends SequenceNode {
|
||||
TupleNode() { this.getNode() instanceof Py::Tuple }
|
||||
|
||||
override ControlFlowNode getElement(int n) {
|
||||
exists(Py::Tuple t |
|
||||
t = this.getNode() and
|
||||
t.getElt(n) = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a list literal. */
|
||||
class ListNode extends SequenceNode {
|
||||
ListNode() { this.getNode() instanceof Py::List }
|
||||
|
||||
override ControlFlowNode getElement(int n) {
|
||||
exists(Py::List l |
|
||||
l = this.getNode() and
|
||||
l.getElt(n) = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a set literal. */
|
||||
class SetNode extends ControlFlowNode {
|
||||
SetNode() { this.getNode() instanceof Py::Set }
|
||||
|
||||
/** Gets the flow node for an element of the set. */
|
||||
ControlFlowNode getAnElement() {
|
||||
exists(Py::Set s |
|
||||
s = this.getNode() and
|
||||
s.getAnElt() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to a dict literal. */
|
||||
class DictNode extends ControlFlowNode {
|
||||
DictNode() { this.getNode() instanceof Py::Dict }
|
||||
|
||||
/** Gets the flow node for a key of the dict. */
|
||||
ControlFlowNode getAKey() {
|
||||
exists(Py::Dict d |
|
||||
d = this.getNode() and
|
||||
d.getAKey() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the flow node for a value of the dict. */
|
||||
ControlFlowNode getAValue() {
|
||||
exists(Py::Dict d |
|
||||
d = this.getNode() and
|
||||
d.getAValue() = result.getNode() and
|
||||
result.getBasicBlock().dominates(this.getBasicBlock())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A control flow node corresponding to an iterable in a `for` loop. */
|
||||
class IterableNode extends ControlFlowNode {
|
||||
IterableNode() {
|
||||
this instanceof SequenceNode
|
||||
or
|
||||
this instanceof SetNode
|
||||
}
|
||||
|
||||
/** Gets the control flow node for an element of this iterable. */
|
||||
ControlFlowNode getAnElement() {
|
||||
result = this.(SequenceNode).getAnElement()
|
||||
or
|
||||
result = this.(SetNode).getAnElement()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
consistencyOverview
|
||||
| deadEnd | 1 |
|
||||
deadEnd
|
||||
| without_loop.py:7:5:7:9 | Break |
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Phase -1 of the dataflow CFG migration: verifies that every variable
|
||||
* binding visible to the AST (`Name.defines(v)`) corresponds to a CFG node
|
||||
* in the new CFG (`semmle.python.controlflow.internal.AstNodeImpl`).
|
||||
*
|
||||
* The expected tag is `cfgdefines=<name>`. Each binding annotation in the
|
||||
* test sources looks like `# $ cfgdefines=x` for a binding currently
|
||||
* covered by the new CFG, or `# $ MISSING: cfgdefines=x` for a binding
|
||||
* that is known to be uncovered (a "red" test case that should be
|
||||
* green-flipped once the corresponding `cfg-ext-*` extension lands).
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
|
||||
import utils.test.InlineExpectationsTest
|
||||
|
||||
module CfgBindingsTest implements TestSig {
|
||||
string getARelevantTag() { result = "cfgdefines" }
|
||||
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg |
|
||||
n.defines(v) and
|
||||
cfg.getAstNode().asExpr() = n and
|
||||
location = n.getLocation() and
|
||||
element = n.toString() and
|
||||
tag = "cfgdefines" and
|
||||
value = v.getId()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import MakeTest<CfgBindingsTest>
|
||||
@@ -0,0 +1,13 @@
|
||||
# Annotated assignment (PEP 526). Both with and without an initializer.
|
||||
|
||||
a: int = 1 # $ cfgdefines=a
|
||||
b: str = "hi" # $ cfgdefines=b
|
||||
|
||||
# Annotation without value: the AST records `c` as defined,
|
||||
# and the new CFG now visits it via the AnnAssignStmt wrapper.
|
||||
c: int # $ cfgdefines=c
|
||||
|
||||
class K: # $ cfgdefines=K
|
||||
field: int = 0 # $ cfgdefines=field
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Compound (tuple/list) assignment targets — actually wired in the new CFG.
|
||||
|
||||
a, b = (1, 2) # $ cfgdefines=a cfgdefines=b
|
||||
[c, d] = [3, 4] # $ cfgdefines=c cfgdefines=d
|
||||
|
||||
# Nested unpacking.
|
||||
(e, (f, g)) = (1, (2, 3)) # $ cfgdefines=e cfgdefines=f cfgdefines=g
|
||||
|
||||
# Star unpacking.
|
||||
h, *i = [1, 2, 3] # $ cfgdefines=h cfgdefines=i
|
||||
|
||||
# Chained assignment with compound target.
|
||||
j = k, l = (5, 6) # $ cfgdefines=j cfgdefines=k cfgdefines=l
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Comprehension and `for` loop targets — wired in the new CFG.
|
||||
# Comprehensions are nested function scopes with a synthetic `.0` parameter
|
||||
# bound to the iterable.
|
||||
|
||||
# Bare-name `for` target.
|
||||
for i in range(3): # $ cfgdefines=i
|
||||
pass
|
||||
|
||||
# Compound `for` target.
|
||||
for k, v in [(1, 2)]: # $ cfgdefines=k cfgdefines=v
|
||||
pass
|
||||
|
||||
# Comprehension targets.
|
||||
_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x cfgdefines=.0
|
||||
_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z cfgdefines=.0
|
||||
_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a cfgdefines=.0
|
||||
|
||||
# Nested comprehensions.
|
||||
_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b cfgdefines=.0
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Reachability of code following a try whose body always returns.
|
||||
#
|
||||
# The new CFG models exception edges for raise-prone expressions when
|
||||
# they appear inside a `try` (or `with`) statement, mirroring Java's
|
||||
# `mayThrow`. This means the body of a `try` has both a normal
|
||||
# completion edge and an exception edge to its handlers, so code
|
||||
# following the try-statement is reachable via the except-handler path
|
||||
# even when the try-body would otherwise always return.
|
||||
#
|
||||
# Code that is not reachable under either normal or exception flow
|
||||
# (for example, the `else` clause of a try whose body unconditionally
|
||||
# raises) remains correctly classified as dead.
|
||||
|
||||
|
||||
def f(obj): # $ cfgdefines=f cfgdefines=obj
|
||||
try:
|
||||
return len(obj)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# The try-body always returns, but `len(obj)` can raise (it is
|
||||
# inside the try, so we model its exception edge). The
|
||||
# `except TypeError: pass` handler falls through to here, making
|
||||
# the code below reachable.
|
||||
try:
|
||||
hint = type(obj).__length_hint__ # $ cfgdefines=hint
|
||||
except AttributeError:
|
||||
return None
|
||||
return hint
|
||||
|
||||
|
||||
def g(): # $ cfgdefines=g
|
||||
try:
|
||||
raise Exception("inner")
|
||||
except:
|
||||
raise Exception("outer")
|
||||
else:
|
||||
# Unreachable: the inner try body always raises (via an explicit
|
||||
# `raise`, which is modelled unconditionally), so the `else:`
|
||||
# clause never runs.
|
||||
hit_inner_else = True
|
||||
|
||||
|
||||
def h(cache, key): # $ cfgdefines=h cfgdefines=cache cfgdefines=key
|
||||
try:
|
||||
return cache[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Same pattern as `f`: reachable via the except-handler fall-through.
|
||||
value = compute(key) # $ cfgdefines=value
|
||||
cache[key] = value
|
||||
return value
|
||||
@@ -0,0 +1,30 @@
|
||||
# Decorated `def`/`class` — wired in the new CFG.
|
||||
|
||||
|
||||
def deco(f): # $ cfgdefines=deco cfgdefines=f
|
||||
return f
|
||||
|
||||
|
||||
@deco
|
||||
def decorated_func(): # $ cfgdefines=decorated_func
|
||||
pass
|
||||
|
||||
|
||||
@deco
|
||||
class DecoratedClass: # $ cfgdefines=DecoratedClass
|
||||
pass
|
||||
|
||||
|
||||
# Stacked decorators.
|
||||
@deco
|
||||
@deco
|
||||
def doubly(): # $ cfgdefines=doubly
|
||||
pass
|
||||
|
||||
|
||||
# Inside a class body.
|
||||
class Outer: # $ cfgdefines=Outer
|
||||
@staticmethod
|
||||
def inner(): # $ cfgdefines=inner
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Exception-handler name bindings. These are already wired in the new
|
||||
# CFG provided the try body can raise; `raise` statements are reliably
|
||||
# treated as exception sources.
|
||||
|
||||
try:
|
||||
raise ValueError("oops")
|
||||
except ValueError as e: # $ cfgdefines=e
|
||||
pass
|
||||
|
||||
try:
|
||||
raise TypeError("oops")
|
||||
except (TypeError, KeyError) as err: # $ cfgdefines=err
|
||||
pass
|
||||
|
||||
# Exception groups (Python 3.11+).
|
||||
try:
|
||||
raise ValueError("oops")
|
||||
except* ValueError as eg: # $ cfgdefines=eg
|
||||
pass
|
||||
14
python/ql/test/library-tests/ControlFlow/bindings/imports.py
Normal file
14
python/ql/test/library-tests/ControlFlow/bindings/imports.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Import aliases — all bound names below are now reachable via the new
|
||||
# CFG's `ImportStmt` wrapper.
|
||||
|
||||
import os # $ cfgdefines=os
|
||||
import os.path # $ cfgdefines=os
|
||||
import os as o # $ cfgdefines=o
|
||||
from os import path # $ cfgdefines=path
|
||||
from os import path as p # $ cfgdefines=p
|
||||
from os import sep, linesep # $ cfgdefines=sep cfgdefines=linesep
|
||||
from os import (
|
||||
getcwd, # $ cfgdefines=getcwd
|
||||
getcwdb, # $ cfgdefines=getcwdb
|
||||
)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Match-statement pattern bindings — wired in the new CFG.
|
||||
|
||||
def f(subject): # $ cfgdefines=f cfgdefines=subject
|
||||
match subject:
|
||||
case x: # $ cfgdefines=x
|
||||
pass
|
||||
case [a, b]: # $ cfgdefines=a cfgdefines=b
|
||||
pass
|
||||
case {"k": v}: # $ cfgdefines=v
|
||||
pass
|
||||
case Point(p, q): # $ cfgdefines=p cfgdefines=q
|
||||
pass
|
||||
case [_, *rest]: # $ cfgdefines=rest
|
||||
pass
|
||||
case (1 | 2) as n: # $ cfgdefines=n
|
||||
pass
|
||||
|
||||
|
||||
class Point: # $ cfgdefines=Point
|
||||
__match_args__ = ("x", "y") # $ cfgdefines=__match_args__
|
||||
x: int # $ cfgdefines=x
|
||||
y: int # $ cfgdefines=y
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Function parameters.
|
||||
|
||||
def positional(a, b): # $ cfgdefines=positional cfgdefines=a cfgdefines=b
|
||||
pass
|
||||
|
||||
|
||||
def with_default(x=1, y=2): # $ cfgdefines=with_default cfgdefines=x cfgdefines=y
|
||||
pass
|
||||
|
||||
|
||||
def with_vararg(*args): # $ cfgdefines=with_vararg cfgdefines=args
|
||||
pass
|
||||
|
||||
|
||||
def with_kwarg(**kwargs): # $ cfgdefines=with_kwarg cfgdefines=kwargs
|
||||
pass
|
||||
|
||||
|
||||
def with_kwonly(*, k1, k2=5): # $ cfgdefines=with_kwonly cfgdefines=k1 cfgdefines=k2
|
||||
pass
|
||||
|
||||
|
||||
def kitchen_sink(a, b=2, *args, k1, k2=5, **kw): # $ cfgdefines=kitchen_sink cfgdefines=a cfgdefines=b cfgdefines=args cfgdefines=k1 cfgdefines=k2 cfgdefines=kw
|
||||
pass
|
||||
|
||||
|
||||
# Methods get `self` / `cls`.
|
||||
class C: # $ cfgdefines=C
|
||||
def method(self, x): # $ cfgdefines=method cfgdefines=self cfgdefines=x
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def cmethod(cls, x): # $ cfgdefines=cmethod cfgdefines=cls cfgdefines=x
|
||||
pass
|
||||
|
||||
|
||||
# Lambda parameter.
|
||||
_ = lambda p: p + 1 # $ cfgdefines=_ cfgdefines=p
|
||||
|
||||
# PEP 570 positional-only.
|
||||
def pos_only(a, b, /, c): # $ cfgdefines=pos_only cfgdefines=a cfgdefines=b cfgdefines=c
|
||||
pass
|
||||
14
python/ql/test/library-tests/ControlFlow/bindings/simple.py
Normal file
14
python/ql/test/library-tests/ControlFlow/bindings/simple.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Simple bindings that should already work in the new CFG.
|
||||
# No MISSING annotations expected.
|
||||
|
||||
x = 1 # $ cfgdefines=x
|
||||
y = x + 1 # $ cfgdefines=y
|
||||
|
||||
def f(): # $ cfgdefines=f
|
||||
pass
|
||||
|
||||
class C: # $ cfgdefines=C
|
||||
pass
|
||||
|
||||
# Re-assignment.
|
||||
x = 2 # $ cfgdefines=x
|
||||
@@ -0,0 +1,4 @@
|
||||
# Starred-target edge cases — wired in the new CFG.
|
||||
|
||||
# Starred target in a Tuple LHS.
|
||||
*head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail
|
||||
@@ -0,0 +1,21 @@
|
||||
# PEP 695 type parameters (Python 3.12+).
|
||||
|
||||
# PEP 695 type-param names on `def`/`class` bind in an annotation scope
|
||||
# that nests the function/class body — they have no CFG node in the
|
||||
# enclosing scope (matching the legacy CFG).
|
||||
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x
|
||||
return x
|
||||
|
||||
|
||||
class Box[T]: # $ cfgdefines=Box
|
||||
item: T # $ SPURIOUS: cfgdefines=item
|
||||
|
||||
|
||||
# Multi-parameter, with bound and variadics.
|
||||
def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs
|
||||
return x
|
||||
|
||||
|
||||
# `type` statement (PEP 695).
|
||||
type Alias[T] = list[T] # $ cfgdefines=Alias cfgdefines=T
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Walrus edge cases — wired in the new CFG.
|
||||
|
||||
# Walrus in expression context.
|
||||
if (y := 5) > 0: # $ cfgdefines=y
|
||||
pass
|
||||
|
||||
# Walrus in a comprehension. The comprehension introduces a synthetic
|
||||
# `.0` parameter bound to the iterable.
|
||||
_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w cfgdefines=.0
|
||||
@@ -0,0 +1,21 @@
|
||||
# `with cm() as x:` bindings — wired in the new CFG.
|
||||
|
||||
class CM: # $ cfgdefines=CM
|
||||
def __enter__(self): return self # $ cfgdefines=__enter__ cfgdefines=self
|
||||
def __exit__(self, *a): pass # $ cfgdefines=__exit__ cfgdefines=self cfgdefines=a
|
||||
|
||||
with CM() as x: # $ cfgdefines=x
|
||||
pass
|
||||
|
||||
# Multiple items.
|
||||
with CM() as a, CM() as b: # $ cfgdefines=a cfgdefines=b
|
||||
pass
|
||||
|
||||
# Parenthesised form (Python 3.10+).
|
||||
with (CM() as p, CM() as q): # $ cfgdefines=p cfgdefines=q
|
||||
pass
|
||||
|
||||
# Compound target in `with`.
|
||||
with CM() as (m, n): # $ cfgdefines=m cfgdefines=n
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
| test_boolean.py:9:10:9:43 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 1) | test_boolean.py:9:59:9:59 | IntegerLiteral | Timestamp 2 | test_boolean.py:7:1:7:27 | Function test_and_both_sides | test_and_both_sides |
|
||||
| test_boolean.py:15:10:15:43 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 0) | test_boolean.py:15:50:15:50 | IntegerLiteral | Timestamp 1 | test_boolean.py:13:1:13:30 | Function test_and_short_circuit | test_and_short_circuit |
|
||||
| test_boolean.py:21:10:21:42 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 0) | test_boolean.py:21:49:21:49 | IntegerLiteral | Timestamp 1 | test_boolean.py:19:1:19:29 | Function test_or_short_circuit | test_or_short_circuit |
|
||||
| test_boolean.py:27:10:27:34 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 1) | test_boolean.py:27:50:27:50 | IntegerLiteral | Timestamp 2 | test_boolean.py:25:1:25:26 | Function test_or_both_sides | test_or_both_sides |
|
||||
| test_boolean.py:40:10:40:61 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 2) | test_boolean.py:40:86:40:86 | IntegerLiteral | Timestamp 3 | test_boolean.py:38:1:38:24 | Function test_chained_and | test_chained_and |
|
||||
| test_boolean.py:46:10:46:61 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 2) | test_boolean.py:46:86:46:86 | IntegerLiteral | Timestamp 3 | test_boolean.py:44:1:44:23 | Function test_chained_or | test_chained_or |
|
||||
| test_boolean.py:52:10:52:95 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 3) | test_boolean.py:52:120:52:120 | IntegerLiteral | Timestamp 4 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or |
|
||||
| test_boolean.py:52:11:52:47 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 1) | test_boolean.py:52:63:52:63 | IntegerLiteral | Timestamp 2 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or |
|
||||
| test_boolean.py:52:78:52:79 | IntegerLiteral | $@ in $@ has no consecutive predecessor (expected 2) | test_boolean.py:52:85:52:85 | IntegerLiteral | Timestamp 3 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or |
|
||||
| test_comparison.py:37:6:37:41 | Compare | $@ in $@ has no consecutive predecessor (expected 1) | test_comparison.py:37:48:37:48 | IntegerLiteral | Timestamp 2 | test_comparison.py:32:1:32:32 | Function test_three_short_circuit | test_three_short_circuit |
|
||||
| test_if.py:96:9:96:9 | x | $@ in $@ has no consecutive predecessor (expected 1) | test_if.py:96:15:96:15 | IntegerLiteral | Timestamp 2 | test_if.py:93:1:93:34 | Function test_if_compound_condition | test_if_compound_condition |
|
||||
| test_if.py:96:9:96:29 | BoolExpr | $@ in $@ has no consecutive predecessor (expected 3) | test_if.py:96:36:96:36 | IntegerLiteral | Timestamp 4 | test_if.py:93:1:93:34 | Function test_if_compound_condition | test_if_compound_condition |
|
||||
| test_if.py:99:13:99:13 | IntegerLiteral | $@ in $@ has no consecutive predecessor (expected 4) | test_if.py:99:19:99:19 | IntegerLiteral | Timestamp 5 | test_if.py:93:1:93:34 | Function test_if_compound_condition | test_if_compound_condition |
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Checks that each annotated node (except the minimum timestamp) has a
|
||||
* predecessor annotation with timestamp `a - 1`. This is the reverse of
|
||||
* ConsecutiveTimestamps: it catches nodes that are reachable but arrived
|
||||
* at from the wrong place (skipping an intermediate node).
|
||||
*
|
||||
* Only applies to functions where all annotations are in the function's
|
||||
* own scope (excludes tests with generators, async, comprehensions, or
|
||||
* lambdas that have annotations in nested scopes).
|
||||
*/
|
||||
|
||||
import OldCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<OldCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerAnnotation ann, int a
|
||||
where consecutivePredecessorTimestamps(ann, a)
|
||||
select ann, "$@ in $@ has no consecutive predecessor (expected " + (a - 1) + ")",
|
||||
ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName()
|
||||
@@ -7,6 +7,7 @@
|
||||
| test_boolean.py:52:11:52:47 | BoolExpr | $@ in $@ has no consecutive successor (expected 3) | test_boolean.py:52:63:52:63 | IntegerLiteral | Timestamp 2 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or |
|
||||
| test_boolean.py:52:27:52:31 | False | $@ in $@ has no consecutive successor (expected 2) | test_boolean.py:52:37:52:37 | IntegerLiteral | Timestamp 1 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or |
|
||||
| test_boolean.py:52:78:52:79 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 4) | test_boolean.py:52:85:52:85 | IntegerLiteral | Timestamp 3 | test_boolean.py:50:1:50:25 | Function test_mixed_and_or | test_mixed_and_or |
|
||||
| test_comparison.py:37:17:37:17 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 2) | test_comparison.py:37:23:37:23 | IntegerLiteral | Timestamp 1 | test_comparison.py:32:1:32:32 | Function test_three_short_circuit | test_three_short_circuit |
|
||||
| test_if.py:95:9:95:13 | False | $@ in $@ has no consecutive successor (expected 2) | test_if.py:95:19:95:19 | IntegerLiteral | Timestamp 1 | test_if.py:93:1:93:34 | Function test_if_compound_condition | test_if_compound_condition |
|
||||
| test_if.py:96:9:96:29 | BoolExpr | $@ in $@ has no consecutive successor (expected 5) | test_if.py:96:36:96:36 | IntegerLiteral | Timestamp 4 | test_if.py:93:1:93:34 | Function test_if_compound_condition | test_if_compound_condition |
|
||||
| test_if.py:96:22:96:22 | y | $@ in $@ has no consecutive successor (expected 4) | test_if.py:96:28:96:28 | IntegerLiteral | Timestamp 3 | test_if.py:93:1:93:34 | Function test_if_compound_condition | test_if_compound_condition |
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/** New-CFG version of AllLiveReachable. */
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerCfgNode a, TestFunction f
|
||||
where allLiveReachable(a, f)
|
||||
select a, "Unreachable live annotation; entry of $@ does not reach this node", f, f.getName()
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* New-CFG version of AnnotationHasCfgNode.
|
||||
*
|
||||
* Checks that every timer annotation has a corresponding CFG node.
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerAnnotation ann
|
||||
where annotationWithoutCfgNode(ann)
|
||||
select ann, "Annotation in $@ has no CFG node", ann.getTestFunction(),
|
||||
ann.getTestFunction().getName()
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* New-CFG version of BasicBlockAnnotationGap.
|
||||
*
|
||||
* Checks that within a basic block, if a node is annotated then its
|
||||
* successor is also annotated (or excluded). A gap in annotations
|
||||
* within a basic block indicates a missing annotation, since there
|
||||
* are no branches to justify the gap.
|
||||
*
|
||||
* Nodes with exceptional successors are excluded, as the exception
|
||||
* edge leaves the basic block and the normal successor may be dead.
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerCfgNode a, CfgNode succ
|
||||
where basicBlockAnnotationGap(a, succ)
|
||||
select a, "Annotated node followed by unannotated $@ in the same basic block", succ,
|
||||
succ.getNode().toString()
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* New-CFG version of BasicBlockOrdering.
|
||||
*
|
||||
* Checks that within a single basic block, annotations appear in
|
||||
* increasing minimum-timestamp order.
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerCfgNode a, TimerCfgNode b, int minA, int minB
|
||||
where basicBlockOrdering(a, b, minA, minB)
|
||||
select a, "Basic block ordering: $@ appears before $@", a.getTimestampExpr(minA),
|
||||
"timestamp " + minA, b.getTimestampExpr(minB), "timestamp " + minB
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* New-CFG version of BranchTimestamps.
|
||||
*
|
||||
* Checks that when a node has both a true and false successor, the
|
||||
* live timestamps on one branch are marked as dead on the other.
|
||||
* This ensures that boolean branches are fully annotated with dead()
|
||||
* markers for the paths not taken.
|
||||
*
|
||||
* Limitation: the `@ t[ts, ...]` / `dead(ts)` annotation scheme can only
|
||||
* model branch-dead-ness for plain boolean control flow that reconverges
|
||||
* linearly after the split — i.e. `if`-with-else and `if`-expression.
|
||||
* It cannot model:
|
||||
*
|
||||
* * loops (`while` / `for`): body timestamps repeat across iterations,
|
||||
* so the loop-exit annotation can't list them as dead;
|
||||
* * `match` statements: each `case` body is a syntactically distinct
|
||||
* sub-tree, and the branches don't reconverge through a common
|
||||
* annotation point in the timeline;
|
||||
* * `try` / `with` and `raise` / `assert`: exception edges are modeled
|
||||
* as true/false but flow to syntactically distinct handlers, with no
|
||||
* reconvergence in the linear annotation order;
|
||||
* * short-circuit `and` / `or` (`BoolExpr`): the branches reconverge at
|
||||
* the BoolExpr's after-node, so timestamps on one branch are live
|
||||
* downstream of the other rather than dead;
|
||||
* * `if` without an `else` clause, and `if`/`elif` chains: the false
|
||||
* branch reconverges with the true branch at the post-if statement
|
||||
* (no-else) or fans out across multiple elif-test annotations,
|
||||
* neither of which fit the binary annotation scheme.
|
||||
*
|
||||
* Branch nodes inside those constructs are therefore whitelisted out
|
||||
* below. The check still fires (and is useful) for plain `if`/`else`
|
||||
* and conditional-expression branching.
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
/**
|
||||
* Holds if `f` contains a construct whose branches the linear-timestamp
|
||||
* annotation scheme cannot describe (see file-level comment).
|
||||
*/
|
||||
private predicate hasUnmodellableBranching(Function f) {
|
||||
exists(AstNode bad |
|
||||
bad.getScope() = f and
|
||||
(
|
||||
bad instanceof While
|
||||
or
|
||||
bad instanceof For
|
||||
or
|
||||
bad instanceof MatchStmt
|
||||
or
|
||||
bad instanceof Try
|
||||
or
|
||||
bad instanceof With
|
||||
or
|
||||
bad instanceof Raise
|
||||
or
|
||||
bad instanceof Assert
|
||||
or
|
||||
bad instanceof BoolExpr
|
||||
or
|
||||
bad instanceof If and
|
||||
(not exists(bad.(If).getAnOrelse()) or bad.(If).isElif())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
from TimerCfgNode node, int ts, string branch
|
||||
where
|
||||
missingBranchTimestamp(node, ts, branch) and
|
||||
not hasUnmodellableBranching(node.getTestFunction())
|
||||
select node,
|
||||
"Timestamp " + ts + " on true/false branch is missing a dead() annotation on the " + branch +
|
||||
" successor in $@", node.getTestFunction(), node.getTestFunction().getName()
|
||||
@@ -0,0 +1 @@
|
||||
| test_comparison.py:37:6:37:41 | Compare | $@ in $@ has no consecutive predecessor (expected 1) | test_comparison.py:37:48:37:48 | IntegerLiteral | Timestamp 2 | test_comparison.py:32:1:32:32 | Function test_three_short_circuit | test_three_short_circuit |
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* New-CFG version of ConsecutivePredecessorTimestamps.
|
||||
*
|
||||
* Checks that each annotated node (except the minimum timestamp) has
|
||||
* a predecessor annotation with timestamp `a - 1`. This is the reverse
|
||||
* of ConsecutiveTimestamps: it catches nodes that are reachable but
|
||||
* arrived at from the wrong place (skipping an intermediate node).
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerAnnotation ann, int a
|
||||
where consecutivePredecessorTimestamps(ann, a)
|
||||
select ann, "$@ in $@ has no consecutive predecessor (expected " + (a - 1) + ")",
|
||||
ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName()
|
||||
@@ -0,0 +1 @@
|
||||
| test_comparison.py:37:17:37:17 | IntegerLiteral | $@ in $@ has no consecutive successor (expected 2) | test_comparison.py:37:23:37:23 | IntegerLiteral | Timestamp 1 | test_comparison.py:32:1:32:32 | Function test_three_short_circuit | test_three_short_circuit |
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* New-CFG version of ConsecutiveTimestamps.
|
||||
*
|
||||
* Checks that consecutive annotated nodes have consecutive timestamps:
|
||||
* for each annotation with timestamp `a`, some CFG node for that annotation
|
||||
* must have a next annotation containing `a + 1`.
|
||||
*
|
||||
* Handles CFG splitting (e.g., finally blocks duplicated for normal/exceptional
|
||||
* flow) by checking that at least one split has the required successor.
|
||||
*
|
||||
* Only applies to functions where all annotations are in the function's
|
||||
* own scope (excludes tests with generators, async, comprehensions, or
|
||||
* lambdas that have annotations in nested scopes).
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerAnnotation ann, int a
|
||||
where consecutiveTimestamps(ann, a)
|
||||
select ann, "$@ in $@ has no consecutive successor (expected " + (a + 1) + ")",
|
||||
ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName()
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Implementation of the evaluation-order CFG signature using the new
|
||||
* shared control flow graph from AstNodeImpl.
|
||||
*/
|
||||
|
||||
private import python as Py
|
||||
import TimerUtils
|
||||
private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
|
||||
private import codeql.controlflow.SuccessorType
|
||||
|
||||
private class NewControlFlowNode = CfgImpl::ControlFlowNode;
|
||||
|
||||
private class NewBasicBlock = CfgImpl::BasicBlock;
|
||||
|
||||
/** New (shared) CFG implementation of the evaluation-order signature. */
|
||||
module NewCfg implements EvalOrderCfgSig {
|
||||
class CfgNode instanceof NewControlFlowNode {
|
||||
// We must pick a *unique* representative CFG node for each AST node. The
|
||||
// shared CFG has several nodes per AST node (before / in-post-order / after
|
||||
// / after-value splits), but the timer test framework keys annotations on
|
||||
// `getNode()` and assumes one CFG node per annotated AST node. Without a
|
||||
// filter, an annotated `f()` would map to both `f()` and `After f()`, which
|
||||
// breaks two framework invariants: (1) the "no shared reachable" check
|
||||
// requires that two distinct nodes sharing a timestamp be mutually
|
||||
// unreachable (true/false branches of a condition), but `Before f()`,
|
||||
// `f()` and `After f()` share the annotation's timestamp *and* lie on one
|
||||
// linear path; and (2) the annotation walk (`nextTimerAnnotation`) halts at
|
||||
// the first reachable representative, so a second node for the same AST
|
||||
// node would stall the walk on the same timestamp instead of advancing to
|
||||
// the next evaluation event.
|
||||
//
|
||||
// We use the "after" node (`isAfter`) rather than the canonical `injects`
|
||||
// node, because `injects` represents short-circuit / conditional
|
||||
// expressions (`and`/`or`/`not`/ternary) by their *before* node, placing
|
||||
// them ahead of their operands — wrong for evaluation order. `isAfter`
|
||||
// instead picks the post-evaluation node: the merged before/after node for
|
||||
// simple leaves, the `TAfterNode` for post-order expressions, and the
|
||||
// `AfterValueNode`(s) for pre-order conditionals, all positioned after the
|
||||
// operands. The two value-split nodes of a conditional are genuinely
|
||||
// distinct evaluation outcomes (handled by `getATrueSuccessor` /
|
||||
// `getAFalseSuccessor`), so they do not violate the uniqueness assumption.
|
||||
CfgNode() { NewControlFlowNode.super.isAfter(_) }
|
||||
|
||||
string toString() { result = NewControlFlowNode.super.toString() }
|
||||
|
||||
Py::Location getLocation() { result = NewControlFlowNode.super.getLocation() }
|
||||
|
||||
Py::AstNode getNode() {
|
||||
result = CfgImpl::astNodeToPyNode(NewControlFlowNode.super.getAstNode())
|
||||
}
|
||||
|
||||
CfgNode getASuccessor() { nextCfgNode(this, result) }
|
||||
|
||||
CfgNode getATrueSuccessor() {
|
||||
NewControlFlowNode.super.isAfterTrue(_) and
|
||||
// Only where there's also a false branch (true boolean split)
|
||||
exists(NewControlFlowNode other | other.isAfterFalse(NewControlFlowNode.super.getAstNode())) and
|
||||
nextCfgNodeFrom(this, result)
|
||||
}
|
||||
|
||||
CfgNode getAFalseSuccessor() {
|
||||
NewControlFlowNode.super.isAfterFalse(_) and
|
||||
// Only where there's also a true branch (true boolean split)
|
||||
exists(NewControlFlowNode other | other.isAfterTrue(NewControlFlowNode.super.getAstNode())) and
|
||||
nextCfgNodeFrom(this, result)
|
||||
}
|
||||
|
||||
CfgNode getAnExceptionalSuccessor() {
|
||||
exists(NewControlFlowNode mid |
|
||||
mid = NewControlFlowNode.super.getAnExceptionSuccessor() and
|
||||
nextCfgNodeFrom(mid, result)
|
||||
)
|
||||
}
|
||||
|
||||
Py::Scope getScope() { result = NewControlFlowNode.super.getEnclosingCallable().asScope() }
|
||||
|
||||
BasicBlock getBasicBlock() { exists(NewBasicBlock bb | bb.getNode(_) = this and result = bb) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `next` is the nearest CfgNode reachable from `n` via
|
||||
* one or more raw CFG successor edges, skipping non-CfgNode intermediaries.
|
||||
*/
|
||||
private predicate nextCfgNodeFrom(NewControlFlowNode n, CfgNode next) {
|
||||
next = n.getASuccessor()
|
||||
or
|
||||
exists(NewControlFlowNode mid |
|
||||
mid = n.getASuccessor() and
|
||||
not mid instanceof CfgNode and
|
||||
nextCfgNodeFrom(mid, next)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `next` is the nearest CfgNode successor of `n`,
|
||||
* skipping synthetic intermediate nodes.
|
||||
*/
|
||||
private predicate nextCfgNode(CfgNode n, CfgNode next) { nextCfgNodeFrom(n, next) }
|
||||
|
||||
class BasicBlock instanceof NewBasicBlock {
|
||||
string toString() { result = NewBasicBlock.super.toString() }
|
||||
|
||||
CfgNode getNode(int n) { result = NewBasicBlock.super.getNode(n) }
|
||||
|
||||
predicate reaches(BasicBlock bb) { this = bb or this.strictlyReaches(bb) }
|
||||
|
||||
predicate strictlyReaches(BasicBlock bb) { NewBasicBlock.super.getASuccessor+() = bb }
|
||||
|
||||
predicate strictlyDominates(BasicBlock bb) { NewBasicBlock.super.strictlyDominates(bb) }
|
||||
}
|
||||
|
||||
CfgNode scopeGetEntryNode(Py::Scope s) {
|
||||
exists(CfgImpl::ControlFlow::EntryNode entry |
|
||||
entry.getEnclosingCallable().asScope() = s and
|
||||
nextCfgNodeFrom(entry, result)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* New-CFG version of NeverReachable.
|
||||
*
|
||||
* Checks that expressions annotated with `t.never` either have no CFG
|
||||
* node, or if they do, that the node is not reachable from its scope's
|
||||
* entry (including within the same basic block).
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerAnnotation ann
|
||||
where neverReachable(ann)
|
||||
select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(),
|
||||
ann.getTestFunction().getName()
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* New-CFG version of NoBackwardFlow.
|
||||
*
|
||||
* Checks that time never flows backward between consecutive timer annotations
|
||||
* in the CFG. For each pair of consecutive annotated nodes (A -> B), there must
|
||||
* exist timestamps a in A and b in B with a < b.
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerCfgNode a, TimerCfgNode b, int minA, int maxB
|
||||
where noBackwardFlow(a, b, minA, maxB)
|
||||
select a, "Backward flow: $@ flows to $@ (max timestamp $@)", a.getTimestampExpr(minA),
|
||||
minA.toString(), b, b.getNode().toString(), b.getTimestampExpr(maxB), maxB.toString()
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* New-CFG version of NoBasicBlock.
|
||||
*
|
||||
* Checks that every annotated CFG node belongs to a basic block.
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from CfgNode n, TestFunction f
|
||||
where noBasicBlock(n, f)
|
||||
select n, "CFG node in $@ does not belong to any basic block", f, f.getName()
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* New-CFG version of NoSharedReachable.
|
||||
*
|
||||
* Checks that two annotations sharing a timestamp value are on
|
||||
* mutually exclusive CFG paths (neither can reach the other).
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerCfgNode a, TimerCfgNode b, int ts
|
||||
where noSharedReachable(a, b, ts)
|
||||
select a, "Shared timestamp $@ but this node reaches $@", a.getTimestampExpr(ts), ts.toString(), b,
|
||||
b.getNode().toString()
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* New-CFG version of StrictForward.
|
||||
*
|
||||
* Stronger version of NoBackwardFlow: for consecutive annotated nodes
|
||||
* A -> B that both have a single timestamp (non-loop code) and B does
|
||||
* NOT dominate A (forward edge), requires max(A) < min(B).
|
||||
*/
|
||||
|
||||
import python
|
||||
import NewCfgImpl
|
||||
|
||||
private module Utils = EvalOrderCfgUtils<NewCfg>;
|
||||
|
||||
private import Utils
|
||||
private import Utils::CfgTests
|
||||
|
||||
from TimerCfgNode a, TimerCfgNode b, int maxA, int minB
|
||||
where strictForward(a, b, maxA, minB)
|
||||
select a, "Strict forward violation: $@ flows to $@", a.getTimestampExpr(maxA), "timestamp " + maxA,
|
||||
b.getTimestampExpr(minB), "timestamp " + minB
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Comparisons and evaluation order.
|
||||
|
||||
Comparison operands are evaluated left-to-right, followed by the comparison
|
||||
node itself. Annotations record the *run-time* evaluation order, so this file
|
||||
self-validates under CPython (``python3 test_comparison.py``).
|
||||
|
||||
Python short-circuits *chained* comparisons (in ``a < b < c`` the operand ``c``
|
||||
is skipped once ``a < b`` is false), whereas the control-flow graph
|
||||
conservatively evaluates every operand. That divergence cannot be expressed in
|
||||
a single annotation, so the affected CFG queries report it (captured in the
|
||||
ConsecutiveTimestamps and NewCfgConsecutivePredecessorTimestamps .expected
|
||||
files).
|
||||
"""
|
||||
|
||||
from timer import test, dead
|
||||
|
||||
|
||||
@test
|
||||
def test_two(t):
|
||||
# Both operands, then the comparison.
|
||||
(1 @ t[0] < 0 @ t[1]) @ t[2]
|
||||
|
||||
|
||||
@test
|
||||
def test_three(t):
|
||||
# Chained comparison, all comparisons hold: operands left-to-right, then
|
||||
# the comparison. Run time and CFG agree.
|
||||
(1 @ t[0] < 2 @ t[1] < 3 @ t[2]) @ t[3]
|
||||
|
||||
|
||||
@test
|
||||
def test_three_short_circuit(t):
|
||||
# ``1 > 2`` is false, so at run time Python short-circuits and never
|
||||
# evaluates ``3``; the comparison is reached at timestamp 2. The CFG still
|
||||
# evaluates ``3`` (dead at run time), which is why the CFG queries report a
|
||||
# gap around this comparison.
|
||||
(1 @ t[0] > 2 @ t[1] < 3 @ t[dead(2)]) @ t[2]
|
||||
@@ -85,7 +85,7 @@ def test_nested_if_else(t):
|
||||
else:
|
||||
z = 2 @ t[dead(4)]
|
||||
else:
|
||||
z = 3 @ t[dead(4)]
|
||||
z = 3 @ t[dead(3), dead(4)]
|
||||
w = 0 @ t[5]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Inline-expectations test for the store/load/delete/parameter
|
||||
* classification predicates on the new-CFG facade.
|
||||
*
|
||||
* Each tag fires when the corresponding predicate (`isLoad`,
|
||||
* `isStore`, `isDelete`, `isParameter`, `isAugLoad`, `isAugStore`)
|
||||
* holds on the canonical CFG node wrapping a `Py::Name` with the
|
||||
* given identifier. Subscript and attribute stores are not covered
|
||||
* by these tags — only the `Name`-typed targets/loads they involve.
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.controlflow.internal.Cfg as Cfg
|
||||
import utils.test.InlineExpectationsTest
|
||||
|
||||
module StoreLoadTest implements TestSig {
|
||||
string getARelevantTag() { result = ["load", "store", "delete", "param", "augload", "augstore"] }
|
||||
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(Cfg::NameNode n |
|
||||
location = n.getLocation() and
|
||||
element = n.toString() and
|
||||
value = n.getId() and
|
||||
(
|
||||
n.isLoad() and not n.isAugLoad() and tag = "load"
|
||||
or
|
||||
n.isStore() and not n.isAugStore() and tag = "store"
|
||||
or
|
||||
n.isDelete() and tag = "delete"
|
||||
or
|
||||
n.isParameter() and tag = "param"
|
||||
or
|
||||
n.isAugLoad() and tag = "augload"
|
||||
or
|
||||
n.isAugStore() and tag = "augstore"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import MakeTest<StoreLoadTest>
|
||||
56
python/ql/test/library-tests/ControlFlow/store-load/test.py
Normal file
56
python/ql/test/library-tests/ControlFlow/store-load/test.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Store/load/delete/parameter classification on the new-CFG facade.
|
||||
#
|
||||
# Each annotated location carries the (sorted, deduplicated) set of
|
||||
# kinds the CFG facade reports there. Comparing against the legacy
|
||||
# 'semmle.python.Flow' classification is done by the comparison query
|
||||
# 'StoreLoadParity.ql' — annotations here are only the positive
|
||||
# assertions for the new facade.
|
||||
#
|
||||
# Tags:
|
||||
# load=<id> -- isLoad() fires on the Name
|
||||
# store=<id> -- isStore() fires
|
||||
# delete=<id> -- isDelete() fires
|
||||
# param=<id> -- isParameter() fires
|
||||
# augload=<id> -- isAugLoad() fires (the LHS of x += ... when read)
|
||||
# augstore=<id> -- isAugStore() fires (the LHS of x += ... when written)
|
||||
|
||||
|
||||
# --- plain load / store / delete ---
|
||||
|
||||
x = 1 # $ store=x
|
||||
y = x + 1 # $ store=y load=x
|
||||
print(y) # $ load=print load=y
|
||||
del x # $ delete=x
|
||||
|
||||
|
||||
# --- function definitions (parameters) ---
|
||||
|
||||
def f(a, b=2, *args, c, **kwargs): # $ store=f param=a param=b param=args param=c param=kwargs
|
||||
return a + b + c # $ load=a load=b load=c
|
||||
|
||||
|
||||
# --- augmented assignment splits one Name into load + store halves ---
|
||||
|
||||
def aug(): # $ store=aug
|
||||
n = 0 # $ store=n
|
||||
n += 1 # $ augload=n augstore=n
|
||||
return n # $ load=n
|
||||
|
||||
|
||||
# --- subscript / attribute stores ---
|
||||
|
||||
class C: # $ store=C
|
||||
pass
|
||||
|
||||
|
||||
def stores(obj, container, idx): # $ store=stores param=obj param=container param=idx
|
||||
obj.attr = 1 # $ load=obj
|
||||
container[idx] = 2 # $ load=container load=idx
|
||||
return obj # $ load=obj
|
||||
|
||||
|
||||
# --- tuple unpacking ---
|
||||
|
||||
def unpack(pair): # $ store=unpack param=pair
|
||||
a, b = pair # $ store=a store=b load=pair
|
||||
return a + b # $ load=a load=b
|
||||
Reference in New Issue
Block a user