Skip to content

Commit 768ebc1

Browse files
Copilotyoff
authored andcommitted
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>
1 parent 5d60a0d commit 768ebc1

9 files changed

Lines changed: 109 additions & 26 deletions

File tree

python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,63 @@ module Ast implements AstSig<Py::Location> {
141141
/**
142142
* A parameter of a callable.
143143
*
144-
* TODO: Implement in order to include parameters in the CFG.
144+
* Modelled per the C# template (`csharp/.../ControlFlowGraph.qll:147-156`):
145+
* each Python parameter (the `Py::Parameter` AST node, which is a `Name`
146+
* or — Python 2 only — a `Tuple` in store context) becomes a CFG node
147+
* at a stable position in the enclosing callable's entry sequence.
148+
*
149+
* Default-value expressions for positional and keyword-only parameters
150+
* are wired separately on the `FunctionDefExpr` / `LambdaExpr` wrappers
151+
* (they evaluate at function-definition time, not at call time).
152+
* `Parameter::getDefaultValue()` returns `none()` here, signalling to
153+
* the shared library that the parameter never falls back to a default
154+
* during call binding. This mirrors C# for non-optional parameters.
145155
*/
146-
class Parameter extends AstNodeImpl {
147-
Parameter() { none() }
148-
149-
override string toString() { none() }
156+
class Parameter extends Expr {
157+
private Py::Parameter param;
150158

151-
override Py::Location getLocation() { none() }
159+
Parameter() { this = TPyExpr(param) }
152160

153-
override Callable getEnclosingCallable() { none() }
161+
/** Gets the underlying Python parameter. */
162+
Py::Parameter asParameter() { result = param }
154163

164+
/**
165+
* Gets the default-value expression of this parameter, if any.
166+
*
167+
* Returns `none()`: defaults evaluate at function-definition time and
168+
* are wired into the CFG via `FunctionDefExpr.getDefault` /
169+
* `LambdaExpr.getDefault`. The shared library calls this predicate
170+
* to model the "missing argument → evaluate default" fallback during
171+
* call binding, which Python does not model at the CFG level.
172+
*/
155173
Expr getDefaultValue() { none() }
156174
}
157175

158-
/** Gets the `index`th parameter of callable `c`. */
159-
Parameter callableGetParameter(Callable c, int index) { none() }
176+
/**
177+
* Gets the `index`th parameter of callable `c`, ordered as Python binds
178+
* them at call time: positional, then vararg (`*args`), then
179+
* keyword-only, then kwarg (`**kwargs`).
180+
*/
181+
Parameter callableGetParameter(Callable c, int index) {
182+
exists(Py::Function f | f = c.asScope() |
183+
result.asParameter() =
184+
rank[index + 1](Py::Parameter p, int subOrder, int subIndex |
185+
// positional parameters first
186+
p = f.getArg(subIndex) and subOrder = 0
187+
or
188+
// then *args
189+
p = f.getVararg() and subOrder = 1 and subIndex = 0
190+
or
191+
// then keyword-only parameters
192+
p = f.getKeywordOnlyArg(subIndex) and subOrder = 2
193+
or
194+
// finally **kwargs
195+
p = f.getKwarg() and subOrder = 3 and subIndex = 0
196+
|
197+
p order by subOrder, subIndex
198+
)
199+
)
200+
}
160201

161202
/** A statement. */
162203
class Stmt extends AstNodeImpl, TStmt {

python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
* covered by the new CFG, or `# $ MISSING: cfgdefines=x` for a binding
99
* that is known to be uncovered (a "red" test case that should be
1010
* green-flipped once the corresponding `cfg-ext-*` extension lands).
11-
*
12-
* Parameters (`def f(x):` etc.) are deliberately excluded — Java's
13-
* pattern handles parameter writes at the SSA layer (`hasEntryDef`),
14-
* not as CFG nodes.
1511
*/
1612

1713
import python
@@ -24,7 +20,6 @@ module CfgBindingsTest implements TestSig {
2420
predicate hasActualResult(Location location, string element, string tag, string value) {
2521
exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg |
2622
n.defines(v) and
27-
not py_expr_contexts(_, 4, n) and // exclude parameters
2823
cfg.getAstNode().asExpr() = n and
2924
location = n.getLocation() and
3025
element = n.toString() and
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Comprehension and `for` loop targets — wired in the new CFG.
2+
# Comprehensions are nested function scopes with a synthetic `.0` parameter
3+
# bound to the iterable.
24

35
# Bare-name `for` target.
46
for i in range(3): # $ cfgdefines=i
@@ -9,10 +11,11 @@
911
pass
1012

1113
# Comprehension targets.
12-
_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x
13-
_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z
14-
_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a
14+
_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x cfgdefines=.0
15+
_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z cfgdefines=.0
16+
_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a cfgdefines=.0
1517

1618
# Nested comprehensions.
17-
_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b
19+
_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b cfgdefines=.0
20+
1821

python/ql/test/library-tests/ControlFlow/bindings/decorated.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Decorated `def`/`class` — wired in the new CFG.
22

33

4-
def deco(f): # $ cfgdefines=deco
4+
def deco(f): # $ cfgdefines=deco cfgdefines=f
55
return f
66

77

python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Match-statement pattern bindings.
22

3-
def f(subject): # $ cfgdefines=f
3+
def f(subject): # $ cfgdefines=f cfgdefines=subject
44
match subject:
55
case x: # $ MISSING: cfgdefines=x
66
pass
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Function parameters.
2+
3+
def positional(a, b): # $ cfgdefines=positional cfgdefines=a cfgdefines=b
4+
pass
5+
6+
7+
def with_default(x=1, y=2): # $ cfgdefines=with_default cfgdefines=x cfgdefines=y
8+
pass
9+
10+
11+
def with_vararg(*args): # $ cfgdefines=with_vararg cfgdefines=args
12+
pass
13+
14+
15+
def with_kwarg(**kwargs): # $ cfgdefines=with_kwarg cfgdefines=kwargs
16+
pass
17+
18+
19+
def with_kwonly(*, k1, k2=5): # $ cfgdefines=with_kwonly cfgdefines=k1 cfgdefines=k2
20+
pass
21+
22+
23+
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
24+
pass
25+
26+
27+
# Methods get `self` / `cls`.
28+
class C: # $ cfgdefines=C
29+
def method(self, x): # $ cfgdefines=method cfgdefines=self cfgdefines=x
30+
pass
31+
32+
@classmethod
33+
def cmethod(cls, x): # $ cfgdefines=cmethod cfgdefines=cls cfgdefines=x
34+
pass
35+
36+
37+
# Lambda parameter.
38+
_ = lambda p: p + 1 # $ cfgdefines=_ cfgdefines=p
39+
40+
# PEP 570 positional-only.
41+
def pos_only(a, b, /, c): # $ cfgdefines=pos_only cfgdefines=a cfgdefines=b cfgdefines=c
42+
pass

python/ql/test/library-tests/ControlFlow/bindings/type_params.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PEP 695 type parameters (Python 3.12+).
22

3-
def func[T](x: T) -> T: # $ cfgdefines=func MISSING: cfgdefines=T
3+
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x MISSING: cfgdefines=T
44
return x
55

66

@@ -9,7 +9,7 @@ class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T
99

1010

1111
# Multi-parameter, with bound and variadics.
12-
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
12+
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
1313
return x
1414

1515

python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
if (y := 5) > 0: # $ cfgdefines=y
55
pass
66

7-
# Walrus in a comprehension.
8-
_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w
7+
# Walrus in a comprehension. The comprehension introduces a synthetic
8+
# `.0` parameter bound to the iterable.
9+
_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w cfgdefines=.0
910

1011
# Starred target in a Tuple LHS.
1112
*head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail
1213

14+

python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# `with cm() as x:` bindings — wired in the new CFG.
22

33
class CM: # $ cfgdefines=CM
4-
def __enter__(self): return self # $ cfgdefines=__enter__
5-
def __exit__(self, *a): pass # $ cfgdefines=__exit__
4+
def __enter__(self): return self # $ cfgdefines=__enter__ cfgdefines=self
5+
def __exit__(self, *a): pass # $ cfgdefines=__exit__ cfgdefines=self cfgdefines=a
66

77
with CM() as x: # $ cfgdefines=x
88
pass

0 commit comments

Comments
 (0)