@descryy/adapter-csharp 0.4.1 → 0.5.1
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/adapter.d.ts.map +1 -1
- package/dist/adapter.js +68 -9
- package/dist/adapter.js.map +1 -1
- package/dist/aspnetcore.d.ts +35 -0
- package/dist/aspnetcore.d.ts.map +1 -1
- package/dist/aspnetcore.js +186 -6
- package/dist/aspnetcore.js.map +1 -1
- package/dist/client.d.ts +18 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -1
- package/dist/dto.d.ts +73 -0
- package/dist/dto.d.ts.map +1 -0
- package/dist/dto.js +205 -0
- package/dist/dto.js.map +1 -0
- package/dist/extract.d.ts +27 -0
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +212 -9
- package/dist/extract.js.map +1 -1
- package/dist/json.d.ts +50 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +60 -0
- package/dist/json.js.map +1 -0
- package/dist/parse.d.ts +31 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +39 -5
- package/dist/parse.js.map +1 -1
- package/dist/projects.d.ts +0 -2
- package/dist/projects.d.ts.map +1 -1
- package/dist/projects.js +1 -5
- package/dist/projects.js.map +1 -1
- package/dist/shape-reach.d.ts +22 -4
- package/dist/shape-reach.d.ts.map +1 -1
- package/dist/shape-reach.js +36 -5
- package/dist/shape-reach.js.map +1 -1
- package/package.json +6 -6
package/dist/dto.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DTO` for C# — the transport shapes a ROUTE declares, never a type that
|
|
3
|
+
* looks like one.
|
|
4
|
+
*
|
|
5
|
+
* ## The rule this keeps
|
|
6
|
+
*
|
|
7
|
+
* DEC-043 refused `DTO` because the corpus supplies structurally identical
|
|
8
|
+
* types separable only by name, and `*Request`/`*Response` -> `DTO` is a
|
|
9
|
+
* heuristic. **Nothing in this file reads the candidate type to decide whether
|
|
10
|
+
* it is one.** A type becomes a `DTO` because an ASP.NET Core controller names
|
|
11
|
+
* it, under two rules the framework states itself:
|
|
12
|
+
*
|
|
13
|
+
* - a controller action's **return type is the response body** — a
|
|
14
|
+
* `[ApiController]` (or `ControllerBase`-derived) action is serialized
|
|
15
|
+
* straight to the response, with no view resolution in the path;
|
|
16
|
+
* - **`[FromBody]` on a parameter** says the parameter's type is bound from
|
|
17
|
+
* the request body.
|
|
18
|
+
*
|
|
19
|
+
* Both markers are on the action. The entity beside them keeps its fields and
|
|
20
|
+
* its name and stays a `CLASS`, which is the discrimination golden pattern 07
|
|
21
|
+
* exists to test.
|
|
22
|
+
*
|
|
23
|
+
* ## Refusals
|
|
24
|
+
*
|
|
25
|
+
* - **`[FromRoute]`/`[FromQuery]`/`[FromHeader]`/`[FromServices]` are
|
|
26
|
+
* parameter attributes and are not bodies**, and an unattributed parameter
|
|
27
|
+
* declares nothing either. `[ApiController]`'s binding *inference* would
|
|
28
|
+
* treat a complex-typed parameter as a body with no attribute at all; that
|
|
29
|
+
* is real ASP.NET and deliberately NOT read here — it is an inference rule
|
|
30
|
+
* layered on top of the written one, and admitting it would mint a `DTO` for
|
|
31
|
+
* every service and options object an action happens to take.
|
|
32
|
+
* - **A framework or built-in return type names no shape**: `IActionResult`,
|
|
33
|
+
* `ActionResult<T>`, `Task<T>`, `string`, `void`, a collection. `T` is a
|
|
34
|
+
* generic argument and resolving it is a read this file does not do, so it
|
|
35
|
+
* refuses rather than minting the wrapper.
|
|
36
|
+
* - **A type resolving to no declaration here, or to two, is refused** — a
|
|
37
|
+
* `DTO` with no shape joins nothing, and the wrong one carries the wrong
|
|
38
|
+
* shape into a comparison.
|
|
39
|
+
* - **An EF Core entity stays a `MODEL`** (applied by the caller, which holds
|
|
40
|
+
* both maps): a wrong `MODEL` is a claim about a database, a wrong `DTO` a
|
|
41
|
+
* claim about a wire format.
|
|
42
|
+
*
|
|
43
|
+
* ## Field NAMES, and deliberately not field detail
|
|
44
|
+
*
|
|
45
|
+
* `attrs.fields` carries declared member names — a name-level fact, which rule
|
|
46
|
+
* 3 permits at R2. No `fieldDetail`, no `shapeResolution`, no resolution
|
|
47
|
+
* raise, and `shape-reach.ts` untouched: whether a plain class body's member
|
|
48
|
+
* list is R3-grade evidence is a filed, unanswered question with fleet-wide
|
|
49
|
+
* blast radius. Golden 05's note asks for exactly this shape.
|
|
50
|
+
*/
|
|
51
|
+
import type { CsFile } from "./parse.ts";
|
|
52
|
+
/** The fully-qualified name, spelled exactly as `extract.ts`'s `qualify` does —
|
|
53
|
+
* the key `modelsByName` and the type table both use, so one lookup answers both.
|
|
54
|
+
* Exported for `shape-reach.ts`/`json.ts`, which need the identical key at
|
|
55
|
+
* `prepare()` time, before `extract.ts`'s own copy has run. */
|
|
56
|
+
export declare function qualify(namespace: string, path: readonly string[]): string;
|
|
57
|
+
/** The declaration marker every `DTO` this file mints names in `attrs.schema`. */
|
|
58
|
+
export declare const SCHEMA = "aspnetcore";
|
|
59
|
+
export interface TransportShape {
|
|
60
|
+
/** Declared member names, sorted. Names only — see the header. */
|
|
61
|
+
readonly fields: readonly string[];
|
|
62
|
+
readonly roles: readonly ("request" | "response")[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every type an ASP.NET action declares as a request or response body, across
|
|
66
|
+
* the whole batch. Keyed by the same `path.join(".")` identity the caller's
|
|
67
|
+
* own type map uses.
|
|
68
|
+
*
|
|
69
|
+
* Resolution is by simple name against the declared types — the reach this
|
|
70
|
+
* adapter has. A name declared twice is refused rather than ranked.
|
|
71
|
+
*/
|
|
72
|
+
export declare function readTransportShapes(files: readonly CsFile[]): ReadonlyMap<string, TransportShape>;
|
|
73
|
+
//# sourceMappingURL=dto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../src/dto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,YAAY,CAAC;AAEjD;;;gEAGgE;AAChE,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAG1E;AAED,kFAAkF;AAClF,eAAO,MAAM,MAAM,eAAe,CAAC;AAoEnC,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,EAAE,CAAC;CACrD;AAwBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAgDrC"}
|
package/dist/dto.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DTO` for C# — the transport shapes a ROUTE declares, never a type that
|
|
3
|
+
* looks like one.
|
|
4
|
+
*
|
|
5
|
+
* ## The rule this keeps
|
|
6
|
+
*
|
|
7
|
+
* DEC-043 refused `DTO` because the corpus supplies structurally identical
|
|
8
|
+
* types separable only by name, and `*Request`/`*Response` -> `DTO` is a
|
|
9
|
+
* heuristic. **Nothing in this file reads the candidate type to decide whether
|
|
10
|
+
* it is one.** A type becomes a `DTO` because an ASP.NET Core controller names
|
|
11
|
+
* it, under two rules the framework states itself:
|
|
12
|
+
*
|
|
13
|
+
* - a controller action's **return type is the response body** — a
|
|
14
|
+
* `[ApiController]` (or `ControllerBase`-derived) action is serialized
|
|
15
|
+
* straight to the response, with no view resolution in the path;
|
|
16
|
+
* - **`[FromBody]` on a parameter** says the parameter's type is bound from
|
|
17
|
+
* the request body.
|
|
18
|
+
*
|
|
19
|
+
* Both markers are on the action. The entity beside them keeps its fields and
|
|
20
|
+
* its name and stays a `CLASS`, which is the discrimination golden pattern 07
|
|
21
|
+
* exists to test.
|
|
22
|
+
*
|
|
23
|
+
* ## Refusals
|
|
24
|
+
*
|
|
25
|
+
* - **`[FromRoute]`/`[FromQuery]`/`[FromHeader]`/`[FromServices]` are
|
|
26
|
+
* parameter attributes and are not bodies**, and an unattributed parameter
|
|
27
|
+
* declares nothing either. `[ApiController]`'s binding *inference* would
|
|
28
|
+
* treat a complex-typed parameter as a body with no attribute at all; that
|
|
29
|
+
* is real ASP.NET and deliberately NOT read here — it is an inference rule
|
|
30
|
+
* layered on top of the written one, and admitting it would mint a `DTO` for
|
|
31
|
+
* every service and options object an action happens to take.
|
|
32
|
+
* - **A framework or built-in return type names no shape**: `IActionResult`,
|
|
33
|
+
* `ActionResult<T>`, `Task<T>`, `string`, `void`, a collection. `T` is a
|
|
34
|
+
* generic argument and resolving it is a read this file does not do, so it
|
|
35
|
+
* refuses rather than minting the wrapper.
|
|
36
|
+
* - **A type resolving to no declaration here, or to two, is refused** — a
|
|
37
|
+
* `DTO` with no shape joins nothing, and the wrong one carries the wrong
|
|
38
|
+
* shape into a comparison.
|
|
39
|
+
* - **An EF Core entity stays a `MODEL`** (applied by the caller, which holds
|
|
40
|
+
* both maps): a wrong `MODEL` is a claim about a database, a wrong `DTO` a
|
|
41
|
+
* claim about a wire format.
|
|
42
|
+
*
|
|
43
|
+
* ## Field NAMES, and deliberately not field detail
|
|
44
|
+
*
|
|
45
|
+
* `attrs.fields` carries declared member names — a name-level fact, which rule
|
|
46
|
+
* 3 permits at R2. No `fieldDetail`, no `shapeResolution`, no resolution
|
|
47
|
+
* raise, and `shape-reach.ts` untouched: whether a plain class body's member
|
|
48
|
+
* list is R3-grade evidence is a filed, unanswered question with fleet-wide
|
|
49
|
+
* blast radius. Golden 05's note asks for exactly this shape.
|
|
50
|
+
*/
|
|
51
|
+
/** The fully-qualified name, spelled exactly as `extract.ts`'s `qualify` does —
|
|
52
|
+
* the key `modelsByName` and the type table both use, so one lookup answers both.
|
|
53
|
+
* Exported for `shape-reach.ts`/`json.ts`, which need the identical key at
|
|
54
|
+
* `prepare()` time, before `extract.ts`'s own copy has run. */
|
|
55
|
+
export function qualify(namespace, path) {
|
|
56
|
+
const joined = path.join(".");
|
|
57
|
+
return namespace === "" ? joined : `${namespace}.${joined}`;
|
|
58
|
+
}
|
|
59
|
+
/** The declaration marker every `DTO` this file mints names in `attrs.schema`. */
|
|
60
|
+
export const SCHEMA = "aspnetcore";
|
|
61
|
+
/** ASP.NET's own statement that a parameter is bound from the request body. */
|
|
62
|
+
const BODY_PARAMETER = "FromBody";
|
|
63
|
+
/** Action attributes that make a method a route handler at all. */
|
|
64
|
+
const VERB_ATTRIBUTES = new Set([
|
|
65
|
+
"HttpGet",
|
|
66
|
+
"HttpPost",
|
|
67
|
+
"HttpPut",
|
|
68
|
+
"HttpPatch",
|
|
69
|
+
"HttpDelete",
|
|
70
|
+
"HttpHead",
|
|
71
|
+
"HttpOptions",
|
|
72
|
+
"Route",
|
|
73
|
+
]);
|
|
74
|
+
/**
|
|
75
|
+
* Framework plumbing and built-ins. A return type here is refused: the real
|
|
76
|
+
* payload of `ActionResult<T>`/`Task<T>` is its type argument, and this reader
|
|
77
|
+
* does not resolve generics.
|
|
78
|
+
*/
|
|
79
|
+
const NOT_A_SHAPE = new Set([
|
|
80
|
+
"IActionResult",
|
|
81
|
+
"ActionResult",
|
|
82
|
+
"Task",
|
|
83
|
+
"ValueTask",
|
|
84
|
+
"IResult",
|
|
85
|
+
"JsonResult",
|
|
86
|
+
"ObjectResult",
|
|
87
|
+
"ContentResult",
|
|
88
|
+
"FileResult",
|
|
89
|
+
"IEnumerable",
|
|
90
|
+
"ICollection",
|
|
91
|
+
"IList",
|
|
92
|
+
"List",
|
|
93
|
+
"IQueryable",
|
|
94
|
+
"Dictionary",
|
|
95
|
+
"IDictionary",
|
|
96
|
+
"string",
|
|
97
|
+
"object",
|
|
98
|
+
"void",
|
|
99
|
+
"String",
|
|
100
|
+
"Object",
|
|
101
|
+
"bool",
|
|
102
|
+
"byte",
|
|
103
|
+
"sbyte",
|
|
104
|
+
"char",
|
|
105
|
+
"decimal",
|
|
106
|
+
"double",
|
|
107
|
+
"float",
|
|
108
|
+
"int",
|
|
109
|
+
"uint",
|
|
110
|
+
"long",
|
|
111
|
+
"ulong",
|
|
112
|
+
"short",
|
|
113
|
+
"ushort",
|
|
114
|
+
"Boolean",
|
|
115
|
+
"Int32",
|
|
116
|
+
"Int64",
|
|
117
|
+
"Decimal",
|
|
118
|
+
"Double",
|
|
119
|
+
"Guid",
|
|
120
|
+
"DateTime",
|
|
121
|
+
"DateTimeOffset",
|
|
122
|
+
"Stream",
|
|
123
|
+
]);
|
|
124
|
+
/**
|
|
125
|
+
* Is this type an ASP.NET controller whose actions return bodies? Read from
|
|
126
|
+
* its own declaration only — `[ApiController]`, or a `Controller`/
|
|
127
|
+
* `ControllerBase` base. `aspnetcore.ts` additionally walks the base chain to
|
|
128
|
+
* admit a controller whose evidence is on an in-repo base class; this reader
|
|
129
|
+
* deliberately does not, because an unresolved base there produces a disclosed
|
|
130
|
+
* refusal while here it would produce a node type, and rule 2 prefers the
|
|
131
|
+
* narrower claim. Measured consequence: a jellyfin-style
|
|
132
|
+
* `XController : BaseJellyfinApiController` yields no DTO, a disclosed gap.
|
|
133
|
+
*/
|
|
134
|
+
function declaresBodies(type) {
|
|
135
|
+
if (type.attributes.includes("ApiController"))
|
|
136
|
+
return true;
|
|
137
|
+
return type.baseList.some((base) => base === "Controller" || base === "ControllerBase");
|
|
138
|
+
}
|
|
139
|
+
/** A simple type name this reader is willing to carry, or `undefined`. */
|
|
140
|
+
function shapeName(name) {
|
|
141
|
+
if (name === undefined || name === null || name === "")
|
|
142
|
+
return undefined;
|
|
143
|
+
const bare = name.replace(/\?$/, "");
|
|
144
|
+
return NOT_A_SHAPE.has(bare) ? undefined : bare;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Every type an ASP.NET action declares as a request or response body, across
|
|
148
|
+
* the whole batch. Keyed by the same `path.join(".")` identity the caller's
|
|
149
|
+
* own type map uses.
|
|
150
|
+
*
|
|
151
|
+
* Resolution is by simple name against the declared types — the reach this
|
|
152
|
+
* adapter has. A name declared twice is refused rather than ranked.
|
|
153
|
+
*/
|
|
154
|
+
export function readTransportShapes(files) {
|
|
155
|
+
/** Simple name -> key, or `null` once ambiguous. */
|
|
156
|
+
const declared = new Map();
|
|
157
|
+
const fieldsOf = new Map();
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
for (const type of file.types) {
|
|
160
|
+
const key = qualify(file.namespace, type.path);
|
|
161
|
+
fieldsOf.set(key, [...type.members.keys()].sort());
|
|
162
|
+
declared.set(type.name, declared.has(type.name) ? null : key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const roles = new Map();
|
|
166
|
+
const record = (simple, role) => {
|
|
167
|
+
if (simple === undefined)
|
|
168
|
+
return;
|
|
169
|
+
const key = declared.get(simple);
|
|
170
|
+
if (key === undefined || key === null)
|
|
171
|
+
return;
|
|
172
|
+
const existing = roles.get(key);
|
|
173
|
+
if (existing === undefined)
|
|
174
|
+
roles.set(key, new Set([role]));
|
|
175
|
+
else
|
|
176
|
+
existing.add(role);
|
|
177
|
+
};
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
for (const type of file.types) {
|
|
180
|
+
if (!declaresBodies(type))
|
|
181
|
+
continue;
|
|
182
|
+
for (const action of file.funcs) {
|
|
183
|
+
// Actions of THIS type only: `ownerPath` is the declaring type's
|
|
184
|
+
// nesting path, so comparing it to the type's own path keeps a second
|
|
185
|
+
// controller in the same file from lending its actions to this one.
|
|
186
|
+
if (action.ownerPath.join(".") !== type.path.join("."))
|
|
187
|
+
continue;
|
|
188
|
+
if (!action.attributes.some((each) => VERB_ATTRIBUTES.has(each)))
|
|
189
|
+
continue;
|
|
190
|
+
record(shapeName(action.returns), "response");
|
|
191
|
+
for (const [name, attributes] of action.scope.parameterAttributes) {
|
|
192
|
+
if (!attributes.includes(BODY_PARAMETER))
|
|
193
|
+
continue;
|
|
194
|
+
record(shapeName(action.scope.locals.get(name)), "request");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const shapes = new Map();
|
|
200
|
+
for (const [key, directions] of roles) {
|
|
201
|
+
shapes.set(key, { fields: fieldsOf.get(key) ?? [], roles: [...directions].sort() });
|
|
202
|
+
}
|
|
203
|
+
return shapes;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=dto.js.map
|
package/dist/dto.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.js","sourceRoot":"","sources":["../src/dto.ts"],"names":[],"mappings":"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"}
|
package/dist/extract.d.ts
CHANGED
|
@@ -64,5 +64,32 @@ export interface Extraction {
|
|
|
64
64
|
readonly edges: readonly IREdge[];
|
|
65
65
|
readonly unresolved: readonly UnresolvedRef[];
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The fallback graph when `extract()` throws mid-run, per
|
|
69
|
+
* `DEC-NEXT-degraded-graph-shape-for-adapters-without-one.md`.
|
|
70
|
+
*
|
|
71
|
+
* `FILE`, not `MODULE`: a C# module's identity here is
|
|
72
|
+
* `assembly::namespace::stem`, and both the assembly (`.csproj` resolution,
|
|
73
|
+
* `projects.ts`) and the namespace (`namespace Foo.Bar;`, a declaration
|
|
74
|
+
* *inside* the file) are exactly the machinery a mid-extraction throw means
|
|
75
|
+
* just failed or cannot be trusted to have finished. Unlike Go's import path
|
|
76
|
+
* or Rust's crate/module path — both resolved from the file's own location
|
|
77
|
+
* plus a manifest, never from the file's parsed content — nothing about a C#
|
|
78
|
+
* file's identity is recoverable without a successful parse. `FILE` (via the
|
|
79
|
+
* shared `@descryy/adapter-common#fileNode` helper) is the fact that
|
|
80
|
+
* survives: the file exists, on disk, at this path, and that is all this
|
|
81
|
+
* function claims.
|
|
82
|
+
*
|
|
83
|
+
* One disclosure row, not one per file — the fact is about the run. Anchored
|
|
84
|
+
* to the first file's `FILE` node because the ledger requires a `fromNodeId`
|
|
85
|
+
* and `FILE` is the only node type this path emits.
|
|
86
|
+
*/
|
|
87
|
+
export declare function degradedGraph(input: {
|
|
88
|
+
readonly repo: string;
|
|
89
|
+
readonly workspace: string | undefined;
|
|
90
|
+
readonly producedBy: string;
|
|
91
|
+
readonly files: readonly string[];
|
|
92
|
+
readonly cause: string;
|
|
93
|
+
}): Extraction;
|
|
67
94
|
export declare function extract(input: ExtractInput): Extraction;
|
|
68
95
|
//# sourceMappingURL=extract.d.ts.map
|
package/dist/extract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAIrB,OAAO,KAAK,EAAE,MAAM,EAAwD,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAyB,KAAK,QAAQ,EAAgB,MAAM,aAAa,CAAC;AAKjF,OAAO,EAAuC,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAI7F,eAAO,MAAM,QAAQ,WAAW,CAAC;AAmDjC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GAAG,UAAU,CA0Bb;AAiCD,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,UAAU,CAwtCvD"}
|
package/dist/extract.js
CHANGED
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* no edge. Extension methods are the same call for the same reason.
|
|
23
23
|
*/
|
|
24
24
|
import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
|
|
25
|
+
import { fileNode } from "@descryy/adapter-common";
|
|
25
26
|
import { efModels, isGenerated } from "./efcore.js";
|
|
27
|
+
import { readTransportShapes, SCHEMA as DTO_SCHEMA } from "./dto.js";
|
|
28
|
+
import { hasJsonShapeEvidence } from "./json.js";
|
|
26
29
|
import { decidedShape, SHAPE_LEVEL } from "./shape-reach.js";
|
|
27
30
|
import { aspNetRoutes } from "./aspnetcore.js";
|
|
28
31
|
import { resolveNullable, nullableDirectives } from "./nullability.js";
|
|
@@ -52,9 +55,12 @@ const REASONS = {
|
|
|
52
55
|
noProject: "no .csproj covers this file, so it belongs to no assembly and nothing else can name its " +
|
|
53
56
|
"types. Disclosed rather than given an invented identity.",
|
|
54
57
|
noBaseContext: "`base` in a type whose base class is not in the analysed set.",
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
+
belowLevel: "resolved to a declaration, but this run was capped below the level that evidence earns. " +
|
|
59
|
+
"Not an analysis gap and not a scope boundary — a limit the caller asked for.",
|
|
60
|
+
handlerUnresolved: "this route's handler could not be resolved to a FUNCTION node: a minimal-API registration's " +
|
|
61
|
+
"second argument is an inline lambda (no name to resolve), names a delegate this run could " +
|
|
62
|
+
"not join to a declared method, or an attribute-routed handler's declaring type left the " +
|
|
63
|
+
"analysed set.",
|
|
58
64
|
};
|
|
59
65
|
const INHERITANCE_HOPS = 12;
|
|
60
66
|
const PENDING_PASSES = 3;
|
|
@@ -64,6 +70,49 @@ const PENDING_PASSES = 3;
|
|
|
64
70
|
* as an annotation — C# has no naming convention for tests the way XCTest does.
|
|
65
71
|
*/
|
|
66
72
|
const TEST_ATTRIBUTES = new Set(["Fact", "Theory", "Test", "TestCase", "TestMethod", "DataTestMethod"]);
|
|
73
|
+
/**
|
|
74
|
+
* The fallback graph when `extract()` throws mid-run, per
|
|
75
|
+
* `DEC-NEXT-degraded-graph-shape-for-adapters-without-one.md`.
|
|
76
|
+
*
|
|
77
|
+
* `FILE`, not `MODULE`: a C# module's identity here is
|
|
78
|
+
* `assembly::namespace::stem`, and both the assembly (`.csproj` resolution,
|
|
79
|
+
* `projects.ts`) and the namespace (`namespace Foo.Bar;`, a declaration
|
|
80
|
+
* *inside* the file) are exactly the machinery a mid-extraction throw means
|
|
81
|
+
* just failed or cannot be trusted to have finished. Unlike Go's import path
|
|
82
|
+
* or Rust's crate/module path — both resolved from the file's own location
|
|
83
|
+
* plus a manifest, never from the file's parsed content — nothing about a C#
|
|
84
|
+
* file's identity is recoverable without a successful parse. `FILE` (via the
|
|
85
|
+
* shared `@descryy/adapter-common#fileNode` helper) is the fact that
|
|
86
|
+
* survives: the file exists, on disk, at this path, and that is all this
|
|
87
|
+
* function claims.
|
|
88
|
+
*
|
|
89
|
+
* One disclosure row, not one per file — the fact is about the run. Anchored
|
|
90
|
+
* to the first file's `FILE` node because the ledger requires a `fromNodeId`
|
|
91
|
+
* and `FILE` is the only node type this path emits.
|
|
92
|
+
*/
|
|
93
|
+
export function degradedGraph(input) {
|
|
94
|
+
const scope = { repo: input.repo, workspace: input.workspace };
|
|
95
|
+
const nodes = input.files.map((file) => fileNode(scope, file, input.producedBy));
|
|
96
|
+
const anchor = nodes[0];
|
|
97
|
+
const unresolved = anchor === undefined
|
|
98
|
+
? []
|
|
99
|
+
: [
|
|
100
|
+
{
|
|
101
|
+
fromNodeId: anchor.id,
|
|
102
|
+
edgeType: "IMPORTS",
|
|
103
|
+
rawTarget: "(entire repository)",
|
|
104
|
+
file: anchor.file,
|
|
105
|
+
line: 1,
|
|
106
|
+
producedBy: input.producedBy,
|
|
107
|
+
reason: `NO GRAPH WAS PRODUCED for ${String(input.files.length)} C# file(s). ${input.cause}. ` +
|
|
108
|
+
"Files are listed because their existence and path are facts about the filesystem; their " +
|
|
109
|
+
"contents are absent. Every finding, coverage figure and 'not affected' statement over " +
|
|
110
|
+
"this repository is UNSUPPORTED — this is a failed analysis, not an empty repository.",
|
|
111
|
+
attrs: { refusalClass: "capability-gap" },
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
return { nodes, edges: [], unresolved };
|
|
115
|
+
}
|
|
67
116
|
function qualify(namespace, path) {
|
|
68
117
|
const joined = path.join(".");
|
|
69
118
|
return namespace === "" ? joined : `${namespace}.${joined}`;
|
|
@@ -117,6 +166,9 @@ export function extract(input) {
|
|
|
117
166
|
* `ViewComponent` carried the entity's columns.
|
|
118
167
|
*/
|
|
119
168
|
const modelsByName = new Map(orm.models.map((model) => [model.fqn, model]));
|
|
169
|
+
// Transport shapes an ASP.NET ROUTE declares — never a type this reader
|
|
170
|
+
// guessed at from its name (DEC-043). Own file, `dto.ts`.
|
|
171
|
+
const shapesByType = readTransportShapes(input.files);
|
|
120
172
|
const push = (edge, level) => {
|
|
121
173
|
if (level > input.reached)
|
|
122
174
|
return false;
|
|
@@ -162,6 +214,8 @@ export function extract(input) {
|
|
|
162
214
|
// Sorted: DEC-083's "lexicographically first" is only reproducible if the
|
|
163
215
|
// input is ordered, not left to filesystem enumeration order.
|
|
164
216
|
const units = [...input.files].sort((a, b) => a.file.localeCompare(b.file));
|
|
217
|
+
/** Path -> the `CsFile` declared there, for `responseShapeEvidence`'s per-part provenance lookup. */
|
|
218
|
+
const fileByPath = new Map(units.map((unit) => [unit.file, unit]));
|
|
165
219
|
const keyOf = (unit, fqn) => `${unit.assembly}::${fqn}`;
|
|
166
220
|
for (const unit of units) {
|
|
167
221
|
const stem = unit.file.slice(unit.file.lastIndexOf("/") + 1).replace(/\.cs$/i, "");
|
|
@@ -204,7 +258,16 @@ export function extract(input) {
|
|
|
204
258
|
}
|
|
205
259
|
// The kind is decided here and the id is minted from it. DEC-004 hashes
|
|
206
260
|
// the kind, so a MODEL cannot be patched onto a CLASS afterwards.
|
|
207
|
-
|
|
261
|
+
// An EF Core entity outranks a declared transport shape where both
|
|
262
|
+
// somehow apply: a wrong `MODEL` is a claim about a database and a wrong
|
|
263
|
+
// `DTO` a claim about a wire format, so the narrower evidence wins — the
|
|
264
|
+
// same tie-break, for the same reason, as adapter-python's and
|
|
265
|
+
// adapter-java's.
|
|
266
|
+
const kind = modelsByName.has(fqn)
|
|
267
|
+
? "MODEL"
|
|
268
|
+
: shapesByType.has(fqn)
|
|
269
|
+
? "DTO"
|
|
270
|
+
: "CLASS";
|
|
208
271
|
const declared = {
|
|
209
272
|
kind,
|
|
210
273
|
id: nodeId(scope, kind, symbolQsp(holder, [fqn]), LANGUAGE),
|
|
@@ -232,13 +295,26 @@ export function extract(input) {
|
|
|
232
295
|
declared.members.set(member, { type: written });
|
|
233
296
|
}
|
|
234
297
|
const model = declared.kind === "MODEL" ? modelsByName.get(declared.fqn) : undefined;
|
|
298
|
+
const shape = declared.kind === "DTO" ? shapesByType.get(declared.fqn) : undefined;
|
|
235
299
|
/**
|
|
236
300
|
* The level this node's strongest fact carries. A `MODEL` bundles two
|
|
237
301
|
* different claims — *this type is persisted* (a `DbSet<T>` or an
|
|
238
302
|
* `IEntityTypeConfiguration<T>`, read at R0/R1) and *this is its mapped
|
|
239
303
|
* shape* — and only the second is R3-grade, so the shape's own level is
|
|
240
|
-
* also stated in `attrs.shapeResolution` (DEC-058's carrier
|
|
241
|
-
*
|
|
304
|
+
* also stated in `attrs.shapeResolution` (DEC-058's carrier).
|
|
305
|
+
*
|
|
306
|
+
* **Node-scoped, and deliberately unrelated to the `USES_API`-edge
|
|
307
|
+
* `shapeResolution` this file also now writes** (see
|
|
308
|
+
* `responseShapeEvidence` above,
|
|
309
|
+
* `DEC-NEXT-dto-shape-evidence-for-api-contracts`). This one is the EF
|
|
310
|
+
* Core-mapped, PERSISTED database shape — `descry-core`'s contract engine
|
|
311
|
+
* (`engine.ts:286`) reads only the edge-level attribute for API
|
|
312
|
+
* wire-shape comparison and never reads a node-level `shapeResolution` at
|
|
313
|
+
* all, on a `MODEL` or otherwise. A type can be both — an entity that is
|
|
314
|
+
* also serialized straight to the wire — but the two facts rest on
|
|
315
|
+
* different attributes (EF Core's vs. JSON serialization's) and are
|
|
316
|
+
* asserted independently; this one is not the mechanism the API-shape
|
|
317
|
+
* ruling built.
|
|
242
318
|
*
|
|
243
319
|
* `decidedShape` and not `columns.length > 0`: a model whose every column
|
|
244
320
|
* came back `unresolved` has had no shape read (DEC-180 §2 — unknown is
|
|
@@ -264,6 +340,18 @@ export function extract(input) {
|
|
|
264
340
|
// Written whenever the type has more than one part, so an adjudicator
|
|
265
341
|
// that searches only `file` has something to search instead.
|
|
266
342
|
...(files.length > 1 || partial ? { partial: true, declaredIn: files } : {}),
|
|
343
|
+
...(shape === undefined
|
|
344
|
+
? {}
|
|
345
|
+
: {
|
|
346
|
+
// The declaration this node rests on, named the way a `MODEL`
|
|
347
|
+
// names its ORM. NAMES only — no `fieldDetail`, no
|
|
348
|
+
// `shapeResolution`, no level raise: whether a plain class
|
|
349
|
+
// body's member list is R3-grade evidence is a filed,
|
|
350
|
+
// unanswered question and `shape-reach.ts` is untouched.
|
|
351
|
+
schema: DTO_SCHEMA,
|
|
352
|
+
declaredDirections: shape.roles,
|
|
353
|
+
...(shape.fields.length > 0 ? { fields: shape.fields } : {}),
|
|
354
|
+
}),
|
|
267
355
|
...(model === undefined
|
|
268
356
|
? {}
|
|
269
357
|
: {
|
|
@@ -438,6 +526,48 @@ export function extract(input) {
|
|
|
438
526
|
return { declared: claimants[0] };
|
|
439
527
|
return { reason: REASONS.ambiguousName };
|
|
440
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* `DEC-NEXT-dto-shape-evidence-for-api-contracts`'s ruling, applied at the
|
|
531
|
+
* one place a C# `USES_API` edge states a response type: a call's sole
|
|
532
|
+
* generic type argument (`GetFromJsonAsync<OrderResponse>(url)`, read by
|
|
533
|
+
* `genericArgumentOf` in `parse.ts`). Mirrors `adapter-java`'s
|
|
534
|
+
* `responseShapeEvidence` and `adapter-python`'s Pydantic gate — resolve
|
|
535
|
+
* the written name through the SAME scope rules every other name in this
|
|
536
|
+
* adapter goes through, then require real `System.Text.Json`/
|
|
537
|
+
* `Newtonsoft.Json` evidence before claiming R3, never the type argument
|
|
538
|
+
* alone.
|
|
539
|
+
*
|
|
540
|
+
* Gated on `input.reached >= 3` INSIDE the returned attrs, not by an early
|
|
541
|
+
* return one level up — a guard placed above this function would be green
|
|
542
|
+
* at R3 and silently wrong at R2, the monotonic sweep's own failure mode.
|
|
543
|
+
*
|
|
544
|
+
* A `MODEL` is excluded even when JSON-annotated: that is an EF Core
|
|
545
|
+
* persistence claim (see the comment beside `attrs.shapeResolution` at the
|
|
546
|
+
* `MODEL` node above), and DEC-NEXT scopes this ruling to *wire* shape
|
|
547
|
+
* evidence, a different fact resting on different attributes.
|
|
548
|
+
*/
|
|
549
|
+
function responseShapeEvidence(unit, written) {
|
|
550
|
+
if (input.reached < 3 || written === undefined)
|
|
551
|
+
return {};
|
|
552
|
+
const resolved = resolveType(written, unit);
|
|
553
|
+
if (!("declared" in resolved))
|
|
554
|
+
return {};
|
|
555
|
+
const { declared } = resolved;
|
|
556
|
+
if (declared.kind !== "DTO")
|
|
557
|
+
return {}; // a MODEL: persisted shape, not wire shape, or a plain CLASS
|
|
558
|
+
// `parts[i]` was declared in `files[i]` — each file contributes at most
|
|
559
|
+
// one part for a given type (one `unit.types` entry per declaration), so
|
|
560
|
+
// the two arrays grow in lockstep. A part's OWN file gates its OWN
|
|
561
|
+
// provenance: a `using` in one file of a `partial` type does not make an
|
|
562
|
+
// attribute in a different file of it verified.
|
|
563
|
+
const evidenced = declared.parts.some((part, index) => {
|
|
564
|
+
const partFile = fileByPath.get(declared.files[index] ?? declared.unit.file) ?? declared.unit;
|
|
565
|
+
return hasJsonShapeEvidence(partFile, part);
|
|
566
|
+
});
|
|
567
|
+
if (!evidenced)
|
|
568
|
+
return {};
|
|
569
|
+
return { responseType: declared.id, shapeResolution: 3 };
|
|
570
|
+
}
|
|
441
571
|
/** Every type in a declaration's supertype chain, across all its parts. */
|
|
442
572
|
function* ancestry(start) {
|
|
443
573
|
const queue = [start];
|
|
@@ -766,7 +896,9 @@ export function extract(input) {
|
|
|
766
896
|
return { state: "none" };
|
|
767
897
|
};
|
|
768
898
|
const read = clientCalls(fn.scope.refs, declared.id, (ref) => writtenReceiverType(ref, fn.scope, host), declared.isTest, fn.scope.parameterNames, clientBaseOf);
|
|
769
|
-
|
|
899
|
+
for (const call of read.calls) {
|
|
900
|
+
clientCallSites.push({ ...call, ...responseShapeEvidence(unit, call.responseTypeWritten) });
|
|
901
|
+
}
|
|
770
902
|
for (const call of read.calls) {
|
|
771
903
|
if (!functionClientBase.has(call.fromId))
|
|
772
904
|
functionClientBase.set(call.fromId, call.clientBase);
|
|
@@ -791,14 +923,32 @@ export function extract(input) {
|
|
|
791
923
|
});
|
|
792
924
|
}
|
|
793
925
|
}
|
|
926
|
+
/**
|
|
927
|
+
* Mapped-column reads, grouped by the entity they land on. Edge
|
|
928
|
+
* identity is `(from, to, type)`, so a method reading three columns of
|
|
929
|
+
* one entity is **one** edge naming three columns, not three edges — a
|
|
930
|
+
* singular `field` attribute would make the second unrepresentable
|
|
931
|
+
* (DEC-046). Sorted on the way out so the attribute is a property of
|
|
932
|
+
* the source and not of statement order.
|
|
933
|
+
*/
|
|
934
|
+
const columnReads = new Map();
|
|
794
935
|
for (const ref of fn.scope.refs) {
|
|
795
936
|
switch (ref.kind) {
|
|
796
937
|
case "dynamic":
|
|
797
938
|
ledger(declared.id, "CALLS", ref.raw, unit, ref.line, REASONS.valueTarget);
|
|
798
939
|
break;
|
|
799
|
-
case "type":
|
|
940
|
+
case "type": {
|
|
941
|
+
const read = mappedColumnRead(ref, fn.scope, unit, host);
|
|
942
|
+
if (read !== undefined) {
|
|
943
|
+
const columns = columnReads.get(read.to);
|
|
944
|
+
if (columns === undefined)
|
|
945
|
+
columnReads.set(read.to, new Set([read.column]));
|
|
946
|
+
else
|
|
947
|
+
columns.add(read.column);
|
|
948
|
+
}
|
|
800
949
|
emitType(declared.id, ref, unit, host, fn.scope);
|
|
801
950
|
break;
|
|
951
|
+
}
|
|
802
952
|
case "new":
|
|
803
953
|
emitType(declared.id, ref, unit, host, fn.scope);
|
|
804
954
|
emitConstructor(declared.id, ref, unit, host, declared.isTest);
|
|
@@ -810,8 +960,54 @@ export function extract(input) {
|
|
|
810
960
|
break;
|
|
811
961
|
}
|
|
812
962
|
}
|
|
963
|
+
for (const [to, columns] of columnReads) {
|
|
964
|
+
if (push({ from: declared.id, to, type: "READS", attrs: { fields: [...columns].sort() } }, 2))
|
|
965
|
+
continue;
|
|
966
|
+
ledger(declared.id, "READS", [...columns].sort().join(", "), unit, fn.startLine, REASONS.belowLevel);
|
|
967
|
+
}
|
|
813
968
|
}
|
|
814
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* `FUNCTION --READS--> MODEL` for `order.TotalAmount` — golden 07.
|
|
972
|
+
*
|
|
973
|
+
* C# needs no accessor convention the way Java does: an EF Core entity's
|
|
974
|
+
* columns **are** its properties, so the read is written directly and the
|
|
975
|
+
* only question is whether the receiver and the member are both known.
|
|
976
|
+
* Three written things have to agree, and no name decides any of them:
|
|
977
|
+
*
|
|
978
|
+
* 1. the access is a **read** — `parse.ts` withholds `ref.member` for an
|
|
979
|
+
* invocation's callee and for an assignment's left-hand side, which
|
|
980
|
+
* are the same tree shape and are, respectively, a method and a write;
|
|
981
|
+
* 2. the receiver's type is written down and resolves, through the same
|
|
982
|
+
* `ownerOfReceiver` every `CALLS` edge here goes through; and
|
|
983
|
+
* 3. the resolved type was admitted as a `MODEL` by `efcore.ts`, and the
|
|
984
|
+
* member is one of the **columns that mapping declares** — not merely
|
|
985
|
+
* a property, so `[NotMapped]` and a computed property are refused.
|
|
986
|
+
*
|
|
987
|
+
* Level 2: the edge names a member. It compares no shape and proves none,
|
|
988
|
+
* and the cap makes a name-level fact R2 — the column's shape is already on
|
|
989
|
+
* the `MODEL` node at R3, carrying its own `shapeResolution`.
|
|
990
|
+
*/
|
|
991
|
+
function mappedColumnRead(ref, scope_, unit, host) {
|
|
992
|
+
if (ref.speculative !== true || ref.member === undefined)
|
|
993
|
+
return undefined;
|
|
994
|
+
// `this.X` / `base.X` reach the enclosing type, not a receiver whose type
|
|
995
|
+
// the site wrote down; an entity reading its own column is not the fact
|
|
996
|
+
// golden 07 is about and `push` would drop the self-edge anyway.
|
|
997
|
+
if (ref.name === "this" || ref.name === "base")
|
|
998
|
+
return undefined;
|
|
999
|
+
const owner = ownerOfReceiver(ref.name, scope_, unit, host);
|
|
1000
|
+
if (!("declared" in owner))
|
|
1001
|
+
return undefined;
|
|
1002
|
+
if (owner.declared.kind !== "MODEL")
|
|
1003
|
+
return undefined;
|
|
1004
|
+
const model = modelsByName.get(owner.declared.fqn);
|
|
1005
|
+
if (model === undefined)
|
|
1006
|
+
return undefined;
|
|
1007
|
+
if (!model.columns.some((column) => column.name === ref.member))
|
|
1008
|
+
return undefined;
|
|
1009
|
+
return { to: owner.declared.id, column: ref.member };
|
|
1010
|
+
}
|
|
815
1011
|
/**
|
|
816
1012
|
* DEC-084 in one function: base-list position is a language guarantee, not
|
|
817
1013
|
* a convention, so only the first entry of a class base list is ever
|
|
@@ -1047,7 +1243,14 @@ export function extract(input) {
|
|
|
1047
1243
|
// (traced base, DEC-097) or `none` (no traced mechanism, literal
|
|
1048
1244
|
// path stands alone). See `client-base.ts` for why `unresolved` is
|
|
1049
1245
|
// never produced by this build.
|
|
1050
|
-
attrs: {
|
|
1246
|
+
attrs: {
|
|
1247
|
+
callerKind: call.callerKind,
|
|
1248
|
+
via: call.method,
|
|
1249
|
+
clientBase: call.clientBase,
|
|
1250
|
+
...(call.responseType === undefined
|
|
1251
|
+
? {}
|
|
1252
|
+
: { responseType: call.responseType, shapeResolution: call.shapeResolution }),
|
|
1253
|
+
},
|
|
1051
1254
|
}, 2);
|
|
1052
1255
|
}
|
|
1053
1256
|
nodes.push(...routeNodes.values(), ...endpointNodes.values());
|