From 8e84b854a32424436482f614b9ab5b0565657498 Mon Sep 17 00:00:00 2001 From: Max Hsu Date: Fri, 12 Jun 2026 00:12:33 +0800 Subject: [PATCH] feat(resolution): index GoFrame g.Meta routes, link to controller methods (#747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GoFrame binds routes reflectively — the path/method live in a g.Meta struct tag on the request type, and the handler is the controller method named after that type with the Req suffix dropped (ChatReq -> Chat). So there was no literal route string and no call edge: route questions could only be answered lexically. Extend goResolver.extract to read the g.Meta tag (path: + method:, defaulting to ANY when method is absent) into a route node and link it to the controller method by the naming convention. Route node and its method edge are emitted together — a g.Meta whose type doesn't follow the …Req convention is skipped rather than left as an orphan route node. Only the relative path: from the tag is captured; joining the s.Group("/api", …) prefix is left to a follow-up. Follow-up to #720 (structural coverage, distinct from the ranking fix). --- __tests__/frameworks.test.ts | 27 ++++++++++++++++++++ src/resolution/frameworks/go.ts | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/__tests__/frameworks.test.ts b/__tests__/frameworks.test.ts index c0e874908..8ad98b355 100644 --- a/__tests__/frameworks.test.ts +++ b/__tests__/frameworks.test.ts @@ -942,6 +942,33 @@ describe('goResolver.extract', () => { const { references } = goResolver.extract!('routes.go', src); expect(references[0].referenceName).toBe('listUsers'); }); + + it('extracts a GoFrame g.Meta route and links it to the controller method (#747)', () => { + // GoFrame binds reflectively: the route lives in a `g.Meta` struct tag on the + // request type, and the handler is the controller method named after that type + // (ChatReq -> Chat). No literal route string / call edge exists otherwise. + const src = `package v1 +type ChatReq struct { + g.Meta \`path:"/chat" method:"post"\` + Content string \`json:"content"\` +} +`; + const { nodes, references } = goResolver.extract!('api/chat/v1/chat.go', src); + expect(nodes[0].kind).toBe('route'); + expect(nodes[0].name).toBe('POST /chat'); + expect(references[0].referenceName).toBe('Chat'); + expect(references[0].fromNodeId).toBe(nodes[0].id); + }); + + it('defaults a GoFrame g.Meta route with no method tag to ANY', () => { + const src = `type ListReq struct { + g.Meta \`path:"/list"\` +} +`; + const { nodes, references } = goResolver.extract!('api/v1/list.go', src); + expect(nodes[0].name).toBe('ANY /list'); + expect(references[0].referenceName).toBe('List'); + }); }); import { rustResolver } from '../src/resolution/frameworks/rust'; diff --git a/src/resolution/frameworks/go.ts b/src/resolution/frameworks/go.ts index b09201946..18c843532 100644 --- a/src/resolution/frameworks/go.ts +++ b/src/resolution/frameworks/go.ts @@ -131,6 +131,50 @@ export const goResolver: FrameworkResolver = { } } + // GoFrame binds routes reflectively: the path/method live in a `g.Meta` + // struct tag on the request type, so there is no literal route call to match + // above. Emit a route node from the tag and link it to the controller method, + // which GoFrame names after the request type with the `Req` suffix dropped + // (`ChatReq` -> `Chat`). Only the relative `path:` from the tag is captured; + // joining the `s.Group("/api", …)` prefix is left to a follow-up. (#747) + const goframeRegex = /\btype\s+(\w+)\s+struct\s*\{\s*g\.Meta\s+`([^`]*)`/g; + while ((match = goframeRegex.exec(safe)) !== null) { + const [, structName, tag] = match; + const routePath = /\bpath:"([^"]+)"/.exec(tag!)?.[1]; + if (!routePath) continue; // a g.Meta without a path: tag is not a route + // Keep route node and its controller-method edge together: skip types that + // don't follow the `…Req` convention rather than emit an orphan route node. + if (!structName!.endsWith('Req')) continue; + const methodName = structName!.slice(0, -'Req'.length); + if (!methodName) continue; + const method = /\bmethod:"([^"]+)"/.exec(tag!)?.[1]?.toUpperCase() ?? 'ANY'; + const line = safe.slice(0, match.index).split('\n').length; + + const routeNode: Node = { + id: `route:${filePath}:${line}:${method}:${routePath}`, + kind: 'route', + name: `${method} ${routePath}`, + qualifiedName: `${filePath}::route:${routePath}`, + filePath, + startLine: line, + endLine: line, + startColumn: 0, + endColumn: match[0].length, + language: 'go', + updatedAt: now, + }; + nodes.push(routeNode); + references.push({ + fromNodeId: routeNode.id, + referenceName: methodName, + referenceKind: 'references', + line, + column: 0, + filePath, + language: 'go', + }); + } + return { nodes, references }; }, };