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
183 lines
5.8 KiB
Go
183 lines
5.8 KiB
Go
package resolver
|
|
|
|
import (
|
|
"regexp"
|
|
"sort"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// observerChannelVia marks a synthesized field-backed observer-channel call
|
|
// edge.
|
|
const observerChannelVia = "observer.channel"
|
|
|
|
// maxObserverCallbacksPerChannel caps how many callbacks one channel field may
|
|
// fan a dispatcher out to. A field registered with dozens of callbacks is
|
|
// almost always a generic bus where a dispatcher→callback edge per pair is
|
|
// noise; real domain observers wire a handful.
|
|
const maxObserverCallbacksPerChannel = 40
|
|
|
|
var (
|
|
// observerRegistrarRe matches a method that registers an observer into a
|
|
// channel field: onFoo / subscribe / addListener / register / watch / …
|
|
observerRegistrarRe = regexp.MustCompile(`(?i)^(on[a-z]\w*|subscribe|addlistener|addeventlistener|register|watch|listen|addcallback)$`)
|
|
// observerDispatcherRe matches a method that fires a channel field:
|
|
// emit / trigger / notify / dispatch / fire / publish / flush.
|
|
observerDispatcherRe = regexp.MustCompile(`(?i)(emit|trigger|notify|dispatch|fire|publish|flush)`)
|
|
)
|
|
|
|
// observerChannel pairs the registrar and dispatcher methods that share one
|
|
// field-backed channel.
|
|
type observerChannel struct {
|
|
registrars []*graph.Node
|
|
dispatchers []*graph.Node
|
|
}
|
|
|
|
// ResolveObserverChannelCalls is the framework-dispatch synthesizer for
|
|
// field-backed observer channels — the dynamic-dispatch hole where a method
|
|
// stores callbacks in a field and another method invokes them. A field is a
|
|
// channel when a registrar-named method (onFoo / subscribe / register / …) and
|
|
// a dispatcher-named method (emit / trigger / notify / …) both access it. For
|
|
// each such channel the pass finds the functions registered at the registrar's
|
|
// call sites — a function referenced on the same line as the registrar call —
|
|
// and synthesizes a calls edge from every dispatcher to every registered
|
|
// callback, the runtime dispatch the static call graph cannot see.
|
|
//
|
|
// High-precision, low-recall by design: named registrar/dispatcher methods
|
|
// paired by a shared field, callbacks taken from the registrar's call sites,
|
|
// and the fan-out capped. Full recompute and idempotent: edges are re-derived
|
|
// from the field-access and call metadata, graph.AddEdge dedupes by key, and
|
|
// graph.EvictFile drops the edge when either endpoint's file is reindexed.
|
|
// Edges ride at ast_inferred and carry synthesizer provenance.
|
|
//
|
|
// Returns the number of observer-channel call edges synthesized.
|
|
func ResolveObserverChannelCalls(g graph.Store) int {
|
|
if g == nil {
|
|
return 0
|
|
}
|
|
|
|
channels := map[string]*observerChannel{}
|
|
for e := range g.EdgesByKind(graph.EdgeAccessesField) {
|
|
if e == nil || e.To == "" || e.From == "" {
|
|
continue
|
|
}
|
|
m := g.GetNode(e.From)
|
|
if m == nil || (m.Kind != graph.KindMethod && m.Kind != graph.KindFunction) {
|
|
continue
|
|
}
|
|
isReg := observerRegistrarRe.MatchString(m.Name)
|
|
isDisp := observerDispatcherRe.MatchString(m.Name)
|
|
if !isReg && !isDisp {
|
|
continue
|
|
}
|
|
c := channels[e.To]
|
|
if c == nil {
|
|
c = &observerChannel{}
|
|
channels[e.To] = c
|
|
}
|
|
if isReg {
|
|
c.registrars = append(c.registrars, m)
|
|
}
|
|
if isDisp {
|
|
c.dispatchers = append(c.dispatchers, m)
|
|
}
|
|
}
|
|
if len(channels) == 0 {
|
|
return 0
|
|
}
|
|
|
|
// Stable iteration so a re-run produces edges in the same order.
|
|
fieldIDs := make([]string, 0, len(channels))
|
|
for id := range channels {
|
|
fieldIDs = append(fieldIDs, id)
|
|
}
|
|
sort.Strings(fieldIDs)
|
|
|
|
var batch []*graph.Edge
|
|
synthesized := 0
|
|
for _, field := range fieldIDs {
|
|
c := channels[field]
|
|
if len(c.registrars) == 0 || len(c.dispatchers) == 0 {
|
|
continue
|
|
}
|
|
callbacks := observerCallbacks(g, c.registrars)
|
|
if len(callbacks) == 0 || len(callbacks) > maxObserverCallbacksPerChannel {
|
|
continue
|
|
}
|
|
for _, disp := range c.dispatchers {
|
|
for _, cb := range callbacks {
|
|
if cb == disp.ID {
|
|
continue
|
|
}
|
|
if !sameDispatchBoundary(disp, g.GetNode(cb)) {
|
|
continue
|
|
}
|
|
batch = append(batch, observerChannelEdge(disp, cb, field))
|
|
synthesized++
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, e := range batch {
|
|
g.AddEdge(e)
|
|
}
|
|
return synthesized
|
|
}
|
|
|
|
// observerCallbacks collects the functions registered through a channel's
|
|
// registrar methods: a function/method referenced on the same file+line as a
|
|
// call to the registrar (the callback argument). Returns a sorted, de-duped ID
|
|
// list for deterministic edge synthesis.
|
|
func observerCallbacks(g graph.Store, registrars []*graph.Node) []string {
|
|
seen := map[string]bool{}
|
|
for _, reg := range registrars {
|
|
for _, in := range g.GetInEdges(reg.ID) {
|
|
if in == nil || in.Kind != graph.EdgeCalls || in.From == "" {
|
|
continue
|
|
}
|
|
for _, ref := range g.GetOutEdges(in.From) {
|
|
if ref == nil || ref.To == "" {
|
|
continue
|
|
}
|
|
if ref.Line != in.Line || ref.FilePath != in.FilePath {
|
|
continue
|
|
}
|
|
if ref.Kind != graph.EdgeReads && ref.Kind != graph.EdgeReferences {
|
|
continue
|
|
}
|
|
t := g.GetNode(ref.To)
|
|
if t == nil || (t.Kind != graph.KindFunction && t.Kind != graph.KindMethod) {
|
|
continue
|
|
}
|
|
seen[ref.To] = true
|
|
}
|
|
}
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for id := range seen {
|
|
out = append(out, id)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// observerChannelEdge builds one dispatcher→callback synthesized call edge.
|
|
func observerChannelEdge(from *graph.Node, toID, field string) *graph.Edge {
|
|
return &graph.Edge{
|
|
From: from.ID,
|
|
To: toID,
|
|
Kind: graph.EdgeCalls,
|
|
FilePath: from.FilePath,
|
|
Line: from.StartLine,
|
|
Confidence: 0.5,
|
|
ConfidenceLabel: graph.ConfidenceLabelFor(graph.EdgeCalls, 0.5),
|
|
Origin: graph.OriginASTInferred,
|
|
Meta: map[string]any{
|
|
"via": observerChannelVia,
|
|
"channel_field": field,
|
|
MetaSynthesizedBy: SynthObserverChannel,
|
|
MetaProvenance: ProvenanceHeuristic,
|
|
},
|
|
}
|
|
}
|