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
263 lines
10 KiB
Go
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 }
|