Skip to content

Commit 01c6b2b

Browse files
yoffCopilot
andcommitted
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>
1 parent f123072 commit 01c6b2b

2 files changed

Lines changed: 79 additions & 7 deletions

File tree

python/ql/lib/semmle/python/controlflow/internal/AstNodeImpl.qll

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ private import codeql.controlflow.ControlFlowGraph
1515
private import codeql.controlflow.SuccessorType
1616
private import codeql.util.Void
1717

18+
/**
19+
* Gets the bound `Name` of a PEP 695 type parameter (`TypeVar`,
20+
* `ParamSpec`, or `TypeVarTuple`). The base `TypeParameter` class does
21+
* not expose `getName()`; this helper dispatches over the subtypes.
22+
*/
23+
private Py::Name typeParameterName(Py::TypeParameter tp) {
24+
result = tp.(Py::TypeVar).getName()
25+
or
26+
result = tp.(Py::ParamSpec).getName()
27+
or
28+
result = tp.(Py::TypeVarTuple).getName()
29+
}
30+
1831
/** Provides the Python implementation of the shared CFG `AstSig`. */
1932
module Ast implements AstSig<Py::Location> {
2033
private newtype TAstNode =
@@ -797,6 +810,37 @@ module Ast implements AstSig<Py::Location> {
797810
override AstNode getChild(int index) { result = this.getTarget(index) }
798811
}
799812

813+
/**
814+
* A PEP 695 `type` statement (`type Alias[T1, T2] = value`).
815+
*
816+
* The type parameters bind at statement-evaluation time. The value
817+
* expression is captured for lazy evaluation but the alias `Name`
818+
* itself binds the resulting `TypeAliasType` object — so the CFG must
819+
* visit at minimum the type-parameter names and the alias name.
820+
*/
821+
additional class TypeAliasStmt extends Stmt {
822+
private Py::TypeAlias ta;
823+
824+
TypeAliasStmt() { this = TPyStmt(ta) }
825+
826+
/** Gets the alias `Name` bound by this statement. */
827+
Expr getName() { result.asExpr() = ta.getName() }
828+
829+
/**
830+
* Gets the `n`th PEP 695 type-parameter name (a `Name` in store
831+
* context), in declaration order.
832+
*/
833+
Expr getTypeParamName(int n) { result.asExpr() = typeParameterName(ta.getTypeParameter(n)) }
834+
835+
int getNumberOfTypeParams() { result = count(ta.getATypeParameter()) }
836+
837+
override AstNode getChild(int index) {
838+
result = this.getTypeParamName(index)
839+
or
840+
index = this.getNumberOfTypeParams() and result = this.getName()
841+
}
842+
}
843+
800844
/** A `try` statement. */
801845
class TryStmt extends Stmt {
802846
private Py::Try tryStmt;
@@ -1359,9 +1403,24 @@ module Ast implements AstSig<Py::Location> {
13591403

13601404
ClassDefExpr() { this = TPyExpr(classExpr) }
13611405

1406+
/**
1407+
* Gets the `n`th PEP 695 type-parameter name (a `Name` in store
1408+
* context), in declaration order. These bind in the enclosing scope
1409+
* at class-definition time, so the CFG must visit them.
1410+
*/
1411+
Expr getTypeParamName(int n) {
1412+
result.asExpr() = typeParameterName(classExpr.getTypeParameter(n))
1413+
}
1414+
1415+
int getNumberOfTypeParams() { result = count(classExpr.getATypeParameter()) }
1416+
13621417
Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) }
13631418

1364-
override AstNode getChild(int index) { result = this.getBase(index) }
1419+
override AstNode getChild(int index) {
1420+
result = this.getTypeParamName(index)
1421+
or
1422+
result = this.getBase(index - this.getNumberOfTypeParams())
1423+
}
13651424
}
13661425

13671426
/** A function definition expression (has default args evaluated at definition time). */
@@ -1370,6 +1429,17 @@ module Ast implements AstSig<Py::Location> {
13701429

13711430
FunctionDefExpr() { this = TPyExpr(funcExpr) }
13721431

1432+
/**
1433+
* Gets the `n`th PEP 695 type-parameter name (a `Name` in store
1434+
* context), in declaration order. These bind in the enclosing scope
1435+
* at function-definition time, so the CFG must visit them.
1436+
*/
1437+
Expr getTypeParamName(int n) {
1438+
result.asExpr() = typeParameterName(funcExpr.getInnerScope().getTypeParameter(n))
1439+
}
1440+
1441+
int getNumberOfTypeParams() { result = count(funcExpr.getInnerScope().getATypeParameter()) }
1442+
13731443
/**
13741444
* Gets the `n`th default for a positional argument, in evaluation
13751445
* order. Note that `Args.getDefault(int)` is indexed by argument
@@ -1390,9 +1460,11 @@ module Ast implements AstSig<Py::Location> {
13901460
int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) }
13911461

13921462
override AstNode getChild(int index) {
1393-
result = this.getDefault(index)
1463+
result = this.getTypeParamName(index)
13941464
or
1395-
result = this.getKwDefault(index - this.getNumberOfDefaults())
1465+
result = this.getDefault(index - this.getNumberOfTypeParams())
1466+
or
1467+
result = this.getKwDefault(index - this.getNumberOfTypeParams() - this.getNumberOfDefaults())
13961468
}
13971469
}
13981470

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
# PEP 695 type parameters (Python 3.12+).
22

3-
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x MISSING: cfgdefines=T
3+
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x cfgdefines=T
44
return x
55

66

7-
class Box[T]: # $ cfgdefines=Box MISSING: cfgdefines=T
7+
class Box[T]: # $ cfgdefines=Box cfgdefines=T
88
item: T # $ cfgdefines=item
99

1010

1111
# Multi-parameter, with bound and variadics.
12-
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
12+
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
1313
return x
1414

1515

1616
# `type` statement (PEP 695).
17-
type Alias[T] = list[T] # $ MISSING: cfgdefines=Alias MISSING: cfgdefines=T
17+
type Alias[T] = list[T] # $ cfgdefines=Alias cfgdefines=T
1818

0 commit comments

Comments
 (0)