Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15fb007c53 | ||
|
|
09d78cadec | ||
|
|
9e62f7c121 | ||
|
|
acef549791 |
@@ -11,7 +11,8 @@ name: Scope Resolution Parity
|
||||
# TWICE on every PR:
|
||||
#
|
||||
# 1. `REGISTRY_PRIMARY_<LANG>=0` — legacy DAG path (guarantees we haven't
|
||||
# broken the old path while migrating).
|
||||
# broken the old path while migrating). Known legacy gaps may be skipped
|
||||
# through the resolver test helper's expected-failure list.
|
||||
# 2. `REGISTRY_PRIMARY_<LANG>=1` — registry-primary path (guarantees the
|
||||
# new path carries the same behavior — the parity gate).
|
||||
#
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.3",
|
||||
"version": "1.6.4-rc.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.3",
|
||||
"version": "1.6.4-rc.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.6.3",
|
||||
"version": "1.6.4-rc.7",
|
||||
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
||||
"author": "Abhigyan Patwari",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
* `emitScopeCaptures`. Returns `undefined`; zero work done. This is
|
||||
* the state of every language today — `ParsedFile` production stays
|
||||
* dormant until a language migrates.
|
||||
* 2. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
|
||||
* 3. **Swallows exceptions from either side.** A failure here returns
|
||||
* 2. Short-circuits empty / whitespace-only files. There is no scope
|
||||
* content to extract, and some tree-sitter queries do not match an
|
||||
* otherwise valid empty root node.
|
||||
* 3. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
|
||||
* 4. **Swallows exceptions from either side.** A failure here returns
|
||||
* `undefined` and emits a warning via `onWarn`; legacy parsing on
|
||||
* the same file continues unaffected by the scope-extraction miss.
|
||||
* Scope-based resolution is the new path under construction — it
|
||||
@@ -41,6 +44,7 @@ export function extractParsedFile(
|
||||
cachedTree?: unknown,
|
||||
): ParsedFile | undefined {
|
||||
if (provider.emitScopeCaptures === undefined) return undefined;
|
||||
if (sourceText.trim().length === 0) return undefined;
|
||||
try {
|
||||
const captures = provider.emitScopeCaptures(sourceText, filePath, cachedTree);
|
||||
return extractScope(captures, filePath, provider);
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
using Collision.Models;
|
||||
|
||||
namespace Collision.App
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public string GetName() { return "app"; }
|
||||
}
|
||||
|
||||
public class Program
|
||||
{
|
||||
public void Run()
|
||||
{
|
||||
var u = new User();
|
||||
u.GetName();
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RootNamespace>Collision</RootNamespace>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace Collision.Models
|
||||
{
|
||||
public class User
|
||||
{
|
||||
public string GetName() { return "models"; }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* C#: heritage resolution via base_list + ambiguous namespace-import refusal
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { describe, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES,
|
||||
CROSS_FILE_FIXTURES,
|
||||
createResolverParityIt,
|
||||
getRelationships,
|
||||
getNodesByLabel,
|
||||
getNodesByLabelFull,
|
||||
@@ -14,6 +15,8 @@ import {
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
const it = createResolverParityIt('csharp');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Heritage: class + interface resolution via base_list
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2485,3 +2488,50 @@ describe('C# large-file + frozen-bucket regression (issue #1066)', () => {
|
||||
expect(['import-resolved', 'global']).toContain(save!.rel.reason);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Issue #1066 companion regression: small-file trigger for the same
|
||||
// frozen-bucket failure. Where csharp-large-cache-miss-resolution exercises
|
||||
// the path through tree-sitter cache-miss reparse on >32 KB files, this
|
||||
// fixture trips the same `Object.freeze` contract on the populator's
|
||||
// namespace-import loop in a single small file pair: the importer locally
|
||||
// declares a class with the same simple name as a sibling reachable through
|
||||
// `using`, so the extractor pre-populates (and freezes) `User` in the
|
||||
// importer's Module bindings before populateNamespaceSiblings tries to
|
||||
// append the cross-file `Collision.Models.User`. Pre-#1082 the populator
|
||||
// pushed onto the frozen array → "Cannot add property N, object is not
|
||||
// extensible" → whole scopeResolution phase aborted. Post-#1082 the
|
||||
// augmentation channel keeps both bindings visible to readers, with the
|
||||
// local `Collision.App.User` taking precedence per origin ordering.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# frozen-binding collision via using-import (issue #1066 companion)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-frozen-binding-collision'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('completes scopeResolution without throwing on the colliding bucket', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(expect.arrayContaining(['User', 'Program']));
|
||||
});
|
||||
|
||||
it('detects both User declarations across the two namespaces', () => {
|
||||
const users = getNodesByLabelFull(result, 'Class').filter((n) => n.name === 'User');
|
||||
expect(users.length).toBe(2);
|
||||
const paths = users.map((u) => u.properties.filePath).sort();
|
||||
expect(paths).toEqual(['App/Program.cs', 'Models/User.cs']);
|
||||
});
|
||||
|
||||
it('resolves Program.Run -> local Collision.App.User constructor (origin:local shadows namespace)', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const ctor = calls.find(
|
||||
(c) => c.source === 'Run' && c.target === 'User' && c.targetLabel === 'Class',
|
||||
);
|
||||
expect(ctor).toBeDefined();
|
||||
expect(ctor!.targetFilePath).toBe('App/Program.cs');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,54 @@
|
||||
* Shared test helpers for language resolution integration tests.
|
||||
*/
|
||||
import path from 'path';
|
||||
import { it as vitestIt } from 'vitest';
|
||||
import { runPipelineFromRepo } from '../../../src/core/ingestion/pipeline.js';
|
||||
import type { PipelineOptions } from '../../../src/core/ingestion/pipeline.js';
|
||||
import type { PipelineResult } from '../../../src/types/pipeline.js';
|
||||
import type { GraphRelationship } from '../../../src/core/graph/types.js';
|
||||
import type { GraphRelationship } from 'gitnexus-shared';
|
||||
|
||||
const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, ReadonlySet<string>>> = {
|
||||
csharp: new Set([
|
||||
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path',
|
||||
]),
|
||||
};
|
||||
|
||||
type ResolverParityEnv = Readonly<Record<string, string | undefined>>;
|
||||
type VitestIt = typeof vitestIt;
|
||||
type CallableIt = (name: string, ...args: unknown[]) => unknown;
|
||||
|
||||
export function resolverParityFlagName(languageSlug: string): string {
|
||||
return `REGISTRY_PRIMARY_${languageSlug.toUpperCase().replace(/-/g, '_')}`;
|
||||
}
|
||||
|
||||
export function isLegacyResolverParityRun(
|
||||
languageSlug: string,
|
||||
env: ResolverParityEnv = process.env,
|
||||
): boolean {
|
||||
const value = env[resolverParityFlagName(languageSlug)]?.trim().toLowerCase();
|
||||
return value === '0' || value === 'false' || value === 'no';
|
||||
}
|
||||
|
||||
export function isLegacyResolverParityExpectedFailure(
|
||||
languageSlug: string,
|
||||
testName: string,
|
||||
env: ResolverParityEnv = process.env,
|
||||
): boolean {
|
||||
if (!isLegacyResolverParityRun(languageSlug, env)) return false;
|
||||
return LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES[languageSlug]?.has(testName) ?? false;
|
||||
}
|
||||
|
||||
export function createResolverParityIt(languageSlug: string): VitestIt {
|
||||
const wrapped = ((name: string, ...args: unknown[]) => {
|
||||
const runner = isLegacyResolverParityExpectedFailure(languageSlug, name)
|
||||
? vitestIt.skip
|
||||
: vitestIt;
|
||||
return (runner as unknown as CallableIt)(name, ...args);
|
||||
}) as VitestIt;
|
||||
|
||||
Object.assign(wrapped, vitestIt);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
export const FIXTURES = path.resolve(__dirname, '..', '..', 'fixtures', 'lang-resolution');
|
||||
export const CROSS_FILE_FIXTURES = path.resolve(
|
||||
|
||||
@@ -67,6 +67,27 @@ describe('extractParsedFile', () => {
|
||||
});
|
||||
|
||||
describe('provider HAS migrated', () => {
|
||||
it('returns undefined without warning for whitespace-only source', () => {
|
||||
const warnings: string[] = [];
|
||||
let called = false;
|
||||
const provider = fakeProvider({
|
||||
emitScopeCaptures: () => {
|
||||
called = true;
|
||||
throw new Error('should not inspect empty source');
|
||||
},
|
||||
});
|
||||
|
||||
for (const src of ['', ' \n\t']) {
|
||||
const result = extractParsedFile(provider, src, 'pkg/__init__.py', (msg) => {
|
||||
warnings.push(msg);
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
}
|
||||
|
||||
expect(called).toBe(false);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('threads emitScopeCaptures output through ScopeExtractor', () => {
|
||||
const provider = fakeProvider({
|
||||
emitScopeCaptures: () => [moduleScopeMatch()],
|
||||
|
||||
@@ -56,15 +56,23 @@ function findDef(file: ParsedFile, name: string) {
|
||||
|
||||
describe('Python scopes — module / class / function', () => {
|
||||
it('case 01: minimal module produces a single Module scope', () => {
|
||||
// Empty source produces a zero-range module node; the central
|
||||
// extractor treats zero-range scopes as malformed (and rightly so —
|
||||
// they collide with sibling-overlap detection on subsequent reparses).
|
||||
// Real Python files always have at least a newline.
|
||||
const f = parse('pass\n');
|
||||
expect(f.scopes).toHaveLength(1);
|
||||
expect(f.scopes[0]!.kind).toBe('Module');
|
||||
});
|
||||
|
||||
it('case 01a: empty and whitespace-only files are skipped without warnings', () => {
|
||||
for (const src of ['', ' \n\t']) {
|
||||
const warnings: string[] = [];
|
||||
const parsed = extractParsedFile(pythonProvider, src, 'pkg/__init__.py', (msg) => {
|
||||
warnings.push(msg);
|
||||
});
|
||||
|
||||
expect(parsed).toBeUndefined();
|
||||
expect(warnings).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('case 01b: large cache-miss files use the adaptive tree-sitter buffer', () => {
|
||||
const padding = 'x'.repeat(600 * 1024);
|
||||
const f = parse(`# ${padding}\ndef after_padding():\n return 1\n`);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
isLegacyResolverParityExpectedFailure,
|
||||
isLegacyResolverParityRun,
|
||||
resolverParityFlagName,
|
||||
} from '../../integration/resolvers/helpers.js';
|
||||
|
||||
const csharpNamespaceRootImportTest =
|
||||
'emits the using-import edge App/Program.cs -> Models/User.cs through the scope-resolution path';
|
||||
|
||||
describe('resolver parity expected legacy failures', () => {
|
||||
it('uses the same env var convention as the parity workflow', () => {
|
||||
expect(resolverParityFlagName('csharp')).toBe('REGISTRY_PRIMARY_CSHARP');
|
||||
expect(resolverParityFlagName('c-plus-plus')).toBe('REGISTRY_PRIMARY_C_PLUS_PLUS');
|
||||
});
|
||||
|
||||
it('recognizes only legacy parity runs', () => {
|
||||
expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: '0' })).toBe(true);
|
||||
expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: 'false' })).toBe(true);
|
||||
expect(isLegacyResolverParityRun('csharp', { REGISTRY_PRIMARY_CSHARP: '1' })).toBe(false);
|
||||
expect(isLegacyResolverParityRun('csharp', {})).toBe(false);
|
||||
});
|
||||
|
||||
it('matches configured expected failures only during the legacy run', () => {
|
||||
expect(
|
||||
isLegacyResolverParityExpectedFailure('csharp', csharpNamespaceRootImportTest, {
|
||||
REGISTRY_PRIMARY_CSHARP: '0',
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isLegacyResolverParityExpectedFailure('csharp', csharpNamespaceRootImportTest, {
|
||||
REGISTRY_PRIMARY_CSHARP: '1',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isLegacyResolverParityExpectedFailure(
|
||||
'csharp',
|
||||
'detects exactly 3 classes and 2 interfaces',
|
||||
{
|
||||
REGISTRY_PRIMARY_CSHARP: '0',
|
||||
},
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user