@descryy/adapter-csharp 0.1.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/dist/parse.js ADDED
@@ -0,0 +1,861 @@
1
+ /**
2
+ * One C# file, read into the shape the resolver wants.
3
+ *
4
+ * Hand-written walk, as in Java, Go, Rust and PHP, and for the same reason:
5
+ * containment is what a resolver needs and a query's match set is exactly what
6
+ * discards it.
7
+ *
8
+ * ## What C# changes
9
+ *
10
+ * Three things here are genuinely new work rather than a translation of an
11
+ * earlier language's rules.
12
+ *
13
+ * - **A type may be declared in several files.** `partial class` is the first
14
+ * construct in this project whose declaration is legitimately multi-file and
15
+ * says so in the source. DEC-083 settles the identity question; this reader
16
+ * only has to collect the parts.
17
+ * - **`using` imports a namespace, not a type.** PHP's `use` names one class
18
+ * and resolves in one step. C#'s brings in every type in a namespace, so a
19
+ * bare name is resolved by searching the enclosing namespaces outward and
20
+ * then each import — which means a name may be found in two places, and
21
+ * that is an ambiguity to disclose rather than a tie to break.
22
+ * - **The base list does not say which entry is the class.** DEC-084 decides
23
+ * it: at most one base class, and the language requires it first, so entries
24
+ * after the first are interfaces unconditionally and only one entry per
25
+ * class declaration ever needs resolving. The `IFoo` convention is not read.
26
+ *
27
+ * Nested types are ordinary here in a way they are not in PHP, so a type carries
28
+ * its nesting path rather than a single name.
29
+ */
30
+ /** Keywords that name no user type. `var` is handled separately. */
31
+ const PREDEFINED = new Set([
32
+ "int", "uint", "long", "ulong", "short", "ushort", "byte", "sbyte",
33
+ "float", "double", "decimal", "bool", "char", "string", "object",
34
+ "void", "dynamic", "nint", "nuint", "var",
35
+ ]);
36
+ /**
37
+ * The type a type expression names.
38
+ *
39
+ * `undefined` — none (predefined, `var`, or absent).
40
+ * `null` — a type is named but which one is ambiguous.
41
+ * `string` — the single type named, as written, generic arguments dropped.
42
+ *
43
+ * Generic arguments are dropped deliberately: `List<Order>` names `List`, and
44
+ * `Order` inside it is a separate reference the walker records on its own. A
45
+ * single collapsed name would attribute the collection's methods to its element.
46
+ */
47
+ export function typeName(node) {
48
+ if (node === null || node === undefined)
49
+ return undefined;
50
+ switch (node.type) {
51
+ case "predefined_type":
52
+ case "implicit_type":
53
+ return undefined;
54
+ case "identifier": {
55
+ const text = node.text.trim();
56
+ return PREDEFINED.has(text) ? undefined : text;
57
+ }
58
+ case "qualified_name": {
59
+ const text = node.text.trim().replace(/<[^>]*>/g, "");
60
+ return text === "" ? undefined : text;
61
+ }
62
+ case "generic_name":
63
+ return typeName(node.namedChild(0));
64
+ case "nullable_type":
65
+ return typeName(node.childForFieldName("type") ?? node.namedChild(0));
66
+ case "array_type":
67
+ return typeName(node.childForFieldName("type") ?? node.namedChild(0));
68
+ case "pointer_type":
69
+ case "ref_type":
70
+ case "scoped_type":
71
+ return typeName(node.namedChild(0));
72
+ case "tuple_type":
73
+ return null;
74
+ default: {
75
+ if (node.namedChildCount === 1)
76
+ return typeName(node.namedChild(0));
77
+ if (node.namedChildCount === 0) {
78
+ const text = node.text.trim();
79
+ if (text === "" || PREDEFINED.has(text))
80
+ return undefined;
81
+ return /^[\w.]+$/.test(text) ? text : undefined;
82
+ }
83
+ return null;
84
+ }
85
+ }
86
+ }
87
+ function hasModifier(node, keyword) {
88
+ for (let index = 0; index < node.childCount; index += 1) {
89
+ const child = node.child(index);
90
+ if (child?.type === "modifier" && child.text === keyword)
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+ /**
96
+ * `[Fact]`, `[Theory]`, `[Xunit.Fact]` -> the last segment, **and its arguments**.
97
+ *
98
+ * The names alone were enough while `isTest` was the only consumer. A framework
99
+ * extractor needs the other half: `[Route("System/ActivityLog")]` collapses to
100
+ * `Route`, and the argument that is dropped **is the route**.
101
+ *
102
+ * That is the third instance of one shape in this adapter set — identity
103
+ * normalises, fidelity preserves, and both readings are correct for different
104
+ * consumers. The first two were a property's written type (`string` against
105
+ * `string?`) and a base list's generic argument
106
+ * (`IEntityTypeConfiguration<Order>`). Each collapse surfaced downstream as a
107
+ * clean zero rather than as an error, so both are carried side by side now.
108
+ */
109
+ function attributeArgumentsOf(node) {
110
+ const found = {};
111
+ for (let index = 0; index < node.childCount; index += 1) {
112
+ const child = node.child(index);
113
+ if (child?.type !== "attribute_list")
114
+ continue;
115
+ for (let inner = 0; inner < child.namedChildCount; inner += 1) {
116
+ const attribute = child.namedChild(inner);
117
+ if (attribute?.type !== "attribute")
118
+ continue;
119
+ const name = attribute.childForFieldName("name")?.text.trim();
120
+ if (name === undefined)
121
+ continue;
122
+ const bare = name.replace(/<[^>]*>/g, "");
123
+ const simple = bare.slice(bare.lastIndexOf(".") + 1);
124
+ // The first string literal argument, as written. A non-literal argument
125
+ // yields nothing rather than its source text: a path assembled at runtime
126
+ // is not a path this adapter can read, and DEC-128 refuses it.
127
+ const literal = /\(\s*"([^"]*)"/.exec(attribute.text);
128
+ if (literal !== null && found[simple] === undefined)
129
+ found[simple] = literal[1];
130
+ }
131
+ }
132
+ return found;
133
+ }
134
+ function attributesOf(node) {
135
+ const names = [];
136
+ for (let index = 0; index < node.childCount; index += 1) {
137
+ const child = node.child(index);
138
+ if (child?.type !== "attribute_list")
139
+ continue;
140
+ for (let inner = 0; inner < child.namedChildCount; inner += 1) {
141
+ const attribute = child.namedChild(inner);
142
+ if (attribute?.type !== "attribute")
143
+ continue;
144
+ const name = attribute.childForFieldName("name")?.text.trim();
145
+ if (name === undefined)
146
+ continue;
147
+ const bare = name.replace(/<[^>]*>/g, "");
148
+ names.push(bare.slice(bare.lastIndexOf(".") + 1));
149
+ }
150
+ }
151
+ return names;
152
+ }
153
+ function describeReceiver(node) {
154
+ if (node === null || node === undefined)
155
+ return { written: undefined };
156
+ switch (node.type) {
157
+ case "identifier":
158
+ case "predefined_type":
159
+ return { written: node.text.trim() };
160
+ case "this_expression":
161
+ return { written: "this" };
162
+ case "base_expression":
163
+ return { written: "base" };
164
+ case "generic_name":
165
+ return { written: node.namedChild(0)?.text.trim() };
166
+ case "qualified_name":
167
+ return { written: node.text.trim() };
168
+ case "parenthesized_expression":
169
+ return describeReceiver(node.namedChild(0));
170
+ case "object_creation_expression": {
171
+ // `new Order(x).Total()` — the type is at the site.
172
+ const named = typeName(node.childForFieldName("type"));
173
+ return typeof named === "string" ? { written: undefined, type: named } : { written: undefined, opaque: true };
174
+ }
175
+ case "cast_expression": {
176
+ // `((IRepository)x).FindById()` — also written at the site.
177
+ const named = typeName(node.childForFieldName("type"));
178
+ return typeof named === "string" ? { written: undefined, type: named } : { written: undefined, opaque: true };
179
+ }
180
+ case "conditional_access_expression": {
181
+ // `x?.y.z()` and `a?[0].F()`. The null-conditional changes only whether the
182
+ // chain is evaluated, never what it denotes: `x?.y` names exactly what
183
+ // `x.y` names. Unwrapping it here is what lets the ordinary member-access
184
+ // machinery below do the work; falling through to `default` made every
185
+ // such receiver opaque, and before the flag above existed, implicit.
186
+ const object = describeReceiver(node.namedChild(0));
187
+ const binding = node.namedChild(1);
188
+ if (binding?.type !== "member_binding_expression") {
189
+ // `a?[0]` is an indexer. The element type is not written at the site.
190
+ return { written: undefined, opaque: true };
191
+ }
192
+ const member = binding.namedChild(0);
193
+ const memberName = (member?.type === "generic_name" ? member.namedChild(0)?.text : member?.text)?.trim();
194
+ if (memberName === undefined || object.written === undefined)
195
+ return { written: undefined, opaque: true };
196
+ return {
197
+ written: `${object.written}.${memberName}`,
198
+ property: { of: object.written, name: memberName },
199
+ };
200
+ }
201
+ case "member_access_expression": {
202
+ // `this._repository.FindById()` — the type is on the member declaration,
203
+ // but `Shop.Orders.Money.Format()` is a *namespace* path, not a member
204
+ // chain. Both are `member_access_expression`, and telling them apart needs
205
+ // the resolver, so the whole written path is handed over and the resolver
206
+ // tries it as a qualified name first.
207
+ const object = describeReceiver(node.childForFieldName("expression"));
208
+ const member = node.childForFieldName("name");
209
+ if (member === null)
210
+ return { written: undefined, opaque: true };
211
+ const memberName = member.type === "generic_name" ? member.namedChild(0)?.text : member.text;
212
+ if (memberName === undefined || object.written === undefined) {
213
+ // The object could not be written down, so neither can the member path.
214
+ // Opaque rather than absent — see `Receiver.opaque`.
215
+ return object.type === undefined && object.call === undefined
216
+ ? { written: undefined, opaque: true }
217
+ : { written: undefined };
218
+ }
219
+ return {
220
+ written: `${object.written}.${memberName.trim()}`,
221
+ property: { of: object.written, name: memberName.trim() },
222
+ };
223
+ }
224
+ case "invocation_expression": {
225
+ const call = callSignature(node);
226
+ return call === undefined ? { written: undefined, opaque: true } : { written: undefined, call };
227
+ }
228
+ default:
229
+ // A binary expression, a literal, a lambda, a `switch` expression — a
230
+ // receiver is written and this reader cannot say what it denotes.
231
+ return { written: undefined, opaque: true };
232
+ }
233
+ }
234
+ /** The `(receiver, name)` an invocation names, when both are written. */
235
+ function callSignature(node) {
236
+ const callee = node.childForFieldName("function");
237
+ if (callee === null)
238
+ return undefined;
239
+ if (callee.type === "identifier")
240
+ return { receiver: undefined, name: callee.text.trim() };
241
+ if (callee.type === "generic_name") {
242
+ const bare = callee.namedChild(0)?.text.trim();
243
+ return bare === undefined ? undefined : { receiver: undefined, name: bare };
244
+ }
245
+ if (callee.type === "member_access_expression") {
246
+ const member = callee.childForFieldName("name");
247
+ if (member === null)
248
+ return undefined;
249
+ const name = (member.type === "generic_name" ? member.namedChild(0)?.text : member.text)?.trim();
250
+ if (name === undefined)
251
+ return undefined;
252
+ const receiver = describeReceiver(callee.childForFieldName("expression"));
253
+ if (receiver.written === undefined)
254
+ return undefined;
255
+ return { receiver: receiver.written, name };
256
+ }
257
+ return undefined;
258
+ }
259
+ /**
260
+ * The first argument of an invocation, only when it is a bare string literal.
261
+ *
262
+ * Mirrors Java's `firstStringArgumentOf` exactly: `argument_list`'s first
263
+ * named child is an `argument`, whose own first named child is the value —
264
+ * `string_literal` when the call site wrote one, an `identifier` or anything
265
+ * else otherwise. `"abc"` -> `abc`; a concatenation or interpolated string
266
+ * (`$"..."`, `interpolated_string_expression`) returns `undefined` rather than
267
+ * the fragment it can see, the same refusal-not-guess DEC-127 already applies
268
+ * to Java's read of the identical shape.
269
+ */
270
+ function firstStringArgumentOf(invocation) {
271
+ const args = invocation.childForFieldName("arguments");
272
+ if (args === null)
273
+ return undefined;
274
+ const first = args.namedChild(0)?.namedChild(0);
275
+ if (first === null || first === undefined || first.type !== "string_literal")
276
+ return undefined;
277
+ return first.text.slice(1, -1);
278
+ }
279
+ /**
280
+ * The first argument's raw text, whatever shape it is. Mirrors Java's
281
+ * `firstArgumentTextOf`. See {@link CsRef.firstArgumentText}.
282
+ */
283
+ function firstArgumentTextOf(invocation) {
284
+ const args = invocation.childForFieldName("arguments");
285
+ if (args === null)
286
+ return undefined;
287
+ const first = args.namedChild(0)?.namedChild(0);
288
+ return first === null || first === undefined ? undefined : first.text;
289
+ }
290
+ function pushInvocation(scope, node) {
291
+ const callee = node.childForFieldName("function");
292
+ if (callee === null)
293
+ return;
294
+ if (callee.type === "identifier" || callee.type === "generic_name") {
295
+ const name = (callee.type === "generic_name" ? callee.namedChild(0)?.text : callee.text)?.trim();
296
+ if (name === undefined)
297
+ return;
298
+ scope.refs.push({
299
+ kind: "call",
300
+ name,
301
+ receiver: undefined,
302
+ line: node.startPosition.row + 1,
303
+ raw: node.text.slice(0, 200),
304
+ });
305
+ return;
306
+ }
307
+ if (callee.type === "member_access_expression" || callee.type === "conditional_access_expression") {
308
+ const holder = callee.type === "member_access_expression" ? callee : callee.namedChild(0);
309
+ const member = callee.type === "member_access_expression"
310
+ ? callee.childForFieldName("name")
311
+ : callee.namedChild(1)?.namedChild(0);
312
+ if (holder === null || member === null || member === undefined)
313
+ return;
314
+ const name = (member.type === "generic_name" ? member.namedChild(0)?.text : member.text)?.trim();
315
+ if (name === undefined)
316
+ return;
317
+ const receiver = describeReceiver(callee.type === "member_access_expression" ? callee.childForFieldName("expression") : holder);
318
+ // A receiver was written and could not be read. Disclosing that is the whole
319
+ // difference between a gap and a wrong answer: falling through with
320
+ // `receiver: undefined` makes `emitCall` treat it as an implicit `self`.
321
+ if (receiver.opaque === true) {
322
+ scope.refs.push({
323
+ kind: "dynamic",
324
+ name: callee.text.slice(0, 60),
325
+ receiver: undefined,
326
+ line: node.startPosition.row + 1,
327
+ raw: node.text.slice(0, 200),
328
+ });
329
+ return;
330
+ }
331
+ const literal = firstStringArgumentOf(node);
332
+ const argumentText = firstArgumentTextOf(node);
333
+ scope.refs.push({
334
+ kind: "call",
335
+ name,
336
+ receiver: receiver.written,
337
+ ...(receiver.type === undefined ? {} : { receiverType: receiver.type }),
338
+ ...(receiver.property === undefined ? {} : { receiverProperty: receiver.property }),
339
+ ...(receiver.call === undefined ? {} : { receiverCall: receiver.call }),
340
+ line: node.startPosition.row + 1,
341
+ raw: node.text.slice(0, 200),
342
+ // Spread rather than assigned — `exactOptionalPropertyTypes` distinguishes
343
+ // absent from present-and-undefined, and the distinction is the point:
344
+ // absent means "not a call worth reading further," present-and-undefined
345
+ // would wrongly mean "a call whose first argument is nothing."
346
+ ...(literal === undefined ? {} : { firstStringArgument: literal }),
347
+ ...(argumentText === undefined ? {} : { firstArgumentText: argumentText }),
348
+ });
349
+ return;
350
+ }
351
+ // `handler(payload)` where `handler` is a delegate field, `_map[key](x)`,
352
+ // `((Func<int,int>)f)(x)`. The target is a value; which method it holds is a
353
+ // runtime fact. Golden pattern 12.
354
+ scope.refs.push({
355
+ kind: "dynamic",
356
+ name: callee.text.slice(0, 60),
357
+ receiver: undefined,
358
+ line: node.startPosition.row + 1,
359
+ raw: node.text.slice(0, 200),
360
+ });
361
+ }
362
+ /**
363
+ * A `variable_declarator`'s own initialiser, whether or not the declaration's
364
+ * type was written explicitly — `recordDeclarator` only needs this for `var`,
365
+ * but a field's own inline initialiser (`private readonly HttpClient _client
366
+ * = factory.CreateClient();`) matters for D-FIX-3's client-base extractor
367
+ * (DEC-164) even when the field's type is written explicitly, so this is a
368
+ * standalone reader rather than folded into `recordDeclarator`'s own `var`
369
+ * branch.
370
+ */
371
+ function initialiserOf(declarator) {
372
+ let initialiser;
373
+ for (let inner = 0; inner < declarator.namedChildCount; inner += 1) {
374
+ const child = declarator.namedChild(inner);
375
+ if (child !== null && child.type !== "bracketed_argument_list")
376
+ initialiser = child;
377
+ }
378
+ return initialiser;
379
+ }
380
+ /** `var x = new Order();` and `var x = Build();` — bind the local's type. */
381
+ function recordDeclarator(scope, declaration) {
382
+ const written = typeName(declaration.childForFieldName("type"));
383
+ for (let index = 0; index < declaration.namedChildCount; index += 1) {
384
+ const declarator = declaration.namedChild(index);
385
+ if (declarator?.type !== "variable_declarator")
386
+ continue;
387
+ const name = declarator.childForFieldName("name")?.text.trim();
388
+ if (name === undefined)
389
+ continue;
390
+ if (typeof written === "string") {
391
+ scope.locals.set(name, written);
392
+ continue;
393
+ }
394
+ // `var` — the type is in the initialiser, which is still source.
395
+ const initialiser = initialiserOf(declarator);
396
+ if (initialiser === undefined) {
397
+ scope.locals.set(name, written ?? null);
398
+ continue;
399
+ }
400
+ if (initialiser.type === "object_creation_expression") {
401
+ const created = typeName(initialiser.childForFieldName("type"));
402
+ scope.locals.set(name, typeof created === "string" ? created : null);
403
+ continue;
404
+ }
405
+ if (initialiser.type === "invocation_expression") {
406
+ const call = callSignature(initialiser);
407
+ if (call !== undefined) {
408
+ scope.pending.set(name, call);
409
+ continue;
410
+ }
411
+ }
412
+ scope.locals.set(name, null);
413
+ }
414
+ }
415
+ function readParameters(node, scope, members) {
416
+ if (node === null || node === undefined)
417
+ return;
418
+ for (let index = 0; index < node.namedChildCount; index += 1) {
419
+ const parameter = node.namedChild(index);
420
+ if (parameter?.type !== "parameter")
421
+ continue;
422
+ const name = parameter.childForFieldName("name")?.text.trim();
423
+ const declared = typeName(parameter.childForFieldName("type"));
424
+ if (name !== undefined) {
425
+ scope.locals.set(name, declared ?? null);
426
+ scope.parameterNames.add(name);
427
+ }
428
+ if (typeof declared === "string") {
429
+ scope.refs.push({
430
+ kind: "type",
431
+ name: declared,
432
+ receiver: undefined,
433
+ line: parameter.startPosition.row + 1,
434
+ raw: parameter.text.slice(0, 160),
435
+ });
436
+ }
437
+ // A primary constructor on a `class`/`record` declares members, and this is
438
+ // the only place their types are written.
439
+ if (members !== undefined && name !== undefined)
440
+ members.set(name, declared ?? null);
441
+ }
442
+ }
443
+ function walkBody(node, scope) {
444
+ const stack = [node];
445
+ while (stack.length > 0) {
446
+ const current = stack.pop();
447
+ if (current === undefined)
448
+ continue;
449
+ switch (current.type) {
450
+ case "variable_declaration":
451
+ recordDeclarator(scope, current);
452
+ break;
453
+ case "invocation_expression":
454
+ pushInvocation(scope, current);
455
+ break;
456
+ case "assignment_expression": {
457
+ // `Client = factory.CreateClient(...)` and `this.Client = ...` — only
458
+ // D-FIX-3's client-base extractor (DEC-164) reads this, and only from
459
+ // a `.ctor`'s own scope (extract.ts's choice, not this reader's). The
460
+ // invocation itself is walked normally regardless (it is still a
461
+ // child of this node) and produces its own `CsRef`; this case exists
462
+ // purely to remember *which name the result was bound to*.
463
+ const left = current.childForFieldName("left");
464
+ const right = current.childForFieldName("right");
465
+ if (left !== null && right !== null && right.type === "invocation_expression") {
466
+ let bound;
467
+ if (left.type === "identifier")
468
+ bound = left.text.trim();
469
+ else if (left.type === "member_access_expression") {
470
+ const receiver = left.childForFieldName("expression")?.text.trim();
471
+ if (receiver === "this")
472
+ bound = left.childForFieldName("name")?.text.trim();
473
+ }
474
+ if (bound !== undefined) {
475
+ const call = callSignature(right);
476
+ if (call !== undefined)
477
+ scope.propertyAssignments.set(bound, call);
478
+ }
479
+ }
480
+ break;
481
+ }
482
+ case "object_creation_expression": {
483
+ const created = typeName(current.childForFieldName("type"));
484
+ if (typeof created === "string") {
485
+ scope.refs.push({
486
+ kind: "new",
487
+ name: created,
488
+ receiver: undefined,
489
+ line: current.startPosition.row + 1,
490
+ raw: current.text.slice(0, 200),
491
+ });
492
+ }
493
+ break;
494
+ }
495
+ case "declaration_pattern": {
496
+ // `if (item is Folder folder)` and `case Folder folder:` — a pattern
497
+ // that BINDS a local whose type is written immediately before it. Every
498
+ // call through that local was previously disclosed as an untyped
499
+ // receiver, which was honest but wasteful: the type is right there.
500
+ // jellyfin's `MetadataService.GetChildrenForMetadataUpdates` is the
501
+ // canonical shape, and it is idiomatic modern C# rather than a corner.
502
+ const declared = typeName(current.namedChild(0));
503
+ const bound = current.namedChild(1)?.text.trim();
504
+ if (bound !== undefined)
505
+ scope.locals.set(bound, typeof declared === "string" ? declared : null);
506
+ if (typeof declared === "string") {
507
+ scope.refs.push({
508
+ kind: "type",
509
+ name: declared,
510
+ receiver: undefined,
511
+ line: current.startPosition.row + 1,
512
+ raw: current.text.slice(0, 120),
513
+ });
514
+ }
515
+ break;
516
+ }
517
+ case "catch_declaration": {
518
+ const caught = typeName(current.childForFieldName("type") ?? current.namedChild(0));
519
+ const bound = current.childForFieldName("name")?.text.trim();
520
+ if (bound !== undefined)
521
+ scope.locals.set(bound, typeof caught === "string" ? caught : null);
522
+ if (typeof caught === "string") {
523
+ scope.refs.push({
524
+ kind: "type",
525
+ name: caught,
526
+ receiver: undefined,
527
+ line: current.startPosition.row + 1,
528
+ raw: current.text.slice(0, 120),
529
+ });
530
+ }
531
+ break;
532
+ }
533
+ case "member_access_expression": {
534
+ // `Status.Pending`, `Order.Empty` — a type referenced as a value, which
535
+ // is golden pattern 13. Only recorded when the holder is a bare name;
536
+ // the resolver decides whether that name is a type or a variable, and
537
+ // declines when it is neither.
538
+ const holder = current.childForFieldName("expression");
539
+ if (holder !== null && (holder.type === "identifier" || holder.type === "qualified_name")) {
540
+ scope.refs.push({
541
+ kind: "type",
542
+ name: holder.text.trim(),
543
+ receiver: undefined,
544
+ speculative: true,
545
+ line: current.startPosition.row + 1,
546
+ raw: current.text.slice(0, 120),
547
+ });
548
+ }
549
+ break;
550
+ }
551
+ default:
552
+ break;
553
+ }
554
+ for (let index = 0; index < current.namedChildCount; index += 1) {
555
+ const child = current.namedChild(index);
556
+ if (child !== null)
557
+ stack.push(child);
558
+ }
559
+ }
560
+ }
561
+ function readFunction(node, ownerPath, name) {
562
+ const scope = { refs: [], locals: new Map(), pending: new Map(), parameterNames: new Set(), propertyAssignments: new Map() };
563
+ readParameters(node.childForFieldName("parameters"), scope);
564
+ const returns = typeName(node.childForFieldName("returns"));
565
+ if (typeof returns === "string") {
566
+ scope.refs.push({
567
+ kind: "type",
568
+ name: returns,
569
+ receiver: undefined,
570
+ line: node.startPosition.row + 1,
571
+ raw: node.text.slice(0, 120),
572
+ });
573
+ }
574
+ const body = node.childForFieldName("body");
575
+ if (body !== null)
576
+ walkBody(body, scope);
577
+ return {
578
+ name,
579
+ ownerPath,
580
+ isStatic: hasModifier(node, "static"),
581
+ isAbstract: hasModifier(node, "abstract"),
582
+ attributes: attributesOf(node),
583
+ attributeArguments: attributeArgumentsOf(node),
584
+ returns,
585
+ startLine: node.startPosition.row + 1,
586
+ endLine: node.endPosition.row + 1,
587
+ scope,
588
+ };
589
+ }
590
+ /** The base list, **in written order** — DEC-084 reads position as evidence. */
591
+ function baseListOf(node) {
592
+ const names = [];
593
+ const written = [];
594
+ for (let index = 0; index < node.namedChildCount; index += 1) {
595
+ const child = node.namedChild(index);
596
+ if (child?.type !== "base_list")
597
+ continue;
598
+ for (let inner = 0; inner < child.namedChildCount; inner += 1) {
599
+ const entry = child.namedChild(inner);
600
+ if (entry === null || entry.type === "argument_list")
601
+ continue;
602
+ const source = entry.type === "primary_constructor_base_type"
603
+ ? (entry.childForFieldName("type") ?? entry.namedChild(0))
604
+ : entry;
605
+ const named = typeName(source);
606
+ if (typeof named === "string") {
607
+ names.push(named);
608
+ written.push(source?.text.trim() ?? named);
609
+ }
610
+ }
611
+ }
612
+ return { names, written };
613
+ }
614
+ const FORMS = {
615
+ class_declaration: "class",
616
+ interface_declaration: "interface",
617
+ struct_declaration: "struct",
618
+ record_declaration: "record",
619
+ record_struct_declaration: "record",
620
+ enum_declaration: "enum",
621
+ };
622
+ export function readFile(root, file, hasError) {
623
+ let namespaceName = "";
624
+ const usings = [];
625
+ const types = [];
626
+ const funcs = [];
627
+ const readUsing = (node) => {
628
+ const alias = node.childForFieldName("name")?.text.trim();
629
+ let target;
630
+ for (let index = 0; index < node.namedChildCount; index += 1) {
631
+ const child = node.namedChild(index);
632
+ if (child === null)
633
+ continue;
634
+ if (child.type === "identifier" && child.text.trim() === alias)
635
+ continue;
636
+ if (child.type === "qualified_name" || child.type === "identifier" || child.type === "generic_name") {
637
+ target = child.text.trim();
638
+ }
639
+ }
640
+ if (target === undefined)
641
+ return;
642
+ usings.push({
643
+ target,
644
+ alias,
645
+ isStatic: /\busing\s+static\b/.test(node.text),
646
+ isGlobal: /^\s*global\b/.test(node.text),
647
+ line: node.startPosition.row + 1,
648
+ });
649
+ };
650
+ const readType = (node, form, enclosing) => {
651
+ const name = node.childForFieldName("name")?.text.trim();
652
+ if (name === undefined)
653
+ return;
654
+ const path = [...enclosing, name];
655
+ const members = new Map();
656
+ const properties = [];
657
+ const typeRefs = [];
658
+ const memberProvenance = new Map();
659
+ // A primary constructor's parameters are members, and their types are
660
+ // written nowhere else: `record OrderRecord(string Id, Status Status)`.
661
+ const primary = node.childForFieldName("parameters");
662
+ if (primary !== null) {
663
+ const holder = { refs: typeRefs, locals: new Map(), pending: new Map(), parameterNames: new Set(), propertyAssignments: new Map() };
664
+ readParameters(primary, holder, members);
665
+ }
666
+ for (let index = 0; index < node.namedChildCount; index += 1) {
667
+ const child = node.namedChild(index);
668
+ if (child?.type === "parameter_list") {
669
+ const holder = { refs: typeRefs, locals: new Map(), pending: new Map(), parameterNames: new Set(), propertyAssignments: new Map() };
670
+ readParameters(child, holder, members);
671
+ }
672
+ }
673
+ const body = node.childForFieldName("body");
674
+ if (body !== null) {
675
+ for (let index = 0; index < body.namedChildCount; index += 1) {
676
+ const member = body.namedChild(index);
677
+ if (member === null)
678
+ continue;
679
+ const nested = FORMS[member.type];
680
+ if (nested !== undefined) {
681
+ readType(member, nested, path);
682
+ continue;
683
+ }
684
+ if (member.type === "method_declaration") {
685
+ const method = member.childForFieldName("name")?.text.trim();
686
+ if (method !== undefined)
687
+ funcs.push(readFunction(member, path, method));
688
+ continue;
689
+ }
690
+ if (member.type === "constructor_declaration") {
691
+ funcs.push(readFunction(member, path, ".ctor"));
692
+ continue;
693
+ }
694
+ if (member.type === "property_declaration") {
695
+ const property = member.childForFieldName("name")?.text.trim();
696
+ const declared = typeName(member.childForFieldName("type"));
697
+ if (property !== undefined)
698
+ members.set(property, declared ?? null);
699
+ if (property !== undefined) {
700
+ const accessorList = member.childForFieldName("accessors");
701
+ properties.push({
702
+ name: property,
703
+ // Raw text, because `string` and `string?` share a base name and
704
+ // have opposite nullability. See {@link CsProperty}.
705
+ written: member.childForFieldName("type")?.text.trim() ?? "",
706
+ declared,
707
+ isPublic: hasModifier(member, "public"),
708
+ // An expression-bodied property (`=> x`) is a getter with no
709
+ // accessor list, so both forms count.
710
+ hasGetter: member.childForFieldName("value") !== null ||
711
+ (accessorList?.text.includes("get") ?? false),
712
+ attributes: attributesOf(member),
713
+ line: member.startPosition.row + 1,
714
+ });
715
+ }
716
+ if (typeof declared === "string") {
717
+ typeRefs.push({
718
+ kind: "type",
719
+ name: declared,
720
+ receiver: undefined,
721
+ line: member.startPosition.row + 1,
722
+ raw: member.text.slice(0, 160),
723
+ });
724
+ }
725
+ // A property with a body is a function as well as a member.
726
+ const accessors = member.childForFieldName("accessors");
727
+ if (accessors !== null) {
728
+ const holder = { refs: typeRefs, locals: new Map(), pending: new Map(), parameterNames: new Set(), propertyAssignments: new Map() };
729
+ walkBody(accessors, holder);
730
+ }
731
+ const arrow = member.childForFieldName("value");
732
+ if (arrow !== null) {
733
+ const holder = { refs: typeRefs, locals: new Map(), pending: new Map(), parameterNames: new Set(), propertyAssignments: new Map() };
734
+ walkBody(arrow, holder);
735
+ }
736
+ continue;
737
+ }
738
+ if (member.type === "field_declaration") {
739
+ for (let inner = 0; inner < member.namedChildCount; inner += 1) {
740
+ const declaration = member.namedChild(inner);
741
+ if (declaration?.type !== "variable_declaration")
742
+ continue;
743
+ const declared = typeName(declaration.childForFieldName("type"));
744
+ for (let each = 0; each < declaration.namedChildCount; each += 1) {
745
+ const declarator = declaration.namedChild(each);
746
+ if (declarator?.type !== "variable_declarator")
747
+ continue;
748
+ const field = declarator.childForFieldName("name")?.text.trim();
749
+ if (field !== undefined)
750
+ members.set(field, declared ?? null);
751
+ // D-FIX-3's client-base extractor (DEC-164): a field's own
752
+ // inline initialiser, when it is a traceable invocation.
753
+ // `CleanArchitecture`'s `private readonly HttpClient _client =
754
+ // factory.CreateClient();` is the measured shape.
755
+ if (field !== undefined) {
756
+ const initialiser = initialiserOf(declarator);
757
+ if (initialiser?.type === "invocation_expression") {
758
+ const call = callSignature(initialiser);
759
+ if (call !== undefined)
760
+ memberProvenance.set(field, call);
761
+ }
762
+ }
763
+ }
764
+ if (typeof declared === "string") {
765
+ typeRefs.push({
766
+ kind: "type",
767
+ name: declared,
768
+ receiver: undefined,
769
+ line: member.startPosition.row + 1,
770
+ raw: member.text.slice(0, 160),
771
+ });
772
+ }
773
+ }
774
+ }
775
+ }
776
+ }
777
+ const { names: baseList, written: baseListWritten } = baseListOf(node);
778
+ for (const supertype of baseList) {
779
+ typeRefs.push({
780
+ kind: "type",
781
+ name: supertype,
782
+ receiver: undefined,
783
+ line: node.startPosition.row + 1,
784
+ raw: supertype,
785
+ });
786
+ }
787
+ types.push({
788
+ name,
789
+ path,
790
+ form,
791
+ isPartial: hasModifier(node, "partial"),
792
+ isAbstract: hasModifier(node, "abstract"),
793
+ baseList,
794
+ baseListWritten,
795
+ members,
796
+ properties,
797
+ attributes: attributesOf(node),
798
+ attributeArguments: attributeArgumentsOf(node),
799
+ startLine: node.startPosition.row + 1,
800
+ endLine: node.endPosition.row + 1,
801
+ typeRefs,
802
+ memberProvenance,
803
+ });
804
+ };
805
+ const walk = (node, enclosing) => {
806
+ for (let index = 0; index < node.namedChildCount; index += 1) {
807
+ const child = node.namedChild(index);
808
+ if (child === null)
809
+ continue;
810
+ if (child.type === "using_directive") {
811
+ readUsing(child);
812
+ continue;
813
+ }
814
+ if (child.type === "file_scoped_namespace_declaration") {
815
+ // `namespace Shop.Orders;` — no body. Everything after it in the file is
816
+ // inside it, so the remaining siblings are walked with it in force. This
817
+ // is the modern default and permits exactly one namespace per file.
818
+ const name = child.childForFieldName("name")?.text.trim();
819
+ const nested = name === undefined ? enclosing : enclosing === "" ? name : `${enclosing}.${name}`;
820
+ if (namespaceName === "" && nested !== "")
821
+ namespaceName = nested;
822
+ for (let after = index + 1; after < node.namedChildCount; after += 1) {
823
+ const sibling = node.namedChild(after);
824
+ if (sibling === null)
825
+ continue;
826
+ const form = FORMS[sibling.type];
827
+ if (form !== undefined)
828
+ readType(sibling, form, []);
829
+ else if (sibling.type === "using_directive")
830
+ readUsing(sibling);
831
+ }
832
+ return;
833
+ }
834
+ if (child.type === "namespace_declaration") {
835
+ // Block form. A file may legally declare several, and this reader takes
836
+ // the *first* — recorded as a known simplification rather than left as a
837
+ // silent one. Types after a second `namespace` block would be given a
838
+ // namespace they do not have.
839
+ const name = child.childForFieldName("name")?.text.trim();
840
+ const nested = name === undefined ? enclosing : enclosing === "" ? name : `${enclosing}.${name}`;
841
+ if (namespaceName === "" && nested !== "")
842
+ namespaceName = nested;
843
+ const body = child.childForFieldName("body");
844
+ if (body !== null)
845
+ walk(body, nested);
846
+ continue;
847
+ }
848
+ const form = FORMS[child.type];
849
+ if (form !== undefined) {
850
+ readType(child, form, []);
851
+ continue;
852
+ }
853
+ if (child.type === "preproc_if" || child.type === "declaration_list" || child.type === "global_statement") {
854
+ walk(child, enclosing);
855
+ }
856
+ }
857
+ };
858
+ walk(root, "");
859
+ return { file, namespace: namespaceName, usings, types, funcs, hasError, assembly: "" };
860
+ }
861
+ //# sourceMappingURL=parse.js.map