* Go gRPC stub → impl bridge. The protoc-gen-go-grpc codegen emits an * `UnimplementedXxxServer` struct in `*_grpc.pb.go` carrying one method * per service RPC; the real handler is a hand-written struct in another * file (`x/bank/keeper/msg_server.go::msgServer.Send` in cosmos-sdk). * Go's struc
(queries: QueryBuilder)
| 760 | * stub's source line is the wiring site shown in the trace trail. |
| 761 | */ |
| 762 | function goGrpcStubImplEdges(queries: QueryBuilder): Edge[] { |
| 763 | const edges: Edge[] = []; |
| 764 | const seen = new Set<string>(); |
| 765 | |
| 766 | const STUB_RE = /^Unimplemented.*Server$/; |
| 767 | // gRPC internal-helper methods that appear on every Unimplemented*Server; |
| 768 | // not part of the service contract, so exclude when computing the RPC-method |
| 769 | // signature used to match impls. |
| 770 | const isInternalMarker = (n: string) => n.startsWith('mustEmbed') || n === 'testEmbeddedByValue'; |
| 771 | |
| 772 | // Methods directly contained by each Go struct, name-only. Built once. |
| 773 | const methodNamesByStruct = new Map<string, Set<string>>(); |
| 774 | const methodNodesByStruct = new Map<string, Node[]>(); |
| 775 | const goStructs: Node[] = []; |
| 776 | for (const s of queries.getNodesByKind('struct')) { |
| 777 | if (s.language !== 'go') continue; |
| 778 | goStructs.push(s); |
| 779 | const ms = queries |
| 780 | .getOutgoingEdges(s.id, ['contains']) |
| 781 | .map((e) => queries.getNodeById(e.target)) |
| 782 | .filter((n): n is Node => !!n && n.kind === 'method'); |
| 783 | methodNodesByStruct.set(s.id, ms); |
| 784 | methodNamesByStruct.set(s.id, new Set(ms.map((m) => m.name))); |
| 785 | } |
| 786 | |
| 787 | for (const stub of goStructs) { |
| 788 | if (!STUB_RE.test(stub.name)) continue; |
| 789 | // The stub MUST live in a generated file — that's what tells us this is |
| 790 | // a protoc-emitted scaffold rather than someone naming a struct |
| 791 | // `UnimplementedXxxServer` by hand. Without this gate we'd also bridge |
| 792 | // such hand-written structs and create misleading edges. |
| 793 | if (!isGeneratedFile(stub.filePath)) continue; |
| 794 | |
| 795 | const stubMethods = (methodNodesByStruct.get(stub.id) ?? []).filter( |
| 796 | (m) => !isInternalMarker(m.name), |
| 797 | ); |
| 798 | if (stubMethods.length === 0) continue; |
| 799 | const stubMethodNames = stubMethods.map((m) => m.name); |
| 800 | |
| 801 | for (const cand of goStructs) { |
| 802 | if (cand.id === stub.id) continue; |
| 803 | // Skip generated-file candidates — they're siblings (msgClient, |
| 804 | // UnsafeMsgServer, …) whose method sets coincidentally match. |
| 805 | if (isGeneratedFile(cand.filePath)) continue; |
| 806 | |
| 807 | const candNames = methodNamesByStruct.get(cand.id); |
| 808 | if (!candNames) continue; |
| 809 | // Subset: every RPC method must exist on the candidate by name. |
| 810 | // Signature-level match would tighten this further, but name-match |
| 811 | // alone already gives one-to-one pairing in real codebases because |
| 812 | // gRPC method-name sets are highly distinctive (Send + MultiSend + |
| 813 | // UpdateParams + SetSendEnabled is unique to bank's MsgServer). |
| 814 | if (!stubMethodNames.every((n) => candNames.has(n))) continue; |
| 815 | |
| 816 | const candMethods = methodNodesByStruct.get(cand.id) ?? []; |
| 817 | let added = 0; |
| 818 | for (const sm of stubMethods) { |
| 819 | if (added >= MAX_CALLBACKS_PER_CHANNEL) break; |
no test coverage detected