mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
289 lines
11 KiB
TypeScript
289 lines
11 KiB
TypeScript
/**
|
|
* Regression tests for the provider-driven resolveEnclosingOwner hook.
|
|
*
|
|
* Verifies that:
|
|
* 1. findEnclosingClassInfo delegates to the resolveEnclosingOwner hook
|
|
* 2. Ruby's resolveEnclosingOwner correctly remaps singleton_class → class/module
|
|
* 3. The hook returns null to skip containers (keep walking up)
|
|
* 4. Without the hook, the generic behavior is preserved
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import Parser from 'tree-sitter';
|
|
import Ruby from 'tree-sitter-ruby';
|
|
import { findEnclosingClassInfo } from '../../src/core/ingestion/utils/ast-helpers.js';
|
|
import { rubyProvider } from '../../src/core/ingestion/languages/ruby.js';
|
|
|
|
let Kotlin: unknown;
|
|
try {
|
|
Kotlin = require('tree-sitter-kotlin');
|
|
} catch {
|
|
// Kotlin grammar may not be installed
|
|
}
|
|
|
|
const parser = new Parser();
|
|
|
|
const parseRuby = (code: string) => {
|
|
parser.setLanguage(Ruby);
|
|
return parser.parse(code);
|
|
};
|
|
|
|
const parseKotlin = (code: string) => {
|
|
parser.setLanguage(Kotlin as Parser.Language);
|
|
return parser.parse(code);
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ruby resolveEnclosingOwner hook
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby resolveEnclosingOwner', () => {
|
|
it('remaps singleton_class to enclosing class for findEnclosingClassInfo', () => {
|
|
const tree = parseRuby(`
|
|
class Animal
|
|
class << self
|
|
def from_habitat(habitat)
|
|
end
|
|
end
|
|
end
|
|
`);
|
|
// Navigate to the method node inside singleton_class
|
|
const classNode = tree.rootNode.child(0)!;
|
|
const bodyStmt = classNode.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const singletonClass = bodyStmt.namedChildren.find((c) => c.type === 'singleton_class')!;
|
|
const innerBody = singletonClass.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = innerBody.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
const info = findEnclosingClassInfo(
|
|
methodNode,
|
|
'animal.rb',
|
|
rubyProvider.resolveEnclosingOwner,
|
|
);
|
|
|
|
expect(info).not.toBeNull();
|
|
expect(info!.className).toBe('Animal');
|
|
expect(info!.classId).toContain('Animal');
|
|
});
|
|
|
|
it('remaps singleton_class inside module to enclosing module', () => {
|
|
const tree = parseRuby(`
|
|
module Helpers
|
|
class << self
|
|
def greet
|
|
end
|
|
end
|
|
end
|
|
`);
|
|
const moduleNode = tree.rootNode.child(0)!;
|
|
const bodyStmt = moduleNode.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const singletonClass = bodyStmt.namedChildren.find((c) => c.type === 'singleton_class')!;
|
|
const innerBody = singletonClass.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = innerBody.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
const info = findEnclosingClassInfo(
|
|
methodNode,
|
|
'helpers.rb',
|
|
rubyProvider.resolveEnclosingOwner,
|
|
);
|
|
|
|
expect(info).not.toBeNull();
|
|
expect(info!.className).toBe('Helpers');
|
|
// Ruby modules are labeled `Trait` so mixin heritage resolves through
|
|
// the class-like type registry; the enclosing class id switches labels
|
|
// in lockstep with the structure-phase label.
|
|
expect(info!.classId).toContain('Trait');
|
|
});
|
|
|
|
it('returns null for file-level singleton_class without enclosing class', () => {
|
|
const tree = parseRuby(`
|
|
class << self
|
|
def orphan
|
|
end
|
|
end
|
|
`);
|
|
const singletonClass = tree.rootNode.child(0)!;
|
|
const innerBody = singletonClass.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = innerBody.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
const info = findEnclosingClassInfo(
|
|
methodNode,
|
|
'orphan.rb',
|
|
rubyProvider.resolveEnclosingOwner,
|
|
);
|
|
|
|
// No enclosing class/module — should return null
|
|
expect(info).toBeNull();
|
|
});
|
|
|
|
it('non-singleton containers pass through unchanged', () => {
|
|
const tree = parseRuby(`
|
|
class Dog
|
|
def bark
|
|
end
|
|
end
|
|
`);
|
|
const classNode = tree.rootNode.child(0)!;
|
|
const bodyStmt = classNode.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = bodyStmt.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
const info = findEnclosingClassInfo(methodNode, 'dog.rb', rubyProvider.resolveEnclosingOwner);
|
|
|
|
expect(info).not.toBeNull();
|
|
expect(info!.className).toBe('Dog');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Kotlin: findEnclosingClassInfo without resolveEnclosingOwner
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Kotlin enclosing owner resolution (no resolveEnclosingOwner needed)', () => {
|
|
(Kotlin ? it : it.skip)('companion_object methods resolve to the companion object name', () => {
|
|
const tree = parseKotlin(`
|
|
class UserService {
|
|
companion object Factory {
|
|
fun create(): UserService = UserService()
|
|
}
|
|
}
|
|
`);
|
|
// Navigate to the function_declaration inside companion object
|
|
const classNode = tree.rootNode.child(0)!;
|
|
const classBody = classNode.namedChild(1)!;
|
|
const companion = classBody.namedChild(0)!;
|
|
const companionBody = companion.namedChildren.find((c) => c.type === 'class_body')!;
|
|
const funcDecl = companionBody.namedChildren.find((c) => c.type === 'function_declaration')!;
|
|
|
|
const info = findEnclosingClassInfo(funcDecl, 'service.kt');
|
|
|
|
expect(info).not.toBeNull();
|
|
// companion_object is a valid CLASS_CONTAINER_TYPES — its name resolves via generic logic
|
|
expect(info!.className).toBe('Factory');
|
|
});
|
|
|
|
(Kotlin ? it : it.skip)('object_declaration methods resolve to the object name', () => {
|
|
const tree = parseKotlin(`
|
|
object Singleton {
|
|
fun instance(): Singleton = Singleton()
|
|
}
|
|
`);
|
|
const objDecl = tree.rootNode.child(0)!;
|
|
const objBody = objDecl.namedChildren.find((c) => c.type === 'class_body')!;
|
|
const funcDecl = objBody.namedChildren.find((c) => c.type === 'function_declaration')!;
|
|
|
|
const info = findEnclosingClassInfo(funcDecl, 'singleton.kt');
|
|
|
|
expect(info).not.toBeNull();
|
|
expect(info!.className).toBe('Singleton');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Future-proofing: invariants of the resolveEnclosingOwner hook contract
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// A future provider implementer might:
|
|
// (a) Return a non-container node by mistake (e.g. a raw identifier).
|
|
// (b) Return `current` (identity), expecting "use this container as-is".
|
|
//
|
|
// Neither case must produce an infinite loop. These tests pin the contract.
|
|
|
|
describe('findEnclosingClassInfo: hook contract guards', () => {
|
|
it('handles a hook that returns a non-container node without infinite-looping', () => {
|
|
// Ruby: class Outer { class Inner { def foo } }
|
|
// Hook will redirect from `Inner` (a CLASS_CONTAINER_TYPES node) to a
|
|
// raw `identifier`/`constant` node, which is NOT in CLASS_CONTAINER_TYPES.
|
|
const tree = parseRuby(`
|
|
class Outer
|
|
class Inner
|
|
def foo
|
|
end
|
|
end
|
|
end
|
|
`);
|
|
|
|
const outerClass = tree.rootNode.child(0)!;
|
|
const outerBody = outerClass.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const innerClass = outerBody.namedChildren.find((c) => c.type === 'class')!;
|
|
const innerBody = innerClass.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = innerBody.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
let calls = 0;
|
|
const start = Date.now();
|
|
// Resolve-hook returns a non-container child node (the class name identifier/constant).
|
|
// Per the documented contract, the walk must not infinite-loop: after the hook
|
|
// remaps, the next iteration sees a non-container node and continues walking
|
|
// up via `current = current.parent` at the end of the loop body.
|
|
const info = findEnclosingClassInfo(methodNode, 'nested.rb', (node) => {
|
|
calls += 1;
|
|
// Bail out if the contract is broken — fail fast rather than hang the suite.
|
|
if (calls > 50) throw new Error('hook called too many times — possible infinite loop');
|
|
// Always redirect to the class's name node (a 'constant' in tree-sitter-ruby),
|
|
// which is NOT in CLASS_CONTAINER_TYPES.
|
|
const nameNode = node.childForFieldName?.('name');
|
|
return nameNode ?? node;
|
|
});
|
|
const elapsed = Date.now() - start;
|
|
|
|
// Must complete quickly — no hang.
|
|
expect(elapsed).toBeLessThan(1000);
|
|
// Hook was exercised.
|
|
expect(calls).toBeGreaterThan(0);
|
|
// The function should return null (no resolvable container) rather than loop.
|
|
// It should NOT throw, and behavior is well-defined.
|
|
expect(info === null || (info && typeof info.className === 'string')).toBe(true);
|
|
});
|
|
|
|
it('handles a hook that returns the input node (identity) without infinite-looping', () => {
|
|
// Identity return is the documented "use this container as-is" branch.
|
|
// The existing `resolved === current` short-circuit must keep the algorithm
|
|
// moving forward (no re-evaluation), and the container is used directly.
|
|
const tree = parseRuby(`
|
|
class Wolf
|
|
def howl
|
|
end
|
|
end
|
|
`);
|
|
const classNode = tree.rootNode.child(0)!;
|
|
const bodyStmt = classNode.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = bodyStmt.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
let calls = 0;
|
|
const start = Date.now();
|
|
const info = findEnclosingClassInfo(methodNode, 'wolf.rb', (node) => {
|
|
calls += 1;
|
|
if (calls > 50) throw new Error('hook called too many times — possible infinite loop');
|
|
return node; // identity — equivalent to "no remap"
|
|
});
|
|
const elapsed = Date.now() - start;
|
|
|
|
expect(elapsed).toBeLessThan(1000);
|
|
expect(info).not.toBeNull();
|
|
expect(info!.className).toBe('Wolf');
|
|
// Hook should be called exactly once per CLASS_CONTAINER_TYPES node visited
|
|
// (here: just `class Wolf`). If the identity branch re-entered the hook, calls > 1.
|
|
expect(calls).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Generic behavior: no hook → container used as-is
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('findEnclosingClassInfo without resolveEnclosingOwner', () => {
|
|
it('returns the first matching container without any remapping', () => {
|
|
const tree = parseRuby(`
|
|
class Dog
|
|
def bark
|
|
end
|
|
end
|
|
`);
|
|
const classNode = tree.rootNode.child(0)!;
|
|
const bodyStmt = classNode.namedChildren.find((c) => c.type === 'body_statement')!;
|
|
const methodNode = bodyStmt.namedChildren.find((c) => c.type === 'method')!;
|
|
|
|
// Without the hook, generic behavior applies
|
|
const info = findEnclosingClassInfo(methodNode, 'dog.rb');
|
|
|
|
expect(info).not.toBeNull();
|
|
expect(info!.className).toBe('Dog');
|
|
});
|
|
});
|