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

1435 lines
51 KiB
Go

// Temporal Go SDK call attribution.
//
// Workflows orchestrate activities through a thin set of dispatch
// helpers exposed by `go.temporal.io/sdk/workflow`:
//
// workflow.ExecuteActivity(ctx, ActivityFn, args...)
// workflow.ExecuteLocalActivity(ctx, ActivityFn, args...)
// workflow.ExecuteChildWorkflow(ctx, WorkflowFn, args...)
//
// and activities / workflows enter the runtime via
// `go.temporal.io/sdk/worker`:
//
// w.RegisterActivity(MyActivity)
// w.RegisterActivityWithOptions(MyActivity, activity.RegisterOptions{Name: "..."})
// w.RegisterWorkflow(MyWorkflow)
// w.RegisterWorkflowWithOptions(MyWorkflow, workflow.RegisterOptions{Name: "..."})
//
// Tree-sitter sees `workflow.ExecuteActivity(...)` as a selector_expression
// call whose receiver text is "workflow" and method is the helper name;
// `w.RegisterActivity(...)` as a selector call whose method is the
// register helper. Neither shape resolves to anything useful through
// the normal Go call-resolution path (the target lives in an external
// SDK module). The helpers below recognise the call shapes and stamp
// dedicated `via=temporal.stub` / `via=temporal.register` placeholders
// that the resolver's ResolveTemporalCalls pass turns into edges from
// the workflow to the activity (or from one workflow to the child
// workflow) it dispatches.
package languages
import (
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// goWorkflowPkgPath is the canonical import path of the Temporal Go SDK
// workflow package whose helpers (ExecuteActivity, SetQueryHandler,
// SignalExternalWorkflow, …) the detectors gate on.
const goWorkflowPkgPath = "go.temporal.io/sdk/workflow"
// goWorkflowReceiverAlias returns the local name the workflow package is
// imported under in this file — the explicit alias for
// `import wf "go.temporal.io/sdk/workflow"`, or "workflow" for a plain
// import. Returns "" when the file does not import the workflow package.
// The detectors canonicalise a matching receiver to "workflow" so an
// aliased import (`wf.ExecuteActivity(...)`) is still recognised.
func goWorkflowReceiverAlias(root *sitter.Node, src []byte) string {
if root == nil {
return ""
}
var found string
var walk func(n *sitter.Node)
walk = func(n *sitter.Node) {
if n == nil || found != "" {
return
}
if n.Type() == "import_spec" {
pathNode := n.ChildByFieldName("path")
if pathNode != nil {
p := pathNode.Content(src)
if len(p) >= 2 {
p = p[1 : len(p)-1] // strip the surrounding quotes
}
if p == goWorkflowPkgPath {
if nameNode := n.ChildByFieldName("name"); nameNode != nil {
found = nameNode.Content(src)
} else if i := strings.LastIndex(goWorkflowPkgPath, "/"); i >= 0 {
found = goWorkflowPkgPath[i+1:]
}
return
}
}
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
walk(n.NamedChild(i))
if found != "" {
return
}
}
}
walk(root)
return found
}
// goCanonicalWorkflowReceiver maps a call receiver to "workflow" when it
// matches the file's workflow-package alias, so the receiver-gated
// detectors recognise an aliased import. Other receivers pass through
// unchanged. wfAlias == "" (package not imported) is a no-op.
func goCanonicalWorkflowReceiver(receiver, wfAlias string) string {
if wfAlias != "" && receiver == wfAlias {
return "workflow"
}
return receiver
}
// goTemporalDispatchKind reports whether (receiver, method) names one
// of the Temporal workflow dispatch helpers and, if so, returns the
// canonical kind ("activity" or "workflow") plus whether the call is
// the `LocalActivity` variant. Returns ok=false for everything else.
//
// We require the receiver text to be exactly "workflow" — the
// canonical SDK alias. Users who alias the import (e.g.
// `import wf "go.temporal.io/sdk/workflow"`) won't be detected, which
// matches how the existing gRPC stub detector handles SDK aliasing
// (the canonical alias dominates >99% of real-world code).
func goTemporalDispatchKind(receiver, method string) (kind string, local bool, ok bool) {
if receiver != "workflow" {
return "", false, false
}
switch method {
case "ExecuteActivity":
return "activity", false, true
case "ExecuteLocalActivity":
return "activity", true, true
case "ExecuteChildWorkflow":
return "workflow", false, true
}
return "", false, false
}
// goTemporalRegisterKind reports whether a method name is one of the
// Temporal worker registration helpers and, if so, returns the kind
// ("activity" or "workflow") being registered. The receiver isn't
// required — `RegisterActivity` is distinctive enough across the SDK
// surface that a name match has zero realistic false positives.
//
// `RegisterActivities` (plural — registers every exported method on
// a struct as an activity) is recognised too; the resolver pass will
// promote each method of the struct to a temporal activity.
func goTemporalRegisterKind(method string) (kind string, plural bool, ok bool) {
switch method {
case "RegisterActivity", "RegisterActivityWithOptions":
return "activity", false, true
case "RegisterWorkflow", "RegisterWorkflowWithOptions":
return "workflow", false, true
case "RegisterActivities":
return "activity", true, true
}
return "", false, false
}
// goTemporalSignalQueryOutKind reports whether (receiver, method) names
// an OUTBOUND signal-send or query-call against an already-running
// workflow and, if so, returns the kind ("signal" / "query") plus the
// 1-based position of the signal/query-name argument.
//
// workflow.SignalExternalWorkflow(ctx, wid, rid, "name", arg) // wf -> wf
// client.SignalWorkflow(ctx, wid, rid, "name", arg) // svc -> wf
// client.QueryWorkflow(ctx, wid, rid, "name", args...) // svc -> wf
//
// SignalExternalWorkflow is gated on the canonical "workflow" receiver
// (it is a workflow-package function). SignalWorkflow / QueryWorkflow
// live on the client and are called on an arbitrary client variable, so
// — like the Register* helpers — they are matched by method name alone;
// the string-literal name gate below keeps that high-precision. There is
// deliberately no workflow.QueryWorkflow (querying is client-side) and no
// SignalExternalWorkflowAsync (SignalExternalWorkflow returns a Future).
func goTemporalSignalQueryOutKind(receiver, method string) (kind string, namePos int, ok bool) {
switch method {
case "SignalExternalWorkflow":
if receiver == "workflow" {
return "signal", 4, true
}
case "SignalWorkflow":
return "signal", 4, true
case "QueryWorkflow":
return "query", 4, true
}
return "", 0, false
}
// goTemporalHandlerKind reports whether (receiver, method) names one of
// the Temporal in-workflow handler-declaration helpers and, if so,
// returns the canonical kind ("query" / "signal" / "update").
//
// workflow.SetQueryHandler(ctx, "name", fn)
// workflow.SetQueryHandlerWithOptions(ctx, "name", fn, opts)
// workflow.GetSignalChannel(ctx, "name")
// workflow.GetSignalChannelWithOptions(ctx, "name", opts)
// workflow.SetUpdateHandler(ctx, "name", fn)
// workflow.SetUpdateHandlerWithOptions(ctx, "name", fn, opts)
//
// These mirror the Java SDK's `@QueryMethod` / `@SignalMethod` /
// `@UpdateMethod` annotations: a workflow declares, from inside its
// body, the named query / signal / update channels it serves. As with
// the dispatch helpers we require the receiver text to be exactly the
// canonical "workflow" alias.
func goTemporalHandlerKind(receiver, method string) (kind string, ok bool) {
if receiver != "workflow" {
return "", false
}
switch method {
case "SetQueryHandler", "SetQueryHandlerWithOptions":
return "query", true
case "GetSignalChannel", "GetSignalChannelWithOptions":
return "signal", true
case "SetUpdateHandler", "SetUpdateHandlerWithOptions":
return "update", true
}
return "", false
}
// goTemporalHandlerName extracts the query / signal / update name from a
// handler-declaration call — the second positional argument (after the
// workflow.Context). Unlike dispatch names we accept ONLY a string
// literal: handler names are matched by string at runtime, so a
// non-literal (variable / selector) can't be pinned to a name here and
// is left undetected, keeping the detector high-precision. Returns ""
// when the second argument is missing or is not a string literal.
func goTemporalHandlerName(callNode *sitter.Node, src []byte) string {
if callNode == nil || callNode.Type() != "call_expression" {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return ""
}
count := 0
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
count++
if count == 2 {
switch c.Type() {
case "interpreted_string_literal", "raw_string_literal":
return goTemporalNameFromExpr(c, src)
}
return ""
}
}
return ""
}
// goTemporalDispatchArg returns the second positional argument node of a
// dispatch call (`workflow.ExecuteActivity(ctx, X, args...)` → X), or
// nil. X is either a string literal ("MyActivity"), a bare identifier
// (MyActivity), or a selector expression (pkg.MyActivity, recv.Method);
// goTemporalNameFromExpr reduces it to the trailing identifier — the
// name the worker registers under (the bare function name unless
// `RegisterActivityWithOptions` overrides it). Returned as a node, not a
// reduced name, so the env-default refinement can inspect the argument's
// shape (a bare identifier is the only case it tries to resolve to a
// literal default). Returns nil when the call has fewer than two
// positional arguments.
func goTemporalDispatchArg(callNode *sitter.Node) *sitter.Node {
if callNode == nil || callNode.Type() != "call_expression" {
return nil
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return nil
}
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 c
}
}
return nil
}
// goTemporalRegisterName extracts the registered function name from a
// `worker.RegisterActivity(F)` / `worker.RegisterWorkflow(F)` call —
// the first positional argument, which is the function reference.
// Same expression shapes as the dispatch-name argument.
func goTemporalRegisterName(callNode *sitter.Node, src []byte) string {
if callNode == nil || callNode.Type() != "call_expression" {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return ""
}
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
return goTemporalNameFromExpr(c, src)
}
return ""
}
// applyGoTemporalRegisterMeta stamps `via=temporal.register` plus
// `temporal_kind` (activity / workflow) and `temporal_name` (the
// function-reference identifier) onto an EdgeCalls edge derived from
// a Temporal worker-registration call. No-op when c.tempKind isn't
// the "register_*" form set by goTemporalRegisterKind.
//
// The resolver's ResolveTemporalCalls pass walks every edge carrying
// this meta to discover (name → registered function) pairs, then
// stamps `temporal_role` on the registered function nodes and uses
// the map to rewrite matching stub-call placeholders.
func applyGoTemporalRegisterMeta(edge *graph.Edge, c goDeferredCall) {
if edge == nil || c.tempKind == "" || c.tempName == "" {
return
}
var kind string
switch c.tempKind {
case "register_activity":
kind = "activity"
case "register_workflow":
kind = "workflow"
default:
return
}
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["via"] = "temporal.register"
edge.Meta["temporal_kind"] = kind
edge.Meta["temporal_name"] = c.tempName
if c.tempRegisteredName != "" {
edge.Meta["temporal_registered_name"] = c.tempRegisteredName
}
if c.tempRegisterPlural {
edge.Meta["temporal_register_plural"] = true
}
}
// goTemporalRegisterStructType returns the struct TYPE name from the first
// argument of a `w.RegisterActivities(&MyActivities{})` call — the struct
// whose exported methods are each registered as an activity. Handles the
// `&T{}` pointer and `T{}` value composite-literal forms and a qualified
// `pkg.T{}`. Returns "" when the argument is not a composite literal (e.g.
// a pre-built variable, which carries no static type here).
func goTemporalRegisterStructType(callNode *sitter.Node, src []byte) string {
if callNode == nil || callNode.Type() != "call_expression" {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil || args.NamedChildCount() == 0 {
return ""
}
arg := args.NamedChild(0)
if arg == nil {
return ""
}
if arg.Type() == "unary_expression" {
if op := arg.ChildByFieldName("operand"); op != nil {
arg = op
}
}
if arg.Type() != "composite_literal" {
return ""
}
typ := arg.ChildByFieldName("type")
if typ == nil {
return ""
}
switch typ.Type() {
case "type_identifier", "identifier":
return typ.Content(src)
case "qualified_type":
if name := typ.ChildByFieldName("name"); name != nil {
return name.Content(src)
}
case "pointer_type":
// `&T` already unwrapped above, but a `*T` element type can appear.
if inner := typ.ChildByFieldName("type"); inner != nil {
switch inner.Type() {
case "type_identifier", "identifier":
return inner.Content(src)
case "qualified_type":
if name := inner.ChildByFieldName("name"); name != nil {
return name.Content(src)
}
}
}
}
return ""
}
// goTemporalRegisterNameOverride extracts the `Name:` string-literal
// field from the RegisterOptions composite literal passed as the second
// argument of a `RegisterActivityWithOptions` / `RegisterWorkflowWithOptions`
// call — the canonical registered name that overrides the bare function
// name (the name an `ExecuteActivity(ctx, "<name>", …)` dispatch must
// match). Returns "" when there is no second composite-literal argument or
// no string-literal Name field.
//
// w.RegisterActivityWithOptions(MyActivity,
// activity.RegisterOptions{Name: "ChargeCard"})
func goTemporalRegisterNameOverride(callNode *sitter.Node, src []byte) string {
if callNode == nil || callNode.Type() != "call_expression" {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return ""
}
// Second positional argument = the options struct.
var opts *sitter.Node
count := 0
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
count++
if count == 2 {
opts = c
break
}
}
if opts == nil {
return ""
}
// Unwrap a `&RegisterOptions{...}` pointer literal.
if opts.Type() == "unary_expression" {
if op := opts.ChildByFieldName("operand"); op != nil {
opts = op
}
}
if opts.Type() != "composite_literal" {
return ""
}
body := opts.ChildByFieldName("body")
if body == nil {
return ""
}
unwrap := func(n *sitter.Node) *sitter.Node {
// A keyed-element key/value may be wrapped in a literal_element
// node depending on the grammar revision; reduce to the inner node.
if n != nil && n.Type() == "literal_element" && n.NamedChildCount() == 1 {
return n.NamedChild(0)
}
return n
}
for i, _nc := 0, int(body.NamedChildCount()); i < _nc; i++ {
kv := body.NamedChild(i)
if kv == nil || kv.Type() != "keyed_element" || kv.NamedChildCount() < 2 {
continue
}
key := unwrap(kv.NamedChild(0))
val := unwrap(kv.NamedChild(1))
if key == nil || val == nil || key.Content(src) != "Name" {
continue
}
if lit, ok := goStringLiteralValue(val, src); ok {
return lit
}
}
return ""
}
// applyGoTemporalHandlerMeta stamps `via=temporal.handler` plus
// `temporal_kind` (query / signal / update) and `temporal_name` (the
// handler's string name) onto the EdgeCalls edge derived from a
// `workflow.SetQueryHandler` / `GetSignalChannel` / `SetUpdateHandler`
// call. No-op when c.tempHandlerKind / c.tempName are unset.
//
// The edge originates from the enclosing workflow function, so the
// graph records — per workflow — the named query / signal / update
// handlers it exposes, symmetric with the Java side's per-method
// `@QueryMethod` / `@SignalMethod` / `@UpdateMethod` annotation edges.
func applyGoTemporalHandlerMeta(edge *graph.Edge, c goDeferredCall) {
if edge == nil || c.tempHandlerKind == "" || c.tempName == "" {
return
}
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["via"] = "temporal.handler"
edge.Meta["temporal_kind"] = c.tempHandlerKind
edge.Meta["temporal_name"] = c.tempName
}
// applyGoTemporalSignalQueryMeta stamps the outbound signal-send /
// query-call meta onto an EdgeCalls edge derived from
// `SignalExternalWorkflow` / `SignalWorkflow` / `QueryWorkflow`:
// `via=temporal.signal-send` or `temporal.query-call`, plus
// `temporal_kind` (signal / query) and `temporal_name` (the literal
// signal/query name). No-op when c.tempOutKind / c.tempName are unset.
//
// These are the consumer side of the signal/query namespaces; the
// provider side is the in-workflow handler (GetSignalChannel /
// SetQueryHandler), tagged via=temporal.handler.
func applyGoTemporalSignalQueryMeta(edge *graph.Edge, c goDeferredCall) {
if edge == nil || c.tempOutKind == "" || c.tempName == "" {
return
}
var via string
switch c.tempOutKind {
case "signal":
via = "temporal.signal-send"
case "query":
via = "temporal.query-call"
default:
return
}
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["via"] = via
edge.Meta["temporal_kind"] = c.tempOutKind
edge.Meta["temporal_name"] = c.tempName
}
// markGoTemporalWrapper stamps a dispatch-wrapper marker on the enclosing
// function node: a function that calls workflow.ExecuteActivity /
// ExecuteChildWorkflow with one of its own parameters as the dispatch
// name. temporal_wrapper_kind records the kind (activity / workflow) and
// temporal_wrapper_param the forwarded parameter name. The marker lets a
// future interprocedural pass propagate a caller's literal/const argument
// through the wrapper to the real handler; today it documents the wrapper
// so the unresolvable parameter-named stub is suppressed rather than
// emitted as noise.
func markGoTemporalWrapper(result *parser.ExtractionResult, callerID, kind, param string) {
if result == nil || callerID == "" {
return
}
for _, n := range result.Nodes {
if n.ID == callerID {
if n.Meta == nil {
n.Meta = map[string]any{}
}
n.Meta["temporal_wrapper_kind"] = kind
n.Meta["temporal_wrapper_param"] = param
return
}
}
}
// goTemporalStartKind reports whether a method name is one of the
// service-side workflow-START helpers and, if so, returns the 1-based
// positional index of the workflow argument.
//
// client.ExecuteWorkflow(ctx, opts, workflow, args...) // workflow @ 3
// client.SignalWithStartWorkflow(ctx, wfID, sig, arg, opts, workflow, args...) // workflow @ 6
//
// Both are client methods invoked on an arbitrary client variable, so —
// like SignalWorkflow / QueryWorkflow and the Register* helpers — they are
// matched by method name alone; ExecuteWorkflow / SignalWithStartWorkflow
// are distinctive enough across the SDK surface for that to be precise.
func goTemporalStartKind(method string) (wfPos int, ok bool) {
switch method {
case "ExecuteWorkflow":
return 3, true
case "SignalWithStartWorkflow":
return 6, true
}
return 0, false
}
// goTemporalNthArgName reduces the n-th (1-based) positional argument of a
// call to the trailing identifier that names a workflow — handling a func
// reference (OrderWorkflow), a selector (pkg.OrderWorkflow), or a string
// type name ("OrderWorkflow"), via goTemporalNameFromExpr. Returns "" when
// the call has fewer than n positional arguments or the argument is not a
// reducible name. Unlike goTemporalNthStringLiteralArg this accepts a
// non-literal, because a workflow START usually passes the workflow
// function value, whose name is the registered type.
func goTemporalNthArgName(callNode *sitter.Node, n int, src []byte) string {
if callNode == nil || callNode.Type() != "call_expression" {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return ""
}
count := 0
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
count++
if count == n {
return goTemporalNameFromExpr(c, src)
}
}
return ""
}
// applyGoTemporalStartMeta stamps `via=temporal.start` plus
// `temporal_kind=workflow` and `temporal_name` (the started workflow's
// name) onto the EdgeCalls edge derived from a client.ExecuteWorkflow /
// SignalWithStartWorkflow call. No-op when c.tempStartName is unset. The
// resolver rewrites this edge to the registered workflow node, so
// get_callers on a Go workflow surfaces the services that start it.
func applyGoTemporalStartMeta(edge *graph.Edge, c goDeferredCall) {
if edge == nil || c.tempStartName == "" {
return
}
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["via"] = "temporal.start"
edge.Meta["temporal_kind"] = "workflow"
edge.Meta["temporal_name"] = c.tempStartName
}
// goTemporalNthStringLiteralArg returns the unquoted value of the n-th
// (1-based) positional argument of a call when that argument is a string
// literal, else "". Used to extract the signal/query name from an
// outbound send/call — names are matched by string at runtime, so only a
// literal can be pinned here (a variable / constant is left undetected,
// keeping the detector high-precision).
func goTemporalNthStringLiteralArg(callNode *sitter.Node, n int, src []byte) string {
if callNode == nil || callNode.Type() != "call_expression" {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return ""
}
count := 0
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
count++
if count == n {
switch c.Type() {
case "interpreted_string_literal", "raw_string_literal":
return goTemporalNameFromExpr(c, src)
}
return ""
}
}
return ""
}
// goTemporalNameFromExpr reduces a single argument expression to the
// trailing identifier that names the activity / workflow. Handles
// string literals (`"MyActivity"` and the Go raw-string variant),
// bare identifiers (`MyActivity`), and selector expressions
// (`pkg.MyActivity`, `a.Method`). Returns "" for any other shape
// (function literals, ternary-style expressions, etc.) — keeps the
// detector high-precision rather than guessing.
func goTemporalNameFromExpr(node *sitter.Node, src []byte) string {
if node == nil {
return ""
}
switch node.Type() {
case "interpreted_string_literal", "raw_string_literal":
text := node.Content(src)
if len(text) >= 2 && (text[0] == '"' || text[0] == '`') {
return text[1 : len(text)-1]
}
return text
case "identifier":
return node.Content(src)
case "selector_expression":
if field := node.ChildByFieldName("field"); field != nil {
return field.Content(src)
}
case "unary_expression":
// `&MyActivity` (rare; mostly seen for struct-method registration)
if op := node.ChildByFieldName("operand"); op != nil {
return goTemporalNameFromExpr(op, src)
}
case "call_expression":
// `GetX()` / `pkg.GetX()` — the dispatch name is the called func's
// trailing identifier; the resolver derefs it to the func's return
// literal via the const-deref map. Recurse into the `function`
// child: a selector resolves to "GetX", a bare identifier to "GetX".
if fn := node.ChildByFieldName("function"); fn != nil {
return goTemporalNameFromExpr(fn, src)
}
}
return ""
}
// goTemporalEnvDefaultName attempts to resolve a bare-identifier dispatch
// name to the string-literal default of an env-var-with-default
// assignment in the enclosing function. Returns the default and true for
// one of these shapes (anchored on a literal os.Getenv / os.LookupEnv
// read so the value is provably env-sourced):
//
// name := cmp.Or(os.Getenv("KEY"), "Default") // any call mixing an
// // os.Getenv read with a
// // string-literal arg
// name := os.Getenv("KEY")
// if name == "" { name = "Default" } // (or `name, ok := os.LookupEnv(...)`
// // followed by a literal assign)
//
// Intra-procedural and literal-only: only assignments lexically before
// the dispatch call are considered, and anything that isn't an
// os.Getenv-anchored literal default returns "", false. This is a
// deliberately narrow data-flow shortcut, not general constant
// propagation — see the speculative tier the resolver lands it at.
// The result is reported as exactly one of `litDef` (a string-literal default)
// or `constName` (a constant-reference default the resolver substitutes through
// constVal), plus a `source` tier marker: "os_getenv" / "allowlist" (trusted
// literal), "const_ref" (trusted helper with a constant default — same visible
// tier as allowlist), or "heuristic" (env-named-helper guess, hidden tier;
// stays heuristic even when its default is a constant, the helper itself being
// the weak link).
func goTemporalEnvDefaultName(callNode *sitter.Node, name string, src []byte, extra map[string]bool) (litDef string, constName string, source string, ok bool) {
body := goEnclosingFuncBody(callNode)
if body == nil {
return "", "", "", false
}
limit := callNode.StartByte()
// Collect every assignment to `name` lexically before the dispatch
// call, in source order, WITHOUT descending into nested func_literal
// bodies — a closure is a separate scope, and matching a shadowing
// same-named variable declared there would be a false positive.
var assigns []*sitter.Node
var walk func(n *sitter.Node)
walk = func(n *sitter.Node) {
if n == nil {
return
}
if n.Type() == "func_literal" {
return // do not descend into nested closures
}
if (n.Type() == "short_var_declaration" || n.Type() == "assignment_statement") &&
n.StartByte() < limit && goAssignHasTarget(n, name, src) {
assigns = append(assigns, n)
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
walk(n.NamedChild(i))
}
}
walk(body)
// Replay the writes in order. The dispatch name is env-default-sourced
// only if, after the LAST write before the call, the variable still
// holds an env-or-default value: either a `cmp.Or(os.Getenv, "lit")`
// assignment, an allow-listed / heuristic env-helper call
// (`GetEnvOrDefault(KEY, "lit")` / `cfg.ActivityFromEnv(KEY, "lit")`),
// or a string-literal assignment that followed an os.Getenv /
// os.LookupEnv read (the `name := os.Getenv(...); if name == "" { name =
// "lit" }` shape). Any other later write — a plain reassignment `name =
// pick()` — clears the env-sourcing, and we leave the dispatch
// unresolved rather than guess.
//
// resolvedSource records HOW the default was recognised so the resolver
// can tier the edge: "os_getenv" / "allowlist" / "const_ref" land at the
// inferred (visible) tier, "heuristic" stays at the hidden speculative
// tier. The default is reported as exactly one of resLit (a string-literal
// default) or resConst (a constant-reference default the resolver
// substitutes through its const-deref index).
resLit := ""
resConst := ""
resolvedSource := ""
resolvedOK := false
envReadSeen := false
// commit records a resolved default, routing a constant-reference value
// into resConst (and downgrading "const_ref" → "heuristic" when the helper
// itself is only a heuristic match — the helper, not the constant, is the
// weak link).
commit := func(val string, isConst bool, trustedSource string) {
if isConst {
resLit, resConst = "", val
if trustedSource == "heuristic" {
resolvedSource = "heuristic"
} else {
resolvedSource = "const_ref"
}
} else {
resLit, resConst, resolvedSource = val, "", trustedSource
}
resolvedOK, envReadSeen = true, false
}
clear := func() {
resLit, resConst, resolvedSource, resolvedOK, envReadSeen = "", "", "", false, false
}
for _, a := range assigns {
rhs := goAssignRHSExpr(a, name, src)
switch {
case rhs == nil:
clear()
case rhs.Type() == "call_expression" && goIsEnvRead(rhs, src):
// `name := os.Getenv("K")` — default still pending.
clear()
envReadSeen = true
case rhs.Type() == "call_expression":
// `name := cmp.Or(os.Getenv("K"), "lit")` — self-contained.
if v, isC, ok := goCallEnvDefaultLiteral(rhs, src); ok {
commit(v, isC, "os_getenv")
} else if v, isC, ok := goEnvHelperDefaultLiteral(rhs, src, extra); ok {
// Allow-listed env-helper (`GetEnvOrDefault(KEY, "lit")`).
commit(v, isC, "allowlist")
} else if v, isC, ok := goEnvHelperHeuristicDefault(rhs, src); ok {
// Generic "env"-named helper — lower-trust heuristic.
commit(v, isC, "heuristic")
} else {
clear()
}
default:
// `name = "lit"` / `name = CONST` — only a default when it
// follows an env read.
if v, isC, ok := goArgDefaultValue(rhs, src); ok && envReadSeen {
commit(v, isC, "os_getenv")
} else {
clear()
}
}
}
return resLit, resConst, resolvedSource, resolvedOK
}
// goTemporalVarTrace traces a bare-identifier dispatch name to its
// intra-procedural assignment when that assignment is a plain literal /
// constant reference / const-returning func call. The env-var-with-default
// shape is handled by goTemporalEnvDefaultName and takes precedence; this is
// the general fallback for `name := <value>; workflow.ExecuteActivity(ctx,
// name, …)` — the "meta_vars" broken-dispatch category (activity /
// activityName / type). It returns the LAST assignment to `name` lexically
// before the dispatch, reduced to exactly one of:
//
// litDef : string-literal value (`name := "ChargeActivity"`)
// constName: a constant NAME (`name := pkg.ChargeName` / `name := CHARGE`)
// funcName : a const-returning func name (`name := GetChargeName()`)
//
// The resolver validates constName / funcName against its constVal index, so
// an identifier that is not actually a string constant simply fails to
// resolve and stays a broken_dispatch — no false-resolution risk. The
// last-assignment-wins rule is a best-effort static guess (a later
// conditional reassignment may not execute), so the resolver lands these at
// the inferred / convention tier, not the register-confirmed 0.9. Returns
// ok=false when there is no traceable assignment to `name`.
func goTemporalVarTrace(callNode *sitter.Node, name string, src []byte) (litDef, constName string, ok bool) {
body := goEnclosingFuncBody(callNode)
if body == nil {
return "", "", false
}
limit := callNode.StartByte()
var rhs *sitter.Node
assigns := 0
var walk func(n *sitter.Node)
walk = func(n *sitter.Node) {
if n == nil {
return
}
if (n.Type() == "short_var_declaration" || n.Type() == "assignment_statement") &&
n.StartByte() < limit && goAssignHasTarget(n, name, src) {
// Count every assignment to `name` (the single-assignment guard
// below relies on the total); rhs is the index-matched value, which
// may be nil for a multi-value call.
assigns++
rhs = goAssignRHSExpr(n, name, src)
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
walk(n.NamedChild(i))
}
}
walk(body)
// Trace ONLY when the variable has exactly ONE assignment before the
// dispatch. Multiple assignments mean the live value is conditional /
// reassigned (e.g. an env-default write later overwritten by a plain
// call) — guessing the last one would be a false-resolution risk, so we
// leave the dispatch a broken_dispatch rather than guess.
if assigns != 1 || rhs == nil {
return "", "", false
}
switch rhs.Type() {
case "interpreted_string_literal", "raw_string_literal":
if v, okk := goStringLiteralValue(rhs, src); okk && v != "" {
return v, "", true
}
case "identifier":
// Bare const reference (`name := CHARGE`). Admitted ONLY when the
// referenced identifier is a package-level CONST declared in this
// file — a function parameter or arbitrary local (`actName :=
// picked`) is exactly the "don't guess at arbitrary variables" case
// the plain-var path deliberately refuses. The const NAME is emitted
// as temporal_name so the resolver's const-deref map substitutes the
// literal. Guarded by length to skip throwaway names; skip
// self-reference.
if cn := rhs.Content(src); len(cn) > 2 && cn != name && goIdentIsFileConst(callNode, cn, src) {
return "", cn, true
}
case "selector_expression":
// Package / receiver const reference (`name := config.ChargeName`).
if field := rhs.ChildByFieldName("field"); field != nil {
if cn := field.Content(src); len(cn) > 2 {
return "", cn, true
}
}
case "call_expression":
// Const-returning name getter (`name := GetChargeName()`). os.Getenv
// is the env path's job, not this one. Require ZERO arguments: a call
// WITH args (`wfutils.PickActivity("KEY", "default")`) is an
// env/helper-style call whose default is the env path's responsibility
// (and which the env path deliberately declines for unknown helpers) —
// treating it as a const-return getter would mislabel it. The callee's
// returned value is resolved in-file to a literal / const name, since
// the resolver has no func-returning-name channel.
if goIsEnvRead(rhs, src) {
return "", "", false
}
if a := rhs.ChildByFieldName("arguments"); a != nil && a.NamedChildCount() > 0 {
return "", "", false
}
if lit, cn, okk := goTemporalFuncReturnName(rhs, callNode, src); okk {
return lit, cn, true
}
}
return "", "", false
}
// goIdentIsFileConst reports whether `ident` is the name of a constant
// declared anywhere in the same source file as fromNode. Used to gate the
// variable-trace bare-identifier case so it admits a `name := chargeName`
// const reference but refuses a `actName := picked` parameter / local —
// honouring the "don't guess at arbitrary variables" invariant. The scan
// inspects every const_spec name under the file root.
func goIdentIsFileConst(fromNode *sitter.Node, ident string, src []byte) bool {
root := fromNode
for root.Parent() != nil {
root = root.Parent()
}
found := false
var walk func(n *sitter.Node)
walk = func(n *sitter.Node) {
if n == nil || found {
return
}
if n.Type() == "const_spec" {
if nm := n.ChildByFieldName("name"); nm != nil && nm.Content(src) == ident {
found = true
return
}
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
walk(n.NamedChild(i))
}
}
walk(root)
return found
}
// goTemporalFuncReturnName resolves a no-argument func/method call used to
// supply a dispatch name (`name := GetChargeName()`) to the value the callee
// unconditionally returns — either a string literal (emitted as the dispatch
// name directly) or a bare const reference (emitted as a const name for the
// resolver to dereference). It locates the callee declaration by simple name
// within the SAME file as the call, then returns the single `return <expr>`
// it finds when that expr is a string literal or a one-hop identifier
// reference. Returns ok=false when the callee is not found in-file, is not a
// plain identifier call, has multiple/none returns, or returns something
// other than a literal / bare const — keeping resolution precision-safe.
func goTemporalFuncReturnName(call, fromNode *sitter.Node, src []byte) (litDef, constName string, ok bool) {
fn := call.ChildByFieldName("function")
if fn == nil || fn.Type() != "identifier" {
return "", "", false
}
callee := fn.Content(src)
if callee == "" {
return "", "", false
}
root := fromNode
for root.Parent() != nil {
root = root.Parent()
}
var decl *sitter.Node
var find func(n *sitter.Node)
find = func(n *sitter.Node) {
if n == nil || decl != nil {
return
}
if n.Type() == "function_declaration" {
if nm := n.ChildByFieldName("name"); nm != nil && nm.Content(src) == callee {
decl = n
return
}
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
find(n.NamedChild(i))
}
}
find(root)
if decl == nil {
return "", "", false
}
body := decl.ChildByFieldName("body")
if body == nil {
return "", "", false
}
var ret *sitter.Node
count := 0
var walk func(n *sitter.Node)
walk = func(n *sitter.Node) {
if n == nil {
return
}
if n.Type() == "return_statement" {
count++
if n.NamedChildCount() == 1 {
ret = n.NamedChild(0)
}
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
walk(n.NamedChild(i))
}
}
walk(body)
if count != 1 || ret == nil {
return "", "", false
}
// A return_statement's value is wrapped in an expression_list; unwrap a
// single-expression list down to the lone expression.
if ret.Type() == "expression_list" {
if ret.NamedChildCount() != 1 {
return "", "", false
}
ret = ret.NamedChild(0)
}
if ret == nil {
return "", "", false
}
switch ret.Type() {
case "interpreted_string_literal", "raw_string_literal":
if v, okk := goStringLiteralValue(ret, src); okk && v != "" {
return v, "", true
}
case "identifier":
if cn := ret.Content(src); len(cn) > 2 {
return "", cn, true
}
case "selector_expression":
if field := ret.ChildByFieldName("field"); field != nil {
if cn := field.Content(src); len(cn) > 2 {
return "", cn, true
}
}
}
return "", "", false
}
// goEnclosingFuncBody walks up from n to the nearest function-like
// ancestor and returns its body block, or nil.
func goEnclosingFuncBody(n *sitter.Node) *sitter.Node {
for cur := n; cur != nil; cur = cur.Parent() {
switch cur.Type() {
case "function_declaration", "method_declaration", "func_literal":
return cur.ChildByFieldName("body")
}
}
return nil
}
// goAssignHasTarget reports whether `name` appears among the left-hand
// targets of a short_var_declaration / assignment_statement.
func goAssignHasTarget(assign *sitter.Node, name string, src []byte) bool {
left := assign.ChildByFieldName("left")
if left == nil {
return false
}
for i, _nc := 0, int(left.NamedChildCount()); i < _nc; i++ {
c := left.NamedChild(i)
if c != nil && c.Type() == "identifier" && c.Content(src) == name {
return true
}
}
return false
}
// goAssignRHSExpr returns the right-hand expression assigned to `name`,
// matching the RHS position to the matched LHS target position for a
// parallel assignment (`a, name := x, "v"` → "v"). A single RHS shared by
// multiple targets is a multi-value call (`a, b := f()`) with no per-target
// literal to trace, so it returns nil. Returns nil when `name` is not a
// target of the assignment.
func goAssignRHSExpr(assign *sitter.Node, name string, src []byte) *sitter.Node {
left := assign.ChildByFieldName("left")
right := assign.ChildByFieldName("right")
if left == nil || right == nil || right.NamedChildCount() == 0 {
return nil
}
idx := -1
for i, _nc := 0, int(left.NamedChildCount()); i < _nc; i++ {
c := left.NamedChild(i)
if c != nil && c.Type() == "identifier" && c.Content(src) == name {
idx = i
break
}
}
if idx < 0 {
return nil
}
// Parallel assignment: take the RHS at the matched target position.
if left.NamedChildCount() == right.NamedChildCount() {
return right.NamedChild(idx)
}
// Single target, single value.
if left.NamedChildCount() == 1 && right.NamedChildCount() == 1 {
return right.NamedChild(0)
}
// Multi-value call (`a, b := f()`): no per-target literal to trace.
return nil
}
// goIsEnvRead reports whether a call_expression is `os.Getenv(...)` or
// `os.LookupEnv(...)`.
func goIsEnvRead(call *sitter.Node, src []byte) bool {
fn := call.ChildByFieldName("function")
if fn == nil || fn.Type() != "selector_expression" {
return false
}
op := fn.ChildByFieldName("operand")
field := fn.ChildByFieldName("field")
if op == nil || field == nil || op.Content(src) != "os" {
return false
}
switch field.Content(src) {
case "Getenv", "LookupEnv":
return true
}
return false
}
// goArgDefaultValue reduces an env-or-default helper's DEFAULT argument to its
// dispatch-name value. A string literal (`"ChargeActivity"`) yields the literal
// with isConst=false. A constant REFERENCE — a bare identifier
// (`ACTIVITY_NAME_DEFAULT`) or a selector_expression
// (`config.ACTIVITY_NAME_DEFAULT`) — yields the constant's NAME with
// isConst=true; the resolver later substitutes the constant's literal value
// through its const-deref index (the constant body usually lives in another
// package, invisible at extract time). Identifiers are admitted optimistically
// because the resolver validates them against the const-deref index — an
// identifier that is not actually a string constant simply fails to resolve and
// stays a broken_dispatch, so there is no false-resolution risk. Returns
// ("", false, false) for any other shape.
func goArgDefaultValue(node *sitter.Node, src []byte) (val string, isConst bool, ok bool) {
if node == nil {
return "", false, false
}
if lit, okk := goStringLiteralValue(node, src); okk {
return lit, false, true
}
switch node.Type() {
case "identifier":
return node.Content(src), true, true
case "selector_expression":
if field := node.ChildByFieldName("field"); field != nil {
return field.Content(src), true, true
}
}
return "", false, false
}
// goCallEnvDefaultLiteral inspects a `cmp.Or(os.Getenv("KEY"), "Default")`
// call and returns its literal default. cmp.Or returns the FIRST non-zero
// argument, so when the env read yields "" at runtime the value is the
// first string-literal argument that follows — hence we return the FIRST
// literal, not the last. Gated on the cmp.Or callee: an arbitrary user
// function mixing an env read with a literal (`combine(os.Getenv("K"),
// "Suffix")`) is deliberately NOT treated as env-or-default — only the
// stdlib cmp.Or idiom qualifies, since cmp.Or is the one combinator whose
// "first non-zero" semantics make the literal a provable default. Returns
// ("", false, false) when the callee is not cmp.Or, no os.Getenv /
// os.LookupEnv read is present, or there is no default argument. The default
// may be a string literal (isConst=false) or a constant reference
// (isConst=true) the resolver substitutes through its const-deref index.
func goCallEnvDefaultLiteral(call *sitter.Node, src []byte) (val string, isConst bool, ok bool) {
if !goIsCmpOr(call, src) {
return "", false, false
}
args := call.ChildByFieldName("arguments")
if args == nil {
return "", false, false
}
hasEnvRead := false
firstVal := ""
firstConst := false
haveDefault := false
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
if c.Type() == "call_expression" && goIsEnvRead(c, src) {
hasEnvRead = true
continue
}
if v, isC, okk := goArgDefaultValue(c, src); okk && !haveDefault {
firstVal, firstConst, haveDefault = v, isC, true
}
}
if hasEnvRead && haveDefault {
return firstVal, firstConst, true
}
return "", false, false
}
// goIsCmpOr reports whether a call_expression is a call to the stdlib
// `cmp.Or` — the canonical "first non-zero" combinator used for the
// env-or-default idiom. Matched by the canonical `cmp` package alias
// (consistent with the os.Getenv / "workflow" receiver gates elsewhere).
func goIsCmpOr(call *sitter.Node, src []byte) bool {
fn := call.ChildByFieldName("function")
if fn == nil || fn.Type() != "selector_expression" {
return false
}
op := fn.ChildByFieldName("operand")
field := fn.ChildByFieldName("field")
return op != nil && field != nil &&
op.Content(src) == "cmp" && field.Content(src) == "Or"
}
// goEnvHelperNames is the built-in allow-list of project-local env-or-default
// helper function names whose 2nd argument is the literal default. Matched
// case-insensitively; a corporate fork extends it at runtime via the per-repo
// allow-list threaded through `extra` (see goEnvHelperDefaultLiteral).
var goEnvHelperNames = []string{
"GetEnvOrDefault",
"GetEnvOrDefaultValue",
"EnvOr",
"GetenvDefault",
"GetEnvDefault",
}
// goEnvHelperDefaultLiteral recognises a call to a project-local
// env-or-default helper by name — `wfutils.GetEnvOrDefault(KEY, "Default")`
// or the bare `EnvOr(KEY, "Default")` — and returns the string-literal 2nd
// argument as the default. The callee name is taken from a bare identifier
// or, for a selector_expression, its trailing `field`; it is compared
// case-insensitively (strings.EqualFold) against the built-in goEnvHelperNames
// PLUS `extra` — the per-repo corporate allow-list (lower-cased keys) loaded
// from the git-ignored `.gortex/temporal-allowlist.yaml`. A match here is
// "allowlist"-sourced, so the resolver lands the edge at the inferred (visible)
// tier — that is how a corporate agent PROMOTES its own helper above the
// generic "env"-name heuristic. Returns ("", false) for any non-matching name
// or a non-string-literal 2nd arg.
func goEnvHelperDefaultLiteral(call *sitter.Node, src []byte, extra map[string]bool) (val string, isConst bool, ok bool) {
fn := call.ChildByFieldName("function")
if fn == nil {
return "", false, false
}
var callee string
switch fn.Type() {
case "identifier":
callee = fn.Content(src)
case "selector_expression":
if field := fn.ChildByFieldName("field"); field != nil {
callee = field.Content(src)
}
}
if callee == "" {
return "", false, false
}
matched := false
for _, name := range goEnvHelperNames {
if strings.EqualFold(callee, name) {
matched = true
break
}
}
if !matched && extra[strings.ToLower(callee)] {
matched = true
}
if !matched {
return "", false, false
}
args := call.ChildByFieldName("arguments")
if args == nil || args.NamedChildCount() < 2 {
return "", false, false
}
return goArgDefaultValue(args.NamedChild(1), src)
}
// goEnvHelperHeuristicDefault is the generic-recall fallback for env-or-default
// helpers whose NAME is not in the allow-list. It fires on a structural anchor
// — the callee's (bare or selector-trailing) name contains "env"
// (case-insensitive), the near-universal marker of an env-reading helper
// (`cfg.ActivityFromEnv("KEY", "Default")`, `getEnvActivity(...)`) — and takes
// the 2nd argument's string literal as the default. Deliberately lower-trust
// than the allow-list path: the caller tags the resulting edge
// `temporal_env_source=heuristic` so the resolver keeps it at the hidden
// speculative tier (where the LLM cleaning pass verifies or prunes it), rather
// than asserting it as a real dispatch. Returns ("", false) when the name lacks
// the "env" marker or the 2nd argument is not a string literal — so a plain
// `pick(KEY, "X")` (no env marker) is left untouched, preserving precision.
func goEnvHelperHeuristicDefault(call *sitter.Node, src []byte) (val string, isConst bool, ok bool) {
fn := call.ChildByFieldName("function")
if fn == nil {
return "", false, false
}
var callee string
switch fn.Type() {
case "identifier":
callee = fn.Content(src)
case "selector_expression":
if field := fn.ChildByFieldName("field"); field != nil {
callee = field.Content(src)
}
}
if callee == "" || !strings.Contains(strings.ToLower(callee), "env") {
return "", false, false
}
args := call.ChildByFieldName("arguments")
if args == nil || args.NamedChildCount() < 2 {
return "", false, false
}
return goArgDefaultValue(args.NamedChild(1), src)
}
// goStringLiteralValue returns the unquoted value of a Go string literal
// node, or ("", false) for any other node type.
func goStringLiteralValue(n *sitter.Node, src []byte) (string, bool) {
if n == nil {
return "", false
}
switch n.Type() {
case "interpreted_string_literal", "raw_string_literal":
return goTemporalNameFromExpr(n, src), true
}
return "", false
}
// goTemporalCallArgNames extracts positional arg names from a call expression.
//
// PURPOSE — extract positional arg names from a call expression for wrapper-following
// RATIONALE — qualifying args are string literals, selectors, Capitalized
//
// identifiers, OR a bare lowercase identifier that is one of the ENCLOSING
// function's own parameters (a name forwarded THROUGH this call) — the latter
// is what lets depth>1 wrapper-following propagate a name across multiple hops.
// `callerParams` is the enclosing function's parameter-name set (may be nil).
//
// KEYWORDS — arg_names, wrapper-following, call_expression, forwarded-param
func goTemporalCallArgNames(callNode *sitter.Node, src []byte, callerParams map[string]bool) ([]string, bool) {
if callNode == nil || callNode.Type() != "call_expression" {
return nil, false
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return nil, false
}
const maxArgs = 8
var out []string
qualifying := false
count := 0
for i := 0; i < int(args.NamedChildCount()) && count < maxArgs; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
count++
name := ""
switch c.Type() {
case "interpreted_string_literal", "raw_string_literal":
name = goTemporalNameFromExpr(c, src)
qualifying = true
case "selector_expression":
name = goTemporalNameFromExpr(c, src)
qualifying = true
case "identifier":
name = c.Content(src)
if name != "" && name[0] >= 'A' && name[0] <= 'Z' {
qualifying = true
} else if callerParams[name] {
// A bare lowercase identifier that is the enclosing function's
// own parameter: this call forwards a caller-supplied name
// THROUGH (`runStep(ctx, name, …)` inside a wrapper). Recording
// it lets the wrapper-following resolver discover the caller as
// a transitive wrapper and propagate the name up another level.
qualifying = true
}
}
out = append(out, name)
}
if !qualifying {
return nil, false
}
return out, true
}
// attachGoTemporalCallArgNames attaches arg_names + callee meta to a call edge.
//
// PURPOSE — attach arg_names and callee meta to a call edge for wrapper-following
// RATIONALE — the resolver's wrapper pass needs both the arg values and the callee name
//
// to match caller edges to wrapper definitions
//
// KEYWORDS — arg_names, callee, wrapper-following, edge meta
func attachGoTemporalCallArgNames(edge *graph.Edge, c goDeferredCall, callNode *sitter.Node, src []byte, callerParams map[string]bool) {
names, ok := goTemporalCallArgNames(callNode, src, callerParams)
if !ok {
return
}
if edge.Meta == nil {
edge.Meta = map[string]any{}
}
edge.Meta["arg_names"] = names
// callee: the function/method name being called
if c.isSelector {
edge.Meta["callee"] = c.method
} else if c.callName != "" {
edge.Meta["callee"] = c.callName
}
}
// goCompositeLiteralType walks up from a keyed_element node to find the
// enclosing composite_literal and returns the simple type name.
//
// PURPOSE — extracts the receiver struct type from a struct literal so the
// executor-field pass can key the field assignment by (type, field).
// RATIONALE — tree-sitter does not expose a direct parent-of-kind API;
// walking the Parent chain is the standard idiom in this codebase.
// KEYWORDS — composite-literal, type-name, executor-field
func goCompositeLiteralType(keyed *sitter.Node, src []byte) string {
for n := keyed; n != nil; n = n.Parent() {
if n.Type() != "composite_literal" {
continue
}
t := n.ChildByFieldName("type")
if t == nil {
return ""
}
switch t.Type() {
case "type_identifier":
return t.Content(src)
case "pointer_type":
if inner := t.NamedChild(0); inner != nil && inner.Type() == "type_identifier" {
return inner.Content(src)
}
case "qualified_type":
if f := t.ChildByFieldName("name"); f != nil {
return f.Content(src)
}
}
return ""
}
return ""
}