fix(csharp): keep BCL/external roots gated through scan truncation (#1908, Codex F1)

A single scan truncation (unreadable dir/file, depth/dir cap) set one
repo-wide `truncated` flag that made csharpSuffixFallbackAllowed fail
open for EVERY import, silently re-enabling the #1881 BCL->local suffix
matches. Add a CSHARP_EXTERNAL_ROOTS denylist (System/Microsoft/...): an
external-rooted using that does not align with an in-repo declared
namespace stays BLOCKED even under truncation, while genuinely
local-looking usings still fail open. A repo that declares the root is
allowed via the alignment escape hatch. Shared predicate, so both legs
inherit it.
This commit is contained in:
Gergo Magyar 2026-05-30 07:27:18 +00:00
parent 61c0150a62
commit dbfab7d4f8
2 changed files with 91 additions and 7 deletions

View file

@ -11,20 +11,80 @@
import type { CSharpNamespaceEvidence } from './language-config.js';
/**
* Top-level namespace segments that clearly belong to the BCL / runtime / a
* ubiquitous third-party package — i.e. roots a normal repo does NOT declare.
* These stay gated even when the namespace scan is truncated, so a single
* unreadable file / capped subtree can't silently re-enable BCL→local suffix
* matches repo-wide (#1881). A repo that legitimately declares one of these
* roots is still allowed via the alignment escape hatch below.
*/
const CSHARP_EXTERNAL_ROOTS: ReadonlySet<string> = new Set([
// .NET BCL / runtime
'System',
'Microsoft',
'Windows',
'Mono',
// ubiquitous third-party NuGet roots
'Newtonsoft',
'Serilog',
'AutoMapper',
'MediatR',
'Polly',
'FluentValidation',
'Grpc',
'Google',
'Azure',
'Amazon',
'AWSSDK',
// common test frameworks
'Xunit',
'NUnit',
'Moq',
'FluentAssertions',
'NSubstitute',
'Shouldly',
]);
/** Whether `targetRaw`'s top-level segment is a clearly-external root. */
function isExternalRoot(targetRaw: string): boolean {
const dot = targetRaw.indexOf('.');
const top = dot === -1 ? targetRaw : targetRaw.slice(0, dot);
return CSHARP_EXTERNAL_ROOTS.has(top);
}
/**
* Whether the unanchored suffix fallback may run for `targetRaw`.
*
* Fails OPEN when the namespace scan was truncated (large repos must not
* silently lose legitimate edges, #1881 #11) and when no evidence was
* threaded at all (preserves legacy permissive behavior). Otherwise defers
* to {@link importAlignsWithDeclaredNamespaces}.
* threaded at all (preserves legacy permissive behavior). The truncation
* fail-open is carved out for clearly-external roots (BCL / well-known
* packages) that the repo does not declare, so one incomplete scan can't
* re-open the #1881 hole repo-wide. Otherwise defers to
* {@link importAlignsWithDeclaredNamespaces}.
*/
export function csharpSuffixFallbackAllowed(
targetRaw: string,
evidence: CSharpNamespaceEvidence | undefined,
): boolean {
if (evidence === undefined) return true;
if (evidence.truncated) return true;
if (evidence.truncated) {
// Keep clearly-external roots blocked through truncation UNLESS the repo
// actually declares an aligning namespace (the alignment check is the
// escape hatch — a repo that declares `namespace System;` still resolves).
if (
isExternalRoot(targetRaw) &&
!importAlignsWithDeclaredNamespaces(
targetRaw,
evidence.declaredNamespaces,
evidence.rootNamespaces,
)
) {
return false;
}
return true;
}
return importAlignsWithDeclaredNamespaces(
targetRaw,
evidence.declaredNamespaces,

View file

@ -467,16 +467,40 @@ describe('csharpSuffixFallbackAllowed — fail-open safety valves (#1881)', () =
expect(csharpSuffixFallbackAllowed('System.Threading.Tasks', undefined)).toBe(true);
});
it('fails OPEN (allows) when the namespace scan was truncated (#6)', () => {
// Same non-aligned import again: an incomplete (capped/unreadable) scan
// must not silently drop a legitimate edge, so truncation re-opens the
// fallback for every import.
it('keeps a clearly-external BCL root BLOCKED even when the scan was truncated (#1881, Codex F1)', () => {
// A single truncation must NOT silently re-enable BCL→local suffix matches
// repo-wide: System.* stays gated through truncation when the repo does not
// declare it. (This reverses the prior blanket-fail-open for external roots.)
expect(
csharpSuffixFallbackAllowed('System.Threading.Tasks', {
declaredNamespaces: declared,
rootNamespaces: roots,
truncated: true,
}),
).toBe(false);
});
it('fails OPEN for a genuinely local-looking import when the scan was truncated (#6)', () => {
// Non-external roots still fail open under truncation so an incomplete
// (capped/unreadable) scan does not silently drop a legitimate in-repo edge.
expect(
csharpSuffixFallbackAllowed('MyApp.Internal.Widget', {
declaredNamespaces: declared,
rootNamespaces: roots,
truncated: true,
}),
).toBe(true);
});
it('lets an external root fail OPEN through truncation when the repo declares it (escape hatch)', () => {
// If the repo actually declares the (normally-external) root, the alignment
// escape hatch allows the import even under truncation.
expect(
csharpSuffixFallbackAllowed('System.Threading.Tasks', {
declaredNamespaces: new Set(['System.Threading']),
rootNamespaces: new Set(['System']),
truncated: true,
}),
).toBe(true);
});
});