Files
codegraph/src/extraction/languages/ruby.ts
T
Colby McHenry 59ea5a43be 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.
2026-04-07 09:17:34 -05:00

83 lines
2.9 KiB
TypeScript

import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';
export const rubyExtractor: LanguageExtractor = {
functionTypes: ['method'],
classTypes: ['class'],
methodTypes: ['method', 'singleton_method'],
interfaceTypes: [], // Ruby uses modules (handled via visitNode hook)
structTypes: [],
enumTypes: [],
typeAliasTypes: [],
importTypes: ['call'], // require/require_relative
callTypes: ['call', 'method_call'],
variableTypes: ['assignment'], // Ruby uses assignment like Python
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;
while (sibling) {
if (sibling.type === 'call') {
const methodName = getChildByField(sibling, 'method');
if (methodName) {
const text = methodName.text;
if (text === 'private') return 'private';
if (text === 'protected') return 'protected';
if (text === 'public') return 'public';
}
}
sibling = sibling.previousNamedSibling;
}
return 'public';
},
extractImport: (node, source) => {
const importText = source.substring(node.startIndex, node.endIndex).trim();
// Check if this is a require/require_relative call
const identifier = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
if (!identifier) return null;
const methodName = getNodeText(identifier, source);
if (methodName !== 'require' && methodName !== 'require_relative') {
return null; // Not an import, skip
}
// Find the argument (string)
const argList = node.namedChildren.find((c: SyntaxNode) => c.type === 'argument_list');
if (argList) {
const stringNode = argList.namedChildren.find((c: SyntaxNode) => c.type === 'string');
if (stringNode) {
const stringContent = stringNode.namedChildren.find((c: SyntaxNode) => c.type === 'string_content');
if (stringContent) {
return { moduleName: getNodeText(stringContent, source), signature: importText };
}
}
}
return null;
},
};