mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-12 08:41:34 +00:00
* fix(deps): upgrade native tree-sitter to 0.25.1 * fix(tree-sitter): preserve sync loading and update verified capture baselines Route C# scope queries through the synchronous native grammar loader so the compiled analyze module remains require-compatible on Node 22.18. Refresh only the audited empty-Python-module and renamed-PHP-fixture captures, and pin the runtime assertion to 0.25.1. Validated 479 targeted tests, all 16 scope-capture benchmark cases, Python capture/import guards, compiled C# worker parsing, build, typecheck and formatting. Performance budgets are unchanged. * fix(tree-sitter): refresh upstream vendors and preserve parser semantics * fix(ci): refresh prebuilds and support Windows ARM Refresh macOS, Linux ARM, and Windows x64 grammar binaries from the successful native CI builds. Pin node-gyp 12.4.0 so the Windows ARM jobs recognize Visual Studio 2026. Use local Git filtering for E2E to avoid PR-files API timeouts on generated parser diffs. Validation: 143 focused tests, Actionlint, and all 16 scope-capture benchmark checks passed. Full npm test run started; native database suites still running. * fix(ci): refresh Windows ARM grammar prebuilds Import all seven Windows ARM grammar artifacts from successful native build run 37965761760. Artifact filenames and SHA-256 hashes verified; all 42 native build jobs passed. Validation: 31 prebuild coverage tests passed. * fix(bench): stabilize scope capture timing guard Warm each input size seven times and use 21 median samples while retaining isolated timing blocks. Yield between languages so native finalizers release allocations outside the measured sections. Keep all fixture sizes, fingerprints, scaling formulas, and budgets unchanged. Validation: all 16 language guards passed with a 2 GB heap limit (1.38 deep-chain ratio; 1518 MB peak RSS). Linear and quadratic controls passed their expected guard outcomes. Formatting and graph change analysis passed. Full npm test is running without failures so far. * fix(parser): reduce TypeScript capture traversal cost * fix(test): await parser worker readiness before teardown * fix(parser): load C# synchronously in workers * fix(grpc): resolve native proto detections against their own file With the native proto grammar active, sibling .proto files that declare the same short service name under different packages tied in the directory-based resolveProtoConflict heuristic and both providers were dropped. A proto-origin detection already knows its defining file, so use that file's entry before falling back to the heuristic. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(packaging): assert native grammars load from the installed tarball The packaged-install smoke only ran `gitnexus --version`, so a grammar that failed to load from the published package (bundled runtime, patched peer manifests, vendored prebuilds, C# sync loader, Protobuf wrapper) would ship silently; the dev-tree ABI smoke also accepts vendored grammars that report unavailable. Parse a sample for every registered grammar plus the native Protobuf plugin from the installed package, on the main thread and in a worker thread. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(tree-sitter): track compatibility workarounds instead of 0.25 readiness The daily readiness report still targeted tree-sitter@0.25.0, so after the upgrade it would keep reporting "Blocked" and never close its tracking issue. Repurpose it to track what we carry for the bundled runtime: derive the target from the package.json pin, list each pack-time peer patch from prepare-tree-sitter-bundle.cjs with its drop condition (bump, wait for an upstream release, transitive, or intentionally pinned), and reword the buckets. The workflow adopts the issue opened under the old title. Also update the grammar-update-monitor header and auto-PR text, which still described the 0.21.1 runtime and a tree-sitter-c hold. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(dart): assert both nested labels target the loop; fix Zig call-shape doc The nested-label CFG test only jumped to `outer` and checked reachability, which still passes when a jump is retargeted to the inner loop (its exit reaches the outer header). Jump to both labels and assert the exact `continue` edges; dropping the inner label now fails the test. Update the Zig callable-flow comment: call_expression now wraps actuals in a required `arguments:` field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
291 lines
11 KiB
TypeScript
291 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';
|
|
import { requireVendoredGrammar } from '../../src/core/tree-sitter/vendored-grammars.js';
|
|
|
|
// Vendored grammar — loaded from vendor/ by absolute path, never node_modules (#2111).
|
|
let Kotlin: Parser.Language | null = null;
|
|
try {
|
|
Kotlin = requireVendoredGrammar('tree-sitter-kotlin');
|
|
} catch {
|
|
// Kotlin grammar may not have a prebuild for this platform
|
|
}
|
|
|
|
const parser = new Parser();
|
|
|
|
const parseRuby = (code: string) => {
|
|
parser.setLanguage(Ruby);
|
|
return parser.parse(code);
|
|
};
|
|
|
|
const parseKotlin = (code: string) => {
|
|
parser.setLanguage(Kotlin);
|
|
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');
|
|
});
|
|
});
|