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

692 lines
28 KiB
Go

package resolver
import (
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// fakeLSPHelper is a deterministic mock implementing LSPHelper for
// tests. exts narrows which file paths it claims; defs is the
// canonical mapping from (callerPath, line, name) → (defPath, line).
type fakeLSPHelper struct {
exts []string
defs map[lspKey]lspAnswer
calls int
inFlight int
maxInFlight int // high-water mark of concurrent Definition calls
mu sync.Mutex
hangCh chan struct{} // optional: when non-nil, blocks Definition until closed (timeout testing)
}
type lspKey struct {
path string
line int
name string
}
type lspAnswer struct {
defPath string
defLine int
}
func (f *fakeLSPHelper) SupportsPath(relPath string) bool {
if len(f.exts) == 0 {
return true
}
for _, e := range f.exts {
if hasSuffix(relPath, e) {
return true
}
}
return false
}
func (f *fakeLSPHelper) Definition(relPath string, line int, name string) (string, int, bool) {
f.mu.Lock()
f.calls++
f.inFlight++
if f.inFlight > f.maxInFlight {
f.maxInFlight = f.inFlight
}
f.mu.Unlock()
if f.hangCh != nil {
<-f.hangCh
}
f.mu.Lock()
f.inFlight--
f.mu.Unlock()
a, ok := f.defs[lspKey{path: relPath, line: line, name: name}]
if !ok {
return "", 0, false
}
return a.defPath, a.defLine, true
}
func hasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
// TestLSPHotPath_BarrelReExport — the canonical case the heuristic
// loses: a method called by selector through a barrel re-export. The
// AST resolver can find a same-named target anywhere in the repo
// (potentially the wrong one); the LSP definition lookup pins the
// edge to the precise re-exported declaration.
//
// Driven through the single-file ResolveFile path: that is where the LSP
// helper is consulted inline (LSP-first), the behaviour an interactive edit
// relies on. A whole-graph ResolveAll runs in bulk mode, where LSP is deferred
// to a post-loop mop-up for the edges the heuristic cascade leaves unresolved
// (see TestLSPHotPath_BulkDefersLSP*).
func TestLSPHotPath_BarrelReExport(t *testing.T) {
g := graph.New()
// Files
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/real.ts", Kind: graph.KindFile, Name: "real.ts", FilePath: "src/real.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/decoy.ts", Kind: graph.KindFile, Name: "decoy.ts", FilePath: "src/decoy.ts", Language: "typescript"})
// Decoy: same name in another file — the heuristic resolver
// would pick this first because filterByReachability and same-
// dir bias both fail.
g.AddNode(&graph.Node{
ID: "src/decoy.ts::doWork", Kind: graph.KindFunction, Name: "doWork",
FilePath: "src/decoy.ts", StartLine: 12, EndLine: 14, Language: "typescript",
})
// Real definition the LSP will report.
g.AddNode(&graph.Node{
ID: "src/real.ts::doWork", Kind: graph.KindFunction, Name: "doWork",
FilePath: "src/real.ts", StartLine: 7, EndLine: 9, Language: "typescript",
})
// Caller
g.AddNode(&graph.Node{
ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt",
FilePath: "src/caller.ts", StartLine: 3, EndLine: 5, Language: "typescript",
})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::doWork",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 4,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 4, name: "doWork"}: {defPath: "src/real.ts", defLine: 7},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveFile("src/caller.ts")
require.Equal(t, 1, stats.Resolved)
assert.Equal(t, "src/real.ts::doWork", callEdge.To, "edge must bind to LSP-reported definition, not the decoy")
assert.Equal(t, graph.OriginLSPResolved, callEdge.Origin)
require.NotNil(t, callEdge.Meta)
assert.Equal(t, "lsp", callEdge.Meta["resolved_by"])
assert.Equal(t, 1, helper.calls)
}
// TestLSPHotPath_FallthroughOnMiss — when the LSP returns no answer,
// the heuristic cascade still runs. The edge gets resolved by the
// AST resolver and its Origin reflects the AST tier (NOT lsp_*).
func TestLSPHotPath_FallthroughOnMiss(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/a.ts", Kind: graph.KindFile, Name: "a.ts", FilePath: "src/a.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/b.ts", Kind: graph.KindFile, Name: "b.ts", FilePath: "src/b.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/a.ts::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "src/a.ts", Language: "typescript"})
g.AddNode(&graph.Node{
ID: "src/b.ts::theTarget", Kind: graph.KindFunction, Name: "theTarget",
FilePath: "src/b.ts", StartLine: 4, EndLine: 6, Language: "typescript",
})
callEdge := &graph.Edge{
From: "src/a.ts::caller", To: "unresolved::theTarget",
Kind: graph.EdgeCalls, FilePath: "src/a.ts", Line: 2,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{}, // empty — every call misses
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
require.Equal(t, 1, stats.Resolved, "heuristic cascade should still resolve")
assert.Equal(t, "src/b.ts::theTarget", callEdge.To)
assert.NotEqual(t, graph.OriginLSPResolved, callEdge.Origin, "miss → heuristic tier, not lsp_resolved")
if callEdge.Meta != nil {
assert.NotEqual(t, "lsp", callEdge.Meta["resolved_by"])
}
}
// TestLSPHotPath_ExtensionGate — the helper short-circuits on
// SupportsPath, so a Go-file edge doesn't trigger any LSP call when
// the helper claims only TS extensions. The heuristic resolver
// produces the answer.
func TestLSPHotPath_ExtensionGate(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: "b.go", Kind: graph.KindFile, Name: "b.go", FilePath: "b.go", Language: "go"})
g.AddNode(&graph.Node{ID: "a.go::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "a.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "b.go::target", Kind: graph.KindFunction, Name: "target",
FilePath: "b.go", StartLine: 3, EndLine: 5, Language: "go",
})
callEdge := &graph.Edge{
From: "a.go::caller", To: "unresolved::target",
Kind: graph.EdgeCalls, FilePath: "a.go", Line: 2,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts", ".tsx"},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "b.go::target", callEdge.To)
assert.Equal(t, 0, helper.calls, "helper must NOT be called for non-claimed extensions")
}
// TestLSPHotPath_KindGate — the LSP helper returns a file-node
// location, but the edge is a `calls` edge that must land on a
// function/method/closure. lspKindAcceptableFor rejects the bind
// and the heuristic falls through.
func TestLSPHotPath_KindGate(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/util.ts", Kind: graph.KindFile, Name: "util.ts", FilePath: "src/util.ts", Language: "typescript", StartLine: 1})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{
ID: "src/util.ts::reallyDoIt", Kind: graph.KindFunction, Name: "reallyDoIt",
FilePath: "src/util.ts", StartLine: 5, EndLine: 7, Language: "typescript",
})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::reallyDoIt",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 2,
}
g.AddEdge(callEdge)
// LSP points the edge at the file node, not the function node —
// the kind-gate should reject.
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 2, name: "reallyDoIt"}: {defPath: "src/util.ts", defLine: 1},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
require.Equal(t, 1, stats.Resolved)
// Heuristic picked the function node, not the file.
assert.Equal(t, "src/util.ts::reallyDoIt", callEdge.To)
// Origin must NOT be lsp_resolved — gate rejected the LSP answer.
assert.NotEqual(t, graph.OriginLSPResolved, callEdge.Origin)
}
// TestLSPHotPath_MethodSelector — the resolver receives an unresolved
// `*.Name` selector target. tryResolveViaLSP strips the prefix and
// asks the helper for `Name`. On a hit, the method edge binds to the
// LSP-reported target across files.
func TestLSPHotPath_MethodSelector(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/svc.ts", Kind: graph.KindFile, Name: "svc.ts", FilePath: "src/svc.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/decoy.ts", Kind: graph.KindFile, Name: "decoy.ts", FilePath: "src/decoy.ts", Language: "typescript"})
g.AddNode(&graph.Node{
ID: "src/svc.ts::Service.handle", Kind: graph.KindMethod, Name: "handle",
FilePath: "src/svc.ts", StartLine: 10, EndLine: 12, Language: "typescript",
})
g.AddNode(&graph.Node{
ID: "src/decoy.ts::Other.handle", Kind: graph.KindMethod, Name: "handle",
FilePath: "src/decoy.ts", StartLine: 5, EndLine: 7, Language: "typescript",
})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::*.handle",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 9,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 9, name: "handle"}: {defPath: "src/svc.ts", defLine: 10},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveFile("src/caller.ts")
require.Equal(t, 1, stats.Resolved)
assert.Equal(t, "src/svc.ts::Service.handle", callEdge.To)
assert.Equal(t, graph.OriginLSPResolved, callEdge.Origin)
assert.Equal(t, "lsp", callEdge.Meta["resolved_by"])
}
// TestLSPHotPath_MethodValueReadPromotesToReferences — when the LSP
// helper binds an EdgeReads to a KindMethod (the `mux.HandleFunc("/p",
// h.foo)` shape where h.foo is passed as a method value), the kind
// must be promoted to EdgeReferences. The heuristic cascade already
// does this in resolver.go's `*. + Reads/Writes` case; the LSP hot
// path used to short-circuit before that branch ran and silently
// leave the kind as EdgeReads — which GetCallers/FindUsages drop
// (they only follow Calls/Matches/References). Every HTTP handler in
// every router-style codebase looked like dead code as a result.
func TestLSPHotPath_MethodValueReadPromotesToReferences(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/routes.go", Kind: graph.KindFile, Name: "routes.go", FilePath: "src/routes.go", Language: "go"})
g.AddNode(&graph.Node{ID: "src/handler.go", Kind: graph.KindFile, Name: "handler.go", FilePath: "src/handler.go", Language: "go"})
g.AddNode(&graph.Node{ID: "src/routes.go::RegisterRoutes", Kind: graph.KindFunction, Name: "RegisterRoutes", FilePath: "src/routes.go", Language: "go"})
g.AddNode(&graph.Node{
ID: "src/handler.go::Handler.HandleHealth", Kind: graph.KindMethod, Name: "HandleHealth",
FilePath: "src/handler.go", StartLine: 42, EndLine: 45, Language: "go",
})
readEdge := &graph.Edge{
From: "src/routes.go::RegisterRoutes", To: "unresolved::*.HandleHealth",
Kind: graph.EdgeReads, FilePath: "src/routes.go", Line: 10,
}
g.AddEdge(readEdge)
helper := &fakeLSPHelper{
exts: []string{".go"},
defs: map[lspKey]lspAnswer{
{path: "src/routes.go", line: 10, name: "HandleHealth"}: {defPath: "src/handler.go", defLine: 42},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveFile("src/routes.go")
require.Equal(t, 1, stats.Resolved)
assert.Equal(t, "src/handler.go::Handler.HandleHealth", readEdge.To)
assert.Equal(t, graph.OriginLSPResolved, readEdge.Origin)
assert.Equal(t, graph.EdgeReferences, readEdge.Kind,
"LSP-bound EdgeReads on a KindMethod must be promoted so get_callers surfaces it")
}
// TestLSPHotPath_FunctionValueReadPromotesToReferences — companion
// to the method-value test above. The cobra/CLI pattern
// `&cobra.Command{RunE: runClean}` emits EdgeReads with To=
// "unresolved::runClean", and the LSP helper happily binds it to
// the runClean function. Without promotion the wire-up site is
// invisible to get_callers, so every cobra subcommand looked dead.
func TestLSPHotPath_FunctionValueReadPromotesToReferences(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "cmd/x/main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "cmd/x/main.go", Language: "go"})
g.AddNode(&graph.Node{ID: "cmd/x/clean.go", Kind: graph.KindFile, Name: "clean.go", FilePath: "cmd/x/clean.go", Language: "go"})
g.AddNode(&graph.Node{ID: "cmd/x/main.go::init", Kind: graph.KindFunction, Name: "init", 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", StartLine: 20, EndLine: 30, Language: "go",
})
readEdge := &graph.Edge{
From: "cmd/x/main.go::init", To: "unresolved::runClean",
Kind: graph.EdgeReads, FilePath: "cmd/x/main.go", Line: 13,
}
g.AddEdge(readEdge)
helper := &fakeLSPHelper{
exts: []string{".go"},
defs: map[lspKey]lspAnswer{
{path: "cmd/x/main.go", line: 13, name: "runClean"}: {defPath: "cmd/x/clean.go", defLine: 20},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveFile("cmd/x/main.go")
require.Equal(t, 1, stats.Resolved)
assert.Equal(t, "cmd/x/clean.go::runClean", readEdge.To)
assert.Equal(t, graph.OriginLSPResolved, readEdge.Origin)
assert.Equal(t, graph.EdgeReferences, readEdge.Kind,
"LSP-bound EdgeReads on a KindFunction must promote to References")
}
// TestLSPHotPath_NilHelper — when no helper is installed, the
// resolver runs heuristic-only as in the pre-N5 world.
func TestLSPHotPath_NilHelper(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "a.ts", Kind: graph.KindFile, Name: "a.ts", FilePath: "a.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "b.ts", Kind: graph.KindFile, Name: "b.ts", FilePath: "b.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "a.ts::caller", Kind: graph.KindFunction, Name: "caller", FilePath: "a.ts", Language: "typescript"})
g.AddNode(&graph.Node{
ID: "b.ts::tgt", Kind: graph.KindFunction, Name: "tgt",
FilePath: "b.ts", StartLine: 1, EndLine: 3, Language: "typescript",
})
callEdge := &graph.Edge{
From: "a.ts::caller", To: "unresolved::tgt",
Kind: graph.EdgeCalls, FilePath: "a.ts", Line: 1,
}
g.AddEdge(callEdge)
r := New(g)
// no helper installed
stats := r.ResolveAll()
assert.Equal(t, 1, stats.Resolved)
assert.Equal(t, "b.ts::tgt", callEdge.To)
}
// TestLSPHotPath_IdentifierFromTarget — covers the prefix stripping
// for the target shapes the resolver dispatches on.
func TestIdentifierFromTarget(t *testing.T) {
cases := []struct {
in, want string
}{
{"foo", "foo"},
{"*.handle", "handle"},
{"extern::pkg/sub::Symbol", "Symbol"},
{"extern::pkg::A::B", "B"},
{"import::pkg/foo", ""},
{"pyrel::foo", ""},
{"grpc::Svc::Method", ""},
}
for _, c := range cases {
got := identifierFromTarget(c.in)
assert.Equalf(t, c.want, got, "input=%q", c.in)
}
}
// TestLSPKindAcceptableFor covers the kind-gate rules.
func TestLSPKindAcceptableFor(t *testing.T) {
cases := []struct {
ek graph.EdgeKind
nk graph.NodeKind
want bool
}{
{graph.EdgeCalls, graph.KindFunction, true},
{graph.EdgeCalls, graph.KindMethod, true},
{graph.EdgeCalls, graph.KindFile, false},
{graph.EdgeCalls, graph.KindImport, false},
{graph.EdgeExtends, graph.KindType, true},
{graph.EdgeExtends, graph.KindFunction, false},
{graph.EdgeImplements, graph.KindInterface, true},
{graph.EdgeImplements, graph.KindMethod, false},
{graph.EdgeReads, graph.KindField, true},
{graph.EdgeReads, graph.KindVariable, true},
{graph.EdgeReads, graph.KindFile, false},
{graph.EdgeReferences, graph.KindType, true},
{graph.EdgeReferences, graph.KindFile, false},
}
for _, c := range cases {
got := lspKindAcceptableFor(c.ek, c.nk)
assert.Equalf(t, c.want, got, "edge=%s node=%s", c.ek, c.nk)
}
}
// TestLSPHotPath_LSPIndexCaching — multiple edges resolving via LSP
// to the same definition should hit the lspIndex cache, not rescan
// the file each time.
func TestLSPHotPath_LSPIndexCaching(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/svc.ts", Kind: graph.KindFile, Name: "svc.ts", FilePath: "src/svc.ts", Language: "typescript"})
g.AddNode(&graph.Node{
ID: "src/svc.ts::theTarget", Kind: graph.KindFunction, Name: "theTarget",
FilePath: "src/svc.ts", StartLine: 4, EndLine: 6, Language: "typescript",
})
g.AddNode(&graph.Node{ID: "src/caller.ts::callerA", Kind: graph.KindFunction, Name: "callerA", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/caller.ts::callerB", Kind: graph.KindFunction, Name: "callerB", FilePath: "src/caller.ts", Language: "typescript"})
e1 := &graph.Edge{From: "src/caller.ts::callerA", To: "unresolved::theTarget", Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 3}
e2 := &graph.Edge{From: "src/caller.ts::callerB", To: "unresolved::theTarget", Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 7}
g.AddEdge(e1)
g.AddEdge(e2)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 3, name: "theTarget"}: {defPath: "src/svc.ts", defLine: 4},
{path: "src/caller.ts", line: 7, name: "theTarget"}: {defPath: "src/svc.ts", defLine: 4},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveFile("src/caller.ts")
require.Equal(t, 2, stats.Resolved)
assert.Equal(t, "src/svc.ts::theTarget", e1.To)
assert.Equal(t, "src/svc.ts::theTarget", e2.To)
assert.Equal(t, graph.OriginLSPResolved, e1.Origin)
assert.Equal(t, graph.OriginLSPResolved, e2.Origin)
}
// TestLSPHotPath_NoOpAfterFileMiss — when LSP returns a path that
// doesn't exist in the graph, the bind should fall through. This
// protects against off-by-one path mismatches.
func TestLSPHotPath_NoOpAfterFileMiss(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::ghost",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 2,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 2, name: "ghost"}: {defPath: "nonexistent/file.ts", defLine: 1},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
// LSP miss path triggered (no graph node at that file) — heuristic
// has nothing to find either, so the edge is left unresolved.
assert.Equal(t, 0, stats.Resolved)
assert.Equal(t, 1, stats.Unresolved)
}
// TestLSPHotPath_BulkDefersLSP_ResolvesUnresolved — the deferral contract on
// the whole-graph ResolveAll (bulk) path. The heuristic cascade cannot bind
// the call (there is no graph node named "doThing"), so the edge is collected
// and bound in the post-loop deferred LSP batch instead of an inline round-
// trip inside the parallel workers. resolveEdge never consults the helper in
// bulk mode, so the single recorded call can only have come from the deferred
// batch, and maxInFlight==1 confirms the batch ran the call serially, off the
// parallel worker barrier.
func TestLSPHotPath_BulkDefersLSP_ResolvesUnresolved(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/svc.ts", Kind: graph.KindFile, Name: "svc.ts", FilePath: "src/svc.ts", Language: "typescript"})
// The definition the LSP reports lives under a different name than the
// call site uses (a rename / barrel re-export), so the name-only heuristic
// finds nothing to bind — only the LSP location lookup can resolve it.
g.AddNode(&graph.Node{
ID: "src/svc.ts::renamedTarget", Kind: graph.KindFunction, Name: "renamedTarget",
FilePath: "src/svc.ts", StartLine: 5, EndLine: 7, Language: "typescript",
})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::doThing",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 3,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 3, name: "doThing"}: {defPath: "src/svc.ts", defLine: 5},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
require.Equal(t, 1, stats.Resolved)
assert.Equal(t, "src/svc.ts::renamedTarget", callEdge.To, "deferred LSP batch must bind the heuristic-unresolved edge")
assert.Equal(t, graph.OriginLSPResolved, callEdge.Origin)
require.NotNil(t, callEdge.Meta)
assert.Equal(t, "lsp", callEdge.Meta["resolved_by"])
assert.Equal(t, 1, helper.calls, "helper is consulted exactly once, in the deferred batch")
assert.Equal(t, 1, helper.maxInFlight, "deferred LSP calls run serially, off the parallel worker barrier")
}
// TestLSPHotPath_BulkDefersLSPEvenWhenHeuristicResolves — the other half of
// the deferral contract: the helper is never consulted INLINE during the bulk
// chunk loop, but a heuristic-resolved LSP-eligible edge is still collected
// for the post-loop deferred batch so LSP keeps its override authority. Here
// the heuristic binds the sole same-dir candidate and LSP confirms the same
// node; the edge ends LSP-stamped and the single helper call ran serially in
// the deferred batch (maxInFlight==1), off the parallel worker barrier —
// resolveEdge never touches the helper in bulk mode.
func TestLSPHotPath_BulkDefersLSPEvenWhenHeuristicResolves(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/svc.ts", Kind: graph.KindFile, Name: "svc.ts", FilePath: "src/svc.ts", Language: "typescript"})
g.AddNode(&graph.Node{
ID: "src/svc.ts::realFn", Kind: graph.KindFunction, Name: "realFn",
FilePath: "src/svc.ts", StartLine: 4, EndLine: 6, Language: "typescript",
})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::realFn",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 3,
}
g.AddEdge(callEdge)
// The heuristic resolves the call itself (sole same-dir candidate), and the
// helper confirms the same definition. The deferred batch must still run so
// LSP retains override authority — here it upgrades the edge's provenance.
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 3, name: "realFn"}: {defPath: "src/svc.ts", defLine: 4},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
require.Equal(t, 1, stats.Resolved, "the confirmed edge stays counted once, not double-counted by the deferred batch")
assert.Equal(t, "src/svc.ts::realFn", callEdge.To)
assert.Equal(t, graph.OriginLSPResolved, callEdge.Origin, "the deferred batch confirms the bind and stamps LSP provenance")
require.NotNil(t, callEdge.Meta)
assert.Equal(t, "lsp", callEdge.Meta["resolved_by"])
assert.Equal(t, 1, helper.calls, "helper consulted exactly once, in the deferred batch")
assert.Equal(t, 1, helper.maxInFlight, "deferred LSP calls run serially, off the parallel worker barrier")
}
// TestLSPHotPath_BulkLSPOverridesHeuristicMisbind — the regression that closes
// the bulk-mode LSP-override gap. The heuristic CONFIDENTLY binds a bare call
// to a same-directory sibling that shadows the name (src/neighbor.ts::foo at
// ast_resolved) because the import bringing the real symbol into scope can't be
// expanded. LSP knows the real definition lives cross-directory in
// lib/real.ts. In bulk mode the edge must still be deferred despite the
// confident heuristic bind, and the deferred batch must OVERRIDE it to the
// LSP-reported definition — matching the LSP-first single-file path. Before the
// fix, a heuristic-resolved edge was never deferred, so the mis-bind persisted
// across every restart and get_callers/find_usages attributed the call to the
// wrong function.
func TestLSPHotPath_BulkLSPOverridesHeuristicMisbind(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/neighbor.ts", Kind: graph.KindFile, Name: "neighbor.ts", FilePath: "src/neighbor.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "lib/real.ts", Kind: graph.KindFile, Name: "real.ts", FilePath: "lib/real.ts", Language: "typescript"})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
// Same-directory shadow the heuristic will confidently (wrongly) pick.
g.AddNode(&graph.Node{
ID: "src/neighbor.ts::foo", Kind: graph.KindFunction, Name: "foo",
FilePath: "src/neighbor.ts", StartLine: 2, EndLine: 4, Language: "typescript",
})
// The real definition, cross-directory — only LSP resolves to it.
g.AddNode(&graph.Node{
ID: "lib/real.ts::foo", Kind: graph.KindFunction, Name: "foo",
FilePath: "lib/real.ts", StartLine: 7, EndLine: 9, Language: "typescript",
})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::foo",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 4,
}
g.AddEdge(callEdge)
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 4, name: "foo"}: {defPath: "lib/real.ts", defLine: 7},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
require.Equal(t, 1, stats.Resolved)
assert.Equal(t, "lib/real.ts::foo", callEdge.To,
"deferred LSP batch must override the heuristic's same-dir mis-bind")
assert.Equal(t, graph.OriginLSPResolved, callEdge.Origin)
require.NotNil(t, callEdge.Meta)
assert.Equal(t, "lsp", callEdge.Meta["resolved_by"])
assert.Equal(t, 1, helper.calls, "helper consulted once, in the deferred batch")
}
// TestLSPHotPath_BulkDeferRespectsKindGate — a deferred edge whose LSP target
// is an unacceptable kind (a file node for a calls edge) is rejected by the
// same kind-gate the inline path applies, and left unresolved rather than
// mis-bound.
func TestLSPHotPath_BulkDeferRespectsKindGate(t *testing.T) {
g := graph.New()
g.AddNode(&graph.Node{ID: "src/caller.ts", Kind: graph.KindFile, Name: "caller.ts", FilePath: "src/caller.ts", Language: "typescript", StartLine: 1})
g.AddNode(&graph.Node{ID: "src/util.ts", Kind: graph.KindFile, Name: "util.ts", FilePath: "src/util.ts", Language: "typescript", StartLine: 1})
g.AddNode(&graph.Node{ID: "src/caller.ts::callIt", Kind: graph.KindFunction, Name: "callIt", FilePath: "src/caller.ts", Language: "typescript"})
callEdge := &graph.Edge{
From: "src/caller.ts::callIt", To: "unresolved::missing",
Kind: graph.EdgeCalls, FilePath: "src/caller.ts", Line: 2,
}
g.AddEdge(callEdge)
// LSP points at the util.ts FILE node (line 1) — the kind-gate must reject
// binding a calls edge to a file.
helper := &fakeLSPHelper{
exts: []string{".ts"},
defs: map[lspKey]lspAnswer{
{path: "src/caller.ts", line: 2, name: "missing"}: {defPath: "src/util.ts", defLine: 1},
},
}
r := New(g)
r.SetLSPHelper(helper)
stats := r.ResolveAll()
assert.Equal(t, 0, stats.Resolved)
assert.Equal(t, 1, helper.calls, "deferred batch attempted the bind")
assert.True(t, graph.IsUnresolvedTarget(callEdge.To), "kind-gated LSP answer must leave the edge unresolved")
}