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

467 lines
14 KiB
Go

package goanalysis
import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// stdlibCallerFixture writes a one-package module that calls fmt.Println
// (a top-level stdlib func) and uses fmt.Stringer (a stdlib interface).
// Drives the externals attribution tests.
func stdlibCallerFixture(t *testing.T, root string) {
t.Helper()
writeGoMod(t, root, "example.com/stdlib")
writeFile(t, root, "main.go", `package main
import "fmt"
type Banner struct{}
func (b Banner) String() string { return "hi" }
func Greet() {
fmt.Println("hello")
var _ fmt.Stringer = Banner{}
}
`)
}
func TestExternals_AttributesStdlibCallToModuleNode(t *testing.T) {
root := resolvedTempDir(t)
stdlibCallerFixture(t, root)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Greet", Kind: graph.KindFunction, Name: "Greet",
FilePath: "main.go", StartLine: 9, EndLine: 12, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner", Kind: graph.KindType, Name: "Banner",
FilePath: "main.go", StartLine: 5, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner.String", Kind: graph.KindMethod, Name: "String",
FilePath: "main.go", StartLine: 7, EndLine: 7, Language: "go",
})
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
// fmt.Println landed as ext::go:fmt::Println.
println := g.GetNode("ext::go:fmt::Println")
require.NotNil(t, println, "expected synthetic node for fmt.Println")
assert.Equal(t, graph.KindFunction, println.Kind)
assert.Equal(t, "Println", println.Name)
assert.Equal(t, "go", println.Language)
require.NotNil(t, println.Meta)
assert.True(t, println.Meta["external"].(bool))
assert.Equal(t, "stdlib", println.Meta["module_kind"])
assert.Equal(t, "fmt", println.Meta["import_path"])
assert.Equal(t, "go-types", println.Meta["semantic_source"])
sig, _ := println.Meta["signature"].(string)
assert.Contains(t, sig, "Println")
// Stdlib module node materialised exactly once.
stdlib := g.GetNode("module::go:stdlib")
require.NotNil(t, stdlib, "expected synthetic stdlib module node")
assert.Equal(t, graph.KindModule, stdlib.Kind)
assert.Equal(t, "stdlib", stdlib.Meta["module_kind"])
// Greet → fmt.Println via EdgeCalls at lsp_resolved.
var callEdge *graph.Edge
for _, e := range g.GetOutEdges("main.go::Greet") {
if e.To == "ext::go:fmt::Println" && e.Kind == graph.EdgeCalls {
callEdge = e
break
}
}
require.NotNil(t, callEdge, "expected EdgeCalls Greet → ext::go:fmt::Println")
assert.Equal(t, graph.OriginLSPResolved, callEdge.Origin)
assert.Equal(t, "go-types", callEdge.Meta["semantic_source"])
// External symbol → module via EdgeDependsOnModule.
var modEdge *graph.Edge
for _, e := range g.GetOutEdges("ext::go:fmt::Println") {
if e.To == "module::go:stdlib" && e.Kind == graph.EdgeDependsOnModule {
modEdge = e
break
}
}
require.NotNil(t, modEdge, "external symbol must link to its module via EdgeDependsOnModule")
assert.Equal(t, graph.OriginLSPResolved, modEdge.Origin)
// fmt.Stringer landed as a KindInterface external node.
stringer := g.GetNode("ext::go:fmt::Stringer")
require.NotNil(t, stringer, "expected synthetic node for fmt.Stringer")
assert.Equal(t, graph.KindInterface, stringer.Kind)
}
func TestExternals_DepInModuleCacheGetsModuleNode(t *testing.T) {
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/depcaller")
// We rely on x/tools being in the module cache for the host machine —
// it's an indirect dep of the gortex module, so go/packages will find
// it. The fixture imports a tiny well-known type to keep the test
// resilient to API shifts.
writeFile(t, root, "go.mod", `module example.com/depcaller
go 1.21
require golang.org/x/tools v0.20.0
`)
writeFile(t, root, "main.go", `package main
import "golang.org/x/tools/go/packages"
func Use() *packages.Config {
return &packages.Config{}
}
`)
// Skip cleanly when module cache isn't populated for this dep.
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Use", Kind: graph.KindFunction, Name: "Use",
FilePath: "main.go", StartLine: 5, EndLine: 7, Language: "go",
})
p := newTestProvider(t)
res, err := p.Enrich(g, root)
if err != nil {
t.Skipf("dep not in module cache (CI without GOFLAGS=-mod=mod cache): %v", err)
}
require.NotNil(t, res)
var modulePathPrefix = "module::go:golang.org/x/tools"
var foundModule *graph.Node
for _, n := range g.AllNodes() {
if n.Kind != graph.KindModule {
continue
}
if strings.HasPrefix(n.ID, modulePathPrefix) {
foundModule = n
break
}
}
if foundModule == nil {
t.Skipf("x/tools not loaded with module info — host environment probably uses GOFLAGS=-mod=vendor")
}
assert.Equal(t, "module_cache", foundModule.Meta["module_kind"])
assert.Equal(t, "go", foundModule.Language)
// Use → some ext::go:golang.org/x/tools/go/packages::Config edge.
var edge *graph.Edge
for _, e := range g.GetOutEdges("main.go::Use") {
if strings.HasPrefix(e.To, "ext::go:golang.org/x/tools/go/packages::") {
edge = e
break
}
}
require.NotNil(t, edge, "expected an edge from Use to a packages.* external node")
assert.Equal(t, graph.OriginLSPResolved, edge.Origin)
}
func TestExternals_ClaimsResolverStubEdge(t *testing.T) {
root := resolvedTempDir(t)
stdlibCallerFixture(t, root)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Greet", Kind: graph.KindFunction, Name: "Greet",
FilePath: "main.go", StartLine: 9, EndLine: 12, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner", Kind: graph.KindType, Name: "Banner",
FilePath: "main.go", StartLine: 5, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner.String", Kind: graph.KindMethod, Name: "String",
FilePath: "main.go", StartLine: 7, EndLine: 7, Language: "go",
})
// Pre-seed the resolver-shaped stub edge that the parser+resolver
// would have written before externals attribution landed.
// Goanalysis must rewrite this edge to point at the real ext::
// node rather than leaving a duplicate.
stubEdge := &graph.Edge{
From: "main.go::Greet", To: "stdlib::fmt::Println", Kind: graph.EdgeCalls,
FilePath: "main.go", Line: 10,
Confidence: 0.0, ConfidenceLabel: "INFERRED",
Origin: graph.OriginTextMatched,
}
g.AddEdge(stubEdge)
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
// The original stub bucket must drain; the new bucket must hold the edge.
stubInBucket := false
for _, e := range g.GetInEdges("stdlib::fmt::Println") {
_ = e
stubInBucket = true
}
assert.False(t, stubInBucket, "stub bucket stdlib::fmt::Println must be empty after externals upgrade")
var realEdge *graph.Edge
for _, e := range g.GetOutEdges("main.go::Greet") {
if e.To == "ext::go:fmt::Println" && e.Kind == graph.EdgeCalls {
realEdge = e
break
}
}
require.NotNil(t, realEdge)
assert.Equal(t, graph.OriginLSPResolved, realEdge.Origin, "claimed stub edges must be promoted to lsp_resolved")
assert.Equal(t, 1.0, realEdge.Confidence)
require.NotNil(t, realEdge.Meta)
assert.Equal(t, "go-types", realEdge.Meta["semantic_source"])
// And the stub edge identity (same pointer) must now point at the real node.
assert.Same(t, stubEdge, realEdge, "claim should mutate the existing edge struct, not duplicate it")
}
func TestExternals_ClaimsFuzzyStubByLineAndName(t *testing.T) {
// The parser emits `unresolved::*.Method` for method calls on
// external receivers it can't classify (e.g. `os.Stdout.Write`).
// The fuzzy claim path matches by (line, trailing-name) so the
// stub doesn't survive alongside the real ext::go: node.
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/methodcall")
writeFile(t, root, "main.go", `package main
import "os"
func Write() {
os.Stdout.Write([]byte("x"))
}
`)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Write", Kind: graph.KindFunction, Name: "Write",
FilePath: "main.go", StartLine: 5, EndLine: 7, Language: "go",
})
stubEdge := &graph.Edge{
From: "main.go::Write", To: "unresolved::*.Write", Kind: graph.EdgeCalls,
FilePath: "main.go", Line: 6,
Confidence: 0.0, ConfidenceLabel: "INFERRED",
Origin: graph.OriginTextMatched,
}
g.AddEdge(stubEdge)
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
// stub bucket drained — fuzzy claim should have rewritten the edge.
stubInBucket := false
for _, e := range g.GetInEdges("unresolved::*.Write") {
_ = e
stubInBucket = true
}
assert.False(t, stubInBucket, "fuzzy claim must drain the unresolved::*.Write bucket")
// And the real ext::go:os::File.Write node holds the call.
realNode := g.GetNode("ext::go:os::File.Write")
require.NotNil(t, realNode, "expected ext::go:os::File.Write to be created")
assert.Equal(t, graph.KindMethod, realNode.Kind)
assert.Equal(t, "File", realNode.Meta["receiver"])
var realEdge *graph.Edge
for _, e := range g.GetOutEdges("main.go::Write") {
if e.To == "ext::go:os::File.Write" && e.Kind == graph.EdgeCalls {
realEdge = e
break
}
}
require.NotNil(t, realEdge, "expected EdgeCalls Write → ext::go:os::File.Write")
assert.Equal(t, graph.OriginLSPResolved, realEdge.Origin)
assert.Same(t, stubEdge, realEdge, "fuzzy claim should mutate the existing edge struct")
}
func TestExternals_FuzzyClaimDoesNotMatchAcrossLines(t *testing.T) {
// Conservatism check: if a stub edge is at a different line, the
// fuzzy pass must NOT claim it — that would clobber unrelated
// outbound calls that happen to share a name.
root := resolvedTempDir(t)
stdlibCallerFixture(t, root)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Greet", Kind: graph.KindFunction, Name: "Greet",
FilePath: "main.go", StartLine: 9, EndLine: 12, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner", Kind: graph.KindType, Name: "Banner",
FilePath: "main.go", StartLine: 5, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner.String", Kind: graph.KindMethod, Name: "String",
FilePath: "main.go", StartLine: 7, EndLine: 7, Language: "go",
})
// Stub on a wildly-wrong line (line 99) — must survive untouched.
wrongLineStub := &graph.Edge{
From: "main.go::Greet", To: "unresolved::*.Println", Kind: graph.EdgeCalls,
FilePath: "main.go", Line: 99,
}
g.AddEdge(wrongLineStub)
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
// The off-line stub must still be in its bucket — we didn't claim it.
stillThere := false
for _, e := range g.GetInEdges("unresolved::*.Println") {
_ = e
stillThere = true
}
assert.True(t, stillThere, "fuzzy claim must not match across lines")
}
func TestExternals_SkipsBuiltinsAndUniverseScope(t *testing.T) {
root := resolvedTempDir(t)
writeGoMod(t, root, "example.com/builtins")
writeFile(t, root, "main.go", `package main
func Loop(xs []int) int {
n := 0
for _, x := range xs {
n += x
}
return len(xs) + n
}
`)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Loop", Kind: graph.KindFunction, Name: "Loop",
FilePath: "main.go", StartLine: 3, EndLine: 9, Language: "go",
})
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
// `int` and `len` are universe-scope objects (Pkg() == nil). They must
// not become external nodes — that would be noise.
for _, n := range g.AllNodes() {
if n.Kind == graph.KindModule {
continue
}
if !strings.HasPrefix(n.ID, "ext::") {
continue
}
t.Fatalf("unexpected external node from universe-scope obj: %s", n.ID)
}
}
func TestExternals_IdempotentOnReEnrich(t *testing.T) {
root := resolvedTempDir(t)
stdlibCallerFixture(t, root)
g := graph.New()
g.AddNode(&graph.Node{
ID: "main.go::Greet", Kind: graph.KindFunction, Name: "Greet",
FilePath: "main.go", StartLine: 9, EndLine: 12, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner", Kind: graph.KindType, Name: "Banner",
FilePath: "main.go", StartLine: 5, EndLine: 5, Language: "go",
})
g.AddNode(&graph.Node{
ID: "main.go::Banner.String", Kind: graph.KindMethod, Name: "String",
FilePath: "main.go", StartLine: 7, EndLine: 7, Language: "go",
})
p := newTestProvider(t)
_, err := p.Enrich(g, root)
require.NoError(t, err)
_, err = p.Enrich(g, root)
require.NoError(t, err)
// Count Greet → fmt.Println edges. Re-running Enrich must not
// duplicate them; the second pass should confirm the existing edge
// in place.
matchingEdges := 0
for _, e := range g.GetOutEdges("main.go::Greet") {
if e.To == "ext::go:fmt::Println" && e.Kind == graph.EdgeCalls {
matchingEdges++
}
}
assert.Equal(t, 1, matchingEdges, "re-enrichment must not duplicate the external call edge")
// One stdlib module node only.
count := 0
for _, n := range g.AllNodes() {
if n.ID == "module::go:stdlib" {
count++
}
}
assert.Equal(t, 1, count)
}
func TestExternals_GoModuleNodeID(t *testing.T) {
tests := []struct {
path string
version string
want string
}{
{"github.com/foo/bar", "v1.2.3", "module::go:github.com/foo/bar@v1.2.3"},
{"github.com/foo/bar", "", "module::go:github.com/foo/bar"},
{"stdlib", "", "module::go:stdlib"},
}
for _, tt := range tests {
assert.Equal(t, tt.want, goModuleNodeID(tt.path, tt.version))
}
}
func TestExternals_ShortModulePath(t *testing.T) {
tests := map[string]string{
"": "",
"fmt": "fmt",
"github.com/foo/bar": "bar",
"github.com/foo/bar/v2": "bar",
"github.com/foo/bar/v10": "bar",
"github.com/foo/bar/sub": "sub",
"single": "single",
"vibrant": "vibrant", // not a major-version segment
}
keys := make([]string, 0, len(tests))
for k := range tests {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
assert.Equal(t, tests[k], shortModulePath(k), "input=%q", k)
}
}
func TestExternals_IsMajorVersion(t *testing.T) {
cases := map[string]bool{
"": false,
"v": false,
"v0": true,
"v1": true,
"v999": true,
"v1.0": false, // dots are not part of the suffix
"v1a": false,
"vibrant": false, // first letter v but rest non-digit
"version1": false,
}
keys := make([]string, 0, len(cases))
for k := range cases {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
assert.Equal(t, cases[k], isMajorVersion(k), "input=%q", k)
}
}