chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zzet/gortex/internal/graph"
|
||||
)
|
||||
|
||||
// getFileNodesCounter wraps a graph.Store and counts GetFileNodes calls so a
|
||||
// test can prove the per-file receiver rebind builds a package's type index
|
||||
// once (O(files)) rather than rebuilding it per file (O(files^2)).
|
||||
type getFileNodesCounter struct {
|
||||
graph.Store
|
||||
calls int
|
||||
}
|
||||
|
||||
func (c *getFileNodesCounter) GetFileNodes(fp string) []*graph.Node {
|
||||
c.calls++
|
||||
return c.Store.GetFileNodes(fp)
|
||||
}
|
||||
|
||||
// TestRebindGoMethodReceiversForFile_MemoizesPackageTypeIndex is the regression
|
||||
// for the scoped-tail quadratic: dispatching the per-file receiver rebind over
|
||||
// every file of a D-file package rebuilt the package's type index from scratch
|
||||
// each time — D GetFileNodes per file, D^2 for the package. The index is a pure
|
||||
// function of the directory and is not mutated by the tail, so it must be built
|
||||
// once and reused, collapsing the package cost back to O(D).
|
||||
func TestRebindGoMethodReceiversForFile_MemoizesPackageTypeIndex(t *testing.T) {
|
||||
const dfiles = 12
|
||||
dir := "repoa/pkg"
|
||||
typeFile := dir + "/t0.go"
|
||||
|
||||
cs := &getFileNodesCounter{Store: graph.New()}
|
||||
// The canonical type T lives in file 0; every other file declares a method
|
||||
// on T whose receiver is a phantom <methodfile>::T the rebind must lift onto
|
||||
// file 0's T — the cross-file-method shape the pass exists to fix.
|
||||
cs.AddNode(&graph.Node{ID: typeFile, Kind: graph.KindFile, Name: "t0.go", FilePath: typeFile, Language: "go", RepoPrefix: "repoa"})
|
||||
cs.AddNode(&graph.Node{ID: typeFile + "::T", Kind: graph.KindType, Name: "T", FilePath: typeFile, Language: "go", RepoPrefix: "repoa"})
|
||||
for i := 1; i < dfiles; i++ {
|
||||
f := fmt.Sprintf("%s/m%d.go", dir, i)
|
||||
cs.AddNode(&graph.Node{ID: f, Kind: graph.KindFile, Name: filepath.Base(f), FilePath: f, Language: "go", RepoPrefix: "repoa"})
|
||||
m := fmt.Sprintf("%s::T.M%d", f, i)
|
||||
cs.AddNode(&graph.Node{ID: m, Kind: graph.KindMethod, Name: fmt.Sprintf("M%d", i), FilePath: f, Language: "go", RepoPrefix: "repoa"})
|
||||
cs.AddEdge(&graph.Edge{From: m, To: f + "::T", Kind: graph.EdgeMemberOf, FilePath: f, Line: 1})
|
||||
}
|
||||
|
||||
r := New(cs)
|
||||
r.buildDirIndexes()
|
||||
defer r.clearDirIndexes()
|
||||
|
||||
files := []string{typeFile}
|
||||
for i := 1; i < dfiles; i++ {
|
||||
files = append(files, fmt.Sprintf("%s/m%d.go", dir, i))
|
||||
}
|
||||
cs.calls = 0
|
||||
for _, f := range files {
|
||||
r.rebindGoMethodReceiversForFile(f)
|
||||
}
|
||||
|
||||
// Memoized: the type index is built once (dfiles GetFileNodes) plus one
|
||||
// memberOf fetch per file — ~2·D. The quadratic rebuild would be ~D·(D+1)
|
||||
// (132 for D=12); 3·D (36) sits well below it and above the linear cost.
|
||||
assert.LessOrEqualf(t, cs.calls, 3*dfiles,
|
||||
"per-file receiver rebind rebuilt the package type index per file (%d GetFileNodes for %d files)", cs.calls, dfiles)
|
||||
|
||||
// Correctness: every phantom cross-file receiver rebound onto file 0's T.
|
||||
for i := 1; i < dfiles; i++ {
|
||||
f := fmt.Sprintf("%s/m%d.go", dir, i)
|
||||
assert.Truef(t, hasEdgeKind(cs, fmt.Sprintf("%s::T.M%d", f, i), typeFile+"::T", graph.EdgeMemberOf),
|
||||
"method M%d's receiver must rebind onto the canonical type", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRebindGoMethodReceivers_CollapsesCrossFileMethods is the
|
||||
// regression for the Go extractor emitting EdgeMemberOf targets as
|
||||
// <methodfile>::TypeName. When methods on the same type live in
|
||||
// different files of the same package, the parser produces a phantom
|
||||
// type ID per method-file; the rebind pass must collapse them onto
|
||||
// the canonical <typefile>::TypeName node so InferImplements and the
|
||||
// downstream MCP tools (find_implementations, class_hierarchy) see
|
||||
// the consolidated method set.
|
||||
func TestRebindGoMethodReceivers_CollapsesCrossFileMethods(t *testing.T) {
|
||||
g := graph.New()
|
||||
|
||||
// Type defined in indexer.go.
|
||||
typeID := "internal/indexer/indexer.go::Indexer"
|
||||
g.AddNode(&graph.Node{
|
||||
ID: typeID, Kind: graph.KindType, Name: "Indexer",
|
||||
FilePath: "internal/indexer/indexer.go", Language: "go",
|
||||
})
|
||||
|
||||
// Method declared in a *different* file in the same package — the
|
||||
// parser emits a phantom receiver target.
|
||||
methodID := "internal/indexer/crash_isolation.go::Indexer.crashIsolationEnabled"
|
||||
g.AddNode(&graph.Node{
|
||||
ID: methodID, Kind: graph.KindMethod, Name: "crashIsolationEnabled",
|
||||
FilePath: "internal/indexer/crash_isolation.go", Language: "go",
|
||||
})
|
||||
phantomTarget := "internal/indexer/crash_isolation.go::Indexer"
|
||||
memberEdge := &graph.Edge{
|
||||
From: methodID, To: phantomTarget, Kind: graph.EdgeMemberOf,
|
||||
FilePath: "internal/indexer/crash_isolation.go", Line: 23,
|
||||
}
|
||||
g.AddEdge(memberEdge)
|
||||
|
||||
// Sanity: pre-pass the phantom target has no real node.
|
||||
require.Nil(t, g.GetNode(phantomTarget), "phantom target must not exist as a real node")
|
||||
|
||||
r := New(g)
|
||||
r.rebindGoMethodReceivers()
|
||||
|
||||
// Post-pass: the edge points at the canonical type node.
|
||||
assert.Equal(t, typeID, memberEdge.To,
|
||||
"EdgeMemberOf must be rewritten from <methodfile>::Type to canonical <typefile>::Type")
|
||||
|
||||
// And the same-file method on the type works too — covered by not
|
||||
// breaking a control case:
|
||||
g2 := graph.New()
|
||||
g2.AddNode(&graph.Node{
|
||||
ID: "pkg/foo.go::Foo", Kind: graph.KindType, Name: "Foo",
|
||||
FilePath: "pkg/foo.go", Language: "go",
|
||||
})
|
||||
g2.AddNode(&graph.Node{
|
||||
ID: "pkg/foo.go::Foo.Bar", Kind: graph.KindMethod, Name: "Bar",
|
||||
FilePath: "pkg/foo.go", Language: "go",
|
||||
})
|
||||
sameFileEdge := &graph.Edge{
|
||||
From: "pkg/foo.go::Foo.Bar", To: "pkg/foo.go::Foo",
|
||||
Kind: graph.EdgeMemberOf, FilePath: "pkg/foo.go", Line: 5,
|
||||
}
|
||||
g2.AddEdge(sameFileEdge)
|
||||
|
||||
New(g2).rebindGoMethodReceivers()
|
||||
assert.Equal(t, "pkg/foo.go::Foo", sameFileEdge.To,
|
||||
"same-file method edge must be left unchanged")
|
||||
}
|
||||
|
||||
// TestRebindGoMethodReceivers_LanguageGated guards against the pass
|
||||
// rewriting non-Go EdgeMemberOf edges. Java/TS/Python group methods
|
||||
// in the class body so their EdgeMemberOf targets are already
|
||||
// in-file; we don't want the pass touching them.
|
||||
func TestRebindGoMethodReceivers_LanguageGated(t *testing.T) {
|
||||
g := graph.New()
|
||||
|
||||
// A type and a method in the same Go package — would normally be
|
||||
// a rebind candidate.
|
||||
g.AddNode(&graph.Node{
|
||||
ID: "pkg/types.go::Server", Kind: graph.KindType, Name: "Server",
|
||||
FilePath: "pkg/types.go", Language: "go",
|
||||
})
|
||||
// But the METHOD is declared as TypeScript (e.g. a TS extractor
|
||||
// that emits the same EdgeMemberOf shape for some bridging
|
||||
// reason). Pass must leave it alone.
|
||||
tsMethod := &graph.Node{
|
||||
ID: "pkg/handler.ts::Server.serve", Kind: graph.KindMethod, Name: "serve",
|
||||
FilePath: "pkg/handler.ts", Language: "typescript",
|
||||
}
|
||||
g.AddNode(tsMethod)
|
||||
edge := &graph.Edge{
|
||||
From: tsMethod.ID, To: "pkg/handler.ts::Server",
|
||||
Kind: graph.EdgeMemberOf, FilePath: "pkg/handler.ts", Line: 1,
|
||||
}
|
||||
g.AddEdge(edge)
|
||||
|
||||
New(g).rebindGoMethodReceivers()
|
||||
assert.Equal(t, "pkg/handler.ts::Server", edge.To,
|
||||
"non-Go method edge must NOT be rewritten by the Go-only rebind pass")
|
||||
}
|
||||
|
||||
// TestRebindGoMethodReceivers_AmbiguousNameSkipped guards against the
|
||||
// pass picking an arbitrary winner when two distinct types share the
|
||||
// same name in the same package (shouldn't happen in valid Go, but
|
||||
// the pass should leave the phantom alone rather than mis-bind).
|
||||
func TestRebindGoMethodReceivers_AmbiguousNameSkipped(t *testing.T) {
|
||||
g := graph.New()
|
||||
g.AddNode(&graph.Node{
|
||||
ID: "pkg/a.go::Dup", Kind: graph.KindType, Name: "Dup",
|
||||
FilePath: "pkg/a.go", Language: "go",
|
||||
})
|
||||
g.AddNode(&graph.Node{
|
||||
ID: "pkg/b.go::Dup", Kind: graph.KindType, Name: "Dup",
|
||||
FilePath: "pkg/b.go", Language: "go",
|
||||
})
|
||||
g.AddNode(&graph.Node{
|
||||
ID: "pkg/c.go::Dup.M", Kind: graph.KindMethod, Name: "M",
|
||||
FilePath: "pkg/c.go", Language: "go",
|
||||
})
|
||||
edge := &graph.Edge{
|
||||
From: "pkg/c.go::Dup.M", To: "pkg/c.go::Dup",
|
||||
Kind: graph.EdgeMemberOf, FilePath: "pkg/c.go", Line: 1,
|
||||
}
|
||||
g.AddEdge(edge)
|
||||
|
||||
New(g).rebindGoMethodReceivers()
|
||||
assert.Equal(t, "pkg/c.go::Dup", edge.To,
|
||||
"ambiguous type name in same package must leave the edge phantom rather than guess")
|
||||
}
|
||||
Reference in New Issue
Block a user