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

2883 lines
100 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package languages
import (
"bytes"
"fmt"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
"github.com/zzet/gortex/internal/parser/tsitter/golang"
)
// qGoAll is a single tree-sitter query that alternates over every pattern
// the Go extractor needs. One tree walk per file replaces the 20+
// `parser.RunQuery` calls the previous design made per Extract call
// (each of which recompiled its query and ran an independent cursor
// over the whole tree). Capture names are disjoint across patterns so
// the dispatch in Extract can branch on which name is set.
const qGoAll = `
[
(package_clause (package_identifier) @pkg.name)
(function_declaration
name: (identifier) @func.name
parameters: (parameter_list) @func.params
result: (_)? @func.result) @func.def
(method_declaration
receiver: (parameter_list) @method.receiver
name: (field_identifier) @method.name
parameters: (parameter_list) @method.params
result: (_)? @method.result) @method.def
(type_declaration
(type_spec
name: (type_identifier) @typedef.name
type: (_) @typedef.body)) @typedef.def
(type_declaration
(type_alias
name: (type_identifier) @alias.name
type: (_) @alias.type)) @alias.def
(import_spec
name: (package_identifier)? @import.alias
path: (interpreted_string_literal) @import.path) @import.spec
(call_expression
function: (identifier) @call.name) @call.expr
(call_expression
function: (selector_expression
operand: (_) @callm.receiver
field: (field_identifier) @callm.method)) @callm.expr
(var_declaration
(var_spec
name: (identifier) @var.name
type: (_)? @var.type)) @var.def
(const_declaration
(const_spec
name: (identifier) @const.name)) @const.def
(short_var_declaration
left: (expression_list (identifier) @svar.name)
right: (expression_list (_) @svar.value)) @svar.def
(composite_literal
type: (type_identifier) @comp.type) @comp.expr
(composite_literal
type: (qualified_type
package: (package_identifier) @compq.pkg
name: (type_identifier) @compq.type)) @compq.expr
(field_declaration
type: (type_identifier) @ftype.name) @ftype.decl
(const_spec
type: (type_identifier) @ctype.name) @ctype.decl
(var_spec
type: (type_identifier) @vtype.name) @vtype.decl
(parameter_declaration
type: (type_identifier) @ptype.name) @ptype.decl
; Type assertions: x.(T) / x.(pkg.T). The asserted type is a
; reference to T. Without this an interface used only via a type
; assertion has no incoming edge and looks like dead code.
(type_assertion_expression
type: (type_identifier) @assert.name) @assert.decl
(type_assertion_expression
type: (qualified_type
package: (package_identifier) @assertq.pkg
name: (type_identifier) @assertq.name)) @assertq.decl
(argument_list
(selector_expression
operand: (_) @selarg.receiver
field: (field_identifier) @selarg.field)) @selarg.list
(argument_list
(identifier) @identarg.name) @identarg.list
(keyed_element
(literal_element (identifier) @fieldval.key)
(literal_element (identifier) @fieldval.value)) @fieldval.elem
; Struct field set to a string literal, e.g. ActivityName: "Charge".
; The Temporal step/executor resolver joins these to a struct whose
; method dispatches via that field.
(keyed_element
(literal_element (identifier) @fieldstr.key)
(literal_element (interpreted_string_literal) @fieldstr.val)) @fieldstr.elem
(keyed_element
(literal_element (identifier) @fieldsel.key)
(literal_element
(selector_expression
operand: (_) @fieldsel.receiver
field: (field_identifier) @fieldsel.method))) @fieldsel.elem
; Assignment LHS selector. EdgeWrites from the enclosing function
; to the field. Covers plain "s.field = x" and "s.field += x".
(assignment_statement
left: (expression_list
(selector_expression
operand: (_) @assign.receiver
field: (field_identifier) @assign.field))) @assign.def
; Inc/dec selector statements: s.field++ / s.field-- both write.
(inc_statement
(selector_expression
operand: (_) @incsel.receiver
field: (field_identifier) @incsel.field)) @incsel.def
(dec_statement
(selector_expression
operand: (_) @decsel.receiver
field: (field_identifier) @decsel.field)) @decsel.def
; Return-statement value-side reads. "return s.port, s.addr" reads
; both fields. We capture each selector in the return expression
; list so the schema's "every value use is a read" rule covers
; functions whose only output is the read.
(return_statement
(expression_list
(selector_expression
operand: (_) @retsel.receiver
field: (field_identifier) @retsel.field)) @retsel.list)
(return_statement
(expression_list
(identifier) @retident.name) @retident.list)
]
`
// GoExtractor extracts Go source files into graph nodes and edges.
type GoExtractor struct {
lang *sitter.Language
qAll *parser.PreparedQuery
// envHelperExtra is the per-repo corporate env-helper allow-list (lower-
// cased names) loaded from the git-ignored `.gortex/temporal-allowlist.yaml`.
// Names here are merged with the built-in goEnvHelperNames when recognising
// a Temporal env-or-default dispatch helper, promoting the resolved edge to
// the inferred (visible) tier. Empty/nil when no local allow-list is loaded.
envHelperExtra map[string]bool
}
// SetEnvHelperNames installs the per-repo corporate env-helper allow-list on
// the extractor. Names are stored lower-cased for case-insensitive matching.
// Called once during extractor registration (config is not available at parse
// time); a nil / empty slice clears it. Safe to call before indexing begins;
// must not be called concurrently with Extract.
func (e *GoExtractor) SetEnvHelperNames(names []string) {
if len(names) == 0 {
e.envHelperExtra = nil
return
}
m := make(map[string]bool, len(names))
for _, n := range names {
if n = strings.TrimSpace(n); n != "" {
m[strings.ToLower(n)] = true
}
}
e.envHelperExtra = m
}
func NewGoExtractor() *GoExtractor {
lang := golang.GetLanguage()
return &GoExtractor{
lang: lang,
qAll: parser.MustPreparedQuery(qGoAll, lang),
}
}
func (e *GoExtractor) Language() string { return "go" }
func (e *GoExtractor) Extensions() []string { return []string{".go"} }
// --- Deferred match buffers ----------------------------------------
type goDeferredCall struct {
callName string // plain call
method string // selector call method name
receiver string // selector call receiver text
line int // 1-based line of call_expression
isSelector bool
spawn bool // call is launched via `go` — emit EdgeSpawns alongside EdgeCalls
// returnUsage is how the call site consumes the return value
// (graph.ReturnUsage* label), classified from the call node's
// parent chain at capture time and stamped as edge Meta on every
// EdgeCalls emitted for this site. Empty when unclassifiable.
returnUsage string
// gRPC server registration. Set when this call is the generated
// `Register<Service>Server(registrar, impl)` helper: grpcRegService
// is the service name and grpcRegArgNode is the second-argument AST
// node naming the impl. The impl type itself is resolved in the
// post-pass below — after the file-wide type environment is fully
// populated, since the impl may be a variable declared anywhere in
// the file. Stamped as edge Meta so the resolver's
// ResolveGRPCStubCalls pass can join client-stub calls to the
// server-side handler method.
grpcRegService string
grpcRegArgNode *sitter.Node
// Temporal SDK call attribution. tempKind is "" for non-Temporal
// calls; "activity" / "workflow" / "register_activity" /
// "register_workflow" identifies which Temporal pattern this call
// is. tempName is the activity / workflow name extracted from the
// call (function reference or string literal). tempLocal flags
// `workflow.ExecuteLocalActivity`. The actual edge target +
// `via=temporal.stub` / `via=temporal.register` meta is stamped
// in the call post-pass below; the resolver's ResolveTemporalCalls
// pass joins stub calls to the registered handler.
tempKind string
tempName string
tempLocal bool
// tempEnvDefault is set when tempName was resolved from a bare
// variable read from an env var with a literal default (e.g.
// `cmp.Or(os.Getenv("K"), "Default")`). The stub edge is then tagged
// `temporal_name_origin=env_default` so the resolver lands it at the
// speculative tier — the runtime env value may differ from the default.
tempEnvDefault bool
// tempVarTrace is set when tempName was recovered by tracing a bare
// local dispatch variable to its last intra-procedural assignment — a
// string literal, a const reference, or a no-arg const-returning func
// (the "meta_vars" broken-dispatch category). The stub edge is tagged
// `temporal_name_origin=var_trace` so the resolver lands it at the
// speculative tier — last-assignment-wins is a best-effort static guess.
tempVarTrace bool
// tempHandlerKind is "query" / "signal" / "update" when this call
// is a `workflow.SetQueryHandler` / `GetSignalChannel` /
// `SetUpdateHandler` in-workflow handler declaration; tempName then
// carries the handler's string name. `via=temporal.handler` meta is
// stamped on the emitted edge in the call post-pass below.
tempHandlerKind string
// tempDefaultConst is set when the env-default's default argument was a
// constant REFERENCE (`config.ACTIVITY_NAME_DEFAULT` / a bare const name)
// rather than a string literal. temporal_name then stays the dispatch
// variable name; the resolver substitutes the constant's literal value
// from its const-deref index. Emitted as `temporal_default_const`.
tempDefaultConst string
// tempEnvSource records HOW the env-default name was recognised:
// "os_getenv" (provable os.Getenv / os.LookupEnv read), "allowlist" (a
// helper name in the configured env-helper allow-list), or "heuristic"
// (a generic env-named helper matched structurally). Emitted as
// `temporal_env_source` so the resolver can tier the edge: allow-list /
// os.Getenv land at the inferred tier, the heuristic stays speculative.
tempEnvSource string
// tempOutKind is "signal" / "query" when this call is an outbound
// signal-send / query-call against a running workflow
// (SignalExternalWorkflow / SignalWorkflow / QueryWorkflow); tempName
// then carries the signal/query name. `via=temporal.signal-send` /
// `temporal.query-call` meta is stamped on the emitted edge below.
tempOutKind string
// tempRegisteredName is the canonical registered name when a
// RegisterActivityWithOptions / RegisterWorkflowWithOptions call
// overrides it via RegisterOptions{Name: "..."}. tempName still holds
// the function-reference identifier (used to locate the node); this
// is the name the activity/workflow is dispatched under and becomes
// the resolver's index key. Empty when no Name override is present.
tempRegisteredName string
// tempRegisterPlural marks a `w.RegisterActivities(&MyActivities{})`
// struct registration: tempName then holds the struct TYPE name and
// the resolver promotes every exported method of that struct to a
// temporal activity keyed by the method name.
tempRegisterPlural bool
// tempStartName is the workflow name when this call STARTS a workflow
// (client.ExecuteWorkflow / SignalWithStartWorkflow). `via=temporal.start`
// meta is stamped on the emitted edge and the resolver rewrites it to
// the registered workflow — the "who starts this workflow" edge.
tempStartName string
// callNode is the call_expression AST node, retained so the general
// call-edge emission can extract positional arg names
// (attachGoTemporalCallArgNames) for the resolver's wrapper-following
// pass; the executor-field resolver also uses it to inspect the
// dispatch argument shape (e.g. `e.ActivityName` as a selector).
// Nil for synthetic / non-call entries.
callNode *sitter.Node
}
// PURPOSE — captures a struct-literal field assignment to a string literal
// so the Temporal executor-field resolver can join the construction site
// to a dispatch method that reads the same field.
// RATIONALE — separate from goDeferredCall because it fires on composite
// literals, not call expressions; the two passes share a single tree walk.
// KEYWORDS — temporal, executor, struct-field
type goExecutorField struct {
typeName string
field string
value string
line int
}
type goDeferredTypeRef struct {
typeName string
pkg string // optional qualifier
line int
kind graph.EdgeKind
}
type goDeferredValueSel struct {
field string
receiver string
line int
kind graph.EdgeKind
}
type goDeferredValueIdent struct {
name string
line int
}
func (e *GoExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
tree, err := parser.ParseFile(src, e.lang)
if err != nil {
return nil, err
}
// Wrap in a ref-counted handle and stamp it on the result so the
// indexer can hand it to contract enrichers. The wrapper owns
// the tree from here; callers Release() once they're done. We
// don't defer Close — closing happens in the indexer after the
// contract pass releases its ref.
pt := parser.NewParseTree(tree, src, "go")
result := &parser.ExtractionResult{Tree: pt}
root := tree.RootNode()
fileNode := &graph.Node{
ID: filePath,
Kind: graph.KindFile,
Name: filePath,
FilePath: filePath,
StartLine: 1,
EndLine: int(root.EndPoint().Row) + 1,
Language: "go",
}
fileID := fileNode.ID
result.Nodes = append(result.Nodes, fileNode)
imports := map[string]string{} // alias → importPath
// Local alias the Temporal workflow package is imported under in this
// file (default "workflow"; "" when not imported). The receiver-gated
// temporal detectors canonicalise a matching receiver to "workflow" so
// an aliased `import wf "go.temporal.io/sdk/workflow"` is recognised.
wfAlias := goWorkflowReceiverAlias(root, src)
tenv := make(typeEnv)
// paramsByFunc: enclosing-function ID → (param/receiver name → type).
// Function parameters and method receivers shadow file-wide tenv at
// call resolution time so each function's locals stay sandboxed.
paramsByFunc := map[string]typeEnv{}
// paramNamesByFunc: function/method ID → set of ALL its parameter
// names (paramsByFunc only keeps params with non-builtin types). Used
// by the Temporal wrapper detector to recognise a dispatch whose name
// is a forwarded parameter.
paramNamesByFunc := map[string]map[string]bool{}
// seenIDs: per-file node-ID collision set. Two declarations that build
// the same base ID (e.g. two func init()) both survive as distinct nodes
// via a line suffix instead of one silently overwriting the other.
seenIDs := map[string]bool{}
seenTypeName := map[string]bool{} // dedup when alias + typedef match same name
var calls []goDeferredCall
// grpcStubVars maps a local variable name to the gRPC service it is
// a client stub for, seeded from `v := pkg.New<Service>Client(conn)`
// short-var declarations. Lets `v.Method(...)` call sites resolve as
// gRPC stub calls in the post-pass below.
grpcStubVars := map[string]string{}
var typeRefs []goDeferredTypeRef
var instantiates []goDeferredTypeRef
var valueSels []goDeferredValueSel
var valueIdents []goDeferredValueIdent
var fieldValSels []goDeferredValueSel
var fieldValIdents []goDeferredValueIdent
var observabilityEvents []goObservabilityEvent
var pubsubCandidates []goPubsubCandidate
var stringEvents []goStringEvent
var flagEvents []goFlagEvent
var configEvents []goConfigEvent
var sqlEvents []goSQLEvent
// writes buffers selector LHS of assignment / inc / dec
// statements. Emitted in the post-pass once funcRanges and tenv
// are settled so each EdgeWrites is attributed to its enclosing
// function and (when known) carries the receiver type for the
// resolver to land on the right field node.
var writes []goDeferredValueSel
var executorFields []goExecutorField
parser.EachMatch(e.qAll, root, src, func(m parser.QueryResult) {
switch {
// Package: just a marker; not emitted as a graph node.
case m.Captures["pkg.name"] != nil:
// No-op (the package name is not currently surfaced as a node).
case m.Captures["func.def"] != nil:
e.emitFunction(m, filePath, fileID, src, result, paramsByFunc, paramNamesByFunc, imports, seenIDs)
case m.Captures["method.def"] != nil:
e.emitMethod(m, filePath, fileID, src, result, paramsByFunc, paramNamesByFunc, imports, seenIDs)
case m.Captures["typedef.def"] != nil:
e.emitTypeDecl(m, filePath, fileID, src, result, seenTypeName)
case m.Captures["alias.def"] != nil:
e.emitTypeAlias(m, filePath, fileID, src, result, seenTypeName)
case m.Captures["import.spec"] != nil:
e.emitImport(m, filePath, fileID, result, imports, fileNode)
case m.Captures["call.expr"] != nil:
expr := m.Captures["call.expr"]
callName := m.Captures["call.name"].Text
dc := goDeferredCall{
callName: callName,
line: expr.StartLine + 1,
spawn: isGoroutineSpawn(expr.Node),
returnUsage: classifyReturnUsage(expr.Node, src, goReturnUsageSpec),
callNode: expr.Node,
}
if svc, argNode, ok := grpcRegisterArgNode(expr.Node, callName); ok {
dc.grpcRegService, dc.grpcRegArgNode = svc, argNode
}
calls = append(calls, dc)
case m.Captures["callm.expr"] != nil:
expr := m.Captures["callm.expr"]
method := m.Captures["callm.method"].Text
receiver := m.Captures["callm.receiver"].Text
dc := goDeferredCall{
method: method,
receiver: receiver,
line: expr.StartLine + 1,
isSelector: true,
spawn: isGoroutineSpawn(expr.Node),
returnUsage: classifyReturnUsage(expr.Node, src, goReturnUsageSpec),
callNode: expr.Node,
}
dc.callNode = expr.Node
if svc, argNode, ok := grpcRegisterArgNode(expr.Node, method); ok {
dc.grpcRegService, dc.grpcRegArgNode = svc, argNode
}
// Temporal workflow → activity dispatch:
// `workflow.ExecuteActivity(ctx, X, ...)` etc. Canonicalise an
// aliased workflow-package receiver (e.g. `wf`) to "workflow"
// so the receiver-gated detectors recognise it.
tempRecv := goCanonicalWorkflowReceiver(receiver, wfAlias)
if kind, local, ok := goTemporalDispatchKind(tempRecv, method); ok {
argNode := goTemporalDispatchArg(expr.Node)
if name := goTemporalNameFromExpr(argNode, src); name != "" {
dc.tempKind = kind
dc.tempName = name
dc.tempLocal = local
// Env-default refinement: when the name is a bare local
// variable, try to resolve it to an env-var-with-literal
// -default so the dispatch lands on the default activity.
if argNode != nil && argNode.Type() == "identifier" {
if litDef, constName, source, ok := goTemporalEnvDefaultName(expr.Node, name, src, e.envHelperExtra); ok {
dc.tempEnvDefault = true
dc.tempEnvSource = source
if constName != "" {
// Const-reference default: keep temporal_name as the
// dispatch variable; the resolver substitutes the
// constant's literal value via temporal_default_const.
dc.tempDefaultConst = constName
} else {
dc.tempName = litDef
}
} else if lit, cn, ok := goTemporalVarTrace(expr.Node, name, src); ok {
// Variable tracing (Cat 2): the dispatch name is a bare
// local var assigned a string literal, a const
// reference, or a no-arg const-returning func. Emit the
// traced value as the dispatch name — a literal resolves
// directly; a const NAME is dereferenced by the
// resolver's const-deref map (a non-const name simply
// fails to deref and stays a broken_dispatch).
switch {
case lit != "":
dc.tempName = lit
case cn != "":
dc.tempName = cn
}
dc.tempVarTrace = true
}
}
}
} else if kind, plural, ok := goTemporalRegisterKind(method); ok {
// Temporal worker registration:
// `w.RegisterActivity(F)` etc.
if plural {
// `w.RegisterActivities(&MyActivities{})` — every
// exported method of the struct becomes an activity.
// tempName carries the struct TYPE name; the resolver
// promotes the methods.
if st := goTemporalRegisterStructType(expr.Node, src); st != "" {
dc.tempKind = "register_" + kind
dc.tempName = st
dc.tempRegisterPlural = true
}
} else if name := goTemporalRegisterName(expr.Node, src); name != "" {
dc.tempKind = "register_" + kind
dc.tempName = name
// RegisterActivityWithOptions / RegisterWorkflowWithOptions
// may override the registered name via
// RegisterOptions{Name: "..."} — that is the name a
// dispatch matches against, so capture it as the index key.
if override := goTemporalRegisterNameOverride(expr.Node, src); override != "" {
dc.tempRegisteredName = override
}
}
} else if hkind, ok := goTemporalHandlerKind(tempRecv, method); ok {
// Temporal in-workflow handler declaration:
// `workflow.SetQueryHandler(ctx, "name", fn)` etc.
if name := goTemporalHandlerName(expr.Node, src); name != "" {
dc.tempHandlerKind = hkind
dc.tempName = name
}
} else if okind, namePos, ok := goTemporalSignalQueryOutKind(tempRecv, method); ok {
// Outbound signal-send / query-call against a running
// workflow: SignalExternalWorkflow / SignalWorkflow /
// QueryWorkflow. The name is the 4th positional literal.
if name := goTemporalNthStringLiteralArg(expr.Node, namePos, src); name != "" {
dc.tempOutKind = okind
dc.tempName = name
}
} else if wfPos, ok := goTemporalStartKind(method); ok {
// Service-side workflow START: client.ExecuteWorkflow /
// SignalWithStartWorkflow. The workflow is the wfPos-th
// positional arg (a func ref, selector, or string type name).
if name := goTemporalNthArgName(expr.Node, wfPos, src); name != "" {
dc.tempStartName = name
}
}
calls = append(calls, dc)
if name, ok := detectGoLogEvent(expr.Node, method, src); ok {
observabilityEvents = append(observabilityEvents, goObservabilityEvent{
method: method,
name: name,
line: expr.StartLine + 1,
})
}
if cand, ok := detectGoPubsubCall(expr.Node, method, src); ok {
pubsubCandidates = append(pubsubCandidates, cand)
}
receiverText := m.Captures["callm.receiver"].Text
if name, ok := detectGoMetric(expr.Node, method, src); ok {
stringEvents = append(stringEvents, goStringEvent{
context: stringCtxMetric,
method: method,
value: name,
line: expr.StartLine + 1,
})
}
if msg, ok := detectGoErrorMessage(expr.Node, receiverText, method, src); ok {
stringEvents = append(stringEvents, goStringEvent{
context: stringCtxErrorMsg,
method: receiverText + "." + method,
value: msg,
line: expr.StartLine + 1,
})
}
if route, ok := detectGoRoute(expr.Node, method, src); ok {
stringEvents = append(stringEvents, goStringEvent{
context: stringCtxRoute,
method: method,
value: route,
line: expr.StartLine + 1,
})
}
if provider, flagName, ok := detectGoFlagCheck(expr.Node, method, src); ok {
flagEvents = append(flagEvents, goFlagEvent{
provider: provider,
method: method,
name: flagName,
line: expr.StartLine + 1,
})
}
if source, op, key, ok := detectGoConfigKey(expr.Node, m.Captures["callm.receiver"].Text, method, src); ok {
// Gate provider-specific classifications on the import
// set so a `req.GetString("query")` against an mcp
// request type doesn't get mistaken for a viper read in
// a file that never imports viper.
if goConfigSourceImported(source, imports) {
configEvents = append(configEvents, goConfigEvent{
source: source,
op: op,
method: method,
key: key,
line: expr.StartLine + 1,
})
}
}
if tables, cols, query, ok := detectGoSQLCall(expr.Node, method, src); ok {
sqlEvents = append(sqlEvents, goSQLEvent{
method: method,
tables: tables,
columns: cols,
query: query,
line: expr.StartLine + 1,
})
}
case m.Captures["var.def"] != nil:
e.emitVar(m, filePath, fileID, result, tenv)
case m.Captures["const.def"] != nil:
e.emitConst(m, filePath, fileID, src, result)
case m.Captures["svar.def"] != nil:
e.recordShortVarType(m, src, tenv)
recordGoGRPCStubVar(m, src, grpcStubVars)
case m.Captures["comp.expr"] != nil:
expr := m.Captures["comp.expr"]
instantiates = append(instantiates, goDeferredTypeRef{
typeName: m.Captures["comp.type"].Text,
line: expr.StartLine + 1,
kind: graph.EdgeInstantiates,
})
case m.Captures["compq.expr"] != nil:
expr := m.Captures["compq.expr"]
instantiates = append(instantiates, goDeferredTypeRef{
typeName: m.Captures["compq.type"].Text,
pkg: m.Captures["compq.pkg"].Text,
line: expr.StartLine + 1,
kind: graph.EdgeInstantiates,
})
case m.Captures["ftype.decl"] != nil:
decl := m.Captures["ftype.decl"]
typeRefs = append(typeRefs, goDeferredTypeRef{
typeName: m.Captures["ftype.name"].Text,
line: decl.StartLine + 1,
kind: graph.EdgeReferences,
})
case m.Captures["ctype.decl"] != nil:
decl := m.Captures["ctype.decl"]
typeRefs = append(typeRefs, goDeferredTypeRef{
typeName: m.Captures["ctype.name"].Text,
line: decl.StartLine + 1,
kind: graph.EdgeReferences,
})
case m.Captures["vtype.decl"] != nil:
decl := m.Captures["vtype.decl"]
typeRefs = append(typeRefs, goDeferredTypeRef{
typeName: m.Captures["vtype.name"].Text,
line: decl.StartLine + 1,
kind: graph.EdgeReferences,
})
case m.Captures["ptype.decl"] != nil:
decl := m.Captures["ptype.decl"]
typeRefs = append(typeRefs, goDeferredTypeRef{
typeName: m.Captures["ptype.name"].Text,
line: decl.StartLine + 1,
kind: graph.EdgeReferences,
})
case m.Captures["assert.decl"] != nil:
decl := m.Captures["assert.decl"]
typeRefs = append(typeRefs, goDeferredTypeRef{
typeName: m.Captures["assert.name"].Text,
line: decl.StartLine + 1,
kind: graph.EdgeReferences,
})
case m.Captures["assertq.decl"] != nil:
decl := m.Captures["assertq.decl"]
typeRefs = append(typeRefs, goDeferredTypeRef{
typeName: m.Captures["assertq.name"].Text,
pkg: m.Captures["assertq.pkg"].Text,
line: decl.StartLine + 1,
kind: graph.EdgeReferences,
})
case m.Captures["selarg.list"] != nil:
list := m.Captures["selarg.list"]
valueSels = append(valueSels, goDeferredValueSel{
field: m.Captures["selarg.field"].Text,
receiver: m.Captures["selarg.receiver"].Text,
line: list.StartLine + 1,
kind: graph.EdgeReads,
})
case m.Captures["identarg.list"] != nil:
list := m.Captures["identarg.list"]
valueIdents = append(valueIdents, goDeferredValueIdent{
name: m.Captures["identarg.name"].Text,
line: list.StartLine + 1,
})
case m.Captures["fieldval.elem"] != nil:
elem := m.Captures["fieldval.elem"]
fieldValIdents = append(fieldValIdents, goDeferredValueIdent{
name: m.Captures["fieldval.value"].Text,
line: elem.StartLine + 1,
})
case m.Captures["fieldstr.elem"] != nil:
elem := m.Captures["fieldstr.elem"]
if typeName := goCompositeLiteralType(elem.Node, src); typeName != "" {
val := m.Captures["fieldstr.val"].Text
if len(val) >= 2 && (val[0] == '"' || val[0] == '`') {
val = val[1 : len(val)-1]
}
executorFields = append(executorFields, goExecutorField{
typeName: typeName,
field: m.Captures["fieldstr.key"].Text,
value: val,
line: elem.StartLine + 1,
})
}
case m.Captures["assign.def"] != nil:
def := m.Captures["assign.def"]
writes = append(writes, goDeferredValueSel{
field: m.Captures["assign.field"].Text,
receiver: m.Captures["assign.receiver"].Text,
line: def.StartLine + 1,
kind: graph.EdgeWrites,
})
case m.Captures["incsel.def"] != nil:
def := m.Captures["incsel.def"]
writes = append(writes, goDeferredValueSel{
field: m.Captures["incsel.field"].Text,
receiver: m.Captures["incsel.receiver"].Text,
line: def.StartLine + 1,
kind: graph.EdgeWrites,
})
case m.Captures["decsel.def"] != nil:
def := m.Captures["decsel.def"]
writes = append(writes, goDeferredValueSel{
field: m.Captures["decsel.field"].Text,
receiver: m.Captures["decsel.receiver"].Text,
line: def.StartLine + 1,
kind: graph.EdgeWrites,
})
case m.Captures["fieldsel.elem"] != nil:
elem := m.Captures["fieldsel.elem"]
fieldValSels = append(fieldValSels, goDeferredValueSel{
field: m.Captures["fieldsel.method"].Text,
receiver: m.Captures["fieldsel.receiver"].Text,
line: elem.StartLine + 1,
kind: graph.EdgeReads,
})
case m.Captures["retsel.list"] != nil:
list := m.Captures["retsel.list"]
valueSels = append(valueSels, goDeferredValueSel{
field: m.Captures["retsel.field"].Text,
receiver: m.Captures["retsel.receiver"].Text,
line: list.StartLine + 1,
kind: graph.EdgeReads,
})
case m.Captures["retident.list"] != nil:
list := m.Captures["retident.list"]
valueIdents = append(valueIdents, goDeferredValueIdent{
name: m.Captures["retident.name"].Text,
line: list.StartLine + 1,
})
}
})
// All function/method nodes have been emitted; now map call sites to
// their enclosing definition.
funcRanges := buildFuncRanges(result)
// lookupRecvType resolves a receiver-expression name first against
// the enclosing function's parameter scope, then the file-wide tenv.
// Per-function scope is needed so calls like `func F(reg *Registry)
// { reg.Register(...) }` get the right `receiver_type` even when
// another function in the same file has a parameter or local named
// `reg` with a different type.
lookupRecvType := func(callerID, name string) (string, bool) {
if callerID != "" {
if scope, ok := paramsByFunc[callerID]; ok {
if t, ok := scope[name]; ok {
return t, true
}
}
}
t, ok := tenv[name]
return t, ok
}
// recvNameByID maps a method node ID to its receiver variable name, so a
// selector call can be classified as invoked on the method's own receiver
// (s.counter.Increment() / s.helper()) — the basis for indirect
// receiver-field-mutation attribution.
recvNameByID := map[string]string{}
recvTypeByID := map[string]string{}
for _, n := range result.Nodes {
if n.Kind == graph.KindMethod && n.Meta != nil {
if rn, _ := n.Meta["recv_name"].(string); rn != "" {
recvNameByID[n.ID] = rn
}
if rt, _ := n.Meta["receiver"].(string); rt != "" {
recvTypeByID[n.ID] = rt
}
}
}
// --- Calls ---
for _, c := range calls {
callerID := findEnclosingFunc(funcRanges, c.line)
if callerID == "" {
continue
}
// gRPC client-stub call: `pkg.New<Service>Client(conn).Method(...)`
// (inline chain) or `stub.Method(...)` where `stub` came from a
// `New<Service>Client` constructor. Emitted to a dedicated
// placeholder that the resolver's ResolveGRPCStubCalls pass lands
// on the server-side handler method. Intercepted ahead of normal
// selector handling so the call doesn't resolve onto the generated
// client-interface method instead of the actual handler.
if c.isSelector {
if svc, ok := goGRPCStubService(c, grpcStubVars); ok {
target := "unresolved::grpc::" + svc + "::" + c.method
edge := &graph.Edge{
From: callerID, To: target,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
Meta: map[string]any{
"via": "grpc.stub",
"grpc_service": svc,
"grpc_method": c.method,
},
}
stampReturnUsage(edge, c.returnUsage)
result.Edges = append(result.Edges, edge)
emitGoSpawnEdge(c, callerID, target, filePath, result)
continue
}
// Temporal workflow → activity / child-workflow dispatch:
// `workflow.ExecuteActivity(ctx, X, ...)` etc. Emitted to a
// dedicated placeholder that the resolver's
// ResolveTemporalCalls pass lands on the activity / workflow
// function the worker has registered under that name.
// Intercepted ahead of normal selector handling so the call
// doesn't resolve onto the SDK's generic `ExecuteActivity`
// helper instead of the actual activity body.
if c.tempKind == "activity" || c.tempKind == "workflow" {
// Wrapper detection: when the dispatch name is a PARAMETER of
// the enclosing function, this function is a dispatch wrapper
// (e.g. executeActivity(ctx, ao, name, …) {
// workflow.ExecuteActivity(ctx, name, …) }). The parameter is
// not a real activity name, so the stub could never resolve —
// suppress the noise and mark the wrapper instead. Propagating
// a caller's literal/const argument into the dispatch
// (cross-file wrapper-FOLLOWING) needs call-argument flow and
// is a deliberate, documented blind spot for now.
if !c.tempEnvDefault {
if names, ok := paramNamesByFunc[callerID]; ok {
if names[c.tempName] {
markGoTemporalWrapper(result, callerID, c.tempKind, c.tempName)
// Emit a wrapper-stub edge that the resolver's
// wrapper-following pass can use as an anchor:
// from=callerID, target=placeholder,
// temporal_name_param=paramName. No literal
// temporal_name resolution here (it's a param);
// the resolver synthesises a proper stub for each
// caller that passes a literal at this position.
target := "unresolved::temporal::" + c.tempKind + "::" + c.tempName
meta := map[string]any{
"via": "temporal.stub",
"temporal_kind": c.tempKind,
"temporal_name": c.tempName,
"temporal_name_param": c.tempName,
}
if c.tempLocal {
meta["temporal_local"] = true
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: target,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
Meta: meta,
})
continue
}
}
}
target := "unresolved::temporal::" + c.tempKind + "::" + c.tempName
meta := map[string]any{
"via": "temporal.stub",
"temporal_kind": c.tempKind,
"temporal_name": c.tempName,
}
if c.tempLocal {
meta["temporal_local"] = true
}
if c.tempEnvDefault {
meta["temporal_name_origin"] = "env_default"
if c.tempEnvSource != "" {
meta["temporal_env_source"] = c.tempEnvSource
}
if c.tempDefaultConst != "" {
meta["temporal_default_const"] = c.tempDefaultConst
}
}
if c.tempVarTrace {
// Variable-traced dispatch name (Cat 2): the name was the
// last intra-procedural assignment to a bare local — a
// best-effort static guess (a later conditional reassign
// may not execute). The resolver lands it at the
// speculative tier, and (when the traced value is a const
// NAME) dereferences it to the literal via the const map.
meta["temporal_name_origin"] = "var_trace"
}
if recvName := recvNameByID[callerID]; recvName != "" {
if arg := goTemporalDispatchArg(c.callNode); arg != nil && arg.Type() == "selector_expression" {
op := arg.ChildByFieldName("operand")
fld := arg.ChildByFieldName("field")
if op != nil && fld != nil && op.Content(src) == recvName {
meta["temporal_name_field"] = fld.Content(src)
if rt := recvTypeByID[callerID]; rt != "" {
meta["temporal_recv_type"] = rt
}
}
}
}
edge := &graph.Edge{
From: callerID, To: target,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
Meta: meta,
}
stampReturnUsage(edge, c.returnUsage)
result.Edges = append(result.Edges, edge)
emitGoSpawnEdge(c, callerID, target, filePath, result)
continue
}
}
var target string
if !c.isSelector {
target = "unresolved::" + c.callName
edge := &graph.Edge{
From: callerID, To: target,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
}
applyGoGRPCRegisterMeta(edge, c, src, tenv)
applyGoTemporalRegisterMeta(edge, c)
applyGoTemporalHandlerMeta(edge, c)
applyGoTemporalSignalQueryMeta(edge, c)
applyGoTemporalStartMeta(edge, c)
stampReturnUsage(edge, c.returnUsage)
attachGoTemporalCallArgNames(edge, c, c.callNode, src, paramNamesByFunc[callerID])
result.Edges = append(result.Edges, edge)
emitGoSpawnEdge(c, callerID, target, filePath, result)
continue
}
if importPath, ok := imports[c.receiver]; ok {
target = "unresolved::extern::" + importPath + "::" + c.method
edge := &graph.Edge{
From: callerID, To: target,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
}
applyGoGRPCRegisterMeta(edge, c, src, tenv)
applyGoTemporalRegisterMeta(edge, c)
applyGoTemporalHandlerMeta(edge, c)
applyGoTemporalSignalQueryMeta(edge, c)
applyGoTemporalStartMeta(edge, c)
stampReturnUsage(edge, c.returnUsage)
attachGoTemporalCallArgNames(edge, c, c.callNode, src, paramNamesByFunc[callerID])
result.Edges = append(result.Edges, edge)
emitGoSpawnEdge(c, callerID, target, filePath, result)
continue
}
target = "unresolved::*." + c.method
edge := &graph.Edge{
From: callerID, To: target,
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
}
// Classify the call against the enclosing method's own receiver so the
// capability synthesizer can attribute indirect field mutations:
// s.counter.Increment() → recv_field=counter (mutates s.counter)
// s.helper() → recv_self=true (mutates s's fields
// transitively via helper)
if recvName := recvNameByID[callerID]; recvName != "" {
if c.receiver == recvName {
edge.Meta = map[string]any{"recv_self": true}
} else if f, ok := ownReceiverField(c.receiver, recvName); ok {
edge.Meta = map[string]any{"recv_field": f}
}
}
if recvType, ok := lookupRecvType(callerID, c.receiver); ok {
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["receiver_type"] = recvType
} else if strings.Contains(c.receiver, ".") || strings.Contains(c.receiver, "(") {
// Chain walk needs both the file-wide tenv and the
// enclosing function's parameter scope as one map. Compose
// without mutating either.
composed := tenv
if scope, ok := paramsByFunc[callerID]; ok && len(scope) > 0 {
composed = make(typeEnv, len(tenv)+len(scope))
for k, v := range tenv {
composed[k] = v
}
for k, v := range scope {
composed[k] = v
}
}
stampFactoryChainReceiver(edge, c.receiver, resolveChainType(c.receiver, composed, result))
}
applyGoGRPCRegisterMeta(edge, c, src, tenv)
applyGoTemporalRegisterMeta(edge, c)
applyGoTemporalSignalQueryMeta(edge, c)
applyGoTemporalStartMeta(edge, c)
stampReturnUsage(edge, c.returnUsage)
attachGoTemporalCallArgNames(edge, c, c.callNode, src, paramNamesByFunc[callerID])
result.Edges = append(result.Edges, edge)
emitGoSpawnEdge(c, callerID, target, filePath, result)
}
// --- Observability events ---
emitGoObservabilityEvents(observabilityEvents,
func(line int) string { return findEnclosingFunc(funcRanges, line) },
filePath, result)
// --- Event pub/sub edges ---
// Transport resolution needs the file's full import set, which the
// tree walk above finished collecting, so it runs here in the
// post-pass alongside the other deferred coverage emitters.
emitGoPubsubEvents(pubsubCandidates, importPathValues(imports),
func(line int) string { return findEnclosingFunc(funcRanges, line) },
filePath, result)
// --- String observations (metrics, error messages, routes) ---
emitGoStringEvents(stringEvents,
func(line int) string { return findEnclosingFunc(funcRanges, line) },
filePath, result)
// --- Feature flag checks ---
emitGoFlagChecks(flagEvents,
func(line int) string { return findEnclosingFunc(funcRanges, line) },
filePath, result)
// --- Config keys (viper) ---
emitGoConfigKeys(configEvents,
func(line int) string { return findEnclosingFunc(funcRanges, line) },
filePath, result)
// --- SQL queries ---
// Dialect inference uses the same imports map the call walker
// already builds. When the file imports a recognised SQL
// driver, KindTable nodes get db::<dialect>::... IDs; otherwise
// they fall back to db::generic::...
emitGoSQLEvents(sqlEvents, inferGoSQLDialect(imports),
func(line int) string { return findEnclosingFunc(funcRanges, line) },
filePath, result)
// --- Composite literals (instantiations) ---
for _, r := range instantiates {
callerID := findEnclosingFunc(funcRanges, r.line)
if callerID == "" {
callerID = filePath
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: "unresolved::" + r.typeName,
Kind: r.kind, FilePath: filePath, Line: r.line,
})
}
// --- Type assertions + declaration type references ---
for _, r := range typeRefs {
callerID := findEnclosingFunc(funcRanges, r.line)
if callerID == "" {
callerID = filePath
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: "unresolved::" + r.typeName,
Kind: r.kind, FilePath: filePath, Line: r.line,
})
}
// --- Function/method values passed as arguments ---
// Selector expression in arg position: h.handleHealth
for _, v := range valueSels {
callerID := findEnclosingFunc(funcRanges, v.line)
if callerID == "" {
callerID = filePath
}
edge := &graph.Edge{
From: callerID, To: "unresolved::*." + v.field,
Kind: v.kind, FilePath: filePath, Line: v.line,
}
if recvType, ok := lookupRecvType(callerID, v.receiver); ok {
edge.Meta = map[string]any{"receiver_type": recvType}
}
result.Edges = append(result.Edges, edge)
}
// Bare identifier in arg position: funcName as a value.
for _, v := range valueIdents {
if isGoBuiltinOrKeyword(v.name) {
continue
}
callerID := findEnclosingFunc(funcRanges, v.line)
if callerID == "" {
callerID = filePath
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: "unresolved::" + v.name,
Kind: graph.EdgeReads, FilePath: filePath, Line: v.line,
})
}
// Bare identifier as struct field value: &X{RunE: runClean}
for _, v := range fieldValIdents {
if isGoBuiltinOrKeyword(v.name) {
continue
}
callerID := findEnclosingFunc(funcRanges, v.line)
if callerID == "" {
callerID = filePath
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID, To: "unresolved::" + v.name,
Kind: graph.EdgeReads, FilePath: filePath, Line: v.line,
})
}
// Selector as struct field value: {Handler: h.handleHealth}
for _, v := range fieldValSels {
callerID := findEnclosingFunc(funcRanges, v.line)
if callerID == "" {
callerID = filePath
}
edge := &graph.Edge{
From: callerID, To: "unresolved::*." + v.field,
Kind: v.kind, FilePath: filePath, Line: v.line,
}
if recvType, ok := lookupRecvType(callerID, v.receiver); ok {
edge.Meta = map[string]any{"receiver_type": recvType}
}
result.Edges = append(result.Edges, edge)
}
// Temporal step/executor field: `ActivityExecutor{ActivityName: "X"}`.
// PURPOSE — emits a marker edge so the resolver can join the string
// literal at the construction site to the dispatch that reads this field.
// RATIONALE — separate pass because it fires on keyed_element nodes, not
// call expressions; the construction site and dispatch site may be in
// different functions or even files.
// KEYWORDS — temporal, executor-field, marker
for _, ef := range executorFields {
callerID := findEnclosingFunc(funcRanges, ef.line)
if callerID == "" {
callerID = filePath
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID,
To: "unresolved::temporal-executor::" + ef.typeName + "::" + ef.field,
Kind: graph.EdgeCalls, FilePath: filePath, Line: ef.line,
Meta: map[string]any{
"via": "temporal.executor-field",
"executor_type": ef.typeName,
"executor_field": ef.field,
"executor_value": ef.value,
},
})
}
// Assignment / inc / dec selector LHS — EdgeWrites from the
// enclosing function to the assigned field. Same resolution path
// as the value-side selectors: the resolver lands on the field
// node when we know the receiver's type, otherwise the edge stays
// unresolved::*.field for downstream cleanup.
for _, v := range writes {
callerID := findEnclosingFunc(funcRanges, v.line)
if callerID == "" {
callerID = filePath
}
edge := &graph.Edge{
From: callerID, To: "unresolved::*." + v.field,
Kind: v.kind, FilePath: filePath, Line: v.line,
}
if recvType, ok := lookupRecvType(callerID, v.receiver); ok {
edge.Meta = map[string]any{"receiver_type": recvType}
}
result.Edges = append(result.Edges, edge)
}
// ORM TableName() override resolution. detectGoORMModel uses gorm's
// snake_case+plural naming convention, but gorm honours an explicit
// `func (T) TableName() string { return "..." }` override. The
// override method emits as a separate KindMethod node, so this
// post-pass runs after every node in the file has been emitted and
// rewires any convention-derived EdgeModelsTable to the literal.
rewireORMTableNameOverrides(root, src, result)
// WebSocket upgrade handlers → real-time endpoint edges.
emitGoWebSocketEdges(src, filePath, result)
// Same-file constant/variable value references → impact-radius reads.
captureValueRefCandidates(result, root, filePath, src)
captureFnValueCandidates(result, root, filePath, src)
// Gin middleware-chain dispatcher + route registrations → resolver hints.
mineGinMiddleware(src, result)
// Composite/generic element type references — generic arguments,
// map key/value, channel element. These positions are dropped by the
// bare-name type passes, so without this find_usages of a type misses
// every place it appears only as an argument/element type.
emitGoTypeArgReferences(root, src, filePath, fileID, funcRanges, result)
// GoFrame reflective routes: g.Meta-tagged request structs → controllers.
captureGoFrameRoutes(result, root, filePath, src)
return result, nil
}
// --- Per-match emit helpers -----------------------------------------
func (e *GoExtractor) emitFunction(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, paramsByFunc map[string]typeEnv, paramNamesByFunc map[string]map[string]bool, imports map[string]string, seenIDs map[string]bool) {
name := m.Captures["func.name"].Text
def := m.Captures["func.def"]
id, ok := disambiguateID(seenIDs, filePath+"::"+name, def.StartLine+1)
if !ok {
return
}
if pc, ok := m.Captures["func.params"]; ok && pc != nil {
if names := extractGoParamNames(pc.Node, src); len(names) > 0 {
paramNamesByFunc[id] = names
}
}
node := &graph.Node{
ID: id,
Kind: graph.KindFunction,
Name: name,
FilePath: filePath,
StartLine: def.StartLine + 1,
EndLine: def.EndLine + 1,
Language: "go",
Meta: make(map[string]any),
}
node.Meta["signature"] = buildFuncSignature(name, m.Captures["func.params"], m.Captures["func.result"])
if paramsCap, ok := m.Captures["func.params"]; ok && paramsCap != nil {
if scope := extractGoParamTypes(paramsCap.Node, src); len(scope) > 0 {
paramsByFunc[id] = scope
}
}
if resultCap, ok := m.Captures["func.result"]; ok && resultCap.Text != "" {
if rt := normalizeGoTypeName(resultCap.Text); rt != "" {
node.Meta["return_type"] = rt
}
}
if tp := goTypeParams(def.Node, src); len(tp) > 0 {
node.Meta["type_params"] = tp
}
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
node.Meta["doc"] = doc
}
node.Meta["visibility"] = VisibilityByCase(name)
if body := goFuncBody(def.Node); body != nil {
StampFunctionMetrics(node, body, "go")
StampLoopSignals(node, body, src, "go")
}
scanGoPragmas(src, def.StartLine, node)
result.Nodes = append(result.Nodes, node)
// Record func-returning-literal into ConstValues sidecar for const-deref dispatch.
if v, ok := goFuncSingleReturnLiteral(def.Node, src); ok {
result.ConstValues = append(result.ConstValues, parser.ConstValue{
NodeID: id, FilePath: filePath, Value: v,
})
}
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
emitGoThrowsEdges(node, m.Captures["func.result"], filePath, result)
emitGoFunctionShape(id, def.Node, m.Captures["func.params"], m.Captures["func.result"],
src, filePath, def.StartLine+1, imports, result)
}
// goFuncBody returns the `block` body child of a function/method
// declaration node, or nil for declarations without a body (interface
// method shapes, abstract decls). Used by complexity counting.
func goFuncBody(decl *sitter.Node) *sitter.Node {
if decl == nil {
return nil
}
if b := decl.ChildByFieldName("body"); b != nil {
return b
}
for i, _nc := 0, int(decl.ChildCount()); i < _nc; i++ {
c := decl.Child(i)
if c != nil && c.Type() == "block" {
return c
}
}
return nil
}
// ownReceiverField returns the single field name when a selector-call receiver
// is exactly `<recvName>.<field>` — the one-hop case `s.counter.Increment()`
// where counter is a field of the enclosing method's own receiver. Deeper
// chains (s.a.b) and call expressions return ok=false so the indirect-mutation
// synthesizer never over-attributes past the first own-receiver hop.
func ownReceiverField(receiver, recvName string) (string, bool) {
prefix := recvName + "."
if !strings.HasPrefix(receiver, prefix) {
return "", false
}
rest := receiver[len(prefix):]
if rest == "" || strings.ContainsAny(rest, ".([ ") {
return "", false
}
return rest, true
}
func (e *GoExtractor) emitMethod(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, paramsByFunc map[string]typeEnv, paramNamesByFunc map[string]map[string]bool, imports map[string]string, seenIDs map[string]bool) {
name := m.Captures["method.name"].Text
def := m.Captures["method.def"]
receiverText := m.Captures["method.receiver"].Text
receiverType := extractReceiverType(receiverText)
id, idOK := disambiguateID(seenIDs, filePath+"::"+receiverType+"."+name, def.StartLine+1)
if !idOK {
return
}
if paramsCap, ok := m.Captures["method.params"]; ok && paramsCap != nil {
if names := extractGoParamNames(paramsCap.Node, src); len(names) > 0 {
paramNamesByFunc[id] = names
}
}
scope := typeEnv{}
if recvName := extractReceiverName(receiverText); recvName != "" && receiverType != "" {
scope[recvName] = receiverType
}
if paramsCap, ok := m.Captures["method.params"]; ok && paramsCap != nil {
for k, v := range extractGoParamTypes(paramsCap.Node, src) {
scope[k] = v
}
}
if len(scope) > 0 {
paramsByFunc[id] = scope
}
node := &graph.Node{
ID: id,
Kind: graph.KindMethod,
Name: name,
FilePath: filePath,
StartLine: def.StartLine + 1,
EndLine: def.EndLine + 1,
Language: "go",
Meta: map[string]any{
"receiver": receiverType,
},
}
if recvName := extractReceiverName(receiverText); recvName != "" {
node.Meta["recv_name"] = recvName
}
node.Meta["signature"] = buildMethodSignature(receiverText, name, m.Captures["method.params"], m.Captures["method.result"])
if resultCap, ok := m.Captures["method.result"]; ok && resultCap.Text != "" {
if rt := normalizeGoTypeName(resultCap.Text); rt != "" {
node.Meta["return_type"] = rt
}
}
if tp := goTypeParams(def.Node, src); len(tp) > 0 {
node.Meta["type_params"] = tp
}
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
node.Meta["doc"] = doc
}
node.Meta["visibility"] = VisibilityByCase(name)
if body := goFuncBody(def.Node); body != nil {
StampFunctionMetrics(node, body, "go")
StampLoopSignals(node, body, src, "go")
}
scanGoPragmas(src, def.StartLine, node)
result.Nodes = append(result.Nodes, node)
// Record method-returning-literal into ConstValues sidecar for const-deref dispatch.
if v, ok := goFuncSingleReturnLiteral(def.Node, src); ok {
result.ConstValues = append(result.ConstValues, parser.ConstValue{
NodeID: id, FilePath: filePath, Value: v,
})
}
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
typeID := filePath + "::" + receiverType
result.Edges = append(result.Edges, &graph.Edge{
From: id, To: typeID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: def.StartLine + 1,
})
emitGoThrowsEdges(node, m.Captures["method.result"], filePath, result)
emitGoFunctionShape(id, def.Node, m.Captures["method.params"], m.Captures["method.result"],
src, filePath, def.StartLine+1, imports, result)
}
// goTypeParams reads the `type_parameters` child of a Go declaration
// node (function_declaration, method_declaration, type_spec) and
// returns the declared type parameters as a list of {name, bound}
// maps. Multi-name parameter declarations like `[T, U comparable]`
// produce one entry per name, each with the same bound. Returns nil
// when the declaration is not generic.
func goTypeParams(decl *sitter.Node, src []byte) []map[string]string {
if decl == nil {
return nil
}
tps := decl.ChildByFieldName("type_parameters")
if tps == nil {
// type_spec uses a different field shape — fall back to a
// child-type scan.
for i, _nc := 0, int(decl.ChildCount()); i < _nc; i++ {
c := decl.Child(i)
if c != nil && c.Type() == "type_parameter_list" {
tps = c
break
}
}
}
if tps == nil {
return nil
}
var out []map[string]string
for i, _nc := 0, int(tps.NamedChildCount()); i < _nc; i++ {
pd := tps.NamedChild(i)
if pd == nil {
continue
}
if pd.Type() != "parameter_declaration" && pd.Type() != "type_parameter_declaration" {
continue
}
// One parameter_declaration may carry multiple identifier
// names that share a single type/bound:
// `[T, U comparable]` → two names, one bound.
var names []string
var bound string
// Names appear via ChildByFieldName("name") — Go grammar uses
// field 'name' for the leading identifier list. For multi-name
// declarations the grammar emits multiple entries with the
// same field name; we walk children to find them all.
for j, _nc := 0, int(pd.ChildCount()); j < _nc; j++ {
c := pd.Child(j)
if c == nil {
continue
}
t := c.Type()
if t == "identifier" || t == "field_identifier" || t == "type_identifier" {
names = append(names, c.Content(src))
}
}
if tn := pd.ChildByFieldName("type"); tn != nil {
bound = strings.TrimSpace(tn.Content(src))
}
for _, n := range names {
entry := map[string]string{"name": n}
if bound != "" {
entry["bound"] = bound
}
out = append(out, entry)
}
}
return out
}
// emitGoThrowsEdges inspects the result-type capture and emits an
// EdgeThrows edge when the function returns an error. Two cases:
//
// - last return type is the bare `error` interface → edge to the
// synthetic external::error sentinel so reverse-walks land on
// a single node regardless of file/repo.
// - last return type is a custom error type (`*MyErr`, `MyErr`)
// → edge to that type, resolved by name later.
//
// Functions that return only non-error types produce no edge.
func emitGoThrowsEdges(node *graph.Node, resultCap *parser.CapturedNode, filePath string, result *parser.ExtractionResult) {
if resultCap == nil || resultCap.Text == "" {
return
}
errType := parseLastReturnTypeForError(resultCap.Text)
if errType == "" {
return
}
target := "external::error"
if errType != "error" {
target = "unresolved::" + errType
}
result.Edges = append(result.Edges, &graph.Edge{
From: node.ID,
To: target,
Kind: graph.EdgeThrows,
FilePath: filePath,
Line: node.StartLine,
Origin: graph.OriginASTInferred,
})
}
// parseLastReturnTypeForError pulls the last identifier from a Go
// result type expression and returns it when it looks like an error
// type. Recognises the bare `error` interface plus the conventional
// `*MyError` / `MyError` shapes. Returns "" for non-error returns.
func parseLastReturnTypeForError(result string) string {
s := strings.TrimSpace(result)
if s == "" {
return ""
}
// Strip parens for tuple returns like `(int, error)`.
if strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")") {
s = s[1 : len(s)-1]
}
parts := strings.Split(s, ",")
last := strings.TrimSpace(parts[len(parts)-1])
if last == "" {
return ""
}
// Strip leading `*` for pointer returns.
last = strings.TrimPrefix(last, "*")
// Take the rightmost identifier of `pkg.Foo`.
if i := strings.LastIndex(last, "."); i >= 0 {
last = last[i+1:]
}
// Strip generic instantiation suffix `[T]`.
if i := strings.Index(last, "["); i >= 0 {
last = last[:i]
}
last = strings.TrimSpace(last)
if last == "error" {
return "error"
}
// Heuristic: identifier ending in "Error" or "Err" — common Go
// error type convention. Avoids false positives on things like
// `Result` or `Response` while catching MyError, ParseErr, etc.
if strings.HasSuffix(last, "Error") || strings.HasSuffix(last, "Err") {
return last
}
return ""
}
// emitTypeDecl handles the generic `type X <body>` form. The body node
// discriminates struct vs interface vs named primitive — interfaces
// carry their method-signature set in Meta for structural inference.
func (e *GoExtractor) emitTypeDecl(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
name := m.Captures["typedef.name"].Text
if seen[name] {
return
}
seen[name] = true
def := m.Captures["typedef.def"]
body := m.Captures["typedef.body"]
id := filePath + "::" + name
node := &graph.Node{
ID: id, Name: name,
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
Language: "go",
}
isStruct := false
isInterface := false
if body != nil && body.Node != nil && body.Node.Type() == "interface_type" {
node.Kind = graph.KindInterface
node.Meta = map[string]any{"methods": extractInterfaceMethods(body.Node, src)}
isInterface = true
} else {
node.Kind = graph.KindType
node.Meta = map[string]any{}
if body != nil && body.Node != nil && body.Node.Type() == "struct_type" {
isStruct = true
}
}
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
node.Meta["doc"] = doc
}
node.Meta["visibility"] = VisibilityByCase(name)
// Structural flavor: struct/interface bodies are discriminated above;
// everything else funnelling through `type X <named>` is a newtype.
flavor := "newtype"
if isInterface {
flavor = "interface"
} else if isStruct {
flavor = "struct"
}
node.Meta["type_flavor"] = flavor
result.Nodes = append(result.Nodes, node)
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
if isStruct {
emitGoStructFields(body.Node, src, id, name, filePath, fileID, result)
// ORM model attribution: emit EdgeModelsTable when the struct
// carries gorm tags or embeds gorm.Model. Runs after field
// emission so the field metadata is in place for downstream
// analyzers that walk both edges together.
detectGoORMModel(body.Node, src, id, name, filePath, result)
}
if isInterface {
for _, embed := range extractEmbeddedInterfaceTypes(body.Node, src) {
result.Edges = append(result.Edges, &graph.Edge{
From: id,
To: "unresolved::" + embed,
Kind: graph.EdgeComposes,
FilePath: filePath,
Line: def.StartLine + 1,
Origin: graph.OriginASTInferred,
})
}
}
// Newtype `type X Y` where Y is a named type produces an
// EdgeExtends so blast-radius walks catch the dependency. Struct
// and interface bodies don't get an extends edge — their member
// fields/methods carry the reference set instead.
if !isStruct && body != nil && body.Node != nil {
if isExtendsNewtypeBody(body.Node.Type()) {
if target := canonicalizeGoTypeRef(body.Text); target != "" {
result.Edges = append(result.Edges, &graph.Edge{
From: id,
To: "unresolved::" + target,
Kind: graph.EdgeExtends,
FilePath: filePath,
Line: def.StartLine + 1,
Origin: graph.OriginASTInferred,
})
}
}
}
}
// isExtendsNewtypeBody reports whether a type_spec body's AST node
// is a named-type reference that should produce an EdgeExtends edge.
// Composite anonymous bodies (struct_type / interface_type / map_type
// / func_type) are excluded — their member set carries the
// references; emitting an extends edge to "anonymous" provides no
// additional signal.
func isExtendsNewtypeBody(t string) bool {
switch t {
case "type_identifier", "qualified_type", "pointer_type",
"slice_type", "array_type", "channel_type", "generic_type":
return true
}
return false
}
// emitGoStructFields walks a struct_type node's field_declaration list
// and emits one KindField node per declared field. Each field gets a
// MemberOf edge to its owner type and a Defines edge from the file.
// Embedded fields (anonymous struct/interface inclusion) emit a single
// field node named after the embedded type.
func emitGoStructFields(structNode *sitter.Node, src []byte, ownerID, ownerName, filePath, fileID string, result *parser.ExtractionResult) {
if structNode == nil {
return
}
var fieldList *sitter.Node
for i, _nc := 0, int(structNode.ChildCount()); i < _nc; i++ {
c := structNode.Child(i)
if c != nil && c.Type() == "field_declaration_list" {
fieldList = c
break
}
}
if fieldList == nil {
return
}
for i, _nc := 0, int(fieldList.NamedChildCount()); i < _nc; i++ {
decl := fieldList.NamedChild(i)
if decl == nil || decl.Type() != "field_declaration" {
continue
}
// Walk the field_declaration's children once: collect
// field_identifier names and the trailing type node. The
// grammar exposes both via ChildByFieldName, but real-world
// trees contain a mix of named/positional children for
// embedded vs explicit fields, so a manual walk is the
// reliable form.
var nameNodes []*sitter.Node
var typeNode *sitter.Node
for j, _nc := 0, int(decl.NamedChildCount()); j < _nc; j++ {
c := decl.NamedChild(j)
if c == nil {
continue
}
switch c.Type() {
case "field_identifier":
nameNodes = append(nameNodes, c)
case "type_identifier", "qualified_type", "pointer_type",
"generic_type", "slice_type", "array_type", "map_type",
"channel_type", "function_type", "interface_type",
"struct_type":
if typeNode == nil {
typeNode = c
}
}
}
var fieldType string
if typeNode != nil {
// Keep the verbatim type text — primitives ("string",
// "int", "[]byte", etc.) are valid field types and
// agents want to see them. normalizeGoTypeName drops
// primitives because it's tuned for receiver-type
// resolution; field metadata has different needs.
fieldType = strings.TrimSpace(typeNode.Content(src))
}
if len(nameNodes) > 0 {
for _, nm := range nameNodes {
emitGoFieldNode(decl, nm, nm.Content(src), fieldType, ownerID, ownerName, filePath, fileID, src, result)
}
continue
}
// Embedded field: type itself is the field name.
if typeNode != nil {
if fieldName := embeddedFieldName(typeNode, src); fieldName != "" {
emitGoFieldNode(decl, typeNode, fieldName, fieldType, ownerID, ownerName, filePath, fileID, src, result)
// Composition signal: the owning type embeds another
// type. Distinct from EdgeExtends (newtype) and from
// the field's MemberOf — gives blast-radius walks
// "what types are composed into X" in one hop.
if target := canonicalizeGoTypeRef(fieldType); target != "" {
result.Edges = append(result.Edges, &graph.Edge{
From: ownerID,
To: "unresolved::" + target,
Kind: graph.EdgeComposes,
FilePath: filePath,
Line: int(typeNode.StartPoint().Row) + 1,
Origin: graph.OriginASTInferred,
})
}
}
}
}
}
func emitGoFieldNode(decl, anchor *sitter.Node, fieldName, fieldType, ownerID, ownerName, filePath, fileID string, src []byte, result *parser.ExtractionResult) {
id := filePath + "::" + ownerName + "." + fieldName
startLine := int(anchor.StartPoint().Row) + 1
endLine := int(decl.EndPoint().Row) + 1
meta := map[string]any{
"receiver": ownerName,
"visibility": VisibilityByCase(fieldName),
}
if fieldType != "" {
meta["field_type"] = fieldType
}
if doc := ExtractDocAbove(src, int(anchor.StartPoint().Row), DocLangSlashSlash); doc != "" {
meta["doc"] = doc
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindField, Name: fieldName,
FilePath: filePath, StartLine: startLine, EndLine: endLine,
Language: "go", Meta: meta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine,
})
result.Edges = append(result.Edges, &graph.Edge{
From: id, To: ownerID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine,
})
}
// embeddedFieldName returns the trailing identifier of a Go embedded
// field type. Strips one leading `*` for pointer-embedded fields and
// drops the package qualifier. Returns "" when no identifier is found.
func embeddedFieldName(typeNode *sitter.Node, src []byte) string {
if typeNode == nil {
return ""
}
switch typeNode.Type() {
case "type_identifier":
return typeNode.Content(src)
case "pointer_type":
// Recurse into the pointed-to type.
for i, _nc := 0, int(typeNode.NamedChildCount()); i < _nc; i++ {
if n := embeddedFieldName(typeNode.NamedChild(i), src); n != "" {
return n
}
}
case "qualified_type":
// pkg.Foo — take the trailing identifier.
for i := int(typeNode.NamedChildCount()) - 1; i >= 0; i-- {
c := typeNode.NamedChild(i)
if c != nil && c.Type() == "type_identifier" {
return c.Content(src)
}
}
case "generic_type":
// Foo[T] — name is the inner type_identifier.
for i, _nc := 0, int(typeNode.NamedChildCount()); i < _nc; i++ {
if n := embeddedFieldName(typeNode.NamedChild(i), src); n != "" {
return n
}
}
}
return ""
}
func (e *GoExtractor) emitTypeAlias(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
name := m.Captures["alias.name"].Text
if seen[name] {
return
}
seen[name] = true
def := m.Captures["alias.def"]
id := filePath + "::" + name
node := &graph.Node{
ID: id, Kind: graph.KindType, Name: name,
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
Language: "go",
Meta: map[string]any{
// Mark this node as an alias so consumers can scope queries
// without inspecting the edge set. Newtypes get no flag —
// the absence is the signal.
"alias": true,
},
}
if doc := ExtractDocAbove(src, def.StartLine, DocLangSlashSlash); doc != "" {
node.Meta["doc"] = doc
}
node.Meta["visibility"] = VisibilityByCase(name)
node.Meta["type_flavor"] = "type_alias"
result.Nodes = append(result.Nodes, node)
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
// EdgeAliases captures the `type X = Y` relationship distinctly
// from EdgeExtends (newtype). Renaming Y is a no-op for X's
// callers in the alias case but breaks promoted method sets in
// the newtype case — agents need to distinguish.
if aliasType, ok := m.Captures["alias.type"]; ok && aliasType != nil {
if target := canonicalizeGoTypeRef(aliasType.Text); target != "" {
result.Edges = append(result.Edges, &graph.Edge{
From: id,
To: "unresolved::" + target,
Kind: graph.EdgeAliases,
FilePath: filePath,
Line: def.StartLine + 1,
Origin: graph.OriginASTInferred,
})
}
}
}
// emitImport records an import edge and populates the alias→path map
// used when classifying selector calls against imported packages. Blank
// and dot imports are skipped in the map (they don't introduce a
// callable identifier) but still produce EdgeImports.
//
// In addition to the existing file→import edge, emits a per-import
// node (KindImport) with Meta carrying the import path, alias (if
// renamed), and is_external flag. Lets agents query "what does this
// file import from <pkg>" with one graph hop.
func (e *GoExtractor) emitImport(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult, imports map[string]string, fileNode *graph.Node) {
pathCap := m.Captures["import.path"]
importPath := strings.Trim(pathCap.Text, `"`)
line := pathCap.StartLine + 1
// Mark the file as a cgo user when it imports "C" — the
// pseudo-import that triggers Go's cgo preprocessor and
// signals the file has a C source dependency. Agents asking
// "which files would break if we removed cgo" or "what's the
// porting surface to a non-cgo build" can then filter by
// this meta flag without parsing source.
if importPath == "C" && fileNode != nil {
if fileNode.Meta == nil {
fileNode.Meta = map[string]any{}
}
fileNode.Meta["uses_cgo"] = true
}
rawAlias := ""
if a, ok := m.Captures["import.alias"]; ok {
rawAlias = strings.TrimSpace(a.Text)
}
displayName := rawAlias
mapAlias := rawAlias
switch rawAlias {
case "_", ".":
// Blank and dot imports keep their special behaviour for
// the alias map (no callable identifier introduced) but the
// import node still gets emitted so reverse-walks work.
displayName = importPath
if i := strings.LastIndex(importPath, "/"); i >= 0 {
displayName = importPath[i+1:]
}
case "":
displayName = importPath
if i := strings.LastIndex(importPath, "/"); i >= 0 {
displayName = importPath[i+1:]
}
mapAlias = displayName
}
importNodeID := filePath + "::import::" + importPath
importMeta := map[string]any{
"path": importPath,
"is_external": isExternalGoImport(importPath),
}
if rawAlias != "" {
importMeta["alias"] = rawAlias
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: importNodeID,
Kind: graph.KindImport,
Name: displayName,
FilePath: filePath,
StartLine: line,
EndLine: line,
Language: "go",
Meta: importMeta,
})
// File → import-node edge. EdgeContains is the semantic fit (the
// file *contains* an import statement; it doesn't *define* the
// imported package). The disk-backed GetFileSubGraph walks
// EdgeDefines EdgeContains from the file node to enumerate the
// full neighbourhood in one edge-index pass.
result.Edges = append(result.Edges, &graph.Edge{
From: fileID,
To: importNodeID,
Kind: graph.EdgeContains,
FilePath: filePath,
Line: line,
})
// Existing file → unresolved import-path edge for resolver
// behaviour (downstream code resolves the path to the imported
// repo's file node). Kept additive so consumers that read
// EdgeImports keep working unchanged.
result.Edges = append(result.Edges, &graph.Edge{
From: fileID,
To: "unresolved::import::" + importPath,
Kind: graph.EdgeImports,
FilePath: filePath,
Line: line,
})
if rawAlias == "_" || rawAlias == "." {
return
}
imports[mapAlias] = importPath
}
// isExternalGoImport returns true when the import path doesn't look
// like a stdlib import. Heuristic: the first path segment contains a
// dot — i.e. it's a module path like `github.com/...` or
// `golang.org/...`. Stdlib paths (`fmt`, `os/exec`) have no dot in
// the first segment.
func isExternalGoImport(path string) bool {
if path == "" {
return false
}
first := path
if i := strings.Index(path, "/"); i >= 0 {
first = path[:i]
}
return strings.Contains(first, ".")
}
func (e *GoExtractor) emitVar(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult, tenv typeEnv) {
nameCap := m.Captures["var.name"]
def := m.Captures["var.def"]
if nameCap == nil || nameCap.Text == "" || nameCap.Text == "_" {
return
}
name := nameCap.Text
id := filePath + "::" + name
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: graph.KindVariable, Name: name,
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
Language: "go",
Meta: map[string]any{"visibility": VisibilityByCase(name)},
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
if typeCap, ok := m.Captures["var.type"]; ok && typeCap.Text != "" {
if typeName := normalizeGoTypeName(typeCap.Text); typeName != "" {
tenv[name] = typeName
}
}
}
func (e *GoExtractor) emitConst(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult) {
nameCap := m.Captures["const.name"]
def := m.Captures["const.def"]
if nameCap == nil || nameCap.Text == "" || nameCap.Text == "_" {
return
}
name := nameCap.Text
id := filePath + "::" + name
// Detect Go's enum-by-convention: a const_declaration block that
// contains the `iota` identifier anywhere within its body marks
// every spec in that block as an enum member. The heuristic
// matches both the explicit form (`Red Color = iota`) and the
// implicit-continuation form (`Red Color = iota; Green; Blue`),
// which share the same enclosing block. False positives are
// possible if the user happens to write `iota` inside a const
// block for an unrelated reason — vanishingly rare in practice.
kind := graph.KindConstant
if def != nil && def.Node != nil && containsGoIotaBlock(def.Text) {
kind = graph.KindEnumMember
}
meta := map[string]any{"visibility": VisibilityByCase(name)}
// Const-to-const alias (Cat 3): `const ALIAS = RealName` carries no
// literal of its own, so record the referenced const NAME. The
// resolver's const-deref map collapses alias chains (ALIAS = REAL =
// "lit") against the constant_values sidecar by a bounded fixpoint, so
// an ALL_CAPS dispatch name that is itself a const alias still
// dereferences to the underlying literal. A const WITH a literal value
// is handled by the constant_values sidecar below, not here.
if def != nil && def.Node != nil {
if _, ok := goConstLiteralValue(def.Node, name, src); !ok {
if ref, ok := goConstRefName(def.Node, name, src); ok {
meta["const_ref"] = ref
}
}
}
result.Nodes = append(result.Nodes, &graph.Node{
ID: id, Kind: kind, Name: name,
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
Language: "go",
Meta: meta,
})
result.Edges = append(result.Edges, &graph.Edge{
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
})
// Retain the literal value (string / numeric) for the queryable
// constant_values sidecar, so the resolver can dereference a
// const-identifier dispatch name to its value across files. Computed
// constants (iota, expressions) carry no literal and are skipped.
if kind == graph.KindConstant {
if v, ok := goConstLiteralValue(def.Node, name, src); ok {
result.ConstValues = append(result.ConstValues, parser.ConstValue{
NodeID: id, FilePath: filePath, Value: v,
})
}
}
}
// PURPOSE — reports the string literal a function body returns when the body
// is exactly one `return "<literal>"` statement. Used by the const-value
// sidecar to record func-returning-constant nodes for Temporal dispatch
// resolution without adding a new resolver pass.
// RATIONALE — mirrors goConstLiteralValue for the function case.
// KEYWORDS — temporal, const-deref, single-return-literal
func goFuncSingleReturnLiteral(declNode *sitter.Node, src []byte) (string, bool) {
if declNode == nil {
return "", false
}
body := declNode.ChildByFieldName("body")
if body == nil || body.Type() != "block" {
return "", false
}
// The Go grammar wraps a block's statements in a `statement_list` node;
// descend into it when present so the single-statement scan sees the
// real statements rather than the wrapper.
stmtParent := body
if body.NamedChildCount() == 1 {
if only := body.NamedChild(0); only != nil && only.Type() == "statement_list" {
stmtParent = only
}
}
var ret *sitter.Node
stmts := 0
for i, _nc := 0, int(stmtParent.NamedChildCount()); i < _nc; i++ {
c := stmtParent.NamedChild(i)
if c == nil || c.Type() == "comment" {
continue
}
stmts++
if c.Type() == "return_statement" {
ret = c
}
}
if stmts != 1 || ret == nil {
return "", false
}
// return_statement -> expression_list -> single string literal
var expr *sitter.Node
for i, _nc := 0, int(ret.NamedChildCount()); i < _nc; i++ {
n := ret.NamedChild(i)
if n == nil {
continue
}
if n.Type() == "expression_list" {
if n.NamedChildCount() != 1 {
return "", false
}
expr = n.NamedChild(0)
} else {
expr = n
}
}
if expr == nil {
return "", false
}
switch expr.Type() {
case "interpreted_string_literal", "raw_string_literal":
t := expr.Content(src)
if len(t) >= 2 && (t[0] == '"' || t[0] == '`') {
return t[1 : len(t)-1], true
}
return t, true
}
return "", false
}
// goConstLiteralValue extracts the literal value of a const_spec
// (`const X = "literal"` / `const X = 42`) from the spec's value field,
// when that value is a string or numeric literal. Returns ("", false)
// for computed / multi-value / non-literal specs.
//
// constSpec may be the const_spec itself or the enclosing
// const_declaration. For a grouped block (`const ( A = ...; B = ... )`)
// the declaration holds several specs; name selects the matching one so
// each member's value is captured independently (not just single-spec
// blocks). For a single-spec block name still selects correctly.
func goConstLiteralValue(constSpec *sitter.Node, name string, src []byte) (string, bool) {
if constSpec == nil {
return "", false
}
spec := constSpec
if spec.Type() != "const_spec" {
// def captures the const_declaration; pick the spec whose name
// field matches the const being emitted (grouped blocks hold
// several), falling back to the lone spec when there is exactly
// one and no name match (defensive).
var found, only *sitter.Node
count := 0
for i, _nc := 0, int(spec.NamedChildCount()); i < _nc; i++ {
c := spec.NamedChild(i)
if c == nil || c.Type() != "const_spec" {
continue
}
count++
only = c
if nameNode := c.ChildByFieldName("name"); nameNode != nil && nameNode.Content(src) == name {
found = c
break
}
}
switch {
case found != nil:
spec = found
case count == 1 && only != nil:
spec = only
default:
return "", false
}
}
valueList := spec.ChildByFieldName("value")
if valueList == nil || valueList.NamedChildCount() != 1 {
return "", false
}
v := valueList.NamedChild(0)
if v == nil {
return "", false
}
switch v.Type() {
case "interpreted_string_literal", "raw_string_literal":
return goTemporalNameFromExpr(v, src), true
case "int_literal", "float_literal":
return v.Content(src), true
}
return "", false
}
// goConstRefName returns the NAME of the constant that `name`'s value is a
// plain one-hop reference to (`const ALIAS = RealName`), or ("", false) when
// the value is not a single bare-identifier reference. The resolver aliases
// one constant's value to another via the constant_values sidecar (chains
// collapsed by a bounded fixpoint), so an ALL_CAPS dispatch name that is
// itself a const alias still resolves to the underlying literal.
func goConstRefName(constDecl *sitter.Node, name string, src []byte) (string, bool) {
if constDecl == nil {
return "", false
}
for i, _nc := 0, int(constDecl.NamedChildCount()); i < _nc; i++ {
spec := constDecl.NamedChild(i)
if spec == nil || spec.Type() != "const_spec" {
continue
}
nameNode := spec.ChildByFieldName("name")
if nameNode == nil || nameNode.Content(src) != name {
continue
}
val := spec.ChildByFieldName("value")
if val == nil {
return "", false
}
expr := val
if val.Type() == "expression_list" {
expr = val.NamedChild(0)
}
if expr == nil || expr.Type() != "identifier" {
return "", false
}
ref := expr.Content(src)
if ref == "" || ref == name {
return "", false
}
return ref, true
}
return "", false
}
// containsGoIotaBlock reports whether a const_declaration's source
// text contains the bare `iota` identifier. Word-boundary anchoring
// keeps `bigiotabig` and similar identifier substrings from
// triggering. Simpler than walking the AST for a one-off detection.
func containsGoIotaBlock(text string) bool {
if text == "" {
return false
}
for i := 0; i+4 <= len(text); i++ {
if text[i:i+4] != "iota" {
continue
}
before := byte(' ')
if i > 0 {
before = text[i-1]
}
after := byte(' ')
if i+4 < len(text) {
after = text[i+4]
}
if !isGoIdentByte(before) && !isGoIdentByte(after) {
return true
}
}
return false
}
func isGoIdentByte(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_'
}
// recordShortVarType infers a type from the RHS expression of a short
// variable declaration (`x := NewFoo()` or `x := &Foo{...}`) and records
// it in tenv so subsequent selector-call edges attach a receiver_type
// meta. No graph output — short-var-declared locals are not first-class
// nodes in the current schema.
func (e *GoExtractor) recordShortVarType(m parser.QueryResult, src []byte, tenv typeEnv) {
name := m.Captures["svar.name"].Text
valueCap := m.Captures["svar.value"]
if valueCap == nil || valueCap.Node == nil {
return
}
if inferred := inferTypeFromGoExpr(valueCap.Node, src); inferred != "" {
tenv[name] = inferred
}
}
// --- gRPC stub call extraction (M4) --------------------------------
//
// The Go gRPC code generator produces a stable surface that lets us
// resolve cross-service RPC calls structurally, without a type checker:
//
// - Client construction: `pkg.New<Service>Client(conn)` returns a
// `<Service>Client` stub interface.
// - RPC invocation: `stub.<Method>(ctx, req, ...)`.
// - Server registration: `pkg.Register<Service>Server(registrar, impl)`
// binds a concrete impl type to the service.
//
// recordGoGRPCStubVar / goGRPCStubService recognise the consumer side
// and route each RPC call to an `unresolved::grpc::<Service>::<Method>`
// placeholder; parseGoGRPCRegister recognises the server side and
// stamps `grpc_register_*` meta on the registration call edge. The
// resolver's ResolveGRPCStubCalls pass joins the two.
// recordGoGRPCStubVar inspects a short-var declaration's RHS for the
// gRPC client-stub constructor convention `pkg.New<Service>Client(conn)`
// and, when matched, records `varName → serviceName` so later method
// calls on that variable resolve as gRPC stub calls.
func recordGoGRPCStubVar(m parser.QueryResult, src []byte, stubs map[string]string) {
nameCap := m.Captures["svar.name"]
valueCap := m.Captures["svar.value"]
if nameCap == nil || valueCap == nil || valueCap.Node == nil {
return
}
if svc, ok := goGRPCServiceFromConstructor(valueCap.Node, src); ok {
stubs[nameCap.Text] = svc
}
}
// goGRPCServiceFromConstructor returns the gRPC service name when node
// is a `New<Service>Client(...)` call expression (qualified or not).
func goGRPCServiceFromConstructor(node *sitter.Node, src []byte) (string, bool) {
if node == nil || node.Type() != "call_expression" {
return "", false
}
fn := node.ChildByFieldName("function")
if fn == nil {
return "", false
}
var name string
switch fn.Type() {
case "identifier":
name = fn.Content(src)
case "selector_expression":
if field := fn.ChildByFieldName("field"); field != nil {
name = field.Content(src)
}
}
return grpcServiceFromClientCtor(name)
}
// grpcServiceFromClientCtor maps `New<Service>Client` → `<Service>`.
func grpcServiceFromClientCtor(name string) (string, bool) {
const pfx, sfx = "New", "Client"
if len(name) <= len(pfx)+len(sfx) {
return "", false
}
if !strings.HasPrefix(name, pfx) || !strings.HasSuffix(name, sfx) {
return "", false
}
return name[len(pfx) : len(name)-len(sfx)], true
}
// goGRPCStubService reports whether a selector call's receiver is a
// gRPC client stub — either a variable seeded by recordGoGRPCStubVar,
// or an inline `pkg.New<Service>Client(conn)` chain — and returns the
// service name.
func goGRPCStubService(c goDeferredCall, stubVars map[string]string) (string, bool) {
if svc, ok := stubVars[c.receiver]; ok {
return svc, true
}
return grpcServiceFromInlineChain(c.receiver)
}
// grpcServiceFromInlineChain matches the inline-chain receiver text
// `pkg.New<Service>Client(...)` (or unqualified) and returns the
// service. The receiver must be exactly one balanced call expression —
// a longer chain like `New<Service>Client(c).Foo()` is rejected so the
// trailing method isn't mistaken for an RPC on the stub.
func grpcServiceFromInlineChain(receiver string) (string, bool) {
open := strings.IndexByte(receiver, '(')
if open < 0 || !strings.HasSuffix(receiver, ")") {
return "", false
}
depth := 0
for i := open; i < len(receiver); i++ {
switch receiver[i] {
case '(':
depth++
case ')':
depth--
if depth == 0 && i != len(receiver)-1 {
return "", false
}
}
}
head := receiver[:open]
if dot := strings.LastIndexByte(head, '.'); dot >= 0 {
head = head[dot+1:]
}
return grpcServiceFromClientCtor(head)
}
// grpcRegisterArgNode recognises the generated gRPC server-registration
// call `Register<Service>Server(registrar, impl)` (qualified or not)
// and returns the service name plus the AST node of the second
// argument (the impl). The impl *type* is resolved later, in the call
// post-pass, once the file-wide type environment is fully populated.
// Returns ok=false for any other call.
func grpcRegisterArgNode(callNode *sitter.Node, funcName string) (service string, argNode *sitter.Node, ok bool) {
service, ok = grpcServiceFromRegister(funcName)
if !ok {
return "", nil, false
}
if callNode == nil || callNode.Type() != "call_expression" {
return "", nil, false
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return "", nil, false
}
// Second positional argument names the server implementation.
count := 0
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
count++
if count == 2 {
return service, c, true
}
}
return "", nil, false
}
// grpcServiceFromRegister maps `Register<Service>Server` → `<Service>`.
func grpcServiceFromRegister(name string) (string, bool) {
const pfx, sfx = "Register", "Server"
if len(name) <= len(pfx)+len(sfx) {
return "", false
}
if !strings.HasPrefix(name, pfx) || !strings.HasSuffix(name, sfx) {
return "", false
}
return name[len(pfx) : len(name)-len(sfx)], true
}
// goGRPCImplType extracts a type name from a gRPC server-registration
// second argument: `&userServer{}`, `userServer{}`, `pkg.userServer{}`,
// `NewUserServer()`, `pkg.NewUserServer()`, or a bare identifier whose
// type is known from the file's type environment. Returns "" when the
// expression doesn't unambiguously name a type.
func goGRPCImplType(node *sitter.Node, src []byte, tenv typeEnv) string {
if node == nil {
return ""
}
switch node.Type() {
case "unary_expression", "composite_literal", "call_expression":
return normalizeGoTypeName(inferTypeFromGoExpr(node, src))
case "identifier":
return normalizeGoTypeName(tenv[node.Content(src)])
}
return ""
}
// applyGoGRPCRegisterMeta resolves the impl type named by a gRPC
// server-registration call's second argument — against the now-complete
// file-wide type environment — and stamps `grpc_register_*` meta onto
// the registration call's edge so the resolver's ResolveGRPCStubCalls
// pass can join client-stub calls to the server handler. No-op for
// non-registration calls and for impl arguments whose type can't be
// named (e.g. a bare field selector).
func applyGoGRPCRegisterMeta(edge *graph.Edge, c goDeferredCall, src []byte, tenv typeEnv) {
if edge == nil || c.grpcRegService == "" || c.grpcRegArgNode == nil {
return
}
impl := goGRPCImplType(c.grpcRegArgNode, src, tenv)
if impl == "" {
return
}
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["grpc_register_service"] = c.grpcRegService
edge.Meta["grpc_register_impl"] = impl
}
// --- Helpers -------------------------------------------------------
type funcRange struct {
id string
startLine int
endLine int
}
func buildFuncRanges(result *parser.ExtractionResult) []funcRange {
var ranges []funcRange
for _, n := range result.Nodes {
if n.Kind == graph.KindFunction || n.Kind == graph.KindMethod {
ranges = append(ranges, funcRange{
id: n.ID, startLine: n.StartLine, endLine: n.EndLine,
})
}
}
return ranges
}
func findEnclosingFunc(ranges []funcRange, line int) string {
for _, r := range ranges {
if line >= r.startLine && line <= r.endLine {
return r.id
}
}
return ""
}
// extractReceiverName extracts the receiver name from a Go receiver
// parameter list. "(s *Server)" -> "s", "(Server)" -> "" (unnamed).
// Used to seed paramsByFunc so calls like `s.foo()` inside the method
// body resolve via the receiver's type.
func extractReceiverName(receiver string) string {
receiver = strings.Trim(receiver, "()")
parts := strings.Fields(receiver)
if len(parts) < 2 {
return ""
}
name := parts[0]
if name == "" || name == "_" {
return ""
}
return name
}
// extractGoParamTypes walks a parameter_list AST node and returns a
// map from parameter name to its declared (normalized) type. Multi-name
// declarations like `(a, b *Foo)` produce one entry per name. Unnamed
// parameters, blank identifiers, and types that normalizeGoTypeName
// drops (primitives, map/chan/func) are skipped — callers only care
// about names that point at receiver types we could resolve methods on.
// extractGoParamNames returns the set of ALL parameter names declared in a
// parameter_list, regardless of type (unlike extractGoParamTypes, which
// keeps only params with a non-builtin type for receiver resolution). Used
// by the Temporal wrapper detector to recognise a forwarded parameter.
func extractGoParamNames(paramList *sitter.Node, src []byte) map[string]bool {
if paramList == nil {
return nil
}
out := map[string]bool{}
for i, _nc := 0, int(paramList.NamedChildCount()); i < _nc; i++ {
decl := paramList.NamedChild(i)
if decl == nil {
continue
}
if t := decl.Type(); t != "parameter_declaration" && t != "variadic_parameter_declaration" {
continue
}
typeNode := decl.ChildByFieldName("type")
for j, _nc := 0, int(decl.NamedChildCount()); j < _nc; j++ {
c := decl.NamedChild(j)
if c == nil || c == typeNode {
continue
}
if c.Type() == "identifier" {
out[c.Content(src)] = true
}
}
}
return out
}
func extractGoParamTypes(paramList *sitter.Node, src []byte) typeEnv {
if paramList == nil {
return nil
}
out := typeEnv{}
for i, _nc := 0, int(paramList.NamedChildCount()); i < _nc; i++ {
decl := paramList.NamedChild(i)
if decl == nil {
continue
}
t := decl.Type()
if t != "parameter_declaration" && t != "variadic_parameter_declaration" {
continue
}
typeNode := decl.ChildByFieldName("type")
if typeNode == nil {
continue
}
typeName := normalizeGoTypeName(typeNode.Content(src))
if typeName == "" {
continue
}
// Walk all identifier children — Go allows multiple names per
// parameter declaration sharing one type. Skip the type node
// itself (which may also be reported as a named child).
for j, _nc := 0, int(decl.NamedChildCount()); j < _nc; j++ {
c := decl.NamedChild(j)
if c == nil || c == typeNode {
continue
}
if c.Type() == "identifier" {
name := c.Content(src)
if name == "" || name == "_" {
continue
}
out[name] = typeName
}
}
}
return out
}
// extractReceiverType extracts the type name from a Go receiver parameter list.
// "(s *Server)" -> "Server", "(s Server)" -> "Server".
func extractReceiverType(receiver string) string {
receiver = strings.Trim(receiver, "()")
parts := strings.Fields(receiver)
if len(parts) == 0 {
return ""
}
typePart := parts[len(parts)-1]
typePart = strings.TrimPrefix(typePart, "*")
return typePart
}
func buildFuncSignature(name string, params, result *parser.CapturedNode) string {
sig := fmt.Sprintf("func %s%s", name, captureText(params))
if result != nil && result.Text != "" {
sig += " " + result.Text
}
return sig
}
func buildMethodSignature(receiver, name string, params, result *parser.CapturedNode) string {
sig := fmt.Sprintf("func (%s) %s%s", receiver, name, captureText(params))
if result != nil && result.Text != "" {
sig += " " + result.Text
}
return sig
}
// extractInterfaceMethods walks the children of an interface_type node
// and returns the names of all method_spec / method_elem entries.
func extractInterfaceMethods(ifaceNode *sitter.Node, src []byte) []string {
var methods []string
for i, _nc := 0, int(ifaceNode.NamedChildCount()); i < _nc; i++ {
child := ifaceNode.NamedChild(i)
if child.Type() == "method_elem" || child.Type() == "method_spec" {
for j, _nc := 0, int(child.NamedChildCount()); j < _nc; j++ {
nameNode := child.NamedChild(j)
if nameNode.Type() == "field_identifier" {
methods = append(methods, nameNode.Content(src))
break
}
}
}
}
return methods
}
// extractEmbeddedInterfaceTypes walks the children of an
// interface_type node and returns the canonicalised names of
// embedded interface references. The Go grammar wraps embedded
// interfaces and type-set elements inside `type_elem` nodes; older
// grammar versions (and gofmt edge cases) sometimes inline the bare
// type_identifier / qualified_type directly. Both shapes are
// accepted so EdgeComposes is emitted regardless of which the
// grammar produces.
func extractEmbeddedInterfaceTypes(ifaceNode *sitter.Node, src []byte) []string {
var out []string
visit := func(n *sitter.Node) {
if n == nil {
return
}
switch n.Type() {
case "type_identifier", "qualified_type", "generic_type":
if t := canonicalizeGoTypeRef(n.Content(src)); t != "" {
out = append(out, t)
}
}
}
for i, _nc := 0, int(ifaceNode.NamedChildCount()); i < _nc; i++ {
child := ifaceNode.NamedChild(i)
if child == nil {
continue
}
switch child.Type() {
case "type_elem":
// Modern grammar: each embedded type is one type_elem
// child. The named child of type_elem is the type
// reference itself.
for j, _nc := 0, int(child.NamedChildCount()); j < _nc; j++ {
visit(child.NamedChild(j))
}
case "type_identifier", "qualified_type", "generic_type":
visit(child)
}
}
return out
}
func captureText(c *parser.CapturedNode) string {
if c == nil {
return "()"
}
return c.Text
}
// --- Type environment -----------------------------------------------
// typeEnv maps variable name → inferred type name within a file.
type typeEnv map[string]string
// normalizeGoTypeName strips pointer prefix and package qualifier.
// "*pkg.Foo" → "Foo", "[]*Foo" → "Foo", "map[K]V" → "" (skipped —
// receiver typing doesn't help for map/slice/chan types).
func normalizeGoTypeName(t string) string {
t = strings.TrimSpace(t)
// Strip array / slice prefixes.
t = strings.TrimPrefix(t, "[]")
if strings.HasPrefix(t, "[") {
if end := strings.Index(t, "]"); end >= 0 {
t = t[end+1:]
}
}
// Skip map/chan/func types — can't meaningfully resolve a method call
// through them at the grain we support.
if strings.HasPrefix(t, "map[") || strings.HasPrefix(t, "chan ") || strings.HasPrefix(t, "func(") {
return ""
}
// Strip pointer prefix.
t = strings.TrimPrefix(t, "*")
// Keep only last segment of a package-qualified name.
if i := strings.LastIndex(t, "."); i >= 0 {
t = t[i+1:]
}
// Skip generics.
if i := strings.Index(t, "["); i >= 0 {
t = t[:i]
}
// Skip Go primitives — a method call receiver is never a primitive in
// code we can link to.
switch t {
case "string", "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64", "complex64", "complex128",
"bool", "byte", "rune", "error", "any":
return ""
}
if t == "" {
return ""
}
return t
}
// inferTypeFromGoExpr inspects the AST of a short-var RHS and returns
// the inferred type name — supports composite literals (`Foo{...}`),
// pointer composite literals (`&Foo{...}`), qualified composite
// literals (`pkg.Foo{...}`), and the Go constructor convention
// (`NewFoo(...)` → `Foo`). Returns "" when the expression doesn't
// unambiguously name a type.
func inferTypeFromGoExpr(node *sitter.Node, src []byte) string {
switch node.Type() {
case "composite_literal":
return compositeLiteralType(node, src)
case "unary_expression":
// `&Foo{...}` — operator is "&" (first child), operand is
// the composite literal.
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
c := node.NamedChild(i)
if c != nil && c.Type() == "composite_literal" {
return compositeLiteralType(c, src)
}
}
case "call_expression":
// Constructor convention: NewFoo(...) → Foo. Only applies
// when the called identifier starts with "New".
fn := node.ChildByFieldName("function")
if fn == nil {
return ""
}
var callName string
switch fn.Type() {
case "identifier":
callName = fn.Content(src)
case "selector_expression":
field := fn.ChildByFieldName("field")
if field != nil {
callName = field.Content(src)
}
}
if strings.HasPrefix(callName, "New") && len(callName) > 3 {
return callName[3:]
}
}
return ""
}
// compositeLiteralType returns the type name of a composite literal,
// handling both `Foo{...}` (type_identifier) and `pkg.Foo{...}`
// (qualified_type) shapes.
func compositeLiteralType(lit *sitter.Node, src []byte) string {
t := lit.ChildByFieldName("type")
if t == nil {
return ""
}
switch t.Type() {
case "type_identifier":
return t.Content(src)
case "qualified_type":
nameNode := t.ChildByFieldName("name")
if nameNode != nil {
return nameNode.Content(src)
}
case "pointer_type":
// *Foo{...} — rare but defensible.
for i, _nc := 0, int(t.NamedChildCount()); i < _nc; i++ {
c := t.NamedChild(i)
if c != nil && c.Type() == "type_identifier" {
return c.Content(src)
}
}
}
return ""
}
// Chained-receiver / factory-chain return-type resolution (resolveChainType,
// stripCallArgs, findMethodReturnType, findFactoryReturnType) is shared across
// every AST extractor and lives in helpers_chaintype.go.
// scanGoPragmas inspects up to 5 source lines immediately before a
// function or method declaration looking for `//go:*` or `//export`
// comments and stamps them onto the node's Meta. Lets callers flag
// special Go entry points (cgo exports, linkname) so dead-code
// detection doesn't mark them as dead.
//
// Allocation contract: scans backwards using bytes.LastIndexByte
// against the original src — no per-call lineStarts slice, no per-
// line string copy until a pragma actually matches. The predecessor
// implementation built an O(startLine) []int of line offsets per
// call, which on a Go monorepo like k8s (12 k+ .go files, hundreds
// of symbols each) was the single biggest allocation in the
// indexer (4.57 GB / 14 % of total).
func scanGoPragmas(src []byte, startLine int, node *graph.Node) {
// startLine is 0-based (matches tree-sitter's row numbering at the
// call site). One forward scan locates the '\n' that ends row
// startLine 1, and the backward walk from there is bounded at 5
// iterations regardless of file size.
if startLine <= 0 || len(src) == 0 {
return
}
end := -1
{
row := 0
for i := 0; i < len(src); i++ {
if src[i] != '\n' {
continue
}
row++
end = i
if row == startLine {
break
}
}
}
if end < 0 {
return
}
for scanned := 0; scanned < 5 && end >= 0; scanned++ {
prev := bytes.LastIndexByte(src[:end], '\n')
line := bytes.TrimSpace(src[prev+1 : end])
if len(line) != 0 && !bytes.HasPrefix(line, []byte("//")) {
return
}
if bytes.HasPrefix(line, []byte("//export ")) {
node.Meta["cgo_export"] = true
return
}
if bytes.HasPrefix(line, []byte("//go:linkname ")) {
node.Meta["go_linkname"] = true
return
}
if prev < 0 {
return
}
end = prev
}
}
// isGoBuiltinOrKeyword returns true for identifiers that should not be
// treated as function-value references (common Go builtins, type names,
// literals).
func isGoBuiltinOrKeyword(name string) bool {
switch name {
case "nil", "true", "false", "err", "ok", "ctx",
"string", "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64", "complex64", "complex128",
"bool", "byte", "rune", "error", "any",
"make", "new", "len", "cap", "append", "copy", "delete",
"panic", "recover", "print", "println", "close":
return true
}
// Skip lowercase single-letter identifiers (loop vars, etc.)
if len(name) == 1 && name[0] >= 'a' && name[0] <= 'z' {
return true
}
return false
}