goframe-synthesizer.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * GoFrame route → controller-method dispatch synthesis (#747).
  3. *
  4. * GoFrame binds routes reflectively (`group.Bind(user.NewV1())`), so the route
  5. * declared in a request type's `g.Meta` tag has no static edge to the method
  6. * that serves it. The `goframeResolver` extract pass turns each `g.Meta` into a
  7. * `route` node carrying its request type in the qualifiedName; this whole-graph
  8. * pass closes the loop by joining each route to its handler.
  9. *
  10. * The join key is the REQUEST TYPE, not the method name — GoFrame method names
  11. * are free (`DeptSearchReq` is served by `List`, `DeptAddReq` by `Add`), so the
  12. * only reliable link is the request type appearing in the handler's parameter
  13. * signature:
  14. *
  15. * func (c *sysDeptController) Add(ctx context.Context, req *system.DeptAddReq) (…)
  16. * ^^^^^^^^^^^^^^^^ the join
  17. *
  18. * Go method nodes already carry that signature, so no source re-read is needed.
  19. * Each synthesized edge is `kind:'calls'`, `provenance:'heuristic'`,
  20. * `metadata.synthesizedBy:'goframe-route'` — a reflective-dispatch bridge, so
  21. * `codegraph_explore` surfaces it as a dynamic hop rather than a literal call,
  22. * and the handler's callers list the route that reaches it. A project with no
  23. * GoFrame routes is a no-op.
  24. */
  25. import type { Edge, Node } from '../types';
  26. import type { ResolutionContext } from './types';
  27. import { GOFRAME_ROUTE_MARKER } from './frameworks/goframe';
  28. const FANOUT_CAP = 2000; // backstop only; real apps are 1 route → 1 method.
  29. /**
  30. * Pointer-parameter types in a Go method signature, in both qualified and bare
  31. * forms: `(ctx context.Context, req *cash.ListReq)` → `["cash.ListReq",
  32. * "ListReq"]`. The qualified form disambiguates the many identical bare names a
  33. * large app defines (one `ListReq` per module); the bare form is the fallback
  34. * for a same-package (unqualified) handler. The response pointer (`*cash.ListRes`)
  35. * is captured too but never matches a request type, so it drops out of the join.
  36. */
  37. function pointerParamTypes(sig: string): string[] {
  38. const out: string[] = [];
  39. const re = /\*\s*(?:(\w+)\.)?([A-Z]\w*)\b/g;
  40. let m: RegExpExecArray | null;
  41. while ((m = re.exec(sig)) !== null) {
  42. if (m[1]) out.push(`${m[1]}.${m[2]}`);
  43. out.push(m[2]!);
  44. }
  45. return out;
  46. }
  47. /** The addon/plugin module a path lives under (`addons/hgexample/…` → `hgexample`),
  48. * or `''` for the core app. Large GoFrame apps ship demo addons that CLONE the
  49. * whole module tree — identical package names and request types — so the package
  50. * qualifier can't tell an addon's `config.GetReq` from core's. The addon root can. */
  51. function addonRoot(p: string): string {
  52. return /(?:^|\/)addons\/([^/]+)\//.exec(p)?.[1] ?? '';
  53. }
  54. /**
  55. * Pick the one handler for a route from same-request-type candidates. Usually a
  56. * single candidate. When several share the request type (a cloned addon module),
  57. * keep controller-dir methods, then the one in the route's own module (core route
  58. * → core handler, addon route → that addon's handler). Ambiguity left over ⇒ no
  59. * edge (silent beats wrong).
  60. */
  61. function selectHandler(candidates: Node[], routeFile: string): Node | null {
  62. if (candidates.length === 1) return candidates[0]!;
  63. let cands = candidates.filter((h) => /\/controller(s)?\//.test(h.filePath));
  64. if (cands.length === 0) cands = candidates;
  65. if (cands.length === 1) return cands[0]!;
  66. const ar = addonRoot(routeFile);
  67. const sameModule = cands.filter((h) => addonRoot(h.filePath) === ar);
  68. return sameModule.length === 1 ? sameModule[0]! : null;
  69. }
  70. export function goframeRouteEdges(ctx: ResolutionContext): Edge[] {
  71. // Route nodes the goframe extractor created, keyed by their package-qualified
  72. // request type (`cash.ListReq`). `wanted` holds every key a handler signature
  73. // could match — the qualified form plus its bare type fallback.
  74. const routesByReqType = new Map<string, Node[]>();
  75. const wanted = new Set<string>();
  76. for (const route of ctx.getNodesByKind('route')) {
  77. if (route.language !== 'go') continue;
  78. const marker = route.qualifiedName.indexOf(GOFRAME_ROUTE_MARKER);
  79. if (marker < 0) continue;
  80. const joinKey = route.qualifiedName.slice(marker + GOFRAME_ROUTE_MARKER.length);
  81. if (!joinKey) continue;
  82. let arr = routesByReqType.get(joinKey);
  83. if (!arr) { arr = []; routesByReqType.set(joinKey, arr); }
  84. arr.push(route);
  85. wanted.add(joinKey);
  86. const dot = joinKey.lastIndexOf('.');
  87. if (dot >= 0) wanted.add(joinKey.slice(dot + 1)); // bare fallback
  88. }
  89. if (routesByReqType.size === 0) return [];
  90. // Handler candidates: Go methods whose signature takes a wanted request type by
  91. // pointer, indexed by every matching (qualified + bare) form so a route can
  92. // match precisely on `pkg.Type` and fall back to the bare `Type`.
  93. const handlersByKey = new Map<string, Node[]>();
  94. for (const method of ctx.getNodesByKind('method')) {
  95. if (method.language !== 'go' || !method.signature) continue;
  96. for (const t of pointerParamTypes(method.signature)) {
  97. if (!wanted.has(t)) continue;
  98. let arr = handlersByKey.get(t);
  99. if (!arr) { arr = []; handlersByKey.set(t, arr); }
  100. arr.push(method);
  101. }
  102. }
  103. const edges: Edge[] = [];
  104. const seen = new Set<string>();
  105. let added = 0;
  106. for (const [joinKey, routes] of routesByReqType) {
  107. const bare = joinKey.includes('.') ? joinKey.slice(joinKey.lastIndexOf('.') + 1) : joinKey;
  108. // Precise package-qualified match first; bare type only as a fallback (covers
  109. // a same-package handler or an aliased import where the bare name is unique).
  110. const candidates = handlersByKey.get(joinKey) ?? handlersByKey.get(bare);
  111. if (!candidates || candidates.length === 0) continue;
  112. const requestType = bare;
  113. for (const route of routes) {
  114. const handler = selectHandler(candidates, route.filePath);
  115. if (!handler || route.id === handler.id) continue;
  116. const key = `${route.id}>${handler.id}`;
  117. if (seen.has(key) || added >= FANOUT_CAP) continue;
  118. seen.add(key);
  119. edges.push({
  120. source: route.id,
  121. target: handler.id,
  122. kind: 'calls',
  123. line: route.startLine,
  124. provenance: 'heuristic',
  125. metadata: {
  126. synthesizedBy: 'goframe-route',
  127. route: route.name,
  128. requestType,
  129. registeredAt: `${handler.filePath}:${handler.startLine}`,
  130. },
  131. });
  132. added++;
  133. }
  134. }
  135. return edges;
  136. }