ruby.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import type { Node as SyntaxNode } from 'web-tree-sitter';
  2. import { getNodeText, getChildByField } from '../tree-sitter-helpers';
  3. import type { LanguageExtractor } from '../tree-sitter-types';
  4. export const rubyExtractor: LanguageExtractor = {
  5. functionTypes: ['method'],
  6. classTypes: ['class'],
  7. methodTypes: ['method', 'singleton_method'],
  8. interfaceTypes: [], // Ruby uses modules (handled via visitNode hook)
  9. structTypes: [],
  10. enumTypes: [],
  11. typeAliasTypes: [],
  12. importTypes: ['call'], // require/require_relative
  13. callTypes: ['call', 'method_call'],
  14. variableTypes: ['assignment'], // Ruby uses assignment like Python
  15. nameField: 'name',
  16. bodyField: 'body',
  17. paramsField: 'parameters',
  18. visitNode: (node, ctx) => {
  19. // Ruby mixins: `include Mod`, `extend Mod`, `prepend Mod[, Other]` — the
  20. // primary composition mechanism (ActiveSupport concerns, Comparable, …).
  21. // These parse as a bare `call` to `include`/`extend`/`prepend` with the
  22. // module(s) as constant arguments, so without special handling they'd be
  23. // mis-extracted as a call to a method named "include" and the module would
  24. // record no dependent — even though it's mixed into a class. Emit an
  25. // `implements` edge (enclosing class/module → mixed-in module), so editing a
  26. // concern surfaces every class that includes it.
  27. if (node.type === 'call' && !node.childForFieldName('receiver')) {
  28. const method = node.childForFieldName('method');
  29. const mname = method?.text;
  30. if (mname === 'include' || mname === 'extend' || mname === 'prepend') {
  31. const parentId = ctx.nodeStack.length > 0 ? ctx.nodeStack[ctx.nodeStack.length - 1] : undefined;
  32. const args = node.childForFieldName('arguments')
  33. ?? node.namedChildren.find((c: SyntaxNode) => c.type === 'argument_list');
  34. if (parentId && args) {
  35. for (let i = 0; i < args.namedChildCount; i++) {
  36. const arg = args.namedChild(i);
  37. // `Mod` is `constant`, `Foo::Bar` is `scope_resolution`. Skip
  38. // `extend self` / dynamic args (`include foo()`).
  39. if (arg && (arg.type === 'constant' || arg.type === 'scope_resolution')) {
  40. ctx.addUnresolvedReference({
  41. fromNodeId: parentId,
  42. referenceName: getNodeText(arg, ctx.source),
  43. referenceKind: 'implements',
  44. filePath: ctx.filePath,
  45. line: node.startPosition.row + 1,
  46. column: node.startPosition.column,
  47. });
  48. }
  49. }
  50. return true; // handled — don't also extract as a call to "include"
  51. }
  52. }
  53. }
  54. if (node.type !== 'module') return false;
  55. const nameNode = node.childForFieldName('name');
  56. if (!nameNode) return false;
  57. const name = nameNode.text;
  58. const moduleNode = ctx.createNode('module', name, node);
  59. if (!moduleNode) return false;
  60. // Push module onto scope stack so children get proper qualified names
  61. ctx.pushScope(moduleNode.id);
  62. const body = node.childForFieldName('body');
  63. if (body) {
  64. for (let i = 0; i < body.namedChildCount; i++) {
  65. const child = body.namedChild(i);
  66. if (child) ctx.visitNode(child);
  67. }
  68. }
  69. ctx.popScope();
  70. return true; // handled
  71. },
  72. extractBareCall: (node, _source) => {
  73. // Ruby bare method calls (no parens, no receiver) parse as plain identifiers.
  74. // e.g., `reset` in a method body is `identifier "reset"` not a `call` node.
  75. if (node.type !== 'identifier') return undefined;
  76. const parent = node.parent;
  77. if (!parent) return undefined;
  78. // Only statement-level identifiers — direct children of block/body nodes
  79. const BLOCK_PARENTS = new Set([
  80. 'body_statement', 'then', 'else', 'do', 'begin',
  81. 'rescue', 'ensure', 'when',
  82. ]);
  83. if (!BLOCK_PARENTS.has(parent.type)) return undefined;
  84. const name = node.text;
  85. // Skip Ruby keywords/literals
  86. const SKIP = new Set([
  87. 'true', 'false', 'nil', 'self', 'super',
  88. '__FILE__', '__LINE__', '__dir__',
  89. ]);
  90. if (SKIP.has(name)) return undefined;
  91. // Skip constants (uppercase start) — these are class/module refs, not calls
  92. if (name.length > 0 && name.charCodeAt(0) >= 65 && name.charCodeAt(0) <= 90) return undefined;
  93. return name;
  94. },
  95. getVisibility: (node) => {
  96. // Ruby visibility is based on preceding visibility modifiers
  97. let sibling = node.previousNamedSibling;
  98. while (sibling) {
  99. if (sibling.type === 'call') {
  100. const methodName = getChildByField(sibling, 'method');
  101. if (methodName) {
  102. const text = methodName.text;
  103. if (text === 'private') return 'private';
  104. if (text === 'protected') return 'protected';
  105. if (text === 'public') return 'public';
  106. }
  107. }
  108. sibling = sibling.previousNamedSibling;
  109. }
  110. return 'public';
  111. },
  112. extractImport: (node, source) => {
  113. const importText = source.substring(node.startIndex, node.endIndex).trim();
  114. // Check if this is a require/require_relative call
  115. const identifier = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
  116. if (!identifier) return null;
  117. const methodName = getNodeText(identifier, source);
  118. if (methodName !== 'require' && methodName !== 'require_relative') {
  119. return null; // Not an import, skip
  120. }
  121. // Find the argument (string)
  122. const argList = node.namedChildren.find((c: SyntaxNode) => c.type === 'argument_list');
  123. if (argList) {
  124. const stringNode = argList.namedChildren.find((c: SyntaxNode) => c.type === 'string');
  125. if (stringNode) {
  126. const stringContent = stringNode.namedChildren.find((c: SyntaxNode) => c.type === 'string_content');
  127. if (stringContent) {
  128. return { moduleName: getNodeText(stringContent, source), signature: importText };
  129. }
  130. }
  131. }
  132. return null;
  133. },
  134. };