@@ -72,10 +72,10 @@ type pyWalker struct {
7272 // canonical qualified type. Reset at the entry of every handleFunction call.
7373 localTypes map [string ]string
7474
75- // paramNames is the set of parameter names of the function whose body is being
76- // walked. Used to suppress same-module value-ref resolution for a param that
77- // shadows a same-named top-level def (e.g. get_user(user_id)). Nil outside a body .
78- paramNames map [string ]bool
75+ // localBound is the set of names bound in the current function's own scope
76+ // (params + assigned/iterated/aliased names). Guards bare-identifier call
77+ // and value-reference resolution against shadowing. Reset per handleFunction call .
78+ localBound map [string ]bool
7979
8080 // Per-function complexity state, set up by handleFunction around walkForCalls.
8181 // metrics is nil outside a function body walk. loopDepth is the current loop
@@ -804,11 +804,11 @@ func (w *pyWalker) handleFunction(node *sitter.Node, decorators []string) {
804804 // workaround) resolves to an edge. Must run before collectParamTypes/
805805 // collectLocalTypes so those see the local imports too.
806806 w .registerBodyImports (bodyNode )
807- w .paramNames = collectParamNames (node .ChildByFieldName ("parameters" ), w .src )
808807 w .localTypes = collectParamTypes (node .ChildByFieldName ("parameters" ), w .src , w .importMap , w .module )
809808 for k , v := range collectLocalTypes (bodyNode , w .src , w .importMap , w .module ) {
810809 w .localTypes [k ] = v
811810 }
811+ w .localBound = collectLocalBoundNames (node .ChildByFieldName ("parameters" ), bodyNode , w .src )
812812 // Set up per-function complexity tracking for this body walk. The props
813813 // map is shared by reference with the fact in w.out, so writing to it
814814 // after the walk updates the emitted fact.
@@ -843,7 +843,7 @@ func (w *pyWalker) handleFunction(node *sitter.Node, decorators []string) {
843843 w .metrics = nil
844844 w .selfName = ""
845845 w .localTypes = nil
846- w .paramNames = nil
846+ w .localBound = nil
847847 }
848848 // Walk parameter-default expressions (e.g. `body = Depends(parse_login_body)`)
849849 // for call and value-reference edges. Metrics are already finalized/nil here, so
@@ -982,40 +982,16 @@ func (w *pyWalker) valueRefTarget(name string) string {
982982 return t
983983 }
984984 // Same-module top-level def (function or class) referenced by name. Skip when the
985- // name is a parameter of the enclosing function (it shadows the def — e.g.
985+ // name is bound in the enclosing function's own scope — a param or a
986+ // local assigned/iterated/aliased name (it shadows the def — e.g.
986987 // get_user(user_id) passing the param, not the same-named function). Rescues
987988 // `f(local_helper)` where local_helper is a same-module def.
988- if w .idx != nil && ! w .paramNames [name ] && w .idx .moduleDefs [w .module ][name ] {
989+ if w .idx != nil && ! w .localBound [name ] && w .idx .moduleDefs [w .module ][name ] {
989990 return w .module + "." + name
990991 }
991992 return ""
992993}
993994
994- // collectParamNames returns the set of parameter names declared by a function's
995- // parameters node (all forms: bare, typed, defaulted, *args, **kwargs).
996- func collectParamNames (params * sitter.Node , src []byte ) map [string ]bool {
997- out := make (map [string ]bool )
998- if params == nil {
999- return out
1000- }
1001- for i := uint (0 ); i < uint (params .ChildCount ()); i ++ {
1002- c := params .Child (i )
1003- switch c .Kind () {
1004- case "identifier" :
1005- out [pyText (c , src )] = true
1006- case "typed_parameter" , "list_splat_pattern" , "dictionary_splat_pattern" :
1007- if id := firstChildOfKind (c , "identifier" ); id != nil {
1008- out [pyText (id , src )] = true
1009- }
1010- case "default_parameter" , "typed_default_parameter" :
1011- if n := c .ChildByFieldName ("name" ); n != nil {
1012- out [pyText (n , src )] = true
1013- }
1014- }
1015- }
1016- return out
1017- }
1018-
1019995// stringRefRelation returns a reference edge for a string literal that names an
1020996// internal symbol by dotted path (e.g. lazy_load_command("airflow.cli.commands.x.y")
1021997// or a provider "class-name": "airflow.providers….short_circuit_task"). Only plain
@@ -1158,6 +1134,18 @@ func (w *pyWalker) walkForCalls(node *sitter.Node) {
11581134 if kind == "dictionary" || kind == "list" || kind == "set" || kind == "tuple" {
11591135 w .emitCollectionValueRefs (node )
11601136 }
1137+ if kind == "assignment" {
1138+ for _ , ident := range collectRefValueIdents (node .ChildByFieldName ("right" )) {
1139+ w .emitValueRef (ident )
1140+ }
1141+ }
1142+ if kind == "return_statement" {
1143+ for i := uint (0 ); i < uint (node .ChildCount ()); i ++ {
1144+ for _ , ident := range collectRefValueIdents (node .Child (i )) {
1145+ w .emitValueRef (ident )
1146+ }
1147+ }
1148+ }
11611149
11621150 // Complexity metrics: count decision points so the single body walk doubles
11631151 // as the cyclomatic/loop pass (mirrors the Go extractor).
@@ -1399,6 +1387,9 @@ func (w *pyWalker) emitCallEdge(fn *sitter.Node) {
13991387 if pyBuiltins [name ] {
14001388 return
14011389 }
1390+ if w .localBound [name ] {
1391+ return // shadowed by a param/local — not the module-level def of this name
1392+ }
14021393 if pyCapitalized (name ) {
14031394 owner .Relations = append (owner .Relations , facts.Relation {
14041395 Kind : facts .RelInstantiates ,
@@ -1501,12 +1492,12 @@ func (w *pyWalker) resolveCall(name string) string {
15011492 if target , ok := w .importMap [name ]; ok {
15021493 return target // "" means external → no edge
15031494 }
1504- // Same-module top-level function. A bare callee that shadows a parameter is the
1505- // parameter , not the module-level def (e.g. def wrapper(cb): cb()). When an index
1506- // is available, resolve only names that are actually module-level defs, so callable
1507- // locals/params/loop vars don't fabricate edges. Without an index (single-file
1495+ // Same-module top-level function. A bare callee that shadows a param/local/loop-var
1496+ // is that local binding , not the module-level def (e.g. def wrapper(cb): cb()). When
1497+ // an index is available, resolve only names that are actually module-level defs, so
1498+ // callable locals/params/loop vars don't fabricate edges. Without an index (single-file
15081499 // extraction) fall back to best-effort; production always supplies one.
1509- if w .paramNames [name ] {
1500+ if w .localBound [name ] {
15101501 return ""
15111502 }
15121503 if w .idx != nil {
@@ -1518,6 +1509,102 @@ func (w *pyWalker) resolveCall(name string) string {
15181509 return w .module + "." + name
15191510}
15201511
1512+ // pyBindTargets recursively binds an assignment/parameter target node into out:
1513+ // identifier -> itself; unpacking forms -> recurse into elements. attribute
1514+ // (self.x) and subscript (d[k]) targets are not name bindings and are skipped.
1515+ func pyBindTargets (node * sitter.Node , src []byte , out map [string ]bool ) {
1516+ if node == nil {
1517+ return
1518+ }
1519+ switch node .Kind () {
1520+ case "identifier" :
1521+ out [pyText (node , src )] = true
1522+ case "pattern_list" , "tuple_pattern" , "list_pattern" , "list_splat_pattern" , "dictionary_splat_pattern" :
1523+ for i := uint (0 ); i < uint (node .ChildCount ()); i ++ {
1524+ pyBindTargets (node .Child (i ), src , out )
1525+ }
1526+ }
1527+ }
1528+
1529+ // pyParamBoundNames adds every name a `parameters` node binds to out, covering
1530+ // all parameter shapes (x, x=1, x: T, x: T = 1, *args, **kwargs).
1531+ func pyParamBoundNames (params * sitter.Node , src []byte , out map [string ]bool ) {
1532+ if params == nil {
1533+ return
1534+ }
1535+ for i := uint (0 ); i < uint (params .ChildCount ()); i ++ {
1536+ c := params .Child (i )
1537+ switch c .Kind () {
1538+ case "identifier" , "list_splat_pattern" , "dictionary_splat_pattern" :
1539+ pyBindTargets (c , src , out )
1540+ case "default_parameter" , "typed_default_parameter" :
1541+ pyBindTargets (c .ChildByFieldName ("name" ), src , out )
1542+ case "typed_parameter" :
1543+ for j := uint (0 ); j < uint (c .ChildCount ()); j ++ {
1544+ pyBindTargets (c .Child (j ), src , out )
1545+ }
1546+ }
1547+ }
1548+ }
1549+
1550+ // collectLocalBoundNames returns every name bound in a function's own scope:
1551+ // its parameters plus every name assigned, iterated, or aliased in its body.
1552+ // Used to guard bare-identifier call resolution — a name bound here refers to
1553+ // a local value, not a same-module def of the same name.
1554+ func collectLocalBoundNames (params , body * sitter.Node , src []byte ) map [string ]bool {
1555+ bound := make (map [string ]bool )
1556+ pyParamBoundNames (params , src , bound )
1557+ walkLocalBoundNames (body , src , bound )
1558+ return bound
1559+ }
1560+
1561+ // walkLocalBoundNames walks a function body collecting bound names, stopping
1562+ // at nested function/class/lambda scopes (their bindings belong to them).
1563+ func walkLocalBoundNames (node * sitter.Node , src []byte , bound map [string ]bool ) {
1564+ if node == nil {
1565+ return
1566+ }
1567+ switch node .Kind () {
1568+ case "function_definition" , "class_definition" , "decorated_definition" , "lambda" :
1569+ return
1570+ case "assignment" , "augmented_assignment" :
1571+ pyBindTargets (node .ChildByFieldName ("left" ), src , bound )
1572+ case "for_statement" :
1573+ pyBindTargets (node .ChildByFieldName ("left" ), src , bound )
1574+ case "named_expression" :
1575+ pyBindTargets (node .ChildByFieldName ("name" ), src , bound )
1576+ case "with_item" :
1577+ if v := node .ChildByFieldName ("value" ); v != nil && v .Kind () == "as_pattern" {
1578+ pyBindTargets (v .ChildByFieldName ("alias" ), src , bound )
1579+ }
1580+ }
1581+ for i := uint (0 ); i < uint (node .ChildCount ()); i ++ {
1582+ walkLocalBoundNames (node .Child (i ), src , bound )
1583+ }
1584+ }
1585+
1586+ // collectRefValueIdents returns the bare identifier(s) a value expression
1587+ // resolves to: itself if it's a plain identifier, or each identifier element
1588+ // of a tuple (expression_list), e.g. `cb = handler` / `a, b = f, g`.
1589+ func collectRefValueIdents (node * sitter.Node ) []* sitter.Node {
1590+ if node == nil {
1591+ return nil
1592+ }
1593+ switch node .Kind () {
1594+ case "identifier" :
1595+ return []* sitter.Node {node }
1596+ case "expression_list" :
1597+ var out []* sitter.Node
1598+ for i := uint (0 ); i < uint (node .ChildCount ()); i ++ {
1599+ if c := node .Child (i ); c .Kind () == "identifier" {
1600+ out = append (out , c )
1601+ }
1602+ }
1603+ return out
1604+ }
1605+ return nil
1606+ }
1607+
15211608// collectPyMethodNames returns the set of function names declared directly in a
15221609// class body node.
15231610func collectPyMethodNames (body * sitter.Node , src []byte ) map [string ]bool {
0 commit comments