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

229 lines
7.6 KiB
Go

package resolver
import (
"sort"
"github.com/zzet/gortex/internal/graph"
)
// macroExpansionVia marks a call edge synthesized at a function-like
// macro's expansion (use) site.
//
// The C/C++ extractor recovers the calls hidden inside a function-like
// macro's replacement list and attributes them to the macro's `#define`
// line — that is where the call *text* lives. But it is not where the
// call *happens*: a `CALL_M(o);` in real code expands to `(o)->run()` at
// the invocation line, inside the invoking function. This pass adds, at
// the use site, the caller -> callee edge the expansion implies, so a
// forward call walk (get_call_chain) shows `caller -> run` where the
// macro is invoked, not only at the definition.
const macroExpansionVia = "macro_expansion"
// macroFunctionKindMeta is the Meta["macro_kind"] value the extractor
// stamps on a function-like (parameterised) macro node — the only macro
// shape whose use site is a call_expression and whose body can hide
// calls. Object-like macros are skipped.
const macroFunctionKindMeta = "function"
// ResolveMacroExpansionCalls mints, for every site that invokes a
// function-like macro, a direct caller -> callee call edge for each call
// the macro's body hides — attributed to the USE-SITE line and file
// rather than the macro's `#define` line.
//
// Recoverable seam: a function-like macro invocation `CALL_M(o)` parses
// (the preprocessor is not run) as an ordinary call_expression naming
// CALL_M, so the extractor emits `caller --(use line)--> unresolved::CALL_M`.
// The macro node itself is KindMacro, which the call resolver never binds
// (it accepts only function / method targets), so the use-site edge stays
// an `unresolved::<macro>` placeholder carrying the use-site line — the
// durable signal this pass keys on. The macro node already carries the
// recovered body callees as its out-edges (`macro --(define line)-->
// callee`, emitted by the extractor).
//
// For each use-site edge whose name uniquely binds — within the caller's
// repo — to one function-like macro that recovered callees, the pass
// emits `caller --(use line)--> callee` for each recovered callee. The
// edge rides at OriginASTInferred (a heuristic materialisation, never an
// upgrade over a compiler-verified fact) and carries the macro-expansion
// provenance. It is idempotent: the edge key includes the line,
// graph.AddEdge dedupes by key, and an existing edge at that key authored
// by anything other than this pass is left untouched, so a real (e.g.
// LSP-resolved) edge is never overwritten or downgraded.
//
// Conservative by construction: a macro name bound by more than one
// function-like macro in the same repo is ambiguous and skipped; the
// macro's def-site recovered-callee edges and the use-site placeholder
// both remain, so this pass only ever adds the use-site attribution and
// never removes the existing edges.
//
// Returns the number of use-site call edges the pass owns after this run.
func ResolveMacroExpansionCalls(g graph.Store) int {
if g == nil {
return 0
}
// Index function-like macros that recovered body callees, keyed by
// (repo, name). A name bound by more than one such macro in a repo is
// ambiguous: mark it so every use site of that name is skipped.
type macroEntry struct {
node *graph.Node
callees []string
ambiguous bool
}
byKey := map[string]*macroEntry{}
macroKey := func(repo, name string) string { return repo + "\x00" + name }
for _, n := range nodesByKindsOrAll(g, graph.KindMacro) {
if n == nil || n.Meta == nil || n.Name == "" {
continue
}
if k, _ := n.Meta["macro_kind"].(string); k != macroFunctionKindMeta {
continue
}
callees := macroBodyCallees(g, n.ID)
if len(callees) == 0 {
continue
}
key := macroKey(n.RepoPrefix, n.Name)
if e, ok := byKey[key]; ok {
e.ambiguous = true
continue
}
byKey[key] = &macroEntry{node: n, callees: callees}
}
if len(byKey) == 0 {
return 0
}
// Collect use-site edges first so the AddEdge writes below cannot
// mutate a live EdgesByKind iteration. A use site is either an
// unresolved `unresolved::<macro>` placeholder (the common case — the
// call resolver leaves macro-named calls unresolved) or a call edge
// landed directly on the macro node (defensive: no resolver does this
// today, but a future one might).
type useSite struct {
caller string
file string
line int
entry *macroEntry
}
var sites []useSite
for e := range g.EdgesByKind(graph.EdgeCalls) {
if e == nil || e.From == "" || e.To == "" {
continue
}
var entry *macroEntry
switch {
case graph.IsUnresolvedTarget(e.To):
name := graph.UnresolvedName(e.To)
if name == "" {
continue
}
caller := g.GetNode(e.From)
if caller == nil {
continue
}
ent, ok := byKey[macroKey(caller.RepoPrefix, name)]
if !ok || ent.ambiguous {
continue
}
entry = ent
default:
target := g.GetNode(e.To)
if target == nil || target.Kind != graph.KindMacro {
continue
}
ent, ok := byKey[macroKey(target.RepoPrefix, target.Name)]
if !ok || ent.ambiguous || ent.node.ID != target.ID {
continue
}
entry = ent
}
sites = append(sites, useSite{caller: e.From, file: e.FilePath, line: e.Line, entry: entry})
}
if len(sites) == 0 {
return 0
}
// Deterministic order so the owned-edge count and the batched writes
// are stable across runs.
sort.Slice(sites, func(i, j int) bool {
if sites[i].caller != sites[j].caller {
return sites[i].caller < sites[j].caller
}
if sites[i].file != sites[j].file {
return sites[i].file < sites[j].file
}
if sites[i].line != sites[j].line {
return sites[i].line < sites[j].line
}
return sites[i].entry.node.ID < sites[j].entry.node.ID
})
owned := 0
for _, s := range sites {
for _, callee := range s.entry.callees {
if callee == s.caller {
continue
}
if existing := existingCallEdge(g, s.caller, callee, s.file, s.line); existing != nil {
if v, _ := existing.Meta["via"].(string); v != macroExpansionVia {
// A real edge already occupies this exact identity
// slot — never overwrite or downgrade it.
continue
}
}
g.AddEdge(&graph.Edge{
From: s.caller,
To: callee,
Kind: graph.EdgeCalls,
FilePath: s.file,
Line: s.line,
Confidence: ConfidenceHeuristic,
ConfidenceLabel: graph.ConfidenceLabelFor(graph.EdgeCalls, ConfidenceHeuristic),
Origin: graph.OriginASTInferred,
Meta: map[string]any{
"via": macroExpansionVia,
"macro": s.entry.node.Name,
MetaSynthesizedBy: SynthMacroExpansion,
MetaProvenance: ProvenanceHeuristic,
},
})
owned++
}
}
return owned
}
// macroBodyCallees returns the distinct call targets a macro node's body
// recovered — the `To` of each EdgeCalls out-edge the extractor emitted
// from the macro node — in stable order.
func macroBodyCallees(g graph.Store, macroID string) []string {
seen := map[string]bool{}
var out []string
for _, e := range g.GetOutEdges(macroID) {
if e == nil || e.Kind != graph.EdgeCalls || e.To == "" {
continue
}
if seen[e.To] {
continue
}
seen[e.To] = true
out = append(out, e.To)
}
sort.Strings(out)
return out
}
// existingCallEdge returns the call edge already at the exact
// (from, to, file, line) identity slot, or nil. Used both to keep the
// pass idempotent and to refuse to overwrite a real edge.
func existingCallEdge(g graph.Store, from, to, file string, line int) *graph.Edge {
for _, e := range g.GetOutEdges(from) {
if e != nil && e.Kind == graph.EdgeCalls && e.To == to && e.FilePath == file && e.Line == line {
return e
}
}
return nil
}