feat: Add Ruby module extraction with containment and qualified names
Addresses Ruby methods inside modules missing owner in qualified_name by adding visitNode hook to extract module AST nodes. Methods inside modules now get Module::method qualified names with proper containment relationships. Includes ExtractorContext wiring with pushScope/popScope for language hooks and updates isInsideClassLikeNode to include module kind for nested method handling.
This commit is contained in:
@@ -6,7 +6,7 @@ export const rubyExtractor: LanguageExtractor = {
|
||||
functionTypes: ['method'],
|
||||
classTypes: ['class'],
|
||||
methodTypes: ['method', 'singleton_method'],
|
||||
interfaceTypes: [], // Ruby uses modules
|
||||
interfaceTypes: [], // Ruby uses modules (handled via visitNode hook)
|
||||
structTypes: [],
|
||||
enumTypes: [],
|
||||
typeAliasTypes: [],
|
||||
@@ -16,6 +16,28 @@ export const rubyExtractor: LanguageExtractor = {
|
||||
nameField: 'name',
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
visitNode: (node, ctx) => {
|
||||
if (node.type !== 'module') return false;
|
||||
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return false;
|
||||
const name = nameNode.text;
|
||||
|
||||
const moduleNode = ctx.createNode('module', name, node);
|
||||
if (!moduleNode) return false;
|
||||
|
||||
// Push module onto scope stack so children get proper qualified names
|
||||
ctx.pushScope(moduleNode.id);
|
||||
const body = node.childForFieldName('body');
|
||||
if (body) {
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const child = body.namedChild(i);
|
||||
if (child) ctx.visitNode(child);
|
||||
}
|
||||
}
|
||||
ctx.popScope();
|
||||
return true; // handled
|
||||
},
|
||||
getVisibility: (node) => {
|
||||
// Ruby visibility is based on preceding visibility modifiers
|
||||
let sibling = node.previousNamedSibling;
|
||||
|
||||
@@ -56,6 +56,10 @@ export interface ExtractorContext {
|
||||
visitFunctionBody(body: SyntaxNode, functionId: string): void;
|
||||
/** Add an unresolved reference */
|
||||
addUnresolvedReference(ref: UnresolvedReference): void;
|
||||
/** Push a node ID onto the scope stack (for containment/qualified name building) */
|
||||
pushScope(nodeId: string): void;
|
||||
/** Pop the last node ID from the scope stack */
|
||||
popScope(): void;
|
||||
/** Current file path */
|
||||
readonly filePath: string;
|
||||
/** Current source text */
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '../types';
|
||||
import { getParser, detectLanguage, isLanguageSupported } from './grammars';
|
||||
import { generateNodeId, getNodeText, getChildByField, getPrecedingDocstring } from './tree-sitter-helpers';
|
||||
import type { LanguageExtractor } from './tree-sitter-types';
|
||||
import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
|
||||
import { EXTRACTORS } from './languages';
|
||||
import { LiquidExtractor } from './liquid-extractor';
|
||||
import { SvelteExtractor } from './svelte-extractor';
|
||||
@@ -223,6 +223,13 @@ export class TreeSitterExtractor {
|
||||
const nodeType = node.type;
|
||||
let skipChildren = false;
|
||||
|
||||
// Language-specific custom visitor hook
|
||||
if (this.extractor.visitNode) {
|
||||
const ctx = this.makeExtractorContext();
|
||||
const handled = this.extractor.visitNode(node, ctx);
|
||||
if (handled) return;
|
||||
}
|
||||
|
||||
// Pascal-specific AST handling
|
||||
if (this.language === 'pascal') {
|
||||
skipChildren = this.visitPascalNode(node);
|
||||
@@ -409,6 +416,26 @@ export class TreeSitterExtractor {
|
||||
return parts.join('::');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an ExtractorContext for passing to language-specific visitNode hooks.
|
||||
*/
|
||||
private makeExtractorContext(): ExtractorContext {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
return {
|
||||
createNode: (kind, name, node, extra) => self.createNode(kind, name, node, extra),
|
||||
visitNode: (node) => self.visitNode(node),
|
||||
visitFunctionBody: (body, functionId) => self.visitFunctionBody(body, functionId),
|
||||
addUnresolvedReference: (ref) => self.unresolvedReferences.push(ref),
|
||||
pushScope: (nodeId) => self.nodeStack.push(nodeId),
|
||||
popScope: () => self.nodeStack.pop(),
|
||||
get filePath() { return self.filePath; },
|
||||
get source() { return self.source; },
|
||||
get nodeStack() { return self.nodeStack; },
|
||||
get nodes() { return self.nodes; },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current node stack indicates we are inside a class-like node
|
||||
* (class, struct, interface, trait). File nodes do not count as class-like.
|
||||
@@ -424,7 +451,8 @@ export class TreeSitterExtractor {
|
||||
parentNode.kind === 'struct' ||
|
||||
parentNode.kind === 'interface' ||
|
||||
parentNode.kind === 'trait' ||
|
||||
parentNode.kind === 'enum'
|
||||
parentNode.kind === 'enum' ||
|
||||
parentNode.kind === 'module'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user