Skip to content

Commit 336c7a4

Browse files
Copilotyoff
authored andcommitted
Python: add CFG-binding gap tests (red)
Adds inline-expectation tests for the new shared CFG implementation in python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll, covering every Python binding construct that introduces a variable. The test files use MISSING: annotations to record bindings whose defining Name AST node is *not* currently reachable from the new CFG. These are the 'red' half of red-green commit pairs: subsequent commits will extend AstNodeImpl to cover each construct and remove the corresponding MISSING: marker. Confirmed-broken categories: - Import aliases (from x import a) - Annotated assignment (x: int = 1) - Exception handler (except E as e) - Match patterns (case x, case [a,b], case ... as v) - PEP 695 type params (def f[T], class C[T]) Confirmed-working (no MISSING:): - Compound targets, with-as, comprehensions, decorated def/class, walrus, starred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a70abdd commit 336c7a4

13 files changed

Lines changed: 230 additions & 0 deletions

File tree

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

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Phase -1 of the dataflow CFG migration: verifies that every variable
3+
* binding visible to the AST (`Name.defines(v)`) corresponds to a CFG node
4+
* in the new CFG (`semmle.python.controlflow.internal.AstNodeImpl`).
5+
*
6+
* The expected tag is `cfgdefines=<name>`. Each binding annotation in the
7+
* test sources looks like `# $ cfgdefines=x` for a binding currently
8+
* covered by the new CFG, or `# $ MISSING: cfgdefines=x` for a binding
9+
* that is known to be uncovered (a "red" test case that should be
10+
* 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.
15+
*/
16+
17+
import python
18+
import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
19+
import utils.test.InlineExpectationsTest
20+
21+
module CfgBindingsTest implements TestSig {
22+
string getARelevantTag() { result = "cfgdefines" }
23+
24+
predicate hasActualResult(Location location, string element, string tag, string value) {
25+
exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg |
26+
n.defines(v) and
27+
not py_expr_contexts(_, 4, n) and // exclude parameters
28+
cfg.getAstNode().asExpr() = n and
29+
location = n.getLocation() and
30+
element = n.toString() and
31+
tag = "cfgdefines" and
32+
value = v.getId()
33+
)
34+
}
35+
}
36+
37+
import MakeTest<CfgBindingsTest>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Annotated assignment (PEP 526). Both with and without an initializer.
2+
3+
a: int = 1 # $ MISSING: cfgdefines=a
4+
b: str = "hi" # $ MISSING: cfgdefines=b
5+
6+
# Annotation without value: the AST records `c` as defined,
7+
# but currently the new CFG has no node for it.
8+
c: int # $ MISSING: cfgdefines=c
9+
10+
class K: # $ cfgdefines=K
11+
field: int = 0 # $ MISSING: cfgdefines=field
12+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compound (tuple/list) assignment targets — actually wired in the new CFG.
2+
3+
a, b = (1, 2) # $ cfgdefines=a cfgdefines=b
4+
[c, d] = [3, 4] # $ cfgdefines=c cfgdefines=d
5+
6+
# Nested unpacking.
7+
(e, (f, g)) = (1, (2, 3)) # $ cfgdefines=e cfgdefines=f cfgdefines=g
8+
9+
# Star unpacking.
10+
h, *i = [1, 2, 3] # $ cfgdefines=h cfgdefines=i
11+
12+
# Chained assignment with compound target.
13+
j = k, l = (5, 6) # $ cfgdefines=j cfgdefines=k cfgdefines=l
14+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Comprehension and `for` loop targets — wired in the new CFG.
2+
3+
# Bare-name `for` target.
4+
for i in range(3): # $ cfgdefines=i
5+
pass
6+
7+
# Compound `for` target.
8+
for k, v in [(1, 2)]: # $ cfgdefines=k cfgdefines=v
9+
pass
10+
11+
# 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
15+
16+
# Nested comprehensions.
17+
_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b
18+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Decorated `def`/`class` — wired in the new CFG.
2+
3+
4+
def deco(f): # $ cfgdefines=deco
5+
return f
6+
7+
8+
@deco
9+
def decorated_func(): # $ cfgdefines=decorated_func
10+
pass
11+
12+
13+
@deco
14+
class DecoratedClass: # $ cfgdefines=DecoratedClass
15+
pass
16+
17+
18+
# Stacked decorators.
19+
@deco
20+
@deco
21+
def doubly(): # $ cfgdefines=doubly
22+
pass
23+
24+
25+
# Inside a class body.
26+
class Outer: # $ cfgdefines=Outer
27+
@staticmethod
28+
def inner(): # $ cfgdefines=inner
29+
pass
30+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Exception-handler name bindings. These are already wired in the new
2+
# CFG provided the try body can raise; `raise` statements are reliably
3+
# treated as exception sources.
4+
5+
try:
6+
raise ValueError("oops")
7+
except ValueError as e: # $ cfgdefines=e
8+
pass
9+
10+
try:
11+
raise TypeError("oops")
12+
except (TypeError, KeyError) as err: # $ cfgdefines=err
13+
pass
14+
15+
# Exception groups (Python 3.11+).
16+
try:
17+
raise ValueError("oops")
18+
except* ValueError as eg: # $ cfgdefines=eg
19+
pass
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Import aliases. All bound names below currently lack a CFG node.
2+
3+
import os # $ MISSING: cfgdefines=os
4+
import os.path # $ MISSING: cfgdefines=os
5+
import os as o # $ MISSING: cfgdefines=o
6+
from os import path # $ MISSING: cfgdefines=path
7+
from os import path as p # $ MISSING: cfgdefines=p
8+
from os import sep, linesep # $ MISSING: cfgdefines=sep MISSING: cfgdefines=linesep
9+
from os import (
10+
getcwd, # $ MISSING: cfgdefines=getcwd
11+
getcwdb, # $ MISSING: cfgdefines=getcwdb
12+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Match-statement pattern bindings.
2+
3+
def f(subject): # $ cfgdefines=f
4+
match subject:
5+
case x: # $ MISSING: cfgdefines=x
6+
pass
7+
case [a, b]: # $ MISSING: cfgdefines=a MISSING: cfgdefines=b
8+
pass
9+
case {"k": v}: # $ MISSING: cfgdefines=v
10+
pass
11+
case Point(p, q): # $ MISSING: cfgdefines=p MISSING: cfgdefines=q
12+
pass
13+
case [_, *rest]: # $ MISSING: cfgdefines=rest
14+
pass
15+
case (1 | 2) as n: # $ MISSING: cfgdefines=n
16+
pass
17+
18+
19+
class Point: # $ cfgdefines=Point
20+
__match_args__ = ("x", "y") # $ cfgdefines=__match_args__
21+
x: int # $ MISSING: cfgdefines=x
22+
y: int # $ MISSING: cfgdefines=y
23+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Simple bindings that should already work in the new CFG.
2+
# No MISSING annotations expected.
3+
4+
x = 1 # $ cfgdefines=x
5+
y = x + 1 # $ cfgdefines=y
6+
7+
def f(): # $ cfgdefines=f
8+
pass
9+
10+
class C: # $ cfgdefines=C
11+
pass
12+
13+
# Re-assignment.
14+
x = 2 # $ cfgdefines=x

0 commit comments

Comments
 (0)