Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

741 lines
30 KiB
Go

package resolver
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
func TestResolveAll_InternalCall(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/b.go", Kind: graph.KindFile, Name: "b.go", FilePath: "pkg/b.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/a.go::Foo", Kind: graph.KindFunction, Name: "Foo", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/b.go::Bar", Kind: graph.KindFunction, Name: "Bar", FilePath: "pkg/b.go", Language: "go"})
// Foo calls Bar (unresolved).
callEdge := &graph.Edge{From: "pkg/a.go::Foo", To: "unresolved::Bar", Kind: graph.EdgeCalls, FilePath: "pkg/a.go", Line: 5}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/b.go::Bar", callEdge.To)
// A unique same-package bare-name call is structurally unambiguous, so it
// is stamped ast_resolved — not left to backfill to the name-only tier
// that find_usages suppresses by default.
assert.Equal(t, graph.OriginASTResolved, callEdge.Origin,
"a unique same-package bare-name call must be ast_resolved, not text_matched")
}
// TestResolveFunctionCall_UniqueBareCallTier is the F2.1 regression: a
// bare-name call to the sole same-file or same-package definition is
// grammar-grounded and structurally unambiguous, so it must be stamped
// ast_resolved (above the name-only tier find_usages suppresses by default).
// Ambiguous picks and non-call edges stay untagged so suppression can still
// drop them.
func TestResolveFunctionCall_UniqueBareCallTier(t *testing.T) {
t.Run("same-file unique call is ast_resolved", func(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/a.go::Foo", Kind: graph.KindFunction, Name: "Foo", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/a.go::Baz", Kind: graph.KindFunction, Name: "Baz", FilePath: "pkg/a.go", Language: "go"})
e := &graph.Edge{From: "pkg/a.go::Baz", To: "unresolved::Foo", Kind: graph.EdgeCalls, FilePath: "pkg/a.go", Line: 3}
g.AddEdge(e)
New(g).ResolveAll()
assert.Equal(t, "pkg/a.go::Foo", e.To, "same-file call must resolve to Foo")
assert.Equal(t, graph.OriginASTResolved, e.Origin, "unique same-file call must be ast_resolved")
})
t.Run("ambiguous same-package pick stays untagged", func(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/b.go", Kind: graph.KindFile, Name: "b.go", FilePath: "pkg/b.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/c.go", Kind: graph.KindFile, Name: "c.go", FilePath: "pkg/c.go", Language: "go"})
// Two same-package definitions carry the name Foo (a function and a
// method) — enough to make the bare-name pick ambiguous.
g.AddNode(&graph.Node{ID: "pkg/a.go::Foo", Kind: graph.KindFunction, Name: "Foo", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/b.go::T.Foo", Kind: graph.KindMethod, Name: "Foo", FilePath: "pkg/b.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/c.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/c.go", Language: "go"})
e := &graph.Edge{From: "pkg/c.go::Caller", To: "unresolved::Foo", Kind: graph.EdgeCalls, FilePath: "pkg/c.go", Line: 3}
g.AddEdge(e)
New(g).ResolveAll()
assert.Equal(t, "", e.Origin,
"an ambiguous same-package pick must stay untagged so suppression can drop it")
})
}
func TestResolveAll_ExternalImport(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "main.go", Language: "go"})
importEdge := &graph.Edge{From: "main.go", To: "unresolved::import::fmt", Kind: graph.EdgeImports, FilePath: "main.go", Line: 3}
g.AddEdge(importEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.External)
assert.Equal(t, "external::fmt", importEdge.To)
}
func TestResolveAll_MethodCall(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/a.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/b.go::Server.Start", Kind: graph.KindMethod, Name: "Start", FilePath: "pkg/b.go", Language: "go"})
callEdge := &graph.Edge{From: "pkg/a.go::Caller", To: "unresolved::*.Start", Kind: graph.EdgeCalls, FilePath: "pkg/a.go", Line: 10}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/b.go::Server.Start", callEdge.To)
}
func TestResolveAll_Unresolvable(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "a.go::Foo", Kind: graph.KindFunction, Name: "Foo", FilePath: "a.go", Language: "go"})
callEdge := &graph.Edge{From: "a.go::Foo", To: "unresolved::NonExistent", Kind: graph.EdgeCalls, FilePath: "a.go", Line: 5}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Unresolved)
}
// Stdlib classification: Go stdlib paths have no dot in the first
// segment ("fmt", "encoding/json"), and extern:: edges for them get a
// `stdlib::` prefix after resolution.
func TestResolveAll_ExternStdlib(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "a.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "a.go", Language: "go"})
callEdge := &graph.Edge{
From: "a.go::Caller", To: "unresolved::extern::encoding/json::NewEncoder",
Kind: graph.EdgeCalls, FilePath: "a.go", Line: 5,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.External)
assert.Equal(t, "stdlib::encoding/json::NewEncoder", callEdge.To)
}
// Third-party module paths always carry a dot in the first segment
// (github.com/..., golang.org/...) and resolve to a `dep::` stub.
func TestResolveAll_ExternDep(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "a.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "a.go", Language: "go"})
callEdge := &graph.Edge{
From: "a.go::Caller", To: "unresolved::extern::github.com/pkg/errors::Wrap",
Kind: graph.EdgeCalls, FilePath: "a.go", Line: 5,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.External)
assert.Equal(t, "dep::github.com/pkg/errors::Wrap", callEdge.To)
}
// A TypeScript selector call into a String built-in (`.startsWith`) has
// no in-graph target, but we classify it rather than mark it unresolved
// — flow traces render `builtin (js · string)` instead of plain
// `unresolved`.
func TestResolveAll_BuiltinFromTS(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/foo.ts", Kind: graph.KindFile, Name: "foo.ts", FilePath: "src/foo.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/foo.ts::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "src/foo.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/foo.ts::Caller", To: "unresolved::*.startsWith",
Kind: graph.EdgeCalls, FilePath: "src/foo.ts", Line: 5,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.External)
assert.Equal(t, "builtin::ts::string::startsWith", callEdge.To)
}
// Python `list.append` — verifies the py classifier branch.
func TestResolveAll_BuiltinFromPython(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "a.py", Kind: graph.KindFile, Name: "a.py", FilePath: "a.py", Language: "python"})
g.AddNode(&graph.Node{ID: "a.py::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "a.py", Language: "python"})
callEdge := &graph.Edge{
From: "a.py::caller", To: "unresolved::*.append",
Kind: graph.EdgeCalls, FilePath: "a.py", Line: 2,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.External)
assert.Equal(t, "builtin::py::list::append", callEdge.To)
}
// Unknown method on a known language still falls through to Unresolved
// — the classifier is not a catch-all.
func TestResolveAll_UnknownBuiltinStaysUnresolved(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/foo.ts", Kind: graph.KindFile, Name: "foo.ts", FilePath: "src/foo.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/foo.ts::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "src/foo.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/foo.ts::caller", To: "unresolved::*.mySuperObscureThing",
Kind: graph.EdgeCalls, FilePath: "src/foo.ts", Line: 2,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Unresolved)
}
// When the referenced symbol is actually indexed (e.g. another repo in
// a multi-repo setup), the extern edge rewires to the real node.
func TestResolveAll_ExternResolvesToIndexedSymbol(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "consumer/main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "consumer/main.go", Language: "go"})
g.AddNode(&graph.Node{ID: "consumer/main.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "consumer/main.go", Language: "go"})
g.AddNode(&graph.Node{ID: "mylib/pkg/pkg.go", Kind: graph.KindFile, Name: "pkg.go", FilePath: "mylib/pkg/pkg.go", Language: "go"})
g.AddNode(&graph.Node{ID: "mylib/pkg/pkg.go::DoThing", Kind: graph.KindFunction, Name: "DoThing", FilePath: "mylib/pkg/pkg.go", Language: "go"})
callEdge := &graph.Edge{
From: "consumer/main.go::Caller", To: "unresolved::extern::mylib/pkg::DoThing",
Kind: graph.EdgeCalls, FilePath: "consumer/main.go", Line: 5,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "mylib/pkg/pkg.go::DoThing", callEdge.To)
}
func TestInferImplements(t *testing.T) {
g := graph.New()
// Interface with two methods.
g.AddNode(&graph.Node{
ID: "pkg/store.go::Repository", Kind: graph.KindInterface, Name: "Repository",
FilePath: "pkg/store.go", Language: "go", StartLine: 1,
Meta: map[string]any{"methods": []string{"FindByID", "Save"}},
})
// Type that satisfies the interface.
g.AddNode(&graph.Node{
ID: "pkg/db.go::SQLRepo", Kind: graph.KindType, Name: "SQLRepo",
FilePath: "pkg/db.go", Language: "go", StartLine: 1,
})
g.AddNode(&graph.Node{
ID: "pkg/db.go::SQLRepo.FindByID", Kind: graph.KindMethod, Name: "FindByID",
FilePath: "pkg/db.go", Language: "go", StartLine: 5,
})
g.AddNode(&graph.Node{
ID: "pkg/db.go::SQLRepo.Save", Kind: graph.KindMethod, Name: "Save",
FilePath: "pkg/db.go", Language: "go", StartLine: 10,
})
g.AddEdge(&graph.Edge{From: "pkg/db.go::SQLRepo.FindByID", To: "pkg/db.go::SQLRepo", Kind: graph.EdgeMemberOf, FilePath: "pkg/db.go", Line: 5})
g.AddEdge(&graph.Edge{From: "pkg/db.go::SQLRepo.Save", To: "pkg/db.go::SQLRepo", Kind: graph.EdgeMemberOf, FilePath: "pkg/db.go", Line: 10})
// Type that does NOT satisfy (missing Save).
g.AddNode(&graph.Node{
ID: "pkg/db.go::ReadOnly", Kind: graph.KindType, Name: "ReadOnly",
FilePath: "pkg/db.go", Language: "go", StartLine: 20,
})
g.AddNode(&graph.Node{
ID: "pkg/db.go::ReadOnly.FindByID", Kind: graph.KindMethod, Name: "FindByID",
FilePath: "pkg/db.go", Language: "go", StartLine: 22,
})
g.AddEdge(&graph.Edge{From: "pkg/db.go::ReadOnly.FindByID", To: "pkg/db.go::ReadOnly", Kind: graph.EdgeMemberOf, FilePath: "pkg/db.go", Line: 22})
r := New(g)
added := r.InferImplements()
assert.Equal(t, 1, added)
// Verify the implements edge was added.
edges := g.GetInEdges("pkg/store.go::Repository")
var implEdges []*graph.Edge
for _, e := range edges {
if e.Kind == graph.EdgeImplements {
implEdges = append(implEdges, e)
}
}
assert.Len(t, implEdges, 1)
assert.Equal(t, "pkg/db.go::SQLRepo", implEdges[0].From)
}
func TestInferImplements_EmptyInterface(t *testing.T) {
g := graph.New()
// Empty interface should not match anything.
g.AddNode(&graph.Node{
ID: "pkg/any.go::Any", Kind: graph.KindInterface, Name: "Any",
FilePath: "pkg/any.go", Language: "go", StartLine: 1,
Meta: map[string]any{"methods": []string{}},
})
g.AddNode(&graph.Node{
ID: "pkg/db.go::Foo", Kind: graph.KindType, Name: "Foo",
FilePath: "pkg/db.go", Language: "go", StartLine: 1,
})
r := New(g)
added := r.InferImplements()
assert.Equal(t, 0, added)
}
func TestResolveAll_PackageQualifiedFunctionCall(t *testing.T) {
g := graph.New()
// Caller in languages package calls parser.ParseFile — dispatched as "*.ParseFile"
g.AddNode(&graph.Node{ID: "internal/parser/languages/golang.go::GoExtractor.Extract", Kind: graph.KindMethod, Name: "Extract", FilePath: "internal/parser/languages/golang.go", Language: "go"})
g.AddNode(&graph.Node{ID: "internal/parser/treesitter.go::ParseFile", Kind: graph.KindFunction, Name: "ParseFile", FilePath: "internal/parser/treesitter.go", Language: "go"})
// This is how the Go extractor encodes pkg.Func() calls: "unresolved::*.ParseFile"
callEdge := &graph.Edge{From: "internal/parser/languages/golang.go::GoExtractor.Extract", To: "unresolved::*.ParseFile", Kind: graph.EdgeCalls, FilePath: "internal/parser/languages/golang.go", Line: 94}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "internal/parser/treesitter.go::ParseFile", callEdge.To)
}
func TestResolveMethodCall_TypeAware(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/a.go", Language: "go"})
// Two methods named "Start" on different types.
g.AddNode(&graph.Node{
ID: "pkg/server.go::Server.Start", Kind: graph.KindMethod, Name: "Start",
FilePath: "pkg/server.go", Language: "go",
Meta: map[string]any{"receiver": "Server"},
})
g.AddNode(&graph.Node{
ID: "pkg/client.go::Client.Start", Kind: graph.KindMethod, Name: "Start",
FilePath: "pkg/client.go", Language: "go",
Meta: map[string]any{"receiver": "Client"},
})
// Call edge with type hint → should resolve to Server.Start.
callEdge := &graph.Edge{
From: "pkg/a.go::Caller", To: "unresolved::*.Start",
Kind: graph.EdgeCalls, FilePath: "pkg/a.go", Line: 10,
Meta: map[string]any{"receiver_type": "Server"},
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/server.go::Server.Start", callEdge.To)
assert.Equal(t, 0.95, callEdge.Confidence) // same directory + exact type
}
func TestResolveMethodCall_TypeAware_Fallback(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "pkg/b.go::Server.Start", Kind: graph.KindMethod, Name: "Start",
FilePath: "pkg/b.go", Language: "go",
Meta: map[string]any{"receiver": "Server"},
})
// Call edge with unknown type → falls back to name-only.
callEdge := &graph.Edge{
From: "pkg/a.go::Caller", To: "unresolved::*.Start",
Kind: graph.EdgeCalls, FilePath: "pkg/a.go", Line: 10,
Meta: map[string]any{"receiver_type": "UnknownType"},
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/b.go::Server.Start", callEdge.To) // falls back to any method match
}
func TestResolveMethodCall_NoMeta(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/a.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "pkg/b.go::Server.Start", Kind: graph.KindMethod, Name: "Start",
FilePath: "pkg/b.go", Language: "go",
Meta: map[string]any{"receiver": "Server"},
})
// Call edge with nil Meta → existing behavior.
callEdge := &graph.Edge{
From: "pkg/a.go::Caller", To: "unresolved::*.Start",
Kind: graph.EdgeCalls, FilePath: "pkg/a.go", Line: 10,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/b.go::Server.Start", callEdge.To)
}
// Regression: a method passed as a *value* (e.g. `mux.HandleFunc("/p", h.foo)`)
// is captured by the Go extractor as `unresolved::*.foo` with kind=EdgeReads.
// Before the fix the resolver would re-target the edge to the method but
// leave the kind as EdgeReads, hiding it from both get_callers and
// find_usages. The fix promotes the kind to EdgeReferences when the
// field-then-method fallback lands on a method — so HTTP handlers,
// command tables, callback maps all become visible.
func TestResolveAll_MethodValuePromotesReadToReferences(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/routes.go::RegisterRoutes", Kind: graph.KindFunction, Name: "RegisterRoutes",
FilePath: "pkg/routes.go", Language: "go",
})
g.AddNode(&graph.Node{
ID: "pkg/handler.go::Handler.HandleHealth", Kind: graph.KindMethod, Name: "HandleHealth",
FilePath: "pkg/handler.go", Language: "go",
Meta: map[string]any{"receiver": "Handler"},
})
// Mirror what the Go extractor emits for `h.HandleHealth` in arg position.
edge := &graph.Edge{
From: "pkg/routes.go::RegisterRoutes", To: "unresolved::*.HandleHealth",
Kind: graph.EdgeReads, FilePath: "pkg/routes.go", Line: 42,
Meta: map[string]any{"receiver_type": "Handler"},
}
g.AddEdge(edge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved, "method-value reference should resolve")
assert.Equal(t, "pkg/handler.go::Handler.HandleHealth", edge.To,
"edge should point at the real method")
assert.Equal(t, graph.EdgeReferences, edge.Kind,
"kind should be promoted Reads→References so get_callers/find_usages surface it")
}
// Regression: a return-type or param-type reference must resolve to
// a TYPE, never to a same-named function or method. Before the fix
// the resolver's default case routed `EdgeReturns` / `EdgeTypedAs`
// through resolveFunctionCall, so `func GetLanguage() *tsitter.Language`
// landed on a method named `Language` somewhere unrelated in the
// repo — leaving the `Language` type alias visibly unused and
// triggering false-positive dead-code skulls on every cross-package
// type re-export pattern.
func TestResolveAll_ReturnsAndTypedAsResolveToType(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/tsitter/tsitter.go::Language", Kind: graph.KindType, Name: "Language",
FilePath: "pkg/tsitter/tsitter.go", StartLine: 30, Language: "go",
})
// Decoy method with the same name — pre-fix the resolver picked
// this because it tried resolveFunctionCall first.
g.AddNode(&graph.Node{
ID: "pkg/apex/apex.go::Apex.Language", Kind: graph.KindMethod, Name: "Language",
FilePath: "pkg/apex/apex.go", StartLine: 11, Language: "go",
Meta: map[string]any{"receiver": "Apex"},
})
g.AddNode(&graph.Node{
ID: "pkg/ocaml/ocaml.go::GetLanguage", Kind: graph.KindFunction, Name: "GetLanguage",
FilePath: "pkg/ocaml/ocaml.go", StartLine: 10, Language: "go",
})
g.AddNode(&graph.Node{
ID: "pkg/ocaml/ocaml.go::Run#param:lang", Kind: graph.KindParam, Name: "lang",
FilePath: "pkg/ocaml/ocaml.go", StartLine: 20, Language: "go",
})
// `func GetLanguage() *tsitter.Language` → EdgeReturns
retEdge := &graph.Edge{
From: "pkg/ocaml/ocaml.go::GetLanguage", To: "unresolved::Language",
Kind: graph.EdgeReturns, FilePath: "pkg/ocaml/ocaml.go", Line: 10,
}
g.AddEdge(retEdge)
// `func Run(lang *tsitter.Language)` → EdgeTypedAs on the param
typedAsEdge := &graph.Edge{
From: "pkg/ocaml/ocaml.go::Run#param:lang", To: "unresolved::Language",
Kind: graph.EdgeTypedAs, FilePath: "pkg/ocaml/ocaml.go", Line: 20,
}
g.AddEdge(typedAsEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 2, stats.Resolved)
assert.Equal(t, "pkg/tsitter/tsitter.go::Language", retEdge.To,
"EdgeReturns must resolve to the type, not the same-named method")
assert.Equal(t, "pkg/tsitter/tsitter.go::Language", typedAsEdge.To,
"EdgeTypedAs must resolve to the type, not the same-named method")
}
// Regression: a *bare* function name passed as a value (e.g.
// `&cobra.Command{RunE: runClean}`) is extracted as EdgeReads with
// To=`unresolved::runClean`. The heuristic default case calls
// resolveFunctionCall which lands on the function — but until the
// promotion was added the kind stayed as EdgeReads, hiding the
// wire-up site from get_callers / find_usages. Every cobra subcommand
// (and every other "function pointer as struct field" pattern)
// looked like dead code.
func TestResolveAll_BareFunctionValuePromotesReadToReferences(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "cmd/x/main.go::registerClean", Kind: graph.KindFunction, Name: "registerClean",
FilePath: "cmd/x/main.go", Language: "go",
})
g.AddNode(&graph.Node{
ID: "cmd/x/clean.go::runClean", Kind: graph.KindFunction, Name: "runClean",
FilePath: "cmd/x/clean.go", Language: "go",
})
edge := &graph.Edge{
From: "cmd/x/main.go::registerClean", To: "unresolved::runClean",
Kind: graph.EdgeReads, FilePath: "cmd/x/main.go", Line: 7,
}
g.AddEdge(edge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved, "bare function-value reference should resolve")
assert.Equal(t, "cmd/x/clean.go::runClean", edge.To, "edge should point at the function")
assert.Equal(t, graph.EdgeReferences, edge.Kind,
"kind should be promoted Reads→References so get_callers/find_usages surface the wire-up site")
}
// Regression guard: a *real* field read should stay as EdgeReads even after
// the method-value promotion path was added. We add a struct field named
// `count` and a same-named method on a different type, and make sure the
// field-typed receiver picks the field (not the method) and keeps EdgeReads.
func TestResolveAll_FieldReadStaysAsReads(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/a.go::Caller", Kind: graph.KindFunction, Name: "Caller",
FilePath: "pkg/a.go", Language: "go",
})
// The actual field we want resolution to pick.
g.AddNode(&graph.Node{
ID: "pkg/types.go::Counter.count", Kind: graph.KindField, Name: "count",
FilePath: "pkg/types.go", Language: "go",
Meta: map[string]any{"receiver": "Counter"},
})
edge := &graph.Edge{
From: "pkg/a.go::Caller", To: "unresolved::*.count",
Kind: graph.EdgeReads, FilePath: "pkg/a.go", Line: 7,
Meta: map[string]any{"receiver_type": "Counter"},
}
g.AddEdge(edge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/types.go::Counter.count", edge.To)
assert.Equal(t, graph.EdgeReads, edge.Kind,
"field-typed resolution must NOT be promoted — only method-typed fallback should be")
}
func TestResolveFile(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "a.go", Kind: graph.KindFile, Name: "a.go", FilePath: "a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "a.go::Foo", Kind: graph.KindFunction, Name: "Foo", FilePath: "a.go", Language: "go"})
g.AddNode(&graph.Node{ID: "b.go::Bar", Kind: graph.KindFunction, Name: "Bar", FilePath: "b.go", Language: "go"})
callEdge := &graph.Edge{From: "a.go::Foo", To: "unresolved::Bar", Kind: graph.EdgeCalls, FilePath: "a.go", Line: 5}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveFile("a.go")
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "b.go::Bar", callEdge.To)
}
// TestResolveMethodCall_ImportReachabilityFilter exercises Pass 0:
// when two methods named "Register" exist in different packages and
// only one of those packages is imported by the caller's file, the
// resolver must pick the imported package's method — not the
// alphabetically-first one across the whole graph (which was the
// original RegisterAll → OverlayManager.Register bug).
func TestResolveMethodCall_ImportReachabilityFilter(t *testing.T) {
g := graph.New()
// Caller file in pkg/parser/languages/. Imports pkg/parser only.
g.AddNode(&graph.Node{ID: "pkg/parser/languages/register.go", Kind: graph.KindFile, Name: "register.go", FilePath: "pkg/parser/languages/register.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/parser/languages/register.go::RegisterAll", Kind: graph.KindFunction, Name: "RegisterAll", FilePath: "pkg/parser/languages/register.go", Language: "go"})
// Imported package: pkg/parser
g.AddNode(&graph.Node{ID: "pkg/parser/registry.go", Kind: graph.KindFile, Name: "registry.go", FilePath: "pkg/parser/registry.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "pkg/parser/registry.go::Registry.Register", Kind: graph.KindMethod, Name: "Register",
FilePath: "pkg/parser/registry.go", Language: "go",
Meta: map[string]any{"receiver": "Registry"},
})
// Unrelated package: pkg/daemon. NOT imported.
g.AddNode(&graph.Node{ID: "pkg/daemon/overlay.go", Kind: graph.KindFile, Name: "overlay.go", FilePath: "pkg/daemon/overlay.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "pkg/daemon/overlay.go::OverlayManager.Register", Kind: graph.KindMethod, Name: "Register",
FilePath: "pkg/daemon/overlay.go", Language: "go",
Meta: map[string]any{"receiver": "OverlayManager"},
})
// Import edge: register.go → pkg/parser/registry.go (pre-resolved).
g.AddEdge(&graph.Edge{
From: "pkg/parser/languages/register.go",
To: "pkg/parser/registry.go",
Kind: graph.EdgeImports,
FilePath: "pkg/parser/languages/register.go", Line: 3,
})
// Unresolved method call without receiver_type — exercises the
// fallback path. With Pass 0 in place, only Registry.Register
// survives the reachability filter.
callEdge := &graph.Edge{
From: "pkg/parser/languages/register.go::RegisterAll",
To: "unresolved::*.Register",
Kind: graph.EdgeCalls,
FilePath: "pkg/parser/languages/register.go",
Line: 7,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/parser/registry.go::Registry.Register", callEdge.To,
"reachability filter should pick the imported package's method")
}
// TestResolveMethodCall_LocalityPrefersSamePackage covers the locality
// fallback: when two methods named "Do" survive Pass 0 (both packages
// imported), the same-package method beats any cross-package candidate
// regardless of name order.
func TestResolveMethodCall_LocalityPrefersSamePackage(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/a/main.go", Kind: graph.KindFile, FilePath: "pkg/a/main.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/a/main.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/a/main.go", Language: "go"})
// Same-package candidate: pkg/a/helpers.go::Helper.Do
g.AddNode(&graph.Node{ID: "pkg/a/helpers.go", Kind: graph.KindFile, FilePath: "pkg/a/helpers.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "pkg/a/helpers.go::Helper.Do", Kind: graph.KindMethod, Name: "Do",
FilePath: "pkg/a/helpers.go", Language: "go",
Meta: map[string]any{"receiver": "Helper"},
})
// Imported but cross-package: pkg/aaa/util.go::AAA.Do (sorts first
// alphabetically — the old fallback would pick this).
g.AddNode(&graph.Node{ID: "pkg/aaa/util.go", Kind: graph.KindFile, FilePath: "pkg/aaa/util.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "pkg/aaa/util.go::AAA.Do", Kind: graph.KindMethod, Name: "Do",
FilePath: "pkg/aaa/util.go", Language: "go",
Meta: map[string]any{"receiver": "AAA"},
})
// Caller's file imports pkg/aaa, so both packages are reachable.
g.AddEdge(&graph.Edge{
From: "pkg/a/main.go",
To: "pkg/aaa/util.go",
Kind: graph.EdgeImports,
FilePath: "pkg/a/main.go", Line: 3,
})
callEdge := &graph.Edge{
From: "pkg/a/main.go::Caller",
To: "unresolved::*.Do",
Kind: graph.EdgeCalls,
FilePath: "pkg/a/main.go",
Line: 10,
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "pkg/a/helpers.go::Helper.Do", callEdge.To,
"same-package candidate should win the locality fallback")
}
// TestResolveMethodCall_InterfaceDispatchFanOut verifies that when the
// receiver type names a graph interface AND multiple reachable methods
// of that name exist, the edge is annotated as interface dispatch.
func TestResolveMethodCall_InterfaceDispatchFanOut(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "pkg/main.go", Kind: graph.KindFile, FilePath: "pkg/main.go", Language: "go"})
g.AddNode(&graph.Node{ID: "pkg/main.go::Caller", Kind: graph.KindFunction, Name: "Caller", FilePath: "pkg/main.go", Language: "go"})
// Interface "Notifier" with method "Notify".
g.AddNode(&graph.Node{
ID: "pkg/notifier.go::Notifier", Kind: graph.KindInterface, Name: "Notifier",
FilePath: "pkg/notifier.go", Language: "go",
})
// Two implementations of Notify in the same package as the caller.
g.AddNode(&graph.Node{
ID: "pkg/email.go::EmailNotifier.Notify", Kind: graph.KindMethod, Name: "Notify",
FilePath: "pkg/email.go", Language: "go",
Meta: map[string]any{"receiver": "EmailNotifier"},
})
g.AddNode(&graph.Node{
ID: "pkg/sms.go::SMSNotifier.Notify", Kind: graph.KindMethod, Name: "Notify",
FilePath: "pkg/sms.go", Language: "go",
Meta: map[string]any{"receiver": "SMSNotifier"},
})
// Receiver type is the interface itself — ambiguous dispatch.
callEdge := &graph.Edge{
From: "pkg/main.go::Caller",
To: "unresolved::*.Notify",
Kind: graph.EdgeCalls,
FilePath: "pkg/main.go",
Line: 10,
Meta: map[string]any{"receiver_type": "Notifier"},
}
g.AddEdge(callEdge)
r := New(g)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
require.NotNil(t, callEdge.Meta)
assert.Equal(t, "interface", callEdge.Meta["dispatch"],
"edge should be marked as interface dispatch when receiver is an interface and multiple impls exist")
assert.Equal(t, graph.OriginASTInferred, callEdge.Origin,
"a locality-heuristic pick is ast_inferred — stamping an LSP tier without server evidence poisons min_tier filtering")
}