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
172 lines
4.8 KiB
Go
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)
|
|
}
|