Skip to content

Commit 9aafef3

Browse files
GertLclaude
andcommitted
Fix stale pointer bug in pyWalker and skip @overload stubs
Replace ownerStack/pendingRouteIndices pointer captures with slice indices to prevent silently lost call edges and handler back-fills when w.out reallocates its backing array during body traversal. Skip @overload-decorated function definitions to avoid emitting duplicate symbol facts for type-checker-only stubs that have no runtime behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 96455f4 commit 9aafef3

1 file changed

Lines changed: 35 additions & 17 deletions

File tree

internal/extractors/pythonextractor/python_ast.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ type pyWalker struct {
5050
// typeStack holds enclosing class names so methods get qualified names.
5151
typeStack []string
5252

53-
// ownerStack: top element is the fact that receives RelCalls / RelInstantiates
54-
// discovered while walking its body.
55-
ownerStack []*facts.Fact
53+
// ownerStack: top element is the index into w.out of the fact that receives
54+
// RelCalls / RelInstantiates discovered while walking its body. Indices are
55+
// used instead of pointers because appending to w.out can reallocate the
56+
// backing array, invalidating any previously captured pointer.
57+
ownerStack []int
5658

5759
// importMap maps a local name to its canonical fact target (empty = external).
5860
importMap map[string]string
@@ -62,13 +64,13 @@ type pyWalker struct {
6264
methodSets []map[string]bool
6365
}
6466

65-
func (w *pyWalker) pushOwner(f *facts.Fact) { w.ownerStack = append(w.ownerStack, f) }
67+
func (w *pyWalker) pushOwner(idx int) { w.ownerStack = append(w.ownerStack, idx) }
6668
func (w *pyWalker) popOwner() { w.ownerStack = w.ownerStack[:len(w.ownerStack)-1] }
6769
func (w *pyWalker) currentOwner() *facts.Fact {
6870
if len(w.ownerStack) == 0 {
6971
return nil
7072
}
71-
return w.ownerStack[len(w.ownerStack)-1]
73+
return &w.out[w.ownerStack[len(w.ownerStack)-1]]
7274
}
7375

7476
func (w *pyWalker) enclosingType() string { return strings.Join(w.typeStack, ".") }
@@ -252,8 +254,10 @@ func (w *pyWalker) setImport(local, target string) {
252254
func (w *pyWalker) handleDecoratedDefinition(node *sitter.Node) {
253255
var decorators []string
254256
var pendingApiViewMethods []string
255-
// pendingRoutes holds route facts emitted from decorators before we see the handler name.
256-
var pendingRouteFacts []*facts.Fact
257+
// pendingRouteIndices holds w.out indices of route facts emitted from
258+
// decorators before we see the handler name. Indices are used (not pointers)
259+
// because subsequent appends to w.out may reallocate its backing array.
260+
var pendingRouteIndices []int
257261

258262
for i := uint(0); i < uint(node.ChildCount()); i++ {
259263
c := node.Child(i)
@@ -264,7 +268,7 @@ func (w *pyWalker) handleDecoratedDefinition(node *sitter.Node) {
264268
if m := routeDecoratorRe.FindStringSubmatch(text); m != nil {
265269
method := strings.ToUpper(m[2])
266270
path := m[3]
267-
rf := facts.Fact{
271+
w.out = append(w.out, facts.Fact{
268272
Kind: facts.KindRoute,
269273
Name: method + " " + path,
270274
File: w.relFile,
@@ -274,9 +278,8 @@ func (w *pyWalker) handleDecoratedDefinition(node *sitter.Node) {
274278
"path": path,
275279
"framework": "fastapi",
276280
},
277-
}
278-
w.out = append(w.out, rf)
279-
pendingRouteFacts = append(pendingRouteFacts, &w.out[len(w.out)-1])
281+
})
282+
pendingRouteIndices = append(pendingRouteIndices, len(w.out)-1)
280283
continue
281284
}
282285
// DRF @api_view(['GET','POST']).
@@ -292,11 +295,16 @@ func (w *pyWalker) handleDecoratedDefinition(node *sitter.Node) {
292295
}
293296

294297
case "function_definition":
298+
// @overload stubs are type-checker-only annotations with no runtime
299+
// body — skip them to avoid duplicate symbol facts.
300+
if hasDecorator(decorators, "overload") {
301+
continue
302+
}
295303
w.handleFunction(c, decorators)
296304
handlerName := w.module + "." + w.qualify(pyFuncName(c, w.src))
297305
// Back-fill handler into pending FastAPI route facts.
298-
for _, rf := range pendingRouteFacts {
299-
rf.Props["handler"] = handlerName
306+
for _, idx := range pendingRouteIndices {
307+
w.out[idx].Props["handler"] = handlerName
300308
}
301309
// @api_view routes — emit after we know the handler name.
302310
if len(pendingApiViewMethods) > 0 {
@@ -409,8 +417,7 @@ func (w *pyWalker) handleClass(node *sitter.Node, decorators []string) {
409417
}
410418

411419
w.out = append(w.out, f)
412-
owner := &w.out[len(w.out)-1]
413-
w.pushOwner(owner)
420+
w.pushOwner(len(w.out) - 1)
414421

415422
bodyNode := node.ChildByFieldName("body")
416423
w.pushType(name, collectPyMethodNames(bodyNode, w.src))
@@ -477,8 +484,7 @@ func (w *pyWalker) handleFunction(node *sitter.Node, decorators []string) {
477484
}
478485

479486
w.out = append(w.out, f)
480-
owner := &w.out[len(w.out)-1]
481-
w.pushOwner(owner)
487+
w.pushOwner(len(w.out) - 1)
482488
if bodyNode := node.ChildByFieldName("body"); bodyNode != nil {
483489
w.walkForCalls(bodyNode)
484490
}
@@ -671,6 +677,18 @@ func collectPyMethodNames(body *sitter.Node, src []byte) map[string]bool {
671677
return methods
672678
}
673679

680+
// hasDecorator reports whether any name in decorators has last as its
681+
// last dot-separated component (e.g. "overload" matches both "overload"
682+
// and "typing.overload").
683+
func hasDecorator(decorators []string, last string) bool {
684+
for _, d := range decorators {
685+
if lastComponent(d) == last {
686+
return true
687+
}
688+
}
689+
return false
690+
}
691+
674692
func pyFuncName(node *sitter.Node, src []byte) string {
675693
if n := node.ChildByFieldName("name"); n != nil {
676694
return pyText(n, src)

0 commit comments

Comments
 (0)