@descryy/adapter-java 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,765 @@
1
+ /**
2
+ * One Java compilation unit, read into the shape the resolver wants.
3
+ *
4
+ * The walk is hand-written rather than expressed as tree-sitter queries. A query
5
+ * returns matches; what this needs is *containment* — which class a method
6
+ * belongs to, which method a call sits in, which type a field was declared with —
7
+ * and containment is exactly what a match set discards. The TypeScript and Python
8
+ * extractors are both shaped this way for the same reason.
9
+ */
10
+ /** Annotations that mark a method as a test case, across JUnit 4, 5 and TestNG. */
11
+ const TEST_ANNOTATIONS = new Set([
12
+ "Test",
13
+ "ParameterizedTest",
14
+ "RepeatedTest",
15
+ "TestFactory",
16
+ "TestTemplate",
17
+ ]);
18
+ const TYPE_DECLARATIONS = {
19
+ class_declaration: "class",
20
+ interface_declaration: "interface",
21
+ enum_declaration: "enum",
22
+ record_declaration: "record",
23
+ annotation_type_declaration: "annotation",
24
+ };
25
+ const ABSENT = { kind: "absent" };
26
+ const UNREADABLE = { kind: "unreadable" };
27
+ /** The primitive type nodes. An array of primitives is a reference type. */
28
+ const PRIMITIVE_NODES = new Set(["integral_type", "floating_point_type", "boolean_type", "void_type"]);
29
+ /**
30
+ * Read a type node, stripped of everything that is not identity.
31
+ *
32
+ * `List<String>` is a use of `List`; `Owner[]` is a use of `Owner`; `a.b.C` is a
33
+ * use of `C` qualified by a package. Generic arguments are deliberately *not*
34
+ * followed here — the caller walks them separately when it wants them, so that a
35
+ * field of type `Map<String, Owner>` can record a dependency on `Owner` without
36
+ * `Map` and `Owner` being confused for one another.
37
+ *
38
+ * A caller wanting a name for an edge asks for `kind === "named"` and gets the
39
+ * same behaviour it always had. A caller wanting to know *why* there is no name
40
+ * can now find out. See {@link TypeRead}.
41
+ */
42
+ export function readTypeName(node) {
43
+ if (node === null)
44
+ return ABSENT;
45
+ if (PRIMITIVE_NODES.has(node.type))
46
+ return { kind: "primitive", name: node.text };
47
+ switch (node.type) {
48
+ case "type_identifier":
49
+ return { kind: "named", name: node.text };
50
+ case "generic_type":
51
+ case "array_type":
52
+ case "annotated_type": {
53
+ // An array or an annotated wrapper around a primitive is still a reference
54
+ // type — `int[]` is nullable — so a `primitive` inner read does not
55
+ // propagate outward. Only a name does.
56
+ for (let i = 0; i < node.namedChildCount; i += 1) {
57
+ const found = readTypeName(node.namedChild(i));
58
+ if (found.kind === "named")
59
+ return found;
60
+ }
61
+ return UNREADABLE;
62
+ }
63
+ case "scoped_type_identifier": {
64
+ // Two different shapes wear the same node type, and the qualifier's own
65
+ // node type is what separates them:
66
+ //
67
+ // com.example.Order qualifier is `identifier`/`scoped_identifier`
68
+ // — a package. The identity is `Order`.
69
+ // DiskLruCache.Snapshot qualifier is `type_identifier` — an enclosing
70
+ // type. The identity is the whole path, because
71
+ // `Snapshot` alone is a different question.
72
+ //
73
+ // Returning the last segment for both is what typed a local declared
74
+ // `DiskLruCache.Snapshot` as `Snapshot`, so every `snapshot.getSource()`
75
+ // was refused with "declares no such member" — true of the type it asked
76
+ // about, and it asked about the wrong one. Lane E found the same defect in
77
+ // `adapter-kotlin` from the opposite end (the *outermost* segment).
78
+ const segments = [];
79
+ for (let i = 0; i < node.namedChildCount; i += 1) {
80
+ const child = node.namedChild(i);
81
+ if (child?.type === "type_identifier")
82
+ segments.push(child.text);
83
+ }
84
+ if (segments.length === 0)
85
+ return UNREADABLE;
86
+ return { kind: "named", name: segments.join(".") };
87
+ }
88
+ default:
89
+ return UNREADABLE;
90
+ }
91
+ }
92
+ /** The declared name where one exists, for callers that only want an edge target. */
93
+ function namedOnly(node) {
94
+ const read = readTypeName(node);
95
+ return read.kind === "named" ? read.name : undefined;
96
+ }
97
+ /**
98
+ * Every type name mentioned inside a type node, generic arguments included.
99
+ *
100
+ * `var` is excluded because it is not one. The grammar gives it `type_identifier`
101
+ * like any other, so it arrived in the ledger 479 times on one repository as a
102
+ * type that could not be found — a reference the source never made, filed against
103
+ * recall as though the adapter had missed something.
104
+ *
105
+ * ## A scoped name is one name, not its segments
106
+ *
107
+ * This used to recurse through `scoped_type_identifier` and push each segment,
108
+ * so `DiskLruCache.Snapshot` arrived as `DiskLruCache` **and** `Snapshot`. Both
109
+ * failure directions were reproduced on a synthetic:
110
+ *
111
+ * - the tail resolving to an unrelated top-level `Snapshot` in the same package
112
+ * — `scopesFor` step 3 decides, so nothing else gets a turn. **A wrong edge.**
113
+ * - the tail resolving to nothing, losing the reference to the real nested type
114
+ * and disclosing it as *"no declaration for this simple name"* — true of
115
+ * `Snapshot`, and the source never wrote `Snapshot`.
116
+ *
117
+ * Measured before the change: 28 references across the four Java corpora hit the
118
+ * second case (`mall`'s `Criteria` ×19, `shopizer`'s `Category` ×4). The first
119
+ * was unwitnessed in 2,946 files — it needs a package declaring a top-level type
120
+ * whose name matches a nested one, which real code avoids. Proven on a
121
+ * synthetic, unwitnessed in the corpora; `bench/nested-type-capture.mjs`.
122
+ *
123
+ * Lane E found the mirror of this in `adapter-kotlin`, where `typeNameOf`
124
+ * returned the *outermost* segment instead. Same dropped qualifier, opposite
125
+ * end.
126
+ */
127
+ function allTypeNames(node, out) {
128
+ if (node === null)
129
+ return;
130
+ if (node.type === "scoped_type_identifier") {
131
+ // The whole path as written. Not the segments — `A.B` is a reference to one
132
+ // type, and its qualifier is what tells `B` apart from every other `B`.
133
+ //
134
+ // Built from the *direct* `type_identifier` children rather than `node.text`
135
+ // so that `A<B>.C` yields `A.C` and not the literal source with its generic
136
+ // arguments in it. Anything else under the node — a generic argument list on
137
+ // an outer segment — is still walked, so `B` is not lost.
138
+ const segments = [];
139
+ for (let i = 0; i < node.namedChildCount; i += 1) {
140
+ const child = node.namedChild(i);
141
+ if (child === null)
142
+ continue;
143
+ if (child.type === "type_identifier")
144
+ segments.push(child.text);
145
+ else
146
+ allTypeNames(child, out);
147
+ }
148
+ if (segments.length > 0)
149
+ out.push(segments.join("."));
150
+ return;
151
+ }
152
+ if (node.type === "type_identifier" && node.text !== "var")
153
+ out.push(node.text);
154
+ for (let i = 0; i < node.namedChildCount; i += 1)
155
+ allTypeNames(node.namedChild(i), out);
156
+ }
157
+ /**
158
+ * The first argument of a call, only when it is a bare string literal.
159
+ *
160
+ * A `binary_expression` first argument — `base + "/orders"` — returns
161
+ * `undefined` rather than its left operand. The written prefix is not the path
162
+ * at run time, and DEC-127 refuses an assembled path rather than reporting the
163
+ * fragment it can see. See {@link JavaRef.firstStringArgument}.
164
+ */
165
+ function firstStringArgumentOf(invocation) {
166
+ const args = invocation.childForFieldName("arguments");
167
+ if (args === null)
168
+ return undefined;
169
+ const first = args.namedChild(0);
170
+ if (first === null || first.type !== "string_literal")
171
+ return undefined;
172
+ // `"abc"` -> `abc`. A text block or an escaped quote is left as written; the
173
+ // caller only ever compares it against a path shape, and a path with an
174
+ // escape in it is not one this reader claims.
175
+ return first.text.slice(1, -1);
176
+ }
177
+ /**
178
+ * The first argument's raw text, whatever shape it is. See
179
+ * {@link JavaRef.firstArgumentText}.
180
+ */
181
+ function firstArgumentTextOf(invocation) {
182
+ const args = invocation.childForFieldName("arguments");
183
+ if (args === null)
184
+ return undefined;
185
+ const first = args.namedChild(0);
186
+ return first === null ? undefined : first.text;
187
+ }
188
+ function childOfType(node, type) {
189
+ for (let i = 0; i < node.namedChildCount; i += 1) {
190
+ const child = node.namedChild(i);
191
+ if (child !== null && child.type === type)
192
+ return child;
193
+ }
194
+ return null;
195
+ }
196
+ /**
197
+ * Every string literal an argument declares, quotes stripped.
198
+ *
199
+ * Handles the single form `"/x"` and the array form `{"/a", "/b"}`, because
200
+ * Java's annotation syntax treats them as the same thing and Spring code uses
201
+ * both freely. Anything that is not a literal contributes nothing — see
202
+ * {@link JavaAnnotation.values}.
203
+ */
204
+ function literals(node) {
205
+ if (node === null)
206
+ return [];
207
+ if (node.type === "string_literal")
208
+ return [node.text.slice(1, -1)];
209
+ // Annotation arrays are `element_value_array_initializer`, NOT the
210
+ // `array_initializer` used for ordinary Java array literals. Reading the wrong
211
+ // one silently returns nothing — which is how the array form survived a
212
+ // typecheck, a lint, 158 tests and a green control run while emitting no route
213
+ // for 140 of shopizer's 356 mappings.
214
+ if (node.type !== "element_value_array_initializer" && node.type !== "array_initializer") {
215
+ return [];
216
+ }
217
+ const out = [];
218
+ for (let i = 0; i < node.namedChildCount; i += 1) {
219
+ const element = node.namedChild(i);
220
+ if (element !== null && element.type === "string_literal") {
221
+ out.push(element.text.slice(1, -1));
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+ /**
227
+ * Every annotation on a declaration, with its arguments.
228
+ *
229
+ * Replaces the boolean-only read that `isTest` used: the same walk now produces
230
+ * the whole list, and `isTest` is derived from it. One traversal, one notion of
231
+ * what an annotation is, and no second place to keep in step.
232
+ */
233
+ /**
234
+ * Keyword modifiers on a declaration, annotations excluded.
235
+ *
236
+ * The `modifiers` node holds both — `private static final` sit beside
237
+ * `@Column(...)` as siblings — so the annotation reader walks the named
238
+ * children and this one walks all of them, taking what is left.
239
+ */
240
+ function readModifiers(node) {
241
+ const modifiers = childOfType(node, "modifiers");
242
+ if (modifiers === null)
243
+ return [];
244
+ const out = [];
245
+ for (let i = 0; i < modifiers.childCount; i += 1) {
246
+ const child = modifiers.child(i);
247
+ if (child === null || child.isNamed)
248
+ continue;
249
+ out.push(child.text);
250
+ }
251
+ return out;
252
+ }
253
+ function readAnnotations(node) {
254
+ const modifiers = childOfType(node, "modifiers");
255
+ if (modifiers === null)
256
+ return [];
257
+ const out = [];
258
+ for (let i = 0; i < modifiers.namedChildCount; i += 1) {
259
+ const annotation = modifiers.namedChild(i);
260
+ if (annotation === null)
261
+ continue;
262
+ if (annotation.type !== "marker_annotation" && annotation.type !== "annotation")
263
+ continue;
264
+ const nameNode = annotation.childForFieldName("name");
265
+ if (nameNode === null)
266
+ continue;
267
+ const qualified = nameNode.text;
268
+ const named = new Map();
269
+ const rawArguments = [];
270
+ const values = [];
271
+ const args = annotation.childForFieldName("arguments");
272
+ if (args !== null) {
273
+ for (let a = 0; a < args.namedChildCount; a += 1) {
274
+ const arg = args.namedChild(a);
275
+ if (arg === null)
276
+ continue;
277
+ rawArguments.push(arg.text);
278
+ if (arg.type === "element_value_pair") {
279
+ const key = arg.childForFieldName("key");
280
+ const found = literals(arg.childForFieldName("value"));
281
+ if (key !== null && found.length > 0)
282
+ named.set(key.text, found);
283
+ continue;
284
+ }
285
+ // The unnamed argument. Only literals are taken: a constant reference or
286
+ // a concatenation is a path this adapter cannot read, and DEC-127
287
+ // refuses it rather than recording the expression as a template.
288
+ values.push(...literals(arg));
289
+ }
290
+ }
291
+ out.push({
292
+ // `@Test` and `@org.junit.Test` are the same annotation written two ways.
293
+ name: qualified.slice(qualified.lastIndexOf(".") + 1),
294
+ qualified,
295
+ line: annotation.startPosition.row + 1,
296
+ values,
297
+ named,
298
+ rawArguments,
299
+ });
300
+ }
301
+ return out;
302
+ }
303
+ /** Does this declaration carry `@Test` or one of its siblings? */
304
+ function isTestAnnotated(annotations) {
305
+ return annotations.some((each) => TEST_ANNOTATIONS.has(each.name));
306
+ }
307
+ /**
308
+ * The name a call's receiver is written as, or `(computed)` when it is not a
309
+ * name at all.
310
+ *
311
+ * `object.text` is the receiver's *whole source text*, so a chained call put
312
+ * `config.get("airbyte")` into the ledger's target column — 18.9% of one
313
+ * repository's Java entries carried a target that was an expression rather than
314
+ * a name, and the column is indexed and later compared as a name. This is the
315
+ * same defect the TypeScript adapter carried, found the same way, in the same
316
+ * week: a raw target taken from a node's text instead of from its structure.
317
+ *
318
+ * `(computed)` is deliberately a string rather than `undefined`. `undefined`
319
+ * means *unqualified*, which resolves against the enclosing class — conflating
320
+ * the two would turn every chained call into a claim about `this`.
321
+ *
322
+ * A `method_invocation` object — `client.get().uri(...)` — is peeled the same
323
+ * way `field_access` already is, encoded as `<base>.<name>()` (the literal
324
+ * `()`, regardless of the real call's own arguments — this is a receiver
325
+ * label, not a copy of the source). This exists for exactly one consumer,
326
+ * `client.ts`'s WebClient/RestClient builder-chain reader (ledger row
327
+ * D-FIX-3's `.get().uri(...)` case): the fluent client's verb lives on the
328
+ * call *before* `uri`, not on `uri` itself, so the chain must survive past
329
+ * this function or that verb is unrecoverable. It does not create any new
330
+ * false resolution elsewhere — `receiverTarget` in `extract.ts` already
331
+ * refuses any dotted receiver as `untypedReceiver`, so a `CALLS`/`TESTS` edge
332
+ * from a chain never fires; it previously refused the same call under the
333
+ * `(computed)` label, and now refuses it under a more legible one. A base
334
+ * that itself resolves to `(computed)` (e.g. `new Foo.Builder().x()` — the
335
+ * `object_creation_expression` case, not `method_invocation`) still returns
336
+ * `(computed)` unchanged, so the OkHttp/`java.net.http` builder refusal this
337
+ * file's header documents is untouched.
338
+ */
339
+ function receiverName(node) {
340
+ if (node.type === "identifier")
341
+ return node.text;
342
+ if (node.type === "this")
343
+ return "this";
344
+ if (node.type === "field_access") {
345
+ const object = node.childForFieldName("object");
346
+ const field = node.childForFieldName("field");
347
+ if (object !== null && field !== null) {
348
+ const base = receiverName(object);
349
+ return base === "(computed)" ? "(computed)" : `${base}.${field.text}`;
350
+ }
351
+ }
352
+ if (node.type === "method_invocation") {
353
+ const object = node.childForFieldName("object");
354
+ const name = node.childForFieldName("name");
355
+ if (object !== null && name !== null) {
356
+ const base = receiverName(object);
357
+ return base === "(computed)" ? "(computed)" : `${base}.${name.text}()`;
358
+ }
359
+ }
360
+ return "(computed)";
361
+ }
362
+ /** Record a declared name, poisoning it when the same name gets two types. */
363
+ function declare(table, name, type) {
364
+ const value = type ?? null;
365
+ if (!table.has(name)) {
366
+ table.set(name, value);
367
+ return;
368
+ }
369
+ if (table.get(name) !== value)
370
+ table.set(name, null);
371
+ }
372
+ /**
373
+ * The name a `package_declaration` declares, ignoring any annotations on it.
374
+ *
375
+ * Returns `undefined` rather than `""` when no identifier is found, so the
376
+ * caller keeps whatever it had instead of silently adopting the default
377
+ * package — an unreadable declaration and an absent one are different facts.
378
+ */
379
+ function packageIdentifier(declaration) {
380
+ for (let i = 0; i < declaration.namedChildCount; i += 1) {
381
+ const child = declaration.namedChild(i);
382
+ if (child === null)
383
+ continue;
384
+ if (child.type === "identifier" || child.type === "scoped_identifier")
385
+ return child.text;
386
+ }
387
+ return undefined;
388
+ }
389
+ export function readUnit(root, file, hasError) {
390
+ const unit = {
391
+ file,
392
+ packageName: "",
393
+ identityPackage: "",
394
+ singleTypeImports: new Map(),
395
+ staticMemberImports: new Map(),
396
+ wildcardImports: [],
397
+ importLines: [],
398
+ types: [],
399
+ hasError,
400
+ };
401
+ let packageName = "";
402
+ for (let i = 0; i < root.namedChildCount; i += 1) {
403
+ const statement = root.namedChild(i);
404
+ if (statement === null)
405
+ continue;
406
+ if (statement.type === "package_declaration") {
407
+ // Read by node type, not by position. A package declaration may be
408
+ // annotated — `@NonNullApi package dev.morphia.query;` — and the
409
+ // annotation is `namedChild(0)`, so a positional read returns
410
+ // `"@NonNullApi"` as the package name. Every annotated `package-info.java`
411
+ // in a repository carries the *same* annotation, so all of them then share
412
+ // one identity package and one MODULE id: on `morphia`, 29 files collapsed
413
+ // to 1 node and 28 declarations were lost. The audit reported it as
414
+ // `cross-file-other`, which is the column that column exists for.
415
+ packageName = packageIdentifier(statement) ?? packageName;
416
+ continue;
417
+ }
418
+ if (statement.type === "import_declaration") {
419
+ const wildcard = childOfType(statement, "asterisk") !== null;
420
+ const name = statement.namedChild(0);
421
+ if (name === null)
422
+ continue;
423
+ const fqn = name.text;
424
+ const line = statement.startPosition.row + 1;
425
+ unit.importLines.push({ fqn, wildcard, line });
426
+ if (wildcard) {
427
+ unit.wildcardImports.push(fqn);
428
+ continue;
429
+ }
430
+ // A static import names a member, not a type: `a.b.C.method` binds
431
+ // `method`, and treating it as a type import would bind the simple name
432
+ // `method` to a class that does not exist. Only the declaring type is kept.
433
+ const isStatic = statement.children.some((child) => child?.type === "static");
434
+ const segments = fqn.split(".");
435
+ const owner = isStatic ? segments.slice(0, -1).join(".") : fqn;
436
+ const simple = owner.slice(owner.lastIndexOf(".") + 1);
437
+ if (simple !== "")
438
+ unit.singleTypeImports.set(simple, owner);
439
+ if (isStatic) {
440
+ const member = segments[segments.length - 1];
441
+ if (member !== undefined && member !== "")
442
+ unit.staticMemberImports.set(member, owner);
443
+ }
444
+ continue;
445
+ }
446
+ const kind = TYPE_DECLARATIONS[statement.type];
447
+ if (kind !== undefined)
448
+ readType(statement, kind, packageName, unit.types);
449
+ }
450
+ return { ...unit, packageName };
451
+ }
452
+ function readType(node, kind, prefix, into) {
453
+ const nameNode = node.childForFieldName("name");
454
+ if (nameNode === null)
455
+ return;
456
+ const simpleName = nameNode.text;
457
+ const fqn = prefix === "" ? simpleName : `${prefix}.${simpleName}`;
458
+ const type = {
459
+ simpleName,
460
+ fqn,
461
+ kind,
462
+ startLine: node.startPosition.row + 1,
463
+ endLine: node.endPosition.row + 1,
464
+ extendsNames: [],
465
+ implementsNames: [],
466
+ annotations: readAnnotations(node),
467
+ methods: [],
468
+ fields: new Map(),
469
+ fieldDeclarations: [],
470
+ typeRefs: [],
471
+ };
472
+ const superclass = childOfType(node, "superclass");
473
+ if (superclass !== null) {
474
+ const name = namedOnly(superclass.namedChild(0));
475
+ if (name !== undefined)
476
+ type.extendsNames.push(name);
477
+ }
478
+ // An interface's `extends` is a list and is an interface relationship, not a
479
+ // class one — the vocabulary has both edges and the corpus tests them apart.
480
+ const extendsInterfaces = childOfType(node, "extends_interfaces");
481
+ const superInterfaces = childOfType(node, "super_interfaces");
482
+ for (const clause of [extendsInterfaces, superInterfaces]) {
483
+ if (clause === null)
484
+ continue;
485
+ const list = childOfType(clause, "type_list");
486
+ if (list === null)
487
+ continue;
488
+ for (let i = 0; i < list.namedChildCount; i += 1) {
489
+ const name = namedOnly(list.namedChild(i));
490
+ if (name === undefined)
491
+ continue;
492
+ if (clause === extendsInterfaces)
493
+ type.extendsNames.push(name);
494
+ else
495
+ type.implementsNames.push(name);
496
+ }
497
+ }
498
+ // A record's components are its fields *and* its accessor methods. Java
499
+ // declares both implicitly, so a codebase that uses records — which is most
500
+ // modern Java — has a call graph full of `config.region()` calls with nothing
501
+ // to point at. Four of fourteen recall misses on `airbyte` were exactly this.
502
+ if (kind === "record") {
503
+ const parameters = node.childForFieldName("parameters");
504
+ for (let i = 0; parameters !== null && i < parameters.namedChildCount; i += 1) {
505
+ const component = parameters.namedChild(i);
506
+ if (component === null || component.type !== "formal_parameter")
507
+ continue;
508
+ const name = component.childForFieldName("name");
509
+ const declared = namedOnly(component.childForFieldName("type"));
510
+ if (name === null)
511
+ continue;
512
+ declare(type.fields, name.text, declared);
513
+ // The accessor is declared by the component and has the component's span:
514
+ // there is no separate source for it, and inventing one would be a lie
515
+ // about where the reader should look.
516
+ type.methods.push({
517
+ name: name.text,
518
+ fqn: `${fqn}.${name.text}`,
519
+ startLine: component.startPosition.row + 1,
520
+ endLine: component.endPosition.row + 1,
521
+ isTest: false,
522
+ // A record component's accessor is synthesised, so it carries no
523
+ // annotations of its own — the component's annotations belong to the
524
+ // field, not to the generated getter.
525
+ annotations: [],
526
+ returnType: readTypeName(component.childForFieldName("type")),
527
+ refs: [],
528
+ locals: new Map(),
529
+ parameterNames: new Set(),
530
+ });
531
+ const names = [];
532
+ allTypeNames(component.childForFieldName("type"), names);
533
+ for (const each of names) {
534
+ type.typeRefs.push({
535
+ kind: "type",
536
+ name: each,
537
+ receiver: undefined,
538
+ line: component.startPosition.row + 1,
539
+ raw: component.text.slice(0, 120),
540
+ });
541
+ }
542
+ }
543
+ }
544
+ const body = node.childForFieldName("body");
545
+ if (body !== null)
546
+ readBody(body, type, fqn, into);
547
+ into.push(type);
548
+ }
549
+ function readBody(body, type, fqn, into) {
550
+ for (let i = 0; i < body.namedChildCount; i += 1) {
551
+ const member = body.namedChild(i);
552
+ if (member === null)
553
+ continue;
554
+ // An enum's members do not sit directly in its body: the constants do, and
555
+ // everything else is wrapped in `enum_body_declarations`. Without this an
556
+ // enum contributed constants and no methods at all, so a static factory like
557
+ // `IdType.findByBsonType(…)` had nothing to resolve to.
558
+ if (member.type === "enum_body_declarations") {
559
+ readBody(member, type, fqn, into);
560
+ continue;
561
+ }
562
+ const nested = TYPE_DECLARATIONS[member.type];
563
+ if (nested !== undefined) {
564
+ readType(member, nested, fqn, into);
565
+ continue;
566
+ }
567
+ if (member.type === "field_declaration") {
568
+ const typeNode = member.childForFieldName("type");
569
+ // One read, and the caller asks it two different questions: what name does
570
+ // an edge point at, and can this field hold null. They have different
571
+ // answers for `long`, which is the whole point of {@link TypeRead}.
572
+ const read = readTypeName(typeNode);
573
+ const declared = read.kind === "named" ? read.name : undefined;
574
+ // Annotations sit on the declaration, so `int a, b;` gives both the same
575
+ // set — which is what Java means by it.
576
+ const fieldAnnotations = readAnnotations(member);
577
+ const fieldModifiers = readModifiers(member);
578
+ for (let d = 0; d < member.namedChildCount; d += 1) {
579
+ const declarator = member.namedChild(d);
580
+ if (declarator === null || declarator.type !== "variable_declarator")
581
+ continue;
582
+ const name = declarator.childForFieldName("name");
583
+ if (name === null)
584
+ continue;
585
+ declare(type.fields, name.text, declared);
586
+ type.fieldDeclarations.push({
587
+ name: name.text,
588
+ // A primitive has no declaration to point an edge at, so `declaredType`
589
+ // is `undefined` here and `primitive` is `true` — the two facts the old
590
+ // single sentinel could not tell apart.
591
+ declaredType: declared,
592
+ primitive: read.kind === "primitive",
593
+ line: member.startPosition.row + 1,
594
+ annotations: fieldAnnotations,
595
+ modifiers: fieldModifiers,
596
+ });
597
+ }
598
+ const names = [];
599
+ allTypeNames(typeNode, names);
600
+ for (const name of names) {
601
+ type.typeRefs.push({
602
+ kind: "type",
603
+ name,
604
+ receiver: undefined,
605
+ line: member.startPosition.row + 1,
606
+ raw: typeNode?.text.slice(0, 120) ?? name,
607
+ });
608
+ }
609
+ continue;
610
+ }
611
+ if (member.type === "method_declaration" || member.type === "constructor_declaration") {
612
+ readMethod(member, type, fqn);
613
+ }
614
+ }
615
+ }
616
+ function readMethod(node, type, ownerFqn) {
617
+ const nameNode = node.childForFieldName("name");
618
+ if (nameNode === null)
619
+ return;
620
+ // A constructor is named after its class, so `OrderService` would name both the
621
+ // CLASS and a FUNCTION in one file — two nodes a corpus role binds on `(file,
622
+ // name)` cannot be told apart, and the harness correctly refuses to guess.
623
+ // `<init>` is what the JVM itself calls a constructor and can collide with
624
+ // nothing, because it is not a legal Java identifier.
625
+ const name = node.type === "constructor_declaration" ? "<init>" : nameNode.text;
626
+ const annotations = readAnnotations(node);
627
+ const method = {
628
+ name,
629
+ fqn: `${ownerFqn}.${name}`,
630
+ startLine: node.startPosition.row + 1,
631
+ endLine: node.endPosition.row + 1,
632
+ isTest: isTestAnnotated(annotations),
633
+ annotations,
634
+ returnType: readTypeName(node.childForFieldName("type")),
635
+ refs: [],
636
+ locals: new Map(),
637
+ parameterNames: new Set(),
638
+ };
639
+ const parameters = node.childForFieldName("parameters");
640
+ if (parameters !== null) {
641
+ for (let i = 0; i < parameters.namedChildCount; i += 1) {
642
+ const parameter = parameters.namedChild(i);
643
+ if (parameter === null)
644
+ continue;
645
+ if (parameter.type !== "formal_parameter" && parameter.type !== "spread_parameter")
646
+ continue;
647
+ const name = parameter.childForFieldName("name");
648
+ const declared = namedOnly(parameter.childForFieldName("type"));
649
+ if (name !== null) {
650
+ declare(method.locals, name.text, declared);
651
+ method.parameterNames.add(name.text);
652
+ }
653
+ const names = [];
654
+ allTypeNames(parameter.childForFieldName("type"), names);
655
+ for (const each of names) {
656
+ method.refs.push({
657
+ kind: "type",
658
+ name: each,
659
+ receiver: undefined,
660
+ line: parameter.startPosition.row + 1,
661
+ raw: parameter.text.slice(0, 120),
662
+ });
663
+ }
664
+ }
665
+ }
666
+ const returned = namedOnly(node.childForFieldName("type"));
667
+ if (returned !== undefined) {
668
+ method.refs.push({
669
+ kind: "type",
670
+ name: returned,
671
+ receiver: undefined,
672
+ line: method.startLine,
673
+ raw: returned,
674
+ });
675
+ }
676
+ const body = node.childForFieldName("body");
677
+ if (body !== null)
678
+ walkBody(body, method);
679
+ type.methods.push(method);
680
+ }
681
+ function walkBody(node, method) {
682
+ if (node.type === "local_variable_declaration") {
683
+ const typeNode = node.childForFieldName("type");
684
+ // `var` is inference, and inference without a type checker is a guess.
685
+ const declared = typeNode?.type === "type_identifier" && typeNode.text === "var"
686
+ ? undefined
687
+ : namedOnly(typeNode);
688
+ for (let i = 0; i < node.namedChildCount; i += 1) {
689
+ const declarator = node.namedChild(i);
690
+ if (declarator === null || declarator.type !== "variable_declarator")
691
+ continue;
692
+ const name = declarator.childForFieldName("name");
693
+ if (name !== null)
694
+ declare(method.locals, name.text, declared);
695
+ }
696
+ const names = [];
697
+ allTypeNames(typeNode, names);
698
+ for (const name of names) {
699
+ method.refs.push({
700
+ kind: "type",
701
+ name,
702
+ receiver: undefined,
703
+ line: node.startPosition.row + 1,
704
+ raw: typeNode?.text.slice(0, 120) ?? name,
705
+ });
706
+ }
707
+ }
708
+ if (node.type === "method_invocation") {
709
+ const nameNode = node.childForFieldName("name");
710
+ const object = node.childForFieldName("object");
711
+ if (nameNode !== null) {
712
+ const receiver = object === null ? undefined : receiverName(object);
713
+ const literal = firstStringArgumentOf(node);
714
+ const argumentText = firstArgumentTextOf(node);
715
+ method.refs.push({
716
+ kind: "call",
717
+ name: nameNode.text,
718
+ receiver,
719
+ line: node.startPosition.row + 1,
720
+ raw: receiver === undefined ? nameNode.text : `${receiver}.${nameNode.text}`,
721
+ // Spread rather than assigned, because `exactOptionalPropertyTypes`
722
+ // distinguishes an absent property from one present and undefined, and
723
+ // that distinction is the point: absent means "not a call", present and
724
+ // undefined would mean "a call whose first argument is nothing".
725
+ ...(literal === undefined ? {} : { firstStringArgument: literal }),
726
+ ...(argumentText === undefined ? {} : { firstArgumentText: argumentText }),
727
+ });
728
+ }
729
+ }
730
+ // `Status.PENDING` — an enum constant or a static field, which is a dependency
731
+ // on the declaring type. Without this the corpus's class-used-as-value pattern
732
+ // has nothing to find, because an enum constant is not a call and not a
733
+ // declared type.
734
+ if (node.type === "field_access") {
735
+ const object = node.childForFieldName("object");
736
+ const field = node.childForFieldName("field");
737
+ if (object !== null && object.type === "identifier" && field !== null) {
738
+ method.refs.push({
739
+ kind: "member",
740
+ name: object.text,
741
+ receiver: undefined,
742
+ line: node.startPosition.row + 1,
743
+ raw: `${object.text}.${field.text}`,
744
+ });
745
+ }
746
+ }
747
+ if (node.type === "object_creation_expression") {
748
+ const created = namedOnly(node.childForFieldName("type"));
749
+ if (created !== undefined) {
750
+ method.refs.push({
751
+ kind: "type",
752
+ name: created,
753
+ receiver: undefined,
754
+ line: node.startPosition.row + 1,
755
+ raw: `new ${created}`,
756
+ });
757
+ }
758
+ }
759
+ for (let i = 0; i < node.namedChildCount; i += 1) {
760
+ const child = node.namedChild(i);
761
+ if (child !== null)
762
+ walkBody(child, method);
763
+ }
764
+ }
765
+ //# sourceMappingURL=parse.js.map