@descryy/adapter-csharp 0.1.0 → 0.3.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/LICENSE +8 -0
- package/dist/adapter.d.ts +3 -8
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +57 -65
- package/dist/adapter.js.map +1 -1
- package/dist/aspnetcore.d.ts +53 -95
- package/dist/aspnetcore.d.ts.map +1 -1
- package/dist/aspnetcore.js +56 -101
- package/dist/aspnetcore.js.map +1 -1
- package/dist/client-base.d.ts +51 -88
- package/dist/client-base.d.ts.map +1 -1
- package/dist/client-base.js +46 -79
- package/dist/client-base.js.map +1 -1
- package/dist/client.d.ts +42 -106
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +83 -172
- package/dist/client.js.map +1 -1
- package/dist/efcore.d.ts +59 -113
- package/dist/efcore.d.ts.map +1 -1
- package/dist/efcore.js +119 -204
- package/dist/efcore.js.map +1 -1
- package/dist/extract.d.ts +27 -50
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +154 -248
- package/dist/extract.js.map +1 -1
- package/dist/nullability.d.ts +46 -78
- package/dist/nullability.d.ts.map +1 -1
- package/dist/nullability.js +47 -80
- package/dist/nullability.js.map +1 -1
- package/dist/parse.d.ts +62 -102
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +60 -101
- package/dist/parse.js.map +1 -1
- package/dist/projects.d.ts +24 -40
- package/dist/projects.d.ts.map +1 -1
- package/dist/projects.js +38 -65
- package/dist/projects.js.map +1 -1
- package/package.json +15 -6
package/dist/extract.js
CHANGED
|
@@ -1,40 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* C# files to Canonical IR.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Identity is project + declared namespace, not namespace alone: namespaces
|
|
5
|
+
* don't follow directories and can span projects, so two `.csproj` files may
|
|
6
|
+
* both declare `Shop.Orders.OrderService` (DEC-077's collision — the project
|
|
7
|
+
* is C#'s equivalent of Java's source set).
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* collision — the Java case where one fully-qualified name existed under two
|
|
10
|
-
* source sets and stopped identifying anything — and the project is C#'s
|
|
11
|
-
* equivalent separator.
|
|
9
|
+
* DEC-083: `partial class` is the first multi-file declaration here. Parts
|
|
10
|
+
* merge into one node (ids never carry a file path) — `file` is the
|
|
11
|
+
* lexicographically first declaring path, every part listed in `attrs.declaredIn`.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* DEC-084: `class A : B, IC` doesn't say which entry is the base class, but
|
|
14
|
+
* the language does — at most one, and it's first. So later entries are
|
|
15
|
+
* `IMPLEMENTS` unconditionally, and only the first entry per class ever needs
|
|
16
|
+
* resolving. The `IFoo` naming convention is never read (`System.Exception`
|
|
17
|
+
* disproves it; golden pattern 12 punishes inferring edges from naming habits).
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* declaring path and every part is listed in `attrs.declaredIn`, which is what
|
|
20
|
-
* a precision adjudicator has to search instead of `file`.
|
|
21
|
-
*
|
|
22
|
-
* **DEC-084, the base list.** `class A : B, IC` does not say which entry is the
|
|
23
|
-
* class, but the language does: at most one base class, and it must be first.
|
|
24
|
-
* So entries after the first are `IMPLEMENTS` unconditionally, a struct's and an
|
|
25
|
-
* interface's entries are all `IMPLEMENTS`, and exactly one entry per class ever
|
|
26
|
-
* needs resolving. The `IFoo` prefix is never read — it is a convention,
|
|
27
|
-
* `System.Exception` disproves it in the other direction, and inferring an edge
|
|
28
|
-
* from a naming habit is what golden pattern 12 exists to punish.
|
|
29
|
-
*
|
|
30
|
-
* ## Ambiguity is disclosed, never broken
|
|
31
|
-
*
|
|
32
|
-
* A `using` imports a whole namespace, so a bare name can be found under two of
|
|
33
|
-
* them at once. Real C# resolves that with the full type environment; this
|
|
34
|
-
* adapter has names only, so two candidates produce a ledger entry and no edge.
|
|
35
|
-
* Extension methods are the same answer for the same reason: `x.Foo()` where
|
|
36
|
-
* `Foo` is a static method on an unrelated class, selected by the receiver's
|
|
37
|
-
* type, is not decidable from names.
|
|
19
|
+
* Ambiguity is disclosed, not guessed: a `using` imports a whole namespace, so
|
|
20
|
+
* a bare name can match two of them — real C# resolves via the full type
|
|
21
|
+
* environment, this adapter has names only, so two candidates ledger and mint
|
|
22
|
+
* no edge. Extension methods are the same call for the same reason.
|
|
38
23
|
*/
|
|
39
24
|
import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
|
|
40
25
|
import { efModels, isGenerated } from "./efcore.js";
|
|
@@ -46,13 +31,11 @@ export const LANGUAGE = "csharp";
|
|
|
46
31
|
const CONFIDENCE = { 0: 0.5, 1: 0.7, 2: 0.9, 3: 0.95 };
|
|
47
32
|
const REASONS = {
|
|
48
33
|
outside:
|
|
49
|
-
// Same correction as
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// C#. It is unverifiable either way from inside one adapter, and a
|
|
55
|
-
// disclosure must not assert what its author cannot check.
|
|
34
|
+
// Same correction as Java's, weaker evidence: Java's "scope boundary, not
|
|
35
|
+
// an analysis gap" was measured false for 238/458 entries on `okhttp`
|
|
36
|
+
// (Kotlin+Java). No C# corpus here holds another CLR language (.fs/.vb
|
|
37
|
+
// counts are 0 across all six), so unverifiable either way — don't assert
|
|
38
|
+
// what can't be checked.
|
|
56
39
|
"names a type or member this run did not analyse. Either a scope boundary — a NuGet " +
|
|
57
40
|
"package, the .NET base class library, a project outside the analysed set — or a " +
|
|
58
41
|
"declaration this run could not read, in another language in this same repository. " +
|
|
@@ -72,12 +55,9 @@ const REASONS = {
|
|
|
72
55
|
const INHERITANCE_HOPS = 12;
|
|
73
56
|
const PENDING_PASSES = 3;
|
|
74
57
|
/**
|
|
75
|
-
* Test attributes, by framework
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* This is the same shape Java's `@Test` already handles and it is reading an
|
|
79
|
-
* annotation, not guessing from a method name — C# has no naming convention for
|
|
80
|
-
* tests the way XCTest does.
|
|
58
|
+
* Test attributes, by framework: xUnit `[Fact]`/`[Theory]`, NUnit
|
|
59
|
+
* `[Test]`/`[TestCase]`, MSTest `[TestMethod]`. All declared in source, read
|
|
60
|
+
* as an annotation — C# has no naming convention for tests the way XCTest does.
|
|
81
61
|
*/
|
|
82
62
|
const TEST_ATTRIBUTES = new Set(["Fact", "Theory", "Test", "TestCase", "TestMethod", "DataTestMethod"]);
|
|
83
63
|
function qualify(namespace, path) {
|
|
@@ -90,41 +70,30 @@ export function extract(input) {
|
|
|
90
70
|
const unresolved = [];
|
|
91
71
|
const seenEdge = new Set();
|
|
92
72
|
const scope = input.scope;
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// and emitted with the routes below so both halves mint one `API_ENDPOINT`.
|
|
73
|
+
// Caller's half of the HTTP boundary — collected per-method, emitted with
|
|
74
|
+
// the routes below so both halves mint one `API_ENDPOINT`.
|
|
96
75
|
const clientCallSites = [];
|
|
97
76
|
/**
|
|
98
|
-
* `FUNCTION`/`TEST_CASE` node id -> its index in `nodes`, so
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* per `capabilities.ts`'s own "on every caller" phrasing.
|
|
77
|
+
* `FUNCTION`/`TEST_CASE` node id -> its index in `nodes`, so the client-base
|
|
78
|
+
* extractor (DEC-164) can patch `attrs.clientBase` onto the caller node once
|
|
79
|
+
* `clientCallSites` is known — golden patterns 08a/08b/08c bind `caller_*`
|
|
80
|
+
* to the `FUNCTION`, not the edge.
|
|
103
81
|
*/
|
|
104
82
|
const functionNodeIndex = new Map();
|
|
105
83
|
/** `FUNCTION`/`TEST_CASE` node id -> the `clientBase` this row attaches to it. First writer wins. */
|
|
106
84
|
const functionClientBase = new Map();
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// A MODEL is a MODEL rather than a CLASS, and the node id hashes the kind
|
|
112
|
-
// (DEC-004), so the kind has to be settled BEFORE the id exists. Both
|
|
113
|
-
// extractors also read across files — a `DbSet<Order>` names a type declared
|
|
114
|
-
// elsewhere, and the evidence making a class a controller is usually on a base
|
|
115
|
-
// in another file — so neither can run per-unit.
|
|
85
|
+
// Framework extractors, resolved for the whole batch before any id is minted:
|
|
86
|
+
// the node id hashes MODEL vs CLASS (DEC-004), so kind must be settled first,
|
|
87
|
+
// and both extractors read cross-file (a `DbSet<Order>` names a type declared
|
|
88
|
+
// elsewhere), so neither can run per-unit.
|
|
116
89
|
const sources = input.sources ?? new Map();
|
|
117
90
|
const frameworkFiles = input.files
|
|
118
91
|
.map((parsed) => ({ parsed, source: sources.get(parsed.file) ?? "" }))
|
|
119
92
|
.filter((each) => each.source !== "" && !isGenerated(each.parsed.file, each.source));
|
|
120
93
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* reachable from a `CsFile`, so a file with no directive resolves to `absent`
|
|
125
|
-
* and its reference-typed columns come out `unresolved` and counted. That is
|
|
126
|
-
* the honest degradation: unknown is not false, and DEC-180 §2 is the whole
|
|
127
|
-
* reason this is not allowed to guess.
|
|
94
|
+
* NRT context from directives alone (project/props aren't reachable from a
|
|
95
|
+
* `CsFile`), so an undirected file resolves `absent` and its reference-typed
|
|
96
|
+
* columns come out `unresolved`. Honest degradation — DEC-180 §2 forbids guessing.
|
|
128
97
|
*/
|
|
129
98
|
const contextOf = (file) => {
|
|
130
99
|
if (input.nullableContextOf !== undefined)
|
|
@@ -135,14 +104,13 @@ export function extract(input) {
|
|
|
135
104
|
return resolveNullable({ directives: nullableDirectives(source), project: "absent", directoryProps: "absent" }, source.split("\n").length).context;
|
|
136
105
|
};
|
|
137
106
|
const orm = efModels({ files: frameworkFiles, contextOf });
|
|
138
|
-
/**
|
|
107
|
+
/** Client-base extractor (DEC-164), the `AddHttpClient<T>(...)` half — see `client-base.ts`. */
|
|
139
108
|
const typedClientBaseMap = typedClientBases(frameworkFiles);
|
|
140
109
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* Razor `ViewComponent` carried the entity's columns.
|
|
110
|
+
* Qualified type name -> its mapping, for the CLASS/MODEL decision below.
|
|
111
|
+
* Keyed on the qualified name after a simple-name collision cost real data:
|
|
112
|
+
* two `Basket` types in one repo both became `MODEL`, and a Razor
|
|
113
|
+
* `ViewComponent` carried the entity's columns.
|
|
146
114
|
*/
|
|
147
115
|
const modelsByName = new Map(orm.models.map((model) => [model.fqn, model]));
|
|
148
116
|
const push = (edge, level) => {
|
|
@@ -169,32 +137,26 @@ export function extract(input) {
|
|
|
169
137
|
...(attrs === undefined ? {} : { attrs }),
|
|
170
138
|
});
|
|
171
139
|
};
|
|
172
|
-
//
|
|
173
|
-
// Nodes — types first, so partial parts merge before any member is placed
|
|
174
|
-
// -------------------------------------------------------------------------
|
|
140
|
+
// Nodes — types first, so partial parts merge before any member is placed.
|
|
175
141
|
/** Keyed by `assembly::FullyQualifiedName`, which is what makes it identity. */
|
|
176
142
|
const types = new Map();
|
|
177
143
|
/**
|
|
178
|
-
* The same names, keyed by fully-qualified name alone.
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* test project referencing the app project is the ordinary case — so keying
|
|
184
|
-
* resolution by assembly too would silently drop every cross-project edge.
|
|
144
|
+
* The same names, keyed by fully-qualified name alone. Identity and
|
|
145
|
+
* resolution want different indexes: assembly separates two declarations of
|
|
146
|
+
* one name (belongs in the id), but a `using` reaches across projects (a
|
|
147
|
+
* test project referencing the app is ordinary), so keying resolution by
|
|
148
|
+
* assembly too would drop cross-project edges.
|
|
185
149
|
*
|
|
186
|
-
* `<ProjectReference>`
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* disclose, not a tie to break.
|
|
150
|
+
* `<ProjectReference>` isn't read, so cross-project visibility is unknown.
|
|
151
|
+
* Precision rule: prefer a match in the referencing file's own assembly,
|
|
152
|
+
* else accept only if exactly one assembly declares the name — two
|
|
153
|
+
* claimants is an ambiguity to disclose, not a tie to break.
|
|
191
154
|
*/
|
|
192
155
|
const byFqn = new Map();
|
|
193
156
|
const moduleIdOf = new Map();
|
|
194
157
|
const moduleKeys = new Map();
|
|
195
|
-
// Sorted
|
|
196
|
-
//
|
|
197
|
-
// "lexicographically first" is only reproducible if the input is ordered.
|
|
158
|
+
// Sorted: DEC-083's "lexicographically first" is only reproducible if the
|
|
159
|
+
// input is ordered, not left to filesystem enumeration order.
|
|
198
160
|
const units = [...input.files].sort((a, b) => a.file.localeCompare(b.file));
|
|
199
161
|
const keyOf = (unit, fqn) => `${unit.assembly}::${fqn}`;
|
|
200
162
|
for (const unit of units) {
|
|
@@ -287,10 +249,9 @@ export function extract(input) {
|
|
|
287
249
|
: {
|
|
288
250
|
orm: "efcore",
|
|
289
251
|
...(model.table === null ? {} : { table: model.table }),
|
|
290
|
-
//
|
|
291
|
-
// declaration
|
|
292
|
-
//
|
|
293
|
-
// — the same exception `adapter-java` records for JPA.
|
|
252
|
+
// Not gated on `input.reached`: a mapped column is a framework
|
|
253
|
+
// declaration, not an inference — resolution is the evidence's,
|
|
254
|
+
// not the run's (DEC-058), same exception as adapter-java's JPA.
|
|
294
255
|
...(model.columns.length > 0
|
|
295
256
|
? {
|
|
296
257
|
fields: model.columns.map((column) => ({
|
|
@@ -333,10 +294,9 @@ export function extract(input) {
|
|
|
333
294
|
const id = isCase
|
|
334
295
|
? nodeId(scope, "TEST_CASE", testCaseQsp(holder, [unit.namespace, ...fn.ownerPath].filter((s) => s !== ""), fn.name), LANGUAGE)
|
|
335
296
|
: nodeId(scope, "FUNCTION", symbolQsp(holder, [ownerFqn, fn.name]), LANGUAGE);
|
|
336
|
-
// An overload set is one node
|
|
337
|
-
// types, which
|
|
338
|
-
//
|
|
339
|
-
// chosen an overload would be a claim it cannot support.
|
|
297
|
+
// An overload set is one node: choosing between overloads needs argument
|
|
298
|
+
// types (R3), which this adapter doesn't have — "the method called
|
|
299
|
+
// Format" is the honest claim.
|
|
340
300
|
if (owner.methods.has(fn.name))
|
|
341
301
|
continue;
|
|
342
302
|
owner.methods.set(fn.name, { id, fn, unit, ownerFqn, isTest: isCase });
|
|
@@ -358,24 +318,18 @@ export function extract(input) {
|
|
|
358
318
|
});
|
|
359
319
|
}
|
|
360
320
|
}
|
|
361
|
-
//
|
|
362
|
-
// Resolution
|
|
363
|
-
// -------------------------------------------------------------------------
|
|
321
|
+
// Resolution.
|
|
364
322
|
/**
|
|
365
|
-
* Every fully-qualified name a written name could denote, in this file.
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* ambiguity can be *seen* instead of silently resolved to whichever rule ran
|
|
371
|
-
* first.
|
|
323
|
+
* Every fully-qualified name a written name could denote, in this file. C#
|
|
324
|
+
* searches enclosing namespaces outward then imports; a name found under
|
|
325
|
+
* two imports is genuinely ambiguous without the full type environment, so
|
|
326
|
+
* candidates are collected rather than short-circuited — ambiguity is seen,
|
|
327
|
+
* not silently resolved to whichever rule ran first.
|
|
372
328
|
*/
|
|
373
329
|
/**
|
|
374
|
-
* `global using` declarations, by assembly
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
* and consulted for every unit — the alternative is that a name introduced by
|
|
378
|
-
* one resolves nowhere else, which is the whole point of the construct.
|
|
330
|
+
* `global using` declarations, by assembly — in force for every file in
|
|
331
|
+
* their project, gathered once. Otherwise a name introduced by one file
|
|
332
|
+
* would resolve nowhere else, defeating the construct's point.
|
|
379
333
|
*/
|
|
380
334
|
const globalUsings = new Map();
|
|
381
335
|
for (const unit of units) {
|
|
@@ -395,13 +349,10 @@ export function extract(input) {
|
|
|
395
349
|
return [];
|
|
396
350
|
const found = new Set();
|
|
397
351
|
const visible = usingsFor(unit);
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
// `
|
|
402
|
-
// namespace chain was walked. Innermost wins outright, as in C#: returning
|
|
403
|
-
// here rather than adding to `found` keeps a nested `Foo` from colliding
|
|
404
|
-
// with a namespace-level `Foo` and being reported ambiguous.
|
|
352
|
+
// Enclosing TYPES are searched before namespaces: a nested type used by
|
|
353
|
+
// its simple name from inside its declaring type resolved nowhere when
|
|
354
|
+
// only the namespace chain was walked. Innermost wins outright — return
|
|
355
|
+
// here (not `found`) so a nested `Foo` never collides with a namespace `Foo`.
|
|
405
356
|
if (host !== undefined) {
|
|
406
357
|
let scope_ = host.fqn;
|
|
407
358
|
while (scope_ !== "") {
|
|
@@ -504,22 +455,16 @@ export function extract(input) {
|
|
|
504
455
|
}
|
|
505
456
|
return undefined;
|
|
506
457
|
}
|
|
507
|
-
//
|
|
508
|
-
// D-FIX-3's client-base extractor (DEC-164) — see `client-base.ts`'s own
|
|
509
|
-
// header for the shape and why it is scoped exactly here.
|
|
510
|
-
// ---------------------------------------------------------------------------
|
|
458
|
+
// Client-base extractor (DEC-164) — see `client-base.ts` for shape and scope.
|
|
511
459
|
/**
|
|
512
|
-
* Does `rawName`'s
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
* directly or through one further in-repo indirection?
|
|
460
|
+
* Does `rawName`'s declaration (resolved in `unit`/`host`'s context, same
|
|
461
|
+
* as any other type reference) carry `WebApplicationFactory<` in its
|
|
462
|
+
* written base list, directly or via one further in-repo indirection?
|
|
516
463
|
*
|
|
517
|
-
* `WebApplicationFactory<TEntryPoint>` is
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
* that final hop, the same reason `baseListWritten` exists rather than
|
|
522
|
-
* `baseList` alone (see `CsType.baseListWritten`'s own doc).
|
|
464
|
+
* `WebApplicationFactory<TEntryPoint>` is external (`Microsoft.AspNetCore.Mvc.Testing`),
|
|
465
|
+
* so `resolveType`/`ancestry` never yield it — this reads the written text
|
|
466
|
+
* of each yielded type's base list instead, same reason `baseListWritten`
|
|
467
|
+
* exists rather than `baseList` alone.
|
|
523
468
|
*/
|
|
524
469
|
function derivesFromWebApplicationFactory(rawName, unit, host) {
|
|
525
470
|
const resolved = resolveType(rawName, unit, host);
|
|
@@ -534,10 +479,10 @@ export function extract(input) {
|
|
|
534
479
|
return false;
|
|
535
480
|
}
|
|
536
481
|
/**
|
|
537
|
-
* The one traceable base-setting mechanism
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
482
|
+
* The one traceable base-setting mechanism built here. `receiverType` is
|
|
483
|
+
* the written type name of whatever received `.CreateClient()` — see
|
|
484
|
+
* `client-base.ts` for why `IHttpClientFactory`'s named-client form and
|
|
485
|
+
* other DI shapes are deliberately left untouched.
|
|
541
486
|
*/
|
|
542
487
|
function creatorBaseOf(receiverType, unit, host) {
|
|
543
488
|
if (receiverType === undefined)
|
|
@@ -545,21 +490,17 @@ export function extract(input) {
|
|
|
545
490
|
if (!derivesFromWebApplicationFactory(receiverType, unit, host))
|
|
546
491
|
return undefined;
|
|
547
492
|
// `WebApplicationFactory<T>.CreateClient()`'s documented default
|
|
548
|
-
// `BaseAddress` is `http://localhost
|
|
549
|
-
//
|
|
550
|
-
// is never constructed with its own `BaseAddress` in any of the six), so
|
|
551
|
-
// that override is not read here — disclosed, not silently assumed.
|
|
493
|
+
// `BaseAddress` is `http://localhost/`, origin only. No corpus witness
|
|
494
|
+
// overrides it, so the override path isn't read — disclosed, not assumed.
|
|
552
495
|
return { state: "resolved", path: "" };
|
|
553
496
|
}
|
|
554
497
|
/**
|
|
555
|
-
* `var client = _factory.CreateClient();` —
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* `
|
|
560
|
-
*
|
|
561
|
-
* not even ledgered. Mutates `scope_` in place (same contract `settle`
|
|
562
|
-
* already has) and returns the per-local bases this pass resolved.
|
|
498
|
+
* `var client = _factory.CreateClient();` — read from `scope_.pending`
|
|
499
|
+
* before `settle()` runs, because `settle()`'s fallback nulls exactly this:
|
|
500
|
+
* `CreateClient` is never declared in-repo (inherited from an external
|
|
501
|
+
* base), so every such local was permanently unresolved, invisible to
|
|
502
|
+
* `clientCalls`, before this existed. Mutates `scope_` in place and returns
|
|
503
|
+
* the per-local bases resolved.
|
|
563
504
|
*/
|
|
564
505
|
function resolveClientBaseLocals(scope_, unit, host) {
|
|
565
506
|
const bases = new Map();
|
|
@@ -588,11 +529,9 @@ export function extract(input) {
|
|
|
588
529
|
}
|
|
589
530
|
/**
|
|
590
531
|
* A property or field's own base, traced once per `DeclaredType` and
|
|
591
|
-
* cached
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* writer for a given name wins, matching `findMember`'s own "nearest
|
|
595
|
-
* declaration" precedence.
|
|
532
|
+
* cached. Two shapes: a field's inline initialiser (`memberProvenance`) or
|
|
533
|
+
* a `.ctor`'s assignment (`propertyAssignments`) — first writer wins,
|
|
534
|
+
* matching `findMember`'s "nearest declaration" precedence.
|
|
596
535
|
*/
|
|
597
536
|
const memberClientBaseCache = new Map();
|
|
598
537
|
function memberClientBase(declared) {
|
|
@@ -657,18 +596,15 @@ export function extract(input) {
|
|
|
657
596
|
return resolveType(written, unit, host);
|
|
658
597
|
}
|
|
659
598
|
/**
|
|
660
|
-
* The receiver's type
|
|
661
|
-
* `HttpClient` has none in this
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
* type's own members), same `null`-is-poisoned handling, no `resolveType` call.
|
|
599
|
+
* The receiver's type as written, without resolving to a declaration —
|
|
600
|
+
* `HttpClient` has none in this repo, so `ownerOfReceiver` would refuse it.
|
|
601
|
+
* This needs the name, not the node: same lookup order (`locals`, then host
|
|
602
|
+
* members), same `null`-is-poisoned handling, no `resolveType` call.
|
|
665
603
|
*
|
|
666
|
-
* `ref.receiverProperty`
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* unresolved for the same reason Java's chained builder is: no receiver here
|
|
671
|
-
* has a written type, so nothing is silently guessed.
|
|
604
|
+
* Disclosed limit: `ref.receiverProperty` resolves only through `this`
|
|
605
|
+
* (`this._client.Get()`), not an arbitrary chain (`_service.Client.Get()`).
|
|
606
|
+
* `ref.receiverCall` (`Factory.CreateClient().Get()`) stays unresolved for
|
|
607
|
+
* the same reason as Java's chained builder — nothing is silently guessed.
|
|
672
608
|
*/
|
|
673
609
|
function writtenReceiverType(ref, scope_, host) {
|
|
674
610
|
if (ref.receiverType !== undefined)
|
|
@@ -720,16 +656,14 @@ export function extract(input) {
|
|
|
720
656
|
scope_.locals.set(name, null);
|
|
721
657
|
scope_.pending.clear();
|
|
722
658
|
}
|
|
723
|
-
//
|
|
724
|
-
// Edges
|
|
725
|
-
// -------------------------------------------------------------------------
|
|
659
|
+
// Edges.
|
|
726
660
|
for (const unit of units) {
|
|
727
661
|
const moduleId = moduleIdOf.get(unit.file);
|
|
728
662
|
if (moduleId === undefined)
|
|
729
663
|
continue;
|
|
730
|
-
// IMPORTS. A
|
|
731
|
-
// fans out
|
|
732
|
-
//
|
|
664
|
+
// IMPORTS. A `using` names a namespace, so like Go's package import it
|
|
665
|
+
// fans out to every file declaring a type in it — not an approximation,
|
|
666
|
+
// importing a namespace makes every type in it visible.
|
|
733
667
|
for (const using of unit.usings) {
|
|
734
668
|
const targets = new Set();
|
|
735
669
|
for (const declared of types.values()) {
|
|
@@ -769,30 +703,23 @@ export function extract(input) {
|
|
|
769
703
|
const declared = host?.methods.get(fn.name);
|
|
770
704
|
if (declared === undefined || declared.unit.file !== unit.file || declared.fn !== fn)
|
|
771
705
|
continue;
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
// *before* `settle()` runs — `settle()`'s own fallback nulls out
|
|
775
|
-
// anything it cannot resolve, and `CreateClient` is never declared
|
|
776
|
-
// in-repo (inherited from an external base), so every such local
|
|
777
|
-
// stayed permanently, silently unresolved before this pass existed.
|
|
706
|
+
// Client-base extractor (DEC-164): must run before `settle()`, whose
|
|
707
|
+
// fallback nulls out anything unresolved (see `resolveClientBaseLocals`).
|
|
778
708
|
const localClientBase = resolveClientBaseLocals(fn.scope, unit, host);
|
|
779
709
|
settle(fn.scope, unit, host);
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
// separation Java's `client.ts` keeps from `extract.ts`'s general
|
|
784
|
-
// `CALLS` handling.
|
|
710
|
+
// Caller's half of the HTTP boundary (DEC-241 §4). Read alongside the
|
|
711
|
+
// general ref loop, not inside it — `clientCalls` owns its own
|
|
712
|
+
// candidate predicate, same separation Java's `client.ts` keeps.
|
|
785
713
|
{
|
|
786
714
|
const clientBaseOf = (ref) => {
|
|
787
715
|
// A local traced through `resolveClientBaseLocals` above.
|
|
788
716
|
const local = localClientBase.get(ref.receiver);
|
|
789
717
|
if (local !== undefined)
|
|
790
718
|
return local;
|
|
791
|
-
// `this.Client` / a bare `Client` reaching a
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
// member is never misattributed to the member's own base.
|
|
719
|
+
// `this.Client` / a bare `Client` reaching a host member. Only
|
|
720
|
+
// counts when the receiver isn't also a local or parameter — same
|
|
721
|
+
// precedence `writtenReceiverType` applies, so a shadowing local is
|
|
722
|
+
// never misattributed to the member's own base.
|
|
796
723
|
const propertyName = ref.receiverProperty !== undefined
|
|
797
724
|
? ref.receiverProperty.of === "this"
|
|
798
725
|
? ref.receiverProperty.name
|
|
@@ -805,12 +732,10 @@ export function extract(input) {
|
|
|
805
732
|
if (member !== undefined)
|
|
806
733
|
return member;
|
|
807
734
|
}
|
|
808
|
-
// `AddHttpClient<TClient>(...)
|
|
809
|
-
// straight to `TClient`'s
|
|
810
|
-
// `TClient`
|
|
811
|
-
//
|
|
812
|
-
// `typedClientBases`'s own doc for why this is textual rather than
|
|
813
|
-
// a full type resolution.
|
|
735
|
+
// `AddHttpClient<TClient>(...)`: DI hands the configured client
|
|
736
|
+
// straight to `TClient`'s constructor, so an untraced receiver
|
|
737
|
+
// inside `TClient` is exactly that typed client — matched against
|
|
738
|
+
// the host's simple name (see `typedClientBases` for why textual).
|
|
814
739
|
if (host !== undefined) {
|
|
815
740
|
const simple = host.fqn.slice(host.fqn.lastIndexOf(".") + 1);
|
|
816
741
|
const typed = typedClientBaseMap.get(simple);
|
|
@@ -826,11 +751,9 @@ export function extract(input) {
|
|
|
826
751
|
functionClientBase.set(call.fromId, call.clientBase);
|
|
827
752
|
}
|
|
828
753
|
for (const refusal of read.refusals) {
|
|
829
|
-
// A refusal still carries `clientBase` when DEC-164's
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
// no `USES_API` edge exists to look at, so the node is the only
|
|
833
|
-
// place that fact can live.
|
|
754
|
+
// A refusal still carries `clientBase` when DEC-164's `unresolved`
|
|
755
|
+
// state is why it refused — golden 08b binds `caller_unresolved_base`
|
|
756
|
+
// to the `FUNCTION` node, the only place the fact can live.
|
|
834
757
|
if (refusal.clientBase !== undefined && !functionClientBase.has(refusal.fromId)) {
|
|
835
758
|
functionClientBase.set(refusal.fromId, refusal.clientBase);
|
|
836
759
|
}
|
|
@@ -869,11 +792,9 @@ export function extract(input) {
|
|
|
869
792
|
}
|
|
870
793
|
}
|
|
871
794
|
/**
|
|
872
|
-
* DEC-084 in one function
|
|
873
|
-
*
|
|
874
|
-
*
|
|
875
|
-
* one base class and it must come first. So only the first entry of a class
|
|
876
|
-
* base list is ever ambiguous, and everything else is decided by the language.
|
|
795
|
+
* DEC-084 in one function: base-list position is a language guarantee, not
|
|
796
|
+
* a convention, so only the first entry of a class base list is ever
|
|
797
|
+
* ambiguous — everything else is decided by the language.
|
|
877
798
|
*/
|
|
878
799
|
function emitBaseList(declared, part, unit) {
|
|
879
800
|
part.baseList.forEach((raw, index) => {
|
|
@@ -994,31 +915,24 @@ export function extract(input) {
|
|
|
994
915
|
}
|
|
995
916
|
return ownerOfReceiver(ref.receiver, scope_, unit, host);
|
|
996
917
|
}
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
// R2 and no lower, for the reason `adapter-go` records and `adapter-java`
|
|
1002
|
-
// repeats: telling a controller from an ordinary class is provenance, and a
|
|
1003
|
-
// route emitted from the R0 or R1 rung is a claim stronger than the run that
|
|
1004
|
-
// produced it — which the Normaliser rejects as RESOLUTION_EXCEEDS_BATCH.
|
|
918
|
+
// Routes. R2 and no lower (adapter-go/adapter-java's reason too): telling a
|
|
919
|
+
// controller from an ordinary class is provenance, and an R0/R1 route is a
|
|
920
|
+
// stronger claim than the run supports — Normaliser rejects it as
|
|
921
|
+
// RESOLUTION_EXCEEDS_BATCH.
|
|
1005
922
|
//
|
|
1006
|
-
// Gated on `reached >= 2` alone, not
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
// the route loop itself stays gated on ASP.NET files being present.
|
|
923
|
+
// Gated on `reached >= 2` alone, not `frameworkFiles.length > 0`: a repo can
|
|
924
|
+
// make outbound `HttpClient` calls without being an ASP.NET server itself,
|
|
925
|
+
// so the caller's half must not depend on route files existing. Only the
|
|
926
|
+
// route loop itself is gated on ASP.NET files being present.
|
|
1011
927
|
if (input.reached >= 2) {
|
|
1012
928
|
const routeNodes = new Map();
|
|
1013
929
|
const endpointNodes = new Map();
|
|
1014
930
|
if (frameworkFiles.length > 0) {
|
|
1015
931
|
const census = aspNetRoutes({ files: frameworkFiles });
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
// is the edge type this class's controller status would have produced
|
|
1021
|
-
// evidence for, had the base resolved.
|
|
932
|
+
// `census.unresolvedBaseSites` was computed correctly but never reached
|
|
933
|
+
// this ledger before — a real refusal that was still a silence from
|
|
934
|
+
// `emit()`'s caller's point of view. `SERVES_API` is the edge type this
|
|
935
|
+
// class's controller status would have produced, had the base resolved.
|
|
1022
936
|
const unitByFile = new Map(units.map((each) => [each.file, each]));
|
|
1023
937
|
for (const site of census.unresolvedBaseSites) {
|
|
1024
938
|
const unit = unitByFile.get(site.file);
|
|
@@ -1055,10 +969,9 @@ export function extract(input) {
|
|
|
1055
969
|
},
|
|
1056
970
|
});
|
|
1057
971
|
}
|
|
1058
|
-
// Workspace-scoped, fileless, language-less,
|
|
1059
|
-
// `endpointQsp` every
|
|
1060
|
-
//
|
|
1061
|
-
// they silently fail to join.
|
|
972
|
+
// Workspace-scoped, fileless, language-less, minted with the SAME
|
|
973
|
+
// `endpointQsp` every producer uses (DEC-115): mismatched ids don't
|
|
974
|
+
// conflict, they silently fail to join.
|
|
1062
975
|
const endpointId = nodeId(scope, "API_ENDPOINT", endpointQsp(route.method, route.written), null);
|
|
1063
976
|
if (!endpointNodes.has(endpointId)) {
|
|
1064
977
|
endpointNodes.set(endpointId, {
|
|
@@ -1076,12 +989,10 @@ export function extract(input) {
|
|
|
1076
989
|
push({ from: routeId, to: endpointId, type: "SERVES_API" }, 2);
|
|
1077
990
|
}
|
|
1078
991
|
}
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
//
|
|
1083
|
-
// one each. A call to a route this run did *not* read still mints its
|
|
1084
|
-
// endpoint — either side can exist without the other (golden 08's note).
|
|
992
|
+
// Caller's half. Mints into the SAME `endpointNodes` map, so a call to a
|
|
993
|
+
// route this run also read produces one node with two edges, not two. A
|
|
994
|
+
// call to an unread route still mints its endpoint — either side can
|
|
995
|
+
// exist without the other (golden 08).
|
|
1085
996
|
for (const call of clientCallSites) {
|
|
1086
997
|
const endpoint = endpointFor(scope, call, input.producedBy, normaliseEndpointPath);
|
|
1087
998
|
if (!endpointNodes.has(endpoint.id))
|
|
@@ -1090,24 +1001,19 @@ export function extract(input) {
|
|
|
1090
1001
|
from: call.fromId,
|
|
1091
1002
|
to: endpoint.id,
|
|
1092
1003
|
type: "USES_API",
|
|
1093
|
-
// DEC-164
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
// `client-base.ts`'s own header for why `unresolved` is never
|
|
1098
|
-
// produced by this build.
|
|
1004
|
+
// DEC-164: every `USES_API` edge carries `clientBase` — `resolved`
|
|
1005
|
+
// (traced base, DEC-097) or `none` (no traced mechanism, literal
|
|
1006
|
+
// path stands alone). See `client-base.ts` for why `unresolved` is
|
|
1007
|
+
// never produced by this build.
|
|
1099
1008
|
attrs: { callerKind: call.callerKind, via: call.method, clientBase: call.clientBase },
|
|
1100
1009
|
}, 2);
|
|
1101
1010
|
}
|
|
1102
1011
|
nodes.push(...routeNodes.values(), ...endpointNodes.values());
|
|
1103
1012
|
}
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
//
|
|
1108
|
-
// Done here, after every unit's calls and refusals are known, because
|
|
1109
|
-
// `functionNodeIndex` and `functionClientBase` are both built incrementally
|
|
1110
|
-
// across the whole loop above.
|
|
1013
|
+
// Client-base extractor (DEC-164): patch `attrs.clientBase` onto the caller
|
|
1014
|
+
// node itself, not just the edge — golden 08a/08b/08c bind `caller_*` to
|
|
1015
|
+
// the node, and 08b's refused call has no edge to carry the fact. Done here
|
|
1016
|
+
// since `functionNodeIndex`/`functionClientBase` build incrementally above.
|
|
1111
1017
|
for (const [fnId, base] of functionClientBase) {
|
|
1112
1018
|
const index = functionNodeIndex.get(fnId);
|
|
1113
1019
|
const node = index === undefined ? undefined : nodes[index];
|