| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- /**
- * Laravel Framework Resolver
- *
- * Handles Laravel-specific patterns for reference resolution.
- */
- import { Node } from '../../types';
- import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
- import { stripCommentsForRegex } from '../strip-comments';
- /**
- * Laravel facade mappings to underlying classes
- * Exported for potential use in facade resolution
- */
- export const FACADE_MAPPINGS: Record<string, string> = {
- Auth: 'Illuminate\\Auth\\AuthManager',
- Cache: 'Illuminate\\Cache\\CacheManager',
- Config: 'Illuminate\\Config\\Repository',
- DB: 'Illuminate\\Database\\DatabaseManager',
- Event: 'Illuminate\\Events\\Dispatcher',
- File: 'Illuminate\\Filesystem\\Filesystem',
- Gate: 'Illuminate\\Auth\\Access\\Gate',
- Hash: 'Illuminate\\Hashing\\HashManager',
- Log: 'Illuminate\\Log\\LogManager',
- Mail: 'Illuminate\\Mail\\Mailer',
- Queue: 'Illuminate\\Queue\\QueueManager',
- Redis: 'Illuminate\\Redis\\RedisManager',
- Request: 'Illuminate\\Http\\Request',
- Response: 'Illuminate\\Http\\Response',
- Route: 'Illuminate\\Routing\\Router',
- Session: 'Illuminate\\Session\\SessionManager',
- Storage: 'Illuminate\\Filesystem\\FilesystemManager',
- URL: 'Illuminate\\Routing\\UrlGenerator',
- Validator: 'Illuminate\\Validation\\Factory',
- View: 'Illuminate\\View\\Factory',
- };
- export const laravelResolver: FrameworkResolver = {
- name: 'laravel',
- languages: ['php'],
- detect(context: ResolutionContext): boolean {
- // Check for artisan file (Laravel signature)
- return context.fileExists('artisan') || context.fileExists('app/Http/Kernel.php');
- },
- resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
- // Pattern 1: Model::method() - Eloquent static calls
- const modelMatch = ref.referenceName.match(/^([A-Z][a-zA-Z]+)::(\w+)$/);
- if (modelMatch) {
- const [, className, methodName] = modelMatch;
- const result = resolveModelCall(className!, methodName!, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.85,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 2: Facade calls - Auth::user(), Cache::get()
- const facadeMatch = ref.referenceName.match(/^(Auth|Cache|DB|Log|Mail|Queue|Session|Storage|Validator|Route|Request|Response)::(\w+)$/);
- if (facadeMatch) {
- // Facades typically resolve to external Laravel code
- // Mark as external but note the facade
- return null; // External, can't resolve to local node
- }
- // Pattern 3: Helper function calls - route(), view(), config()
- if (['route', 'view', 'config', 'env', 'app', 'abort', 'redirect', 'response', 'request', 'session', 'url', 'asset', 'mix'].includes(ref.referenceName)) {
- // These are Laravel helpers - external
- return null;
- }
- // Pattern 4: Controller method references
- const controllerMatch = ref.referenceName.match(/^([A-Z][a-zA-Z]+Controller)@(\w+)$/);
- if (controllerMatch) {
- const [, controller, method] = controllerMatch;
- const result = resolveControllerMethod(controller!, method!, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.9,
- resolvedBy: 'framework',
- };
- }
- }
- return null;
- },
- extract(filePath, content) {
- if (!filePath.endsWith('.php')) return { nodes: [], references: [] };
- const nodes: Node[] = [];
- const references: UnresolvedRef[] = [];
- const now = Date.now();
- const safe = stripCommentsForRegex(content, 'php');
- // Route::METHOD('/path', handler-expr)
- // handler-expr can be: [Class::class, 'method'] | 'Controller@method' | Closure | Class::class
- const routeRegex = /Route::(get|post|put|patch|delete|options|any)\s*\(\s*['"]([^'"]+)['"]\s*,\s*([^)]+)\)/g;
- let match: RegExpExecArray | null;
- while ((match = routeRegex.exec(safe)) !== null) {
- const [, method, routePath, handlerExpr] = match;
- const line = safe.slice(0, match.index).split('\n').length;
- const upper = method!.toUpperCase();
- const routeNode: Node = {
- id: `route:${filePath}:${line}:${upper}:${routePath}`,
- kind: 'route',
- name: `${upper} ${routePath}`,
- qualifiedName: `${filePath}::route:${routePath}`,
- filePath,
- startLine: line,
- endLine: line,
- startColumn: 0,
- endColumn: match[0].length,
- language: 'php',
- updatedAt: now,
- };
- nodes.push(routeNode);
- const handlerName = extractLaravelHandler(handlerExpr!);
- if (handlerName) {
- references.push({
- fromNodeId: routeNode.id,
- referenceName: handlerName,
- referenceKind: 'references',
- line,
- column: 0,
- filePath,
- language: 'php',
- });
- }
- }
- // Route::resource('name', Controller::class) / Route::apiResource('name', Controller::class)
- const resourceRegex = /Route::(resource|apiResource)\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*([^)]+))?\)/g;
- while ((match = resourceRegex.exec(safe)) !== null) {
- const [, _fn, resourceName, handlerExpr] = match;
- const line = safe.slice(0, match.index).split('\n').length;
- const routeNode: Node = {
- id: `route:${filePath}:${line}:RESOURCE:${resourceName}`,
- kind: 'route',
- name: `resource:${resourceName}`,
- qualifiedName: `${filePath}::route:${resourceName}`,
- filePath,
- startLine: line,
- endLine: line,
- startColumn: 0,
- endColumn: match[0].length,
- language: 'php',
- updatedAt: now,
- };
- nodes.push(routeNode);
- if (handlerExpr) {
- const controllerName = extractLaravelHandler(handlerExpr);
- if (controllerName) {
- references.push({
- fromNodeId: routeNode.id,
- referenceName: controllerName,
- referenceKind: 'imports',
- line,
- column: 0,
- filePath,
- language: 'php',
- });
- }
- }
- }
- return { nodes, references };
- },
- };
- /**
- * Parse a Laravel route handler expression and return the symbol to link.
- * - `[Class::class, 'method']` -> `method`
- * - `'Controller@method'` -> `method`
- * - `Class::class` -> `Class`
- * - anything else (closure etc) -> null
- */
- function extractLaravelHandler(expr: string): string | null {
- const trimmed = expr.trim();
- // [Class::class, 'method'] — grab the string literal
- const tupleMatch = trimmed.match(/^\[\s*[^,]+,\s*['"]([^'"]+)['"]\s*\]/);
- if (tupleMatch) return tupleMatch[1]!;
- // 'Controller@method'
- const atMatch = trimmed.match(/^['"]([^'"@]+)@([^'"]+)['"]$/);
- if (atMatch) return atMatch[2]!;
- // Controller::class
- const classMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)::class/);
- if (classMatch) return classMatch[1]!;
- return null;
- }
- /**
- * Resolve a Model::method() call
- */
- function resolveModelCall(
- className: string,
- methodName: string,
- context: ResolutionContext
- ): string | null {
- // Try app/Models/ first (Laravel 8+)
- let modelPath = `app/Models/${className}.php`;
- if (context.fileExists(modelPath)) {
- const nodes = context.getNodesInFile(modelPath);
- // Look for the method in this class
- const methodNode = nodes.find(
- (n) => n.kind === 'method' && n.name === methodName
- );
- if (methodNode) {
- return methodNode.id;
- }
- // Return the class itself if method not found
- const classNode = nodes.find(
- (n) => n.kind === 'class' && n.name === className
- );
- if (classNode) {
- return classNode.id;
- }
- }
- // Try app/ (Laravel 7 and below)
- modelPath = `app/${className}.php`;
- if (context.fileExists(modelPath)) {
- const nodes = context.getNodesInFile(modelPath);
- const methodNode = nodes.find(
- (n) => n.kind === 'method' && n.name === methodName
- );
- if (methodNode) {
- return methodNode.id;
- }
- const classNode = nodes.find(
- (n) => n.kind === 'class' && n.name === className
- );
- if (classNode) {
- return classNode.id;
- }
- }
- return null;
- }
- /**
- * Resolve a Controller@method reference
- */
- function resolveControllerMethod(
- controller: string,
- method: string,
- context: ResolutionContext
- ): string | null {
- // Try app/Http/Controllers/
- const controllerPath = `app/Http/Controllers/${controller}.php`;
- if (context.fileExists(controllerPath)) {
- const nodes = context.getNodesInFile(controllerPath);
- const methodNode = nodes.find(
- (n) => n.kind === 'method' && n.name === method
- );
- if (methodNode) {
- return methodNode.id;
- }
- }
- // Try name-based lookup for namespaced controllers
- const controllerCandidates = context.getNodesByName(controller);
- for (const ctrl of controllerCandidates) {
- if (ctrl.kind === 'class' && ctrl.filePath.includes('Controllers')) {
- const nodesInFile = context.getNodesInFile(ctrl.filePath);
- const methodNode = nodesInFile.find(
- (n) => n.kind === 'method' && n.name === method
- );
- if (methodNode) {
- return methodNode.id;
- }
- }
- }
- return null;
- }
|