mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
[cli] Preserve Ruby singleton_class context in sequential parsing (#774)
* fix(parsing): preserve ruby singleton class context * refactor(parsing): clarify singleton class helpers
This commit is contained in:
parent
a6421b3b1b
commit
d786e692af
2 changed files with 60 additions and 15 deletions
|
|
@ -242,9 +242,14 @@ 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.
|
||||
// A file-root `class << self` has no enclosing class/module, so this
|
||||
// intentionally falls through to null rather than synthesizing an owner.
|
||||
if (current.type === 'singleton_class') {
|
||||
current = current.parent;
|
||||
continue;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
current = current.parent;
|
||||
|
|
@ -252,11 +257,22 @@ function seqFindEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
|
|||
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). Implements the full {@link SymbolTableReader}
|
||||
* surface so future extractor additions don't silently fall off an
|
||||
* `as unknown as` cast. */
|
||||
/** Raw enclosing container lookup for extractor-only context.
|
||||
* Unlike seqFindEnclosingClassNode(), this intentionally returns
|
||||
* `singleton_class` so Ruby `class << self` methods preserve static context. */
|
||||
function seqFindRawEnclosingContainerNode(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 sequential extractor contexts. The real
|
||||
* SymbolTable is not fully populated yet at this stage, so use the stub for safety.
|
||||
* Implements the full {@link SymbolTableReader} surface so future extractor additions
|
||||
* don't silently fall off an `as unknown as` cast. */
|
||||
const NOOP_SYMBOL_TABLE_SEQ: SymbolTableReader = {
|
||||
lookupExact: () => undefined,
|
||||
lookupExactFull: () => undefined,
|
||||
|
|
@ -446,22 +462,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 = seqFindRawEnclosingContainerNode(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;
|
||||
|
|
@ -472,7 +490,7 @@ const processParsingSequential = async (
|
|||
methodProps = buildMethodProps(info);
|
||||
seqDefMethodInfo = info;
|
||||
seqDefMethods = result.methods;
|
||||
seqClassNodeId = classNode.id;
|
||||
seqClassNodeId = methodOwnerNode.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue