项目文件夹

文件
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

374 行
9.6 KiB
Go

package contracts
import (
"testing"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
gosrc "github.com/zzet/gortex/internal/parser/tsitter/golang"
)
// makeGoBodyFacts is a test helper: parses Go source via tree-sitter
// and constructs a goBodyFacts for the function whose name matches.
// Returns the BodyFacts and a cleanup that releases the tree.
func makeGoBodyFacts(t *testing.T, src string, funcName string, startLine int) (BodyFacts, func()) {
t.Helper()
lang := gosrc.GetLanguage()
tree, err := parser.ParseFile([]byte(src), lang)
if err != nil {
t.Fatalf("parse: %v", err)
}
pt := parser.NewParseTree(tree, []byte(src), "go")
handler := &graph.Node{
ID: "test.go::" + funcName,
Name: funcName,
Kind: graph.KindFunction,
FilePath: "test.go",
StartLine: startLine,
Language: "go",
}
bf := newGoBodyFacts(pt, handler)
return bf, func() { pt.Release() }
}
func TestGoBodyFacts_VarBindingMethodCall(t *testing.T) {
src := `package h
func ListRepos(w http.ResponseWriter, r *http.Request) {
repos, err := h.svc.ListAll()
if err != nil {
WriteJSON(w, http.StatusInternalServerError, err)
return
}
WriteJSON(w, http.StatusOK, repos)
}
`
bf, cleanup := makeGoBodyFacts(t, src, "ListRepos", 3)
defer cleanup()
b := bf.VarBinding("repos")
if b.Kind != BindingMethodCall {
t.Fatalf("repos kind: want method_call, got %q", b.Kind)
}
if b.CallExpr != "h.svc.ListAll" {
t.Fatalf("repos callExpr: want %q, got %q", "h.svc.ListAll", b.CallExpr)
}
}
func TestGoBodyFacts_VarBindingComposite(t *testing.T) {
src := `package h
func H(w, r) {
user := User{Name: "x"}
users := []User{{Name: "y"}, {Name: "z"}}
ptr := &User{}
count := 0
pi := 3.14
flag := true
name := r.PathValue("id")
hdr := r.Header.Get("X-Foo")
q := r.URL.Query().Get("k")
form := r.FormValue("name")
_ = make([]User, 0, 10)
_ = make(map[string]int)
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
cases := []struct {
name string
kind BindingKind
typeID string
repeated bool
pointer bool
}{
{"user", BindingComposite, "User", false, false},
{"users", BindingSliceLit, "User", true, false},
{"ptr", BindingComposite, "User", false, true},
{"count", BindingIntLit, "int", false, false},
{"pi", BindingFloatLit, "float64", false, false},
{"flag", BindingBoolLit, "bool", false, false},
{"name", BindingPathValue, "string", false, false},
{"hdr", BindingHeaderValue, "string", false, false},
{"q", BindingQueryGet, "string", false, false},
{"form", BindingFormValue, "string", false, false},
}
for _, tc := range cases {
b := bf.VarBinding(tc.name)
if b.Kind != tc.kind {
t.Errorf("%s kind: want %q, got %q", tc.name, tc.kind, b.Kind)
}
if b.TypeID != tc.typeID {
t.Errorf("%s typeID: want %q, got %q", tc.name, tc.typeID, b.TypeID)
}
if b.Repeated != tc.repeated {
t.Errorf("%s repeated: want %v, got %v", tc.name, tc.repeated, b.Repeated)
}
if b.Pointer != tc.pointer {
t.Errorf("%s pointer: want %v, got %v", tc.name, tc.pointer, b.Pointer)
}
}
}
func TestGoBodyFacts_ResponseCalls(t *testing.T) {
src := `package h
func H(w, r) {
users := svc.List()
WriteJSON(w, http.StatusOK, users)
c.JSON(http.StatusCreated, payload)
json.NewEncoder(w).Encode(out)
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
calls := bf.ResponseCalls()
if len(calls) < 3 {
t.Fatalf("response calls: want >=3, got %d", len(calls))
}
wj := findCall(calls, "WriteJSON")
if wj == nil {
t.Fatal("missing WriteJSON")
}
if wj.StatusCode != 200 || !wj.StatusKnown {
t.Errorf("WriteJSON status: want 200, got %d (known=%v)", wj.StatusCode, wj.StatusKnown)
}
if wj.ValueExpr != "users" {
t.Errorf("WriteJSON value: want users, got %q", wj.ValueExpr)
}
cj := findCall(calls, "JSON")
if cj == nil {
t.Fatal("missing JSON")
}
if cj.StatusCode != 201 {
t.Errorf("c.JSON status: want 201, got %d", cj.StatusCode)
}
if cj.ValueExpr != "payload" {
t.Errorf("c.JSON value: want payload, got %q", cj.ValueExpr)
}
enc := findCall(calls, "Encode")
if enc == nil {
t.Fatal("missing Encode")
}
if enc.ValueExpr != "out" {
t.Errorf("Encode value: want out, got %q", enc.ValueExpr)
}
}
func TestGoBodyFacts_MapLiteralEntries_NestedSlices(t *testing.T) {
// This is the "[]any{ truncation" bug fixture: a multi-key
// envelope where one value is a nested slice literal containing
// `}` characters that the old splitMapLiteralBody truncated on.
src := `package h
func H(w, r) {
workspaces := svc.Workspaces()
repos := svc.Repos()
WriteJSON(w, http.StatusOK, map[string]any{
"workspaces": workspaces,
"repos": repos,
"flags": []any{true, false, true},
"meta": map[string]int{"a": 1, "b": 2},
})
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
calls := bf.ResponseCalls()
wj := findCall(calls, "WriteJSON")
if wj == nil {
t.Fatal("missing WriteJSON")
}
if wj.ValueArg == nil {
t.Fatal("WriteJSON value node nil")
}
if wj.ValueArg.Kind() != "composite_literal" {
t.Fatalf("envelope kind: want composite_literal, got %q", wj.ValueArg.Kind())
}
entries := bf.MapLiteralEntries(wj.ValueArg)
if len(entries) != 4 {
t.Fatalf("envelope entries: want 4, got %d (%v)", len(entries), entries)
}
keys := []string{}
for _, e := range entries {
keys = append(keys, e.Key)
}
want := []string{"workspaces", "repos", "flags", "meta"}
for i, k := range want {
if i >= len(keys) || keys[i] != k {
t.Errorf("envelope key[%d]: want %q, got %v", i, k, keys)
break
}
}
// Critical: the "flags" value is `[]any{true, false, true}`.
// The old splitMapLiteralBody regex truncated this at the first
// `}` it saw, producing `"expr": "[]any{"`. The AST walk gets the
// full expression.
flags := entries[2]
if flags.ValueExpr != "[]any{true, false, true}" {
t.Errorf("flags expr: want full slice literal, got %q", flags.ValueExpr)
}
}
func TestGoBodyFacts_StatusWrites(t *testing.T) {
src := `package h
func H(w, r) {
w.WriteHeader(http.StatusBadRequest)
w.WriteHeader(404)
w.WriteHeader(http.StatusOK)
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
got := bf.StatusWrites()
want := []int{400, 404, 200}
if len(got) != len(want) {
t.Fatalf("status writes: want %v, got %v", want, got)
}
for i, w := range want {
if got[i] != w {
t.Errorf("status[%d]: want %d, got %d", i, w, got[i])
}
}
}
func TestGoBodyFacts_QueryReads(t *testing.T) {
src := `package h
func H(w, r) {
a := r.URL.Query().Get("a")
b := r.FormValue("b")
c := r.Header.Get("X-C")
_ = r.PostFormValue("d")
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
keys := bf.QueryReads()
want := []string{"a", "b", "X-C", "d"}
if len(keys) != len(want) {
t.Fatalf("query reads: want %v, got %v", want, keys)
}
for i, w := range want {
if keys[i] != w {
t.Errorf("query[%d]: want %q, got %q", i, w, keys[i])
}
}
}
func TestGoBodyFacts_VarBindingMultiReturn(t *testing.T) {
// `repos, err := svc.List()` — first LHS gets the call binding;
// the second LHS (err) gets the same CallExpr but no resolved
// type so the post-pass doesn't attribute repos's type to err.
src := `package h
func H() {
repos, err := svc.List()
_ = err
_ = repos
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
r := bf.VarBinding("repos")
if r.Kind != BindingMethodCall || r.CallExpr != "svc.List" {
t.Errorf("repos: want method_call svc.List, got %v", r)
}
e := bf.VarBinding("err")
if e.Kind != BindingMethodCall {
t.Errorf("err: want method_call (no type), got %v", e)
}
}
func TestGoBodyFacts_RequestBindings(t *testing.T) {
src := `package h
func H(w, r) {
var req CreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { return }
}
`
bf, cleanup := makeGoBodyFacts(t, src, "H", 2)
defer cleanup()
rbs := bf.RequestBindings()
if len(rbs) != 1 {
t.Fatalf("request bindings: want 1, got %d (%+v)", len(rbs), rbs)
}
if rbs[0].Helper != "Decode" || rbs[0].VarName != "req" {
t.Errorf("first binding: want Decode/req, got %+v", rbs[0])
}
// chase var binding for "req" — should have the declared type.
b := bf.VarBinding("req")
if b.TypeID != "CreateRequest" {
t.Errorf("VarBinding(req): want CreateRequest, got %q", b.TypeID)
}
}
func TestGoBodyFacts_RequestBindings_FrameworkVariants(t *testing.T) {
src := `package h
func F(c *fiber.Ctx) {
var f FiberReq
c.BodyParser(&f)
}
func G(c *gin.Context) {
var g GinReq
c.ShouldBindJSON(&g)
}
func E(c echo.Context) {
var e EchoReq
c.Bind(&e)
}
func U() {
var u UnmarshalReq
json.Unmarshal(body, &u)
}
func A() {
var anon AnonReq
_ = anon
json.NewDecoder(r.Body).Decode(&AnonReq{})
}
`
cases := []struct {
fn string
line int
want string
varN string
}{
{"F", 2, "BodyParser", "f"},
{"G", 6, "ShouldBindJSON", "g"},
{"E", 10, "Bind", "e"},
{"U", 14, "Unmarshal", "u"},
}
for _, tc := range cases {
bf, cleanup := makeGoBodyFacts(t, src, tc.fn, tc.line)
defer cleanup()
rbs := bf.RequestBindings()
if len(rbs) != 1 {
t.Errorf("%s: want 1 binding, got %d", tc.fn, len(rbs))
continue
}
if rbs[0].Helper != tc.want || rbs[0].VarName != tc.varN {
t.Errorf("%s: want %s/%s, got %+v", tc.fn, tc.want, tc.varN, rbs[0])
}
}
// Anonymous composite: Decode(&AnonReq{})
bfA, cleanup := makeGoBodyFacts(t, src, "A", 18)
defer cleanup()
rbs := bfA.RequestBindings()
if len(rbs) != 1 {
t.Fatalf("A: want 1 binding, got %d", len(rbs))
}
if rbs[0].CompositeType != "AnonReq" {
t.Errorf("A: want CompositeType=AnonReq, got %+v", rbs[0])
}
}
func findCall(calls []ResponseCall, helper string) *ResponseCall {
for i := range calls {
if calls[i].Helper == helper {
return &calls[i]
}
}
return nil
}