| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- /**
- * Ruby Framework Resolver
- *
- * Handles Ruby on Rails patterns.
- */
- import { Node } from '../../types';
- import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
- export const railsResolver: FrameworkResolver = {
- name: 'rails',
- detect(context: ResolutionContext): boolean {
- // Check for Gemfile with rails
- const gemfile = context.readFile('Gemfile');
- if (gemfile && gemfile.includes("'rails'")) {
- return true;
- }
- // Check for config/application.rb (Rails signature)
- if (context.fileExists('config/application.rb')) {
- return true;
- }
- // Check for typical Rails directory structure
- return (
- context.fileExists('app/controllers/application_controller.rb') ||
- context.fileExists('config/routes.rb')
- );
- },
- resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
- // Pattern 1: Model references (ActiveRecord)
- if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
- const result = resolveModel(ref.referenceName, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.8,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 2: Controller references
- if (ref.referenceName.endsWith('Controller')) {
- const result = resolveController(ref.referenceName, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.85,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 3: Helper references
- if (ref.referenceName.endsWith('Helper')) {
- const result = resolveHelper(ref.referenceName, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.8,
- resolvedBy: 'framework',
- };
- }
- }
- // Pattern 4: Service/Job references
- if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Job')) {
- const result = resolveService(ref.referenceName, context);
- if (result) {
- return {
- original: ref,
- targetNodeId: result,
- confidence: 0.8,
- resolvedBy: 'framework',
- };
- }
- }
- return null;
- },
- extractNodes(filePath: string, content: string): Node[] {
- const nodes: Node[] = [];
- const now = Date.now();
- // Extract route definitions from config/routes.rb
- if (filePath.includes('routes.rb')) {
- // get/post/put/patch/delete 'path'
- const routePatterns = [
- /(get|post|put|patch|delete)\s+['"]([^'"]+)['"]/g,
- /resources?\s+:(\w+)/g,
- /root\s+['"]([^'"]+)['"]/g,
- /root\s+to:\s*['"]([^'"]+)['"]/g,
- ];
- for (const pattern of routePatterns) {
- let match;
- while ((match = pattern.exec(content)) !== null) {
- const line = content.slice(0, match.index).split('\n').length;
- if (pattern.source.includes('resources')) {
- const [, resourceName] = match;
- nodes.push({
- id: `route:${filePath}:resource:${resourceName}:${line}`,
- kind: 'route',
- name: `resource:${resourceName}`,
- qualifiedName: `${filePath}::resource:${resourceName}`,
- filePath,
- startLine: line,
- endLine: line,
- startColumn: 0,
- endColumn: match[0].length,
- language: 'ruby',
- updatedAt: now,
- });
- } else if (pattern.source.includes('root')) {
- const [, target] = match;
- nodes.push({
- id: `route:${filePath}:root:${line}`,
- kind: 'route',
- name: `/ -> ${target}`,
- qualifiedName: `${filePath}::root`,
- filePath,
- startLine: line,
- endLine: line,
- startColumn: 0,
- endColumn: match[0].length,
- language: 'ruby',
- updatedAt: now,
- });
- } else {
- const [, method, path] = match;
- nodes.push({
- id: `route:${filePath}:${method!.toUpperCase()}:${path}:${line}`,
- kind: 'route',
- name: `${method!.toUpperCase()} ${path}`,
- qualifiedName: `${filePath}::${method!.toUpperCase()}:${path}`,
- filePath,
- startLine: line,
- endLine: line,
- startColumn: 0,
- endColumn: match[0].length,
- language: 'ruby',
- updatedAt: now,
- });
- }
- }
- }
- }
- // Extract controller actions
- if (filePath.includes('controllers/') && filePath.endsWith('.rb')) {
- const actionPattern = /def\s+(\w+)/g;
- let match;
- while ((match = actionPattern.exec(content)) !== null) {
- const [, actionName] = match;
- const line = content.slice(0, match.index).split('\n').length;
- // Skip private methods and common Rails callbacks
- const privateMethods = ['initialize', 'set_', 'before_', 'after_'];
- if (!privateMethods.some((p) => actionName!.startsWith(p))) {
- nodes.push({
- id: `action:${filePath}:${actionName}:${line}`,
- kind: 'method',
- name: actionName!,
- qualifiedName: `${filePath}::${actionName}`,
- filePath,
- startLine: line,
- endLine: line,
- startColumn: 0,
- endColumn: match[0].length,
- language: 'ruby',
- updatedAt: now,
- });
- }
- }
- }
- return nodes;
- },
- };
- // Helper functions
- function resolveModel(name: string, context: ResolutionContext): string | null {
- // Convert CamelCase to snake_case for file lookup
- const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
- const possiblePaths = [
- `app/models/${snakeName}.rb`,
- `app/models/concerns/${snakeName}.rb`,
- ];
- for (const modelPath of possiblePaths) {
- if (context.fileExists(modelPath)) {
- const nodes = context.getNodesInFile(modelPath);
- const modelNode = nodes.find(
- (n) => n.kind === 'class' && n.name === name
- );
- if (modelNode) {
- return modelNode.id;
- }
- }
- }
- // Search all model files
- const allFiles = context.getAllFiles();
- for (const file of allFiles) {
- if (file.includes('app/models/') && file.endsWith('.rb')) {
- const nodes = context.getNodesInFile(file);
- const modelNode = nodes.find(
- (n) => n.kind === 'class' && n.name === name
- );
- if (modelNode) {
- return modelNode.id;
- }
- }
- }
- return null;
- }
- function resolveController(name: string, context: ResolutionContext): string | null {
- // Convert CamelCase to snake_case
- const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
- const possiblePaths = [
- `app/controllers/${snakeName}.rb`,
- `app/controllers/api/${snakeName}.rb`,
- `app/controllers/api/v1/${snakeName}.rb`,
- ];
- for (const controllerPath of possiblePaths) {
- if (context.fileExists(controllerPath)) {
- const nodes = context.getNodesInFile(controllerPath);
- const controllerNode = nodes.find(
- (n) => n.kind === 'class' && n.name === name
- );
- if (controllerNode) {
- return controllerNode.id;
- }
- }
- }
- // Search all controller files
- const allFiles = context.getAllFiles();
- for (const file of allFiles) {
- if (file.includes('controllers/') && file.endsWith('.rb')) {
- const nodes = context.getNodesInFile(file);
- const controllerNode = nodes.find(
- (n) => n.kind === 'class' && n.name === name
- );
- if (controllerNode) {
- return controllerNode.id;
- }
- }
- }
- return null;
- }
- function resolveHelper(name: string, context: ResolutionContext): string | null {
- const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
- const helperPath = `app/helpers/${snakeName}.rb`;
- if (context.fileExists(helperPath)) {
- const nodes = context.getNodesInFile(helperPath);
- const helperNode = nodes.find(
- (n) => n.kind === 'module' && n.name === name
- );
- if (helperNode) {
- return helperNode.id;
- }
- }
- return null;
- }
- function resolveService(name: string, context: ResolutionContext): string | null {
- const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
- const possiblePaths = [
- `app/services/${snakeName}.rb`,
- `app/jobs/${snakeName}.rb`,
- `app/workers/${snakeName}.rb`,
- ];
- for (const servicePath of possiblePaths) {
- if (context.fileExists(servicePath)) {
- const nodes = context.getNodesInFile(servicePath);
- const serviceNode = nodes.find(
- (n) => n.kind === 'class' && n.name === name
- );
- if (serviceNode) {
- return serviceNode.id;
- }
- }
- }
- return null;
- }
|