| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- /**
- * Go Framework Resolver
- *
- * Handles Gin, Echo, Fiber, Chi, and standard library patterns.
- */
- import { Node } from '../../types';
- import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
- import { stripCommentsForRegex } from '../strip-comments';
- export const goResolver: FrameworkResolver = {
- name: 'go',
- languages: ['go'],
- detect(context: ResolutionContext): boolean {
- // Check for go.mod file (Go modules)
- const goMod = context.readFile('go.mod');
- if (goMod) {
- return true;
- }
- // Check for .go files
- const allFiles = context.getAllFiles();
- return allFiles.some((f) => f.endsWith('.go'));
- },
- resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
- // Pattern 1: Handler references
- if (ref.referenceName.endsWith('Handler') || ref.referenceName.startsWith('Handle')) {
- const result = resolveByNameAndKind(ref.referenceName, 'function', HANDLER_DIRS, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.8,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 2: Service/Repository references
- if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository') || ref.referenceName.endsWith('Store')) {
- const result = resolveByNameAndKind(ref.referenceName, null, SERVICE_DIRS, context, SERVICE_KINDS);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.8,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 3: Middleware references
- if (ref.referenceName.endsWith('Middleware') || ref.referenceName.startsWith('Auth') || ref.referenceName.startsWith('Log')) {
- const result = resolveByNameAndKind(ref.referenceName, 'function', MIDDLEWARE_DIRS, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.75,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 4: Model/Entity references (typically PascalCase structs)
- if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
- const result = resolveByNameAndKind(ref.referenceName, 'struct', MODEL_DIRS, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.7,
- resolvedBy: 'framework',
- };
- }
- }
- return null;
- },
- extract(filePath, content) {
- if (!filePath.endsWith('.go')) return { nodes: [], references: [] };
- const nodes: Node[] = [];
- const references: UnresolvedRef[] = [];
- const now = Date.now();
- const safe = stripCommentsForRegex(content, 'go');
- // (router|r|mux|app).METHOD("/path", handler)
- // Handles Gin (GET/POST/...), Chi (Get/Post/...), net/http (HandleFunc/Handle).
- const routeRegex = /\b(?:router|r|mux|app|e)\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Get|Post|Put|Patch|Delete|Handle|HandleFunc)\s*\(\s*"([^"]+)"\s*,\s*([^)]+)\)/g;
- let match: RegExpExecArray | null;
- while ((match = routeRegex.exec(safe)) !== null) {
- const [, rawMethod, routePath, handlerExpr] = match;
- const line = safe.slice(0, match.index).split('\n').length;
- const method =
- rawMethod === 'Handle' || rawMethod === 'HandleFunc'
- ? 'ANY'
- : rawMethod!.toUpperCase();
- 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);
- const handlerName = extractGoTailIdent(handlerExpr!);
- if (handlerName) {
- references.push({
- fromNodeId: routeNode.id,
- referenceName: handlerName,
- referenceKind: 'references',
- line,
- column: 0,
- filePath,
- language: 'go',
- });
- }
- }
- return { nodes, references };
- },
- };
- /** Extract the last identifier from an expression like `pkg.Sub.handler` or `handler`. */
- function extractGoTailIdent(expr: string): string | null {
- const cleaned = expr.trim().replace(/\s+/g, '').replace(/\(\)$/, '');
- const m = cleaned.match(/(?:\.|^)([A-Za-z_][A-Za-z0-9_]*)$/);
- return m ? m[1]! : null;
- }
- // Directory patterns for framework resolution
- const HANDLER_DIRS = ['handler', 'handlers', 'api', 'routes', 'controller', 'controllers'];
- const SERVICE_DIRS = ['service', 'services', 'repository', 'store', 'pkg'];
- const MIDDLEWARE_DIRS = ['middleware', 'middlewares'];
- const MODEL_DIRS = ['model', 'models', 'entity', 'entities', 'domain', 'pkg'];
- const SERVICE_KINDS = new Set(['struct', 'interface']);
- /**
- * Resolve a symbol by name using indexed queries instead of scanning all files.
- * Uses getNodesByName (O(log n) indexed lookup) instead of iterating every file.
- */
- function resolveByNameAndKind(
- name: string,
- kind: string | null,
- preferredDirs: string[],
- context: ResolutionContext,
- kinds?: Set<string>
- ): string | null {
- const candidates = context.getNodesByName(name);
- if (candidates.length === 0) return null;
- // Filter by kind
- const kindFiltered = candidates.filter((n) => {
- if (kinds) return kinds.has(n.kind);
- if (kind) return n.kind === kind;
- return true;
- });
- if (kindFiltered.length === 0) return null;
- // Prefer candidates in framework-conventional directories
- const preferred = kindFiltered.filter((n) =>
- preferredDirs.some((d) => n.filePath.includes(`/${d}/`))
- );
- if (preferred.length > 0) return preferred[0]!.id;
- // Fall back to any match
- return kindFiltered[0]!.id;
- }
|