mirror of
https://github.com/github/codeql.git
synced 2026-05-27 17:41:24 +02:00
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>
This commit is contained in:
@@ -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=<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).
|
||||
*
|
||||
* 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<CfgBindingsTest>
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
12
python/ql/test/library-tests/ControlFlow/bindings/imports.py
Normal file
12
python/ql/test/library-tests/ControlFlow/bindings/imports.py
Normal file
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user