ruby.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /**
  2. * Ruby Framework Resolver
  3. *
  4. * Handles Ruby on Rails patterns.
  5. */
  6. import { Node } from '../../types';
  7. import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
  8. export const railsResolver: FrameworkResolver = {
  9. name: 'rails',
  10. detect(context: ResolutionContext): boolean {
  11. // Check for Gemfile with rails
  12. const gemfile = context.readFile('Gemfile');
  13. if (gemfile && gemfile.includes("'rails'")) {
  14. return true;
  15. }
  16. // Check for config/application.rb (Rails signature)
  17. if (context.fileExists('config/application.rb')) {
  18. return true;
  19. }
  20. // Check for typical Rails directory structure
  21. return (
  22. context.fileExists('app/controllers/application_controller.rb') ||
  23. context.fileExists('config/routes.rb')
  24. );
  25. },
  26. resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  27. // Pattern 1: Model references (ActiveRecord)
  28. if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
  29. const result = resolveModel(ref.referenceName, context);
  30. if (result) {
  31. return {
  32. original: ref,
  33. targetNodeId: result,
  34. confidence: 0.8,
  35. resolvedBy: 'framework',
  36. };
  37. }
  38. }
  39. // Pattern 2: Controller references
  40. if (ref.referenceName.endsWith('Controller')) {
  41. const result = resolveController(ref.referenceName, context);
  42. if (result) {
  43. return {
  44. original: ref,
  45. targetNodeId: result,
  46. confidence: 0.85,
  47. resolvedBy: 'framework',
  48. };
  49. }
  50. }
  51. // Pattern 3: Helper references
  52. if (ref.referenceName.endsWith('Helper')) {
  53. const result = resolveHelper(ref.referenceName, context);
  54. if (result) {
  55. return {
  56. original: ref,
  57. targetNodeId: result,
  58. confidence: 0.8,
  59. resolvedBy: 'framework',
  60. };
  61. }
  62. }
  63. // Pattern 4: Service/Job references
  64. if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Job')) {
  65. const result = resolveService(ref.referenceName, context);
  66. if (result) {
  67. return {
  68. original: ref,
  69. targetNodeId: result,
  70. confidence: 0.8,
  71. resolvedBy: 'framework',
  72. };
  73. }
  74. }
  75. return null;
  76. },
  77. extractNodes(filePath: string, content: string): Node[] {
  78. const nodes: Node[] = [];
  79. const now = Date.now();
  80. // Extract route definitions from config/routes.rb
  81. if (filePath.includes('routes.rb')) {
  82. // get/post/put/patch/delete 'path'
  83. const routePatterns = [
  84. /(get|post|put|patch|delete)\s+['"]([^'"]+)['"]/g,
  85. /resources?\s+:(\w+)/g,
  86. /root\s+['"]([^'"]+)['"]/g,
  87. /root\s+to:\s*['"]([^'"]+)['"]/g,
  88. ];
  89. for (const pattern of routePatterns) {
  90. let match;
  91. while ((match = pattern.exec(content)) !== null) {
  92. const line = content.slice(0, match.index).split('\n').length;
  93. if (pattern.source.includes('resources')) {
  94. const [, resourceName] = match;
  95. nodes.push({
  96. id: `route:${filePath}:resource:${resourceName}:${line}`,
  97. kind: 'route',
  98. name: `resource:${resourceName}`,
  99. qualifiedName: `${filePath}::resource:${resourceName}`,
  100. filePath,
  101. startLine: line,
  102. endLine: line,
  103. startColumn: 0,
  104. endColumn: match[0].length,
  105. language: 'ruby',
  106. updatedAt: now,
  107. });
  108. } else if (pattern.source.includes('root')) {
  109. const [, target] = match;
  110. nodes.push({
  111. id: `route:${filePath}:root:${line}`,
  112. kind: 'route',
  113. name: `/ -> ${target}`,
  114. qualifiedName: `${filePath}::root`,
  115. filePath,
  116. startLine: line,
  117. endLine: line,
  118. startColumn: 0,
  119. endColumn: match[0].length,
  120. language: 'ruby',
  121. updatedAt: now,
  122. });
  123. } else {
  124. const [, method, path] = match;
  125. nodes.push({
  126. id: `route:${filePath}:${method!.toUpperCase()}:${path}:${line}`,
  127. kind: 'route',
  128. name: `${method!.toUpperCase()} ${path}`,
  129. qualifiedName: `${filePath}::${method!.toUpperCase()}:${path}`,
  130. filePath,
  131. startLine: line,
  132. endLine: line,
  133. startColumn: 0,
  134. endColumn: match[0].length,
  135. language: 'ruby',
  136. updatedAt: now,
  137. });
  138. }
  139. }
  140. }
  141. }
  142. // Extract controller actions
  143. if (filePath.includes('controllers/') && filePath.endsWith('.rb')) {
  144. const actionPattern = /def\s+(\w+)/g;
  145. let match;
  146. while ((match = actionPattern.exec(content)) !== null) {
  147. const [, actionName] = match;
  148. const line = content.slice(0, match.index).split('\n').length;
  149. // Skip private methods and common Rails callbacks
  150. const privateMethods = ['initialize', 'set_', 'before_', 'after_'];
  151. if (!privateMethods.some((p) => actionName!.startsWith(p))) {
  152. nodes.push({
  153. id: `action:${filePath}:${actionName}:${line}`,
  154. kind: 'method',
  155. name: actionName!,
  156. qualifiedName: `${filePath}::${actionName}`,
  157. filePath,
  158. startLine: line,
  159. endLine: line,
  160. startColumn: 0,
  161. endColumn: match[0].length,
  162. language: 'ruby',
  163. updatedAt: now,
  164. });
  165. }
  166. }
  167. }
  168. return nodes;
  169. },
  170. };
  171. // Helper functions
  172. function resolveModel(name: string, context: ResolutionContext): string | null {
  173. // Convert CamelCase to snake_case for file lookup
  174. const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
  175. const possiblePaths = [
  176. `app/models/${snakeName}.rb`,
  177. `app/models/concerns/${snakeName}.rb`,
  178. ];
  179. for (const modelPath of possiblePaths) {
  180. if (context.fileExists(modelPath)) {
  181. const nodes = context.getNodesInFile(modelPath);
  182. const modelNode = nodes.find(
  183. (n) => n.kind === 'class' && n.name === name
  184. );
  185. if (modelNode) {
  186. return modelNode.id;
  187. }
  188. }
  189. }
  190. // Search all model files
  191. const allFiles = context.getAllFiles();
  192. for (const file of allFiles) {
  193. if (file.includes('app/models/') && file.endsWith('.rb')) {
  194. const nodes = context.getNodesInFile(file);
  195. const modelNode = nodes.find(
  196. (n) => n.kind === 'class' && n.name === name
  197. );
  198. if (modelNode) {
  199. return modelNode.id;
  200. }
  201. }
  202. }
  203. return null;
  204. }
  205. function resolveController(name: string, context: ResolutionContext): string | null {
  206. // Convert CamelCase to snake_case
  207. const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
  208. const possiblePaths = [
  209. `app/controllers/${snakeName}.rb`,
  210. `app/controllers/api/${snakeName}.rb`,
  211. `app/controllers/api/v1/${snakeName}.rb`,
  212. ];
  213. for (const controllerPath of possiblePaths) {
  214. if (context.fileExists(controllerPath)) {
  215. const nodes = context.getNodesInFile(controllerPath);
  216. const controllerNode = nodes.find(
  217. (n) => n.kind === 'class' && n.name === name
  218. );
  219. if (controllerNode) {
  220. return controllerNode.id;
  221. }
  222. }
  223. }
  224. // Search all controller files
  225. const allFiles = context.getAllFiles();
  226. for (const file of allFiles) {
  227. if (file.includes('controllers/') && file.endsWith('.rb')) {
  228. const nodes = context.getNodesInFile(file);
  229. const controllerNode = nodes.find(
  230. (n) => n.kind === 'class' && n.name === name
  231. );
  232. if (controllerNode) {
  233. return controllerNode.id;
  234. }
  235. }
  236. }
  237. return null;
  238. }
  239. function resolveHelper(name: string, context: ResolutionContext): string | null {
  240. const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
  241. const helperPath = `app/helpers/${snakeName}.rb`;
  242. if (context.fileExists(helperPath)) {
  243. const nodes = context.getNodesInFile(helperPath);
  244. const helperNode = nodes.find(
  245. (n) => n.kind === 'module' && n.name === name
  246. );
  247. if (helperNode) {
  248. return helperNode.id;
  249. }
  250. }
  251. return null;
  252. }
  253. function resolveService(name: string, context: ResolutionContext): string | null {
  254. const snakeName = name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
  255. const possiblePaths = [
  256. `app/services/${snakeName}.rb`,
  257. `app/jobs/${snakeName}.rb`,
  258. `app/workers/${snakeName}.rb`,
  259. ];
  260. for (const servicePath of possiblePaths) {
  261. if (context.fileExists(servicePath)) {
  262. const nodes = context.getNodesInFile(servicePath);
  263. const serviceNode = nodes.find(
  264. (n) => n.kind === 'class' && n.name === name
  265. );
  266. if (serviceNode) {
  267. return serviceNode.id;
  268. }
  269. }
  270. }
  271. return null;
  272. }