@descryy/adapter-java 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/dist/adapter.d.ts +5 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +80 -95
- package/dist/adapter.js.map +1 -1
- package/dist/client.d.ts +65 -119
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +121 -222
- package/dist/client.js.map +1 -1
- package/dist/extract.d.ts +13 -22
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +189 -288
- package/dist/extract.js.map +1 -1
- package/dist/jpa.d.ts +32 -73
- package/dist/jpa.d.ts.map +1 -1
- package/dist/jpa.js +72 -166
- package/dist/jpa.js.map +1 -1
- package/dist/parse.d.ts +83 -152
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +101 -183
- package/dist/parse.js.map +1 -1
- package/dist/spring.d.ts +24 -58
- package/dist/spring.d.ts.map +1 -1
- package/dist/spring.js +80 -209
- package/dist/spring.js.map +1 -1
- package/package.json +15 -6
package/dist/extract.js
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Java units to Canonical IR.
|
|
3
3
|
*
|
|
4
|
-
* ## Why a breadth adapter reaches further in Java than in most languages
|
|
5
|
-
*
|
|
6
4
|
* Every import is explicit and fully qualified, every field and parameter
|
|
7
|
-
* carries a written type,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* through a receiver, which in TypeScript needed R3.
|
|
11
|
-
*
|
|
12
|
-
* That is read as **R2 evidence, and never R3**. R2 is a reference resolved to a
|
|
13
|
-
* specific definition, which is exactly what happens here — the level names the
|
|
14
|
-
* evidence, not the machinery, and Java hands over the same evidence an LSP
|
|
15
|
-
* would without one being run. R3 is a *checked shape*, and a written annotation
|
|
16
|
-
* is not one: it can be shadowed, it can be generic, and nothing here verifies
|
|
17
|
-
* which overload the compiler would select. Claiming R3 for a name-level fact is
|
|
18
|
-
* the confusion DEC-058 was written about, where 83% of an adapter's edges
|
|
19
|
-
* claimed a level they had not earned.
|
|
20
|
-
*
|
|
21
|
-
* `IMPORTS` stays at R1. An import is module resolution and nothing more.
|
|
5
|
+
* carries a written type, so `repo.findById(id)` resolves from the declaration
|
|
6
|
+
* `private final OwnerRepository repo` alone — a call through a receiver,
|
|
7
|
+
* which in TypeScript needed R3.
|
|
22
8
|
*
|
|
23
|
-
*
|
|
9
|
+
* That is **R2 evidence, never R3**: a reference resolved to a specific
|
|
10
|
+
* definition. R3 is a *checked shape*, and a written annotation is not one —
|
|
11
|
+
* it can be shadowed, be generic, and nothing here verifies overload
|
|
12
|
+
* selection. Claiming R3 for a name-level fact is the confusion DEC-058 was
|
|
13
|
+
* written about (83% of an adapter's edges once claimed an unearned level).
|
|
14
|
+
* `IMPORTS` stays at R1 — module resolution, nothing more.
|
|
24
15
|
*
|
|
25
|
-
* `var` declarations, chained calls whose receiver is
|
|
26
|
-
* variables, and
|
|
27
|
-
*
|
|
28
|
-
* missing edge is a disclosed gap
|
|
16
|
+
* Deliberately unresolved: `var` declarations, chained calls whose receiver is
|
|
17
|
+
* another call, generic type variables, and names reachable only through an
|
|
18
|
+
* ambiguous wildcard import. Each goes to the ledger with its reason — a
|
|
19
|
+
* missing edge is a disclosed gap, a guessed one corrupts every layer above.
|
|
29
20
|
*/
|
|
30
21
|
import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
|
|
31
22
|
import { jpaModels } from "./jpa.js";
|
|
@@ -38,14 +29,11 @@ const CONFIDENCE = { 0: 0.5, 1: 0.7, 2: 0.9, 3: 0.95 };
|
|
|
38
29
|
* so it cannot collide with one. */
|
|
39
30
|
export const DEFAULT_PACKAGE = "(default)";
|
|
40
31
|
const REASONS = {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// category. The adapter cannot tell the two apart — deciding needs a
|
|
47
|
-
// repo-wide, cross-adapter declaration index it is not given — so it must
|
|
48
|
-
// stop claiming it can. `bench/cross-language-census.mjs`.
|
|
32
|
+
// Measured on `okhttp`: of 458 `outside` entries naming a qualified type,
|
|
33
|
+
// 238 are declared in a `.kt` file in the same repo — not a scope boundary
|
|
34
|
+
// but an unreadable-language case (rule 6's fourth category). The adapter
|
|
35
|
+
// can't tell the two apart without a cross-adapter declaration index it
|
|
36
|
+
// isn't given. `bench/cross-language-census.mjs`.
|
|
49
37
|
outside: "names a type this run did not analyse. Either a scope boundary — the JDK, a dependency " +
|
|
50
38
|
"on the classpath, a source root outside the analysed set — or a declaration this run " +
|
|
51
39
|
"could not read, in another language in this same repository. This adapter sees only " +
|
|
@@ -69,10 +57,10 @@ const REASONS = {
|
|
|
69
57
|
/**
|
|
70
58
|
* The qualified symbol path for a Java symbol.
|
|
71
59
|
*
|
|
72
|
-
* The Java package *is* the namespace, so no anchor has to be inferred the way
|
|
73
|
-
* TypeScript package root does — DEC-047's
|
|
74
|
-
*
|
|
75
|
-
*
|
|
60
|
+
* The Java package *is* the namespace, so no anchor has to be inferred the way
|
|
61
|
+
* a TypeScript package root does — DEC-047's and DEC-077's problems don't
|
|
62
|
+
* exist here. The nesting path is everything after the package:
|
|
63
|
+
* `Outer.Inner.method`.
|
|
76
64
|
*/
|
|
77
65
|
function qspFor(fqn, unit) {
|
|
78
66
|
const nested = unit.packageName !== "" && fqn.startsWith(`${unit.packageName}.`)
|
|
@@ -83,13 +71,11 @@ function qspFor(fqn, unit) {
|
|
|
83
71
|
/**
|
|
84
72
|
* Whether a unit's identity anchor names a test source set.
|
|
85
73
|
*
|
|
86
|
-
* `identityPackage` is `module/sourceSet/package` (`roots.ts`)
|
|
87
|
-
* set is the second segment when there are three
|
|
88
|
-
* has two segments and
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* declare, not a convention read off a path — the distinction `roots.ts`'s
|
|
92
|
-
* header exists for.
|
|
74
|
+
* `identityPackage` is `module/sourceSet/package` (`roots.ts`) — the source
|
|
75
|
+
* set is the second segment when there are three; a unit with no `src/` split
|
|
76
|
+
* has two segments and returns false rather than guessing. `test`,
|
|
77
|
+
* `testFixtures`, `integrationTest` are names Maven/Gradle declare, not a
|
|
78
|
+
* path convention.
|
|
93
79
|
*/
|
|
94
80
|
function isTestSourceSet(identityPackage) {
|
|
95
81
|
const segments = identityPackage.split("/");
|
|
@@ -97,10 +83,8 @@ function isTestSourceSet(identityPackage) {
|
|
|
97
83
|
return false;
|
|
98
84
|
return /^(test|.*[Tt]est.*)$/.test(segments[1] ?? "");
|
|
99
85
|
}
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
* own identity package. Two survivors is genuinely ambiguous and is refused.
|
|
103
|
-
*/
|
|
86
|
+
/** Choose among a scope's candidates, preferring the unit's own identity
|
|
87
|
+
* package. Two survivors is genuinely ambiguous and is refused. */
|
|
104
88
|
function pick(from, candidates) {
|
|
105
89
|
if (candidates === undefined || candidates.length === 0)
|
|
106
90
|
return { reason: REASONS.outside };
|
|
@@ -114,29 +98,23 @@ function pick(from, candidates) {
|
|
|
114
98
|
/**
|
|
115
99
|
* The scopes a written type name is searched in, in the order Java searches them.
|
|
116
100
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* and a column by another puts a **column on the wrong table**, which is a wrong
|
|
122
|
-
* answer rather than a missing one.
|
|
101
|
+
* Shared rather than duplicated: both the `INHERITS`/`IMPLEMENTS` edges and
|
|
102
|
+
* JPA's `@MappedSuperclass` walk resolve a supertype name this way, and a
|
|
103
|
+
* second copy would drift — putting a column on the wrong table, not just a
|
|
104
|
+
* missing edge.
|
|
123
105
|
*
|
|
124
|
-
* `decides` marks a scope that settles the question even
|
|
125
|
-
* resolve
|
|
126
|
-
* name it fails to resolve is not one a later scope may claim. Only the
|
|
127
|
-
* same-unit scope falls through,
|
|
128
|
-
* only one of them can be the match.
|
|
106
|
+
* `decides` marks a scope that settles the question even on failure to
|
|
107
|
+
* resolve — an explicit single-type import is unambiguous by language rule,
|
|
108
|
+
* so a name it fails to resolve is not one a later scope may claim. Only the
|
|
109
|
+
* same-unit scope falls through, since a file may declare several types.
|
|
129
110
|
*/
|
|
130
111
|
function scopesFor(index, unit, written) {
|
|
131
|
-
// A dotted name is
|
|
132
|
-
//
|
|
133
|
-
// `DiskLruCache.Snapshot
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// Only the whole path used to be tried, so every reference to a repo-declared
|
|
138
|
-
// nested type was lost — 28 across the four corpora, `mall`'s `Criteria` ×19.
|
|
139
|
-
// `decides: true` on that single scope is what made it terminal.
|
|
112
|
+
// A dotted name is either already fully qualified (`com.example.Order`, no
|
|
113
|
+
// scope needed) or a nested type named through its enclosing type
|
|
114
|
+
// (`DiskLruCache.Snapshot`, enclosing name resolved first) — not
|
|
115
|
+
// distinguishable by spelling, so try the whole path, then head+tail.
|
|
116
|
+
// Only the whole path used to be tried, dropping 28 nested-type references
|
|
117
|
+
// across the corpora (`mall`'s `Criteria` x19).
|
|
140
118
|
if (written.includes(".")) {
|
|
141
119
|
const asWritten = index.get(written);
|
|
142
120
|
if (asWritten !== undefined)
|
|
@@ -144,9 +122,8 @@ function scopesFor(index, unit, written) {
|
|
|
144
122
|
const cut = written.lastIndexOf(".");
|
|
145
123
|
const head = written.slice(0, cut);
|
|
146
124
|
const tail = written.slice(cut + 1);
|
|
147
|
-
// The head
|
|
148
|
-
//
|
|
149
|
-
// reimplementing the scope order a second time and letting the two drift.
|
|
125
|
+
// The head resolves by the normal rules (import, same-package, nested) —
|
|
126
|
+
// recurse rather than reimplementing the scope order and letting it drift.
|
|
150
127
|
const ordered = [];
|
|
151
128
|
for (const scope of scopesFor(index, unit, head)) {
|
|
152
129
|
for (const candidate of scope.candidates) {
|
|
@@ -159,8 +136,8 @@ function scopesFor(index, unit, written) {
|
|
|
159
136
|
ordered.push({ candidates: nested, decides: true });
|
|
160
137
|
}
|
|
161
138
|
}
|
|
162
|
-
//
|
|
163
|
-
//
|
|
139
|
+
// A refusal, not a fallback to the bare tail — resolving `A.B` as `B`
|
|
140
|
+
// is the dropped qualifier this branch exists to stop.
|
|
164
141
|
return ordered.length > 0 ? ordered : [{ candidates: [], decides: true }];
|
|
165
142
|
}
|
|
166
143
|
const ordered = [];
|
|
@@ -182,9 +159,9 @@ function scopesFor(index, unit, written) {
|
|
|
182
159
|
ordered.push({ candidates: inPackage, decides: true });
|
|
183
160
|
return ordered;
|
|
184
161
|
}
|
|
185
|
-
// 4. Wildcard imports. Accepted only when exactly one analysed type matches
|
|
186
|
-
// two candidates is genuinely ambiguous
|
|
187
|
-
//
|
|
162
|
+
// 4. Wildcard imports. Accepted only when exactly one analysed type matches —
|
|
163
|
+
// two candidates is genuinely ambiguous, and picking one is a coin toss
|
|
164
|
+
// recorded as fact.
|
|
188
165
|
const candidates = unit.wildcardImports.flatMap((wildcard) => [...(index.get(`${wildcard}.${written}`) ?? [])]);
|
|
189
166
|
if (candidates.length > 0)
|
|
190
167
|
ordered.push({ candidates, decides: true });
|
|
@@ -200,13 +177,10 @@ export function extract(input) {
|
|
|
200
177
|
const clientCallSites = [];
|
|
201
178
|
/**
|
|
202
179
|
* Returns `false` when the edge's evidence outranks the level this run
|
|
203
|
-
* reached, so the caller can disclose
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* resolution it did not perform. Capping the number instead would be worse,
|
|
208
|
-
* because the edge would survive wearing a level its evidence never earned,
|
|
209
|
-
* which is DEC-058's defect exactly.
|
|
180
|
+
* reached, so the caller can disclose it instead of dropping it silently.
|
|
181
|
+
* An edge may not claim an unreached level — the Normaliser rejects it, and
|
|
182
|
+
* capping the number instead would let the edge survive wearing a level its
|
|
183
|
+
* evidence never earned (DEC-058's defect exactly).
|
|
210
184
|
*/
|
|
211
185
|
const push = (edge, level) => {
|
|
212
186
|
if (level > input.reached)
|
|
@@ -241,18 +215,14 @@ export function extract(input) {
|
|
|
241
215
|
...(attrs === undefined ? {} : { attrs }),
|
|
242
216
|
});
|
|
243
217
|
};
|
|
244
|
-
// -------------------------------------------------------------------------
|
|
245
|
-
// Nodes
|
|
246
|
-
// -------------------------------------------------------------------------
|
|
247
218
|
/**
|
|
248
219
|
* Fully-qualified Java name -> every analysed type that declares it.
|
|
249
220
|
*
|
|
250
|
-
* A list, not a single entry
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* because the name was found.
|
|
221
|
+
* A list, not a single entry: the airbyte run declares
|
|
222
|
+
* `...RedisDataFactory` twice in two source sets that never share a
|
|
223
|
+
* classpath. A single-entry map would silently keep the last one and
|
|
224
|
+
* resolve both source sets to it — a wrong edge no recall measurement
|
|
225
|
+
* can catch, since the name was found.
|
|
256
226
|
*/
|
|
257
227
|
const byFqn = new Map();
|
|
258
228
|
const byMember = new Map();
|
|
@@ -260,16 +230,13 @@ export function extract(input) {
|
|
|
260
230
|
/** Simple name -> every analysed fully-qualified name that ends with it. */
|
|
261
231
|
const bySimpleName = new Map();
|
|
262
232
|
/**
|
|
263
|
-
* Every declared type, indexed before any node id exists
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* it
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* So the types are indexed twice, once without ids for the entity decision and
|
|
271
|
-
* once with them for the edges. The alternative was minting a `CLASS` id and
|
|
272
|
-
* rewriting it, which is the exact thing DEC-004 forbids.
|
|
233
|
+
* Every declared type, indexed before any node id exists. A JPA entity is a
|
|
234
|
+
* `MODEL` not a `CLASS`, and the id hashes the kind (DEC-004) — so the kind
|
|
235
|
+
* must be settled before minting, but that needs `@MappedSuperclass`
|
|
236
|
+
* resolution across compilation units, which needs an index `byFqn` can't
|
|
237
|
+
* be (it holds ids). So types are indexed twice: once without ids for the
|
|
238
|
+
* entity decision, once with them for edges — the alternative, minting then
|
|
239
|
+
* rewriting a `CLASS` id, is exactly what DEC-004 forbids.
|
|
273
240
|
*/
|
|
274
241
|
const typesByFqn = new Map();
|
|
275
242
|
for (const unit of input.units) {
|
|
@@ -287,9 +254,8 @@ export function extract(input) {
|
|
|
287
254
|
const found = pick(unit, scope.candidates);
|
|
288
255
|
if ("declared" in found)
|
|
289
256
|
return found.declared;
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// wrong one.
|
|
257
|
+
// Ambiguous supertype refused rather than guessed — two candidates means
|
|
258
|
+
// two different tables, and picking one puts real columns on the wrong one.
|
|
293
259
|
if (scope.decides)
|
|
294
260
|
return undefined;
|
|
295
261
|
}
|
|
@@ -297,12 +263,10 @@ export function extract(input) {
|
|
|
297
263
|
};
|
|
298
264
|
/**
|
|
299
265
|
* The entity map for the whole batch, resolved once before any id is minted.
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* would let a `@Entity` in one source set turn its namesake in another into a
|
|
305
|
-
* MODEL it never was.
|
|
266
|
+
* Keyed by `identityPackage::fqn`, not `fqn` alone — a fqn isn't unique
|
|
267
|
+
* across source sets (the same airbyte duplicate `byFqn` records), and
|
|
268
|
+
* keying on fqn alone would let an `@Entity` in one source set turn its
|
|
269
|
+
* namesake in another into a MODEL it never was.
|
|
306
270
|
*/
|
|
307
271
|
const entitiesByType = new Map();
|
|
308
272
|
for (const unit of input.units) {
|
|
@@ -312,19 +276,14 @@ export function extract(input) {
|
|
|
312
276
|
}
|
|
313
277
|
for (const unit of input.units) {
|
|
314
278
|
const pkg = unit.identityPackage;
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
// Two such files in one package would then share a module id.
|
|
324
|
-
//
|
|
325
|
-
// The stem is preferred because the language guarantees it names the public
|
|
326
|
-
// type; a top-level declaration is the fallback for a file with none, and the
|
|
327
|
-
// stem stands alone only when the file declares no top-level type at all.
|
|
279
|
+
// Named by its *primary* type, which Java requires to match the file name
|
|
280
|
+
// — a symbol path, not a file path, so a moved file keeps its identity
|
|
281
|
+
// (DEC-004). `types[0]` was not that type: nested declarations are
|
|
282
|
+
// appended before their enclosing type, so a file whose outer class held
|
|
283
|
+
// a private interface took its identity from the interface instead
|
|
284
|
+
// (`TeradataJdbcSourceAcceptanceTest.java` became module `SqlConsumer`).
|
|
285
|
+
// The stem is preferred since the language guarantees it names the public
|
|
286
|
+
// type; a top-level declaration is the fallback when the file has none.
|
|
328
287
|
const stem = unit.file.slice(unit.file.lastIndexOf("/") + 1).replace(/\.java$/, "");
|
|
329
288
|
const topLevel = unit.types.filter((each) => unit.packageName === ""
|
|
330
289
|
? !each.fqn.includes(".")
|
|
@@ -337,12 +296,11 @@ export function extract(input) {
|
|
|
337
296
|
nodes.push({
|
|
338
297
|
id: moduleId,
|
|
339
298
|
type: "MODULE",
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
// developer calls the unit and can never collide with a type.
|
|
299
|
+
// Named with its extension, identified without one — Java requires a
|
|
300
|
+
// public type's simple name to equal its file name, so a bare `Status`
|
|
301
|
+
// names both the module and its class, and a `(file, name)`-bound corpus
|
|
302
|
+
// role correctly refuses to guess between them. `Status.java` can never
|
|
303
|
+
// collide with a type name.
|
|
346
304
|
name: `${unitName}.java`,
|
|
347
305
|
file: unit.file,
|
|
348
306
|
range: { startLine: 1, endLine: 1 },
|
|
@@ -351,11 +309,9 @@ export function extract(input) {
|
|
|
351
309
|
resolution: 0,
|
|
352
310
|
attrs: unit.hasError ? { parsedWithErrors: true } : {},
|
|
353
311
|
});
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// out of `byFqn`, so one derivation keeps them in step. Resolved for the
|
|
358
|
-
// whole batch above, because inherited mappings cross compilation units.
|
|
312
|
+
// Kind decided BEFORE the id is minted (DEC-004), not patched on after —
|
|
313
|
+
// everything downstream reads the id out of `byFqn`. Resolved for the
|
|
314
|
+
// whole batch above, since inherited mappings cross compilation units.
|
|
359
315
|
for (const type of unit.types) {
|
|
360
316
|
const model = entitiesByType.get(`${unit.identityPackage}::${type.fqn}`);
|
|
361
317
|
const kind = model === undefined ? "CLASS" : "MODEL";
|
|
@@ -375,10 +331,10 @@ export function extract(input) {
|
|
|
375
331
|
attrs["orm"] = "jpa";
|
|
376
332
|
if (model.table !== null)
|
|
377
333
|
attrs["table"] = model.table;
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
334
|
+
// Not gated on `input.reached`, deliberately — a mapped column is a
|
|
335
|
+
// declaration the framework enforces, not an inference, so its
|
|
336
|
+
// resolution is the evidence's rather than the run's (DEC-058), same
|
|
337
|
+
// as adapter-python's SQLAlchemy reasoning.
|
|
382
338
|
if (model.fields.length > 0)
|
|
383
339
|
attrs["fields"] = model.fields;
|
|
384
340
|
if (model.undecided.length > 0)
|
|
@@ -396,18 +352,13 @@ export function extract(input) {
|
|
|
396
352
|
attrs,
|
|
397
353
|
});
|
|
398
354
|
/**
|
|
399
|
-
* Overloads are one node,
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* is the leak the IR boundary exists to prevent.
|
|
407
|
-
*
|
|
408
|
-
* What is lost is real and is stated: an edge into an overload set does not
|
|
409
|
-
* say which overload. That is a name-level fact, and DEC-058's rule caps it
|
|
410
|
-
* at the level its evidence earned.
|
|
355
|
+
* Overloads are one node, count disclosed rather than hidden. A
|
|
356
|
+
* qualified symbol path carries no parameter types (DEC-011), so
|
|
357
|
+
* `getPet(String)`/`getPet(Integer)` name one symbol — encoding
|
|
358
|
+
* parameters into the path would fix the collision by making Java's
|
|
359
|
+
* identity rule leak language specifics above the IR boundary. What's
|
|
360
|
+
* lost: an edge into an overload set doesn't say which overload — a
|
|
361
|
+
* name-level fact, capped by DEC-058's rule at the level it earned.
|
|
411
362
|
*/
|
|
412
363
|
const overloads = new Map();
|
|
413
364
|
for (const method of type.methods) {
|
|
@@ -419,9 +370,8 @@ export function extract(input) {
|
|
|
419
370
|
}
|
|
420
371
|
for (const [name, group] of overloads) {
|
|
421
372
|
const first = group[0];
|
|
422
|
-
// A `@Test` method is a test case, not a function
|
|
423
|
-
//
|
|
424
|
-
// coverage query count it twice.
|
|
373
|
+
// A `@Test` method is a test case, not a tested function — emitting
|
|
374
|
+
// both would double the node and let a coverage query count it twice.
|
|
425
375
|
const isCase = group.some((each) => each.isTest);
|
|
426
376
|
const memberId = isCase
|
|
427
377
|
? nodeId(scope, "TEST_CASE", testCaseQsp(unit.identityPackage, [type.simpleName], name), LANGUAGE)
|
|
@@ -431,27 +381,19 @@ export function extract(input) {
|
|
|
431
381
|
method: first,
|
|
432
382
|
ownerFqn: type.fqn,
|
|
433
383
|
});
|
|
434
|
-
// DEC-248: a CALLS edge
|
|
435
|
-
//
|
|
436
|
-
// `method`
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// node can point outside its own declared range — `spring-petclinic`'s
|
|
441
|
-
// `Owner.getPet(String)` was credited with a call that only exists in
|
|
442
|
-
// the neighbouring `getPet(Integer)` overload. The range now spans every
|
|
443
|
-
// overload in the group, so it always contains whichever one is the
|
|
444
|
-
// true source of an edge attributed to this node.
|
|
384
|
+
// DEC-248: a CALLS edge can originate from *any* overload's body —
|
|
385
|
+
// `emitCall` resolves against the specific overload, not
|
|
386
|
+
// `member.method` — so the range must span every overload, not just
|
|
387
|
+
// `first`'s lines, or an edge can point outside its own node's range
|
|
388
|
+
// (`spring-petclinic`'s `Owner.getPet(String)` was once credited with
|
|
389
|
+
// a call only in the neighbouring `getPet(Integer)`).
|
|
445
390
|
const startLine = Math.min(...group.map((each) => each.startLine));
|
|
446
391
|
const endLine = Math.max(...group.map((each) => each.endLine));
|
|
447
|
-
// Widening is only
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
// the widening fix above; disclosed here rather than silently letting
|
|
453
|
-
// a change to that unrelated member attribute to this node in diff
|
|
454
|
-
// scoping.
|
|
392
|
+
// Widening is sound only when the group's own declarations fill the
|
|
393
|
+
// span — Java doesn't require overloads to sit adjacent, so the
|
|
394
|
+
// widened range can swallow an unrelated member in the gap
|
|
395
|
+
// (`language-problems.md` §3 row 5). Disclosed rather than letting a
|
|
396
|
+
// change to that member misattribute in diff scoping.
|
|
455
397
|
const groupMembers = new Set(group);
|
|
456
398
|
const includesOtherMember = group.length > 1 &&
|
|
457
399
|
(type.methods.some((each) => !groupMembers.has(each) && each.startLine > startLine && each.startLine < endLine) ||
|
|
@@ -473,20 +415,14 @@ export function extract(input) {
|
|
|
473
415
|
}
|
|
474
416
|
}
|
|
475
417
|
}
|
|
476
|
-
// -------------------------------------------------------------------------
|
|
477
|
-
// Name resolution
|
|
478
|
-
// -------------------------------------------------------------------------
|
|
479
418
|
/**
|
|
480
|
-
*
|
|
481
|
-
* actually see.
|
|
419
|
+
* Resolve a simple or qualified type name as written inside one unit.
|
|
482
420
|
*
|
|
483
|
-
* Java's own rule is the classpath, which this adapter
|
|
484
|
-
* approximation
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* than broken arbitrarily.
|
|
421
|
+
* Java's own rule is the classpath, which this adapter doesn't read. The
|
|
422
|
+
* approximation: a declaration in the same module/source set wins outright,
|
|
423
|
+
* one candidate overall is taken, and a genuine tie is refused rather than
|
|
424
|
+
* broken arbitrarily.
|
|
488
425
|
*/
|
|
489
|
-
/** Resolve a simple or qualified type name as written inside one unit. */
|
|
490
426
|
const resolveType = (unit, written) => {
|
|
491
427
|
for (const scope of scopesFor(byFqn, unit, written)) {
|
|
492
428
|
const found = pick(unit, scope.candidates);
|
|
@@ -500,22 +436,17 @@ export function extract(input) {
|
|
|
500
436
|
/**
|
|
501
437
|
* The declared type of a field, following the scopes Java says are in scope.
|
|
502
438
|
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* instance, so it reaches the type's own fields and inherited ones and stops;
|
|
509
|
-
* a bare name additionally sees the *lexically enclosing* class's fields,
|
|
510
|
-
* which is how a `@Nested` test class reads the fields of the class holding it.
|
|
511
|
-
* Collapsing the two would resolve `this.x` against an outer class it cannot
|
|
512
|
-
* legally reach.
|
|
439
|
+
* `viaThis` distinguishes `this.pet` from a bare `pet`: `this` reaches only
|
|
440
|
+
* the type's own and inherited fields; a bare name additionally sees the
|
|
441
|
+
* *lexically enclosing* class's fields (how a `@Nested` test class reads its
|
|
442
|
+
* holder's fields). Collapsing the two would resolve `this.x` against an
|
|
443
|
+
* outer class it can't legally reach.
|
|
513
444
|
*/
|
|
514
445
|
const fieldTypeOf = (owner, name, viaThis) => {
|
|
515
446
|
const search = [owner];
|
|
516
447
|
const seen = new Set();
|
|
517
448
|
if (!viaThis) {
|
|
518
|
-
//
|
|
449
|
+
// Enclosing types, innermost first: `pkg.Outer.Inner` -> `pkg.Outer`.
|
|
519
450
|
let fqn = owner.type.fqn;
|
|
520
451
|
for (;;) {
|
|
521
452
|
const cut = fqn.lastIndexOf(".");
|
|
@@ -567,14 +498,11 @@ export function extract(input) {
|
|
|
567
498
|
}
|
|
568
499
|
return undefined;
|
|
569
500
|
};
|
|
570
|
-
// -------------------------------------------------------------------------
|
|
571
|
-
// Edges
|
|
572
|
-
// -------------------------------------------------------------------------
|
|
573
501
|
for (const unit of input.units) {
|
|
574
502
|
const moduleId = moduleIdOf.get(unit.file);
|
|
575
503
|
if (moduleId === undefined)
|
|
576
504
|
continue;
|
|
577
|
-
// IMPORTS — module to module. A wildcard import names a package
|
|
505
|
+
// IMPORTS — module to module. A wildcard import names a package, not a
|
|
578
506
|
// compilation unit, so it has nothing to point at and is disclosed instead.
|
|
579
507
|
for (const each of unit.importLines) {
|
|
580
508
|
if (each.wildcard) {
|
|
@@ -616,20 +544,19 @@ export function extract(input) {
|
|
|
616
544
|
const member = byMember.get(`${unit.identityPackage}::${method.fqn}`);
|
|
617
545
|
if (member === undefined)
|
|
618
546
|
continue;
|
|
619
|
-
// The caller's half of the HTTP boundary
|
|
620
|
-
//
|
|
621
|
-
//
|
|
547
|
+
// The caller's half of the HTTP boundary, collected here (enclosing
|
|
548
|
+
// FUNCTION node and receiver scopes both in hand) and emitted with
|
|
549
|
+
// the routes below so both halves mint one `API_ENDPOINT`.
|
|
622
550
|
{
|
|
623
551
|
const read = clientCalls(method, member.id, (name) => writtenReceiverType(method, declared, name),
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
// anchor rather than pattern-matched off the file path.
|
|
552
|
+
// Source set read from the identity-package anchor, not pattern-
|
|
553
|
+
// matched off the file path.
|
|
627
554
|
isTestSourceSet(unit.identityPackage));
|
|
628
555
|
clientCallSites.push(...read.calls);
|
|
629
556
|
for (const refusal of read.refusals) {
|
|
630
557
|
ledger(refusal.fromId, "USES_API", refusal.raw, { file: unit.file, line: refusal.line }, refusal.reason,
|
|
631
|
-
// Unset `refusalClass`
|
|
632
|
-
//
|
|
558
|
+
// Unset `refusalClass` means unclassified (DEC-242) — `attrs`
|
|
559
|
+
// omitted entirely rather than sent with an `undefined` field.
|
|
633
560
|
refusal.refusalClass === undefined
|
|
634
561
|
? undefined
|
|
635
562
|
: {
|
|
@@ -646,20 +573,18 @@ export function extract(input) {
|
|
|
646
573
|
}
|
|
647
574
|
if (ref.kind === "member") {
|
|
648
575
|
// A dotted access whose receiver is a declared variable is a field
|
|
649
|
-
// read, not a type reference
|
|
650
|
-
//
|
|
651
|
-
// and this one was never a candidate.
|
|
576
|
+
// read, not a type reference — never a candidate edge, so nothing
|
|
577
|
+
// is emitted or filed.
|
|
652
578
|
if (method.locals.has(ref.name) || type.fields.has(ref.name))
|
|
653
579
|
continue;
|
|
654
580
|
emitTypeRef(member.id, unit, ref);
|
|
655
581
|
continue;
|
|
656
582
|
}
|
|
657
|
-
// `method`, not `member.method
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
// ever see.
|
|
583
|
+
// `method`, not `member.method` — an overload set's node carries
|
|
584
|
+
// only the *first* overload, so resolving against
|
|
585
|
+
// `member.method.locals` would type overload A's variables with
|
|
586
|
+
// overload B's declarations: a wrong edge no recall measurement
|
|
587
|
+
// could see.
|
|
663
588
|
emitCall(member, method, declared, unit, ref);
|
|
664
589
|
}
|
|
665
590
|
}
|
|
@@ -676,10 +601,10 @@ export function extract(input) {
|
|
|
676
601
|
ledger(from, "USES_TYPE", ref.name, file(unit, ref), resolved.reason);
|
|
677
602
|
}
|
|
678
603
|
function emitCall(member, method, owner, unit, ref) {
|
|
679
|
-
// A coverage relation is a claim about a target
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
// its
|
|
604
|
+
// A coverage relation is a claim about a target; an unresolved reference
|
|
605
|
+
// has none, so it's filed as the call it plainly is — the TypeScript
|
|
606
|
+
// adapter learned this the hard way (21,223 assertion calls once counted
|
|
607
|
+
// in its test-coverage denominator).
|
|
683
608
|
const resolvedType = method.isTest ? "TESTS" : "CALLS";
|
|
684
609
|
const target = receiverTarget(method, owner, unit, ref);
|
|
685
610
|
if ("reason" in target) {
|
|
@@ -688,9 +613,8 @@ export function extract(input) {
|
|
|
688
613
|
}
|
|
689
614
|
let found = findMember(target.declared, ref.name);
|
|
690
615
|
// An unqualified name may be bound by a static import rather than declared
|
|
691
|
-
// on the enclosing type
|
|
692
|
-
// it is resolution
|
|
693
|
-
// and every statically-imported helper is unresolvable.
|
|
616
|
+
// on the enclosing type — the language guarantees that binding, so
|
|
617
|
+
// following it is resolution, not inference.
|
|
694
618
|
if (found === undefined && ref.receiver === undefined) {
|
|
695
619
|
const owningType = unit.staticMemberImports.get(ref.name);
|
|
696
620
|
if (owningType !== undefined) {
|
|
@@ -703,10 +627,9 @@ export function extract(input) {
|
|
|
703
627
|
ledger(member.id, "CALLS", ref.raw, file(unit, ref), REASONS.unmodelled);
|
|
704
628
|
return;
|
|
705
629
|
}
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
// evidence, not the machinery that produced it.
|
|
630
|
+
// A reference resolved to a specific definition is R2 evidence, reached
|
|
631
|
+
// here through the language's declarations rather than an LSP — DEC-058:
|
|
632
|
+
// the level describes the evidence, not the machinery.
|
|
710
633
|
if (!push({ from: member.id, to: found.id, type: resolvedType }, 2)) {
|
|
711
634
|
ledger(member.id, "CALLS", ref.raw, file(unit, ref), REASONS.belowLevel);
|
|
712
635
|
}
|
|
@@ -716,17 +639,16 @@ export function extract(input) {
|
|
|
716
639
|
const receiver = ref.receiver;
|
|
717
640
|
if (receiver === undefined || receiver === "this")
|
|
718
641
|
return { declared: owner };
|
|
719
|
-
// `this.types.findPetTypes()` —
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
// which is how every constructor-injected Spring field is written.
|
|
642
|
+
// `this.types.findPetTypes()` — an explicitly-named field receiver. Left
|
|
643
|
+
// unhandled, `this.types` resolved as neither a variable nor a type and
|
|
644
|
+
// was 3 of 4 recall misses on `spring-petclinic` — how every constructor-
|
|
645
|
+
// injected Spring field is written.
|
|
724
646
|
const viaThis = receiver.startsWith("this.");
|
|
725
647
|
const bare = viaThis ? receiver.slice("this.".length) : receiver;
|
|
726
648
|
if (bare.includes("."))
|
|
727
649
|
return { reason: REASONS.untypedReceiver };
|
|
728
|
-
// A local
|
|
729
|
-
//
|
|
650
|
+
// A local/parameter shadows a field, as the language says — but `this.x`
|
|
651
|
+
// names the field regardless of locals in scope.
|
|
730
652
|
if (!viaThis) {
|
|
731
653
|
const local = method.locals.get(bare);
|
|
732
654
|
if (local === null)
|
|
@@ -744,28 +666,23 @@ export function extract(input) {
|
|
|
744
666
|
const asType = resolveType(unit, receiver);
|
|
745
667
|
if ("declared" in asType)
|
|
746
668
|
return asType;
|
|
747
|
-
// Anything else
|
|
748
|
-
// expression
|
|
669
|
+
// Anything else — a chained call, a field of an unread supertype, an
|
|
670
|
+
// expression — wrote no type down at this reference.
|
|
749
671
|
return { reason: /^[A-Z]/.test(receiver) ? asType.reason : REASONS.untypedReceiver };
|
|
750
672
|
}
|
|
751
673
|
/**
|
|
752
|
-
* The receiver's type **as written**, without resolving it.
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* set**, so it correctly returns `outside` for all of them. The client reader
|
|
758
|
-
* does not need a declaration; it needs the name the source wrote, to compare
|
|
759
|
-
* against a closed set.
|
|
674
|
+
* The receiver's type **as written**, without resolving it. Deliberately
|
|
675
|
+
* not `receiverTarget`: HTTP client types (`RestTemplate`, `WebClient`) are
|
|
676
|
+
* Spring classes outside the analysed set, so that function correctly
|
|
677
|
+
* returns `outside` for them — this one just needs the written name to
|
|
678
|
+
* compare against a closed set.
|
|
760
679
|
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
* type at this reference and returns `undefined`, which the caller turns into
|
|
765
|
-
* a disclosed refusal rather than a guess.
|
|
680
|
+
* Scope order kept to the two cases with a written type: local/parameter,
|
|
681
|
+
* then field. Anything else returns `undefined`, turned into a disclosed
|
|
682
|
+
* refusal rather than a guess.
|
|
766
683
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
684
|
+
* `function` declaration, not `const` arrow, so it hoists — the method loop
|
|
685
|
+
* calling it runs earlier in `extract`'s body.
|
|
769
686
|
*/
|
|
770
687
|
function writtenReceiverType(method, owner, receiver) {
|
|
771
688
|
const viaThis = receiver.startsWith("this.");
|
|
@@ -786,25 +703,21 @@ export function extract(input) {
|
|
|
786
703
|
return field.written;
|
|
787
704
|
}
|
|
788
705
|
;
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
// from an ordinary class is provenance, and a route emitted from the R0 or R1
|
|
793
|
-
// rung is a claim stronger than the run that produced it — which the
|
|
794
|
-
// Normaliser rejects as RESOLUTION_EXCEEDS_BATCH.
|
|
706
|
+
// R2 and no lower — telling a controller from an ordinary class is
|
|
707
|
+
// provenance, and a route emitted below R2 claims more than the run
|
|
708
|
+
// produced (Normaliser rejects it as RESOLUTION_EXCEEDS_BATCH).
|
|
795
709
|
if (input.reached >= 2) {
|
|
796
710
|
const routeNodes = new Map();
|
|
797
711
|
const endpointNodes = new Map();
|
|
798
712
|
for (const unit of input.units) {
|
|
799
|
-
// Spring and JAX-RS
|
|
800
|
-
// provenance in `spring.ts`
|
|
801
|
-
//
|
|
802
|
-
// `language-problems.md` §3's JAX-RS row.
|
|
713
|
+
// Spring and JAX-RS read independently (each gated on its own
|
|
714
|
+
// provenance in `spring.ts`), merged here since both mint the same
|
|
715
|
+
// `API_ROUTE`/`API_ENDPOINT` shape.
|
|
803
716
|
for (const route of [...springRoutes(unit), ...jaxrsRoutes(unit)]) {
|
|
804
717
|
const template = normaliseEndpointPath(route.template);
|
|
805
718
|
const name = `${route.method} ${route.template}`;
|
|
806
|
-
// Repo-scoped
|
|
807
|
-
//
|
|
719
|
+
// Repo-scoped, template-as-written — which service serves a path is
|
|
720
|
+
// what the join must not erase.
|
|
808
721
|
const routeId = nodeId(scope, "API_ROUTE", name, LANGUAGE);
|
|
809
722
|
if (!routeNodes.has(routeId)) {
|
|
810
723
|
routeNodes.set(routeId, {
|
|
@@ -826,24 +739,18 @@ export function extract(input) {
|
|
|
826
739
|
});
|
|
827
740
|
}
|
|
828
741
|
else {
|
|
829
|
-
// A second declaration claiming the same method+path
|
|
830
|
-
// controllers
|
|
831
|
-
// serving
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
// adapters in `language-problems.md` §0.2 #10, fixed here the way
|
|
835
|
-
// `adapter-python` already fixed it — a ledger row on the collision
|
|
836
|
-
// branch, not a change to the underlying node-identity question
|
|
837
|
-
// (`DEC-A-routes-ts-reconciliation`).
|
|
742
|
+
// A second declaration claiming the same method+path is real (two
|
|
743
|
+
// controllers on overlapping prefixes, Spring and JAX-RS both
|
|
744
|
+
// serving one route), not a bug — was silent until now. Fixed the
|
|
745
|
+
// way `adapter-python` did: a ledger row on the collision branch,
|
|
746
|
+
// not a change to node identity (`DEC-A-routes-ts-reconciliation`).
|
|
838
747
|
ledger(
|
|
839
|
-
// Every unit gets a MODULE node in the Nodes section above
|
|
840
|
-
// this Edges section ever runs — always present.
|
|
748
|
+
// Every unit gets a MODULE node in the Nodes section above — always present.
|
|
841
749
|
moduleIdOf.get(unit.file), "SERVES_API", name, { file: unit.file, line: route.line }, REASONS.routeIdCollision);
|
|
842
750
|
}
|
|
843
|
-
// Workspace-scoped, fileless, language-less,
|
|
844
|
-
// endpointQsp every
|
|
845
|
-
//
|
|
846
|
-
// fail to join.
|
|
751
|
+
// Workspace-scoped, fileless, language-less, minted with the SAME
|
|
752
|
+
// endpointQsp every producer uses (DEC-115) — mismatched ids don't
|
|
753
|
+
// conflict, they silently fail to join.
|
|
847
754
|
const endpointId = nodeId(scope, "API_ENDPOINT", endpointQsp(route.method, route.template), null);
|
|
848
755
|
if (!endpointNodes.has(endpointId)) {
|
|
849
756
|
endpointNodes.set(endpointId, {
|
|
@@ -861,13 +768,10 @@ export function extract(input) {
|
|
|
861
768
|
push({ from: routeId, to: endpointId, type: "SERVES_API" }, 2);
|
|
862
769
|
}
|
|
863
770
|
}
|
|
864
|
-
//
|
|
865
|
-
//
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
// each. A call to a route this run did *not* read still mints its endpoint —
|
|
869
|
-
// that is the point of `API_ENDPOINT` being the join node: either side can
|
|
870
|
-
// exist without the other (golden 08's note).
|
|
771
|
+
// The caller's half. Mints into the SAME `endpointNodes` map, so a call to
|
|
772
|
+
// a route this run also read produces one node with two edges, not two.
|
|
773
|
+
// A call to a route this run didn't read still mints its endpoint —
|
|
774
|
+
// `API_ENDPOINT` is the join node; either side can exist alone (golden 08).
|
|
871
775
|
for (const call of clientCallSites) {
|
|
872
776
|
const endpoint = endpointFor(scope, call, input.producedBy, normaliseEndpointPath);
|
|
873
777
|
if (!endpointNodes.has(endpoint.id))
|
|
@@ -876,12 +780,9 @@ export function extract(input) {
|
|
|
876
780
|
from: call.fromId,
|
|
877
781
|
to: endpoint.id,
|
|
878
782
|
type: "USES_API",
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
// nearly all of them. `test` means this edge feeds *Verification
|
|
883
|
-
// status* — which routes have integration coverage — and not the
|
|
884
|
-
// confidence label. DEC-189.
|
|
783
|
+
// 12 of Java's 16 relative-path call sites are test files, none
|
|
784
|
+
// production — `test` feeds Verification status (integration
|
|
785
|
+
// coverage), not the confidence label. DEC-189.
|
|
885
786
|
attrs: { callerKind: call.callerKind, via: call.method },
|
|
886
787
|
}, 2);
|
|
887
788
|
}
|