Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions __tests__/frameworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
44 changes: 44 additions & 0 deletions src/resolution/frameworks/go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
},
};
Expand Down