Skip to content

Commit e71608c

Browse files
committed
fix: resolve implements before staged calls
1 parent e17100d commit e71608c

2 files changed

Lines changed: 94 additions & 2 deletions

File tree

internal/app/ingest/incremental/incremental.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ func (s *Syncer) syncBatchesWithExisting(ctx context.Context, syncStore Store, s
306306
}
307307

308308
lookup := newImportIndexedLookup(syncStore)
309+
// Resolve every implementation relationship before any call edge. Interface
310+
// dispatch may depend on an implements edge stored in a later spool record.
309311
for _, path := range edgeSpool.records {
310312
record, err := edgeSpool.readRecord(path)
311313
if err != nil {
@@ -315,7 +317,20 @@ func (s *Syncer) syncBatchesWithExisting(ctx context.Context, syncStore Store, s
315317
for _, file := range record.Files {
316318
parsedFiles = append(parsedFiles, parsedSyncFile{filePath: file.FilePath, edges: file.Edges})
317319
}
318-
if err := s.resolveAndUpsertEdges(ctx, syncStore, lookup, parsedFiles, stats); err != nil {
320+
if err := s.resolveAndUpsertImplements(ctx, syncStore, lookup, parsedFiles, stats); err != nil {
321+
return nil, err
322+
}
323+
}
324+
for _, path := range edgeSpool.records {
325+
record, err := edgeSpool.readRecord(path)
326+
if err != nil {
327+
return nil, err
328+
}
329+
parsedFiles := make([]parsedSyncFile, 0, len(record.Files))
330+
for _, file := range record.Files {
331+
parsedFiles = append(parsedFiles, parsedSyncFile{filePath: file.FilePath, edges: file.Edges})
332+
}
333+
if err := s.resolveAndUpsertOtherEdges(ctx, syncStore, lookup, parsedFiles, stats); err != nil {
319334
return nil, err
320335
}
321336
}
@@ -419,7 +434,25 @@ func (s *Syncer) stageBatch(ctx context.Context, syncStore Store, files map[stri
419434
// @sideEffect upserts resolved graph edges through the sync store.
420435
// @mutates graph edges, stats.Unresolved
421436
func (s *Syncer) resolveAndUpsertEdges(ctx context.Context, syncStore Store, lookup resolve.NodeLookup, parsedFiles []parsedSyncFile, stats *SyncStats) error {
422-
implementsEdges, otherByFile := partitionParsedSyncEdges(parsedFiles)
437+
if err := s.resolveAndUpsertImplements(ctx, syncStore, lookup, parsedFiles, stats); err != nil {
438+
return err
439+
}
440+
return s.resolveAndUpsertOtherEdges(ctx, syncStore, lookup, parsedFiles, stats)
441+
}
442+
443+
// resolveAndUpsertImplements resolves only implementation relationships from one parsed file set.
444+
// @intent let staged reconciliation finish a global implements pass before resolving interface-dispatch calls.
445+
// @sideEffect upserts resolved implements edges through the sync store.
446+
// @mutates graph edges, stats.Unresolved
447+
func (s *Syncer) resolveAndUpsertImplements(ctx context.Context, syncStore Store, lookup resolve.NodeLookup, parsedFiles []parsedSyncFile, stats *SyncStats) error {
448+
var implementsEdges []graph.Edge
449+
for _, parsed := range parsedFiles {
450+
for _, edge := range parsed.edges {
451+
if edge.Kind == graph.EdgeKindImplements {
452+
implementsEdges = append(implementsEdges, edge)
453+
}
454+
}
455+
}
423456
for _, edgeChunk := range splitEdgeChunks(implementsEdges) {
424457
resolved, err := resolve.ResolveWithOptions(ctx, lookup, edgeChunk, s.opts)
425458
if err != nil {
@@ -434,6 +467,15 @@ func (s *Syncer) resolveAndUpsertEdges(ctx context.Context, syncStore Store, loo
434467
return err
435468
}
436469
}
470+
return nil
471+
}
472+
473+
// resolveAndUpsertOtherEdges resolves non-implements relationships after implementation state is complete.
474+
// @intent preserve file-local import warmup while making interface call resolution independent of spool record order.
475+
// @sideEffect upserts resolved call, import, contains, and related edges through the sync store.
476+
// @mutates graph edges, stats.Unresolved
477+
func (s *Syncer) resolveAndUpsertOtherEdges(ctx context.Context, syncStore Store, lookup resolve.NodeLookup, parsedFiles []parsedSyncFile, stats *SyncStats) error {
478+
_, otherByFile := partitionParsedSyncEdges(parsedFiles)
437479
importsByFile := importEdgesByFile(otherByFile)
438480
for _, parsed := range parsedFiles {
439481
edges := otherByFile[parsed.filePath]

internal/app/ingest/incremental/incremental_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/tae2089/code-context-graph/internal/adapters/outbound/treesitter"
11+
"github.com/tae2089/code-context-graph/internal/app/ingest"
1112
"github.com/tae2089/code-context-graph/internal/domain/graph"
1213
"github.com/tae2089/code-context-graph/internal/domain/reference"
1314
)
@@ -701,6 +702,55 @@ func TestIncremental_ResolvesCallEdgesAcrossMultipleFilesInBatch(t *testing.T) {
701702
}
702703
}
703704

705+
func TestSyncBatchesWithExisting_ResolvesImplementsBeforeEarlierCallBatch(t *testing.T) {
706+
st := newStore()
707+
st.nodes["mcp.FlowTracer"] = &graph.Node{ID: 10, QualifiedName: "mcp.FlowTracer", Kind: graph.NodeKindType, Name: "FlowTracer", FilePath: "mcp/deps.go", StartLine: 1, EndLine: 3, Hash: "iface", Language: "go"}
708+
st.nodes["mcp/deps.go"] = &graph.Node{ID: 11, QualifiedName: "mcp/deps.go", Kind: graph.NodeKindFile, Name: "mcp/deps.go", FilePath: "mcp/deps.go", StartLine: 1, EndLine: 3, Hash: "iface", Language: "go"}
709+
parser := &staticParser{result: map[string]parseResult{
710+
"cmd/main.go": {
711+
nodes: []graph.Node{
712+
{QualifiedName: "cmd/main.go", Kind: graph.NodeKindFile, Name: "cmd/main.go", FilePath: "cmd/main.go", StartLine: 1, EndLine: 20, Hash: "main", Language: "go"},
713+
{QualifiedName: "main.Run", Kind: graph.NodeKindFunction, Name: "Run", FilePath: "cmd/main.go", StartLine: 3, EndLine: 8, Hash: "main", Language: "go"},
714+
},
715+
edges: []graph.Edge{
716+
{Kind: graph.EdgeKindImportsFrom, FilePath: "cmd/main.go", Line: 1, Fingerprint: "imports_from:cmd/main.go:github.com/example/project/mcp:1"},
717+
{Kind: graph.EdgeKindCalls, FilePath: "cmd/main.go", Line: 4, Fingerprint: "calls:cmd/main.go:h.deps.FlowTracer.TraceFlow:4"},
718+
},
719+
},
720+
"flows/tracer.go": {
721+
nodes: []graph.Node{
722+
{QualifiedName: "flows/tracer.go", Kind: graph.NodeKindFile, Name: "flows/tracer.go", FilePath: "flows/tracer.go", StartLine: 1, EndLine: 20, Hash: "impl", Language: "go"},
723+
{QualifiedName: "flows.Tracer", Kind: graph.NodeKindClass, Name: "Tracer", FilePath: "flows/tracer.go", StartLine: 3, EndLine: 7, Hash: "impl", Language: "go"},
724+
{QualifiedName: "flows.Tracer.TraceFlow", Kind: graph.NodeKindFunction, Name: "TraceFlow", FilePath: "flows/tracer.go", StartLine: 5, EndLine: 6, Hash: "impl", Language: "go"},
725+
},
726+
edges: []graph.Edge{{Kind: graph.EdgeKindImplements, FilePath: "flows/tracer.go", Line: 3, Fingerprint: "implements:flows/tracer.go:flows.Tracer:mcp.FlowTracer"}},
727+
},
728+
}}
729+
730+
source := ingest.FileBatchSource(func(visitor ingest.FileBatchVisitor) error {
731+
for _, files := range []map[string]ingest.FileInfo{
732+
{"cmd/main.go": {Hash: "main", Content: []byte("package main")}},
733+
{"flows/tracer.go": {Hash: "impl", Content: []byte("package flows")}},
734+
} {
735+
if err := visitor(files); err != nil {
736+
return err
737+
}
738+
}
739+
return nil
740+
})
741+
742+
syncer := New(st, parser)
743+
if _, err := syncer.SyncBatchesWithExisting(context.Background(), source, nil); err != nil {
744+
t.Fatal(err)
745+
}
746+
for _, edge := range st.upsertedEdges {
747+
if edge.Kind == graph.EdgeKindCalls && edge.FromNodeID != 0 && edge.ToNodeID != 0 {
748+
return
749+
}
750+
}
751+
t.Fatalf("expected earlier call batch to resolve after later implements batch, got %+v", st.upsertedEdges)
752+
}
753+
704754
func TestIncremental_BatchesNodesBeforeResolvingEdges(t *testing.T) {
705755
st := newStore()
706756
st.nodes["mcp.FlowTracer"] = &graph.Node{ID: 10, QualifiedName: "mcp.FlowTracer", Kind: graph.NodeKindType, Name: "FlowTracer", FilePath: "mcp/deps.go", StartLine: 1, EndLine: 3, Hash: "iface", Language: "go"}

0 commit comments

Comments
 (0)