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.
This commit is contained in:
@@ -38,6 +38,35 @@ export const rubyExtractor: LanguageExtractor = {
|
|||||||
ctx.popScope();
|
ctx.popScope();
|
||||||
return true; // handled
|
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) => {
|
getVisibility: (node) => {
|
||||||
// Ruby visibility is based on preceding visibility modifiers
|
// Ruby visibility is based on preceding visibility modifiers
|
||||||
let sibling = node.previousNamedSibling;
|
let sibling = node.previousNamedSibling;
|
||||||
|
|||||||
@@ -198,4 +198,12 @@ export interface LanguageExtractor {
|
|||||||
* structural nodes (classes, structs, enums).
|
* structural nodes (classes, structs, enums).
|
||||||
*/
|
*/
|
||||||
isMisparsedFunction?: (name: string, node: SyntaxNode) => boolean;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1391,6 +1391,20 @@ export class TreeSitterExtractor {
|
|||||||
|
|
||||||
if (this.extractor!.callTypes.includes(nodeType)) {
|
if (this.extractor!.callTypes.includes(nodeType)) {
|
||||||
this.extractCall(node);
|
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.
|
// Extract structural nodes found inside function bodies.
|
||||||
|
|||||||
Reference in New Issue
Block a user