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

410 lines
11 KiB
Go

package goanalysis
import (
"go/token"
"go/types"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/graph"
)
// resolvedTempDir wraps t.TempDir() with EvalSymlinks because on macOS
// t.TempDir() returns /var/folders/... while go/packages reports paths
// as /private/var/folders/... — the relativePath() prefix check would
// otherwise drop every file.
func resolvedTempDir(t *testing.T) string {
t.Helper()
dir, err := filepath.EvalSymlinks(t.TempDir())
require.NoError(t, err)
return dir
}
func writeFile(t *testing.T, dir, rel, content string) {
t.Helper()
full := filepath.Join(dir, rel)
require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
require.NoError(t, os.WriteFile(full, []byte(content), 0o644))
}
func writeGoMod(t *testing.T, dir, module string) {
t.Helper()
writeFile(t, dir, "go.mod", "module "+module+"\n\ngo 1.21\n")
}
// twoPackageCallerFixture writes a 2-package module where pkg/b/b.go calls
// pkg/a/a.go's Hello function. Used by the call-edge tests.
func twoPackageCallerFixture(t *testing.T, root string) {
t.Helper()
writeGoMod(t, root, "example.com/test")
writeFile(t, root, "pkg/a/a.go", `package a
func Hello() string {
return "hi"
}
`)
writeFile(t, root, "pkg/b/b.go", `package b
import "example.com/test/pkg/a"
func Caller() string {
return a.Hello()
}
`)
}
func newTestProvider(t *testing.T) *Provider {
t.Helper()
return NewProvider(ModeTypeCheck, false, zap.NewNop())
}
func TestGoAnalysis_Available(t *testing.T) {
p := newTestProvider(t)
assert.True(t, p.Available(), "go toolchain should be on PATH in tests")
assert.Equal(t, "go-types", p.Name())
assert.Equal(t, []string{"go"}, p.Languages())
}
func TestGoAnalysis_RelativePath(t *testing.T) {
tests := []struct {
name string
absPath string
root string
want string
}{
{"inside root", "/repo/pkg/a/a.go", "/repo", "pkg/a/a.go"},
{"at root", "/repo/main.go", "/repo", "main.go"},
{"outside root returns empty", "/elsewhere/foo.go", "/repo", ""},
{"empty repo", "/repo/main.go", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := relativePath(tt.absPath, tt.root)
assert.Equal(t, tt.want, got)
})
}
}
func TestGoAnalysis_FindContainingFunc_PicksSmallest(t *testing.T) {
g := graph.New()
// Outer function spans lines 10-30.
g.AddNode(&graph.Node{
ID: "main.go::Outer", Kind: graph.KindFunction, Name: "Outer",
FilePath: "main.go", StartLine: 10, EndLine: 30, Language: "go",
})
// Inner method spans lines 15-20 (smaller, should win for line 17).
g.AddNode(&graph.Node{
ID: "main.go::Inner", Kind: graph.KindMethod, Name: "Inner",
FilePath: "main.go", StartLine: 15, EndLine: 20, Language: "go",
})
pos := token.Position{Filename: "/repo/main.go", Line: 17}
got := findContainingFunc(g, nil, nil, "/repo", pos)
require.NotNil(t, got)
assert.Equal(t, "main.go::Inner", got.ID)
// Line 25 is inside Outer only.
pos.Line = 25
got = findContainingFunc(g, nil, nil, "/repo", pos)
require.NotNil(t, got)
assert.Equal(t, "main.go::Outer", got.ID)
// Line 5 is in neither.
pos.Line = 5
got = findContainingFunc(g, nil, nil, "/repo", pos)
assert.Nil(t, got)
}
func TestGoAnalysis_InferEdgeKindFromObj(t *testing.T) {
// Construct minimal go/types objects via a real package load so we get
// real *types.Func / *types.TypeName / *types.Var / *types.Const values.
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/kindtest")
writeFile(t, root, "main.go", `package main
const Pi = 3.14
var Counter int
type Widget struct{}
func Make() *Widget {
return &Widget{}
}
`)
p := newTestProvider(t)
pkgs, _, err := p.loadPackages(root)
require.NoError(t, err)
require.NotEmpty(t, pkgs)
var pi, counter, widget, make_ types.Object
for _, pkg := range pkgs {
if pkg.TypesInfo == nil {
continue
}
for _, obj := range pkg.TypesInfo.Defs {
if obj == nil {
continue
}
switch obj.Name() {
case "Pi":
pi = obj
case "Counter":
counter = obj
case "Widget":
widget = obj
case "Make":
make_ = obj
}
}
}
require.NotNil(t, pi)
require.NotNil(t, counter)
require.NotNil(t, widget)
require.NotNil(t, make_)
assert.Equal(t, graph.EdgeCalls, inferEdgeKindFromObj(make_))
assert.Equal(t, graph.EdgeReferences, inferEdgeKindFromObj(widget))
assert.Equal(t, graph.EdgeReferences, inferEdgeKindFromObj(counter))
assert.Equal(t, graph.EdgeReferences, inferEdgeKindFromObj(pi))
}
func TestGoAnalysis_LoadPackages_Smoke(t *testing.T) {
root := resolvedTempDir(t)
twoPackageCallerFixture(t, root)
p := newTestProvider(t)
pkgs, fset, err := p.loadPackages(root)
require.NoError(t, err)
require.NotEmpty(t, pkgs)
require.NotNil(t, fset)
var sawA, sawB bool
for _, pkg := range pkgs {
require.NotNil(t, pkg.TypesInfo, "every returned package must have TypesInfo")
switch pkg.PkgPath {
case "example.com/test/pkg/a":
sawA = true
case "example.com/test/pkg/b":
sawB = true
}
}
assert.True(t, sawA, "expected pkg a to be loaded")
assert.True(t, sawB, "expected pkg b to be loaded")
}
func TestGoAnalysis_ConfirmsCallEdge(t *testing.T) {
root := resolvedTempDir(t)
twoPackageCallerFixture(t, root)
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/a/a.go::Hello", Kind: graph.KindFunction, Name: "Hello",
FilePath: "pkg/a/a.go", StartLine: 3, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "pkg/b/b.go::Caller", Kind: graph.KindFunction, Name: "Caller",
FilePath: "pkg/b/b.go", StartLine: 5, EndLine: 7, Language: "go",
})
// Pre-seed an INFERRED call edge that go-types should confirm.
g.AddEdge(&graph.Edge{
From: "pkg/b/b.go::Caller", To: "pkg/a/a.go::Hello", Kind: graph.EdgeCalls,
Confidence: 0.7, ConfidenceLabel: "INFERRED",
Origin: graph.OriginASTInferred,
})
p := newTestProvider(t)
result, err := p.Enrich(g, root)
require.NoError(t, err)
require.NotNil(t, result)
assert.Greater(t, result.SymbolsCovered, 0, "should map at least Hello and Caller")
assert.GreaterOrEqual(t, result.EdgesConfirmed, 1, "INFERRED call edge should be confirmed")
// Verify the edge was upgraded in place.
edges := g.GetOutEdges("pkg/b/b.go::Caller")
require.NotEmpty(t, edges)
var confirmed *graph.Edge
for _, e := range edges {
if e.To == "pkg/a/a.go::Hello" && e.Kind == graph.EdgeCalls {
confirmed = e
break
}
}
require.NotNil(t, confirmed, "expected the call edge to still exist")
assert.Equal(t, 1.0, confirmed.Confidence)
assert.Equal(t, "EXTRACTED", confirmed.ConfidenceLabel)
assert.Equal(t, graph.OriginLSPResolved, confirmed.Origin,
"call edges resolved by go/types should land at lsp_resolved")
require.NotNil(t, confirmed.Meta)
assert.Equal(t, "go-types", confirmed.Meta["semantic_source"])
}
func TestGoAnalysis_AddsMissingCallEdge(t *testing.T) {
root := resolvedTempDir(t)
twoPackageCallerFixture(t, root)
g := graph.New()
g.AddNode(&graph.Node{
ID: "pkg/a/a.go::Hello", Kind: graph.KindFunction, Name: "Hello",
FilePath: "pkg/a/a.go", StartLine: 3, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "pkg/b/b.go::Caller", Kind: graph.KindFunction, Name: "Caller",
FilePath: "pkg/b/b.go", StartLine: 5, EndLine: 7, Language: "go",
})
// No pre-seeded edge — the provider must discover the call.
p := newTestProvider(t)
result, err := p.Enrich(g, root)
require.NoError(t, err)
require.GreaterOrEqual(t, result.EdgesAdded, 1, "missing call edge should be added")
edges := g.GetOutEdges("pkg/b/b.go::Caller")
var added *graph.Edge
for _, e := range edges {
if e.To == "pkg/a/a.go::Hello" && e.Kind == graph.EdgeCalls {
added = e
break
}
}
require.NotNil(t, added, "expected a new call edge to be added")
assert.Equal(t, 1.0, added.Confidence)
assert.Equal(t, graph.OriginLSPResolved, added.Origin)
require.NotNil(t, added.Meta)
assert.Equal(t, "go-types", added.Meta["semantic_source"])
}
func TestGoAnalysis_DetectsInterfaceImplementation(t *testing.T) {
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/iface")
writeFile(t, root, "main.go", `package main
type Greeter interface {
Greet() string
}
type EnglishGreeter struct{}
func (e EnglishGreeter) Greet() string {
return "hello"
}
`)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Greeter", Kind: graph.KindInterface, Name: "Greeter",
FilePath: "main.go", StartLine: 3, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::EnglishGreeter", Kind: graph.KindType, Name: "EnglishGreeter",
FilePath: "main.go", StartLine: 7, EndLine: 7, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::EnglishGreeter.Greet", Kind: graph.KindMethod, Name: "Greet",
FilePath: "main.go", StartLine: 9, EndLine: 11, Language: "go",
})
p := newTestProvider(t)
result, err := p.Enrich(g, root)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.EdgesAdded, 1, "implements edge should be added")
edges := g.GetOutEdges("main.go::EnglishGreeter")
var impl *graph.Edge
for _, e := range edges {
if e.To == "main.go::Greeter" && e.Kind == graph.EdgeImplements {
impl = e
break
}
}
require.NotNil(t, impl, "expected EdgeImplements from EnglishGreeter to Greeter")
assert.Equal(t, 1.0, impl.Confidence)
assert.Equal(t, graph.OriginLSPDispatch, impl.Origin,
"implements edges should land at lsp_dispatch (one step from literal target)")
}
func TestGoAnalysis_NoFalseImplementsForUnrelatedTypes(t *testing.T) {
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/unrelated")
writeFile(t, root, "main.go", `package main
type Reader interface {
Read() string
}
type Counter struct{}
func (c Counter) Increment() int {
return 1
}
`)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Reader", Kind: graph.KindInterface, Name: "Reader",
FilePath: "main.go", StartLine: 3, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Counter", Kind: graph.KindType, Name: "Counter",
FilePath: "main.go", StartLine: 7, EndLine: 7, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Counter.Increment", Kind: graph.KindMethod, Name: "Increment",
FilePath: "main.go", StartLine: 9, EndLine: 11, Language: "go",
})
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
for _, e := range g.GetOutEdges("main.go::Counter") {
if e.Kind == graph.EdgeImplements && e.To == "main.go::Reader" {
t.Fatalf("Counter does not implement Reader; provider must not synthesize this edge")
}
}
}
func TestGoAnalysis_EnrichesNodeMeta(t *testing.T) {
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/meta")
writeFile(t, root, "main.go", `package main
func F() (int, error) {
return 0, nil
}
`)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::F", Kind: graph.KindFunction, Name: "F",
FilePath: "main.go", StartLine: 3, EndLine: 5, Language: "go",
})
p := newTestProvider(t)
result, err := p.Enrich(g, root)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.NodesEnriched, 1)
node := g.GetNode("main.go::F")
require.NotNil(t, node)
require.NotNil(t, node.Meta)
semType, ok := node.Meta["semantic_type"].(string)
require.True(t, ok, "semantic_type should be populated as a string")
assert.Contains(t, semType, "func", "semantic_type should describe a function type")
retType, ok := node.Meta["return_type"].(string)
require.True(t, ok, "return_type should be populated for funcs")
assert.Contains(t, retType, "int")
assert.Contains(t, retType, "error")
assert.Equal(t, "go-types", node.Meta["semantic_source"])
}