mirror of
https://github.com/github/codeql.git
synced 2026-06-21 12:51:08 +02:00
Python: visit function parameter and return annotations in new CFG
The new (shared-CFG-based) Python control flow graph in `semmle.python.controlflow.internal.Cfg` previously did not emit CFG nodes for parameter type annotations (`def f(x: T): ...`) or for the return type annotation (`-> T`). The legacy CFG emitted both, and a small number of framework models rely on this: `LocalSources.qll`'s `annotatedInstance` walks the parameter annotation expression by way of its CFG node to track that a parameter receives an instance of the annotated class. After the dataflow flip to the new CFG/SSA this regression manifested as lost flows in any test exercising annotation-based parameter tracking: FastAPI `Depends()` receivers, Pydantic request bodies, Starlette `WebSocket`, the call-graph type-annotation test, and so on. Extend `FunctionDefExpr` to visit each annotation as a child of the function-def expression, in CPython evaluation order: positional parameter annotations, `*args` annotation, keyword-only parameter annotations, `**kwargs` annotation, then the return annotation. (Lambda expressions have no annotations in Python syntax, so `LambdaExpr` is unchanged.) PEP 695 type parameters remain out of scope; they belong to the inner annotation scope, not the enclosing CFG. Restored test results across `framework/aiohttp`, `framework/fastapi`, `framework/lxml`, the `CallGraph-type-annotations` test, and `CWE-022-PathInjection`. Two FastAPI list-comprehension MISSING markers become positive (`taint_test.py:41,55`). CPython CFG consistency remains clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The new (shared-CFG-based) Python control flow graph now visits parameter and return type annotations as CFG nodes for function definitions, matching the legacy CFG. This restores annotation-based type tracking through framework models such as FastAPI's `Depends()`, Pydantic request models, Starlette `WebSocket` handlers, and any other models that flow a class reference through `Parameter.getAnnotation()` to identify instances of the annotated class.
|
||||
@@ -1446,10 +1446,19 @@ module Ast implements AstSig<Py::Location> {
|
||||
|
||||
/**
|
||||
* A function definition expression (visits positional and keyword
|
||||
* defaults, but NOT PEP 695 type parameters — those bind in an
|
||||
* annotation scope that nests the function body, so they belong to
|
||||
* the inner scope's CFG, not the enclosing scope's; the legacy CFG
|
||||
* also omitted them).
|
||||
* defaults followed by parameter and return type annotations, but NOT
|
||||
* PEP 695 type parameters — those bind in an annotation scope that
|
||||
* nests the function body, so they belong to the inner scope's CFG,
|
||||
* not the enclosing scope's; the legacy CFG also omitted them).
|
||||
*
|
||||
* Evaluation order follows CPython: defaults are pushed first, then
|
||||
* keyword-only defaults, then annotations (the `__annotations__` dict
|
||||
* is built last, before `MAKE_FUNCTION`). Annotations are emitted as
|
||||
* CFG nodes so that flows from a class reference into a parameter's
|
||||
* type annotation are visible to dataflow (e.g. so that framework
|
||||
* models like FastAPI's `Depends()` can use a parameter's type hint
|
||||
* to track that the parameter receives an instance of the annotated
|
||||
* class — see `LocalSources::annotatedInstance`).
|
||||
*/
|
||||
additional class FunctionDefExpr extends Expr {
|
||||
private Py::FunctionExpr funcExpr;
|
||||
@@ -1473,15 +1482,61 @@ module Ast implements AstSig<Py::Location> {
|
||||
rank[n + 1](Py::Expr d, int i | d = funcExpr.getArgs().getKwDefault(i) | d order by i)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `n`th annotation expression, in CPython evaluation
|
||||
* order: positional parameter annotations (by argument position),
|
||||
* `*args` annotation, keyword-only parameter annotations (by
|
||||
* argument position), `**kwargs` annotation, then the return
|
||||
* annotation. Each annotation appears at most once.
|
||||
*/
|
||||
Expr getAnnotation(int n) {
|
||||
result.asExpr() =
|
||||
rank[n + 1](Py::Expr a, int subOrder, int subIndex |
|
||||
functionAnnotation(funcExpr, a, subOrder, subIndex)
|
||||
|
|
||||
a order by subOrder, subIndex
|
||||
)
|
||||
}
|
||||
|
||||
int getNumberOfDefaults() { result = count(funcExpr.getArgs().getADefault()) }
|
||||
|
||||
int getNumberOfKwDefaults() { result = count(funcExpr.getArgs().getAKwDefault()) }
|
||||
|
||||
int getNumberOfAnnotations() {
|
||||
result = count(Py::Expr a | functionAnnotation(funcExpr, a, _, _))
|
||||
}
|
||||
|
||||
override AstNode getChild(int index) {
|
||||
result = this.getDefault(index)
|
||||
or
|
||||
result = this.getKwDefault(index - this.getNumberOfDefaults())
|
||||
or
|
||||
result = this.getAnnotation(index - this.getNumberOfDefaults() - this.getNumberOfKwDefaults())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `a` is an annotation of `funcExpr` in slot
|
||||
* `(subOrder, subIndex)`. Slots are CPython evaluation order:
|
||||
* positional param annotations (subOrder 0, subIndex = argument
|
||||
* position), `*args` annotation (1, 0), keyword-only annotations
|
||||
* (2, position), `**kwargs` annotation (3, 0), return annotation
|
||||
* (4, 0).
|
||||
*/
|
||||
private predicate functionAnnotation(
|
||||
Py::FunctionExpr funcExpr, Py::Expr a, int subOrder, int subIndex
|
||||
) {
|
||||
a = funcExpr.getArgs().getAnnotation(subIndex) and subOrder = 0
|
||||
or
|
||||
a = funcExpr.getArgs().getVarargannotation() and subOrder = 1 and subIndex = 0
|
||||
or
|
||||
a = funcExpr.getArgs().getKwAnnotation(subIndex) and subOrder = 2
|
||||
or
|
||||
a = funcExpr.getArgs().getKwargannotation() and subOrder = 3 and subIndex = 0
|
||||
or
|
||||
a = funcExpr.getReturns() and subOrder = 4 and subIndex = 0
|
||||
}
|
||||
|
||||
/** A lambda expression (has default args evaluated at definition time). */
|
||||
additional class LambdaExpr extends Expr {
|
||||
private Py::Lambda lambda;
|
||||
|
||||
Reference in New Issue
Block a user