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

172 lines
4.8 KiB
Go

package resolver
import (
"sort"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// springEventVia is the Meta["via"] tag the Java extractor stamps on a
// Spring publishEvent placeholder.
const springEventVia = "spring-event"
// ResolveSpringEventCalls binds Spring application-event publishers to their
// listeners by event type: a `publishEvent(new OrderPlaced(...))` reaches
// every `@EventListener void on(OrderPlaced e)` and
// `ApplicationListener<OrderPlaced>.onApplicationEvent`. Listeners for a
// base event type also receive a published subtype when the type hierarchy
// edge is present. Type-keyed, so edges land at the typed framework tier.
//
// Returns the number of publisher → listener edges synthesized.
func ResolveSpringEventCalls(g graph.Store) int {
if g == nil {
return 0
}
listenersByType := map[string][]*graph.Node{}
for _, n := range nodesByKindsOrAll(g, graph.KindMethod, graph.KindFunction) {
if n == nil || n.Meta == nil {
continue
}
if t, _ := n.Meta["spring_listener_type"].(string); t != "" {
listenersByType[springSimpleType(t)] = append(listenersByType[springSimpleType(t)], n)
}
}
if len(listenersByType) == 0 {
return 0
}
parents := springTypeParents(g)
resolved := 0
var reindex []graph.EdgeReindex
var batch []*graph.Edge
for e := range g.EdgesByKind(graph.EdgeCalls) {
if e == nil || e.Meta == nil {
continue
}
if v, _ := e.Meta["via"].(string); v != springEventVia {
continue
}
evType, _ := e.Meta["spring_event_type"].(string)
if evType == "" {
continue
}
listeners := springListenersFor(springSimpleType(evType), listenersByType, parents)
if len(listeners) == 0 {
if !graph.IsUnresolvedTarget(e.To) {
oldTo := e.To
e.To = "unresolved::*." + evType
e.Confidence = 0
e.ConfidenceLabel = ""
UnstampSynthesized(e)
reindex = append(reindex, graph.EdgeReindex{Edge: e, OldTo: oldTo})
}
continue
}
// First listener reuses the placeholder edge; the rest fan out.
first := listeners[0]
if e.To != first.ID {
oldTo := e.To
e.To = first.ID
e.Origin = graph.OriginASTInferred
e.Confidence = ConfidenceTyped
e.ConfidenceLabel = graph.ConfidenceLabelFor(graph.EdgeCalls, ConfidenceTyped)
StampSynthesizedTyped(e, SynthSpringEvent)
reindex = append(reindex, graph.EdgeReindex{Edge: e, OldTo: oldTo})
}
resolved++
for _, l := range listeners[1:] {
ne := &graph.Edge{
From: e.From, To: l.ID, Kind: graph.EdgeCalls,
FilePath: e.FilePath, Line: e.Line,
Origin: graph.OriginASTInferred,
Confidence: ConfidenceTyped,
ConfidenceLabel: graph.ConfidenceLabelFor(graph.EdgeCalls, ConfidenceTyped),
Meta: map[string]any{
"via": springEventVia,
"spring_event_type": evType,
MetaSynthesizedBy: SynthSpringEvent,
MetaProvenance: ProvenanceFramework,
},
}
batch = append(batch, ne)
resolved++
}
}
if len(reindex) > 0 {
g.ReindexEdges(reindex)
}
for _, ne := range batch {
g.AddEdge(ne)
}
return resolved
}
// springListenersFor returns the listener methods for an event type and its
// ancestors, deduped and sorted by node ID for deterministic fan-out.
func springListenersFor(evType string, byType map[string][]*graph.Node, parents map[string][]string) []*graph.Node {
seenType := map[string]bool{}
seenNode := map[string]bool{}
var out []*graph.Node
queue := []string{evType}
for len(queue) > 0 {
t := queue[0]
queue = queue[1:]
if seenType[t] {
continue
}
seenType[t] = true
for _, n := range byType[t] {
if n != nil && !seenNode[n.ID] {
seenNode[n.ID] = true
out = append(out, n)
}
}
queue = append(queue, parents[t]...)
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
// springTypeParents builds a simple-name child → parent-type map from the
// graph's extends/implements edges, so a published subtype can reach a
// base-type listener.
func springTypeParents(g graph.Store) map[string][]string {
parents := map[string][]string{}
add := func(kind graph.EdgeKind) {
for e := range g.EdgesByKind(kind) {
if e == nil || e.From == "" || e.To == "" {
continue
}
child := springSimpleType(e.From)
parent := springSimpleType(e.To)
if child != "" && parent != "" && child != parent {
parents[child] = append(parents[child], parent)
}
}
}
add(graph.EdgeExtends)
add(graph.EdgeImplements)
return parents
}
// springSimpleType reduces a node ID / type ref to its simple type name:
// strips the unresolved marker, any `::` / `.` qualifier, and generics.
func springSimpleType(s string) string {
if graph.IsUnresolvedTarget(s) {
s = graph.UnresolvedName(s)
}
if i := strings.LastIndex(s, "::"); i >= 0 {
s = s[i+2:]
}
if i := strings.IndexByte(s, '<'); i >= 0 {
s = s[:i]
}
if i := strings.LastIndexByte(s, '.'); i >= 0 {
s = s[i+1:]
}
return strings.TrimSpace(s)
}