diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index d59b51375a1..cc014440291 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -141,22 +141,63 @@ module Ast implements AstSig { /** * 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 { diff --git a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql index a88b9edc1d4..a507878911b 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql +++ b/python/ql/test/library-tests/ControlFlow/bindings/BindingsTest.ql @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py index eb7c4eade26..6b5f722c1f7 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/comprehension.py @@ -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 + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/decorated.py b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py index c48906c63d8..9b93c166ace 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/decorated.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/decorated.py @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py index 66fafdcb63d..5977e344383 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/match_pattern.py @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/parameters.py b/python/ql/test/library-tests/ControlFlow/bindings/parameters.py new file mode 100644 index 00000000000..7fe5e01e4c4 --- /dev/null +++ b/python/ql/test/library-tests/ControlFlow/bindings/parameters.py @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py index a17f6e29dfd..554b96c218a 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -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 diff --git a/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py index a168240af95..5c0c1bd8319 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/walrus_starred.py @@ -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 + diff --git a/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py index 47f210abc38..5fffe46c5d4 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/with_stmt.py @@ -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