package resolver import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zzet/gortex/internal/graph" ) // TestScope_CStaticPreference asserts that a C `static` function in // the caller's file wins over a globally-linked function with the // same name in another file. Without the scope rule, the generic // locality cascade would still prefer the same-directory candidate; // this test puts both candidates in the SAME directory so only the // static-linkage stamp can disambiguate. func TestScope_CStaticPreference(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "pkg/a.c", Kind: graph.KindFile, FilePath: "pkg/a.c", Language: "c"}) g.AddNode(&graph.Node{ID: "pkg/b.c", Kind: graph.KindFile, FilePath: "pkg/b.c", Language: "c"}) g.AddNode(&graph.Node{ ID: "pkg/a.c::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "pkg/a.c", Language: "c", }) g.AddNode(&graph.Node{ ID: "pkg/a.c::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "pkg/a.c", Language: "c", Meta: map[string]any{MetaScopeStatic: true}, }) g.AddNode(&graph.Node{ ID: "pkg/b.c::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "pkg/b.c", Language: "c", }) e := &graph.Edge{ From: "pkg/a.c::caller", To: "unresolved::helper", Kind: graph.EdgeCalls, FilePath: "pkg/a.c", Line: 5, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "pkg/a.c::helper", e.To, "file-local static must win over a same-name candidate in another file") } // TestScope_CppSameNamespacePreference asserts a C++ function in the // caller's namespace beats a same-named function in a different // namespace, even when both live in the same directory. func TestScope_CppSameNamespacePreference(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "src/a.cpp", Kind: graph.KindFile, FilePath: "src/a.cpp", Language: "cpp"}) g.AddNode(&graph.Node{ ID: "src/a.cpp::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "src/a.cpp", Language: "cpp", Meta: map[string]any{MetaScopeNamespace: "app"}, }) g.AddNode(&graph.Node{ ID: "src/a.cpp::helper#app", Kind: graph.KindFunction, Name: "helper", FilePath: "src/a.cpp", Language: "cpp", Meta: map[string]any{MetaScopeNamespace: "app"}, }) g.AddNode(&graph.Node{ ID: "src/a.cpp::helper#util", Kind: graph.KindFunction, Name: "helper", FilePath: "src/a.cpp", Language: "cpp", Meta: map[string]any{MetaScopeNamespace: "util"}, }) e := &graph.Edge{ From: "src/a.cpp::caller", To: "unresolved::helper", Kind: graph.EdgeCalls, FilePath: "src/a.cpp", Line: 3, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "src/a.cpp::helper#app", e.To, "same-namespace candidate must win over a same-directory cross-namespace one") } // TestScope_CppADLViaArgType asserts argument-dependent lookup: an // unqualified call to `foo` whose argument hints at a type in // namespace N must bind to a `foo` defined in namespace N, even when // the caller's namespace has no such function. func TestScope_CppADLViaArgType(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "src/a.cpp", Kind: graph.KindFile, FilePath: "src/a.cpp", Language: "cpp"}) g.AddNode(&graph.Node{ID: "src/b.cpp", Kind: graph.KindFile, FilePath: "src/b.cpp", Language: "cpp"}) g.AddNode(&graph.Node{ ID: "src/a.cpp::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "src/a.cpp", Language: "cpp", Meta: map[string]any{MetaScopeNamespace: "app"}, }) // The only "process" candidate is in namespace `util` — same- // namespace lookup would miss it; ADL via the arg-type hint // (`util::Widget`) is the only path that lands the bind. g.AddNode(&graph.Node{ ID: "src/b.cpp::process#util", Kind: graph.KindFunction, Name: "process", FilePath: "src/b.cpp", Language: "cpp", Meta: map[string]any{MetaScopeNamespace: "util"}, }) e := &graph.Edge{ From: "src/a.cpp::caller", To: "unresolved::process", Kind: graph.EdgeCalls, FilePath: "src/a.cpp", Line: 4, Meta: map[string]any{MetaScopeArgTypes: "util::Widget"}, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "src/b.cpp::process#util", e.To, "ADL must walk arg-type namespaces when same-namespace lookup misses") } // TestScope_JavaEnclosingClassPreference asserts an unqualified call // inside class X binds to X's method first, even when a same-named // method exists on another class in the same package. func TestScope_JavaEnclosingClassPreference(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "app/User.java", Kind: graph.KindFile, FilePath: "app/User.java", Language: "java"}) g.AddNode(&graph.Node{ ID: "app/User.java::User", Kind: graph.KindType, Name: "User", FilePath: "app/User.java", Language: "java", }) g.AddNode(&graph.Node{ ID: "app/User.java::User.save", Kind: graph.KindMethod, Name: "save", FilePath: "app/User.java", Language: "java", Meta: map[string]any{"receiver": "User", MetaScopeClass: "User"}, }) g.AddNode(&graph.Node{ ID: "app/User.java::User.validate", Kind: graph.KindMethod, Name: "validate", FilePath: "app/User.java", Language: "java", Meta: map[string]any{"receiver": "User", MetaScopeClass: "User"}, }) g.AddNode(&graph.Node{ ID: "app/Other.java::Other.validate", Kind: graph.KindMethod, Name: "validate", FilePath: "app/Other.java", Language: "java", Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"}, }) // User.save() calls validate() unqualified — must bind to User.validate. e := &graph.Edge{ From: "app/User.java::User.save", To: "unresolved::*.validate", Kind: graph.EdgeCalls, FilePath: "app/User.java", Line: 12, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "app/User.java::User.validate", e.To, "unqualified call in class X must bind to X's method, not a same-named one elsewhere") } // TestScope_JavaSuperChainWalk asserts the scope resolver walks the // extends chain when the unqualified call doesn't bind to the // enclosing class. func TestScope_JavaSuperChainWalk(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "app/Base.java", Kind: graph.KindFile, FilePath: "app/Base.java", Language: "java"}) g.AddNode(&graph.Node{ID: "app/Child.java", Kind: graph.KindFile, FilePath: "app/Child.java", Language: "java"}) g.AddNode(&graph.Node{ ID: "app/Base.java::Base", Kind: graph.KindType, Name: "Base", FilePath: "app/Base.java", Language: "java", }) g.AddNode(&graph.Node{ ID: "app/Child.java::Child", Kind: graph.KindType, Name: "Child", FilePath: "app/Child.java", Language: "java", Meta: map[string]any{MetaScopeParentClass: "Base"}, }) g.AddNode(&graph.Node{ ID: "app/Base.java::Base.helper", Kind: graph.KindMethod, Name: "helper", FilePath: "app/Base.java", Language: "java", Meta: map[string]any{"receiver": "Base", MetaScopeClass: "Base"}, }) g.AddNode(&graph.Node{ ID: "app/Child.java::Child.run", Kind: graph.KindMethod, Name: "run", FilePath: "app/Child.java", Language: "java", Meta: map[string]any{"receiver": "Child", MetaScopeClass: "Child"}, }) // Decoy: another class has a same-name `helper`. g.AddNode(&graph.Node{ ID: "app/Other.java::Other.helper", Kind: graph.KindMethod, Name: "helper", FilePath: "app/Other.java", Language: "java", Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"}, }) // Child.run() calls helper() — should walk to Base.helper. e := &graph.Edge{ From: "app/Child.java::Child.run", To: "unresolved::*.helper", Kind: graph.EdgeCalls, FilePath: "app/Child.java", Line: 4, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "app/Base.java::Base.helper", e.To, "super-chain walk must land helper() on the parent class") } // TestScope_PhpParentCall asserts that `parent::foo()` in PHP binds // to the parent class's foo, even with a same-name decoy elsewhere. func TestScope_PhpParentCall(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "src/Base.php", Kind: graph.KindFile, FilePath: "src/Base.php", Language: "php"}) g.AddNode(&graph.Node{ID: "src/Child.php", Kind: graph.KindFile, FilePath: "src/Child.php", Language: "php"}) g.AddNode(&graph.Node{ ID: "src/Base.php::Base", Kind: graph.KindType, Name: "Base", FilePath: "src/Base.php", Language: "php", }) g.AddNode(&graph.Node{ ID: "src/Child.php::Child", Kind: graph.KindType, Name: "Child", FilePath: "src/Child.php", Language: "php", Meta: map[string]any{MetaScopeParentClass: "Base"}, }) g.AddNode(&graph.Node{ ID: "src/Base.php::Base.handle", Kind: graph.KindMethod, Name: "handle", FilePath: "src/Base.php", Language: "php", Meta: map[string]any{"receiver": "Base", MetaScopeClass: "Base"}, }) g.AddNode(&graph.Node{ ID: "src/Child.php::Child.handle", Kind: graph.KindMethod, Name: "handle", FilePath: "src/Child.php", Language: "php", Meta: map[string]any{"receiver": "Child", MetaScopeClass: "Child"}, }) g.AddNode(&graph.Node{ ID: "src/Other.php::Other.handle", Kind: graph.KindMethod, Name: "handle", FilePath: "src/Other.php", Language: "php", Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"}, }) // Child.handle() calls parent::handle() — must bind to Base.handle. e := &graph.Edge{ From: "src/Child.php::Child.handle", To: "unresolved::*.handle", Kind: graph.EdgeCalls, FilePath: "src/Child.php", Line: 7, Meta: map[string]any{MetaScopeKind: ScopeKindParent}, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "src/Base.php::Base.handle", e.To, "parent:: must walk extends chain to Base.handle") } // TestScope_PhpSelfCall asserts `self::foo()` pins to the enclosing // class's method, regardless of where the same-named decoy lives. func TestScope_PhpSelfCall(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "src/Service.php", Kind: graph.KindFile, FilePath: "src/Service.php", Language: "php"}) g.AddNode(&graph.Node{ ID: "src/Service.php::Service.boot", Kind: graph.KindMethod, Name: "boot", FilePath: "src/Service.php", Language: "php", Meta: map[string]any{"receiver": "Service", MetaScopeClass: "Service"}, }) g.AddNode(&graph.Node{ ID: "src/Service.php::Service.init", Kind: graph.KindMethod, Name: "init", FilePath: "src/Service.php", Language: "php", Meta: map[string]any{"receiver": "Service", MetaScopeClass: "Service"}, }) g.AddNode(&graph.Node{ ID: "src/Other.php::Other.init", Kind: graph.KindMethod, Name: "init", FilePath: "src/Other.php", Language: "php", Meta: map[string]any{"receiver": "Other", MetaScopeClass: "Other"}, }) e := &graph.Edge{ From: "src/Service.php::Service.boot", To: "unresolved::*.init", Kind: graph.EdgeCalls, FilePath: "src/Service.php", Line: 3, Meta: map[string]any{MetaScopeKind: ScopeKindSelf}, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "src/Service.php::Service.init", e.To, "self:: must pin to the enclosing class's method") } // TestScope_StampedAsScopeResolution asserts the scope path stamps // `resolution=scope` on the edge so the gain telemetry + agent // observability can tell scope wins from generic-locality wins. func TestScope_StampedAsScopeResolution(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "pkg/a.c", Kind: graph.KindFile, FilePath: "pkg/a.c", Language: "c"}) g.AddNode(&graph.Node{ ID: "pkg/a.c::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "pkg/a.c", Language: "c", }) g.AddNode(&graph.Node{ ID: "pkg/a.c::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "pkg/a.c", Language: "c", Meta: map[string]any{MetaScopeStatic: true}, }) g.AddNode(&graph.Node{ ID: "pkg/b.c::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "pkg/b.c", Language: "c", }) e := &graph.Edge{ From: "pkg/a.c::caller", To: "unresolved::helper", Kind: graph.EdgeCalls, FilePath: "pkg/a.c", Line: 5, } g.AddEdge(e) New(g).ResolveAll() require.NotNil(t, e.Meta) assert.Equal(t, "scope", e.Meta["resolution"], "scope-resolved edge must carry resolution=scope so telemetry can distinguish it") assert.Equal(t, graph.OriginASTResolved, e.Origin, "scope-resolved edge must stamp OriginASTResolved") } // TestScope_FallthroughWhenNoEvidence asserts the scope resolver is a // no-op when none of the language-specific evidence is present — // behaviour stays identical to pre-N8 for unaffected call sites. func TestScope_FallthroughWhenNoEvidence(t *testing.T) { g := graph.New() g.AddNode(&graph.Node{ID: "pkg/a.c", Kind: graph.KindFile, FilePath: "pkg/a.c", Language: "c"}) g.AddNode(&graph.Node{ ID: "pkg/a.c::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "pkg/a.c", Language: "c", }) g.AddNode(&graph.Node{ ID: "pkg/a.c::helper", Kind: graph.KindFunction, Name: "helper", FilePath: "pkg/a.c", Language: "c", // No scope_static, no scope_ns, no scope_class. }) e := &graph.Edge{ From: "pkg/a.c::caller", To: "unresolved::helper", Kind: graph.EdgeCalls, FilePath: "pkg/a.c", Line: 5, } g.AddEdge(e) stats := New(g).ResolveAll() require.Equal(t, 1, stats.Resolved) assert.Equal(t, "pkg/a.c::helper", e.To, "no-evidence edge must still resolve via the generic cascade") // Without scope evidence, resolution should NOT be tagged // "scope" — the generic locality path took over. if e.Meta != nil { assert.NotEqual(t, "scope", e.Meta["resolution"], "generic-locality resolution must not falsely claim scope evidence") } }