go.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /**
  2. * Go Framework Resolver
  3. *
  4. * Handles Gin, Echo, Fiber, Chi, and standard library patterns.
  5. */
  6. import { Node } from '../../types';
  7. import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
  8. import { stripCommentsForRegex } from '../strip-comments';
  9. export const goResolver: FrameworkResolver = {
  10. name: 'go',
  11. languages: ['go'],
  12. detect(context: ResolutionContext): boolean {
  13. // Check for go.mod file (Go modules)
  14. const goMod = context.readFile('go.mod');
  15. if (goMod) {
  16. return true;
  17. }
  18. // Check for .go files
  19. const allFiles = context.getAllFiles();
  20. return allFiles.some((f) => f.endsWith('.go'));
  21. },
  22. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  23. // Pattern 1: Handler references
  24. if (ref.referenceName.endsWith('Handler') || ref.referenceName.startsWith('Handle')) {
  25. const result = resolveByNameAndKind(ref.referenceName, 'function', HANDLER_DIRS, context);
  26. if (result) {
  27. return {
  28. original: ref,
  29. targetNodeId: result,
  30. confidence: 0.8,
  31. resolvedBy: 'framework',
  32. };
  33. }
  34. }
  35. // Pattern 2: Service/Repository references
  36. if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository') || ref.referenceName.endsWith('Store')) {
  37. const result = resolveByNameAndKind(ref.referenceName, null, SERVICE_DIRS, context, SERVICE_KINDS);
  38. if (result) {
  39. return {
  40. original: ref,
  41. targetNodeId: result,
  42. confidence: 0.8,
  43. resolvedBy: 'framework',
  44. };
  45. }
  46. }
  47. // Pattern 3: Middleware references
  48. if (ref.referenceName.endsWith('Middleware') || ref.referenceName.startsWith('Auth') || ref.referenceName.startsWith('Log')) {
  49. const result = resolveByNameAndKind(ref.referenceName, 'function', MIDDLEWARE_DIRS, context);
  50. if (result) {
  51. return {
  52. original: ref,
  53. targetNodeId: result,
  54. confidence: 0.75,
  55. resolvedBy: 'framework',
  56. };
  57. }
  58. }
  59. // Pattern 4: Model/Entity references (typically PascalCase structs)
  60. if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
  61. const result = resolveByNameAndKind(ref.referenceName, 'struct', MODEL_DIRS, context);
  62. if (result) {
  63. return {
  64. original: ref,
  65. targetNodeId: result,
  66. confidence: 0.7,
  67. resolvedBy: 'framework',
  68. };
  69. }
  70. }
  71. return null;
  72. },
  73. extract(filePath, content) {
  74. if (!filePath.endsWith('.go')) return { nodes: [], references: [] };
  75. const nodes: Node[] = [];
  76. const references: UnresolvedRef[] = [];
  77. const now = Date.now();
  78. const safe = stripCommentsForRegex(content, 'go');
  79. // (router|r|mux|app).METHOD("/path", handler)
  80. // Handles Gin (GET/POST/...), Chi (Get/Post/...), net/http (HandleFunc/Handle).
  81. 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;
  82. let match: RegExpExecArray | null;
  83. while ((match = routeRegex.exec(safe)) !== null) {
  84. const [, rawMethod, routePath, handlerExpr] = match;
  85. const line = safe.slice(0, match.index).split('\n').length;
  86. const method =
  87. rawMethod === 'Handle' || rawMethod === 'HandleFunc'
  88. ? 'ANY'
  89. : rawMethod!.toUpperCase();
  90. const routeNode: Node = {
  91. id: `route:${filePath}:${line}:${method}:${routePath}`,
  92. kind: 'route',
  93. name: `${method} ${routePath}`,
  94. qualifiedName: `${filePath}::route:${routePath}`,
  95. filePath,
  96. startLine: line,
  97. endLine: line,
  98. startColumn: 0,
  99. endColumn: match[0].length,
  100. language: 'go',
  101. updatedAt: now,
  102. };
  103. nodes.push(routeNode);
  104. const handlerName = extractGoTailIdent(handlerExpr!);
  105. if (handlerName) {
  106. references.push({
  107. fromNodeId: routeNode.id,
  108. referenceName: handlerName,
  109. referenceKind: 'references',
  110. line,
  111. column: 0,
  112. filePath,
  113. language: 'go',
  114. });
  115. }
  116. }
  117. return { nodes, references };
  118. },
  119. };
  120. /** Extract the last identifier from an expression like `pkg.Sub.handler` or `handler`. */
  121. function extractGoTailIdent(expr: string): string | null {
  122. const cleaned = expr.trim().replace(/\s+/g, '').replace(/\(\)$/, '');
  123. const m = cleaned.match(/(?:\.|^)([A-Za-z_][A-Za-z0-9_]*)$/);
  124. return m ? m[1]! : null;
  125. }
  126. // Directory patterns for framework resolution
  127. const HANDLER_DIRS = ['handler', 'handlers', 'api', 'routes', 'controller', 'controllers'];
  128. const SERVICE_DIRS = ['service', 'services', 'repository', 'store', 'pkg'];
  129. const MIDDLEWARE_DIRS = ['middleware', 'middlewares'];
  130. const MODEL_DIRS = ['model', 'models', 'entity', 'entities', 'domain', 'pkg'];
  131. const SERVICE_KINDS = new Set(['struct', 'interface']);
  132. /**
  133. * Resolve a symbol by name using indexed queries instead of scanning all files.
  134. * Uses getNodesByName (O(log n) indexed lookup) instead of iterating every file.
  135. */
  136. function resolveByNameAndKind(
  137. name: string,
  138. kind: string | null,
  139. preferredDirs: string[],
  140. context: ResolutionContext,
  141. kinds?: Set<string>
  142. ): string | null {
  143. const candidates = context.getNodesByName(name);
  144. if (candidates.length === 0) return null;
  145. // Filter by kind
  146. const kindFiltered = candidates.filter((n) => {
  147. if (kinds) return kinds.has(n.kind);
  148. if (kind) return n.kind === kind;
  149. return true;
  150. });
  151. if (kindFiltered.length === 0) return null;
  152. // Prefer candidates in framework-conventional directories
  153. const preferred = kindFiltered.filter((n) =>
  154. preferredDirs.some((d) => n.filePath.includes(`/${d}/`))
  155. );
  156. if (preferred.length > 0) return preferred[0]!.id;
  157. // Fall back to any match
  158. return kindFiltered[0]!.id;
  159. }