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

263 lines
10 KiB
Go

package contracts
import (
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// Origin labels the confidence with which a Binding was derived.
// Shared vocabulary so the dashboard and downstream consumers see a
// single set of labels regardless of which subsystem populated the
// binding.
type Origin string
const (
OriginTextMatched Origin = "text_matched" // legacy regex enricher
OriginASTInferred Origin = "ast_inferred" // tree-sitter fact, name-based type lookup
OriginASTResolved Origin = "ast_resolved" // tree-sitter fact, resolved against the file's graph
OriginLSPResolved Origin = "lsp_resolved" // compiler-grade (go/types or LSP)
)
// BindingKind labels the syntactic shape of a local-variable binding
// in a handler body. The contract pipeline uses Kind to decide whether
// to walk the graph for a return type, treat the value as a primitive,
// or render the raw expression.
type BindingKind string
const (
BindingUnknown BindingKind = "" // no binding found
BindingMethodCall BindingKind = "method_call" // x := h.svc.GetRepos(...)
BindingFuncCall BindingKind = "func_call" // x := makeFoo(...)
BindingComposite BindingKind = "composite" // x := Foo{...}
BindingSliceLit BindingKind = "slice_lit" // x := []Foo{...}
BindingMakeSlice BindingKind = "make_slice" // x := make([]Foo, ...)
BindingMakeMap BindingKind = "make_map" // x := make(map[K]V, ...)
BindingMapLit BindingKind = "map_lit" // x := map[K]V{...}
BindingPathValue BindingKind = "path_value" // x := r.PathValue("id")
BindingFormValue BindingKind = "form_value" // x := r.FormValue("id")
BindingHeaderValue BindingKind = "header_value" // x := r.Header.Get("X-Foo")
BindingQueryGet BindingKind = "query_get" // x := r.URL.Query().Get("k")
BindingStringLit BindingKind = "string_lit"
BindingIntLit BindingKind = "int_lit"
BindingFloatLit BindingKind = "float_lit"
BindingBoolLit BindingKind = "bool_lit"
)
// Binding is the per-variable fact a contract enricher reads instead
// of running regexes. Subsumes findVarType (schema_enrich_go.go),
// traceVarTypeFromBodyWithShape (indexer.go), and bindLiteralTypeFromBody
// (indexer.go) — all three are deleted in phase 1.
type Binding struct {
Kind BindingKind
TypeID string // graph type-node ID when resolvable; bare type name otherwise
Repeated bool // slice-typed binding (renders [Foo] in the dashboard)
Pointer bool // pointer-typed binding
CallExpr string // "h.svc.GetRepos" for method_call/func_call; empty otherwise
RawExpr string // exact source span of the RHS, for fallback display
Line int // 1-based line where the binding's RHS appears
Origin Origin
}
// IsZero reports whether the binding is empty (no fact found).
func (b Binding) IsZero() bool { return b.Kind == BindingUnknown }
// ResponseCall describes one JSON-response helper invocation in a
// handler body — WriteJSON, respondJSON, json.NewEncoder().Encode, or
// a framework method like c.JSON(code, value).
type ResponseCall struct {
Helper string // "WriteJSON" | "respondJSON" | "Encode" | "JSON" | …
StatusArg *Node // status-code argument (may be nil for helpers without one)
StatusCode int // resolved status code; 0 if not resolvable
StatusKnown bool // distinguishes 0 = unresolved from 0 = legitimate StatusOK
ValueArg *Node // value argument (the body)
ValueExpr string // trimmed source span of the value argument
Line int // 1-based line of the call
}
// RequestBinding describes one call that binds the request body to a
// typed variable: json.NewDecoder(r.Body).Decode(&req), c.BodyParser(&req),
// c.ShouldBindJSON(&req), json.Unmarshal(body, &req).
//
// VarName is the bound variable name (without the leading & or *).
// CompositeType, if non-empty, is the literal type used at the call
// site (e.g. `Decode(&Request{})` → "Request"); the caller can use
// this directly without going through VarBinding.
type RequestBinding struct {
Helper string // "Decode" | "BodyParser" | "ShouldBindJSON" | "Bind" | "Unmarshal"
VarName string // bound variable name (no &/*)
CompositeType string // type literal at the binding call, if any
Line int
}
// KeyValue is one parsed entry from a Go map composite literal —
// `"data": workspaces` becomes `{Key: "data", ValueExpr: "workspaces"}`.
type KeyValue struct {
Key string
ValueExpr string
ValueNode *Node
}
// Node is an opaque AST handle. Wraps a tree-sitter node so consumers
// don't have to import the sitter package directly.
type Node struct {
inner *sitter.Node
src []byte
}
// NewNode wraps a sitter node + source for use by BodyFacts callers.
// Returns nil for a nil input.
func NewNode(n *sitter.Node, src []byte) *Node {
if n == nil {
return nil
}
return &Node{inner: n, src: src}
}
// Inner returns the underlying tree-sitter node. Internal use only —
// avoid reaching past BodyFacts methods in non-extractor code.
func (n *Node) Inner() *sitter.Node {
if n == nil {
return nil
}
return n.inner
}
// Text returns the UTF-8 source span the node covers.
func (n *Node) Text() string {
if n == nil || n.inner == nil {
return ""
}
return n.inner.Content(n.src)
}
// Kind returns the tree-sitter node kind ("identifier", "call_expression", …).
func (n *Node) Kind() string {
if n == nil || n.inner == nil {
return ""
}
return n.inner.Type()
}
// Line returns the 1-based line of the node start.
func (n *Node) Line() int {
if n == nil || n.inner == nil {
return 0
}
return int(n.inner.StartPoint().Row) + 1
}
// BodyFacts is the structured per-handler view a contract enricher
// AND the post-pass resolvers read instead of running regexes.
// Implementations are per-language; the Go implementation is phase 1.
// Other languages register nopBodyFacts; their phase ships a real
// implementation without changes to consumers.
type BodyFacts interface {
// VarBinding returns the binding kind and inferred type for a
// local variable in the handler body. Returns a zero Binding if
// no fact was found.
VarBinding(name string) Binding
// ResponseCalls returns every call to a JSON-response helper in
// the body, in source order.
ResponseCalls() []ResponseCall
// MapLiteralEntries returns the keyed children of a composite
// map literal at `node`. Used to walk envelope shapes without
// the brace-balancing splitMapLiteralBody used to do.
MapLiteralEntries(node *Node) []KeyValue
// StatusWrites returns every WriteHeader call's status code.
StatusWrites() []int
// QueryReads returns the names of every URL/form/header query
// accessor key in the body.
QueryReads() []string
// RequestBindings returns every call that binds the request body
// to a typed variable: Decode/BodyParser/ShouldBind*/Bind/Unmarshal.
// Used by the AST overlay to set request_type without going
// through findVarType.
RequestBindings() []RequestBinding
}
// BodyFactsFactory builds a BodyFacts view for one handler.
type BodyFactsFactory func(tree *parser.ParseTree, handler *graph.Node) BodyFacts
// bodyFactsByLang maps a language code to the factory that produces
// per-handler BodyFacts. Languages without a registered factory get
// nopBodyFacts via FactoryForLang.
var bodyFactsByLang = map[string]BodyFactsFactory{}
// RegisterBodyFactsFactory registers a per-language factory. Called
// from per-language init() in bodyfacts_<lang>.go files.
func RegisterBodyFactsFactory(lang string, factory BodyFactsFactory) {
bodyFactsByLang[lang] = factory
}
// FactoryForLang returns the registered factory for lang, or a factory
// that always returns nopBodyFacts when no implementation exists.
func FactoryForLang(lang string) BodyFactsFactory {
if f, ok := bodyFactsByLang[lang]; ok {
return f
}
return newNopBodyFacts
}
// MakeBodyFacts is a convenience wrapper that handles the nil-tree
// case and dispatches to the registered factory.
func MakeBodyFacts(tree *parser.ParseTree, handler *graph.Node) BodyFacts {
if tree == nil || tree.Tree() == nil || handler == nil {
return nopBodyFacts{}
}
factory := FactoryForLang(tree.Lang())
return factory(tree, handler)
}
// BindingResolver is the optional upgrade hook the AST overlay
// consults when present (set via SetBindingResolver below). When
// the resolver returns (typeName, true), the overlay stamps the
// binding with Origin = OriginLSPResolved instead of OriginASTInferred.
//
// goanalysis.Provider implements this via LookupTypeAtLine. The
// indexer wires it in when --semantic is enabled and the provider
// has run for the repo.
type BindingResolver interface {
// LookupTypeAtLine returns the resolved type name at the given
// 1-based line in the file. Returns ("", false) when the line
// has no resolvable typed declaration.
LookupTypeAtLine(filePath string, line int) (string, bool)
}
// activeBindingResolver is the optional upgrade resolver. nil when
// no provider has been wired in; the overlay falls back to the
// tree-sitter-only path in that case.
var activeBindingResolver BindingResolver
// SetBindingResolver registers the LookupTypeAtLine implementation
// the AST overlay should consult. Called by the indexer once the
// goanalysis (or other semantic) provider has loaded type info.
// Pass nil to clear.
func SetBindingResolver(r BindingResolver) {
activeBindingResolver = r
}
// CurrentBindingResolver returns the wired-in resolver or nil.
func CurrentBindingResolver() BindingResolver { return activeBindingResolver }
// nopBodyFacts is the no-op implementation used for languages without
// a real BodyFacts factory. Returns "unbound" / empty for everything,
// which causes the legacy regex enricher to run in fallback mode.
type nopBodyFacts struct{}
func newNopBodyFacts(_ *parser.ParseTree, _ *graph.Node) BodyFacts {
return nopBodyFacts{}
}
func (nopBodyFacts) VarBinding(string) Binding { return Binding{} }
func (nopBodyFacts) ResponseCalls() []ResponseCall { return nil }
func (nopBodyFacts) MapLiteralEntries(*Node) []KeyValue { return nil }
func (nopBodyFacts) StatusWrites() []int { return nil }
func (nopBodyFacts) QueryReads() []string { return nil }
func (nopBodyFacts) RequestBindings() []RequestBinding { return nil }