feat(go): index GoFrame g.Meta routes and bind them to controller methods (#747) (#957)

GoFrame's standard router binds routes reflectively (group.Bind(ctrl)): the path
and method live in a g.Meta struct tag on a request type, and the controller
method that serves it is matched by that request type at runtime — so there was
no path string and no edge from a route to its handler, and "where is this route
handled / where are routes bound to controllers?" could only be answered
lexically (issue #720's report).

- frameworks/goframe.ts: detect gogf/gf in go.mod, extract each path-bearing
  g.Meta into a route node (requires path:, so response mime:-only tags are
  skipped), encoding the package-qualified request type for the join.
- goframe-synthesizer.ts: join each route -> the controller method whose
  signature takes that request type — NOT by name (DeptSearchReq is served by
  List) — keyed pkg.Type to disambiguate the many identical bare names a large
  app defines one-per-module, with an addon-root tiebreak for cloned demo addons.
  Edge kind calls, provenance heuristic, synthesizedBy goframe-route, surfaced as
  a dynamic-dispatch hop in codegraph_explore.

Validated on real repos: gf-demo-user 7/7, gfast 65/68 (3 genuinely
handler-less), hotgo 242/247 (98%) — 100% precision (0 non-controller handlers,
0 core/addon cross-binding), node count stable. Agent A/B (gfast, sonnet/high,
2 runs/arm): with codegraph 1 explore call / 0 Read / ~20s vs without 7.5 Read
avg + grep-hunting for the non-existent literal route string / ~42s; same correct
answer.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-22 18:16:11 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 6459ead6aa
commit a89315645d
9 changed files with 531 additions and 0 deletions
+72
View File
@@ -944,6 +944,78 @@ describe('goResolver.extract', () => {
});
});
import { goframeResolver } from '../src/resolution/frameworks/goframe';
describe('goframeResolver', () => {
it('detects GoFrame from a gogf/gf dependency in go.mod', () => {
const ctx: any = {
readFile: (f: string) =>
f === 'go.mod' ? 'module example.com/app\nrequire github.com/gogf/gf/v2 v2.7.0\n' : null,
};
expect(goframeResolver.detect(ctx)).toBe(true);
const noGf: any = { readFile: (f: string) => (f === 'go.mod' ? 'module example.com/app\n' : null) };
expect(goframeResolver.detect(noGf)).toBe(false);
});
it('extracts a route node from a g.Meta request struct (method upper-cased)', () => {
const src = `package v1
import "github.com/gogf/gf/v2/frame/g"
type SignInReq struct {
g.Meta \`path:"/user/sign-in" method:"post" tags:"User" summary:"Sign in"\`
Passport string
}
type SignInRes struct{}
`;
const { nodes } = goframeResolver.extract!('api/user/v1/user_sign_in.go', src);
expect(nodes).toHaveLength(1);
expect(nodes[0].kind).toBe('route');
expect(nodes[0].name).toBe('POST /user/sign-in');
// The package-qualified request type is encoded for the synthesizer join.
expect(nodes[0].qualifiedName).toContain('::goframe-route:v1.SignInReq');
});
it('is independent of g.Meta tag attribute order', () => {
const src = `type DeptSearchReq struct {
g.Meta \`path:"/dept/list" tags:"Dept" method:"get" summary:"列表"\`
}`;
const { nodes } = goframeResolver.extract!('api/system/dept.go', src);
expect(nodes[0].name).toBe('GET /dept/list');
expect(nodes[0].qualifiedName).toContain('::goframe-route:DeptSearchReq');
});
it('skips a response g.Meta that has no path (mime-only) and other non-route metadata', () => {
const src = `type ListRes struct {
g.Meta \`mime:"application/json"\`
Items []string
}`;
const { nodes } = goframeResolver.extract!('api/x.go', src);
expect(nodes).toHaveLength(0);
});
it('defaults method to ANY when method: is omitted', () => {
const src = `type PingReq struct {
g.Meta \`path:"/ping"\`
}`;
const { nodes } = goframeResolver.extract!('api/ping.go', src);
expect(nodes[0].name).toBe('ANY /ping');
});
it('extracts every request struct in a multi-route api file', () => {
const src = `type DeptListReq struct { g.Meta \`path:"/dept/list" method:"get"\` }
type DeptListRes struct { g.Meta \`mime:"application/json"\` }
type DeptAddReq struct { g.Meta \`path:"/dept/add" method:"post"\` }
type DeptAddRes struct {}
`;
const { nodes } = goframeResolver.extract!('api/dept.go', src);
expect(nodes.map((n) => n.name).sort()).toEqual(['GET /dept/list', 'POST /dept/add']);
});
it('returns nothing for a non-go file or a file without g.Meta', () => {
expect(goframeResolver.extract!('main.ts', 'const x = 1').nodes).toHaveLength(0);
expect(goframeResolver.extract!('main.go', 'package main\nfunc main() {}\n').nodes).toHaveLength(0);
});
});
import { rustResolver } from '../src/resolution/frameworks/rust';
describe('rustResolver.extract', () => {