java.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /**
  2. * Java Framework Resolver
  3. *
  4. * Handles Spring Boot and general Java patterns.
  5. */
  6. import { Node } from '../../types';
  7. import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
  8. import { stripCommentsForRegex } from '../strip-comments';
  9. export const springResolver: FrameworkResolver = {
  10. name: 'spring',
  11. languages: ['java'],
  12. detect(context: ResolutionContext): boolean {
  13. // Check for pom.xml with Spring
  14. const pomXml = context.readFile('pom.xml');
  15. if (pomXml && (pomXml.includes('spring-boot') || pomXml.includes('springframework'))) {
  16. return true;
  17. }
  18. // Check for build.gradle with Spring
  19. const buildGradle = context.readFile('build.gradle');
  20. if (buildGradle && (buildGradle.includes('spring-boot') || buildGradle.includes('springframework'))) {
  21. return true;
  22. }
  23. const buildGradleKts = context.readFile('build.gradle.kts');
  24. if (buildGradleKts && (buildGradleKts.includes('spring-boot') || buildGradleKts.includes('springframework'))) {
  25. return true;
  26. }
  27. // Check for Spring annotations in Java files
  28. const allFiles = context.getAllFiles();
  29. for (const file of allFiles) {
  30. if (file.endsWith('.java')) {
  31. const content = context.readFile(file);
  32. if (content && (
  33. content.includes('@SpringBootApplication') ||
  34. content.includes('@RestController') ||
  35. content.includes('@Service') ||
  36. content.includes('@Repository')
  37. )) {
  38. return true;
  39. }
  40. }
  41. }
  42. return false;
  43. },
  44. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  45. // Pattern 1: Service references (dependency injection)
  46. if (ref.referenceName.endsWith('Service')) {
  47. const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
  48. if (result) {
  49. return {
  50. original: ref,
  51. targetNodeId: result,
  52. confidence: 0.85,
  53. resolvedBy: 'framework',
  54. };
  55. }
  56. }
  57. // Pattern 2: Repository references
  58. if (ref.referenceName.endsWith('Repository')) {
  59. const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, REPO_DIRS, context);
  60. if (result) {
  61. return {
  62. original: ref,
  63. targetNodeId: result,
  64. confidence: 0.85,
  65. resolvedBy: 'framework',
  66. };
  67. }
  68. }
  69. // Pattern 3: Controller references
  70. if (ref.referenceName.endsWith('Controller')) {
  71. const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, CONTROLLER_DIRS, context);
  72. if (result) {
  73. return {
  74. original: ref,
  75. targetNodeId: result,
  76. confidence: 0.85,
  77. resolvedBy: 'framework',
  78. };
  79. }
  80. }
  81. // Pattern 4: Entity/Model references
  82. if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
  83. const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, ENTITY_DIRS, context);
  84. if (result) {
  85. return {
  86. original: ref,
  87. targetNodeId: result,
  88. confidence: 0.7,
  89. resolvedBy: 'framework',
  90. };
  91. }
  92. }
  93. // Pattern 5: Component references
  94. if (ref.referenceName.endsWith('Component') || ref.referenceName.endsWith('Config')) {
  95. const result = resolveByNameAndKind(ref.referenceName, CLASS_KINDS, COMPONENT_DIRS, context);
  96. if (result) {
  97. return {
  98. original: ref,
  99. targetNodeId: result,
  100. confidence: 0.8,
  101. resolvedBy: 'framework',
  102. };
  103. }
  104. }
  105. return null;
  106. },
  107. extract(filePath, content) {
  108. if (!filePath.endsWith('.java')) return { nodes: [], references: [] };
  109. const nodes: Node[] = [];
  110. const references: UnresolvedRef[] = [];
  111. const now = Date.now();
  112. const safe = stripCommentsForRegex(content, 'java');
  113. // Class-level @RequestMapping prefix (an @RequestMapping whose tail leads to a
  114. // `class`). Joined onto each method's path — and, crucially, NOT treated as a
  115. // route itself (the old regex did, creating one bogus class route and missing
  116. // every BARE method mapping like `@PostMapping` with the path on the class).
  117. let classPrefix = '';
  118. const cls = /@RequestMapping\s*\(([^)]*)\)\s*(?:@[\w.]+(?:\([^)]*\))?\s*)*(?:public\s+|final\s+|abstract\s+)*class\b/.exec(safe);
  119. if (cls) classPrefix = parseMappingPath(cls[1]!);
  120. const VERB: Record<string, string> = {
  121. GetMapping: 'GET', PostMapping: 'POST', PutMapping: 'PUT', PatchMapping: 'PATCH', DeleteMapping: 'DELETE',
  122. };
  123. // Verb-specific method mappings — always method-level, BARE or with a path.
  124. const mappingRegex = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping)\b\s*(\([^)]*\))?/g;
  125. let match: RegExpExecArray | null;
  126. while ((match = mappingRegex.exec(safe)) !== null) {
  127. const method = VERB[match[1]!]!;
  128. const sub = parseMappingPath((match[2] || '').replace(/^\(|\)$/g, ''));
  129. const routePath = joinPath(classPrefix, sub);
  130. const line = safe.slice(0, match.index).split('\n').length;
  131. const routeNode: Node = {
  132. id: `route:${filePath}:${line}:${method}:${routePath}`,
  133. kind: 'route',
  134. name: `${method} ${routePath}`,
  135. qualifiedName: `${filePath}::route:${routePath}`,
  136. filePath,
  137. startLine: line,
  138. endLine: line,
  139. startColumn: 0,
  140. endColumn: match[0].length,
  141. language: 'java',
  142. updatedAt: now,
  143. };
  144. nodes.push(routeNode);
  145. // Method it decorates: first declared method after (skip stacked annotations;
  146. // Java puts the return type before the name). Bounded so we don't grab a far one.
  147. const tail = safe.slice(match.index + match[0].length, match.index + match[0].length + 600);
  148. const methodMatch = tail.match(/\b(?:public|private|protected)\s+[^;{=]*?\s+(\w+)\s*\(/);
  149. if (methodMatch) {
  150. references.push({
  151. fromNodeId: routeNode.id,
  152. referenceName: methodMatch[1]!,
  153. referenceKind: 'references',
  154. line,
  155. column: 0,
  156. filePath,
  157. language: 'java',
  158. });
  159. }
  160. }
  161. // Method-level @RequestMapping (older style: `@RequestMapping(value="/x",
  162. // method=RequestMethod.GET)` on a method). The class-level @RequestMapping is
  163. // the prefix (handled above) — skip it here so it isn't double-counted.
  164. const reqRe = /@RequestMapping\b\s*(\([^)]*\))?/g;
  165. while ((match = reqRe.exec(safe)) !== null) {
  166. const args = (match[1] || '').replace(/^\(|\)$/g, '');
  167. const after = safe.slice(match.index + match[0].length, match.index + match[0].length + 600);
  168. if (/^\s*(?:@[\w.]+(?:\([^)]*\))?\s*)*(?:public\s+|final\s+|abstract\s+)*class\b/.test(after)) continue; // class-level prefix
  169. const methodMatch = after.match(/\b(?:public|private|protected)\s+[^;{=]*?\s+(\w+)\s*\(/);
  170. if (!methodMatch) continue;
  171. const verbM = args.match(/method\s*=\s*(?:RequestMethod\.)?(\w+)/);
  172. const method = verbM ? verbM[1]!.toUpperCase() : 'ANY';
  173. const routePath = joinPath(classPrefix, parseMappingPath(args));
  174. const line = safe.slice(0, match.index).split('\n').length;
  175. const routeNode: Node = {
  176. id: `route:${filePath}:${line}:${method}:${routePath}`,
  177. kind: 'route',
  178. name: `${method} ${routePath}`,
  179. qualifiedName: `${filePath}::route:${routePath}`,
  180. filePath, startLine: line, endLine: line, startColumn: 0, endColumn: match[0].length, language: 'java', updatedAt: now,
  181. };
  182. nodes.push(routeNode);
  183. references.push({
  184. fromNodeId: routeNode.id,
  185. referenceName: methodMatch[1]!,
  186. referenceKind: 'references',
  187. line, column: 0, filePath, language: 'java',
  188. });
  189. }
  190. return { nodes, references };
  191. },
  192. };
  193. // Directory patterns
  194. const SERVICE_DIRS = ['/service/', '/services/'];
  195. const REPO_DIRS = ['/repository/', '/repositories/'];
  196. const CONTROLLER_DIRS = ['/controller/', '/controllers/'];
  197. const ENTITY_DIRS = ['/entity/', '/entities/', '/model/', '/models/', '/domain/'];
  198. const COMPONENT_DIRS = ['/component/', '/components/', '/config/'];
  199. const CLASS_KINDS = new Set(['class']);
  200. const SERVICE_KINDS = new Set(['class', 'interface']);
  201. /** Path string from a mapping's args (`"/x"`, `value = "/x"`, `path = "/x"`); '' if bare. */
  202. function parseMappingPath(args: string): string {
  203. const m = args.match(/["']([^"']*)["']/);
  204. return m ? m[1]! : '';
  205. }
  206. /** Join a class-level prefix and a method sub-path into one normalized `/path`. */
  207. function joinPath(prefix: string, sub: string): string {
  208. const parts = [prefix, sub].map((p) => p.replace(/^\/+|\/+$/g, '')).filter(Boolean);
  209. return '/' + parts.join('/');
  210. }
  211. /**
  212. * Resolve a symbol by name using indexed queries instead of scanning all files.
  213. */
  214. function resolveByNameAndKind(
  215. name: string,
  216. kinds: Set<string>,
  217. preferredDirPatterns: string[],
  218. context: ResolutionContext,
  219. ): string | null {
  220. const candidates = context.getNodesByName(name);
  221. if (candidates.length === 0) return null;
  222. const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
  223. if (kindFiltered.length === 0) return null;
  224. // Prefer candidates in framework-conventional directories
  225. const preferred = kindFiltered.filter((n) =>
  226. preferredDirPatterns.some((d) => n.filePath.includes(d))
  227. );
  228. if (preferred.length > 0) return preferred[0]!.id;
  229. // Fall back to any match
  230. return kindFiltered[0]!.id;
  231. }