From 336c7a44a80facf25bfbe61ea6f7691475efd6a0 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 12:12:13 +0000 Subject: [PATCH] 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> --- .../bindings/BindingsTest.expected | 0 .../ControlFlow/bindings/BindingsTest.ql | 37 +++++++++++++++++++ .../ControlFlow/bindings/annassign.py | 12 ++++++ .../ControlFlow/bindings/compound.py | 14 +++++++ .../ControlFlow/bindings/comprehension.py | 18 +++++++++ .../ControlFlow/bindings/decorated.py | 30 +++++++++++++++ .../ControlFlow/bindings/except_handler.py | 19 ++++++++++ .../ControlFlow/bindings/imports.py | 12 ++++++ .../ControlFlow/bindings/match_pattern.py | 23 ++++++++++++ .../ControlFlow/bindings/simple.py | 14 +++++++ .../ControlFlow/bindings/type_params.py | 18 +++++++++ .../ControlFlow/bindings/walrus_starred.py | 12 ++++++ .../ControlFlow/bindings/with_stmt.py | 21 +++++++++++ 13 files changed, 230 insertions(+) create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.expected create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/annassign.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/compound.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/comprehension.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/decorated.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/except_handler.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/imports.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/simple.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/type_params.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py create mode 100644 python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py diff --git a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.expected b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql new file mode 100644 index 00000000000..a88b9edc1d4 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql @@ -0,0 +1,37 @@ +/** + * 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=`. 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). + * + * 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 +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 + not py_expr_contexts(_, 4, n) and // exclude parameters + cfg.getAstNode().asExpr() = n and + location = n.getLocation() and + element = n.toString() and + tag = "cfgdefines" and + value = v.getId() + ) + } +} + +import MakeTest diff --git a/python/ql/test/library-tests/ControlFlow/bindings/annassign.py b/python/ql/test/library-tests/ControlFlow/bindings/annassign.py new file mode 100644 index 00000000000..9f80b8bffbd --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/annassign.py @@ -0,0 +1,12 @@ +# Annotated assignment (PEP 526). Both with and without an initializer. + +a: int = 1 # $ MISSING: cfgdefines=a +b: str = "hi" # $ MISSING: cfgdefines=b + +# Annotation without value: the AST records `c` as defined, +# but currently the new CFG has no node for it. +c: int # $ MISSING: cfgdefines=c + +class K: # $ cfgdefines=K + field: int = 0 # $ MISSING: cfgdefines=field + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/compound.py b/python/ql/test/library-tests/ControlFlow/bindings/compound.py new file mode 100644 index 00000000000..cb2f36f12ff --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/compound.py @@ -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 + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py new file mode 100644 index 00000000000..eb7c4eade26 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py @@ -0,0 +1,18 @@ +# Comprehension and `for` loop targets — wired in the new CFG. + +# 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 +_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z +_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a + +# Nested comprehensions. +_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/decorated.py b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py new file mode 100644 index 00000000000..c48906c63d8 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py @@ -0,0 +1,30 @@ +# Decorated `def`/`class` — wired in the new CFG. + + +def deco(f): # $ cfgdefines=deco + 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 + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/except_handler.py b/python/ql/test/library-tests/ControlFlow/bindings/except_handler.py new file mode 100644 index 00000000000..57b6c99fe9b --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/except_handler.py @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/imports.py b/python/ql/test/library-tests/ControlFlow/bindings/imports.py new file mode 100644 index 00000000000..1b657c7db6c --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/imports.py @@ -0,0 +1,12 @@ +# Import aliases. All bound names below currently lack a CFG node. + +import os # $ MISSING: cfgdefines=os +import os.path # $ MISSING: cfgdefines=os +import os as o # $ MISSING: cfgdefines=o +from os import path # $ MISSING: cfgdefines=path +from os import path as p # $ MISSING: cfgdefines=p +from os import sep, linesep # $ MISSING: cfgdefines=sep MISSING: cfgdefines=linesep +from os import ( + getcwd, # $ MISSING: cfgdefines=getcwd + getcwdb, # $ MISSING: cfgdefines=getcwdb +) diff --git a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py new file mode 100644 index 00000000000..46e7a8dd4ef --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py @@ -0,0 +1,23 @@ +# Match-statement pattern bindings. + +def f(subject): # $ cfgdefines=f + match subject: + case x: # $ MISSING: cfgdefines=x + pass + case [a, b]: # $ MISSING: cfgdefines=a MISSING: cfgdefines=b + pass + case {"k": v}: # $ MISSING: cfgdefines=v + pass + case Point(p, q): # $ MISSING: cfgdefines=p MISSING: cfgdefines=q + pass + case [_, *rest]: # $ MISSING: cfgdefines=rest + pass + case (1 | 2) as n: # $ MISSING: cfgdefines=n + pass + + +class Point: # $ cfgdefines=Point + __match_args__ = ("x", "y") # $ cfgdefines=__match_args__ + x: int # $ MISSING: cfgdefines=x + y: int # $ MISSING: cfgdefines=y + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/simple.py b/python/ql/test/library-tests/ControlFlow/bindings/simple.py new file mode 100644 index 00000000000..51cb7d828c9 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/simple.py @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py new file mode 100644 index 00000000000..ab32370bd7d --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -0,0 +1,18 @@ +# PEP 695 type parameters (Python 3.12+). + +def func[T](x: T) -> T: # $ cfgdefines=func MISSING: cfgdefines=T + return x + + +class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T + item: T # $ MISSING: cfgdefines=item + + +# 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 + return x + + +# `type` statement (PEP 695). +type Alias[T] = list[T] # $ MISSING: cfgdefines=Alias MISSING: cfgdefines=T + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py new file mode 100644 index 00000000000..a168240af95 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py @@ -0,0 +1,12 @@ +# Walrus and starred-target edge cases — wired in the new CFG. + +# Walrus in expression context. +if (y := 5) > 0: # $ cfgdefines=y + pass + +# Walrus in a comprehension. +_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w + +# Starred target in a Tuple LHS. +*head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py new file mode 100644 index 00000000000..47f210abc38 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py @@ -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__ + def __exit__(self, *a): pass # $ cfgdefines=__exit__ + +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 +