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
214 lines
5.4 KiB
Go
214 lines
5.4 KiB
Go
package indexer
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// indexAll indexes a single-file Go fixture and runs the global
|
|
// resolve + dataflow materialisation pass. Returns the graph for
|
|
// assertions.
|
|
func indexAll(t *testing.T, src string) graph.Store {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644))
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
idx.ResolveAll()
|
|
return g
|
|
}
|
|
|
|
// findEdges returns all edges matching the predicate.
|
|
func findEdges(g graph.Store, kind graph.EdgeKind, match func(*graph.Edge) bool) []*graph.Edge {
|
|
var out []*graph.Edge
|
|
for _, e := range g.AllEdges() {
|
|
if e.Kind != kind {
|
|
continue
|
|
}
|
|
if match(e) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// TestMaterializeDataflowParams_ArgOfLifted verifies the post-pass
|
|
// rewrites EdgeArgOf targets from the function node to the actual
|
|
// param node at the recorded position.
|
|
func TestMaterializeDataflowParams_ArgOfLifted(t *testing.T) {
|
|
src := `package main
|
|
|
|
func Sink(payload string) {}
|
|
|
|
func Driver(input string) {
|
|
Sink(input)
|
|
}
|
|
`
|
|
g := indexAll(t, src)
|
|
|
|
// Look up the resolved Driver function ID — we only know the
|
|
// suffix, the prefix is whatever IndexDirectory uses.
|
|
driverID := findFuncID(t, g, "Driver")
|
|
sinkID := findFuncID(t, g, "Sink")
|
|
|
|
args := findEdges(g, graph.EdgeArgOf, func(e *graph.Edge) bool {
|
|
return e.From == driverID+"#param:input"
|
|
})
|
|
if len(args) == 0 {
|
|
t.Fatalf("no arg_of edges from Driver#param:input; have edges:\n%s", dumpAllEdges(g))
|
|
}
|
|
// The post-pass should have lifted To from sinkID itself to
|
|
// sinkID#param:payload.
|
|
want := sinkID + "#param:payload"
|
|
for _, e := range args {
|
|
if e.To == want {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("arg_of not lifted to %q; got: %s", want, dumpEdges(args))
|
|
}
|
|
|
|
// TestMaterializeDataflowParams_ReturnsToLifted verifies the post-pass
|
|
// rewrites EdgeReturnsTo From from the placeholder caller ID to
|
|
// the resolved callee.
|
|
func TestMaterializeDataflowParams_ReturnsToLifted(t *testing.T) {
|
|
src := `package main
|
|
|
|
func Source() string { return "hi" }
|
|
|
|
func Driver() {
|
|
v := Source()
|
|
_ = v
|
|
}
|
|
`
|
|
g := indexAll(t, src)
|
|
|
|
driverID := findFuncID(t, g, "Driver")
|
|
sourceID := findFuncID(t, g, "Source")
|
|
|
|
rets := findEdges(g, graph.EdgeReturnsTo, func(e *graph.Edge) bool {
|
|
return strings.HasPrefix(e.To, driverID+"#local:v@")
|
|
})
|
|
if len(rets) == 0 {
|
|
t.Fatalf("no returns_to edges; have edges:\n%s", dumpAllEdges(g))
|
|
}
|
|
for _, e := range rets {
|
|
if e.From == sourceID {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("returns_to.From not lifted to %q; got: %s", sourceID, dumpEdges(rets))
|
|
}
|
|
|
|
// TestMaterializeDataflowParams_CrossFileBinding asserts dataflow
|
|
// edges connect across files: `Driver` (in driver.go) calls
|
|
// `Sink` (in sink.go) and the post-pass binds the arg_of edge to
|
|
// Sink's param node, even though neither side has the other's
|
|
// AST visible at extraction time.
|
|
func TestMaterializeDataflowParams_CrossFileBinding(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "sink.go"), []byte(`package main
|
|
|
|
func Sink(payload string) {}
|
|
`), 0o644))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "driver.go"), []byte(`package main
|
|
|
|
func Driver(input string) {
|
|
Sink(input)
|
|
}
|
|
`), 0o644))
|
|
|
|
g := graph.New()
|
|
idx := newTestIndexer(g)
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
idx.ResolveAll()
|
|
|
|
driverID := findFuncID(t, g, "Driver")
|
|
sinkID := findFuncID(t, g, "Sink")
|
|
want := sinkID + "#param:payload"
|
|
|
|
args := findEdges(g, graph.EdgeArgOf, func(e *graph.Edge) bool {
|
|
return e.From == driverID+"#param:input" && e.To == want
|
|
})
|
|
if len(args) == 0 {
|
|
t.Fatalf("missing cross-file arg_of(Driver#param:input → %s); have:\n%s", want, dumpAllEdges(g))
|
|
}
|
|
}
|
|
|
|
// TestMaterializeDataflowParams_NoStrandedPlaceholders asserts the
|
|
// post-pass leaves no stranded placeholders for resolvable callees.
|
|
func TestMaterializeDataflowParams_NoStrandedPlaceholders(t *testing.T) {
|
|
src := `package main
|
|
|
|
func A(x int) int { return x }
|
|
func B(y int) int { return y }
|
|
|
|
func Driver(z int) int {
|
|
return A(B(z))
|
|
}
|
|
`
|
|
g := indexAll(t, src)
|
|
|
|
// All arg_of edges that reference resolved callees should have
|
|
// been lifted to a param-node target.
|
|
driverID := findFuncID(t, g, "Driver")
|
|
args := findEdges(g, graph.EdgeArgOf, func(e *graph.Edge) bool {
|
|
return e.From == driverID+"#param:z" || e.From == ""
|
|
})
|
|
for _, e := range args {
|
|
if !strings.Contains(e.To, "#param:") {
|
|
t.Errorf("expected arg_of.To to contain #param:; got %q", e.To)
|
|
}
|
|
}
|
|
}
|
|
|
|
func findFuncID(t *testing.T, g graph.Store, name string) string {
|
|
t.Helper()
|
|
candidates := g.FindNodesByName(name)
|
|
for _, n := range candidates {
|
|
if n.Kind == graph.KindFunction || n.Kind == graph.KindMethod {
|
|
return n.ID
|
|
}
|
|
}
|
|
t.Fatalf("function %q not found", name)
|
|
return ""
|
|
}
|
|
|
|
func dumpAllEdges(g graph.Store) string {
|
|
var b strings.Builder
|
|
for _, e := range g.AllEdges() {
|
|
b.WriteString(string(e.Kind))
|
|
b.WriteString(" ")
|
|
b.WriteString(e.From)
|
|
b.WriteString(" -> ")
|
|
b.WriteString(e.To)
|
|
b.WriteString("\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func dumpEdges(edges []*graph.Edge) string {
|
|
var b strings.Builder
|
|
for i, e := range edges {
|
|
if i > 0 {
|
|
b.WriteString(" | ")
|
|
}
|
|
b.WriteString(string(e.Kind))
|
|
b.WriteString(" ")
|
|
b.WriteString(e.From)
|
|
b.WriteString(" -> ")
|
|
b.WriteString(e.To)
|
|
}
|
|
return b.String()
|
|
}
|