@@ -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 ) }
6668func (w * pyWalker ) popOwner () { w .ownerStack = w .ownerStack [:len (w .ownerStack )- 1 ] }
6769func (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
7476func (w * pyWalker ) enclosingType () string { return strings .Join (w .typeStack , "." ) }
@@ -252,8 +254,10 @@ func (w *pyWalker) setImport(local, target string) {
252254func (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+
674692func pyFuncName (node * sitter.Node , src []byte ) string {
675693 if n := node .ChildByFieldName ("name" ); n != nil {
676694 return pyText (n , src )
0 commit comments