Skip to content

Commit cd703dc

Browse files
yoffCopilot
andcommitted
Python: omit PEP 695 type-param names from FunctionDefExpr/ClassDefExpr children
PEP 695 type-param names (e.g. `T` in `def func[T]:` or `class Box[T]:`) bind in an annotation scope that nests the function/class body, so their AST scope is the inner function/class — not the enclosing scope where the FunctionDefExpr/ClassDefExpr CFG node lives. Visiting them as children created scope-crossing CFG edges (nonLocalStep violations: 96 across CPython). Drop them from the children list; the legacy CFG omitted them too. TypeAliasStmt is unaffected (its type-params share scope with the alias's enclosing scope). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 350a68d commit cd703dc

2 files changed

Lines changed: 22 additions & 36 deletions

File tree

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

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,49 +1423,34 @@ module Ast implements AstSig<Py::Location> {
14231423
override AstNode getChild(int index) { index = 0 and result = this.getValue() }
14241424
}
14251425

1426-
/** A class definition expression (has base classes evaluated at definition time). */
1426+
/**
1427+
* A class definition expression (visits bases, but NOT PEP 695 type
1428+
* parameters — those bind in an annotation scope that nests the class
1429+
* body, so they belong to the inner scope's CFG, not the enclosing
1430+
* scope's; the legacy CFG also omitted them).
1431+
*/
14271432
additional class ClassDefExpr extends Expr {
14281433
private Py::ClassExpr classExpr;
14291434

14301435
ClassDefExpr() { this = TPyExpr(classExpr) }
14311436

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 class-definition time, so the CFG must visit them.
1436-
*/
1437-
Expr getTypeParamName(int n) {
1438-
result.asExpr() = typeParameterName(classExpr.getTypeParameter(n))
1439-
}
1440-
1441-
int getNumberOfTypeParams() { result = count(classExpr.getATypeParameter()) }
1442-
14431437
Expr getBase(int n) { result.asExpr() = classExpr.getBase(n) }
14441438

1445-
override AstNode getChild(int index) {
1446-
result = this.getTypeParamName(index)
1447-
or
1448-
result = this.getBase(index - this.getNumberOfTypeParams())
1449-
}
1439+
override AstNode getChild(int index) { result = this.getBase(index) }
14501440
}
14511441

1452-
/** A function definition expression (has default args evaluated at definition time). */
1442+
/**
1443+
* A function definition expression (visits positional and keyword
1444+
* defaults, but NOT PEP 695 type parameters — those bind in an
1445+
* annotation scope that nests the function body, so they belong to
1446+
* the inner scope's CFG, not the enclosing scope's; the legacy CFG
1447+
* also omitted them).
1448+
*/
14531449
additional class FunctionDefExpr extends Expr {
14541450
private Py::FunctionExpr funcExpr;
14551451

14561452
FunctionDefExpr() { this = TPyExpr(funcExpr) }
14571453

1458-
/**
1459-
* Gets the `n`th PEP 695 type-parameter name (a `Name` in store
1460-
* context), in declaration order. These bind in the enclosing scope
1461-
* at function-definition time, so the CFG must visit them.
1462-
*/
1463-
Expr getTypeParamName(int n) {
1464-
result.asExpr() = typeParameterName(funcExpr.getInnerScope().getTypeParameter(n))
1465-
}
1466-
1467-
int getNumberOfTypeParams() { result = count(funcExpr.getInnerScope().getATypeParameter()) }
1468-
14691454
/**
14701455
* Gets the `n`th default for a positional argument, in evaluation
14711456
* order. Note that `Args.getDefault(int)` is indexed by argument
@@ -1486,11 +1471,9 @@ module Ast implements AstSig<Py::Location> {
14861471
int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) }
14871472

14881473
override AstNode getChild(int index) {
1489-
result = this.getTypeParamName(index)
1490-
or
1491-
result = this.getDefault(index - this.getNumberOfTypeParams())
1474+
result = this.getDefault(index)
14921475
or
1493-
result = this.getKwDefault(index - this.getNumberOfTypeParams() - this.getNumberOfDefaults())
1476+
result = this.getKwDefault(index - this.getNumberOfDefaults())
14941477
}
14951478
}
14961479

python/ql/test/library-tests/ControlFlow/bindings/type_params.py

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

3-
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x cfgdefines=T
3+
# PEP 695 type-param names on `def`/`class` bind in an annotation scope
4+
# that nests the function/class body — they have no CFG node in the
5+
# enclosing scope (matching the legacy CFG).
6+
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x
47
return x
58

69

7-
class Box[T]: # $ cfgdefines=Box cfgdefines=T
10+
class Box[T]: # $ cfgdefines=Box
811
item: T # $ cfgdefines=item
912

1013

1114
# 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 cfgdefines=T cfgdefines=Ts cfgdefines=P
15+
def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs
1316
return x
1417

1518

0 commit comments

Comments
 (0)