Skip to content

Commit eef1a96

Browse files
committed
fix(ast): handle shadowing of constructor parameters in property references
1 parent f998f65 commit eef1a96

2 files changed

Lines changed: 40 additions & 14 deletions

File tree

internal/extractors/pythonextractor/python_ast.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -975,18 +975,20 @@ func (w *pyWalker) emitCollectionValueRefs(node *sitter.Node) {
975975
// be wrong. Only imported names and same-class methods — which are unambiguously
976976
// real symbol references — resolve.
977977
func (w *pyWalker) valueRefTarget(name string) string {
978+
// A name bound in the enclosing function's own scope — a param or a local
979+
// assigned/iterated/aliased name — shadows a same-class method or same-module def
980+
// (e.g. self.x = x in __init__ passing the param, not the same-named property).
981+
if w.localBound[name] {
982+
return ""
983+
}
978984
if methods := w.currentMethods(); methods[name] {
979985
return w.module + "." + w.enclosingType() + "." + name
980986
}
981987
if t, ok := w.importMap[name]; ok && t != "" {
982988
return t
983989
}
984-
// Same-module top-level def (function or class) referenced by name. Skip when the
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.
987-
// get_user(user_id) passing the param, not the same-named function). Rescues
988-
// `f(local_helper)` where local_helper is a same-module def.
989-
if w.idx != nil && !w.localBound[name] && w.idx.moduleDefs[w.module][name] {
990+
// Same-module top-level def (function or class) referenced by name.
991+
if w.idx != nil && w.idx.moduleDefs[w.module][name] {
990992
return w.module + "." + name
991993
}
992994
return ""
@@ -1484,6 +1486,12 @@ func (w *pyWalker) emitImplementorCalls(owner *facts.Fact, methodName, qualType
14841486

14851487
// resolveCall maps a bare call name to a canonical fact target.
14861488
func (w *pyWalker) resolveCall(name string) string {
1489+
// A bare name that shadows a param/local/loop-var is that local binding, not a
1490+
// same-class method or module-level def (e.g. def wrapper(cb): cb()), so this must
1491+
// gate every branch below, not just the same-module fallback.
1492+
if w.localBound[name] {
1493+
return ""
1494+
}
14871495
// Same-class method.
14881496
if methods := w.currentMethods(); methods[name] {
14891497
return w.module + "." + w.enclosingType() + "." + name
@@ -1492,14 +1500,10 @@ func (w *pyWalker) resolveCall(name string) string {
14921500
if target, ok := w.importMap[name]; ok {
14931501
return target // "" means external → no edge
14941502
}
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
1499-
// extraction) fall back to best-effort; production always supplies one.
1500-
if w.localBound[name] {
1501-
return ""
1502-
}
1503+
// Same-module top-level function. When an index is available, resolve only names
1504+
// that are actually module-level defs, so callable locals/params/loop vars don't
1505+
// fabricate edges. Without an index (single-file extraction) fall back to
1506+
// best-effort; production always supplies one.
15031507
if w.idx != nil {
15041508
if w.idx.moduleDefs[w.module][name] {
15051509
return w.module + "." + name

internal/extractors/pythonextractor/python_ast_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,6 +1470,28 @@ def register(handler):
14701470
}
14711471
}
14721472

1473+
func TestAST_ConstructorParamShadowsSynonymProperty(t *testing.T) {
1474+
src := `
1475+
class DagRun:
1476+
@property
1477+
def state(self):
1478+
return self._state
1479+
1480+
def __init__(self, state=None):
1481+
self.state = state
1482+
`
1483+
result := astExtractIdx(t, "models.py", src)
1484+
idx := byName(result)
1485+
1486+
initFact, ok := idx["models.DagRun.__init__"]
1487+
if !ok {
1488+
t.Fatalf("missing models.DagRun.__init__; keys: %v", keys(idx))
1489+
}
1490+
if calls := relsByKind(initFact, facts.RelCalls); len(calls) != 0 {
1491+
t.Errorf("__init__: expected no RelCalls (state is the param, not the property), got %v", calls)
1492+
}
1493+
}
1494+
14731495
func TestAST_ReturnedPlainVariable_NoPhantomRef(t *testing.T) {
14741496
src := `
14751497
GREETING = "hi"

0 commit comments

Comments
 (0)