@descryy/adapter-java 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +76 -9
- package/dist/adapter.js.map +1 -1
- package/dist/client-base.d.ts +114 -0
- package/dist/client-base.d.ts.map +1 -0
- package/dist/client-base.js +102 -0
- package/dist/client-base.js.map +1 -0
- package/dist/client.d.ts +48 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +87 -4
- package/dist/client.js.map +1 -1
- package/dist/dto.d.ts +86 -0
- package/dist/dto.d.ts.map +1 -0
- package/dist/dto.js +198 -0
- package/dist/dto.js.map +1 -0
- package/dist/external.d.ts +169 -0
- package/dist/external.d.ts.map +1 -0
- package/dist/external.js +262 -0
- package/dist/external.js.map +1 -0
- package/dist/extract.d.ts +29 -0
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +508 -8
- package/dist/extract.js.map +1 -1
- package/dist/jackson.d.ts +46 -0
- package/dist/jackson.d.ts.map +1 -0
- package/dist/jackson.js +73 -0
- package/dist/jackson.js.map +1 -0
- package/dist/parse.d.ts +44 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +154 -6
- package/dist/parse.js.map +1 -1
- package/dist/shape-reach.d.ts +27 -8
- package/dist/shape-reach.d.ts.map +1 -1
- package/dist/shape-reach.js +40 -9
- package/dist/shape-reach.js.map +1 -1
- package/package.json +7 -7
package/dist/extract.js
CHANGED
|
@@ -18,12 +18,27 @@
|
|
|
18
18
|
* ambiguous wildcard import. Each goes to the ledger with its reason — a
|
|
19
19
|
* missing edge is a disclosed gap, a guessed one corrupts every layer above.
|
|
20
20
|
*/
|
|
21
|
-
import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
|
|
21
|
+
import { edgeId, endpointQsp, externalQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
|
|
22
|
+
import { fileNode } from "@descryy/adapter-common";
|
|
22
23
|
import { jpaModels } from "./jpa.js";
|
|
24
|
+
import { readTransportShapes, SCHEMA as DTO_SCHEMA } from "./dto.js";
|
|
25
|
+
import { hasJacksonShapeEvidence } from "./jackson.js";
|
|
23
26
|
import { SHAPE_LEVEL } from "./shape-reach.js";
|
|
24
27
|
import { jaxrsRoutes, springRoutes } from "./spring.js";
|
|
25
28
|
import { clientCalls, endpointFor } from "./client.js";
|
|
29
|
+
import { resolveBaseSite } from "./client-base.js";
|
|
30
|
+
import { attributeExternalCall, attributeExternalStaticCall, attributeExternalType, } from "./external.js";
|
|
26
31
|
export const LANGUAGE = "java";
|
|
32
|
+
/** Which packaging world an external package name belongs to. The name is the
|
|
33
|
+
* ecosystem's, not a resolved classpath's: `org.slf4j:slf4j-api` ships the
|
|
34
|
+
* package `org.slf4j`, and `org.slf4j` is what the source writes and what the
|
|
35
|
+
* node is identified by. */
|
|
36
|
+
const EXTERNAL_ECOSYSTEM = "maven";
|
|
37
|
+
/** An external node is attributed from the file's own `import` block — module
|
|
38
|
+
* resolution and nothing more. R1 exactly: never R0 (the import IS the
|
|
39
|
+
* resolution), never R4 (nothing observed it run).
|
|
40
|
+
* `DEC-NEXT-external-symbol-nodes-at-the-dependency-boundary`. */
|
|
41
|
+
const EXTERNAL_RESOLUTION = 1;
|
|
27
42
|
/** Confidence by the level an edge's evidence earned, never the level reached. */
|
|
28
43
|
const CONFIDENCE = { 0: 0.5, 1: 0.7, 2: 0.9, 3: 0.95 };
|
|
29
44
|
/** A compilation unit with no `package` declaration. Not a legal package name,
|
|
@@ -58,6 +73,39 @@ const REASONS = {
|
|
|
58
73
|
"run's analysed set — most often inherited from a supertype or interface this run did not " +
|
|
59
74
|
"read, rather than declared directly on the controller.",
|
|
60
75
|
};
|
|
76
|
+
/** The two spellings `isX()` is allowed to stand for. */
|
|
77
|
+
const BOOLEANS = new Set(["boolean", "Boolean"]);
|
|
78
|
+
/**
|
|
79
|
+
* `java.beans.Introspector.decapitalize`, to the letter — `TotalAmount`
|
|
80
|
+
* becomes `totalAmount`, and `URL` stays `URL`. Written out rather than
|
|
81
|
+
* lower-casing the first character, because the two-leading-capitals rule is
|
|
82
|
+
* the half that decides whether `getURL()` names a field called `uRL` (it
|
|
83
|
+
* does not) and a mapping lookup on the wrong spelling silently finds nothing.
|
|
84
|
+
*/
|
|
85
|
+
function decapitalize(name) {
|
|
86
|
+
const second = name[1];
|
|
87
|
+
if (second !== undefined && second !== second.toLowerCase() && /[A-Z]/.test(second))
|
|
88
|
+
return name;
|
|
89
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
90
|
+
}
|
|
91
|
+
/** The property a JavaBeans accessor name denotes, or `undefined` for a name
|
|
92
|
+
* that is not one. `isForm` distinguishes the boolean spelling, which is only
|
|
93
|
+
* legal over a `boolean`/`Boolean`. */
|
|
94
|
+
function beansProperty(name) {
|
|
95
|
+
if (name.startsWith("get") && name.length > 3)
|
|
96
|
+
return { name: decapitalize(name.slice(3)), isForm: false };
|
|
97
|
+
if (name.startsWith("is") && name.length > 2)
|
|
98
|
+
return { name: decapitalize(name.slice(2)), isForm: true };
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
/** A declared type reduced to the name the two sides can be compared on:
|
|
102
|
+
* generics dropped, package qualification dropped. `java.lang.String` and
|
|
103
|
+
* `String` are one type written two ways, and refusing the edge over the
|
|
104
|
+
* spelling would lose a real read. */
|
|
105
|
+
function simpleTypeName(written) {
|
|
106
|
+
const bare = written.replace(/<.*$/s, "").replace(/\[\]/g, "").trim();
|
|
107
|
+
return bare.slice(bare.lastIndexOf(".") + 1);
|
|
108
|
+
}
|
|
61
109
|
/**
|
|
62
110
|
* Every declared type, indexed before any node id exists, then every JPA
|
|
63
111
|
* entity resolved against that index.
|
|
@@ -105,6 +153,51 @@ export function readEntities(units) {
|
|
|
105
153
|
}
|
|
106
154
|
return entitiesByType;
|
|
107
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The fallback graph when `extract()` throws mid-run, per
|
|
158
|
+
* `DEC-NEXT-degraded-graph-shape-for-adapters-without-one.md`.
|
|
159
|
+
*
|
|
160
|
+
* `FILE`, not `MODULE`: a Java module's identity here is `identityPackage`,
|
|
161
|
+
* and `identityPackage` is derived from `unit.packageName` — the `package
|
|
162
|
+
* a.b.c;` statement *inside* the file (`roots.ts`'s `identityPackage`,
|
|
163
|
+
* layered over a source-set anchor only when one resolves; the package name
|
|
164
|
+
* itself is never filesystem-derivable, since Java allows — and this corpus
|
|
165
|
+
* contains — a file whose directory does not match its declared package).
|
|
166
|
+
* A mid-extraction throw means exactly this machinery cannot be trusted to
|
|
167
|
+
* have run to completion. Unlike Go's import path or Rust's crate/module
|
|
168
|
+
* path — both resolved from the file's own location plus a manifest, never
|
|
169
|
+
* from parsed content — nothing about a Java file's identity is recoverable
|
|
170
|
+
* without a successful parse. `FILE` (via the shared
|
|
171
|
+
* `@descryy/adapter-common#fileNode` helper) is the fact that survives: the
|
|
172
|
+
* file exists, on disk, at this path, and that is all this function claims.
|
|
173
|
+
*
|
|
174
|
+
* One disclosure row, not one per file — the fact is about the run. Anchored
|
|
175
|
+
* to the first file's `FILE` node because the ledger requires a `fromNodeId`
|
|
176
|
+
* and `FILE` is the only node type this path emits.
|
|
177
|
+
*/
|
|
178
|
+
export function degradedGraph(input) {
|
|
179
|
+
const scope = { repo: input.repo, workspace: input.workspace };
|
|
180
|
+
const nodes = input.files.map((file) => fileNode(scope, file, input.producedBy));
|
|
181
|
+
const anchor = nodes[0];
|
|
182
|
+
const unresolved = anchor === undefined
|
|
183
|
+
? []
|
|
184
|
+
: [
|
|
185
|
+
{
|
|
186
|
+
fromNodeId: anchor.id,
|
|
187
|
+
edgeType: "IMPORTS",
|
|
188
|
+
rawTarget: "(entire repository)",
|
|
189
|
+
file: anchor.file,
|
|
190
|
+
line: 1,
|
|
191
|
+
producedBy: input.producedBy,
|
|
192
|
+
reason: `NO GRAPH WAS PRODUCED for ${String(input.files.length)} Java file(s). ${input.cause}. ` +
|
|
193
|
+
"Files are listed because their existence and path are facts about the filesystem; their " +
|
|
194
|
+
"contents are absent. Every finding, coverage figure and 'not affected' statement over " +
|
|
195
|
+
"this repository is UNSUPPORTED — this is a failed analysis, not an empty repository.",
|
|
196
|
+
attrs: { refusalClass: "capability-gap" },
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
return { nodes, edges: [], unresolved };
|
|
200
|
+
}
|
|
108
201
|
/**
|
|
109
202
|
* The qualified symbol path for a Java symbol.
|
|
110
203
|
*
|
|
@@ -226,6 +319,40 @@ export function extract(input) {
|
|
|
226
319
|
const scope = input.scope;
|
|
227
320
|
/** Outbound HTTP calls, collected in the method loop and emitted with the routes. */
|
|
228
321
|
const clientCallSites = [];
|
|
322
|
+
/**
|
|
323
|
+
* DEC-164: caller node id -> the base this reader traced for it. First
|
|
324
|
+
* writer wins, so a method making two calls is described by the first one
|
|
325
|
+
* read. Patched onto the node after the walk, the way `adapter-csharp`
|
|
326
|
+
* does it — the node exists before its calls are read.
|
|
327
|
+
*/
|
|
328
|
+
const functionClientBase = new Map();
|
|
329
|
+
/**
|
|
330
|
+
* ONE PROGRAM, READ TWICE — DEC-164's origin rule needs every base in the
|
|
331
|
+
* program before any single call site can be judged.
|
|
332
|
+
*
|
|
333
|
+
* `client-base.ts` admits an absolute call path only at an origin some base
|
|
334
|
+
* in this same program resolves to, and Java is the language that forces
|
|
335
|
+
* this to be program-wide rather than per file: one public class per file is
|
|
336
|
+
* not a style here but a rule, so the constant and the absolute call site
|
|
337
|
+
* CANNOT be in the same file. This pre-pass reads only the declaration
|
|
338
|
+
* tables the parse already built — no call sites, no scope resolution — so
|
|
339
|
+
* it is cheap, and with no absolute base anywhere it yields an empty set and
|
|
340
|
+
* changes nothing.
|
|
341
|
+
*/
|
|
342
|
+
const knownOrigins = new Set();
|
|
343
|
+
for (const unit of input.units) {
|
|
344
|
+
const sites = [];
|
|
345
|
+
for (const type of unit.types) {
|
|
346
|
+
sites.push(...type.fieldBases.values());
|
|
347
|
+
for (const method of type.methods)
|
|
348
|
+
sites.push(...method.localBases.values());
|
|
349
|
+
}
|
|
350
|
+
for (const site of sites) {
|
|
351
|
+
const { origin } = resolveBaseSite(site, unit.stringConstants);
|
|
352
|
+
if (origin !== undefined)
|
|
353
|
+
knownOrigins.add(origin);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
229
356
|
/**
|
|
230
357
|
* Returns `false` when the edge's evidence outranks the level this run
|
|
231
358
|
* reached, so the caller can disclose it instead of dropping it silently.
|
|
@@ -254,6 +381,72 @@ export function extract(input) {
|
|
|
254
381
|
file: unit.file,
|
|
255
382
|
line: ref.line,
|
|
256
383
|
});
|
|
384
|
+
/**
|
|
385
|
+
* Node ids already minted for a dependency symbol — one node per symbol and
|
|
386
|
+
* per node type, not per reference site.
|
|
387
|
+
*
|
|
388
|
+
* The three settled rules are asserted here rather than argued: the qsp
|
|
389
|
+
* carries an `ext:` marker (`externalQsp`), the node claims R1 and may never
|
|
390
|
+
* claim R0 or R4, and it owns no outgoing edge because nothing read its body.
|
|
391
|
+
* `thirdParty` and `binding` ride in `attrs`, outside the hash — a package
|
|
392
|
+
* that moved between the JDK and a jar would otherwise change identity with
|
|
393
|
+
* no line of source changing.
|
|
394
|
+
*/
|
|
395
|
+
const externalIds = new Map();
|
|
396
|
+
const mintExternal = (attribution, type) => {
|
|
397
|
+
if (input.reached < EXTERNAL_RESOLUTION)
|
|
398
|
+
return null;
|
|
399
|
+
const qsp = externalQsp(attribution.moduleOrNamespace, attribution.symbolPath);
|
|
400
|
+
const key = `${type}\u0000${qsp}`;
|
|
401
|
+
const existing = externalIds.get(key);
|
|
402
|
+
if (existing !== undefined)
|
|
403
|
+
return existing;
|
|
404
|
+
const id = nodeId(scope, type, qsp, LANGUAGE);
|
|
405
|
+
externalIds.set(key, id);
|
|
406
|
+
nodes.push({
|
|
407
|
+
id,
|
|
408
|
+
type,
|
|
409
|
+
name: attribution.symbolPath[attribution.symbolPath.length - 1] ?? "",
|
|
410
|
+
// No file and no range: the symbol lives in a dependency, and a node that
|
|
411
|
+
// named a file here would be invalidated by a source file it has nothing
|
|
412
|
+
// to do with.
|
|
413
|
+
file: null,
|
|
414
|
+
range: null,
|
|
415
|
+
language: LANGUAGE,
|
|
416
|
+
producedBy: input.producedBy,
|
|
417
|
+
resolution: EXTERNAL_RESOLUTION,
|
|
418
|
+
attrs: { thirdParty: attribution.thirdParty, binding: attribution.binding },
|
|
419
|
+
external: {
|
|
420
|
+
ecosystem: EXTERNAL_ECOSYSTEM,
|
|
421
|
+
// Identity, and it is the package the source wrote — never a resolved
|
|
422
|
+
// classpath entry, never a Maven coordinate, never a version.
|
|
423
|
+
moduleOrNamespace: attribution.moduleOrNamespace,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
return id;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* The context `external.ts` attributes against, for one compilation unit.
|
|
430
|
+
* `ownPackages` and `analysed` are the repository's own; the import tables are
|
|
431
|
+
* the file's. Built lazily per unit and cached, since the two repository-wide
|
|
432
|
+
* sets are the same object every time.
|
|
433
|
+
*/
|
|
434
|
+
const externalContexts = new Map();
|
|
435
|
+
const externalContextFor = (unit) => {
|
|
436
|
+
const cached = externalContexts.get(unit.file);
|
|
437
|
+
if (cached !== undefined)
|
|
438
|
+
return cached;
|
|
439
|
+
const context = {
|
|
440
|
+
imports: {
|
|
441
|
+
singleTypeImports: unit.singleTypeImports,
|
|
442
|
+
staticMemberImports: unit.staticMemberImports,
|
|
443
|
+
},
|
|
444
|
+
ownPackages,
|
|
445
|
+
analysed: analysedFqns,
|
|
446
|
+
};
|
|
447
|
+
externalContexts.set(unit.file, context);
|
|
448
|
+
return context;
|
|
449
|
+
};
|
|
257
450
|
const ledger = (from, edgeType, rawTarget, at, reason, attrs) => {
|
|
258
451
|
unresolved.push({
|
|
259
452
|
fromNodeId: from,
|
|
@@ -284,6 +477,9 @@ export function extract(input) {
|
|
|
284
477
|
// step earlier — it decides the run's resolution level from it — so it is
|
|
285
478
|
// read there and handed on here rather than computed twice (`shape-reach.ts`).
|
|
286
479
|
const entitiesByType = input.entities ?? readEntities(input.units);
|
|
480
|
+
// Transport shapes a Spring ROUTE declares — never a class this reader
|
|
481
|
+
// guessed at from its name (DEC-043). Own file, `dto.ts`.
|
|
482
|
+
const shapesByType = readTransportShapes(input.units);
|
|
287
483
|
for (const unit of input.units) {
|
|
288
484
|
const pkg = unit.identityPackage;
|
|
289
485
|
// Named by its *primary* type, which Java requires to match the file name
|
|
@@ -324,7 +520,12 @@ export function extract(input) {
|
|
|
324
520
|
// whole batch above, since inherited mappings cross compilation units.
|
|
325
521
|
for (const type of unit.types) {
|
|
326
522
|
const model = entitiesByType.get(`${unit.identityPackage}::${type.fqn}`);
|
|
327
|
-
|
|
523
|
+
// A mapped entity outranks a declared transport shape where both somehow
|
|
524
|
+
// apply: a wrong `MODEL` is a claim about a database and a wrong `DTO` a
|
|
525
|
+
// claim about a wire format, so the narrower evidence wins — the same
|
|
526
|
+
// tie-break, for the same reason, as `adapter-python`'s.
|
|
527
|
+
const shape = model === undefined ? shapesByType.get(`${unit.identityPackage}::${type.fqn}`) : undefined;
|
|
528
|
+
const kind = model !== undefined ? "MODEL" : shape !== undefined ? "DTO" : "CLASS";
|
|
328
529
|
const id = nodeId(scope, kind, qspFor(type.fqn, unit), LANGUAGE);
|
|
329
530
|
const declaredAs = byFqn.get(type.fqn);
|
|
330
531
|
if (declaredAs === undefined)
|
|
@@ -345,11 +546,35 @@ export function extract(input) {
|
|
|
345
546
|
* (one annotation, read at R0/R1) and *this is its mapped shape* (the
|
|
346
547
|
* column-by-column read below) — and only the second is R3-grade. So
|
|
347
548
|
* the shape's level is also stated separately in `attrs.shapeResolution`,
|
|
348
|
-
* DEC-058's carrier
|
|
349
|
-
*
|
|
350
|
-
*
|
|
549
|
+
* DEC-058's carrier.
|
|
550
|
+
*
|
|
551
|
+
* **Node-scoped, and deliberately unrelated to the `USES_API`-edge
|
|
552
|
+
* `shapeResolution` this file also now writes** (see
|
|
553
|
+
* `responseShapeEvidence` below, `DEC-NEXT-dto-shape-evidence-for-api-
|
|
554
|
+
* contracts`). This one is the JPA-mapped, PERSISTED database shape —
|
|
555
|
+
* `descry-core`'s contract engine (`engine.ts:286`) reads only the
|
|
556
|
+
* edge-level attribute for API wire-shape comparison and never reads a
|
|
557
|
+
* node-level `shapeResolution` at all, on a `MODEL` or otherwise. A
|
|
558
|
+
* class can be both — an entity Jackson also serializes straight to the
|
|
559
|
+
* wire — but the two facts rest on different annotations (JPA's vs.
|
|
560
|
+
* Jackson's) and are asserted independently; this one is not the
|
|
561
|
+
* mechanism the API-shape ruling built.
|
|
351
562
|
*/
|
|
352
563
|
let level = 0;
|
|
564
|
+
if (shape !== undefined) {
|
|
565
|
+
// The declaration this node rests on, named the way a `MODEL` names
|
|
566
|
+
// its ORM — a node type minted from evidence that does not state the
|
|
567
|
+
// evidence is indistinguishable from a guess outside the adapter.
|
|
568
|
+
attrs["schema"] = DTO_SCHEMA;
|
|
569
|
+
attrs["declaredDirections"] = shape.roles;
|
|
570
|
+
// NAMES only, and no `shapeResolution`/`fieldDetail` and no level
|
|
571
|
+
// raise: whether a plain class body's field list is R3-grade evidence
|
|
572
|
+
// is a filed, unanswered question, and `shape-reach.ts` is untouched
|
|
573
|
+
// by design. Golden 05 asks for exactly this — the DTO nodes without
|
|
574
|
+
// field detail rather than guessed fields.
|
|
575
|
+
if (shape.fields.length > 0)
|
|
576
|
+
attrs["fields"] = shape.fields;
|
|
577
|
+
}
|
|
353
578
|
if (model !== undefined) {
|
|
354
579
|
attrs["orm"] = "jpa";
|
|
355
580
|
if (model.table !== null)
|
|
@@ -465,6 +690,41 @@ export function extract(input) {
|
|
|
465
690
|
}
|
|
466
691
|
return { reason: bySimpleName.has(written) ? REASONS.unknownName : REASONS.outside };
|
|
467
692
|
};
|
|
693
|
+
/**
|
|
694
|
+
* `DEC-NEXT-dto-shape-evidence-for-api-contracts`'s ruling, applied at the
|
|
695
|
+
* one place a Java `USES_API` edge states a response type: a class literal
|
|
696
|
+
* on the call (`getForObject(url, OrderResponse.class)`, read by
|
|
697
|
+
* `classLiteralArgumentOf` in `parse.ts`). Mirrors `adapter-python`'s
|
|
698
|
+
* `extract.ts:3512-3519` gate — resolve the written name through the SAME
|
|
699
|
+
* scope rules every other name in this adapter goes through, then require
|
|
700
|
+
* real Jackson evidence before claiming R3, never the class literal alone.
|
|
701
|
+
*
|
|
702
|
+
* Gated on `input.reached >= 3` INSIDE the returned attrs rather than by an
|
|
703
|
+
* early return one level up — same reasoning as `adapter-python`'s own
|
|
704
|
+
* comment at that site: a guard placed above this function would be green
|
|
705
|
+
* at R3 and silently wrong at R2, the monotonic sweep's own failure mode.
|
|
706
|
+
*
|
|
707
|
+
* A `MODEL` is excluded even when Jackson-annotated: that is a
|
|
708
|
+
* database-persistence claim (see the comment beside `attrs.shapeResolution`
|
|
709
|
+
* at the `MODEL` node above), and DEC-NEXT scopes this ruling to *wire*
|
|
710
|
+
* shape evidence, a different fact resting on different annotations.
|
|
711
|
+
*/
|
|
712
|
+
const responseShapeEvidence = (unit, written) => {
|
|
713
|
+
if (input.reached < 3 || written === undefined)
|
|
714
|
+
return {};
|
|
715
|
+
const resolved = resolveType(unit, written);
|
|
716
|
+
if (!("declared" in resolved))
|
|
717
|
+
return {};
|
|
718
|
+
const { declared } = resolved;
|
|
719
|
+
const key = `${declared.unit.identityPackage}::${declared.type.fqn}`;
|
|
720
|
+
if (entitiesByType.has(key))
|
|
721
|
+
return {}; // a MODEL: persisted shape, not wire shape
|
|
722
|
+
if (shapesByType.get(key) === undefined)
|
|
723
|
+
return {}; // not an established DTO
|
|
724
|
+
if (!hasJacksonShapeEvidence(declared.type, declared.unit))
|
|
725
|
+
return {};
|
|
726
|
+
return { responseType: declared.id, shapeResolution: 3 };
|
|
727
|
+
};
|
|
468
728
|
/**
|
|
469
729
|
* The declared type of a field, following the scopes Java says are in scope.
|
|
470
730
|
*
|
|
@@ -530,6 +790,21 @@ export function extract(input) {
|
|
|
530
790
|
}
|
|
531
791
|
return undefined;
|
|
532
792
|
};
|
|
793
|
+
/**
|
|
794
|
+
* Packages this repository declares for itself, and every fully-qualified
|
|
795
|
+
* name this run read. Both are what `external.ts` refuses against: a type
|
|
796
|
+
* under one of our own packages that went unanalysed is unanalysed code of
|
|
797
|
+
* OURS, and calling it a dependency is the error that never surfaces.
|
|
798
|
+
*
|
|
799
|
+
* This is also the only guard against `REASONS.outside`'s disclosed
|
|
800
|
+
* cross-language case — 238 of `okhttp`'s 458 `outside` rows name a type
|
|
801
|
+
* declared in a `.kt` file in this same repository. A Kotlin class in a
|
|
802
|
+
* package this repository's Java sources also declare is ours by this check.
|
|
803
|
+
* One in a package no Java source declares is not caught, and stays a
|
|
804
|
+
* disclosed limit of a Java-only reader.
|
|
805
|
+
*/
|
|
806
|
+
const ownPackages = new Set(input.units.map((unit) => unit.packageName).filter((name) => name !== "" && name !== DEFAULT_PACKAGE));
|
|
807
|
+
const analysedFqns = new Set(byFqn.keys());
|
|
533
808
|
for (const unit of input.units) {
|
|
534
809
|
const moduleId = moduleIdOf.get(unit.file);
|
|
535
810
|
if (moduleId === undefined)
|
|
@@ -583,8 +858,32 @@ export function extract(input) {
|
|
|
583
858
|
const read = clientCalls(method, member.id, (name) => writtenReceiverType(method, declared, name),
|
|
584
859
|
// Source set read from the identity-package anchor, not pattern-
|
|
585
860
|
// matched off the file path.
|
|
586
|
-
isTestSourceSet(unit.identityPackage))
|
|
587
|
-
|
|
861
|
+
isTestSourceSet(unit.identityPackage), (name) => {
|
|
862
|
+
// DEC-164, resolved with the same scope rules `writtenReceiverType`
|
|
863
|
+
// uses: a local shadows a field, and `this.x` names the field.
|
|
864
|
+
const viaThis = name.startsWith("this.");
|
|
865
|
+
const bare = viaThis ? name.slice("this.".length) : name;
|
|
866
|
+
if (bare.includes("."))
|
|
867
|
+
return undefined;
|
|
868
|
+
const site = (viaThis ? undefined : method.localBases.get(bare)) ?? type.fieldBases.get(bare);
|
|
869
|
+
return site === undefined ? undefined : resolveBaseSite(site, unit.stringConstants).base;
|
|
870
|
+
}, knownOrigins);
|
|
871
|
+
for (const call of read.calls) {
|
|
872
|
+
clientCallSites.push({
|
|
873
|
+
...call,
|
|
874
|
+
...responseShapeEvidence(unit, call.responseTypeWritten),
|
|
875
|
+
});
|
|
876
|
+
if (call.clientBase !== undefined && !functionClientBase.has(call.fromId)) {
|
|
877
|
+
functionClientBase.set(call.fromId, call.clientBase);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const refusal of read.refusals) {
|
|
881
|
+
// A refusal still states a base when DEC-164 reached one — 08b's
|
|
882
|
+
// whole point is that `unresolved` is a reading, not a silence.
|
|
883
|
+
if (refusal.clientBase !== undefined && !functionClientBase.has(refusal.fromId)) {
|
|
884
|
+
functionClientBase.set(refusal.fromId, refusal.clientBase);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
588
887
|
for (const refusal of read.refusals) {
|
|
589
888
|
ledger(refusal.fromId, "USES_API", refusal.raw, { file: unit.file, line: refusal.line }, refusal.reason,
|
|
590
889
|
// Unset `refusalClass` means unclassified (DEC-242) — `attrs`
|
|
@@ -595,9 +894,20 @@ export function extract(input) {
|
|
|
595
894
|
blockedBy: refusal.blockedBy,
|
|
596
895
|
refusalClass: refusal.refusalClass,
|
|
597
896
|
...(refusal.argumentKind === undefined ? {} : { argumentKind: refusal.argumentKind }),
|
|
897
|
+
...(refusal.clientBase === undefined ? {} : { clientBase: refusal.clientBase }),
|
|
898
|
+
...(refusal.callPath === undefined ? {} : { callPath: refusal.callPath }),
|
|
598
899
|
});
|
|
599
900
|
}
|
|
600
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* Mapped-field reads, grouped by the entity they land on. Edge
|
|
904
|
+
* identity is `(from, to, type)`, so a method reading three columns
|
|
905
|
+
* of one entity is **one** edge naming three fields, not three edges
|
|
906
|
+
* — a singular `field` attribute would make the second read
|
|
907
|
+
* unrepresentable (DEC-046). Sorted on the way out so the attribute
|
|
908
|
+
* is a property of the source rather than of statement order.
|
|
909
|
+
*/
|
|
910
|
+
const fieldReads = new Map();
|
|
601
911
|
for (const ref of method.refs) {
|
|
602
912
|
if (ref.kind === "type") {
|
|
603
913
|
emitTypeRef(member.id, unit, ref);
|
|
@@ -612,6 +922,14 @@ export function extract(input) {
|
|
|
612
922
|
emitTypeRef(member.id, unit, ref);
|
|
613
923
|
continue;
|
|
614
924
|
}
|
|
925
|
+
const read = mappedFieldRead(method, declared, unit, ref);
|
|
926
|
+
if (read !== undefined) {
|
|
927
|
+
const fields = fieldReads.get(read.to);
|
|
928
|
+
if (fields === undefined)
|
|
929
|
+
fieldReads.set(read.to, new Set([read.field]));
|
|
930
|
+
else
|
|
931
|
+
fields.add(read.field);
|
|
932
|
+
}
|
|
615
933
|
// `method`, not `member.method` — an overload set's node carries
|
|
616
934
|
// only the *first* overload, so resolving against
|
|
617
935
|
// `member.method.locals` would type overload A's variables with
|
|
@@ -619,6 +937,11 @@ export function extract(input) {
|
|
|
619
937
|
// could see.
|
|
620
938
|
emitCall(member, method, declared, unit, ref);
|
|
621
939
|
}
|
|
940
|
+
for (const [to, fields] of fieldReads) {
|
|
941
|
+
if (push({ from: member.id, to, type: "READS", attrs: { fields: [...fields].sort() } }, 2))
|
|
942
|
+
continue;
|
|
943
|
+
ledger(member.id, "READS", [...fields].sort().join(", "), { file: unit.file, line: method.startLine }, REASONS.belowLevel);
|
|
944
|
+
}
|
|
622
945
|
}
|
|
623
946
|
}
|
|
624
947
|
}
|
|
@@ -630,6 +953,27 @@ export function extract(input) {
|
|
|
630
953
|
}
|
|
631
954
|
return;
|
|
632
955
|
}
|
|
956
|
+
// A type reference that left the repository. When a binding the developer
|
|
957
|
+
// wrote settles which package and which type, it mints a `CLASS` standing
|
|
958
|
+
// for that type in the dependency and is reached by `USES_TYPE` — the node
|
|
959
|
+
// type follows the EDGE, because that is what the edge means. An attributed
|
|
960
|
+
// site files no ledger row: a site that resolves to a node is not an
|
|
961
|
+
// `UnresolvedRef`, and filing both would count it twice.
|
|
962
|
+
//
|
|
963
|
+
// `member` refs are excluded. `parse.ts` calls them a *candidate* only —
|
|
964
|
+
// `Status.PENDING` is an enum constant and `Map.Entry` is a nested type, and
|
|
965
|
+
// nothing written tells them apart. Minting a `CLASS` for the first is a
|
|
966
|
+
// wrong fact, so both are declined.
|
|
967
|
+
if (ref.kind === "type" && resolved.reason === REASONS.outside) {
|
|
968
|
+
const attribution = attributeExternalType(ref.name, externalContextFor(unit));
|
|
969
|
+
if (attribution !== null) {
|
|
970
|
+
const to = mintExternal(attribution, "CLASS");
|
|
971
|
+
if (to !== null) {
|
|
972
|
+
push({ from, to, type: "USES_TYPE" }, EXTERNAL_RESOLUTION);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
633
977
|
ledger(from, "USES_TYPE", ref.name, file(unit, ref), resolved.reason);
|
|
634
978
|
}
|
|
635
979
|
function emitCall(member, method, owner, unit, ref) {
|
|
@@ -640,6 +984,18 @@ export function extract(input) {
|
|
|
640
984
|
const resolvedType = method.isTest ? "TESTS" : "CALLS";
|
|
641
985
|
const target = receiverTarget(method, owner, unit, ref);
|
|
642
986
|
if ("reason" in target) {
|
|
987
|
+
// A call whose receiver's **written** type left the repository. Java
|
|
988
|
+
// writes its types down where Go and PHP do not, so this is the half that
|
|
989
|
+
// makes the boundary worth attributing here at all: `Logger log` under
|
|
990
|
+
// `import org.slf4j.Logger` says which package `log.info(...)` reaches.
|
|
991
|
+
//
|
|
992
|
+
// Only `outside` — `untypedReceiver` means no type was written at this
|
|
993
|
+
// reference (a `var`, a chained call, a generic variable) and there is
|
|
994
|
+
// nothing to read; `unknownName` means the simple name IS declared in this
|
|
995
|
+
// repository and calling it a dependency would be wrong.
|
|
996
|
+
if (target.reason === REASONS.outside && externalCall(member, method, owner, unit, ref)) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
643
999
|
ledger(member.id, "CALLS", ref.raw, file(unit, ref), target.reason);
|
|
644
1000
|
return;
|
|
645
1001
|
}
|
|
@@ -656,6 +1012,21 @@ export function extract(input) {
|
|
|
656
1012
|
}
|
|
657
1013
|
}
|
|
658
1014
|
if (found === undefined) {
|
|
1015
|
+
// An unqualified name bound by a `static` import whose declaring type is
|
|
1016
|
+
// not ours. This is how every JUnit assertion is written, and it arrives
|
|
1017
|
+
// here rather than at `outside` because the *enclosing* type resolved
|
|
1018
|
+
// fine — it simply declares no such member. Python's `from x import y`
|
|
1019
|
+
// shape, and the larger half there too.
|
|
1020
|
+
if (ref.receiver === undefined) {
|
|
1021
|
+
const attribution = attributeExternalStaticCall(ref.name, externalContextFor(unit));
|
|
1022
|
+
if (attribution !== null) {
|
|
1023
|
+
const to = mintExternal(attribution, "FUNCTION");
|
|
1024
|
+
if (to !== null) {
|
|
1025
|
+
push({ from: member.id, to, type: "CALLS" }, EXTERNAL_RESOLUTION);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
659
1030
|
ledger(member.id, "CALLS", ref.raw, file(unit, ref), REASONS.unmodelled);
|
|
660
1031
|
return;
|
|
661
1032
|
}
|
|
@@ -666,6 +1037,113 @@ export function extract(input) {
|
|
|
666
1037
|
ledger(member.id, "CALLS", ref.raw, file(unit, ref), REASONS.belowLevel);
|
|
667
1038
|
}
|
|
668
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* `FUNCTION --READS--> MODEL` for `order.getTotalAmount()` — golden 07.
|
|
1042
|
+
*
|
|
1043
|
+
* **Why the accessor and not the field.** A JPA entity's mapped fields are
|
|
1044
|
+
* `private` in essentially all real code, because field-access mapping is
|
|
1045
|
+
* the default, so a route handler in another class cannot name the field at
|
|
1046
|
+
* all. An adapter admitting only `order.totalAmount` would declare `READS`
|
|
1047
|
+
* and then emit it almost nowhere — a declaration that is true of the code
|
|
1048
|
+
* and useless about it. The read Java actually writes is the accessor.
|
|
1049
|
+
*
|
|
1050
|
+
* **Why that is not the name heuristic rule 2 forbids.** Four *written*
|
|
1051
|
+
* things have to agree before an edge exists, and no name decides any of
|
|
1052
|
+
* them on its own:
|
|
1053
|
+
*
|
|
1054
|
+
* 1. the receiver's declared type resolves — same `receiverTarget` every
|
|
1055
|
+
* `CALLS` edge here goes through, so the receiver is typed by the
|
|
1056
|
+
* source or refused;
|
|
1057
|
+
* 2. that type was admitted as a `MODEL` by `jpa.ts`, on an
|
|
1058
|
+
* `@Entity`/`@Document` annotation;
|
|
1059
|
+
* 3. the property the accessor denotes under the JavaBeans contract is a
|
|
1060
|
+
* **mapped field** of that entity — present in the mapping that was
|
|
1061
|
+
* read, not merely a declared member; and
|
|
1062
|
+
* 4. the entity declares a **zero-parameter** method of exactly that name
|
|
1063
|
+
* whose **declared return type is the field's declared type**.
|
|
1064
|
+
*
|
|
1065
|
+
* (4) is the corroboration that makes (3) more than a spelling: a computed
|
|
1066
|
+
* `getDisplayName()` names no column, and a `getTotal()` returning `Money`
|
|
1067
|
+
* over a `long total` column does not agree with the mapping and is
|
|
1068
|
+
* refused. JPA's own property-access mode maps *through* this contract, so
|
|
1069
|
+
* following it reads a declaration rather than inferring from a name.
|
|
1070
|
+
*
|
|
1071
|
+
* **Level 2, not 3.** `shape-reach.ts` records that exactly one fact kind
|
|
1072
|
+
* in this package reaches R3 — the mapped field *shape* on a `MODEL` — and
|
|
1073
|
+
* this is not that fact. This edge says a member is read **by name**; it
|
|
1074
|
+
* compares no shapes and proves none, and the resolution cap makes a
|
|
1075
|
+
* name-level fact R2. The shape itself is already on the `MODEL` node at
|
|
1076
|
+
* R3, carrying its own `shapeResolution`; this edge points at it and claims
|
|
1077
|
+
* no more than the pointing.
|
|
1078
|
+
*
|
|
1079
|
+
* **Reads only.** `order.setTotal(x)` is a write and is deliberately not
|
|
1080
|
+
* admitted here: a write filed as a read is not a thinner claim, it is the
|
|
1081
|
+
* opposite one, and data propagation is built on the direction.
|
|
1082
|
+
*/
|
|
1083
|
+
function mappedFieldRead(method, owner, unit, ref) {
|
|
1084
|
+
if (ref.receiver === undefined || ref.receiver === "this")
|
|
1085
|
+
return undefined;
|
|
1086
|
+
const property = beansProperty(ref.name);
|
|
1087
|
+
if (property === undefined)
|
|
1088
|
+
return undefined;
|
|
1089
|
+
const target = receiverTarget(method, owner, unit, ref);
|
|
1090
|
+
if ("reason" in target)
|
|
1091
|
+
return undefined;
|
|
1092
|
+
const entity = entitiesByType.get(`${target.declared.unit.identityPackage}::${target.declared.type.fqn}`);
|
|
1093
|
+
if (entity === undefined)
|
|
1094
|
+
return undefined;
|
|
1095
|
+
if (!entity.fields.some((each) => each.name === property.name))
|
|
1096
|
+
return undefined;
|
|
1097
|
+
const accessor = target.declared.type.methods.find((each) => each.name === ref.name && each.parameterNames.size === 0);
|
|
1098
|
+
if (accessor === undefined)
|
|
1099
|
+
return undefined;
|
|
1100
|
+
const field = target.declared.type.fieldDeclarations.find((each) => each.name === property.name);
|
|
1101
|
+
if (field?.writtenType === undefined)
|
|
1102
|
+
return undefined;
|
|
1103
|
+
const returns = accessor.returnType;
|
|
1104
|
+
if (returns.kind !== "named" && returns.kind !== "primitive")
|
|
1105
|
+
return undefined;
|
|
1106
|
+
const column = simpleTypeName(field.writtenType);
|
|
1107
|
+
if (simpleTypeName(returns.name) !== column)
|
|
1108
|
+
return undefined;
|
|
1109
|
+
// `isX()` is the boolean accessor and nothing else — JavaBeans says so,
|
|
1110
|
+
// and without the check an `isbn` column would be read out of `isBn()`.
|
|
1111
|
+
if (property.isForm && !BOOLEANS.has(column))
|
|
1112
|
+
return undefined;
|
|
1113
|
+
return { to: target.declared.id, field: property.name };
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Mint the dependency symbol a call's receiver reaches, and the `CALLS` edge
|
|
1117
|
+
* into it. `true` when it did; `false` leaves the site to the ledger.
|
|
1118
|
+
*
|
|
1119
|
+
* The receiver's type is read **as written** and never inferred, by the same
|
|
1120
|
+
* two scopes `receiverTarget` reads — a local or parameter's declared type,
|
|
1121
|
+
* then a field's — plus the static case where the receiver is itself a type
|
|
1122
|
+
* name. `writtenReceiverType` returns `undefined` for a `var`, a name declared
|
|
1123
|
+
* twice, a chained call and a dotted receiver, and each of those is a site
|
|
1124
|
+
* where the source wrote no type down.
|
|
1125
|
+
*/
|
|
1126
|
+
function externalCall(member, method, owner, unit, ref) {
|
|
1127
|
+
const receiver = ref.receiver;
|
|
1128
|
+
if (receiver === undefined || receiver === "this")
|
|
1129
|
+
return false;
|
|
1130
|
+
const bare = receiver.startsWith("this.") ? receiver.slice("this.".length) : receiver;
|
|
1131
|
+
if (bare === "" || bare.includes("."))
|
|
1132
|
+
return false;
|
|
1133
|
+
// A local or field shadows a type of the same spelling, as the language
|
|
1134
|
+
// says — so the written type is asked for first and the receiver is read as
|
|
1135
|
+
// a type name only when nothing local bound it.
|
|
1136
|
+
const written = writtenReceiverType(method, owner, receiver) ??
|
|
1137
|
+
(/^[A-Z]/.test(bare) ? bare : undefined);
|
|
1138
|
+
const attribution = attributeExternalCall(written, ref.name, externalContextFor(unit));
|
|
1139
|
+
if (attribution === null)
|
|
1140
|
+
return false;
|
|
1141
|
+
const to = mintExternal(attribution, "FUNCTION");
|
|
1142
|
+
if (to === null)
|
|
1143
|
+
return false;
|
|
1144
|
+
push({ from: member.id, to, type: "CALLS" }, EXTERNAL_RESOLUTION);
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
669
1147
|
/** Which type a call's receiver names, from what the source wrote down. */
|
|
670
1148
|
function receiverTarget(method, owner, unit, ref) {
|
|
671
1149
|
const receiver = ref.receiver;
|
|
@@ -830,11 +1308,33 @@ export function extract(input) {
|
|
|
830
1308
|
// 12 of Java's 16 relative-path call sites are test files, none
|
|
831
1309
|
// production — `test` feeds Verification status (integration
|
|
832
1310
|
// coverage), not the confidence label. DEC-189.
|
|
833
|
-
attrs: {
|
|
1311
|
+
attrs: {
|
|
1312
|
+
callerKind: call.callerKind,
|
|
1313
|
+
via: call.method,
|
|
1314
|
+
...(call.responseType === undefined
|
|
1315
|
+
? {}
|
|
1316
|
+
: { responseType: call.responseType, shapeResolution: call.shapeResolution }),
|
|
1317
|
+
},
|
|
834
1318
|
}, 2);
|
|
835
1319
|
}
|
|
836
1320
|
nodes.push(...routeNodes.values(), ...endpointNodes.values());
|
|
837
1321
|
}
|
|
1322
|
+
// DEC-164's client base, patched onto the caller. NOT behind the R2 gate
|
|
1323
|
+
// the `USES_API` edge sits behind: the golden asserts `attrs.clientBase` at
|
|
1324
|
+
// R2 and the field is a reading of the source, not of a rung.
|
|
1325
|
+
// `adapter-python` shipped this patch after an `input.reached` guard and it
|
|
1326
|
+
// was green at R3 and absent at R2 — only the monotonic sweep saw it.
|
|
1327
|
+
//
|
|
1328
|
+
// The field's ABSENCE is DEC-164's fourth state: a method whose receiver's
|
|
1329
|
+
// declaration this reader never saw says nothing about a base, rather than
|
|
1330
|
+
// saying `none`.
|
|
1331
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
1332
|
+
const node = nodes[i];
|
|
1333
|
+
const base = functionClientBase.get(node.id);
|
|
1334
|
+
if (base === undefined)
|
|
1335
|
+
continue;
|
|
1336
|
+
nodes[i] = { ...node, attrs: { ...node.attrs, clientBase: base } };
|
|
1337
|
+
}
|
|
838
1338
|
return { nodes, edges, unresolved };
|
|
839
1339
|
}
|
|
840
1340
|
//# sourceMappingURL=extract.js.map
|