mirror of
https://github.com/github/codeql.git
synced 2026-05-26 17:11:24 +02:00
Python: wire parameters into the shared CFG (C# pattern)
Implements `AstSig::Parameter` and `callableGetParameter(c, i)` in
`AstNodeImpl.qll`, following the C# template
(`csharp/.../ControlFlowGraph.qll:147-156`) rather than Java's
`Parameter() { none() }`.
Each Python parameter (positional, *args, keyword-only, **kwargs) now
becomes a CFG node at a stable position in the enclosing callable's
entry sequence. Defaults still evaluate at function-definition time
via `FunctionDefExpr.getDefault` / `LambdaExpr.getDefault`, so
`Parameter::getDefaultValue()` returns `none()` (the shared CFG
library calls this to model the missing-argument fallback, which
Python does not surface at the CFG level).
The bindings test now exercises parameters (the `py_expr_contexts(_, 4, ...)`
exclusion has been removed). A new `parameters.py` test case covers
positional, defaulted, vararg, kwarg, keyword-only, kitchen-sink,
method (self/cls), lambda, and PEP 570 positional-only parameters.
Several other test files were updated to annotate parameters that the
test had previously hidden (synthetic `.0` comprehension parameter,
method `self`, decorator `f`, etc.).
Verified:
- All 24 ControlFlow/evaluation-order tests still pass.
- CFG consistency query (`python/ql/consistency-queries/CfgConsistency.ql`)
shows zero violations on CPython.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -141,22 +141,63 @@ module Ast implements AstSig<Py::Location> {
|
||||
/**
|
||||
* A parameter of a callable.
|
||||
*
|
||||
* TODO: Implement in order to include parameters in the CFG.
|
||||
* Modelled per the C# template (`csharp/.../ControlFlowGraph.qll:147-156`):
|
||||
* each Python parameter (the `Py::Parameter` AST node, which is a `Name`
|
||||
* or — Python 2 only — a `Tuple` in store context) becomes a CFG node
|
||||
* at a stable position in the enclosing callable's entry sequence.
|
||||
*
|
||||
* Default-value expressions for positional and keyword-only parameters
|
||||
* are wired separately on the `FunctionDefExpr` / `LambdaExpr` wrappers
|
||||
* (they evaluate at function-definition time, not at call time).
|
||||
* `Parameter::getDefaultValue()` returns `none()` here, signalling to
|
||||
* the shared library that the parameter never falls back to a default
|
||||
* during call binding. This mirrors C# for non-optional parameters.
|
||||
*/
|
||||
class Parameter extends AstNodeImpl {
|
||||
Parameter() { none() }
|
||||
class Parameter extends Expr {
|
||||
private Py::Parameter param;
|
||||
|
||||
override string toString() { none() }
|
||||
Parameter() { this = TPyExpr(param) }
|
||||
|
||||
override Py::Location getLocation() { none() }
|
||||
|
||||
override Callable getEnclosingCallable() { none() }
|
||||
/** Gets the underlying Python parameter. */
|
||||
Py::Parameter asParameter() { result = param }
|
||||
|
||||
/**
|
||||
* Gets the default-value expression of this parameter, if any.
|
||||
*
|
||||
* Returns `none()`: defaults evaluate at function-definition time and
|
||||
* are wired into the CFG via `FunctionDefExpr.getDefault` /
|
||||
* `LambdaExpr.getDefault`. The shared library calls this predicate
|
||||
* to model the "missing argument → evaluate default" fallback during
|
||||
* call binding, which Python does not model at the CFG level.
|
||||
*/
|
||||
Expr getDefaultValue() { none() }
|
||||
}
|
||||
|
||||
/** Gets the `index`th parameter of callable `c`. */
|
||||
Parameter callableGetParameter(Callable c, int index) { none() }
|
||||
/**
|
||||
* Gets the `index`th parameter of callable `c`, ordered as Python binds
|
||||
* them at call time: positional, then vararg (`*args`), then
|
||||
* keyword-only, then kwarg (`**kwargs`).
|
||||
*/
|
||||
Parameter callableGetParameter(Callable c, int index) {
|
||||
exists(Py::Function f | f = c.asScope() |
|
||||
result.asParameter() =
|
||||
rank[index + 1](Py::Parameter p, int subOrder, int subIndex |
|
||||
// positional parameters first
|
||||
p = f.getArg(subIndex) and subOrder = 0
|
||||
or
|
||||
// then *args
|
||||
p = f.getVararg() and subOrder = 1 and subIndex = 0
|
||||
or
|
||||
// then keyword-only parameters
|
||||
p = f.getKeywordOnlyArg(subIndex) and subOrder = 2
|
||||
or
|
||||
// finally **kwargs
|
||||
p = f.getKwarg() and subOrder = 3 and subIndex = 0
|
||||
|
|
||||
p order by subOrder, subIndex
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** A statement. */
|
||||
class Stmt extends AstNodeImpl, TStmt {
|
||||
|
||||
@@ -8,10 +8,6 @@
|
||||
* 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).
|
||||
*
|
||||
* Parameters (`def f(x):` etc.) are deliberately excluded — Java's
|
||||
* pattern handles parameter writes at the SSA layer (`hasEntryDef`),
|
||||
* not as CFG nodes.
|
||||
*/
|
||||
|
||||
import python
|
||||
@@ -24,7 +20,6 @@ module CfgBindingsTest implements TestSig {
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg |
|
||||
n.defines(v) and
|
||||
not py_expr_contexts(_, 4, n) and // exclude parameters
|
||||
cfg.getAstNode().asExpr() = n and
|
||||
location = n.getLocation() and
|
||||
element = n.toString() and
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# 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
|
||||
@@ -9,10 +11,11 @@ for k, v in [(1, 2)]: # $ cfgdefines=k cfgdefines=v
|
||||
pass
|
||||
|
||||
# Comprehension targets.
|
||||
_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x
|
||||
_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z
|
||||
_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a
|
||||
_ = [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
|
||||
_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b cfgdefines=.0
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Decorated `def`/`class` — wired in the new CFG.
|
||||
|
||||
|
||||
def deco(f): # $ cfgdefines=deco
|
||||
def deco(f): # $ cfgdefines=deco cfgdefines=f
|
||||
return f
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Match-statement pattern bindings.
|
||||
|
||||
def f(subject): # $ cfgdefines=f
|
||||
def f(subject): # $ cfgdefines=f cfgdefines=subject
|
||||
match subject:
|
||||
case x: # $ MISSING: cfgdefines=x
|
||||
pass
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
# PEP 695 type parameters (Python 3.12+).
|
||||
|
||||
def func[T](x: T) -> T: # $ cfgdefines=func MISSING: cfgdefines=T
|
||||
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x MISSING: cfgdefines=T
|
||||
return x
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T
|
||||
|
||||
|
||||
# Multi-parameter, with bound and variadics.
|
||||
def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi MISSING: cfgdefines=T MISSING: cfgdefines=Ts MISSING: cfgdefines=P
|
||||
def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs MISSING: cfgdefines=T MISSING: cfgdefines=Ts MISSING: cfgdefines=P
|
||||
return x
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
if (y := 5) > 0: # $ cfgdefines=y
|
||||
pass
|
||||
|
||||
# Walrus in a comprehension.
|
||||
_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w
|
||||
# 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
|
||||
|
||||
# Starred target in a Tuple LHS.
|
||||
*head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# `with cm() as x:` bindings — wired in the new CFG.
|
||||
|
||||
class CM: # $ cfgdefines=CM
|
||||
def __enter__(self): return self # $ cfgdefines=__enter__
|
||||
def __exit__(self, *a): pass # $ cfgdefines=__exit__
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user