mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
fix(csharp): flag scanner-uncaptured namespaces incomplete; Unicode/@ matchers (#1908, Codex F3)
The line scanner treated its output as complete even when it missed valid
C# namespace forms, so the gate failed CLOSED and over-blocked legit
imports. Make CS_NAMESPACE_RE/CS_USING_STATIC_RE Unicode-aware (\p{L}\p{N}
+ u flag) and strip leading/segment @ so verbatim/Unicode identifiers are
captured to match the AST. For forms the regex still can't capture (split
across lines, not at line start, attributed), set a per-file 'incomplete'
flag; collectDeclaredNamespaces returns 'truncated' for such files so the
#1881 gate fails OPEN instead of dropping the namespace. High-precision
detectors + guard tests keep ordinary forms (incl. // namespace comments)
from tripping incomplete.
This commit is contained in:
parent
5c84f734ae
commit
8fd1ef9bfe
4 changed files with 185 additions and 8 deletions
|
|
@ -398,12 +398,17 @@ async function collectDeclaredNamespaces(
|
|||
} catch {
|
||||
return 'truncated'; // unreadable source → signal truncation (fail open)
|
||||
}
|
||||
for (const ns of scanner.result().namespaces) {
|
||||
const structure = scanner.result();
|
||||
for (const ns of structure.namespaces) {
|
||||
declaredNamespaces.add(ns);
|
||||
const dot = ns.indexOf('.');
|
||||
rootNamespaces.add(dot === -1 ? ns : ns.slice(0, dot));
|
||||
}
|
||||
return 'ok';
|
||||
// A declaration the scanner could not fully capture (Codex F3) means the
|
||||
// collected namespaces are an incomplete picture of this file — treat it like
|
||||
// a truncated read so the #1881 gate fails OPEN rather than over-block an
|
||||
// import whose namespace was dropped.
|
||||
return structure.incomplete ? 'truncated' : 'ok';
|
||||
}
|
||||
|
||||
export async function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftPackageConfig | null> {
|
||||
|
|
|
|||
|
|
@ -48,19 +48,62 @@ export interface CsharpFileStructure {
|
|||
/** Dotted paths from `using static X.Y.Z;` (including
|
||||
* `global using static` and aliased `using static A = X.Y.Z;`). */
|
||||
readonly usingStaticPaths: readonly string[];
|
||||
/** True when the scanner saw a `namespace` / `using static` declaration it
|
||||
* could not fully capture (keyword not at line start, split across lines, or
|
||||
* an unparseable identifier form). Callers feeding the #1881 gate must treat
|
||||
* this like a truncated scan and fail OPEN, since a dropped namespace would
|
||||
* otherwise over-block a legitimate import (Codex F3). Absent/false on a
|
||||
* cleanly-scanned file. */
|
||||
readonly incomplete?: boolean;
|
||||
}
|
||||
|
||||
// A dotted C# namespace identifier: each segment is an optional verbatim `@`
|
||||
// followed by a Unicode letter/`_` and Unicode letters/digits/`_`. The `u` flag
|
||||
// makes the classes Unicode-aware so `namespace Café.Models;` is captured (the
|
||||
// old ASCII `[A-Za-z…]` truncated it). The `@` markers are stripped from the
|
||||
// capture so it matches the tree-sitter AST's `name` text.
|
||||
const CS_NS_IDENT = String.raw`@?[\p{L}_][\p{L}\p{N}_]*(?:\.@?[\p{L}_][\p{L}\p{N}_]*)*`;
|
||||
|
||||
// Line-anchored matchers for the worker-path fallback (see
|
||||
// `extractCsharpStructureViaScanner`). Anchored at line start (after
|
||||
// indentation); the scanner additionally tracks block-comment / string
|
||||
// state across lines so a keyword at the start of a line inside one of
|
||||
// those regions is skipped.
|
||||
const CS_NAMESPACE_RE = /^[ \t]*namespace[ \t]+([A-Za-z_@][A-Za-z0-9_.]*)/;
|
||||
const CS_NAMESPACE_RE = new RegExp(String.raw`^[ \t]*namespace[ \t]+(${CS_NS_IDENT})`, 'u');
|
||||
// `global using static`, plain `using static`, and the aliased
|
||||
// `using static Alias = NS.Type;` form (the AST keeps the RHS path, so
|
||||
// the optional `Alias =` is skipped and only the dotted path captured).
|
||||
const CS_USING_STATIC_RE =
|
||||
/^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]+(?:[A-Za-z_@][A-Za-z0-9_]*[ \t]*=[ \t]*)?([A-Za-z_@][A-Za-z0-9_.]*)/;
|
||||
const CS_USING_STATIC_RE = new RegExp(
|
||||
String.raw`^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]+(?:@?[\p{L}_][\p{L}\p{N}_]*[ \t]*=[ \t]*)?(${CS_NS_IDENT})`,
|
||||
'u',
|
||||
);
|
||||
|
||||
// Incompleteness detectors — used ONLY when the precise matchers above failed,
|
||||
// to flag a declaration the scanner could not capture (so the file fails the
|
||||
// #1881 gate OPEN instead of silently dropping the namespace). Kept
|
||||
// high-precision so ordinary files never trip them (which would wrongly disable
|
||||
// the gate repo-wide):
|
||||
// - `…_BARE`: the keyword alone on a line (the name is on the next line).
|
||||
// - `…_AT_START`: a line-start declaration the precise matcher couldn't parse.
|
||||
// - `CS_NAMESPACE_AFTER_CODE`: a `namespace` keyword right after a `}`/`;`/`{`/`]`
|
||||
// (real code, NOT a `//` comment), i.e. not at line start.
|
||||
const CS_NAMESPACE_BARE = /^[ \t]*namespace[ \t]*\r?$/;
|
||||
const CS_USING_STATIC_BARE = /^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]*\r?$/;
|
||||
const CS_NAMESPACE_AT_START = /^[ \t]*namespace[ \t]+\S/;
|
||||
const CS_USING_STATIC_AT_START = /^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]+\S/;
|
||||
const CS_NAMESPACE_AFTER_CODE = /[}\];{][ \t]*namespace[ \t]+@?[\p{L}_]/u;
|
||||
|
||||
/** Whether a `code`-state line declares a namespace / using-static the precise
|
||||
* matchers could not capture — see the detectors above. */
|
||||
function looksLikeUncapturedDeclaration(line: string): boolean {
|
||||
return (
|
||||
CS_NAMESPACE_BARE.test(line) ||
|
||||
CS_USING_STATIC_BARE.test(line) ||
|
||||
CS_NAMESPACE_AT_START.test(line) ||
|
||||
CS_USING_STATIC_AT_START.test(line) ||
|
||||
CS_NAMESPACE_AFTER_CODE.test(line)
|
||||
);
|
||||
}
|
||||
|
||||
/** Multi-line lexical state carried line-to-line by the scanner. */
|
||||
type CsScanState = 'code' | 'block' | 'verbatim' | 'raw';
|
||||
|
|
@ -200,6 +243,7 @@ export interface CsharpStructureLineScanner {
|
|||
export function createCsharpStructureScanner(): CsharpStructureLineScanner {
|
||||
const namespaces: string[] = [];
|
||||
const usingStaticPaths: string[] = [];
|
||||
let incomplete = false;
|
||||
let state: CsScanState = 'code';
|
||||
let rawFence = 0;
|
||||
return {
|
||||
|
|
@ -209,16 +253,24 @@ export function createCsharpStructureScanner(): CsharpStructureLineScanner {
|
|||
if (state === 'code') {
|
||||
const ns = CS_NAMESPACE_RE.exec(line);
|
||||
if (ns !== null) {
|
||||
namespaces.push(ns[1]!);
|
||||
namespaces.push(ns[1]!.replace(/@/g, ''));
|
||||
} else {
|
||||
const us = CS_USING_STATIC_RE.exec(line);
|
||||
if (us !== null) usingStaticPaths.push(us[1]!);
|
||||
if (us !== null) {
|
||||
usingStaticPaths.push(us[1]!.replace(/@/g, ''));
|
||||
} else if (looksLikeUncapturedDeclaration(line)) {
|
||||
// A declaration the precise matchers couldn't capture → mark the
|
||||
// file incomplete so the #1881 gate fails OPEN (Codex F3).
|
||||
incomplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
[state, rawFence] = advanceCsScanState(line, state, rawFence);
|
||||
},
|
||||
result(): CsharpFileStructure {
|
||||
return { namespaces, usingStaticPaths };
|
||||
return incomplete
|
||||
? { namespaces, usingStaticPaths, incomplete }
|
||||
: { namespaces, usingStaticPaths };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,4 +96,84 @@ describe('extractCsharpStructureViaScanner', () => {
|
|||
const src = `/* header */ class C {}\nnamespace App.Real;`;
|
||||
expect(extractCsharpStructureViaScanner(src).namespaces).toEqual(['App.Real']);
|
||||
});
|
||||
|
||||
// --- Unicode / @-verbatim identifiers (Codex F3): these must be CAPTURED, not
|
||||
// truncated/dropped, so the #1881 gate doesn't over-block legitimate imports.
|
||||
it('captures a Unicode namespace identifier', () => {
|
||||
const out = extractCsharpStructureViaScanner('namespace Café.Modèles;');
|
||||
expect(out.namespaces).toEqual(['Café.Modèles']);
|
||||
expect(out.incomplete).toBeFalsy();
|
||||
});
|
||||
|
||||
it('captures a non-Latin (Greek) namespace identifier', () => {
|
||||
expect(extractCsharpStructureViaScanner('namespace Ωμέγα.Models;').namespaces).toEqual([
|
||||
'Ωμέγα.Models',
|
||||
]);
|
||||
});
|
||||
|
||||
it('strips a leading @ from a verbatim namespace identifier to match the AST', () => {
|
||||
expect(extractCsharpStructureViaScanner('namespace @namespace.Models;').namespaces).toEqual([
|
||||
'namespace.Models',
|
||||
]);
|
||||
});
|
||||
|
||||
it('strips a mid-path @ from a verbatim namespace segment', () => {
|
||||
expect(extractCsharpStructureViaScanner('namespace App.@class.Models;').namespaces).toEqual([
|
||||
'App.class.Models',
|
||||
]);
|
||||
});
|
||||
|
||||
// --- Forms the line scanner cannot capture must flag `incomplete` so the
|
||||
// caller fails the #1881 gate OPEN (Codex F3) instead of dropping the namespace.
|
||||
it('flags `incomplete` for a namespace declaration split across lines', () => {
|
||||
const out = extractCsharpStructureViaScanner('namespace\n App.Models;');
|
||||
expect(out.namespaces).toEqual([]);
|
||||
expect(out.incomplete).toBe(true);
|
||||
});
|
||||
|
||||
it('flags `incomplete` for a namespace keyword not at line start', () => {
|
||||
const out = extractCsharpStructureViaScanner('class C {} namespace App.Models;');
|
||||
expect(out.incomplete).toBe(true);
|
||||
});
|
||||
|
||||
it('flags `incomplete` for an attributed same-line namespace', () => {
|
||||
const out = extractCsharpStructureViaScanner('[Obsolete] namespace App.Legacy;');
|
||||
expect(out.incomplete).toBe(true);
|
||||
});
|
||||
|
||||
// --- Guards: ordinary / handled forms must NEVER set `incomplete`, or one
|
||||
// exotic line would wrongly disable the gate repo-wide.
|
||||
it('does NOT flag `incomplete` for ordinary handled forms', () => {
|
||||
for (const src of [
|
||||
'namespace App.Models;',
|
||||
'namespace App.Services\n{\n}',
|
||||
'namespace A.One {}\nnamespace A.Two {}',
|
||||
'using static System.Math;\nnamespace App;',
|
||||
'global using static App.Utils.Logger;',
|
||||
'using static M = App.Utils.MathUtils;',
|
||||
'using System.Collections.Generic;\nusing App.Models;',
|
||||
'\t\tnamespace App.Indented;',
|
||||
'public class Global {}',
|
||||
'',
|
||||
]) {
|
||||
expect(extractCsharpStructureViaScanner(src).incomplete).toBeFalsy();
|
||||
}
|
||||
});
|
||||
|
||||
it('does NOT flag `incomplete` for a `// namespace` line comment or a namespace mentioned after `//`', () => {
|
||||
expect(
|
||||
extractCsharpStructureViaScanner('// namespace Fake.Comment;\nnamespace App.Real;')
|
||||
.incomplete,
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
extractCsharpStructureViaScanner('public class C {} // namespace Foo').incomplete,
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does NOT flag `incomplete` for an identifier that merely starts with "namespace"', () => {
|
||||
// `namespaceManager` is an ordinary identifier, not the keyword.
|
||||
expect(
|
||||
extractCsharpStructureViaScanner('var namespaceManager = Get();').incomplete,
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -701,4 +701,44 @@ describe('loadCsharpResolutionConfig — one-pass namespace scan (#1881)', () =>
|
|||
await fsp.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('collects a Unicode namespace through the streamed scan, not truncated (Codex F3)', async () => {
|
||||
// The scanner is now Unicode-aware, so a non-ASCII namespace is captured
|
||||
// end-to-end instead of being dropped (which would over-block its imports).
|
||||
const root = await makeTempRepo({
|
||||
'App.csproj':
|
||||
'<Project><PropertyGroup><RootNamespace>MyApp</RootNamespace></PropertyGroup></Project>',
|
||||
'Models/Café.cs': 'namespace Café.App;\npublic class Modèle {}',
|
||||
});
|
||||
try {
|
||||
const config = await loadCsharpResolutionConfig(root);
|
||||
const ns = config.namespaces!;
|
||||
expect(ns.truncated).toBe(false);
|
||||
expect(ns.declaredNamespaces!.has('Café.App')).toBe(true);
|
||||
} finally {
|
||||
await fsp.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('marks the scan truncated when a file has an uncaptured namespace form, failing the gate OPEN (Codex F3)', async () => {
|
||||
// A namespace split across lines is not captured by the line scanner; the
|
||||
// scan must flag truncated so the dropped namespace fails the #1881 gate
|
||||
// OPEN rather than over-block an import declared in that file.
|
||||
const root = await makeTempRepo({
|
||||
'App.csproj':
|
||||
'<Project><PropertyGroup><RootNamespace>MyApp</RootNamespace></PropertyGroup></Project>',
|
||||
'Weird.cs': 'namespace\n MyApp.Weird;\npublic class W {}',
|
||||
});
|
||||
try {
|
||||
const config = await loadCsharpResolutionConfig(root);
|
||||
const ns = config.namespaces!;
|
||||
expect(ns.truncated).toBe(true);
|
||||
// A local-looking import under the dropped namespace fails open (and U1's
|
||||
// external-root denylist still keeps BCL roots blocked under truncation).
|
||||
expect(csharpSuffixFallbackAllowed('MyApp.Weird.Thing', ns)).toBe(true);
|
||||
expect(csharpSuffixFallbackAllowed('System.Threading.Tasks', ns)).toBe(false);
|
||||
} finally {
|
||||
await fsp.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue