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_.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 }