GitNexus/gitnexus/test/integration/csharp-namespace-precedence-boundaries.test.ts
Abhigyan Patwari caacc3bac2
fix(csharp): rank global-namespace types below the file's own namespaces (#3655)
* fix(csharp): rank global-namespace types below the file's own namespaces

C# global-namespace types were stored in the scope-independent
workspaceFqnBindings channel. lookupNameClaim returns that channel at
every scope, while namespace siblings are consulted only at the module
scope. A type name looked up inside a method therefore stopped at the
first global type with that name. The walk never reached the file's own
or same-namespace declaration. On the merged lang-resolution corpus this
caused four field-chain drops in csharp-field-types and
csharp-deep-field-chain. It also bound calls to other fixtures' global
types. For example, csharp-hello's Demo.Greeter.Main called
csharp-optional-params' Greeter.

The C# hook now stores global types in namespaceFqnBindings under the
'' key. Each file's accessibility list follows C# name lookup: the
declared namespace, the using targets of that declaration, the enclosing
namespaces, then the global namespace. The compilation unit's using
targets come after it. A using after a file-scoped namespace belongs to
that namespace. Shared walker logic is unchanged (comments only); PHP's
FQN keys still use workspaceFqnBindings.

Corpus A/B against main: the four drops resolve and no drop is added.
Wrong cross-fixture edges are retargeted or removed. One global-namespace
fixture now types `this` as its own class and reaches a duplicated global
User. Receiver bench call drops 128 -> 125, all-kind 192 -> 188
(baseline and BASELINE.md updated; the old note that these sites
declined on ambiguity was wrong).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(csharp): initialize voucher fixture payee

* test(csharp): refresh initialized payee captures

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 06:35:08 +00:00

202 lines
7.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
describe('C# namespace lookup boundaries', () => {
it.each([
{
name: 'an earlier dotted namespace cannot override a later namespace',
files: {
'Mixed.cs': `namespace Billing.Reports { public class Empty {} }
namespace Audit { public class Checker { public void Check() { var v = new Voucher(); v.Post(); } } }`,
'Billing.cs': 'namespace Billing; public class Voucher { public void Post() {} }',
'Audit.cs': 'namespace Audit; public class Voucher { public void Post() {} }',
},
caller: 'Check',
member: 'Post',
target: 'Audit.cs',
},
{
name: 'a using inside one namespace cannot leak into a sibling namespace',
files: {
'Mixed.cs': `namespace Tools {
using Lib;
public class Empty {}
}
namespace Consumer { public class Runner { public void Run() { var t = new Token(); t.Go(); } } }`,
'Lib.cs': 'namespace Lib; public class Token { public void Go() {} }',
'Global.cs': 'public class Token { public void Go() {} }',
},
caller: 'Run',
member: 'Go',
target: 'Global.cs',
},
{
name: 'a bare type name cannot select a generic type with a different arity',
files: {
'Global.cs': 'public class ArityToken { public static void Apply() {} }',
'Client.cs': `namespace Client {
public class ArityToken<T> { public static void Apply() {} }
public class Runner { public void Run() { ArityToken.Apply(); } }
}`,
},
caller: 'Run',
member: 'Apply',
target: 'Global.cs',
},
{
name: 'a generic class keeps its own short name inside its body',
files: {
'Global.cs': 'public class ArityToken { public static void Apply() {} }',
'Client.cs': `namespace Client {
public class ArityToken<T> {
public static void Apply() {}
public static void Run() { ArityToken.Apply(); }
}
}`,
},
caller: 'Run',
member: 'Apply',
target: 'Client.cs',
},
{
name: 'an incompatible generic type cannot hide an enclosing zero-arity type',
files: {
'Billing.cs':
'namespace Billing; public class ArityToken { public static void Apply() {} }',
'Client.cs': `namespace Billing.Client {
public class ArityToken<T> { public static void Apply() {} }
public class Runner { public void Run() { ArityToken.Apply(); } }
}`,
},
caller: 'Run',
member: 'Apply',
target: 'Billing.cs',
},
{
name: 'a same-name alias to a closed generic remains a class receiver',
files: {
'Use.cs':
'using Result = Lib.Result<string>; public class Runner { public void Run() { Result.Apply(); } }',
'Lib.cs': 'namespace Lib; public class Result<T> { public static void Apply() {} }',
'Global.cs': 'public class Result { public static void Apply() {} }',
},
caller: 'Run',
member: 'Apply',
target: 'Lib.cs',
},
{
name: 'a nested block namespace uses its qualified identity',
files: {
'Use.cs': `namespace A {
namespace B {
public class Runner { public void Run() { var t = new Token(); t.Go(); } }
}
}`,
'AB.cs': 'namespace A.B; public class Token { public void Go() {} }',
'A.cs': 'namespace A; public class Token { public void Go() {} }',
'B.cs': 'namespace B; public class Token { public void Go() {} }',
},
caller: 'Run',
member: 'Go',
target: 'AB.cs',
},
{
name: 'a trailing semicolon does not make a block namespace file-scoped',
files: {
'Use.cs': `namespace Tools {
using Lib;
public class Empty {}
};
public class Runner { public void Run() { var t = new Token(); t.Go(); } }`,
'Lib.cs': 'namespace Lib; public class Token { public void Go() {} }',
'Global.cs': 'public class Token { public void Go() {} }',
},
caller: 'Run',
member: 'Go',
target: 'Global.cs',
},
{
name: 'a file-scoped namespace may put its semicolon on the next line',
files: {
'Use.cs': `namespace Actual
;
public class Runner { public void Run() { var t = new Token(); t.Go(); } }`,
'Actual.cs': 'namespace Actual; public class Token { public void Go() {} }',
'Global.cs': 'public class Token { public void Go() {} }',
},
caller: 'Run',
member: 'Go',
target: 'Actual.cs',
},
{
name: 'comment punctuation cannot classify a block namespace as file-scoped',
files: {
'Use.cs': `namespace Tools /* ; */
{
using Lib;
public class Empty {}
};
public class Runner { public void Run() { var t = new Token(); t.Go(); } }`,
'Lib.cs': 'namespace Lib; public class Token { public void Go() {} }',
'Global.cs': 'public class Token { public void Go() {} }',
},
caller: 'Run',
member: 'Go',
target: 'Global.cs',
},
])(
'$name',
async ({ files, caller, member, target }) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'csharp-lookup-boundary-'));
try {
for (const [name, content] of Object.entries(files)) {
await fs.writeFile(path.join(root, name), content);
}
const result = await runPipelineFromRepo(root, () => {}, {});
const targets = [...result.graph.iterRelationships()]
.filter(
(edge) =>
edge.type === 'CALLS' &&
result.graph.getNode(edge.sourceId)?.properties.name === caller &&
result.graph.getNode(edge.targetId)?.properties.name === member,
)
.map((edge) => result.graph.getNode(edge.targetId)?.properties.filePath);
expect(targets).toEqual([target]);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
},
120000,
);
it('ranks the global type before a compilation-unit using in type references', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'csharp-type-reference-tier-'));
try {
await fs.writeFile(
path.join(root, 'ABilling.cs'),
'namespace Billing; public class RefVoucher {}',
);
await fs.writeFile(path.join(root, 'ZGlobal.cs'), 'public class RefVoucher {}');
await fs.writeFile(
path.join(root, 'Use.cs'),
'using Billing;\npublic class Box<T> {}\npublic class Consumer { public Box<RefVoucher> field; }',
);
const result = await runPipelineFromRepo(root, () => {}, {});
const targets = [...result.graph.iterRelationships()]
.filter(
(edge) =>
edge.type === 'USES' &&
result.graph.getNode(edge.sourceId)?.properties.filePath === 'Use.cs' &&
result.graph.getNode(edge.targetId)?.properties.name === 'RefVoucher',
)
.map((edge) => result.graph.getNode(edge.targetId)?.properties.filePath);
expect(targets).not.toEqual([]);
expect(new Set(targets)).toEqual(new Set(['ZGlobal.cs']));
} finally {
await fs.rm(root, { recursive: true, force: true });
}
}, 120000);
});