@ecoma-io/archkeep 0.15.0 → 0.16.0
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/README.md +3 -3
- package/cli.mjs +126 -4
- package/commands.mjs +6 -0
- package/lsp.mjs +15 -2
- package/package.json +6 -2
- package/src/analysis/analyze.mjs +15 -0
- package/src/analysis/contract.md +36 -18
- package/src/analysis/csharp.mjs +485 -0
- package/src/analysis/dotnet/csproj.mjs +380 -0
- package/src/analysis/dotnet/mask.mjs +178 -0
- package/src/analysis/dotnet/namespaces.mjs +172 -0
- package/src/analysis/dotnet/resolve.mjs +89 -0
- package/src/analysis/go.mjs +289 -5
- package/src/analysis/java.mjs +329 -0
- package/src/analysis/jvm/gradle.mjs +545 -0
- package/src/analysis/jvm/mask.mjs +170 -0
- package/src/analysis/jvm/maven.mjs +612 -0
- package/src/analysis/jvm/packages.mjs +209 -0
- package/src/analysis/jvm/resolve.mjs +139 -0
- package/src/analysis/kotlin.mjs +210 -0
- package/src/analysis/manifest-util.mjs +30 -0
- package/src/analysis/python.mjs +3 -2
- package/src/analysis/registry.mjs +11 -0
- package/src/analysis/rust.mjs +171 -17
- package/src/analysis/source-util.mjs +155 -6
- package/src/analysis/typescript.mjs +9 -2
- package/src/commands/context.mjs +84 -14
- package/src/commands/provenance.mjs +7 -44
- package/src/commands/rules.mjs +775 -0
- package/src/governance/profile-registry.mjs +0 -1
- package/src/graph/create-dependencies.mjs +138 -15
- package/src/lsp/diagnose.mjs +1 -1
- package/src/lsp/server.mjs +97 -1
- package/src/lsp/workspace-index.mjs +106 -15
- package/src/options.mjs +30 -7
- package/src/process.mjs +10 -1
- package/src/providers/moon.mjs +287 -36
- package/src/providers/native/differential.fixtures.mjs +32 -6
- package/src/providers/native/discover.mjs +83 -4
- package/src/providers/native/graph.mjs +58 -0
- package/src/providers/native/model.mjs +59 -1
- package/src/rules/index.mjs +21 -6
- package/src/rules/reachability.mjs +2 -0
- package/src/rules/tags.mjs +7 -5
- package/src/rules/topology.mjs +5 -3
- package/src/workspace.mjs +115 -23
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C# analyzer — directive extraction over comment-and-literal-masked text,
|
|
3
|
+
* sharing the dotnet core with the namespace index exactly the way
|
|
4
|
+
* `./java.mjs` shares the JVM one (`docs/adr/0006-dotnet-language-integration.md`).
|
|
5
|
+
*
|
|
6
|
+
* Static analysis only, no .NET SDK required (`docs/reference/languages.md`
|
|
7
|
+
* owns why graphs compute on machines with no toolchain). C# fixes five
|
|
8
|
+
* written directive forms plus one alias-adjacent spelling:
|
|
9
|
+
*
|
|
10
|
+
* using X.Y.Z; namespace (whole-namespace import)
|
|
11
|
+
* using static X.Y.Z.Type; static members of a type
|
|
12
|
+
* using Alias = X.Y.Z; namespace/type alias
|
|
13
|
+
* using Alias = X.Y.Z.Type; type alias — resolves by its base
|
|
14
|
+
* global using X.Y.Z; file-set-wide (C# 10+)
|
|
15
|
+
* extern alias X; externally supplied root alias
|
|
16
|
+
*
|
|
17
|
+
* Every dotted subject may carry the `global::` qualifier — `using
|
|
18
|
+
* global::X.Y;`, `using static global::X.Y.T;`, `using A = global::X.Y;` —
|
|
19
|
+
* legal wherever a local name shadows a namespace, and common in generated
|
|
20
|
+
* code. The qualifier is syntax around the subject, so it is stripped before
|
|
21
|
+
* classification and stays out of the specifier, the same way an alias's own
|
|
22
|
+
* name does: `imp` must equal `packageName` for an external ban to fire.
|
|
23
|
+
*
|
|
24
|
+
* A UTF-8 BOM is matched, never stripped — the same byte
|
|
25
|
+
* `./dotnet/namespaces.mjs` and the JVM package declaration tolerate (#221's
|
|
26
|
+
* lesson, `../jvm/packages.mjs`): a first-line directive behind one is read,
|
|
27
|
+
* and every offset this parse returns stays an offset into the bytes on disk
|
|
28
|
+
* (`../contract.md`'s byte-tolerance law). A directive may sit wherever a
|
|
29
|
+
* fresh declaration may — after `;`, `{` or `}` on the same line, so
|
|
30
|
+
* `namespace N { using A.B; }` is read — besides the line head.
|
|
31
|
+
*
|
|
32
|
+
* There is no wildcard form (a plain `using` already imports a whole
|
|
33
|
+
* namespace), no dynamic form, no re-export syntax: `kind` is always
|
|
34
|
+
* `"static"`, and `spelling.path` is always `false` because no C# directive is
|
|
35
|
+
* spelled as a filesystem path — `spelling.namesOnly` is always `true`, so a
|
|
36
|
+
* namespace whose root is literally named `libs` is a name, not a path into a
|
|
37
|
+
* project (#376). `spelling.relative` takes Go's argued answer —
|
|
38
|
+
* true exactly when the directive resolved into its own project — because no
|
|
39
|
+
* C# spelling is relative either, so the bit reads what a directive REACHED
|
|
40
|
+
* rather than how it was written.
|
|
41
|
+
*
|
|
42
|
+
* Using STATEMENTS share their first word with directives and are excluded by
|
|
43
|
+
* shape, not by guesswork: `using var s = …;`, `using (r) { … }` and
|
|
44
|
+
* `using StreamReader r = …;` all fail the body grammars below, which accept
|
|
45
|
+
* exactly a dotted name, `static` plus a dotted name, or ONE identifier, an
|
|
46
|
+
* equals sign, and a right-hand side. Anything else is a statement and stays
|
|
47
|
+
* unread — a resource variable's type would otherwise read as an import of
|
|
48
|
+
* itself.
|
|
49
|
+
*
|
|
50
|
+
* Alias right-hand sides resolve by their generic-free base: `using Grid =
|
|
51
|
+
* Corp.Domain.Grid<int>;` reaches `Corp.Domain.Grid`'s project through the
|
|
52
|
+
* base name, because the constructed type lives where its definition lives.
|
|
53
|
+
* A right-hand side that never reduces to a dotted name — a tuple alias,
|
|
54
|
+
* `using Pair = (int, string);` — introduces no cross-project name at all and
|
|
55
|
+
* classifies external with the written right-hand side standing in as
|
|
56
|
+
* `packageName`.
|
|
57
|
+
*
|
|
58
|
+
* Known parse limits, deliberate and pinned by tests, each erring toward a
|
|
59
|
+
* record naming text the file really contains or toward a documented silence,
|
|
60
|
+
* never toward a wrong project:
|
|
61
|
+
*
|
|
62
|
+
* - A **multi-line directive** (`using A.B.\n C;`) is not read: the body
|
|
63
|
+
* must sit on its own line, terminated by `;`. Every formatter writes one
|
|
64
|
+
* line; the miss is compensated by the manifest resolver's independent
|
|
65
|
+
* edges (`./dotnet/csproj.mjs`).
|
|
66
|
+
* - **Attribute references, inline fully-qualified names and reflection**
|
|
67
|
+
* reach types without any directive; extraction cannot see them. Documented
|
|
68
|
+
* limits whose compensation is the manifest track and tag law.
|
|
69
|
+
* - **Verbatim identifiers** (`@class`) inside a directive are not read — the
|
|
70
|
+
* same class of limit Kotlin's backtick segments pin.
|
|
71
|
+
* - An **unterminated directive** (no `;` before end of line) is not a
|
|
72
|
+
* complete directive and is not read.
|
|
73
|
+
*/
|
|
74
|
+
import { csharpNamespaceIndex } from "./dotnet/namespaces.mjs";
|
|
75
|
+
import { maskCSharpComments } from "./dotnet/mask.mjs";
|
|
76
|
+
import { resolveCsharpSpecifier } from "./dotnet/resolve.mjs";
|
|
77
|
+
import {
|
|
78
|
+
emptyResult,
|
|
79
|
+
fileFailure,
|
|
80
|
+
perWorkspace,
|
|
81
|
+
positionAt,
|
|
82
|
+
projectOwning,
|
|
83
|
+
refuseUnreadTree,
|
|
84
|
+
} from "./source-util.mjs";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One identifier segment. Verbatim identifiers (`@class`) are deliberately
|
|
88
|
+
* outside this grammar — see the limits above.
|
|
89
|
+
*/
|
|
90
|
+
const SEG = String.raw`[\p{L}_][\p{L}\p{Nd}_]*`;
|
|
91
|
+
const DOTTED_NAME = `${SEG}(?:\\.${SEG})*`;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The body of a using directive, captured up to its terminating semicolon.
|
|
95
|
+
* The anchor accepts a fresh declaration's every legal predecessor — line
|
|
96
|
+
* head (through a UTF-8 BOM), `;`, `{`, `}` — so `namespace N { using A.B; }`
|
|
97
|
+
* on one line is read like any formatted tree. The semicolon is a lookahead,
|
|
98
|
+
* never part of the match (#407): consumed, the scan resumed PAST it, and the
|
|
99
|
+
* `;` before a second same-line directive — its only legal anchor — was
|
|
100
|
+
* already behind it, so `using A.B; using C.D;` read only `A.B`.
|
|
101
|
+
*/
|
|
102
|
+
const CS_USING_BODY = new RegExp(
|
|
103
|
+
String.raw`(?:^\uFEFF?|[\n;{}])[ \t]*(?:global[ \t]+)?using[ \t]+([^;\n]+?)[ \t]*(?=;)`,
|
|
104
|
+
"gu",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The directive openers without their bodies — the heads the malformation
|
|
109
|
+
* scan (`csharpDirectiveMalformations`) anchors on. They live beside the
|
|
110
|
+
* regexes they mirror rather than inside the scan, because the two must
|
|
111
|
+
* agree about where a directive opens: a head that matched MORE than the
|
|
112
|
+
* body regex would flag a file the body regexes read fully.
|
|
113
|
+
*/
|
|
114
|
+
const CS_USING_BODY_HEAD = new RegExp(
|
|
115
|
+
String.raw`(?:^\uFEFF?|[\n;{}])[ \t]*(?:global[ \t]+)?using[ \t]+`,
|
|
116
|
+
"gu",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/** The extern-alias opener, for the same scan. */
|
|
120
|
+
const CS_EXTERN_ALIAS_HEAD = new RegExp(
|
|
121
|
+
String.raw`(?:^\uFEFF?|[\n;{}])[ \t]*extern[ \t]+alias[ \t]+`,
|
|
122
|
+
"gu",
|
|
123
|
+
);
|
|
124
|
+
/** The extern-alias directive: `extern alias X;` — recorded, resolved as external. */
|
|
125
|
+
const CS_EXTERN_ALIAS = new RegExp(
|
|
126
|
+
String.raw`(?:^\uFEFF?|[\n;{}])[ \t]*extern[ \t]+alias[ \t]+(${SEG})[ \t]*(?=;)`,
|
|
127
|
+
"gu",
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
/** Exactly one identifier followed by `=`: the alias form. */
|
|
131
|
+
const ALIAS_FORM = new RegExp(String.raw`^(${SEG})[ \t]*=[ \t]*(.+)$`, "su");
|
|
132
|
+
|
|
133
|
+
/** A dotted name, optionally behind the `global::` qualifier: the plain form. */
|
|
134
|
+
const PLAIN_FORM = new RegExp(String.raw`^(?:global::)?(${DOTTED_NAME})$`, "u");
|
|
135
|
+
|
|
136
|
+
/** `static` plus an optionally qualified dotted name: the static-members form. */
|
|
137
|
+
const STATIC_FORM = new RegExp(String.raw`^static[ \t]+(?:global::)?(${DOTTED_NAME})$`, "u");
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Strips one balanced trailing generic argument list from an alias's
|
|
141
|
+
* right-hand side: `Corp.Domain.Grid<int>` becomes `Corp.Domain.Grid`.
|
|
142
|
+
* Unbalanced brackets are left alone — the caller then classifies the raw
|
|
143
|
+
* text external rather than guessing where the type began.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} rhs
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
function withoutGenericArguments(rhs) {
|
|
149
|
+
const open = rhs.indexOf("<");
|
|
150
|
+
if (open === -1 || !rhs.endsWith(">")) return rhs;
|
|
151
|
+
let depth = 0;
|
|
152
|
+
for (let at = open; at < rhs.length; at++) {
|
|
153
|
+
if (rhs[at] === "<") depth++;
|
|
154
|
+
else if (rhs[at] === ">") {
|
|
155
|
+
depth--;
|
|
156
|
+
if (depth === 0) return at === rhs.length - 1 ? rhs.slice(0, open) : rhs;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return rhs;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Classifies one directive body into what resolution may see.
|
|
164
|
+
*
|
|
165
|
+
* The SPECIFIER is the directive's SUBJECT — the dotted name a plain/static
|
|
166
|
+
* directive imports, or an alias's right-hand side — never the statement's
|
|
167
|
+
* form words. That is not style: the `bannedExternalImports` family matches
|
|
168
|
+
* its globs against the specifier and requires it to equal the resolved
|
|
169
|
+
* package name (or a `/`-beneath path of it), so a specifier carrying
|
|
170
|
+
* `static ` or `Alias = ` would silently exempt every static and aliased
|
|
171
|
+
* crossing from every external ban — the exact direction this repository
|
|
172
|
+
* exists to close. Form information lives in the directive's shape, which any
|
|
173
|
+
* reader of the line sees anyway.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} body The trimmed text between `using` and `;`.
|
|
176
|
+
* @returns {{ specifier: string, importableName: string|null, specifierStartInBody: number }|null} `null`
|
|
177
|
+
* when the body is a using STATEMENT's shape, not a directive's.
|
|
178
|
+
*/
|
|
179
|
+
export function classifyUsingBody(body) {
|
|
180
|
+
const trimmed = body.trim();
|
|
181
|
+
if (trimmed === "") return null;
|
|
182
|
+
const staticForm = STATIC_FORM.exec(trimmed);
|
|
183
|
+
if (staticForm) {
|
|
184
|
+
return {
|
|
185
|
+
specifier: staticForm[1],
|
|
186
|
+
importableName: staticForm[1],
|
|
187
|
+
specifierStartInBody: trimmed.indexOf(staticForm[1]),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const plainForm = PLAIN_FORM.exec(trimmed);
|
|
191
|
+
if (plainForm) {
|
|
192
|
+
return {
|
|
193
|
+
specifier: plainForm[1],
|
|
194
|
+
importableName: plainForm[1],
|
|
195
|
+
specifierStartInBody: trimmed.indexOf(plainForm[1]),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const aliasForm = ALIAS_FORM.exec(trimmed);
|
|
199
|
+
if (aliasForm) {
|
|
200
|
+
// The alias name is local syntax; only the right-hand side can cross a
|
|
201
|
+
// boundary, so the right-hand side IS the specifier. A constructed
|
|
202
|
+
// generic keeps its generic-free base as both specifier and importable,
|
|
203
|
+
// because `imp` (the specifier the rule matches against globs) must
|
|
204
|
+
// equal `packageName` for `isConstraintBanningProject` to fire. The
|
|
205
|
+
// `global::` qualifier is stripped with the alias name for the same
|
|
206
|
+
// reason — it is syntax around the subject, not part of it.
|
|
207
|
+
const rhs = aliasForm[2].trim().replace(/^global::/, "");
|
|
208
|
+
const base = withoutGenericArguments(rhs);
|
|
209
|
+
const importableName = new RegExp(`^${DOTTED_NAME}$`, "u").test(base) ? base : null;
|
|
210
|
+
const specifier = importableName ?? rhs;
|
|
211
|
+
return { specifier, importableName, specifierStartInBody: trimmed.indexOf(specifier) };
|
|
212
|
+
}
|
|
213
|
+
// `var s = …`, `Type r = …`, `(expr)` — statement shapes stay unread.
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Every directive in a `.cs` file, in source order and WITHOUT deduplication —
|
|
219
|
+
* one entry per written directive, which is what an import-site record is.
|
|
220
|
+
* Offsets index the ORIGINAL text and point at the specifier's own start, so
|
|
221
|
+
* the reported column is where the written name begins.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} csharpText Raw file contents.
|
|
224
|
+
* @returns {{ specifier: string, importableName: string|null, offset: number }[]}
|
|
225
|
+
*/
|
|
226
|
+
export function parseCSharpDirectiveSites(csharpText) {
|
|
227
|
+
const source = maskCSharpComments(csharpText);
|
|
228
|
+
const sites = [];
|
|
229
|
+
for (const match of source.matchAll(CS_USING_BODY)) {
|
|
230
|
+
const classified = classifyUsingBody(match[1]);
|
|
231
|
+
if (!classified) continue;
|
|
232
|
+
sites.push({
|
|
233
|
+
specifier: classified.specifier,
|
|
234
|
+
importableName: classified.importableName,
|
|
235
|
+
offset: match.index + match[0].indexOf(match[1]) + classified.specifierStartInBody,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
for (const match of source.matchAll(CS_EXTERN_ALIAS)) {
|
|
239
|
+
sites.push({
|
|
240
|
+
specifier: match[1],
|
|
241
|
+
// Extern aliases supply a ROOT name from outside the compilation's
|
|
242
|
+
// sources — resolution against the tracked tree cannot mean anything,
|
|
243
|
+
// so the site records the alias's own name and classifies external
|
|
244
|
+
// (documented limit). The name, not `extern alias X`, is the specifier:
|
|
245
|
+
// form words would silently exempt the site from every external ban.
|
|
246
|
+
importableName: null,
|
|
247
|
+
offset: match.index + match[0].indexOf(match[1]),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return sites.sort((a, b) => a.offset - b.offset);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Why a `.cs` file's directives cannot be fully read, as reasons for
|
|
255
|
+
* `analyzeCSharp` to record as whole-file failures (`contract.md`): a `using`
|
|
256
|
+
* or `extern alias` directive that never reaches its `;`.
|
|
257
|
+
*
|
|
258
|
+
* The detection mirrors the directive regexes' own failure conditions, so a
|
|
259
|
+
* file they read fully is never flagged. The openers are the body regexes'
|
|
260
|
+
* own heads, and the `;` is required to arrive before the next `{`: the brace
|
|
261
|
+
* a type body opens with separates a directive that terminated (`;` first)
|
|
262
|
+
* from one the file truncates — a failed write, a merge marker left
|
|
263
|
+
* mid-directive — which used to parse as zero directive sites with no
|
|
264
|
+
* failure, byte-for-byte identical to a file that imports nothing (#419).
|
|
265
|
+
* A `(` right after the
|
|
266
|
+
* keyword is the using-STATEMENT family — `using (var s = f()) { … }`,
|
|
267
|
+
* `using (x);`, and the `using var x = f();` declaration whose body starts
|
|
268
|
+
* `var` — never a directive; a statement's `;` may arrive inside its own
|
|
269
|
+
* block, so the scan has no opinion there. One silence the rule keeps: a
|
|
270
|
+
* missing `;` that a LATER declaration supplies its own (`extern alias X`
|
|
271
|
+
* then `namespace Shop.App;`) is not seen — the walk reads the next
|
|
272
|
+
* terminator, and attributing a `;` to its declaration is parser work the
|
|
273
|
+
* head regexes do not carry. The same mask the Java scan's terminator walk
|
|
274
|
+
* keeps.
|
|
275
|
+
*
|
|
276
|
+
* The mask runs first (the same `maskCSharpComments` the site parser runs),
|
|
277
|
+
* so directive-shaped text inside strings, raw strings and comments is never
|
|
278
|
+
* read as directive syntax and a compiling file is never reported as broken.
|
|
279
|
+
*
|
|
280
|
+
* The Go posture `goImportMalformations` set (#419's sibling audit): shapes
|
|
281
|
+
* the regexes answer are the documented parse limits; the shape they cannot
|
|
282
|
+
* answer is the failure.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} csharpText Raw file contents.
|
|
285
|
+
* @returns {string[]} At most one reason per kind — `using`, `extern alias` —
|
|
286
|
+
* each naming its line. Empty when the directives read fully.
|
|
287
|
+
*/
|
|
288
|
+
export function csharpDirectiveMalformations(csharpText) {
|
|
289
|
+
const source = maskCSharpComments(csharpText);
|
|
290
|
+
/** @type {string[]} */
|
|
291
|
+
const reasons = [];
|
|
292
|
+
const flagged = new Set();
|
|
293
|
+
const flag = (offset, kind, reason) => {
|
|
294
|
+
if (flagged.has(kind)) return;
|
|
295
|
+
flagged.add(kind);
|
|
296
|
+
reasons.push(`${reason} (line ${positionAt(csharpText, offset).line})`);
|
|
297
|
+
};
|
|
298
|
+
// `;` and `{` ascend with the text, and so do the openers, so each arm
|
|
299
|
+
// walks the same terminator list with its own forward cursor — one pass per
|
|
300
|
+
// arm, not an `indexOf` per opener that rescans the tail (`.cs` content is
|
|
301
|
+
// attacker-supplied per SECURITY.md). Both arms hold ONE rule, the using
|
|
302
|
+
// arm's: the directive's own `;` must arrive before the next `{`. An
|
|
303
|
+
// `indexOf`-style "a `;` exists somewhere later" is what the alias arm
|
|
304
|
+
// briefly held, and it is the weaker claim — a LATER declaration's `;`
|
|
305
|
+
// (`extern alias X` then `namespace Shop.App;`) masked the truncation and
|
|
306
|
+
// the silent direction survived it.
|
|
307
|
+
const terminators = [...source.matchAll(/[;{]/g)];
|
|
308
|
+
const terminatorAfter = () => {
|
|
309
|
+
let cursor = 0;
|
|
310
|
+
return (at) => {
|
|
311
|
+
while (cursor < terminators.length && terminators[cursor].index < at) cursor += 1;
|
|
312
|
+
return terminators[cursor];
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
// The matched spans start at their ANCHORS (a `\n`, `;`, `{` or `}`), so
|
|
316
|
+
// each reason locates the keyword inside the span rather than pointing at
|
|
317
|
+
// m.index — the anchor names the PREVIOUS line when it is a `\n`, and a
|
|
318
|
+
// diagnostic naming the wrong line sends every reader to the wrong
|
|
319
|
+
// directive. The same locate-it move `parseCSharpDirectiveSites` makes for
|
|
320
|
+
// the specifier.
|
|
321
|
+
const usingTerminatorAfter = terminatorAfter();
|
|
322
|
+
for (const m of source.matchAll(CS_USING_BODY_HEAD)) {
|
|
323
|
+
const at = m.index + m[0].length;
|
|
324
|
+
if (source[at] === "(") continue;
|
|
325
|
+
const next = usingTerminatorAfter(at);
|
|
326
|
+
if (next === undefined || next[0] === "{") {
|
|
327
|
+
flag(
|
|
328
|
+
m.index + m[0].indexOf("using"),
|
|
329
|
+
"using",
|
|
330
|
+
"a `using` directive never reaches its `;` — the file is truncated or malformed, so its imports cannot be read",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const externTerminatorAfter = terminatorAfter();
|
|
335
|
+
for (const m of source.matchAll(CS_EXTERN_ALIAS_HEAD)) {
|
|
336
|
+
const next = externTerminatorAfter(m.index + m[0].length);
|
|
337
|
+
if (next === undefined || next[0] === "{") {
|
|
338
|
+
flag(
|
|
339
|
+
m.index + m[0].indexOf("extern"),
|
|
340
|
+
"extern alias",
|
|
341
|
+
"an `extern alias` never reaches its `;` — the file is truncated or malformed, so its imports cannot be read",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return reasons;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The workspace's namespace index, built once per workspace object — the same
|
|
350
|
+
* map the graph resolver below reads, both layers share one answer about
|
|
351
|
+
* who owns a name.
|
|
352
|
+
*/
|
|
353
|
+
const csharpIndexOf = perWorkspace(csharpNamespaceIndex);
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Analyzes one `.cs` file.
|
|
357
|
+
*
|
|
358
|
+
* An ambiguous namespace (two tracked projects declaring the same deepest
|
|
359
|
+
* matched prefix) resolves to `resolved: null` WITH a positioned failure
|
|
360
|
+
* naming both projects — ordinary C#, unresolvable by static reading, where
|
|
361
|
+
* picking either side would report violations against a guess. Intra-project
|
|
362
|
+
* directives are emitted as records (`contract.md`), with
|
|
363
|
+
* `spelling.relative` true exactly there.
|
|
364
|
+
*
|
|
365
|
+
* @param {{ sourceFile: string, text: string, workspace: object }} request
|
|
366
|
+
* @returns {{ imports: object[], failures: object[] }}
|
|
367
|
+
*/
|
|
368
|
+
export function analyzeCSharp({ sourceFile, text, workspace }) {
|
|
369
|
+
const result = emptyResult();
|
|
370
|
+
try {
|
|
371
|
+
const { byName: index } = csharpIndexOf(workspace);
|
|
372
|
+
const owner = projectOwning(workspace.projects, sourceFile);
|
|
373
|
+
// A file truncated inside a directive used to parse as importing nothing,
|
|
374
|
+
// with no failure beside the empty result — the clean verdict over it was
|
|
375
|
+
// the bug (#419). The whole-file shape is what turns the verdict loud:
|
|
376
|
+
// `check` counts the file toward `unchecked` and refuses to call the run
|
|
377
|
+
// complete, instead of reporting a hole as a clean file.
|
|
378
|
+
for (const reason of csharpDirectiveMalformations(text)) {
|
|
379
|
+
result.failures.push(fileFailure(sourceFile, reason));
|
|
380
|
+
}
|
|
381
|
+
for (const site of parseCSharpDirectiveSites(text)) {
|
|
382
|
+
const { line, column } = positionAt(text, site.offset);
|
|
383
|
+
let resolution;
|
|
384
|
+
if (site.importableName === null) {
|
|
385
|
+
resolution = {
|
|
386
|
+
target: null,
|
|
387
|
+
file: null,
|
|
388
|
+
external: true,
|
|
389
|
+
packageName: site.specifier,
|
|
390
|
+
};
|
|
391
|
+
} else {
|
|
392
|
+
const resolved = resolveCsharpSpecifier(site.importableName, index);
|
|
393
|
+
if (resolved.external) {
|
|
394
|
+
// A name no tracked project claims: classified, never dropped, and
|
|
395
|
+
// deliberately NOT added as an externalNodes entry here — only
|
|
396
|
+
// project↔project edges matter to the graph (`AGENTS.md`).
|
|
397
|
+
resolution = {
|
|
398
|
+
target: null,
|
|
399
|
+
file: null,
|
|
400
|
+
external: true,
|
|
401
|
+
packageName: site.importableName,
|
|
402
|
+
};
|
|
403
|
+
} else if (resolved.ambiguous) {
|
|
404
|
+
resolution = null;
|
|
405
|
+
result.failures.push({
|
|
406
|
+
sourceFile,
|
|
407
|
+
line,
|
|
408
|
+
column,
|
|
409
|
+
reason:
|
|
410
|
+
`'${resolved.matchedPrefix}' is declared by more than one project ` +
|
|
411
|
+
`(${resolved.ambiguous.join(", ")}) — the compiler picks by reference order, ` +
|
|
412
|
+
`which this static reader does not model`,
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
resolution = { target: resolved.target, file: null, external: false, packageName: null };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const target = resolution?.target ?? null;
|
|
419
|
+
result.imports.push({
|
|
420
|
+
sourceFile,
|
|
421
|
+
line,
|
|
422
|
+
column,
|
|
423
|
+
specifier: site.specifier,
|
|
424
|
+
kind: "static",
|
|
425
|
+
spelling: {
|
|
426
|
+
path: false,
|
|
427
|
+
relative: target !== null && owner !== null && target === owner.name,
|
|
428
|
+
namesOnly: true,
|
|
429
|
+
},
|
|
430
|
+
resolved: resolution,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
} catch (cause) {
|
|
434
|
+
result.failures.push(fileFailure(sourceFile, `C# analysis failed: ${cause?.message ?? cause}`));
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Static edges between .NET projects derived from written directives — the
|
|
441
|
+
* source-truth half of the two-track principle. `../dotnet/csproj.mjs`'s
|
|
442
|
+
* ProjectReference resolver owns the manifest half; neither replaces the
|
|
443
|
+
* other. Takes the SAME workspace-shaped object that resolver receives: the
|
|
444
|
+
* namespace index both halves read is `perWorkspace`-cached on the object,
|
|
445
|
+
* so a graph computation builds it once no matter which half runs first.
|
|
446
|
+
*
|
|
447
|
+
* Returns raw Nx dependencies ({ source, target, sourceFile, type: "static" }).
|
|
448
|
+
* Ambiguous namespaces draw no edge — analysis reports them loudly instead,
|
|
449
|
+
* and an edge against a guess would be worse than the missing one. An
|
|
450
|
+
* unreadable `.cs` source refuses the whole graph (#364's posture — the
|
|
451
|
+
* index state corrupts every importer of its namespaces, so the failure
|
|
452
|
+
* cannot be attributed to the file's own edges), through the same
|
|
453
|
+
* `refuseUnreadTree` the manifest resolvers hold.
|
|
454
|
+
*
|
|
455
|
+
* @param {{ projects: {name: string, root: string}[], filesOf: (name: string) => string[],
|
|
456
|
+
* readFile: (path: string) => string|null }} workspace
|
|
457
|
+
* @returns {{ source: string, target: string, sourceFile: string, type: string }[]}
|
|
458
|
+
* @throws {Error} when `csharpNamespaceIndex` recorded any failure, naming
|
|
459
|
+
* each unreadable `.cs` source.
|
|
460
|
+
*/
|
|
461
|
+
export function resolveCsharpDependencies(workspace) {
|
|
462
|
+
const { byName: index, failures: indexFailures } = csharpNamespaceIndex(workspace);
|
|
463
|
+
refuseUnreadTree("the C# namespace index", indexFailures);
|
|
464
|
+
const dependencies = [];
|
|
465
|
+
for (const project of workspace.projects) {
|
|
466
|
+
for (const file of workspace.filesOf(project.name)) {
|
|
467
|
+
if (!file.endsWith(".cs")) continue;
|
|
468
|
+
const text = workspace.readFile(file);
|
|
469
|
+
if (text === null) continue;
|
|
470
|
+
for (const site of parseCSharpDirectiveSites(text)) {
|
|
471
|
+
if (site.importableName === null) continue;
|
|
472
|
+
const resolved = resolveCsharpSpecifier(site.importableName, index);
|
|
473
|
+
if (resolved.external || resolved.ambiguous) continue;
|
|
474
|
+
if (resolved.target === project.name) continue;
|
|
475
|
+
dependencies.push({
|
|
476
|
+
source: project.name,
|
|
477
|
+
target: resolved.target,
|
|
478
|
+
sourceFile: file,
|
|
479
|
+
type: "static",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return dependencies;
|
|
485
|
+
}
|