mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
fix(csharp): gate the registry no-csproj direct-match path (#1908, Codex F2)
In the no-csproj branch of resolveCsharpImportTarget, resolveDirectMatch ran BEFORE the gate, so a path-aligned Legacy/System/Threading/Tasks.cs satisfied 'using System.Threading.Tasks;' even though System.* is not a declared in-repo namespace — while the legacy leg (gate-first) blocked it, so the legs were not equivalent. Run csharpSuffixFallbackAllowed first (return null on fail), then direct-match, then progressive stripping — mirroring the legacy ordering. Adds a no-csproj fixture with a deep path-aligned Tasks.cs and dual-leg integration describes (registry + forced-legacy), plus a path-aligned unit case. Parity 2/2.
This commit is contained in:
parent
dbfab7d4f8
commit
5c84f734ae
6 changed files with 169 additions and 5 deletions
|
|
@ -87,16 +87,25 @@ export function resolveCsharpImportTarget(
|
|||
// Namespace path: `System.Collections.Generic` → `System/Collections/Generic`.
|
||||
const pathLike = targetRaw.replace(/\./g, '/');
|
||||
|
||||
// Gate the WHOLE no-csproj path on declared in-repo namespaces — the direct
|
||||
// path/suffix match INCLUDED — so a BCL using can't resolve to a
|
||||
// coincidentally path-aligned local file (e.g. `Legacy/System/Threading/
|
||||
// Tasks.cs` satisfying `using System.Threading.Tasks;`). Running the gate
|
||||
// before `resolveDirectMatch` mirrors the legacy leg's gate-first ordering
|
||||
// (`import-resolvers/configs/csharp.ts`), so the two legs are equivalent
|
||||
// (#1881 parity, Codex F2). The gate keeps its fail-open for
|
||||
// undefined/truncated evidence, so legitimate edges in unscanned repos are
|
||||
// unaffected.
|
||||
if (!csharpSuffixFallbackAllowed(targetRaw, evidence)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Exact file / nested-suffix / namespace-dir direct-child match.
|
||||
const direct = resolveDirectMatch(ctx.allFilePaths, pathLike);
|
||||
if (direct !== null) return direct;
|
||||
|
||||
// Progressive prefix stripping — mirrors csproj's root-namespace mapping
|
||||
// without the csproj. Gated on declared in-repo namespaces so BCL usings
|
||||
// don't match a coincidentally-named local file (#1881).
|
||||
if (!csharpSuffixFallbackAllowed(targetRaw, evidence)) {
|
||||
return null;
|
||||
}
|
||||
// without the csproj.
|
||||
return resolveByProgressiveStripping(ctx.allFilePaths, pathLike);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
// On-disk path (Legacy/System/Threading/Tasks.cs) path-aligns with
|
||||
// `using System.Threading.Tasks;` but declares an UNRELATED in-repo namespace,
|
||||
// so the only way an IMPORTS edge forms is the coincidental path — which the
|
||||
// gate must block in the no-csproj path on BOTH legs (#1881, Codex F2).
|
||||
namespace MyApp.Legacy;
|
||||
|
||||
public class Tasks
|
||||
{
|
||||
public void Run() { }
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/csharp-spurious-edges-no-csproj/Models/User.cs
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/csharp-spurious-edges-no-csproj/Models/User.cs
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace MyApp.Models;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/csharp-spurious-edges-no-csproj/Services/OrderService.cs
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/csharp-spurious-edges-no-csproj/Services/OrderService.cs
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.Threading.Tasks;
|
||||
using MyApp.Models;
|
||||
|
||||
namespace MyApp.Services;
|
||||
|
||||
public class OrderService
|
||||
{
|
||||
public Task ProcessAsync()
|
||||
{
|
||||
var user = new User();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -2675,3 +2675,86 @@ describe('C# spurious import edges — legacy DAG leg (#1881, #8)', () => {
|
|||
expect(legit).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #1881 / Codex F2: in the NO-CSPROJ path the registry leg ran an ungated
|
||||
// direct-match before the gate, so a path-aligned `Legacy/System/Threading/
|
||||
// Tasks.cs` satisfied `using System.Threading.Tasks;`. Both legs must now block
|
||||
// it (gate-first), proving the legs are equivalent. Fixture ships NO .csproj.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('C# spurious import edges — no-csproj direct-match, registry leg (#1881, Codex F2)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Pin to the registry leg: only progressive stripping resolves a no-csproj
|
||||
// namespace import, so the legit-edge assertion below is registry-specific.
|
||||
// Pinning also keeps this deterministic under the parity matrix's legacy run.
|
||||
vi.stubEnv('REGISTRY_PRIMARY_CSHARP', '1');
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-spurious-edges-no-csproj'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('does not emit IMPORTS from System.Threading.Tasks to a path-aligned Legacy/System/Threading/Tasks.cs', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const spurious = imports.find(
|
||||
(e) =>
|
||||
e.sourceFilePath === 'Services/OrderService.cs' &&
|
||||
e.targetFilePath === 'Legacy/System/Threading/Tasks.cs',
|
||||
);
|
||||
expect(spurious).toBeUndefined();
|
||||
});
|
||||
|
||||
it('still emits the legitimate in-repo edge OrderService.cs -> Models/User.cs', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
expect(imports.length).toBeGreaterThan(0);
|
||||
const legit = imports.find(
|
||||
(e) =>
|
||||
e.sourceFilePath === 'Services/OrderService.cs' && e.targetFilePath === 'Models/User.cs',
|
||||
);
|
||||
expect(legit).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('C# spurious import edges — no-csproj direct-match, legacy DAG leg (#1881, Codex F2, #8)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubEnv('REGISTRY_PRIMARY_CSHARP', '0');
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'csharp-spurious-edges-no-csproj'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('does not emit IMPORTS from System.Threading.Tasks to a path-aligned Legacy/System/Threading/Tasks.cs', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const spurious = imports.find(
|
||||
(e) =>
|
||||
e.sourceFilePath === 'Services/OrderService.cs' &&
|
||||
e.targetFilePath === 'Legacy/System/Threading/Tasks.cs',
|
||||
);
|
||||
expect(spurious).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ingested the fixture so the absence of the spurious edge is meaningful (anti-vacuity)', () => {
|
||||
// The legacy DAG leg cannot resolve a no-csproj namespace import to a file
|
||||
// (`using MyApp.Models;` targets a directory of types — only the registry
|
||||
// leg's progressive stripping resolves it without a csproj RootNamespace, a
|
||||
// known registry-superiority gap). So the anti-vacuity guard here asserts
|
||||
// the three fixture files were ingested as graph nodes, proving the spurious
|
||||
// edge is absent because the gate blocked it — not because nothing parsed.
|
||||
const files = getNodesByLabel(result, 'File');
|
||||
expect(files.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -228,6 +228,49 @@ describe('resolveCsharpImportTarget — suffix match against .cs files', () => {
|
|||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('does not map a BCL using to a coincidentally PATH-ALIGNED local file via direct-match (#1881, Codex F2)', () => {
|
||||
// The no-csproj direct-match must be gated too: `Legacy/System/Threading/
|
||||
// Tasks.cs` path-aligns with `using System.Threading.Tasks;` and would
|
||||
// satisfy resolveDirectMatch's nested-suffix match — but System.* is not a
|
||||
// declared in-repo namespace, so the gate (now run FIRST) blocks it.
|
||||
const parsed: ParsedImport = {
|
||||
kind: 'namespace',
|
||||
localName: 'Tasks',
|
||||
importedName: 'System.Threading.Tasks',
|
||||
targetRaw: 'System.Threading.Tasks',
|
||||
};
|
||||
const result = resolveCsharpImportTarget(
|
||||
parsed,
|
||||
ctx(
|
||||
'Services/OrderService.cs',
|
||||
['Services/OrderService.cs', 'Legacy/System/Threading/Tasks.cs', 'Models/User.cs'],
|
||||
new Set(['MyApp.Services', 'MyApp.Legacy', 'MyApp.Models']),
|
||||
),
|
||||
);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('still resolves a legitimate in-repo using via direct-match when evidence is present (Codex F2 guard)', () => {
|
||||
// Gating the direct-match must NOT over-block a legitimate aligned import:
|
||||
// `using MyApp.Services;` aligns (exact declared) so the gate passes and the
|
||||
// namespace-dir direct-child match still resolves.
|
||||
const parsed: ParsedImport = {
|
||||
kind: 'namespace',
|
||||
localName: 'Services',
|
||||
importedName: 'MyApp.Services',
|
||||
targetRaw: 'MyApp.Services',
|
||||
};
|
||||
const result = resolveCsharpImportTarget(
|
||||
parsed,
|
||||
ctx(
|
||||
'MyApp/Program.cs',
|
||||
['MyApp/Program.cs', 'MyApp/Services/UserService.cs'],
|
||||
new Set(['MyApp.Services']),
|
||||
),
|
||||
);
|
||||
expect(result).toBe('MyApp/Services/UserService.cs');
|
||||
});
|
||||
|
||||
it('still resolves in-repo namespace imports via progressive stripping', () => {
|
||||
const parsed: ParsedImport = {
|
||||
kind: 'namespace',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue