Files
codegraph/__tests__/csharp-namespace-visibility.test.ts
Colby Mchenry a8b578f7d8 fix(csharp): a bare type name is one its namespaces can see (#2216)
A bare C# type name could mean any same-named project type. Newtonsoft's
`async Task` tests (`using System.Threading.Tasks;`) bound `Task` to a test
class `Task` in `Newtonsoft.Json.Tests.Schema` 433 times; AutoMapper's DTOs
in `OmmitedDTOModel3` extended `OmmitedDatabaseModel3`'s `BaseEntity`;
Newtonsoft's `BindingFlags` went to its portable-build polyfill.

A type in namespace N is now in reach of a file whose code runs in N or a
namespace inside it, or that imports N: its `using N;`, any file's
`global using N;`, the project files' `<Using Include="N" />`, the SDK's
implicit usings when `<ImplicitUsings>` is on (serilog's own
`System.TimeProvider` polyfill is seen through them), or an alias naming
the type (`using License = AutoMapper.Licensing.License;`). A type in the
global namespace is seen everywhere; qualified names are not judged.

Newtonsoft -529, AutoMapper -72, eShop -4; serilog, CleanArchitecture
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:02:05 +00:00

87 lines
2.4 KiB
TypeScript

/**
* A bare C# type name means a type its file can see: one in its own
* namespace or an enclosing one, or in a namespace it imports (`using`,
* `global using`, the project's `<Using>` / implicit usings), or the one an
* alias names. Newtonsoft's `async Task` tests (`using
* System.Threading.Tasks;`) bound `Task` to a test class of that name in
* `Newtonsoft.Json.Tests.Schema` 433 times.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cs-ns-'));
const files: Record<string, string> = {
'src/App.Tests/Schema/Generator.cs': `namespace App.Tests.Schema
{
public class Task
{
}
}
`,
'src/App.Tests/Reader/ReadTests.cs': `using System.Threading.Tasks;
namespace App.Tests.Reader
{
public class ReadTests
{
public async Task Read()
{
await System.Threading.Tasks.Task.Yield();
}
}
}
`,
'src/App.Tests/Schema/Planner.cs': `namespace App.Tests.Schema
{
public class Planner
{
public Task Next() { return new Task(); }
}
}
`,
'src/App.Tests/Other/Aliased.cs': `using Task = App.Tests.Schema.Task;
namespace App.Tests.Other
{
public class Aliased
{
public Task Make() { return new Task(); }
}
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const reachesTask = (file: string) => {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).some((e) => cg.getNode(e.target)!.qualifiedName === 'App.Tests.Schema::Task');
};
describe('C# type names and namespaces', () => {
it('a type another namespace does not import is not in reach', () => {
expect(reachesTask('src/App.Tests/Reader/ReadTests.cs')).toBe(false);
});
it('the same namespace, and an alias naming the type, reach it', () => {
expect(reachesTask('src/App.Tests/Schema/Planner.cs')).toBe(true);
expect(reachesTask('src/App.Tests/Other/Aliased.cs')).toBe(true);
});
});