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

402 lines
13 KiB
Go

package indexer
import (
"strings"
"github.com/zzet/gortex/internal/graph"
)
// materializeDataflowParams runs after the regular call resolver
// pass to lift the placeholder targets carried by EdgeArgOf and
// EdgeReturnsTo edges to concrete graph IDs. The Go dataflow
// extractor (see internal/parser/languages/go_dataflow.go) emits
// these edges with an `unresolved::` text on the side that
// references the callee — exactly the shape the call resolver
// already knows how to lift. After Resolver.ResolveAll has run
// every placeholder side has been rewritten to a real function /
// method node ID; this pass then:
//
// 1. EdgeArgOf — joins the now-resolved To (a function/method
// node) against its incoming EdgeParamOf edges to find the
// param node at the recorded position (Meta["arg_position"]),
// and rewrites the edge target to the param node ID. When no
// matching param exists (variadic position past the declared
// count, signature mismatch from extern callees, etc.) the
// edge stays pointed at the function node — still a useful
// dataflow hop.
//
// 2. EdgeReturnsTo — joins the placeholder From (currently the
// enclosing caller's function ID) against the resolved
// EdgeCalls edge from the same caller at the same line,
// and rewrites From to the resolved callee. Falls back to
// leaving the placeholder in place when no matching call
// edge can be found (rare; usually means the call resolver
// declined to lift the call edge too).
//
// Both rewrite paths use graph.RemoveEdge + graph.AddEdge so the
// shard buckets / inverted indexes stay consistent with the new
// (From, To, Kind, Line) tuple. Edges whose Meta no longer
// matches their state are stripped of the dataflow markers so a
// re-run of this pass becomes a no-op.
func (idx *Indexer) materializeDataflowParams() {
g := idx.graph
// Only arg_of / returns_to edges are rewritten here. Fetch exactly
// those kinds — each an edges_by_kind index probe on the sqlite
// backend — instead of scanning (and meta-decoding) the whole edge
// set; every other edge in the graph is irrelevant to this pass.
for e := range g.EdgesByKind(graph.EdgeArgOf) {
rewriteArgOf(g, e)
}
for e := range g.EdgesByKind(graph.EdgeReturnsTo) {
rewriteReturnsTo(g, e)
}
}
// materializeDataflowParamsForFile is the single-file equivalent of
// materializeDataflowParams, used on the incremental (fsnotify /
// edit_file) re-index path so a one-line edit doesn't scan the whole
// edge set. fileEdges is the file's freshly-extracted edge slice
// (result.Edges from indexFile); only its From endpoints are read, so
// stale To/From values from before resolution don't matter.
//
// A file's arg_of / returns_to From is NOT always a node in the file,
// so node membership alone is insufficient. Two From classes exist:
// - file nodes: returns_to's From is the caller function, and an
// arg_of whose argument is a bare in-scope identifier has its From
// rewritten by the resolver to that local/param — GetFileNodes
// covers both.
// - synthetic ids: arg_of for a selector (obj.Field), package-
// qualified (pkg.V), global, or nested-call (f(g())) argument keeps
// a synthetic `unresolved::` / `external::` From that never becomes
// a file node. The resolver leaves these untouched, so the id the
// extractor emitted (still present in fileEdges) is the id in the
// graph.
//
// Probing the union of both, then keeping only edges whose FilePath is
// this file, yields exactly the arg_of+returns_to set the whole-graph
// pass would touch for it — faithful, not approximate. Each rewrite
// needs only the edge plus a targeted callee lookup (paramNodeAtPosition
// / findCallTarget). The batch path (Resolver.ResolveAll) still runs the
// whole-graph variant once, where amortising one scan over many files
// is the right trade.
func (idx *Indexer) materializeDataflowParamsForFile(graphPath string, fileEdges []*graph.Edge) {
g := idx.graph
fromSet := make(map[string]struct{})
for _, n := range g.GetFileNodes(graphPath) {
if n != nil && n.ID != "" {
fromSet[n.ID] = struct{}{}
}
}
for _, e := range fileEdges {
if e != nil && (e.Kind == graph.EdgeArgOf || e.Kind == graph.EdgeReturnsTo) && e.From != "" {
fromSet[e.From] = struct{}{}
}
}
if len(fromSet) == 0 {
return
}
froms := make([]string, 0, len(fromSet))
for id := range fromSet {
froms = append(froms, id)
}
// A synthetic From can be shared across files, so restrict the rewrite
// to edges this file actually emitted: every arg_of / returns_to edge
// carries its call-site FilePath, so the filter keeps the set exactly
// the file's own. Collect the file's arg_of / returns_to edges plus the
// distinct callees the arg_of rewrites target, so the per-callee param
// lookup is batched once instead of re-fetched per argument.
var argEdges, retEdges []*graph.Edge
callees := make(map[string]struct{})
for _, edges := range g.GetOutEdgesByNodeIDs(froms) {
for _, e := range edges {
if e == nil || e.FilePath != graphPath {
continue
}
switch e.Kind {
case graph.EdgeArgOf:
argEdges = append(argEdges, e)
if callee, _, ok := argOfRewriteTarget(e); ok {
callees[callee] = struct{}{}
}
case graph.EdgeReturnsTo:
retEdges = append(retEdges, e)
}
}
}
paramIdx := buildParamPositionIndex(g, callees)
for _, e := range argEdges {
rewriteArgOfIndexed(g, e, paramIdx)
}
for _, e := range retEdges {
rewriteReturnsTo(g, e)
}
}
// argOfRewriteTarget reports whether an arg_of edge is a rewrite
// candidate and, if so, the resolved callee id and the argument
// position. An edge already pointing at a param node, or still
// pointing at an unresolved / external stub, is not a candidate. Shared
// by the per-edge (rewriteArgOf) and indexed (rewriteArgOfIndexed) paths
// so the guard lives in one place.
func argOfRewriteTarget(e *graph.Edge) (calleeID string, pos int, ok bool) {
if e == nil || e.Meta == nil {
return "", 0, false
}
pos, ok = argPositionFromMeta(e.Meta)
if !ok {
return "", 0, false
}
to := e.To
if strings.Contains(to, "#param:") {
return "", 0, false
}
if strings.HasPrefix(to, "unresolved::") || strings.HasPrefix(to, "external::") {
return "", 0, false
}
return to, pos, true
}
// rewriteArgOf walks the resolved callee's incoming param_of edges
// and lifts the edge target from the function node to the param
// node at the recorded position. Edges that already point at a
// param node are left alone. Used by the whole-graph (cold) pass; the
// per-file pass uses the batched rewriteArgOfIndexed instead.
func rewriteArgOf(g graph.Store, e *graph.Edge) {
calleeID, pos, ok := argOfRewriteTarget(e)
if !ok {
return
}
paramID := paramNodeAtPosition(g, calleeID, pos)
if paramID == "" {
return
}
oldTo := e.To
g.RemoveEdge(e.From, oldTo, e.Kind)
e.To = paramID
g.AddEdge(e)
}
// rewriteArgOfIndexed is rewriteArgOf with the callee→position→param
// lookup served from a prebuilt index instead of a per-edge
// paramNodeAtPosition (which re-fetched the callee's entire in-edge list
// once per argument). Same rewrite, same guards.
func rewriteArgOfIndexed(g graph.Store, e *graph.Edge, paramIdx map[string]map[int]string) {
calleeID, pos, ok := argOfRewriteTarget(e)
if !ok {
return
}
m := paramIdx[calleeID]
if m == nil {
return
}
paramID := m[pos]
if paramID == "" {
return
}
oldTo := e.To
g.RemoveEdge(e.From, oldTo, e.Kind)
e.To = paramID
g.AddEdge(e)
}
// buildParamPositionIndex maps each callee id to its argument
// position → param-node-id table, built from two batched queries
// (in-edges of all callees, then the param nodes those edges point
// from). It replaces a per-arg_of-edge paramNodeAtPosition, which
// re-fetched a popular callee's whole in-edge list once per argument —
// the dominant cost of the per-file dataflow pass on a large file. The
// position is read from the param node's Meta exactly as
// paramNodeAtPosition does, with the first param at a position winning.
func buildParamPositionIndex(g graph.Store, callees map[string]struct{}) map[string]map[int]string {
if len(callees) == 0 {
return nil
}
ids := make([]string, 0, len(callees))
for id := range callees {
ids = append(ids, id)
}
inEdges := g.GetInEdgesByNodeIDs(ids)
type ownerParam struct{ owner, param string }
var pairs []ownerParam
paramSet := make(map[string]struct{})
for owner, edges := range inEdges {
for _, e := range edges {
if e != nil && e.Kind == graph.EdgeParamOf && e.From != "" {
pairs = append(pairs, ownerParam{owner: owner, param: e.From})
paramSet[e.From] = struct{}{}
}
}
}
if len(pairs) == 0 {
return nil
}
paramIDs := make([]string, 0, len(paramSet))
for id := range paramSet {
paramIDs = append(paramIDs, id)
}
nodes := g.GetNodesByIDs(paramIDs)
idx := make(map[string]map[int]string, len(inEdges))
for _, pr := range pairs {
n := nodes[pr.param]
if n == nil || n.Kind != graph.KindParam {
continue
}
pos, ok := intFromMeta(n.Meta, "position")
if !ok {
continue
}
m := idx[pr.owner]
if m == nil {
m = make(map[int]string)
idx[pr.owner] = m
}
if _, exists := m[pos]; !exists {
m[pos] = n.ID
}
}
return idx
}
// rewriteReturnsTo lifts the placeholder From by joining on the
// resolved EdgeCalls edge from the same caller and line.
func rewriteReturnsTo(g graph.Store, e *graph.Edge) {
if e == nil || e.Meta == nil {
return
}
if _, ok := e.Meta["returns_to_call"]; !ok {
return
}
callLine, _ := intFromMeta(e.Meta, "call_line")
if callLine == 0 {
callLine = e.Line
}
callerID := e.From
calleeText, _ := e.Meta["callee_target"].(string)
resolvedCallee := findCallTarget(g, callerID, callLine, calleeText)
if resolvedCallee == "" {
return
}
oldFrom := e.From
g.RemoveEdge(oldFrom, e.To, e.Kind)
e.From = resolvedCallee
g.AddEdge(e)
}
// findCallTarget returns the resolved To of the EdgeCalls edge
// originating from callerID at the given line. When `calleeText`
// is non-empty it's used as a tie-breaker against the original
// unresolved target string so we don't lift to the wrong call when
// two calls live on the same line. Falls back to the first match
// otherwise.
// outEdgeLightStore is implemented by backends that can return a node's
// out-edges without decoding the per-edge Meta blob. findCallTarget reads
// only endpoints/kind/line, so it opts into the cheaper fetch when the
// backend offers it (the sqlite backend, where the Meta JSON-decode
// otherwise dominates this hot lookup); other stores fall back.
type outEdgeLightStore interface {
GetOutEdgesLight(nodeID string) []*graph.Edge
}
func findCallTarget(g graph.Store, callerID string, line int, calleeText string) string {
var out []*graph.Edge
if ls, ok := g.(outEdgeLightStore); ok {
out = ls.GetOutEdgesLight(callerID)
} else {
out = g.GetOutEdges(callerID)
}
var fallback string
for _, e := range out {
if e.Kind != graph.EdgeCalls {
continue
}
if line != 0 && e.Line != line {
continue
}
if strings.HasPrefix(e.To, "unresolved::") {
continue
}
if calleeText != "" && callTargetMatches(e, calleeText) {
return e.To
}
if fallback == "" {
fallback = e.To
}
}
return fallback
}
// callTargetMatches reports whether a resolved call edge's text
// shape lines up with the dataflow edge's recorded callee_target.
// We compare the trailing path component of the resolved To
// against the unresolved::… form used at extraction time. Used as
// a same-line tie-breaker when more than one call lives on a
// single source line (e.g. `f(g())`).
func callTargetMatches(call *graph.Edge, calleeText string) bool {
if call == nil || calleeText == "" {
return false
}
bare := strings.TrimPrefix(calleeText, "unresolved::")
bare = strings.TrimPrefix(bare, "extern::")
bare = strings.TrimPrefix(bare, "*.")
if bare == "" {
return false
}
to := call.To
if i := strings.LastIndex(to, "::"); i >= 0 {
to = to[i+2:]
}
if i := strings.LastIndex(to, "."); i >= 0 {
to = to[i+1:]
}
return to == bare
}
// paramNodeAtPosition returns the param node ID with the recorded
// position attached to ownerID via EdgeParamOf.
func paramNodeAtPosition(g graph.Store, ownerID string, pos int) string {
in := g.GetInEdges(ownerID)
for _, e := range in {
if e.Kind != graph.EdgeParamOf {
continue
}
n := g.GetNode(e.From)
if n == nil || n.Kind != graph.KindParam {
continue
}
p, ok := intFromMeta(n.Meta, "position")
if !ok {
continue
}
if p == pos {
return n.ID
}
}
return ""
}
// argPositionFromMeta extracts the recorded argument position. The
// metadata roundtrip can yield int or float64 depending on origin
// (extractor vs JSON deserialisation), so accept both.
func argPositionFromMeta(m map[string]any) (int, bool) {
return intFromMeta(m, "arg_position")
}
func intFromMeta(m map[string]any, key string) (int, bool) {
if m == nil {
return 0, false
}
v, ok := m[key]
if !ok {
return 0, false
}
switch x := v.(type) {
case int:
return x, true
case int64:
return int(x), true
case float64:
return int(x), true
}
return 0, false
}