gitnexus 1.6.6-rc.95 → 1.6.6-rc.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ingestion/csharp-namespace-gate.d.ts +25 -0
- package/dist/core/ingestion/csharp-namespace-gate.js +129 -0
- package/dist/core/ingestion/import-resolvers/configs/csharp.js +25 -10
- package/dist/core/ingestion/import-resolvers/csharp.d.ts +6 -2
- package/dist/core/ingestion/import-resolvers/csharp.js +11 -2
- package/dist/core/ingestion/import-resolvers/types.d.ts +3 -1
- package/dist/core/ingestion/language-config.d.ts +40 -3
- package/dist/core/ingestion/language-config.js +232 -35
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +8 -4
- package/dist/core/ingestion/languages/csharp/import-target.js +128 -78
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +22 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +68 -33
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +14 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +15 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure predicates gating C# `using` suffix-fallback resolution so BCL usings
|
|
3
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
4
|
+
* file (#1881).
|
|
5
|
+
*
|
|
6
|
+
* Lives in the shared `ingestion/` layer — NOT under `languages/csharp/` — so
|
|
7
|
+
* BOTH the registry-primary scope resolver (`languages/csharp/import-target.ts`)
|
|
8
|
+
* and the legacy DAG resolver (`import-resolvers/csharp.ts`) can import it
|
|
9
|
+
* without an `import-resolvers/ -> languages/` dependency inversion (#5).
|
|
10
|
+
*/
|
|
11
|
+
import type { CSharpNamespaceEvidence } from './language-config.js';
|
|
12
|
+
/**
|
|
13
|
+
* Whether the unanchored suffix fallback may run for `targetRaw`.
|
|
14
|
+
*
|
|
15
|
+
* Fails OPEN when the namespace scan was truncated (large repos must not
|
|
16
|
+
* silently lose legitimate edges, #1881 #11) and when no evidence was
|
|
17
|
+
* threaded at all (preserves legacy permissive behavior). The truncation
|
|
18
|
+
* fail-open is carved out for clearly-external roots (BCL / well-known
|
|
19
|
+
* packages) that the repo does not declare, so one incomplete scan can't
|
|
20
|
+
* re-open the #1881 hole repo-wide. Otherwise defers to
|
|
21
|
+
* {@link importAlignsWithDeclaredNamespaces}.
|
|
22
|
+
*/
|
|
23
|
+
export declare function csharpSuffixFallbackAllowed(targetRaw: string, evidence: CSharpNamespaceEvidence | undefined): boolean;
|
|
24
|
+
/** True when `targetRaw` plausibly refers to a namespace declared in-repo. */
|
|
25
|
+
export declare function importAlignsWithDeclaredNamespaces(targetRaw: string, declaredNamespaces: ReadonlySet<string> | undefined, rootNamespaces?: ReadonlySet<string>): boolean;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure predicates gating C# `using` suffix-fallback resolution so BCL usings
|
|
3
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
4
|
+
* file (#1881).
|
|
5
|
+
*
|
|
6
|
+
* Lives in the shared `ingestion/` layer — NOT under `languages/csharp/` — so
|
|
7
|
+
* BOTH the registry-primary scope resolver (`languages/csharp/import-target.ts`)
|
|
8
|
+
* and the legacy DAG resolver (`import-resolvers/csharp.ts`) can import it
|
|
9
|
+
* without an `import-resolvers/ -> languages/` dependency inversion (#5).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Top-level namespace segments that clearly belong to the BCL / runtime / a
|
|
13
|
+
* ubiquitous third-party package — i.e. roots a normal repo does NOT declare.
|
|
14
|
+
* These stay gated even when the namespace scan is truncated, so a single
|
|
15
|
+
* unreadable file / capped subtree can't silently re-enable BCL→local suffix
|
|
16
|
+
* matches repo-wide (#1881). A repo that legitimately declares one of these
|
|
17
|
+
* roots is still allowed via the alignment escape hatch below.
|
|
18
|
+
*/
|
|
19
|
+
const CSHARP_EXTERNAL_ROOTS = new Set([
|
|
20
|
+
// .NET BCL / runtime
|
|
21
|
+
'System',
|
|
22
|
+
'Microsoft',
|
|
23
|
+
'Windows',
|
|
24
|
+
'Mono',
|
|
25
|
+
// ubiquitous third-party NuGet roots
|
|
26
|
+
'Newtonsoft',
|
|
27
|
+
'Serilog',
|
|
28
|
+
'AutoMapper',
|
|
29
|
+
'MediatR',
|
|
30
|
+
'Polly',
|
|
31
|
+
'FluentValidation',
|
|
32
|
+
'Grpc',
|
|
33
|
+
'Google',
|
|
34
|
+
'Azure',
|
|
35
|
+
'Amazon',
|
|
36
|
+
'AWSSDK',
|
|
37
|
+
// common test frameworks
|
|
38
|
+
'Xunit',
|
|
39
|
+
'NUnit',
|
|
40
|
+
'Moq',
|
|
41
|
+
'FluentAssertions',
|
|
42
|
+
'NSubstitute',
|
|
43
|
+
'Shouldly',
|
|
44
|
+
]);
|
|
45
|
+
/** Whether `targetRaw`'s top-level segment is a clearly-external root. */
|
|
46
|
+
function isExternalRoot(targetRaw) {
|
|
47
|
+
const dot = targetRaw.indexOf('.');
|
|
48
|
+
const top = dot === -1 ? targetRaw : targetRaw.slice(0, dot);
|
|
49
|
+
return CSHARP_EXTERNAL_ROOTS.has(top);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether the unanchored suffix fallback may run for `targetRaw`.
|
|
53
|
+
*
|
|
54
|
+
* Fails OPEN when the namespace scan was truncated (large repos must not
|
|
55
|
+
* silently lose legitimate edges, #1881 #11) and when no evidence was
|
|
56
|
+
* threaded at all (preserves legacy permissive behavior). The truncation
|
|
57
|
+
* fail-open is carved out for clearly-external roots (BCL / well-known
|
|
58
|
+
* packages) that the repo does not declare, so one incomplete scan can't
|
|
59
|
+
* re-open the #1881 hole repo-wide. Otherwise defers to
|
|
60
|
+
* {@link importAlignsWithDeclaredNamespaces}.
|
|
61
|
+
*/
|
|
62
|
+
export function csharpSuffixFallbackAllowed(targetRaw, evidence) {
|
|
63
|
+
if (evidence === undefined)
|
|
64
|
+
return true;
|
|
65
|
+
if (evidence.truncated) {
|
|
66
|
+
// Keep clearly-external roots blocked through truncation UNLESS the repo
|
|
67
|
+
// actually declares an aligning namespace (the alignment check is the
|
|
68
|
+
// escape hatch — a repo that declares `namespace System;` still resolves).
|
|
69
|
+
if (isExternalRoot(targetRaw) &&
|
|
70
|
+
!importAlignsWithDeclaredNamespaces(targetRaw, evidence.declaredNamespaces, evidence.rootNamespaces)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return importAlignsWithDeclaredNamespaces(targetRaw, evidence.declaredNamespaces, evidence.rootNamespaces);
|
|
76
|
+
}
|
|
77
|
+
/** True when `targetRaw` plausibly refers to a namespace declared in-repo. */
|
|
78
|
+
export function importAlignsWithDeclaredNamespaces(targetRaw, declaredNamespaces, rootNamespaces) {
|
|
79
|
+
if (declaredNamespaces === undefined || declaredNamespaces.size === 0)
|
|
80
|
+
return false;
|
|
81
|
+
// Exact: the import IS a declared in-repo namespace.
|
|
82
|
+
if (declaredNamespaces.has(targetRaw))
|
|
83
|
+
return true;
|
|
84
|
+
// Child-of: the import's IMMEDIATE parent namespace is declared in-repo.
|
|
85
|
+
// Anchoring on the direct parent — not "any declared prefix" — is what stops
|
|
86
|
+
// a declared BCL prefix from green-lighting an unrelated BCL using: a repo
|
|
87
|
+
// that declares `namespace System;` must NOT make `using
|
|
88
|
+
// System.Threading.Tasks;` resolve to a coincidental local `Tasks.cs`,
|
|
89
|
+
// because the import's parent `System.Threading` is not itself declared
|
|
90
|
+
// (#1881). The case this still allows is a type / `using static` import under
|
|
91
|
+
// a declared namespace laid out without its full path on disk, e.g.
|
|
92
|
+
// `using static MyApp.Utils.Logger;` when `MyApp.Utils` is declared.
|
|
93
|
+
const lastDot = targetRaw.lastIndexOf('.');
|
|
94
|
+
if (lastDot > 0 && declaredNamespaces.has(targetRaw.slice(0, lastDot)))
|
|
95
|
+
return true;
|
|
96
|
+
// Ancestor-of: the import is a strict prefix of some declared namespace
|
|
97
|
+
// (e.g. `using MyApp;` when `MyApp.Models` is declared). Only honored when
|
|
98
|
+
// the import also sits at or above an in-repo root namespace, so a BCL prefix
|
|
99
|
+
// can't qualify merely because a file declares something deeper under it
|
|
100
|
+
// (e.g. `System.Threading.Tasks.Extensions`) (#1881).
|
|
101
|
+
const childPrefix = targetRaw + '.';
|
|
102
|
+
for (const ns of declaredNamespaces) {
|
|
103
|
+
if (ns.startsWith(childPrefix)) {
|
|
104
|
+
return isAtOrAboveInRepoRoot(targetRaw, declaredNamespaces, rootNamespaces);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function isAtOrAboveInRepoRoot(targetRaw, declaredNamespaces, rootNamespaces) {
|
|
110
|
+
const descendantPrefix = targetRaw + '.';
|
|
111
|
+
if (rootNamespaces !== undefined && rootNamespaces.size > 0) {
|
|
112
|
+
for (const root of rootNamespaces) {
|
|
113
|
+
// targetRaw equals a root, or is an ancestor of one (e.g. `using MyApp;`
|
|
114
|
+
// for csproj RootNamespace `MyApp.Core`).
|
|
115
|
+
if (root === targetRaw || root.startsWith(descendantPrefix))
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
// No explicit roots (e.g. no csproj): treat the top-level segment of each
|
|
121
|
+
// declared namespace as the implied root.
|
|
122
|
+
for (const ns of declaredNamespaces) {
|
|
123
|
+
const dot = ns.indexOf('.');
|
|
124
|
+
const top = dot === -1 ? ns : ns.slice(0, dot);
|
|
125
|
+
if (top === targetRaw)
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
@@ -5,21 +5,36 @@
|
|
|
5
5
|
import { SupportedLanguages } from '../../../../_shared/index.js';
|
|
6
6
|
import { createStandardStrategy } from '../standard.js';
|
|
7
7
|
import { resolveCSharpImportInternal, resolveCSharpNamespaceDir } from '../csharp.js';
|
|
8
|
+
import { csharpSuffixFallbackAllowed } from '../../csharp-namespace-gate.js';
|
|
8
9
|
/** C# namespace-based resolution strategy via .csproj configs. */
|
|
9
10
|
export const csharpNamespaceStrategy = (rawImportPath, _filePath, ctx) => {
|
|
10
11
|
const csharpConfigs = ctx.configs.csharpConfigs;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
const evidence = ctx.configs.csharpNamespaces;
|
|
13
|
+
if (csharpConfigs.length === 0) {
|
|
14
|
+
// No csproj → there's no namespace→directory mapping to apply, so the
|
|
15
|
+
// generic strategy would normally take over. But that generic suffix match
|
|
16
|
+
// is UNGATED: it re-introduces the BCL→local spurious match the #1881 gate
|
|
17
|
+
// exists to stop. Mirror the registry leg's no-csproj path — defer to the
|
|
18
|
+
// generic strategy ONLY for imports that align with an in-repo declared
|
|
19
|
+
// namespace; for everything else (BCL usings) return an authoritative empty
|
|
20
|
+
// result that STOPS the chain (#2 parity). With no evidence threaded the
|
|
21
|
+
// gate fails open, so behavior is unchanged when the scan didn't run.
|
|
22
|
+
if (!csharpSuffixFallbackAllowed(rawImportPath, evidence)) {
|
|
23
|
+
return { kind: 'files', files: [] };
|
|
18
24
|
}
|
|
19
|
-
|
|
20
|
-
return { kind: 'files', files: resolvedFiles };
|
|
25
|
+
return null;
|
|
21
26
|
}
|
|
22
|
-
|
|
27
|
+
const resolvedFiles = resolveCSharpImportInternal(rawImportPath, csharpConfigs, ctx.normalizedFileList, ctx.allFileList, ctx.index, evidence);
|
|
28
|
+
if (resolvedFiles.length > 1) {
|
|
29
|
+
const dirSuffix = resolveCSharpNamespaceDir(rawImportPath, csharpConfigs);
|
|
30
|
+
if (dirSuffix) {
|
|
31
|
+
return { kind: 'package', files: resolvedFiles, dirSuffix };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Authoritative once csproj configs exist: return even an empty result to
|
|
35
|
+
// STOP the chain, so the generic suffix fallback can't re-introduce the
|
|
36
|
+
// gated BCL→local match this resolver just suppressed (#1881).
|
|
37
|
+
return { kind: 'files', files: resolvedFiles };
|
|
23
38
|
};
|
|
24
39
|
export const csharpImportConfig = {
|
|
25
40
|
language: SupportedLanguages.CSharp,
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* This file contains shared helpers for namespace-based resolution.
|
|
6
6
|
*/
|
|
7
7
|
import type { SuffixIndex } from './utils.js';
|
|
8
|
-
import type { CSharpProjectConfig } from '../language-config.js';
|
|
8
|
+
import type { CSharpProjectConfig, CSharpNamespaceEvidence } from '../language-config.js';
|
|
9
9
|
/**
|
|
10
10
|
* Resolve a C# using-directive import path to matching .cs files (low-level helper).
|
|
11
11
|
* Tries single-file match first, then directory match for namespace imports.
|
|
12
|
+
*
|
|
13
|
+
* The final unanchored suffix fallback is gated on `evidence` so BCL usings
|
|
14
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
15
|
+
* file (#1881). When `evidence` is omitted the fallback stays permissive.
|
|
12
16
|
*/
|
|
13
|
-
export declare function resolveCSharpImportInternal(importPath: string, csharpConfigs: CSharpProjectConfig[], normalizedFileList: string[], allFileList: string[], index?: SuffixIndex): string[];
|
|
17
|
+
export declare function resolveCSharpImportInternal(importPath: string, csharpConfigs: CSharpProjectConfig[], normalizedFileList: string[], allFileList: string[], index?: SuffixIndex, evidence?: CSharpNamespaceEvidence): string[];
|
|
14
18
|
/**
|
|
15
19
|
* Compute the directory suffix for a C# namespace import (for PackageMap).
|
|
16
20
|
* Returns a suffix like "/ProjectDir/Models/" or null if no config matches.
|
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
* This file contains shared helpers for namespace-based resolution.
|
|
6
6
|
*/
|
|
7
7
|
import { suffixResolve } from './utils.js';
|
|
8
|
+
import { csharpSuffixFallbackAllowed } from '../csharp-namespace-gate.js';
|
|
8
9
|
/**
|
|
9
10
|
* Resolve a C# using-directive import path to matching .cs files (low-level helper).
|
|
10
11
|
* Tries single-file match first, then directory match for namespace imports.
|
|
12
|
+
*
|
|
13
|
+
* The final unanchored suffix fallback is gated on `evidence` so BCL usings
|
|
14
|
+
* (e.g. `System.Threading.Tasks`) can't match a coincidentally-named local
|
|
15
|
+
* file (#1881). When `evidence` is omitted the fallback stays permissive.
|
|
11
16
|
*/
|
|
12
|
-
export function resolveCSharpImportInternal(importPath, csharpConfigs, normalizedFileList, allFileList, index) {
|
|
17
|
+
export function resolveCSharpImportInternal(importPath, csharpConfigs, normalizedFileList, allFileList, index, evidence) {
|
|
13
18
|
const namespacePath = importPath.replace(/\./g, '/');
|
|
14
19
|
const results = [];
|
|
15
20
|
for (const config of csharpConfigs) {
|
|
@@ -79,7 +84,11 @@ export function resolveCSharpImportInternal(importPath, csharpConfigs, normalize
|
|
|
79
84
|
return results;
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
// Fallback: suffix matching without namespace stripping (single file)
|
|
87
|
+
// Fallback: suffix matching without namespace stripping (single file).
|
|
88
|
+
// Gated on in-repo declared-namespace evidence (#1881).
|
|
89
|
+
if (!csharpSuffixFallbackAllowed(importPath, evidence)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
83
92
|
const pathParts = namespacePath.split('/').filter(Boolean);
|
|
84
93
|
const fallback = suffixResolve(pathParts, normalizedFileList, allFileList, index);
|
|
85
94
|
return fallback ? [fallback] : [];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Extracted from import-resolution.ts to co-locate types with their consumers.
|
|
5
5
|
*/
|
|
6
|
-
import type { TsconfigPaths, GoModuleConfig, CSharpProjectConfig, ComposerConfig } from '../language-config.js';
|
|
6
|
+
import type { TsconfigPaths, GoModuleConfig, CSharpProjectConfig, CSharpNamespaceEvidence, ComposerConfig } from '../language-config.js';
|
|
7
7
|
import type { SwiftPackageConfig } from '../language-config.js';
|
|
8
8
|
import type { SuffixIndex } from './utils.js';
|
|
9
9
|
import type { SupportedLanguages } from '../../../_shared/index.js';
|
|
@@ -28,6 +28,8 @@ export interface ImportConfigs {
|
|
|
28
28
|
composerConfig: ComposerConfig | null;
|
|
29
29
|
swiftPackageConfig: SwiftPackageConfig | null;
|
|
30
30
|
csharpConfigs: CSharpProjectConfig[];
|
|
31
|
+
/** In-repo namespace evidence gating C# suffix-fallback resolution (#1881). */
|
|
32
|
+
csharpNamespaces?: CSharpNamespaceEvidence;
|
|
31
33
|
}
|
|
32
34
|
/** Pre-built lookup structures for import resolution. Build once, reuse across chunks. */
|
|
33
35
|
export interface ImportResolutionContext {
|
|
@@ -26,6 +26,35 @@ export interface CSharpProjectConfig {
|
|
|
26
26
|
/** Directory containing the .csproj file */
|
|
27
27
|
projectDir: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Declared-namespace evidence used to gate C# suffix-fallback resolution so
|
|
31
|
+
* BCL usings (e.g. `System.Threading.Tasks`) can't match a coincidentally-
|
|
32
|
+
* named local file (#1881).
|
|
33
|
+
*/
|
|
34
|
+
export interface CSharpNamespaceEvidence {
|
|
35
|
+
/** Every `namespace X.Y` declared in-repo (scan may be capped — see `truncated`). */
|
|
36
|
+
readonly declaredNamespaces?: ReadonlySet<string>;
|
|
37
|
+
/** csproj RootNamespace values plus the top-level segment of each declared
|
|
38
|
+
* namespace — the anchor set for the parent-namespace gate direction. */
|
|
39
|
+
readonly rootNamespaces?: ReadonlySet<string>;
|
|
40
|
+
/** True when the BFS hit its dir/depth cap, so the namespace set may be
|
|
41
|
+
* incomplete; the gate fails open (allows) in that case. */
|
|
42
|
+
readonly truncated?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** Result of a single BFS over a repo collecting both csproj configs and
|
|
45
|
+
* declared `.cs` namespaces (one disk traversal — see `scanCSharpProject`). */
|
|
46
|
+
export interface CSharpProjectScan {
|
|
47
|
+
readonly configs: CSharpProjectConfig[];
|
|
48
|
+
readonly declaredNamespaces: ReadonlySet<string>;
|
|
49
|
+
readonly rootNamespaces: ReadonlySet<string>;
|
|
50
|
+
readonly truncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Project the one-pass {@link CSharpProjectScan} into the
|
|
53
|
+
* {@link CSharpNamespaceEvidence} both import-resolution legs thread to the
|
|
54
|
+
* #1881 gate — one shape, two carriers (`ImportConfigs.csharpNamespaces` for
|
|
55
|
+
* the legacy DAG, `CsharpResolutionConfig.namespaces` for the scope resolver).
|
|
56
|
+
* Keeps the field mapping in one place so the two carriers can't drift. */
|
|
57
|
+
export declare function csharpScanToEvidence(scan: CSharpProjectScan): CSharpNamespaceEvidence;
|
|
29
58
|
/** Swift Package Manager module config */
|
|
30
59
|
export interface SwiftPackageConfig {
|
|
31
60
|
/** Map of target name -> source directory path (e.g., "SiuperModel" -> "Package/Sources/SiuperModel") */
|
|
@@ -43,10 +72,18 @@ export declare function loadGoModulePath(repoRoot: string): Promise<GoModuleConf
|
|
|
43
72
|
/** Parse composer.json to extract PSR-4 autoload mappings (including autoload-dev). */
|
|
44
73
|
export declare function loadComposerConfig(repoRoot: string): Promise<ComposerConfig | null>;
|
|
45
74
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
75
|
+
* Single BFS over a repo that collects BOTH .csproj configs and the set of
|
|
76
|
+
* `namespace` declarations from `.cs` files.
|
|
77
|
+
*
|
|
78
|
+
* The csproj walk is cheap (a handful of project files); the namespace scan
|
|
79
|
+
* is NOT — it opens and reads every `.cs` file in the repo to collect its
|
|
80
|
+
* `namespace` declarations. That `.cs` read cost is the price of the #1881
|
|
81
|
+
* gate, not a saving: collapsing the csproj and namespace walks into one BFS
|
|
82
|
+
* avoids a second directory traversal, but the per-file `.cs` reads are new
|
|
83
|
+
* work this scan introduces. Reads within a directory are issued in bounded
|
|
84
|
+
* windows (see below); directories are still visited breadth-first.
|
|
48
85
|
*/
|
|
49
|
-
export declare function
|
|
86
|
+
export declare function scanCSharpProject(repoRoot: string): Promise<CSharpProjectScan>;
|
|
50
87
|
export declare function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftPackageConfig | null>;
|
|
51
88
|
/** Load all language-specific configs once for an ingestion run. */
|
|
52
89
|
export declare function loadImportConfigs(repoRoot: string): Promise<ImportConfigs>;
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
+
import { createReadStream } from 'fs';
|
|
3
|
+
import { createInterface } from 'readline';
|
|
2
4
|
import path from 'path';
|
|
3
5
|
import { isDev } from './utils/env.js';
|
|
4
6
|
import { logger } from '../logger.js';
|
|
7
|
+
/** Project the one-pass {@link CSharpProjectScan} into the
|
|
8
|
+
* {@link CSharpNamespaceEvidence} both import-resolution legs thread to the
|
|
9
|
+
* #1881 gate — one shape, two carriers (`ImportConfigs.csharpNamespaces` for
|
|
10
|
+
* the legacy DAG, `CsharpResolutionConfig.namespaces` for the scope resolver).
|
|
11
|
+
* Keeps the field mapping in one place so the two carriers can't drift. */
|
|
12
|
+
export function csharpScanToEvidence(scan) {
|
|
13
|
+
return {
|
|
14
|
+
declaredNamespaces: scan.declaredNamespaces,
|
|
15
|
+
rootNamespaces: scan.rootNamespaces,
|
|
16
|
+
truncated: scan.truncated,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
5
19
|
// ============================================================================
|
|
6
20
|
// LANGUAGE-SPECIFIC CONFIG LOADERS
|
|
7
21
|
// ============================================================================
|
|
@@ -89,55 +103,236 @@ export async function loadComposerConfig(repoRoot) {
|
|
|
89
103
|
return null;
|
|
90
104
|
}
|
|
91
105
|
}
|
|
106
|
+
// BFS bounds shared by the C# project/namespace scan. Sized to comfortably
|
|
107
|
+
// exceed normal C# repos so `truncated` stays the rare exception it was meant
|
|
108
|
+
// to be: a too-low cap trips `truncated=true` on ordinary repos, which makes
|
|
109
|
+
// `csharpSuffixFallbackAllowed` fail OPEN for every import and silently
|
|
110
|
+
// disables the #1881 gate. Truncation remains the safety valve for genuinely
|
|
111
|
+
// pathological trees (deep generated output, huge monorepos).
|
|
112
|
+
const CSHARP_SCAN_MAX_DEPTH = 24;
|
|
113
|
+
const CSHARP_SCAN_MAX_DIRS = 20000;
|
|
114
|
+
// Bound on in-flight file reads per directory so a directory with thousands of
|
|
115
|
+
// `.cs` files can't exhaust file descriptors / spike memory. Mirrors the
|
|
116
|
+
// Phase-1 walker's `READ_CONCURRENCY` (see `filesystem-walker.ts`).
|
|
117
|
+
const CSHARP_SCAN_READ_CONCURRENCY = 32;
|
|
118
|
+
const CSHARP_SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'bin', 'obj']);
|
|
119
|
+
const CSHARP_ROOT_NAMESPACE_RE = /<RootNamespace>\s*([^<]+)\s*<\/RootNamespace>/;
|
|
120
|
+
// Declared `namespace` names are extracted with the comment/string-aware
|
|
121
|
+
// scanner shared with the scope-resolution namespace-siblings pass
|
|
122
|
+
// (`extractCsharpStructureViaScanner`), not a bare regex: a regex matches
|
|
123
|
+
// `namespace` inside comments and string literals, seeding the #1881 gate
|
|
124
|
+
// with phantom namespaces. Imported lazily (and memoized) so the always-on
|
|
125
|
+
// `loadImportConfigs` path — every repo, every language — doesn't eagerly
|
|
126
|
+
// pull tree-sitter-c-sharp in via `namespace-siblings.ts` → `query.ts`.
|
|
127
|
+
let csharpScannerFactoryPromise;
|
|
128
|
+
function getCsharpStructureScannerFactory() {
|
|
129
|
+
if (csharpScannerFactoryPromise === undefined) {
|
|
130
|
+
csharpScannerFactoryPromise = import('./languages/csharp/namespace-siblings.js').then((mod) => mod.createCsharpStructureScanner);
|
|
131
|
+
}
|
|
132
|
+
return csharpScannerFactoryPromise;
|
|
133
|
+
}
|
|
92
134
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
135
|
+
* Single BFS over a repo that collects BOTH .csproj configs and the set of
|
|
136
|
+
* `namespace` declarations from `.cs` files.
|
|
137
|
+
*
|
|
138
|
+
* The csproj walk is cheap (a handful of project files); the namespace scan
|
|
139
|
+
* is NOT — it opens and reads every `.cs` file in the repo to collect its
|
|
140
|
+
* `namespace` declarations. That `.cs` read cost is the price of the #1881
|
|
141
|
+
* gate, not a saving: collapsing the csproj and namespace walks into one BFS
|
|
142
|
+
* avoids a second directory traversal, but the per-file `.cs` reads are new
|
|
143
|
+
* work this scan introduces. Reads within a directory are issued in bounded
|
|
144
|
+
* windows (see below); directories are still visited breadth-first.
|
|
95
145
|
*/
|
|
96
|
-
export async function
|
|
146
|
+
export async function scanCSharpProject(repoRoot) {
|
|
97
147
|
const configs = [];
|
|
98
|
-
|
|
148
|
+
const declaredNamespaces = new Set();
|
|
149
|
+
const rootNamespaces = new Set();
|
|
99
150
|
const scanQueue = [{ dir: repoRoot, depth: 0 }];
|
|
100
|
-
const maxDepth = 5;
|
|
101
|
-
const maxDirs = 100;
|
|
102
151
|
let dirsScanned = 0;
|
|
103
|
-
|
|
152
|
+
let truncated = false;
|
|
153
|
+
while (scanQueue.length > 0) {
|
|
154
|
+
if (dirsScanned >= CSHARP_SCAN_MAX_DIRS) {
|
|
155
|
+
truncated = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
104
158
|
const { dir, depth } = scanQueue.shift();
|
|
105
159
|
dirsScanned++;
|
|
160
|
+
let entries;
|
|
106
161
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Unreadable directory → its `.cs` namespaces are missed, so the scan is
|
|
166
|
+
// incomplete. Mark truncated so the #1881 gate fails OPEN (allows the
|
|
167
|
+
// suffix fallback) rather than wrongly blocking an import whose declaring
|
|
168
|
+
// namespace lived in the unread subtree (#5).
|
|
169
|
+
truncated = true;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
// Collect read targets, then issue them in bounded windows (rather than all
|
|
173
|
+
// at once) so a directory with thousands of `.cs` files can't exhaust file
|
|
174
|
+
// descriptors / spike memory. csproj reads keep entry order (config
|
|
175
|
+
// precedence matters); `.cs` namespace results land in shared Sets where
|
|
176
|
+
// order is irrelevant.
|
|
177
|
+
const csprojNames = [];
|
|
178
|
+
const csNames = [];
|
|
179
|
+
for (const entry of entries) {
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
if (CSHARP_SCAN_SKIP_DIRS.has(entry.name))
|
|
182
|
+
continue;
|
|
183
|
+
if (depth < CSHARP_SCAN_MAX_DEPTH) {
|
|
116
184
|
scanQueue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
117
185
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const csprojPath = path.join(dir, entry.name);
|
|
121
|
-
const content = await fs.readFile(csprojPath, 'utf-8');
|
|
122
|
-
const nsMatch = content.match(/<RootNamespace>\s*([^<]+)\s*<\/RootNamespace>/);
|
|
123
|
-
const rootNamespace = nsMatch ? nsMatch[1].trim() : entry.name.replace(/\.csproj$/, '');
|
|
124
|
-
const projectDir = path.relative(repoRoot, dir).replace(/\\/g, '/');
|
|
125
|
-
configs.push({ rootNamespace, projectDir });
|
|
126
|
-
if (isDev) {
|
|
127
|
-
logger.info(`📦 Loaded C# project: ${entry.name} (namespace: ${rootNamespace}, dir: ${projectDir})`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// Can't read .csproj
|
|
132
|
-
}
|
|
186
|
+
else {
|
|
187
|
+
truncated = true; // a real subtree was pruned at the depth cap
|
|
133
188
|
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!entry.isFile())
|
|
192
|
+
continue;
|
|
193
|
+
if (entry.name.endsWith('.csproj')) {
|
|
194
|
+
csprojNames.push(entry.name);
|
|
195
|
+
}
|
|
196
|
+
else if (entry.name.endsWith('.cs')) {
|
|
197
|
+
csNames.push(entry.name);
|
|
134
198
|
}
|
|
135
199
|
}
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
for (let i = 0; i < csprojNames.length; i += CSHARP_SCAN_READ_CONCURRENCY) {
|
|
201
|
+
const batch = csprojNames.slice(i, i + CSHARP_SCAN_READ_CONCURRENCY);
|
|
202
|
+
const settled = await Promise.allSettled(batch.map((name) => readCsprojConfig(path.join(dir, name), name, repoRoot, dir)));
|
|
203
|
+
for (const r of settled) {
|
|
204
|
+
const config = r.status === 'fulfilled' ? r.value : null;
|
|
205
|
+
if (config) {
|
|
206
|
+
configs.push(config);
|
|
207
|
+
rootNamespaces.add(config.rootNamespace);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (let i = 0; i < csNames.length; i += CSHARP_SCAN_READ_CONCURRENCY) {
|
|
212
|
+
const batch = csNames.slice(i, i + CSHARP_SCAN_READ_CONCURRENCY);
|
|
213
|
+
const settled = await Promise.allSettled(batch.map((name) => collectDeclaredNamespaces(path.join(dir, name), declaredNamespaces, rootNamespaces)));
|
|
214
|
+
// A `.cs` that was unreadable (or whose read/scan unexpectedly rejected)
|
|
215
|
+
// leaves its namespaces uncollected → mark truncated to fail the #1881
|
|
216
|
+
// gate OPEN rather than wrongly suppress an import. The scan streams each
|
|
217
|
+
// file, so file size no longer trips truncation.
|
|
218
|
+
for (const r of settled) {
|
|
219
|
+
if (r.status !== 'fulfilled' || r.value === 'truncated')
|
|
220
|
+
truncated = true;
|
|
221
|
+
}
|
|
138
222
|
}
|
|
139
223
|
}
|
|
140
|
-
|
|
224
|
+
if (truncated) {
|
|
225
|
+
// Surface the fail-open so an incomplete scan (dir/depth cap, or an
|
|
226
|
+
// unreadable directory or `.cs` file) silently disabling the #1881 gate
|
|
227
|
+
// repo-wide is observable (#4) rather than a mystery edge regression.
|
|
228
|
+
logger.warn(`[csharp] namespace scan of ${repoRoot} truncated (dir cap ${CSHARP_SCAN_MAX_DIRS}, depth cap ${CSHARP_SCAN_MAX_DEPTH}, an unreadable directory, or an unreadable .cs file); the #1881 suffix-fallback gate fails open for unmatched usings`);
|
|
229
|
+
}
|
|
230
|
+
return { configs, declaredNamespaces, rootNamespaces, truncated };
|
|
231
|
+
}
|
|
232
|
+
// Generous soft budget for locating `<RootNamespace>`: a real .csproj declares
|
|
233
|
+
// it in the first PropertyGroup near the top, so this is only reached by a
|
|
234
|
+
// pathological project file with a huge leading ItemGroup and no early
|
|
235
|
+
// RootNamespace. On hit we OMIT the config rather than guess a root (Codex F4).
|
|
236
|
+
const CSPROJ_ROOT_SCAN_MAX_BYTES = 4 * 1024 * 1024;
|
|
237
|
+
// Overlap kept across stream chunks so a `<RootNamespace>` tag straddling a
|
|
238
|
+
// chunk boundary is still matched (the tag + a short namespace value fit well
|
|
239
|
+
// within this window).
|
|
240
|
+
const CSPROJ_TAG_OVERLAP = 512;
|
|
241
|
+
/**
|
|
242
|
+
* Stream a `.csproj` just far enough to find `<RootNamespace>`, in constant
|
|
243
|
+
* memory and without a stat-then-read filesystem race. Returns the namespace
|
|
244
|
+
* when found; otherwise `rootNamespace: null` with `capHit` distinguishing a
|
|
245
|
+
* genuine read-to-EOF absence (`false`) from "not found within the soft budget"
|
|
246
|
+
* (`true`) — so the caller never synthesizes a wrong filename root for a late
|
|
247
|
+
* tag (Codex F4).
|
|
248
|
+
*/
|
|
249
|
+
async function findCsprojRootNamespace(csprojPath) {
|
|
250
|
+
const stream = createReadStream(csprojPath, { encoding: 'utf-8' });
|
|
251
|
+
let window = '';
|
|
252
|
+
let bytesRead = 0;
|
|
253
|
+
try {
|
|
254
|
+
for await (const chunk of stream) {
|
|
255
|
+
const text = chunk;
|
|
256
|
+
bytesRead += text.length;
|
|
257
|
+
window =
|
|
258
|
+
(window.length > CSPROJ_TAG_OVERLAP ? window.slice(-CSPROJ_TAG_OVERLAP) : window) + text;
|
|
259
|
+
const match = window.match(CSHARP_ROOT_NAMESPACE_RE);
|
|
260
|
+
if (match) {
|
|
261
|
+
stream.destroy();
|
|
262
|
+
return { rootNamespace: match[1].trim(), capHit: false };
|
|
263
|
+
}
|
|
264
|
+
if (bytesRead >= CSPROJ_ROOT_SCAN_MAX_BYTES) {
|
|
265
|
+
stream.destroy();
|
|
266
|
+
return { rootNamespace: null, capHit: true };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Unreadable .csproj: don't guess a filename root either — omit the config.
|
|
272
|
+
return { rootNamespace: null, capHit: true };
|
|
273
|
+
}
|
|
274
|
+
return { rootNamespace: null, capHit: false }; // read to EOF, tag genuinely absent
|
|
275
|
+
}
|
|
276
|
+
async function readCsprojConfig(csprojPath, fileName, repoRoot, dir) {
|
|
277
|
+
const { rootNamespace: found, capHit } = await findCsprojRootNamespace(csprojPath);
|
|
278
|
+
// A late `<RootNamespace>` we couldn't reach (capHit) or an unreadable file
|
|
279
|
+
// must NOT synthesize a filename root — a wrong authoritative root would make
|
|
280
|
+
// imports under the real root resolve to nothing and suppress the fallback
|
|
281
|
+
// (Codex F4). Omit the config so the no-csproj fallback stays available. Only
|
|
282
|
+
// fall back to the filename on a genuine read-to-EOF absence of the tag.
|
|
283
|
+
if (capHit)
|
|
284
|
+
return null;
|
|
285
|
+
const rootNamespace = found ?? fileName.replace(/\.csproj$/, '');
|
|
286
|
+
const projectDir = path.relative(repoRoot, dir).replace(/\\/g, '/');
|
|
287
|
+
if (isDev) {
|
|
288
|
+
logger.info(`📦 Loaded C# project: ${fileName} (namespace: ${rootNamespace}, dir: ${projectDir})`);
|
|
289
|
+
}
|
|
290
|
+
return { rootNamespace, projectDir };
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Stream one `.cs` file line-by-line and collect its declared `namespace` names
|
|
294
|
+
* into the shared Sets.
|
|
295
|
+
*
|
|
296
|
+
* Streaming (rather than reading the whole file into a string) keeps memory
|
|
297
|
+
* constant regardless of file size, so a large generated `.cs` (`*.g.cs`, EF /
|
|
298
|
+
* gRPC output) is fully scanned instead of skipped by a per-file size cap —
|
|
299
|
+
* which would otherwise trip `truncated` and disable the #1881 gate repo-wide.
|
|
300
|
+
* Only the cheap line scan streams here; the tree-sitter PARSE path keeps its
|
|
301
|
+
* own size cap.
|
|
302
|
+
*
|
|
303
|
+
* Returns `'truncated'` when the file could not be read, so the caller marks the
|
|
304
|
+
* scan truncated and the #1881 gate fails OPEN rather than wrongly suppress an
|
|
305
|
+
* import declared in the unread file. Returns `'ok'` on a complete read.
|
|
306
|
+
*/
|
|
307
|
+
async function collectDeclaredNamespaces(filePath, declaredNamespaces, rootNamespaces) {
|
|
308
|
+
const createScanner = await getCsharpStructureScannerFactory();
|
|
309
|
+
const scanner = createScanner();
|
|
310
|
+
try {
|
|
311
|
+
// `crlfDelay: Infinity` treats every `\r\n` as a single break; the line
|
|
312
|
+
// scanner is terminator-agnostic, so a streamed scan yields the same
|
|
313
|
+
// namespaces as scanning the whole file content at once.
|
|
314
|
+
const lines = createInterface({
|
|
315
|
+
input: createReadStream(filePath, { encoding: 'utf-8' }),
|
|
316
|
+
crlfDelay: Infinity,
|
|
317
|
+
});
|
|
318
|
+
for await (const line of lines) {
|
|
319
|
+
scanner.pushLine(line);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return 'truncated'; // unreadable source → signal truncation (fail open)
|
|
324
|
+
}
|
|
325
|
+
const structure = scanner.result();
|
|
326
|
+
for (const ns of structure.namespaces) {
|
|
327
|
+
declaredNamespaces.add(ns);
|
|
328
|
+
const dot = ns.indexOf('.');
|
|
329
|
+
rootNamespaces.add(dot === -1 ? ns : ns.slice(0, dot));
|
|
330
|
+
}
|
|
331
|
+
// A declaration the scanner could not fully capture (Codex F3) means the
|
|
332
|
+
// collected namespaces are an incomplete picture of this file — treat it like
|
|
333
|
+
// a truncated read so the #1881 gate fails OPEN rather than over-block an
|
|
334
|
+
// import whose namespace was dropped.
|
|
335
|
+
return structure.incomplete ? 'truncated' : 'ok';
|
|
141
336
|
}
|
|
142
337
|
export async function loadSwiftPackageConfig(repoRoot) {
|
|
143
338
|
// Swift imports are module-name based (e.g., `import SiuperModel`)
|
|
@@ -172,11 +367,13 @@ export async function loadSwiftPackageConfig(repoRoot) {
|
|
|
172
367
|
// ============================================================================
|
|
173
368
|
/** Load all language-specific configs once for an ingestion run. */
|
|
174
369
|
export async function loadImportConfigs(repoRoot) {
|
|
370
|
+
const csharpScan = await scanCSharpProject(repoRoot);
|
|
175
371
|
return {
|
|
176
372
|
tsconfigPaths: await loadTsconfigPaths(repoRoot),
|
|
177
373
|
goModule: await loadGoModulePath(repoRoot),
|
|
178
374
|
composerConfig: await loadComposerConfig(repoRoot),
|
|
179
375
|
swiftPackageConfig: await loadSwiftPackageConfig(repoRoot),
|
|
180
|
-
csharpConfigs:
|
|
376
|
+
csharpConfigs: csharpScan.configs,
|
|
377
|
+
csharpNamespaces: csharpScanToEvidence(csharpScan),
|
|
181
378
|
};
|
|
182
379
|
}
|
|
@@ -9,17 +9,21 @@
|
|
|
9
9
|
* match. Cross-file partial-class aggregation runs at graph-bridge
|
|
10
10
|
* time (Unit 6) via `populateOwners`.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* When `.csproj` configs are available, consults the legacy
|
|
13
|
+
* namespace-directory resolver first. Both that resolver's suffix
|
|
14
|
+
* fallback and the progressive prefix stripping below are gated on
|
|
15
|
+
* declared in-repo namespaces so BCL usings like `System.Threading.Tasks`
|
|
16
|
+
* cannot spuriously match a local `Tasks.cs` (#1881).
|
|
16
17
|
*
|
|
17
18
|
* Returning `null` lets the finalize algorithm mark the edge as
|
|
18
19
|
* `linkStatus: 'unresolved'`.
|
|
19
20
|
*/
|
|
20
21
|
import type { ParsedImport, WorkspaceIndex } from '../../../../_shared/index.js';
|
|
22
|
+
import type { CSharpProjectConfig, CSharpNamespaceEvidence } from '../../language-config.js';
|
|
21
23
|
export interface CsharpResolveContext {
|
|
22
24
|
readonly fromFile: string;
|
|
23
25
|
readonly allFilePaths: ReadonlySet<string>;
|
|
26
|
+
readonly csharpConfigs?: readonly CSharpProjectConfig[];
|
|
27
|
+
readonly namespaces?: CSharpNamespaceEvidence;
|
|
24
28
|
}
|
|
25
29
|
export declare function resolveCsharpImportTarget(parsedImport: ParsedImport, workspaceIndex: WorkspaceIndex): string | null;
|
|
@@ -9,84 +9,143 @@
|
|
|
9
9
|
* match. Cross-file partial-class aggregation runs at graph-bridge
|
|
10
10
|
* time (Unit 6) via `populateOwners`.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* When `.csproj` configs are available, consults the legacy
|
|
13
|
+
* namespace-directory resolver first. Both that resolver's suffix
|
|
14
|
+
* fallback and the progressive prefix stripping below are gated on
|
|
15
|
+
* declared in-repo namespaces so BCL usings like `System.Threading.Tasks`
|
|
16
|
+
* cannot spuriously match a local `Tasks.cs` (#1881).
|
|
16
17
|
*
|
|
17
18
|
* Returning `null` lets the finalize algorithm mark the edge as
|
|
18
19
|
* `linkStatus: 'unresolved'`.
|
|
19
20
|
*/
|
|
21
|
+
import { resolveCSharpImportInternal } from '../../import-resolvers/csharp.js';
|
|
22
|
+
import { buildSuffixIndex } from '../../import-resolvers/utils.js';
|
|
23
|
+
import { csharpSuffixFallbackAllowed } from '../../csharp-namespace-gate.js';
|
|
24
|
+
// Memoize on Set identity: the orchestrator passes the SAME `allFilePaths`
|
|
25
|
+
// Set through every `resolveImportTarget` call in a pass, so this rebuilds
|
|
26
|
+
// the normalized list + suffix index once instead of once per import (#1881 #2).
|
|
27
|
+
const workspaceFileIndexCache = new WeakMap();
|
|
28
|
+
function getWorkspaceFileIndex(allFilePaths) {
|
|
29
|
+
const cached = workspaceFileIndexCache.get(allFilePaths);
|
|
30
|
+
if (cached)
|
|
31
|
+
return cached;
|
|
32
|
+
const all = [...allFilePaths];
|
|
33
|
+
const normalized = all.map((f) => f.replace(/\\/g, '/'));
|
|
34
|
+
const built = { normalized, all, index: buildSuffixIndex(normalized, all) };
|
|
35
|
+
workspaceFileIndexCache.set(allFilePaths, built);
|
|
36
|
+
return built;
|
|
37
|
+
}
|
|
20
38
|
export function resolveCsharpImportTarget(parsedImport, workspaceIndex) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// CsharpResolveContext-shaped object; narrow structurally rather
|
|
24
|
-
// than via a cast chain so unexpected shapes return null cleanly.
|
|
25
|
-
const ctx = workspaceIndex;
|
|
26
|
-
if (ctx === undefined ||
|
|
27
|
-
typeof ctx.fromFile !== 'string' ||
|
|
28
|
-
!(ctx.allFilePaths instanceof Set)) {
|
|
39
|
+
const ctx = narrowContext(workspaceIndex);
|
|
40
|
+
if (ctx === null)
|
|
29
41
|
return null;
|
|
30
|
-
}
|
|
31
42
|
if (parsedImport.kind === 'dynamic-unresolved')
|
|
32
43
|
return null;
|
|
33
44
|
if (parsedImport.targetRaw === null || parsedImport.targetRaw === '')
|
|
34
45
|
return null;
|
|
46
|
+
const targetRaw = parsedImport.targetRaw;
|
|
47
|
+
const evidence = ctx.namespaces;
|
|
48
|
+
const csharpConfigs = ctx.csharpConfigs ?? [];
|
|
49
|
+
if (csharpConfigs.length > 0) {
|
|
50
|
+
const { normalized, all, index } = getWorkspaceFileIndex(ctx.allFilePaths);
|
|
51
|
+
const fromCsproj = resolveCSharpImportInternal(targetRaw, [...csharpConfigs], normalized, all, index, evidence);
|
|
52
|
+
if (fromCsproj.length > 0)
|
|
53
|
+
return fromCsproj[0];
|
|
54
|
+
// csproj configs are authoritative: mirror legacy `configs/csharp.ts`,
|
|
55
|
+
// which returns an empty result to STOP the chain. Falling through to the
|
|
56
|
+
// ungated `resolveDirectMatch` would re-introduce the BCL→local match the
|
|
57
|
+
// internal resolver's gate just suppressed (#1881 parity, #2).
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
35
60
|
// Namespace path: `System.Collections.Generic` → `System/Collections/Generic`.
|
|
36
|
-
const pathLike =
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
61
|
+
const pathLike = targetRaw.replace(/\./g, '/');
|
|
62
|
+
// Gate the WHOLE no-csproj path on declared in-repo namespaces — the direct
|
|
63
|
+
// path/suffix match INCLUDED — so a BCL using can't resolve to a
|
|
64
|
+
// coincidentally path-aligned local file (e.g. `Legacy/System/Threading/
|
|
65
|
+
// Tasks.cs` satisfying `using System.Threading.Tasks;`). Running the gate
|
|
66
|
+
// before `resolveDirectMatch` mirrors the legacy leg's gate-first ordering
|
|
67
|
+
// (`import-resolvers/configs/csharp.ts`), so the two legs are equivalent
|
|
68
|
+
// (#1881 parity, Codex F2). The gate keeps its fail-open for
|
|
69
|
+
// undefined/truncated evidence, so legitimate edges in unscanned repos are
|
|
70
|
+
// unaffected.
|
|
71
|
+
if (!csharpSuffixFallbackAllowed(targetRaw, evidence)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Exact file / nested-suffix / namespace-dir direct-child match.
|
|
75
|
+
const direct = resolveDirectMatch(ctx.allFilePaths, pathLike);
|
|
76
|
+
if (direct !== null)
|
|
77
|
+
return direct;
|
|
78
|
+
// Progressive prefix stripping — mirrors csproj's root-namespace mapping
|
|
79
|
+
// without the csproj.
|
|
80
|
+
return resolveByProgressiveStripping(ctx.allFilePaths, pathLike);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `WorkspaceIndex` is an opaque `unknown` placeholder in the shared contract;
|
|
84
|
+
* the orchestrator hands us a `CsharpResolveContext`-shaped object. Narrow
|
|
85
|
+
* structurally rather than via a cast chain so unexpected shapes fail cleanly.
|
|
86
|
+
*/
|
|
87
|
+
function narrowContext(workspaceIndex) {
|
|
88
|
+
const ctx = workspaceIndex;
|
|
89
|
+
if (ctx === undefined ||
|
|
90
|
+
typeof ctx.fromFile !== 'string' ||
|
|
91
|
+
!(ctx.allFilePaths instanceof Set)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return ctx;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* First-pass resolution against the full namespace path:
|
|
98
|
+
* exact whole-path file > nested suffix file > first `.cs` directly inside
|
|
99
|
+
* the namespace directory.
|
|
100
|
+
*/
|
|
101
|
+
function resolveDirectMatch(allFilePaths, pathLike) {
|
|
102
|
+
const exactName = `${pathLike}.cs`;
|
|
103
|
+
const nestedSuffix = `/${exactName}`;
|
|
43
104
|
let suffixFile = null;
|
|
44
|
-
|
|
45
|
-
const dirPrefix = `${pathLike}/`;
|
|
46
|
-
const suffixDirPrefix = `/${dirPrefix}`;
|
|
47
|
-
for (const raw of ctx.allFilePaths) {
|
|
105
|
+
for (const raw of allFilePaths) {
|
|
48
106
|
const f = raw.replace(/\\/g, '/');
|
|
49
107
|
if (!f.endsWith('.cs'))
|
|
50
108
|
continue;
|
|
51
|
-
if (f ===
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
if (suffixFile === null && f.endsWith(`${suffix}.cs`)) {
|
|
109
|
+
if (f === exactName)
|
|
110
|
+
return raw; // exact whole-path match wins
|
|
111
|
+
if (suffixFile === null && f.endsWith(nestedSuffix))
|
|
56
112
|
suffixFile = raw;
|
|
57
|
-
}
|
|
58
|
-
if (directoryChild === null) {
|
|
59
|
-
// Namespace-to-directory match: pick the first `.cs` directly in
|
|
60
|
-
// the namespace dir (not nested deeper). Legacy resolver emits
|
|
61
|
-
// all of them; we take one so the scope-resolver contract stays
|
|
62
|
-
// single-target.
|
|
63
|
-
const atRoot = f.startsWith(dirPrefix);
|
|
64
|
-
const atNested = f.includes(suffixDirPrefix);
|
|
65
|
-
if (atRoot || atNested) {
|
|
66
|
-
const idx = atRoot ? 0 : f.indexOf(suffixDirPrefix) + 1;
|
|
67
|
-
const after = f.slice(idx + dirPrefix.length);
|
|
68
|
-
if (after.length > 0 && !after.includes('/')) {
|
|
69
|
-
directoryChild = raw;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
113
|
}
|
|
74
|
-
if (exactFile !== null)
|
|
75
|
-
return exactFile;
|
|
76
114
|
if (suffixFile !== null)
|
|
77
115
|
return suffixFile;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
return findDirectChild(allFilePaths, pathLike);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* First `.cs` file that lives directly inside the namespace directory
|
|
120
|
+
* `dirSegment` (at repo root or nested under a project prefix), not deeper.
|
|
121
|
+
* The legacy resolver emits all of them; the scope-resolver contract is
|
|
122
|
+
* single-target so we take one.
|
|
123
|
+
*/
|
|
124
|
+
function findDirectChild(allFilePaths, dirSegment) {
|
|
125
|
+
const dirPrefix = `${dirSegment}/`;
|
|
126
|
+
const nestedDirPrefix = `/${dirPrefix}`;
|
|
127
|
+
for (const raw of allFilePaths) {
|
|
128
|
+
const f = raw.replace(/\\/g, '/');
|
|
129
|
+
if (!f.endsWith('.cs'))
|
|
130
|
+
continue;
|
|
131
|
+
const atRoot = f.startsWith(dirPrefix);
|
|
132
|
+
const atNested = f.includes(nestedDirPrefix);
|
|
133
|
+
if (!atRoot && !atNested)
|
|
134
|
+
continue;
|
|
135
|
+
const idx = atRoot ? 0 : f.indexOf(nestedDirPrefix) + 1;
|
|
136
|
+
const after = f.slice(idx + dirPrefix.length);
|
|
137
|
+
if (after.length > 0 && !after.includes('/'))
|
|
138
|
+
return raw;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Try each suffix of the namespace path against `.cs` files and directories,
|
|
144
|
+
* stripping leading segments one at a time. Models `using CrossFile.Models;`
|
|
145
|
+
* resolving to `Models/User.cs` in a repo laid out without the `CrossFile/`
|
|
146
|
+
* prefix (the scope-resolver layer has no csproj to consult).
|
|
147
|
+
*/
|
|
148
|
+
function resolveByProgressiveStripping(allFilePaths, pathLike) {
|
|
90
149
|
const segments = pathLike.split('/').filter(Boolean);
|
|
91
150
|
for (let skip = 1; skip < segments.length; skip++) {
|
|
92
151
|
const tail = segments.slice(skip).join('/');
|
|
@@ -94,30 +153,21 @@ export function resolveCsharpImportTarget(parsedImport, workspaceIndex) {
|
|
|
94
153
|
continue;
|
|
95
154
|
const tailFile = `${tail}.cs`;
|
|
96
155
|
const tailSuffix = `/${tailFile}`;
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
let tailDirectChild = null;
|
|
100
|
-
for (const raw of ctx.allFilePaths) {
|
|
156
|
+
let tailFileMatch = null;
|
|
157
|
+
for (const raw of allFilePaths) {
|
|
101
158
|
const f = raw.replace(/\\/g, '/');
|
|
102
159
|
if (!f.endsWith('.cs'))
|
|
103
160
|
continue;
|
|
104
|
-
if (f === tailFile)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return raw;
|
|
108
|
-
if (tailDirectChild === null) {
|
|
109
|
-
const atRoot = f.startsWith(tailDir);
|
|
110
|
-
const atNested = f.includes(tailSuffixDir);
|
|
111
|
-
if (atRoot || atNested) {
|
|
112
|
-
const idx = atRoot ? 0 : f.indexOf(tailSuffixDir) + 1;
|
|
113
|
-
const after = f.slice(idx + tailDir.length);
|
|
114
|
-
if (after.length > 0 && !after.includes('/'))
|
|
115
|
-
tailDirectChild = raw;
|
|
116
|
-
}
|
|
161
|
+
if (f === tailFile || f.endsWith(tailSuffix)) {
|
|
162
|
+
tailFileMatch = raw;
|
|
163
|
+
break;
|
|
117
164
|
}
|
|
118
165
|
}
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
166
|
+
if (tailFileMatch !== null)
|
|
167
|
+
return tailFileMatch;
|
|
168
|
+
const child = findDirectChild(allFilePaths, tail);
|
|
169
|
+
if (child !== null)
|
|
170
|
+
return child;
|
|
121
171
|
}
|
|
122
172
|
return null;
|
|
123
173
|
}
|
|
@@ -44,6 +44,13 @@ export interface CsharpFileStructure {
|
|
|
44
44
|
/** Dotted paths from `using static X.Y.Z;` (including
|
|
45
45
|
* `global using static` and aliased `using static A = X.Y.Z;`). */
|
|
46
46
|
readonly usingStaticPaths: readonly string[];
|
|
47
|
+
/** True when the scanner saw a `namespace` / `using static` declaration it
|
|
48
|
+
* could not fully capture (keyword not at line start, split across lines, or
|
|
49
|
+
* an unparseable identifier form). Callers feeding the #1881 gate must treat
|
|
50
|
+
* this like a truncated scan and fail OPEN, since a dropped namespace would
|
|
51
|
+
* otherwise over-block a legitimate import (Codex F3). Absent/false on a
|
|
52
|
+
* cleanly-scanned file. */
|
|
53
|
+
readonly incomplete?: boolean;
|
|
47
54
|
}
|
|
48
55
|
/** Line-scanner used when no cached tree is available (worker-parsed files
|
|
49
56
|
* can't transfer native tree-sitter Trees across MessageChannels, so
|
|
@@ -59,6 +66,21 @@ export interface CsharpFileStructure {
|
|
|
59
66
|
* AST is a declaration whose keyword is not at the start of a code line
|
|
60
67
|
* (split across lines, or sharing a line with a comment/string closer).
|
|
61
68
|
* Mirrors PHP's `extractNamespaceViaScanner` (issue #1741). */
|
|
69
|
+
/** Incremental form of {@link extractCsharpStructureViaScanner}: feed lines one
|
|
70
|
+
* at a time via `pushLine` (in source order), then read the accumulated
|
|
71
|
+
* structure with `result()`. Lets a caller stream a file off disk
|
|
72
|
+
* (`createReadStream` + `readline`) and scan it for `namespace` / `using
|
|
73
|
+
* static` declarations in CONSTANT memory rather than buffering the whole file
|
|
74
|
+
* into a string — the line splitting and per-line matching are identical, so a
|
|
75
|
+
* streamed scan yields the same result as scanning the full content. The line
|
|
76
|
+
* terminator must be stripped (as `readline` does, or `String.split('\n')`); a
|
|
77
|
+
* trailing `\r` on a CRLF line is inert to both the matchers and the lexer. */
|
|
78
|
+
export interface CsharpStructureLineScanner {
|
|
79
|
+
pushLine(line: string): void;
|
|
80
|
+
result(): CsharpFileStructure;
|
|
81
|
+
}
|
|
82
|
+
/** Create a fresh stateful line scanner — see {@link CsharpStructureLineScanner}. */
|
|
83
|
+
export declare function createCsharpStructureScanner(): CsharpStructureLineScanner;
|
|
62
84
|
export declare function extractCsharpStructureViaScanner(content: string): CsharpFileStructure;
|
|
63
85
|
/** Content + (optional) pre-parsed tree-sitter trees keyed by filePath.
|
|
64
86
|
* The orchestrator builds `fileContents` from the pipeline's file list;
|
|
@@ -34,16 +34,45 @@
|
|
|
34
34
|
* re-parsing every file from scratch (that re-parse dominated worker-mode
|
|
35
35
|
* scope-resolution time). See `extractCsharpStructureViaScanner`.
|
|
36
36
|
*/
|
|
37
|
+
// A dotted C# namespace identifier: each segment is an optional verbatim `@`
|
|
38
|
+
// followed by a Unicode letter/`_` and Unicode letters/digits/`_`. The `u` flag
|
|
39
|
+
// makes the classes Unicode-aware so `namespace Café.Models;` is captured (the
|
|
40
|
+
// old ASCII `[A-Za-z…]` truncated it). The `@` markers are stripped from the
|
|
41
|
+
// capture so it matches the tree-sitter AST's `name` text.
|
|
42
|
+
const CS_NS_IDENT = String.raw `@?[\p{L}_][\p{L}\p{N}_]*(?:\.@?[\p{L}_][\p{L}\p{N}_]*)*`;
|
|
37
43
|
// Line-anchored matchers for the worker-path fallback (see
|
|
38
44
|
// `extractCsharpStructureViaScanner`). Anchored at line start (after
|
|
39
45
|
// indentation); the scanner additionally tracks block-comment / string
|
|
40
46
|
// state across lines so a keyword at the start of a line inside one of
|
|
41
47
|
// those regions is skipped.
|
|
42
|
-
const CS_NAMESPACE_RE =
|
|
48
|
+
const CS_NAMESPACE_RE = new RegExp(String.raw `^[ \t]*namespace[ \t]+(${CS_NS_IDENT})`, 'u');
|
|
43
49
|
// `global using static`, plain `using static`, and the aliased
|
|
44
50
|
// `using static Alias = NS.Type;` form (the AST keeps the RHS path, so
|
|
45
51
|
// the optional `Alias =` is skipped and only the dotted path captured).
|
|
46
|
-
const CS_USING_STATIC_RE =
|
|
52
|
+
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');
|
|
53
|
+
// Incompleteness detectors — used ONLY when the precise matchers above failed,
|
|
54
|
+
// to flag a declaration the scanner could not capture (so the file fails the
|
|
55
|
+
// #1881 gate OPEN instead of silently dropping the namespace). Kept
|
|
56
|
+
// high-precision so ordinary files never trip them (which would wrongly disable
|
|
57
|
+
// the gate repo-wide):
|
|
58
|
+
// - `…_BARE`: the keyword alone on a line (the name is on the next line).
|
|
59
|
+
// - `…_AT_START`: a line-start declaration the precise matcher couldn't parse.
|
|
60
|
+
// - `CS_NAMESPACE_AFTER_CODE`: a `namespace` keyword right after a `}`/`;`/`{`/`]`
|
|
61
|
+
// (real code, NOT a `//` comment), i.e. not at line start.
|
|
62
|
+
const CS_NAMESPACE_BARE = /^[ \t]*namespace[ \t]*\r?$/;
|
|
63
|
+
const CS_USING_STATIC_BARE = /^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]*\r?$/;
|
|
64
|
+
const CS_NAMESPACE_AT_START = /^[ \t]*namespace[ \t]+\S/;
|
|
65
|
+
const CS_USING_STATIC_AT_START = /^[ \t]*(?:global[ \t]+)?using[ \t]+static[ \t]+\S/;
|
|
66
|
+
const CS_NAMESPACE_AFTER_CODE = /[}\];{][ \t]*namespace[ \t]+@?[\p{L}_]/u;
|
|
67
|
+
/** Whether a `code`-state line declares a namespace / using-static the precise
|
|
68
|
+
* matchers could not capture — see the detectors above. */
|
|
69
|
+
function looksLikeUncapturedDeclaration(line) {
|
|
70
|
+
return (CS_NAMESPACE_BARE.test(line) ||
|
|
71
|
+
CS_USING_STATIC_BARE.test(line) ||
|
|
72
|
+
CS_NAMESPACE_AT_START.test(line) ||
|
|
73
|
+
CS_USING_STATIC_AT_START.test(line) ||
|
|
74
|
+
CS_NAMESPACE_AFTER_CODE.test(line));
|
|
75
|
+
}
|
|
47
76
|
/** Advance the scanner's lexical state across one line, consuming block
|
|
48
77
|
* comments (slash-star), line comments (`//`), single-line regular /
|
|
49
78
|
* interpolated strings, verbatim strings (`@"…"`), and raw string literals
|
|
@@ -160,42 +189,48 @@ function advanceCsScanState(line, state, rawFence) {
|
|
|
160
189
|
}
|
|
161
190
|
return [state, rawFence];
|
|
162
191
|
}
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
* `treeCache` is empty for them). Re-parsing every C# file here with
|
|
166
|
-
* tree-sitter was the dominant scope-resolution cost on large worker-mode
|
|
167
|
-
* runs — for a multi-thousand-file solution this loop alone re-parsed the
|
|
168
|
-
* whole repo a second time. The scanner extracts the same `namespaces` /
|
|
169
|
-
* `usingStaticPaths` the AST walk produces for line-anchored declarations,
|
|
170
|
-
* while tracking block-comment and string state across lines (via
|
|
171
|
-
* `advanceCsScanState`) so a `namespace` / `using static` keyword at the
|
|
172
|
-
* start of a line inside a block comment, verbatim string, or raw string
|
|
173
|
-
* literal is NOT mistaken for a declaration. The remaining trade-off vs the
|
|
174
|
-
* AST is a declaration whose keyword is not at the start of a code line
|
|
175
|
-
* (split across lines, or sharing a line with a comment/string closer).
|
|
176
|
-
* Mirrors PHP's `extractNamespaceViaScanner` (issue #1741). */
|
|
177
|
-
export function extractCsharpStructureViaScanner(content) {
|
|
192
|
+
/** Create a fresh stateful line scanner — see {@link CsharpStructureLineScanner}. */
|
|
193
|
+
export function createCsharpStructureScanner() {
|
|
178
194
|
const namespaces = [];
|
|
179
195
|
const usingStaticPaths = [];
|
|
196
|
+
let incomplete = false;
|
|
180
197
|
let state = 'code';
|
|
181
198
|
let rawFence = 0;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
return {
|
|
200
|
+
pushLine(line) {
|
|
201
|
+
// Only match when the line START is real code — keywords reached while
|
|
202
|
+
// inside a block comment / multi-line string are skipped.
|
|
203
|
+
if (state === 'code') {
|
|
204
|
+
const ns = CS_NAMESPACE_RE.exec(line);
|
|
205
|
+
if (ns !== null) {
|
|
206
|
+
namespaces.push(ns[1].replace(/@/g, ''));
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const us = CS_USING_STATIC_RE.exec(line);
|
|
210
|
+
if (us !== null) {
|
|
211
|
+
usingStaticPaths.push(us[1].replace(/@/g, ''));
|
|
212
|
+
}
|
|
213
|
+
else if (looksLikeUncapturedDeclaration(line)) {
|
|
214
|
+
// A declaration the precise matchers couldn't capture → mark the
|
|
215
|
+
// file incomplete so the #1881 gate fails OPEN (Codex F3).
|
|
216
|
+
incomplete = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
194
219
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
[state, rawFence] = advanceCsScanState(line, state, rawFence);
|
|
221
|
+
},
|
|
222
|
+
result() {
|
|
223
|
+
return incomplete
|
|
224
|
+
? { namespaces, usingStaticPaths, incomplete }
|
|
225
|
+
: { namespaces, usingStaticPaths };
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function extractCsharpStructureViaScanner(content) {
|
|
230
|
+
const scanner = createCsharpStructureScanner();
|
|
231
|
+
for (const line of content.split('\n'))
|
|
232
|
+
scanner.pushLine(line);
|
|
233
|
+
return scanner.result();
|
|
199
234
|
}
|
|
200
235
|
/** Build a structural view of a C# file. Prefers `cachedTree` (handed in
|
|
201
236
|
* via `treeCache`) and walks the tree-sitter AST — the authoritative
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-workspace config for C# scope-resolution import targeting.
|
|
3
|
+
*
|
|
4
|
+
* Loaded once per analyze pass via `csharpScopeResolver.loadResolutionConfig`
|
|
5
|
+
* and threaded into `resolveCsharpImportTarget`. The pure gate predicates live
|
|
6
|
+
* in `../../csharp-namespace-gate.ts` (shared with the legacy DAG resolver).
|
|
7
|
+
*/
|
|
8
|
+
import { type CSharpProjectConfig, type CSharpNamespaceEvidence } from '../../language-config.js';
|
|
9
|
+
export interface CsharpResolutionConfig {
|
|
10
|
+
readonly csharpConfigs: readonly CSharpProjectConfig[];
|
|
11
|
+
/** In-repo declared-namespace evidence gating suffix-fallback resolution (#1881). */
|
|
12
|
+
readonly namespaces?: CSharpNamespaceEvidence;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadCsharpResolutionConfig(repoRoot: string): Promise<CsharpResolutionConfig>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-workspace config for C# scope-resolution import targeting.
|
|
3
|
+
*
|
|
4
|
+
* Loaded once per analyze pass via `csharpScopeResolver.loadResolutionConfig`
|
|
5
|
+
* and threaded into `resolveCsharpImportTarget`. The pure gate predicates live
|
|
6
|
+
* in `../../csharp-namespace-gate.ts` (shared with the legacy DAG resolver).
|
|
7
|
+
*/
|
|
8
|
+
import { scanCSharpProject, csharpScanToEvidence, } from '../../language-config.js';
|
|
9
|
+
export async function loadCsharpResolutionConfig(repoRoot) {
|
|
10
|
+
const scan = await scanCSharpProject(repoRoot);
|
|
11
|
+
return {
|
|
12
|
+
csharpConfigs: scan.configs,
|
|
13
|
+
namespaces: csharpScanToEvidence(scan),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -11,13 +11,21 @@ import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.
|
|
|
11
11
|
import { csharpProvider } from '../csharp.js';
|
|
12
12
|
import { csharpArityCompatibility, csharpMergeBindings, resolveCsharpImportTarget, } from './index.js';
|
|
13
13
|
import { populateCsharpNamespaceSiblings } from './namespace-siblings.js';
|
|
14
|
+
import { loadCsharpResolutionConfig } from './resolution-config.js';
|
|
14
15
|
import { unwrapCsharpCollectionAccessor } from './accessor-unwrap.js';
|
|
15
16
|
const csharpScopeResolver = {
|
|
16
17
|
language: SupportedLanguages.CSharp,
|
|
17
18
|
languageProvider: csharpProvider,
|
|
18
19
|
importEdgeReason: 'csharp-scope: using',
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
loadResolutionConfig: (repoPath) => loadCsharpResolutionConfig(repoPath),
|
|
21
|
+
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) => {
|
|
22
|
+
const config = resolutionConfig;
|
|
23
|
+
const ws = {
|
|
24
|
+
fromFile,
|
|
25
|
+
allFilePaths,
|
|
26
|
+
csharpConfigs: config?.csharpConfigs,
|
|
27
|
+
namespaces: config?.namespaces,
|
|
28
|
+
};
|
|
21
29
|
// `WorkspaceIndex` is an opaque `unknown` placeholder in the
|
|
22
30
|
// shared contract, so `ws` passes structurally without a cast.
|
|
23
31
|
return resolveCsharpImportTarget({ kind: 'namespace', localName: '_', importedName: '_', targetRaw }, ws);
|
package/package.json
CHANGED