Files
zzet--gortex/internal/parser/languages/go_function_shape_test.go
T
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

313 lines
7.9 KiB
Go

package languages
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
// runGoExtract is a small harness used by the function-shape tests
// — wraps NewGoExtractor().Extract and returns the result so tests
// can assert directly against nodes/edges.
func runGoExtract(t *testing.T, src string) *extractedFixture {
t.Helper()
ext := NewGoExtractor()
result, err := ext.Extract("pkg/foo.go", []byte(src))
if err != nil {
t.Fatalf("extract: %v", err)
}
fix := &extractedFixture{
nodesByID: map[string]*graph.Node{},
nodesByKind: map[graph.NodeKind][]*graph.Node{},
edgesByKind: map[graph.EdgeKind][]*graph.Edge{},
edgesByOwner: map[string][]*graph.Edge{},
}
for _, n := range result.Nodes {
fix.nodesByID[n.ID] = n
fix.nodesByKind[n.Kind] = append(fix.nodesByKind[n.Kind], n)
}
for _, e := range result.Edges {
fix.edgesByKind[e.Kind] = append(fix.edgesByKind[e.Kind], e)
fix.edgesByOwner[e.From] = append(fix.edgesByOwner[e.From], e)
}
return fix
}
// runGoExtractWithEnvHelpers is runGoExtract with a per-repo corporate
// env-helper allow-list installed on the extractor first — exercises the
// config-driven promotion path.
func runGoExtractWithEnvHelpers(t *testing.T, src string, envHelpers []string) *extractedFixture {
t.Helper()
ext := NewGoExtractor()
ext.SetEnvHelperNames(envHelpers)
result, err := ext.Extract("pkg/foo.go", []byte(src))
if err != nil {
t.Fatalf("extract: %v", err)
}
fix := &extractedFixture{
nodesByID: map[string]*graph.Node{},
nodesByKind: map[graph.NodeKind][]*graph.Node{},
edgesByKind: map[graph.EdgeKind][]*graph.Edge{},
edgesByOwner: map[string][]*graph.Edge{},
}
for _, n := range result.Nodes {
fix.nodesByID[n.ID] = n
fix.nodesByKind[n.Kind] = append(fix.nodesByKind[n.Kind], n)
}
for _, e := range result.Edges {
fix.edgesByKind[e.Kind] = append(fix.edgesByKind[e.Kind], e)
fix.edgesByOwner[e.From] = append(fix.edgesByOwner[e.From], e)
}
return fix
}
type extractedFixture struct {
nodesByID map[string]*graph.Node
nodesByKind map[graph.NodeKind][]*graph.Node
edgesByKind map[graph.EdgeKind][]*graph.Edge
edgesByOwner map[string][]*graph.Edge
}
func TestGoFunctionShape_ParamsAndReturns(t *testing.T) {
src := `package foo
func Add(a, b int) (int, error) {
return a + b, nil
}
`
fix := runGoExtract(t, src)
params := fix.nodesByKind[graph.KindParam]
if len(params) != 2 {
t.Fatalf("expected 2 params, got %d: %+v", len(params), params)
}
wantNames := map[string]bool{"a": false, "b": false}
for _, p := range params {
if _, ok := wantNames[p.Name]; !ok {
t.Errorf("unexpected param name %q", p.Name)
continue
}
wantNames[p.Name] = true
if p.Meta["type"] != "int" {
t.Errorf("param %q: type meta = %v, want int", p.Name, p.Meta["type"])
}
}
for n, seen := range wantNames {
if !seen {
t.Errorf("missing param %q", n)
}
}
paramOf := fix.edgesByKind[graph.EdgeParamOf]
if len(paramOf) != 2 {
t.Errorf("EdgeParamOf count = %d, want 2", len(paramOf))
}
for _, e := range paramOf {
if e.To != "pkg/foo.go::Add" {
t.Errorf("param edge target = %q, want owner", e.To)
}
}
typedAs := fix.edgesByKind[graph.EdgeTypedAs]
if len(typedAs) != 2 {
t.Errorf("EdgeTypedAs count = %d, want 2", len(typedAs))
}
for _, e := range typedAs {
if e.To != "unresolved::int" {
t.Errorf("typed_as target = %q", e.To)
}
}
returns := fix.edgesByKind[graph.EdgeReturns]
if len(returns) != 2 {
t.Fatalf("EdgeReturns count = %d, want 2 (int + error)", len(returns))
}
gotTargets := map[string]bool{}
for _, e := range returns {
gotTargets[e.To] = true
if e.From != "pkg/foo.go::Add" {
t.Errorf("return edge from = %q", e.From)
}
}
if !gotTargets["unresolved::int"] || !gotTargets["external::error"] {
t.Errorf("return targets wrong: %v", gotTargets)
}
}
func TestGoFunctionShape_GenericParams(t *testing.T) {
src := `package foo
func Map[T any, U comparable](in []T) []U {
return nil
}
`
fix := runGoExtract(t, src)
gens := fix.nodesByKind[graph.KindGenericParam]
if len(gens) != 2 {
t.Fatalf("expected 2 generic params, got %d: %+v", len(gens), gens)
}
want := map[string]string{"T": "any", "U": "comparable"}
for _, g := range gens {
bound, _ := g.Meta["bound"].(string)
if want[g.Name] != bound {
t.Errorf("generic %q bound = %q, want %q", g.Name, bound, want[g.Name])
}
}
}
func TestGoFunctionShape_ClosureCaptures(t *testing.T) {
src := `package foo
func Run(prefix string) func(string) string {
count := 0
return func(s string) string {
count++
return prefix + s
}
}
`
fix := runGoExtract(t, src)
captures := fix.edgesByKind[graph.EdgeCaptures]
captured := map[string]bool{}
for _, e := range captures {
if name, _ := e.Meta["name"].(string); name != "" {
captured[name] = true
}
}
if !captured["count"] {
t.Errorf("EdgeCaptures missing for 'count'; got %v", captured)
}
if !captured["prefix"] {
t.Errorf("EdgeCaptures missing for 'prefix'; got %v", captured)
}
// 's' is a parameter of the closure, not a capture.
if captured["s"] {
t.Errorf("'s' is a closure parameter, should not be captured")
}
}
func TestGoFunctionShape_ClosureLocalsShadowOuter(t *testing.T) {
src := `package foo
func Outer() {
v := 1
go func() {
v := 99
_ = v
}()
}
`
fix := runGoExtract(t, src)
captures := fix.edgesByKind[graph.EdgeCaptures]
for _, e := range captures {
if name, _ := e.Meta["name"].(string); name == "v" {
// The closure re-declares v with `:=`. Inside the
// closure body, `v` resolves to the inner binding,
// so EdgeCaptures should NOT be emitted for v.
t.Errorf("EdgeCaptures emitted for shadowed local 'v'")
}
}
}
func TestGoFunctionShape_ClosureNoCaptureWithoutOuterRefs(t *testing.T) {
src := `package foo
func Run() {
go func() {
x := 1
_ = x
}()
}
`
fix := runGoExtract(t, src)
captures := fix.edgesByKind[graph.EdgeCaptures]
if len(captures) != 0 {
names := []string{}
for _, e := range captures {
n, _ := e.Meta["name"].(string)
names = append(names, n)
}
t.Errorf("expected no EdgeCaptures; got %v", names)
}
}
func TestGoFunctionShape_Closure(t *testing.T) {
src := `package foo
func Run() {
go func() {
println("hi")
}()
}
`
fix := runGoExtract(t, src)
closures := fix.nodesByKind[graph.KindClosure]
if len(closures) != 1 {
t.Fatalf("expected 1 closure, got %d: %+v", len(closures), closures)
}
c := closures[0]
if c.Meta == nil && c.Name == "" {
t.Errorf("closure name empty")
}
memberOf := fix.edgesByKind[graph.EdgeMemberOf]
hasClosureLink := false
for _, e := range memberOf {
if e.From == c.ID && e.To == "pkg/foo.go::Run" {
hasClosureLink = true
}
}
if !hasClosureLink {
t.Errorf("closure has no member_of link to enclosing function")
}
}
func TestGoFunctionShape_MethodReceiverShape(t *testing.T) {
src := `package foo
type Server struct{}
func (s *Server) Handle(req string) error {
return nil
}
`
fix := runGoExtract(t, src)
params := fix.nodesByKind[graph.KindParam]
if len(params) != 1 || params[0].Name != "req" {
t.Fatalf("expected single 'req' param, got %+v", params)
}
owner := "pkg/foo.go::Server.Handle"
for _, e := range fix.edgesByKind[graph.EdgeParamOf] {
if e.From != params[0].ID || e.To != owner {
t.Errorf("param edge wrong: %s -> %s", e.From, e.To)
}
}
returnEdges := fix.edgesByKind[graph.EdgeReturns]
if len(returnEdges) != 1 || returnEdges[0].To != "external::error" {
t.Errorf("return edges wrong: %+v", returnEdges)
}
}
func TestGoFunctionShape_VariadicAndBlankIdentifier(t *testing.T) {
src := `package foo
func Log(_ string, args ...any) {}
`
fix := runGoExtract(t, src)
params := fix.nodesByKind[graph.KindParam]
if len(params) != 1 {
t.Fatalf("expected only 'args' (blank identifier skipped), got %+v", params)
}
if params[0].Name != "args" {
t.Errorf("param name = %q", params[0].Name)
}
if v, _ := params[0].Meta["variadic"].(bool); !v {
t.Errorf("variadic flag missing on args param")
}
}