laravel.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /**
  2. * Laravel Framework Resolver
  3. *
  4. * Handles Laravel-specific patterns for reference resolution.
  5. */
  6. import { Node } from '../../types';
  7. import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
  8. import { stripCommentsForRegex } from '../strip-comments';
  9. /**
  10. * Laravel facade mappings to underlying classes
  11. * Exported for potential use in facade resolution
  12. */
  13. export const FACADE_MAPPINGS: Record<string, string> = {
  14. Auth: 'Illuminate\\Auth\\AuthManager',
  15. Cache: 'Illuminate\\Cache\\CacheManager',
  16. Config: 'Illuminate\\Config\\Repository',
  17. DB: 'Illuminate\\Database\\DatabaseManager',
  18. Event: 'Illuminate\\Events\\Dispatcher',
  19. File: 'Illuminate\\Filesystem\\Filesystem',
  20. Gate: 'Illuminate\\Auth\\Access\\Gate',
  21. Hash: 'Illuminate\\Hashing\\HashManager',
  22. Log: 'Illuminate\\Log\\LogManager',
  23. Mail: 'Illuminate\\Mail\\Mailer',
  24. Queue: 'Illuminate\\Queue\\QueueManager',
  25. Redis: 'Illuminate\\Redis\\RedisManager',
  26. Request: 'Illuminate\\Http\\Request',
  27. Response: 'Illuminate\\Http\\Response',
  28. Route: 'Illuminate\\Routing\\Router',
  29. Session: 'Illuminate\\Session\\SessionManager',
  30. Storage: 'Illuminate\\Filesystem\\FilesystemManager',
  31. URL: 'Illuminate\\Routing\\UrlGenerator',
  32. Validator: 'Illuminate\\Validation\\Factory',
  33. View: 'Illuminate\\View\\Factory',
  34. };
  35. export const laravelResolver: FrameworkResolver = {
  36. name: 'laravel',
  37. languages: ['php'],
  38. detect(context: ResolutionContext): boolean {
  39. // Check for artisan file (Laravel signature)
  40. return context.fileExists('artisan') || context.fileExists('app/Http/Kernel.php');
  41. },
  42. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  43. // Pattern 1: Model::method() - Eloquent static calls
  44. const modelMatch = ref.referenceName.match(/^([A-Z][a-zA-Z]+)::(\w+)$/);
  45. if (modelMatch) {
  46. const [, className, methodName] = modelMatch;
  47. const result = resolveModelCall(className!, methodName!, 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: Facade calls - Auth::user(), Cache::get()
  58. const facadeMatch = ref.referenceName.match(/^(Auth|Cache|DB|Log|Mail|Queue|Session|Storage|Validator|Route|Request|Response)::(\w+)$/);
  59. if (facadeMatch) {
  60. // Facades typically resolve to external Laravel code
  61. // Mark as external but note the facade
  62. return null; // External, can't resolve to local node
  63. }
  64. // Pattern 3: Helper function calls - route(), view(), config()
  65. if (['route', 'view', 'config', 'env', 'app', 'abort', 'redirect', 'response', 'request', 'session', 'url', 'asset', 'mix'].includes(ref.referenceName)) {
  66. // These are Laravel helpers - external
  67. return null;
  68. }
  69. // Pattern 4: Controller method references
  70. const controllerMatch = ref.referenceName.match(/^([A-Z][a-zA-Z]+Controller)@(\w+)$/);
  71. if (controllerMatch) {
  72. const [, controller, method] = controllerMatch;
  73. const result = resolveControllerMethod(controller!, method!, context);
  74. if (result) {
  75. return {
  76. original: ref,
  77. targetNodeId: result,
  78. confidence: 0.9,
  79. resolvedBy: 'framework',
  80. };
  81. }
  82. }
  83. return null;
  84. },
  85. extract(filePath, content) {
  86. if (!filePath.endsWith('.php')) return { nodes: [], references: [] };
  87. const nodes: Node[] = [];
  88. const references: UnresolvedRef[] = [];
  89. const now = Date.now();
  90. const safe = stripCommentsForRegex(content, 'php');
  91. // Route::METHOD('/path', handler-expr)
  92. // handler-expr can be: [Class::class, 'method'] | 'Controller@method' | Closure | Class::class
  93. const routeRegex = /Route::(get|post|put|patch|delete|options|any)\s*\(\s*['"]([^'"]+)['"]\s*,\s*([^)]+)\)/g;
  94. let match: RegExpExecArray | null;
  95. while ((match = routeRegex.exec(safe)) !== null) {
  96. const [, method, routePath, handlerExpr] = match;
  97. const line = safe.slice(0, match.index).split('\n').length;
  98. const upper = method!.toUpperCase();
  99. const routeNode: Node = {
  100. id: `route:${filePath}:${line}:${upper}:${routePath}`,
  101. kind: 'route',
  102. name: `${upper} ${routePath}`,
  103. qualifiedName: `${filePath}::route:${routePath}`,
  104. filePath,
  105. startLine: line,
  106. endLine: line,
  107. startColumn: 0,
  108. endColumn: match[0].length,
  109. language: 'php',
  110. updatedAt: now,
  111. };
  112. nodes.push(routeNode);
  113. const handlerName = extractLaravelHandler(handlerExpr!);
  114. if (handlerName) {
  115. references.push({
  116. fromNodeId: routeNode.id,
  117. referenceName: handlerName,
  118. referenceKind: 'references',
  119. line,
  120. column: 0,
  121. filePath,
  122. language: 'php',
  123. });
  124. }
  125. }
  126. // Route::resource('name', Controller::class) / Route::apiResource('name', Controller::class)
  127. const resourceRegex = /Route::(resource|apiResource)\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*([^)]+))?\)/g;
  128. while ((match = resourceRegex.exec(safe)) !== null) {
  129. const [, _fn, resourceName, handlerExpr] = match;
  130. const line = safe.slice(0, match.index).split('\n').length;
  131. const routeNode: Node = {
  132. id: `route:${filePath}:${line}:RESOURCE:${resourceName}`,
  133. kind: 'route',
  134. name: `resource:${resourceName}`,
  135. qualifiedName: `${filePath}::route:${resourceName}`,
  136. filePath,
  137. startLine: line,
  138. endLine: line,
  139. startColumn: 0,
  140. endColumn: match[0].length,
  141. language: 'php',
  142. updatedAt: now,
  143. };
  144. nodes.push(routeNode);
  145. if (handlerExpr) {
  146. const controllerName = extractLaravelHandler(handlerExpr);
  147. if (controllerName) {
  148. references.push({
  149. fromNodeId: routeNode.id,
  150. referenceName: controllerName,
  151. referenceKind: 'imports',
  152. line,
  153. column: 0,
  154. filePath,
  155. language: 'php',
  156. });
  157. }
  158. }
  159. }
  160. return { nodes, references };
  161. },
  162. };
  163. /**
  164. * Parse a Laravel route handler expression and return the symbol to link.
  165. * - `[Class::class, 'method']` -> `method`
  166. * - `'Controller@method'` -> `method`
  167. * - `Class::class` -> `Class`
  168. * - anything else (closure etc) -> null
  169. */
  170. function extractLaravelHandler(expr: string): string | null {
  171. const trimmed = expr.trim();
  172. // [Class::class, 'method'] — grab the string literal
  173. const tupleMatch = trimmed.match(/^\[\s*[^,]+,\s*['"]([^'"]+)['"]\s*\]/);
  174. if (tupleMatch) return tupleMatch[1]!;
  175. // 'Controller@method'
  176. const atMatch = trimmed.match(/^['"]([^'"@]+)@([^'"]+)['"]$/);
  177. if (atMatch) return atMatch[2]!;
  178. // Controller::class
  179. const classMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)::class/);
  180. if (classMatch) return classMatch[1]!;
  181. return null;
  182. }
  183. /**
  184. * Resolve a Model::method() call
  185. */
  186. function resolveModelCall(
  187. className: string,
  188. methodName: string,
  189. context: ResolutionContext
  190. ): string | null {
  191. // Try app/Models/ first (Laravel 8+)
  192. let modelPath = `app/Models/${className}.php`;
  193. if (context.fileExists(modelPath)) {
  194. const nodes = context.getNodesInFile(modelPath);
  195. // Look for the method in this class
  196. const methodNode = nodes.find(
  197. (n) => n.kind === 'method' && n.name === methodName
  198. );
  199. if (methodNode) {
  200. return methodNode.id;
  201. }
  202. // Return the class itself if method not found
  203. const classNode = nodes.find(
  204. (n) => n.kind === 'class' && n.name === className
  205. );
  206. if (classNode) {
  207. return classNode.id;
  208. }
  209. }
  210. // Try app/ (Laravel 7 and below)
  211. modelPath = `app/${className}.php`;
  212. if (context.fileExists(modelPath)) {
  213. const nodes = context.getNodesInFile(modelPath);
  214. const methodNode = nodes.find(
  215. (n) => n.kind === 'method' && n.name === methodName
  216. );
  217. if (methodNode) {
  218. return methodNode.id;
  219. }
  220. const classNode = nodes.find(
  221. (n) => n.kind === 'class' && n.name === className
  222. );
  223. if (classNode) {
  224. return classNode.id;
  225. }
  226. }
  227. return null;
  228. }
  229. /**
  230. * Resolve a Controller@method reference
  231. */
  232. function resolveControllerMethod(
  233. controller: string,
  234. method: string,
  235. context: ResolutionContext
  236. ): string | null {
  237. // Try app/Http/Controllers/
  238. const controllerPath = `app/Http/Controllers/${controller}.php`;
  239. if (context.fileExists(controllerPath)) {
  240. const nodes = context.getNodesInFile(controllerPath);
  241. const methodNode = nodes.find(
  242. (n) => n.kind === 'method' && n.name === method
  243. );
  244. if (methodNode) {
  245. return methodNode.id;
  246. }
  247. }
  248. // Try name-based lookup for namespaced controllers
  249. const controllerCandidates = context.getNodesByName(controller);
  250. for (const ctrl of controllerCandidates) {
  251. if (ctrl.kind === 'class' && ctrl.filePath.includes('Controllers')) {
  252. const nodesInFile = context.getNodesInFile(ctrl.filePath);
  253. const methodNode = nodesInFile.find(
  254. (n) => n.kind === 'method' && n.name === method
  255. );
  256. if (methodNode) {
  257. return methodNode.id;
  258. }
  259. }
  260. }
  261. return null;
  262. }