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

355 lines
13 KiB
Go

package contracts
import (
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// -----------------------------------------------------------------------------
// Go — net/http (stdlib) provider
// -----------------------------------------------------------------------------
// Matches:
//
// json.NewDecoder(r.Body).Decode(&req)
// jsoniter.NewDecoder(r.Body).Decode(&req)
// decoder.Decode(&req)
//
// The capture is the target variable; the detector then looks up its
// type inside the handler body or the file-scoped node list.
var goStdlibDecodeRe = regexp.MustCompile(`(?:json|jsoniter|decoder)\.?(?:NewDecoder)?\([^)]*\.Body\)?\.Decode\(\s*&?(\w+)\s*\)`)
// Matches:
//
// json.Unmarshal(body, &req) | json.Unmarshal(data, req)
var goUnmarshalRe = regexp.MustCompile(`json\.Unmarshal\([^,]+,\s*&?(\w+)\s*\)`)
// Matches provider-side response encoders:
//
// json.NewEncoder(w).Encode(resp)
// WriteJSON(w, status, resp)
var goStdlibEncodeRe = regexp.MustCompile(`json\.NewEncoder\([^)]+\)\.Encode\(\s*&?(\w+)\s*\)`)
// JSON response helpers. Custom wrappers are the norm in handwritten
// Go servers — `respondJSON`, `writeJSON`, `sendJSON`, `renderJSON`,
// `h.json`, `render.JSON` all converge on the same (w, code, value)
// shape. Matching any of these gets us the status code and response
// value expression in one pass. The name capture is case-insensitive
// only for the leading letter so `WriteJSON` still matches.
var goWriteJSONRe = regexp.MustCompile(`\b(?:[A-Za-z_]\w*\.)?(?:[Rr]espond|[Ww]rite|[Ss]end|[Rr]ender)(?:JSON|Json)\(\s*\w+\s*,\s*([^,]+?)\s*,\s*([^)]+?)\s*\)`)
// (Envelope splitting now uses splitMapLiteralBody — a brace/bracket-
// balanced byte walker — rather than a regex. The regex form
// truncated nested literals like `[]any{evt1, evt2}` at the first
// `}` and produced unusable expr values like `"[]any{"`.)
// r.URL.Query().Get("x"), r.FormValue("x"), r.PostFormValue("x").
var goQueryParamRe = regexp.MustCompile(`\b(?:URL\.Query\(\)\.Get|FormValue|PostFormValue)\(\s*["` + "`" + `]([^"` + "`" + `]+)["` + "`" + `]\s*\)`)
// w.WriteHeader(<expr>) — literal int or http.StatusX.
var goWriteHeaderRe = regexp.MustCompile(`\bWriteHeader\(\s*([^)]+?)\s*\)`)
// Return bare status literal: "return http.StatusBadRequest" in helpers.
var goStatusConstRe = regexp.MustCompile(`\bhttp\.(Status[A-Z]\w+)\b`)
func init() {
// Provider-side detectors. Each one's regex is narrow enough that
// running all of them on every Go provider handler doesn't cause
// cross-framework false positives.
schemaEnrichers = append(schemaEnrichers,
schemaEnricher{
name: "go-stdlib-provider",
languages: []string{"go"},
roles: []Role{RoleProvider},
detect: goNetHTTPDetect,
},
schemaEnricher{
name: "go-gin-provider",
languages: []string{"go"},
roles: []Role{RoleProvider},
detect: goGinDetect,
},
schemaEnricher{
name: "go-fiber-provider",
languages: []string{"go"},
roles: []Role{RoleProvider},
detect: goFiberDetect,
},
schemaEnricher{
name: "go-echo-provider",
languages: []string{"go"},
roles: []Role{RoleProvider},
detect: goEchoDetect,
},
// Consumer side — picks up the outgoing payload and the
// decode target around the call site. Same detector handles
// all Go HTTP clients (stdlib, resty, etc.) because the
// surrounding idioms are the same.
schemaEnricher{
name: "go-consumer",
languages: []string{"go"},
roles: []Role{RoleConsumer},
detect: goConsumerDetect,
},
)
}
// -----------------------------------------------------------------------------
// Go provider detectors
// -----------------------------------------------------------------------------
func goNetHTTPDetect(body string, fileNodes []*graph.Node) schemaHints {
var h schemaHints
if m := goStdlibDecodeRe.FindStringSubmatch(body); len(m) > 1 {
setRequestType(&h, m[1], body, fileNodes, m[0])
} else if m := goUnmarshalRe.FindStringSubmatch(body); len(m) > 1 {
setRequestType(&h, m[1], body, fileNodes, m[0])
}
if m := goStdlibEncodeRe.FindStringSubmatch(body); len(m) > 1 {
setResponseType(&h, m[1], body, fileNodes, m[0])
} else if m := goWriteJSONRe.FindStringSubmatch(body); len(m) > 2 {
if code, ok := parseStatusExpr(m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
setResponseType(&h, m[2], body, fileNodes, m[0])
}
h.QueryParams = append(h.QueryParams, allSubmatches(body, goQueryParamRe, 1)...)
for _, m := range goWriteHeaderRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr(m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
for _, m := range goStatusConstRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr("http." + m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
return h
}
// Gin: c.BindJSON / c.ShouldBindJSON / c.ShouldBind, c.JSON(status, obj),
// c.Query("x"), c.DefaultQuery("x", ...), c.Param("x").
var (
ginBindRe = regexp.MustCompile(`\b(?:ShouldBindJSON|BindJSON|ShouldBind)\(\s*&?(\w+)\s*\)`)
ginJSONRe = regexp.MustCompile(`\.JSON\(\s*([^,]+?)\s*,\s*([A-Za-z_][\w\.]*(?:\{[^}]*\})?)\s*\)`)
ginQueryRe = regexp.MustCompile(`\b(?:DefaultQuery|Query)\(\s*["` + "`" + `]([^"` + "`" + `]+)["` + "`" + `]`)
ginStatusRe = regexp.MustCompile(`\.Status\(\s*([^)]+?)\s*\)`)
)
func goGinDetect(body string, fileNodes []*graph.Node) schemaHints {
var h schemaHints
if m := ginBindRe.FindStringSubmatch(body); len(m) > 1 {
setRequestType(&h, m[1], body, fileNodes, m[0])
}
for _, m := range ginJSONRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr(m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
setResponseType(&h, m[2], body, fileNodes, m[0])
}
h.QueryParams = append(h.QueryParams, allSubmatches(body, ginQueryRe, 1)...)
for _, m := range ginStatusRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr(m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
// Pick up any bare http.StatusX references too.
for _, m := range goStatusConstRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr("http." + m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
return h
}
// Fiber: c.BodyParser(&req), c.JSON(obj), c.Status(200), c.Query("x").
var (
fiberBindRe = regexp.MustCompile(`\bBodyParser\(\s*&?(\w+)\s*\)`)
fiberJSONRe = regexp.MustCompile(`\.JSON\(\s*&?([A-Za-z_][\w\.]*(?:\{[^}]*\})?)\s*\)`)
fiberQueryRe = regexp.MustCompile(`\.Query\(\s*["` + "`" + `]([^"` + "`" + `]+)["` + "`" + `]`)
fiberStatusRe = regexp.MustCompile(`\.Status\(\s*([^)]+?)\s*\)`)
)
func goFiberDetect(body string, fileNodes []*graph.Node) schemaHints {
var h schemaHints
if m := fiberBindRe.FindStringSubmatch(body); len(m) > 1 {
setRequestType(&h, m[1], body, fileNodes, m[0])
}
if m := fiberJSONRe.FindStringSubmatch(body); len(m) > 1 {
setResponseType(&h, m[1], body, fileNodes, m[0])
}
h.QueryParams = append(h.QueryParams, allSubmatches(body, fiberQueryRe, 1)...)
for _, m := range fiberStatusRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr(m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
for _, m := range goStatusConstRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr("http." + m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
return h
}
// Echo: c.Bind(&req), c.JSON(status, obj), c.QueryParam("x"),
// c.Param("x").
var (
echoBindRe = regexp.MustCompile(`\bc\.Bind\(\s*&?(\w+)\s*\)`)
echoQueryRe = regexp.MustCompile(`\bQueryParam\(\s*["` + "`" + `]([^"` + "`" + `]+)["` + "`" + `]`)
)
func goEchoDetect(body string, fileNodes []*graph.Node) schemaHints {
var h schemaHints
if m := echoBindRe.FindStringSubmatch(body); len(m) > 1 {
setRequestType(&h, m[1], body, fileNodes, m[0])
}
// Echo's JSON signature is identical to gin's, so the gin regex
// also fires here via the shared enricher chain — the driver
// merges. But we still pull status codes from h.WriteHeader etc.
for _, m := range ginJSONRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr(m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
setResponseType(&h, m[2], body, fileNodes, m[0])
}
h.QueryParams = append(h.QueryParams, allSubmatches(body, echoQueryRe, 1)...)
for _, m := range goStatusConstRe.FindAllStringSubmatch(body, -1) {
if code, ok := parseStatusExpr("http." + m[1]); ok {
h.StatusCodes = append(h.StatusCodes, code)
}
}
return h
}
// -----------------------------------------------------------------------------
// Go consumer detector — best-effort extraction of the outgoing payload
// and any decode target near the call site.
// -----------------------------------------------------------------------------
var (
// Outbound body: json.Marshal(<expr>) within the call window.
goMarshalRe = regexp.MustCompile(`json\.Marshal\(\s*&?(\w+)\s*\)`)
// Decode target: json.NewDecoder(resp.Body).Decode(&result) or
// json.Unmarshal(body, &result).
goDecodeRespRe = regexp.MustCompile(`json\.NewDecoder\([^)]+\.Body\)\.Decode\(\s*&?(\w+)\s*\)`)
)
func goConsumerDetect(body string, fileNodes []*graph.Node) schemaHints {
var h schemaHints
if m := goMarshalRe.FindStringSubmatch(body); len(m) > 1 {
setRequestType(&h, m[1], body, fileNodes, m[0])
}
if m := goDecodeRespRe.FindStringSubmatch(body); len(m) > 1 {
setResponseType(&h, m[1], body, fileNodes, m[0])
} else if m := goUnmarshalRe.FindStringSubmatch(body); len(m) > 1 {
setResponseType(&h, m[1], body, fileNodes, m[0])
}
return h
}
// -----------------------------------------------------------------------------
// Shared helpers for Go detectors
// -----------------------------------------------------------------------------
// setRequestType records a request-type hint when the regex pattern
// captured an identifier that's directly a type name (e.g.
// `Decode(&Request{})` → "Request"). Variable-to-type resolution and
// envelope walking moved to the BodyFacts AST overlay (Phase 1 of
// spec-contract-extraction.md), which runs after this function and
// overrides the hint with structurally-correct facts.
//
// The body parameter is retained for signature symmetry with the
// callers (the regex detectors take body as their primary input).
func setRequestType(h *schemaHints, ident, body string, fileNodes []*graph.Node, matchText string) {
_ = body
if looksLikeType(ident) {
h.RequestType = resolveTypeInFile(ident, fileNodes)
return
}
if h.RequestExpr == "" {
h.RequestExpr = strings.TrimSpace(matchText)
}
}
// setResponseType records a response-type hint. Type resolution and
// envelope walking moved to the BodyFacts AST overlay (Phase 1 of
// spec-contract-extraction.md). This function only handles the cases
// the AST overlay can't override safely:
//
// 1. ident is itself a type name (e.g. `JSON(http.StatusOK, ErrResp{...})`)
// 2. ident is a compound literal expression — record as ResponseExpr
// for the post-pass to chase
// 3. ident is a bare identifier — record as ResponseExpr; the AST
// overlay will replace it if it can resolve the var binding
//
// The body parameter is retained for signature symmetry; it's no
// longer consulted (the AST overlay reads from the parse tree).
func setResponseType(h *schemaHints, ident, body string, fileNodes []*graph.Node, matchText string) {
_ = body
trimmed := strings.TrimSpace(ident)
if looksLikeType(trimmed) {
h.ResponseType = resolveTypeInFile(trimmed, fileNodes)
return
}
if h.ResponseExpr == "" {
switch {
case isCompoundExpr(trimmed):
h.ResponseExpr = trimmed
case isPlainIdent(trimmed):
h.ResponseExpr = trimmed
default:
h.ResponseExpr = strings.TrimSpace(matchText)
}
}
}
func isCompoundExpr(s string) bool {
return strings.ContainsAny(s, "{[(")
}
// looksLikeType is a quick heuristic: starts with an uppercase letter,
// contains only identifier-ish characters. Filters out things like
// "err" or "nil" while keeping "LoginRequest" and "pkg.User".
func looksLikeType(s string) bool {
s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "&")
s = strings.TrimPrefix(s, "*")
if s == "" {
return false
}
// Drop trailing literal `{}` so `Foo{}` still counts as a type.
if i := strings.Index(s, "{"); i >= 0 {
s = s[:i]
}
first := rune(s[0])
if first < 'A' || first > 'Z' {
// `pkg.User` — check the part after the last dot too.
if i := strings.LastIndex(s, "."); i >= 0 {
return looksLikeType(s[i+1:])
}
return false
}
return true
}
func allSubmatches(body string, re *regexp.Regexp, grp int) []string {
ms := re.FindAllStringSubmatch(body, -1)
if len(ms) == 0 {
return nil
}
out := make([]string, 0, len(ms))
for _, m := range ms {
if grp < len(m) {
out = append(out, m[grp])
}
}
return out
}