|
| 1 | +package engine |
| 2 | + |
| 3 | +import ( |
| 4 | + "log" |
| 5 | + "regexp" |
| 6 | + "strings" |
| 7 | + |
| 8 | + "github.com/enola-labs/enola/internal/facts" |
| 9 | +) |
| 10 | + |
| 11 | +// unimplementedEmbed matches the target of the `implements` edge a Go gRPC |
| 12 | +// server impl carries by embedding protoc-gen-go-grpc's forward-compat base |
| 13 | +// type — e.g. "usersv1.UnimplementedUserServiceServer" (optionally package- or |
| 14 | +// alias-qualified) — capturing the service short name ("UserService"). |
| 15 | +var unimplementedEmbed = regexp.MustCompile(`^(?:.*\.)?Unimplemented(.+)Server$`) |
| 16 | + |
| 17 | +// bindGRPCHandlers connects each gRPC server route (emitted from a .proto by the |
| 18 | +// grpc extractor) to the Go method that implements it, so route → handler is |
| 19 | +// traversable by impact_analysis and find_path. |
| 20 | +// |
| 21 | +// The bridge is the protoc-gen-go-grpc forward-compatibility convention: a |
| 22 | +// server impl embeds Unimplemented<Service>Server, which the Go extractor |
| 23 | +// already records as an `implements` edge on the impl struct. The embedded |
| 24 | +// type's short name ("UserService") equals the last segment of the route's |
| 25 | +// rpc_service ("users.v1.UserService"), so the route's rpc_method maps to the |
| 26 | +// struct's method symbol ("users.UserService.<Method>"). |
| 27 | +// |
| 28 | +// It runs post-extraction (like flagUnmatchedRoutes) over the assembled store, |
| 29 | +// before BuildGraph, and is idempotent, so it recomputes safely on every |
| 30 | +// snapshot and append without any per-extractor cache involvement. |
| 31 | +func (e *Engine) bindGRPCHandlers() { |
| 32 | + symbols := e.store.ByKind(facts.KindSymbol) |
| 33 | + |
| 34 | + // Per-repo index of service short name → impl struct symbol name, plus the |
| 35 | + // set of method symbol names (for existence checks). Both are scoped by repo |
| 36 | + // so a route only ever binds to a handler in its own repo. |
| 37 | + implMap := map[string]map[string]string{} // repo → shortName → struct name |
| 38 | + ambiguous := map[string]map[string]bool{} // repo → shortName → seen twice |
| 39 | + methodSet := map[string]map[string]bool{} // repo → method name → exists |
| 40 | + |
| 41 | + for _, s := range symbols { |
| 42 | + kind, _ := s.Props["symbol_kind"].(string) |
| 43 | + switch kind { |
| 44 | + case facts.SymbolStruct: |
| 45 | + short := implShortName(s) |
| 46 | + if short == "" { |
| 47 | + continue |
| 48 | + } |
| 49 | + if implMap[s.Repo] == nil { |
| 50 | + implMap[s.Repo] = map[string]string{} |
| 51 | + ambiguous[s.Repo] = map[string]bool{} |
| 52 | + } |
| 53 | + if existing, ok := implMap[s.Repo][short]; ok && existing != s.Name { |
| 54 | + ambiguous[s.Repo][short] = true |
| 55 | + } else { |
| 56 | + implMap[s.Repo][short] = s.Name |
| 57 | + } |
| 58 | + case facts.SymbolMethod: |
| 59 | + if methodSet[s.Repo] == nil { |
| 60 | + methodSet[s.Repo] = map[string]bool{} |
| 61 | + } |
| 62 | + methodSet[s.Repo][s.Name] = true |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + bound := 0 |
| 67 | + e.store.UpdateWhere(func(f *facts.Fact) { |
| 68 | + if f.Kind != facts.KindRoute || f.Props == nil { |
| 69 | + return |
| 70 | + } |
| 71 | + if f.Props["type"] != "grpc" || f.Props["role"] != "server" { |
| 72 | + return |
| 73 | + } |
| 74 | + short := lastDotSegment(propStr(f, "rpc_service")) |
| 75 | + method := propStr(f, "rpc_method") |
| 76 | + if short == "" || method == "" { |
| 77 | + return |
| 78 | + } |
| 79 | + if ambiguous[f.Repo][short] { |
| 80 | + return // two impls claim this service short name — don't guess |
| 81 | + } |
| 82 | + impl := implMap[f.Repo][short] |
| 83 | + if impl == "" { |
| 84 | + return |
| 85 | + } |
| 86 | + target := impl + "." + method |
| 87 | + if !methodSet[f.Repo][target] { |
| 88 | + return |
| 89 | + } |
| 90 | + if hasRelation(f, facts.RelHandledBy, target) { |
| 91 | + return // idempotent across appends |
| 92 | + } |
| 93 | + f.Relations = append(f.Relations, facts.Relation{Kind: facts.RelHandledBy, Target: target}) |
| 94 | + f.Props["handler"] = target |
| 95 | + bound++ |
| 96 | + }) |
| 97 | + if bound > 0 { |
| 98 | + log.Printf("[engine] bound %d gRPC server route(s) to their Go handler", bound) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +// implShortName returns the gRPC service short name a struct implements by |
| 103 | +// embedding Unimplemented<Service>Server, or "" if it embeds no such type. |
| 104 | +func implShortName(s facts.Fact) string { |
| 105 | + for _, r := range s.Relations { |
| 106 | + if r.Kind != facts.RelImplements { |
| 107 | + continue |
| 108 | + } |
| 109 | + if m := unimplementedEmbed.FindStringSubmatch(r.Target); m != nil { |
| 110 | + return m[1] |
| 111 | + } |
| 112 | + } |
| 113 | + return "" |
| 114 | +} |
| 115 | + |
| 116 | +func hasRelation(f *facts.Fact, kind, target string) bool { |
| 117 | + for _, r := range f.Relations { |
| 118 | + if r.Kind == kind && r.Target == target { |
| 119 | + return true |
| 120 | + } |
| 121 | + } |
| 122 | + return false |
| 123 | +} |
| 124 | + |
| 125 | +func propStr(f *facts.Fact, key string) string { |
| 126 | + if f.Props == nil { |
| 127 | + return "" |
| 128 | + } |
| 129 | + v, _ := f.Props[key].(string) |
| 130 | + return v |
| 131 | +} |
| 132 | + |
| 133 | +// lastDotSegment returns the substring after the final '.', or the whole string |
| 134 | +// if there is none — turning a proto FQN "users.v1.UserService" into the service |
| 135 | +// short name "UserService". |
| 136 | +func lastDotSegment(s string) string { |
| 137 | + if i := strings.LastIndex(s, "."); i >= 0 { |
| 138 | + return s[i+1:] |
| 139 | + } |
| 140 | + return s |
| 141 | +} |
0 commit comments