fix(parsing): preserve ruby singleton class context

This commit is contained in:
ideepakchauhan7 2026-04-11 01:01:26 +05:30
parent d87744fffc
commit 067e45a8d4
2 changed files with 51 additions and 10 deletions

View file

@ -240,9 +240,12 @@ function seqFindEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
let current = node.parent;
while (current) {
if (CLASS_CONTAINER_TYPES.has(current.type)) {
// Return singleton_class directly so the method extractor sees it as
// the owner node and correctly marks methods as static. Name resolution
// for qualified names is handled separately by findEnclosingClassInfo.
// Ruby singleton_class (class << self) has no name field, so owner/class
// resolution should skip it and return the enclosing class/module instead.
if (current.type === 'singleton_class') {
current = current.parent;
continue;
}
return current;
}
current = current.parent;
@ -250,6 +253,15 @@ function seqFindEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
return null;
}
function seqFindMethodOwnerNode(node: SyntaxNode): SyntaxNode | null {
let current = node.parent;
while (current) {
if (CLASS_CONTAINER_TYPES.has(current.type)) return current;
current = current.parent;
}
return null;
}
/** Minimal no-op SymbolTable stub for FieldExtractorContext (sequential path has a real
* SymbolTable, but it's incomplete at this stage — use the stub for safety). */
const NOOP_SYMBOL_TABLE_SEQ = {
@ -438,22 +450,24 @@ const processParsingSequential = async (
let enriched = false;
if (provider.methodExtractor) {
// Try class-based extraction (method inside a class/struct/trait body)
const classNode = seqFindEnclosingClassNode(definitionNode);
if (classNode) {
// Try class-based extraction (method inside a class/struct/trait body).
// Ruby `class << self` needs the singleton_class node for `isStatic`,
// while owner/class resolution still skips it elsewhere.
const methodOwnerNode = seqFindMethodOwnerNode(definitionNode);
if (methodOwnerNode) {
// Cache extract() results per class node to avoid re-traversing the
// same class body for every method it contains (O(N) -> O(1) per hit).
let result:
| { ownerName: string | undefined; methods: MethodInfo[] }
| null
| undefined = seqMethodExtractCache.get(classNode.id);
| undefined = seqMethodExtractCache.get(methodOwnerNode.id);
if (result === undefined) {
result =
provider.methodExtractor.extract(classNode, {
provider.methodExtractor.extract(methodOwnerNode, {
filePath: file.path,
language,
}) ?? null;
seqMethodExtractCache.set(classNode.id, result);
seqMethodExtractCache.set(methodOwnerNode.id, result);
}
if (result?.methods?.length) {
const defLine = definitionNode.startPosition.row + 1;
@ -464,7 +478,7 @@ const processParsingSequential = async (
methodProps = buildMethodProps(info);
seqDefMethodInfo = info;
seqDefMethods = result.methods;
seqClassNodeId = classNode.id;
seqClassNodeId = methodOwnerNode.id;
}
}
}

View file

@ -1262,6 +1262,33 @@ describe('Ruby method enrichment (visibility, isStatic, parameters)', () => {
});
});
describe('Ruby singleton_class handling via sequential path (skipWorkers)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'ruby-method-enrichment'), () => {}, {
skipWorkers: true,
});
}, 60000);
it('keeps Animal as the owner for class << self methods', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
expect(
hasMethod.find((e) => e.source === 'Animal' && e.target === 'from_habitat'),
).toBeDefined();
});
it('marks from_habitat as static in the sequential path', () => {
const methods = getNodesByLabelFull(result, 'Method');
const fromHabitat = methods.find(
(m) => m.name === 'from_habitat' && m.properties.filePath?.includes('animal'),
);
expect(fromHabitat).toBeDefined();
expect(fromHabitat!.properties.isStatic).toBe(true);
expect(fromHabitat!.properties.parameterCount).toBe(1);
});
});
// ---------------------------------------------------------------------------
// Overload Dispatch: methods with different arity resolve via receiver type
// ---------------------------------------------------------------------------