From 9382a087f42e38c9319ca4007a80fd99530b2f4b Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 7 Apr 2026 09:27:00 -0500 Subject: [PATCH] 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. --- src/extraction/languages/ruby.ts | 29 +++++++++++++++++++++++++++++ src/extraction/tree-sitter-types.ts | 8 ++++++++ src/extraction/tree-sitter.ts | 14 ++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/extraction/languages/ruby.ts b/src/extraction/languages/ruby.ts index 5358720..b542616 100644 --- a/src/extraction/languages/ruby.ts +++ b/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; diff --git a/src/extraction/tree-sitter-types.ts b/src/extraction/tree-sitter-types.ts index 75a5944..fb939ca 100644 --- a/src/extraction/tree-sitter-types.ts +++ b/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; } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 0d9cd16..99181d4 100644 --- a/src/extraction/tree-sitter.ts +++ b/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.