@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/adapter.d.ts +31 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +298 -0
- package/dist/adapter.js.map +1 -0
- package/dist/client.d.ts +171 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +471 -0
- package/dist/client.js.map +1 -0
- package/dist/extract.d.ts +49 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +892 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/jpa.d.ts +107 -0
- package/dist/jpa.d.ts.map +1 -0
- package/dist/jpa.js +312 -0
- package/dist/jpa.js.map +1 -0
- package/dist/parse.d.ts +297 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +765 -0
- package/dist/parse.js.map +1 -0
- package/dist/roots.d.ts +9 -0
- package/dist/roots.d.ts.map +1 -0
- package/dist/roots.js +9 -0
- package/dist/roots.js.map +1 -0
- package/dist/spring.d.ts +78 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +397 -0
- package/dist/spring.js.map +1 -0
- package/dist/tree-sitter-java.wasm +0 -0
- package/package.json +36 -0
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
|