Browse Source

feat: Add Ruby bare method call extraction for identifier nodes

Addresses Ruby bare method calls like `reset` that parse as identifier nodes instead of call expressions. Adds extractBareCall hook to detect statement-level identifiers that represent method calls, filtering out keywords, literals, and constants. Enables proper call relationship tracking for Ruby's parentheses-optional method syntax.
Colby McHenry 2 months ago
parent
commit
9382a087f4

+ 29 - 0
src/extraction/languages/ruby.ts

@@ -38,6 +38,35 @@ export const rubyExtractor: LanguageExtractor = {
     ctx.popScope();
     return true; // handled
   },
+  extractBareCall: (node, _source) => {
+    // Ruby bare method calls (no parens, no receiver) parse as plain identifiers.
+    // e.g., `reset` in a method body is `identifier "reset"` not a `call` node.
+    if (node.type !== 'identifier') return undefined;
+
+    const parent = node.parent;
+    if (!parent) return undefined;
+
+    // Only statement-level identifiers — direct children of block/body nodes
+    const BLOCK_PARENTS = new Set([
+      'body_statement', 'then', 'else', 'do', 'begin',
+      'rescue', 'ensure', 'when',
+    ]);
+    if (!BLOCK_PARENTS.has(parent.type)) return undefined;
+
+    const name = node.text;
+
+    // Skip Ruby keywords/literals
+    const SKIP = new Set([
+      'true', 'false', 'nil', 'self', 'super',
+      '__FILE__', '__LINE__', '__dir__',
+    ]);
+    if (SKIP.has(name)) return undefined;
+
+    // Skip constants (uppercase start) — these are class/module refs, not calls
+    if (name.length > 0 && name.charCodeAt(0) >= 65 && name.charCodeAt(0) <= 90) return undefined;
+
+    return name;
+  },
   getVisibility: (node) => {
     // Ruby visibility is based on preceding visibility modifiers
     let sibling = node.previousNamedSibling;

+ 8 - 0
src/extraction/tree-sitter-types.ts

@@ -198,4 +198,12 @@ export interface LanguageExtractor {
    * structural nodes (classes, structs, enums).
    */
   isMisparsedFunction?: (name: string, node: SyntaxNode) => boolean;
+
+  /**
+   * Detect bare method calls that don't use call expression syntax.
+   * Used by Ruby where `reset` (no parens, no receiver) is a method call but
+   * tree-sitter parses it as a plain `identifier` node instead of `call`/`method_call`.
+   * Returns the callee name if this node is a bare call, or undefined if not.
+   */
+  extractBareCall?: (node: SyntaxNode, source: string) => string | undefined;
 }

+ 14 - 0
src/extraction/tree-sitter.ts

@@ -1391,6 +1391,20 @@ export class TreeSitterExtractor {
 
       if (this.extractor!.callTypes.includes(nodeType)) {
         this.extractCall(node);
+      } else if (this.extractor!.extractBareCall) {
+        const calleeName = this.extractor!.extractBareCall(node, this.source);
+        if (calleeName && this.nodeStack.length > 0) {
+          const callerId = this.nodeStack[this.nodeStack.length - 1];
+          if (callerId) {
+            this.unresolvedReferences.push({
+              fromNodeId: callerId,
+              referenceName: calleeName,
+              referenceKind: 'calls',
+              line: node.startPosition.row + 1,
+              column: node.startPosition.column,
+            });
+          }
+        }
       }
 
       // Extract structural nodes found inside function bodies.