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:
Colby McHenry
2026-04-07 09:17:34 -05:00
parent 07d899b735
commit 59ea5a43be
7 changed files with 126 additions and 6 deletions
+64
View File
@@ -1787,6 +1787,70 @@ require_relative 'helper'
});
});
describe('Ruby modules', () => {
it('should extract module as module node with containment', () => {
const code = `
module CachedCounting
def self.disable
@enabled = false
end
def perform_increment!(key, count)
write_cache!(key, count)
end
end
`;
const result = extractFromSource('concerns/cached_counting.rb', code);
const moduleNode = result.nodes.find((n) => n.kind === 'module' && n.name === 'CachedCounting');
expect(moduleNode).toBeDefined();
expect(moduleNode?.qualifiedName).toBe('CachedCounting');
// Methods inside module should have module-qualified names
const disableMethod = result.nodes.find((n) => n.name === 'disable' && n.kind === 'method');
expect(disableMethod).toBeDefined();
expect(disableMethod?.qualifiedName).toBe('CachedCounting::disable');
const incrementMethod = result.nodes.find((n) => n.name === 'perform_increment!' && n.kind === 'method');
expect(incrementMethod).toBeDefined();
expect(incrementMethod?.qualifiedName).toBe('CachedCounting::perform_increment!');
// Containment edge from module to methods
const containsEdges = result.edges.filter((e) => e.source === moduleNode?.id && e.kind === 'contains');
expect(containsEdges.length).toBeGreaterThanOrEqual(2);
});
it('should handle nested modules with classes', () => {
const code = `
module Discourse
module Auth
class AuthProvider
def authenticate(params)
validate(params)
end
end
end
end
`;
const result = extractFromSource('lib/auth.rb', code);
const discourseModule = result.nodes.find((n) => n.kind === 'module' && n.name === 'Discourse');
expect(discourseModule).toBeDefined();
const authModule = result.nodes.find((n) => n.kind === 'module' && n.name === 'Auth');
expect(authModule).toBeDefined();
expect(authModule?.qualifiedName).toBe('Discourse::Auth');
const authProvider = result.nodes.find((n) => n.kind === 'class' && n.name === 'AuthProvider');
expect(authProvider).toBeDefined();
expect(authProvider?.qualifiedName).toBe('Discourse::Auth::AuthProvider');
const authMethod = result.nodes.find((n) => n.name === 'authenticate');
expect(authMethod).toBeDefined();
expect(authMethod?.qualifiedName).toBe('Discourse::Auth::AuthProvider::authenticate');
});
});
describe('C/C++ imports', () => {
it('should extract system include', () => {
const code = `#include <iostream>`;