From 77149be759c451dd429e09046722576562dc5186 Mon Sep 17 00:00:00 2001 From: yoff Date: Mon, 18 May 2026 09:58:27 +0000 Subject: [PATCH] Python: wire PEP 695 type parameters into the shared CFG (green) Adds CFG coverage for the binding 'Name's introduced by PEP 695 type-parameter syntax on functions, classes, and 'type' aliases: def func[T](...): ... class Box[T]: ... def multi[T: int, *Ts, **P](...): ... type Alias[T] = ... For each parametrised AST node, the type-parameter names (and, for 'type' aliases, the alias name itself) are added as children of the enclosing CFG node so that 'Name.defines(v)' has a corresponding position. Bounds and defaults are intentionally not wired (they have no SSA-relevant semantics for our purposes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controlflow/internal/AstNodeImpl.qll | 78 ++++++++++++++++++- .../ControlFlow/bindings/type_params.py | 8 +- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll index 7c3b498cfea..d7e54e64aa8 100644 --- a/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll +++ b/python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll @@ -15,6 +15,19 @@ private import codeql.controlflow.ControlFlowGraph private import codeql.controlflow.SuccessorType private import codeql.util.Void +/** + * Gets the bound `Name` of a PEP 695 type parameter (`TypeVar`, + * `ParamSpec`, or `TypeVarTuple`). The base `TypeParameter` class does + * not expose `getName()`; this helper dispatches over the subtypes. + */ +private Py::Name typeParameterName(Py::TypeParameter tp) { + result = tp.(Py::TypeVar).getName() + or + result = tp.(Py::ParamSpec).getName() + or + result = tp.(Py::TypeVarTuple).getName() +} + /** Provides the Python implementation of the shared CFG `AstSig`. */ module Ast implements AstSig { private newtype TAstNode = @@ -797,6 +810,37 @@ module Ast implements AstSig { override AstNode getChild(int index) { result = this.getTarget(index) } } + /** + * A PEP 695 `type` statement (`type Alias[T1, T2] = value`). + * + * The type parameters bind at statement-evaluation time. The value + * expression is captured for lazy evaluation but the alias `Name` + * itself binds the resulting `TypeAliasType` object — so the CFG must + * visit at minimum the type-parameter names and the alias name. + */ + additional class TypeAliasStmt extends Stmt { + private Py::TypeAlias ta; + + TypeAliasStmt() { this = TPyStmt(ta) } + + /** Gets the alias `Name` bound by this statement. */ + Expr getName() { result.asExpr() = ta.getName() } + + /** + * Gets the `n`th PEP 695 type-parameter name (a `Name` in store + * context), in declaration order. + */ + Expr getTypeParamName(int n) { result.asExpr() = typeParameterName(ta.getTypeParameter(n)) } + + int getNumberOfTypeParams() { result = count(ta.getATypeParameter()) } + + override AstNode getChild(int index) { + result = this.getTypeParamName(index) + or + index = this.getNumberOfTypeParams() and result = this.getName() + } + } + /** A `try` statement. */ class TryStmt extends Stmt { private Py::Try tryStmt; @@ -1359,9 +1403,24 @@ module Ast implements AstSig { ClassDefExpr() { this = TPyExpr(classExpr) } + /** + * Gets the `n`th PEP 695 type-parameter name (a `Name` in store + * context), in declaration order. These bind in the enclosing scope + * at class-definition time, so the CFG must visit them. + */ + Expr getTypeParamName(int n) { + result.asExpr() = typeParameterName(classExpr.getTypeParameter(n)) + } + + int getNumberOfTypeParams() { result = count(classExpr.getATypeParameter()) } + Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) } - override AstNode getChild(int index) { result = this.getBase(index) } + override AstNode getChild(int index) { + result = this.getTypeParamName(index) + or + result = this.getBase(index - this.getNumberOfTypeParams()) + } } /** A function definition expression (has default args evaluated at definition time). */ @@ -1370,6 +1429,17 @@ module Ast implements AstSig { FunctionDefExpr() { this = TPyExpr(funcExpr) } + /** + * Gets the `n`th PEP 695 type-parameter name (a `Name` in store + * context), in declaration order. These bind in the enclosing scope + * at function-definition time, so the CFG must visit them. + */ + Expr getTypeParamName(int n) { + result.asExpr() = typeParameterName(funcExpr.getInnerScope().getTypeParameter(n)) + } + + int getNumberOfTypeParams() { result = count(funcExpr.getInnerScope().getATypeParameter()) } + /** * Gets the `n`th default for a positional argument, in evaluation * order. Note that `Args.getDefault(int)` is indexed by argument @@ -1390,9 +1460,11 @@ module Ast implements AstSig { int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) } override AstNode getChild(int index) { - result = this.getDefault(index) + result = this.getTypeParamName(index) or - result = this.getKwDefault(index - this.getNumberOfDefaults()) + result = this.getDefault(index - this.getNumberOfTypeParams()) + or + result = this.getKwDefault(index - this.getNumberOfTypeParams() - this.getNumberOfDefaults()) } } 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 554b96c218a..3e5aaf9d042 100644 --- a/python/ql/test/library-tests/ControlFlow/bindings/type_params.py +++ b/python/ql/test/library-tests/ControlFlow/bindings/type_params.py @@ -1,18 +1,18 @@ # PEP 695 type parameters (Python 3.12+). -def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x MISSING: cfgdefines=T +def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x cfgdefines=T return x -class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T +class Box[T]: # $ cfgdefines=Box cfgdefines=T item: T # $ cfgdefines=item # Multi-parameter, with bound and variadics. -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 +def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs cfgdefines=T cfgdefines=Ts cfgdefines=P return x # `type` statement (PEP 695). -type Alias[T] = list[T] # $ MISSING: cfgdefines=Alias MISSING: cfgdefines=T +type Alias[T] = list[T] # $ cfgdefines=Alias cfgdefines=T