@cplieger/deadset-ts 3.11.0 → 3.12.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/README.md +2 -2
- package/dist/src/analysis.js +51 -13
- package/dist/src/conformance-record.js +2 -12
- package/dist/src/consumers.js +49 -0
- package/dist/src/discover.js +59 -23
- package/dist/src/findings/completion.js +49 -17
- package/dist/src/findings/non-code-artifacts.js +9 -2
- package/dist/src/findings/reads-and-writes.js +3 -1
- package/dist/src/graph.js +15 -12
- package/dist/src/matrix.js +14 -3
- package/dist/src/references.js +21 -9
- package/dist/src/report.js +4 -1
- package/dist/src/roots.js +13 -8
- package/dist/src/session.js +15 -0
- package/dist/src/sweep.js +11 -0
- package/dist/src/verbs/analyze.js +6 -11
- package/dist/src/verbs/explain.js +49 -35
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +109 -25
- package/src/conformance-record.ts +2 -14
- package/src/consumers.ts +86 -0
- package/src/discover.ts +67 -23
- package/src/findings/completion.ts +56 -20
- package/src/findings/non-code-artifacts.ts +9 -2
- package/src/findings/reads-and-writes.ts +3 -1
- package/src/graph.ts +21 -12
- package/src/matrix.ts +14 -5
- package/src/references.ts +48 -9
- package/src/report.ts +17 -2
- package/src/roots.ts +14 -9
- package/src/session.ts +19 -0
- package/src/sweep.ts +13 -1
- package/src/verbs/analyze.ts +6 -14
- package/src/verbs/explain.ts +57 -40
package/src/graph.ts
CHANGED
|
@@ -39,15 +39,15 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Whether a root of each kind names a caller, which holds it live under both relations: a
|
|
42
|
-
* tool reads the configuration it loads, a runtime runs a test file, a setup file
|
|
43
|
-
* worker
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* tool reads the configuration it loads, a runtime runs a test file, a setup file, a
|
|
43
|
+
* worker and a command, the manifest names what enters the package, and an entry file, a
|
|
44
|
+
* configured root and a pattern assert a caller. A library's published API only supposes
|
|
45
|
+
* a consumer the run may not see, so it is live under reachability alone.
|
|
46
46
|
*/
|
|
47
47
|
const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
|
|
48
|
-
"entry-file":
|
|
49
|
-
"manifest-entry":
|
|
50
|
-
"manifest-binary":
|
|
48
|
+
"entry-file": true,
|
|
49
|
+
"manifest-entry": true,
|
|
50
|
+
"manifest-binary": true,
|
|
51
51
|
"published-api": false,
|
|
52
52
|
configured: true,
|
|
53
53
|
pattern: true,
|
|
@@ -113,6 +113,12 @@ export interface Graph {
|
|
|
113
113
|
readonly exportsOf: readonly (readonly number[])[];
|
|
114
114
|
/** Every root naming a declaration the inventory holds, in the order the root set gives them. */
|
|
115
115
|
readonly rooted: readonly Rooted[];
|
|
116
|
+
/**
|
|
117
|
+
* Every reference a loaded consumer makes to a declaration the inventory holds, in the
|
|
118
|
+
* order given. A consumer is a caller the analysis read, so the sweep holds what it
|
|
119
|
+
* names live under both relations and reaches on from there.
|
|
120
|
+
*/
|
|
121
|
+
readonly consumed: readonly Edge[];
|
|
116
122
|
/** The position of the declaration one identifier names, or {@link OUTSIDE}. */
|
|
117
123
|
at(id: string): number;
|
|
118
124
|
}
|
|
@@ -136,6 +142,7 @@ export function graphOf(
|
|
|
136
142
|
|
|
137
143
|
const out: Edge[][] = symbols.map(() => []);
|
|
138
144
|
const made = symbols.map(() => ({ production: 0, test: 0 }));
|
|
145
|
+
const consumed: Edge[] = [];
|
|
139
146
|
for (const reference of references) {
|
|
140
147
|
const to = at(reference.to);
|
|
141
148
|
if (to === OUTSIDE) {
|
|
@@ -149,11 +156,12 @@ export function graphOf(
|
|
|
149
156
|
counts.production += 1;
|
|
150
157
|
}
|
|
151
158
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
159
|
+
const edge = { to, test: reference.test, evaluation: reference.use === "evaluation" };
|
|
160
|
+
if (reference.consumer !== undefined) {
|
|
161
|
+
consumed.push(edge);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
out[at(reference.from)]?.push(edge);
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
const parent = symbols.map((symbol) => at(symbol.parent));
|
|
@@ -182,6 +190,7 @@ export function graphOf(
|
|
|
182
190
|
subject: symbols.map((symbol) => swept(symbol.kind)),
|
|
183
191
|
exportsOf,
|
|
184
192
|
rooted,
|
|
193
|
+
consumed,
|
|
185
194
|
at,
|
|
186
195
|
};
|
|
187
196
|
}
|
package/src/matrix.ts
CHANGED
|
@@ -69,12 +69,21 @@ function compare(a: string, b: string): number {
|
|
|
69
69
|
return a < b ? -1 : 1;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
|
|
72
|
+
/**
|
|
73
|
+
* What makes one reference one reference, whichever configuration made it. A consumer's
|
|
74
|
+
* position is rendered against its own root, so the consumer is part of what it names.
|
|
75
|
+
*/
|
|
76
|
+
export function referenceKey(reference: Reference): string {
|
|
74
77
|
const { path, line, column } = reference.position;
|
|
75
|
-
return [
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
+
return [
|
|
79
|
+
reference.consumer ?? "",
|
|
80
|
+
reference.from,
|
|
81
|
+
reference.to,
|
|
82
|
+
path,
|
|
83
|
+
String(line),
|
|
84
|
+
String(column),
|
|
85
|
+
reference.use,
|
|
86
|
+
].join("\u0000");
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
/**
|
package/src/references.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { aliasChains } from "./alias-chain.ts";
|
|
|
32
32
|
import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
|
|
33
33
|
import { globExpression } from "./glob.ts";
|
|
34
34
|
import { nodeKey, type Inventory } from "./inventory.ts";
|
|
35
|
+
import { relativePath } from "./paths.ts";
|
|
35
36
|
import { byPosition, renderPosition, type Position } from "./position.ts";
|
|
36
37
|
import type { ProjectView } from "./session.ts";
|
|
37
38
|
|
|
@@ -95,6 +96,21 @@ export interface Reference {
|
|
|
95
96
|
readonly resolution: Resolution;
|
|
96
97
|
/** Whether the file that made the reference is classified as a test file. */
|
|
97
98
|
readonly test: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* The identifier of the consumer whose file made the reference, absent for one the
|
|
101
|
+
* target made. A consumer's declarations are no declarations of the run, so such a
|
|
102
|
+
* reference comes from outside the inventory, and its position is rendered against
|
|
103
|
+
* the consumer's root.
|
|
104
|
+
*/
|
|
105
|
+
readonly consumer?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The consumer one reference pass reads the files of, beside the target it names. */
|
|
109
|
+
export interface ConsumerSide {
|
|
110
|
+
/** The name the consumer publishes itself under. */
|
|
111
|
+
readonly id: string;
|
|
112
|
+
/** The consumer's directory, absolute: the files below it are its own. */
|
|
113
|
+
readonly root: string;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
/** One rule that classified files as test files, and how many files it matched. */
|
|
@@ -162,6 +178,13 @@ export interface ReferenceOptions {
|
|
|
162
178
|
* Each pattern is a rule of its own, which {@link testFileRulesOf} counts.
|
|
163
179
|
*/
|
|
164
180
|
readonly testFiles: readonly string[];
|
|
181
|
+
/**
|
|
182
|
+
* The consumer whose files the pass walks instead of the target's. Its files are the
|
|
183
|
+
* program's own files below its root and not below the target root, each position is
|
|
184
|
+
* rendered against its root and the test patterns read the path below it, and only a
|
|
185
|
+
* reference to a declaration of `held`, the target's, is recorded.
|
|
186
|
+
*/
|
|
187
|
+
readonly consumer?: ConsumerSide;
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
/**
|
|
@@ -555,7 +578,9 @@ function compare(a: string, b: string): number {
|
|
|
555
578
|
*
|
|
556
579
|
* `held` is the inventory of the same project, whose declaration map is what a resolved
|
|
557
580
|
* symbol is looked up in; `targetRoot` is the absolute path every position is rendered
|
|
558
|
-
* against, which is the root the inventory was read against.
|
|
581
|
+
* against, which is the root the inventory was read against. A pass over a consumer
|
|
582
|
+
* walks the consumer's files instead and renders against its root, as
|
|
583
|
+
* {@link ReferenceOptions.consumer} states.
|
|
559
584
|
*
|
|
560
585
|
* The order is by the referencing position, then by the declaration named, the
|
|
561
586
|
* declaration referencing it and the use. That is a total order because one name
|
|
@@ -581,7 +606,18 @@ export function references<Brand>(
|
|
|
581
606
|
let residueFallbacks = 0;
|
|
582
607
|
let shorthandLookups = 0;
|
|
583
608
|
|
|
584
|
-
const
|
|
609
|
+
const consumer = options.consumer;
|
|
610
|
+
const root = consumer?.root ?? targetRoot;
|
|
611
|
+
const files =
|
|
612
|
+
consumer === undefined
|
|
613
|
+
? project.ownSourceFiles()
|
|
614
|
+
: project
|
|
615
|
+
.ownSourceFiles()
|
|
616
|
+
.filter(
|
|
617
|
+
(file) =>
|
|
618
|
+
relativePath(consumer.root, file.fileName) !== undefined &&
|
|
619
|
+
relativePath(targetRoot, file.fileName) === undefined,
|
|
620
|
+
);
|
|
585
621
|
const fileIds = new Set(
|
|
586
622
|
held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
|
|
587
623
|
);
|
|
@@ -615,7 +651,7 @@ export function references<Brand>(
|
|
|
615
651
|
};
|
|
616
652
|
|
|
617
653
|
for (const file of files) {
|
|
618
|
-
const path = renderPosition(file,
|
|
654
|
+
const path = renderPosition(file, root, 0).path;
|
|
619
655
|
const test = patterns.some((pattern) => pattern.test(path));
|
|
620
656
|
if (test) {
|
|
621
657
|
tests.push(path);
|
|
@@ -633,7 +669,7 @@ export function references<Brand>(
|
|
|
633
669
|
found.push({
|
|
634
670
|
from: declaration,
|
|
635
671
|
to: declaration,
|
|
636
|
-
position: renderPosition(file,
|
|
672
|
+
position: renderPosition(file, root, decorator.getStart()),
|
|
637
673
|
use: "decorator",
|
|
638
674
|
resolution: "syntax",
|
|
639
675
|
test,
|
|
@@ -671,7 +707,7 @@ export function references<Brand>(
|
|
|
671
707
|
if (symbol === undefined) {
|
|
672
708
|
continue;
|
|
673
709
|
}
|
|
674
|
-
const position = renderPosition(file,
|
|
710
|
+
const position = renderPosition(file, root, link.at.getStart());
|
|
675
711
|
for (const id of carriedBy(link, symbol)) {
|
|
676
712
|
found.push({
|
|
677
713
|
from: link.from,
|
|
@@ -689,7 +725,7 @@ export function references<Brand>(
|
|
|
689
725
|
if (module === undefined) {
|
|
690
726
|
continue;
|
|
691
727
|
}
|
|
692
|
-
const position = renderPosition(file,
|
|
728
|
+
const position = renderPosition(file, root, evaluation.node.getStart());
|
|
693
729
|
for (const id of chains.declarationsOf(module)) {
|
|
694
730
|
if (fileIds.has(id)) {
|
|
695
731
|
found.push({
|
|
@@ -711,7 +747,7 @@ export function references<Brand>(
|
|
|
711
747
|
if (symbol === undefined || read === undefined) {
|
|
712
748
|
return;
|
|
713
749
|
}
|
|
714
|
-
const position = renderPosition(file,
|
|
750
|
+
const position = renderPosition(file, root, read.key.getStart());
|
|
715
751
|
for (const target of chains.chainOf(symbol)) {
|
|
716
752
|
found.push({
|
|
717
753
|
from: read.from,
|
|
@@ -735,7 +771,7 @@ export function references<Brand>(
|
|
|
735
771
|
if (symbol === undefined) {
|
|
736
772
|
continue;
|
|
737
773
|
}
|
|
738
|
-
const position = renderPosition(file,
|
|
774
|
+
const position = renderPosition(file, root, site.node.getStart());
|
|
739
775
|
// A re-export something imports is used and so is the declaration behind it, so
|
|
740
776
|
// the use names every link of the chain its name stands at the head of.
|
|
741
777
|
for (const target of chains.chainOf(symbol)) {
|
|
@@ -761,7 +797,10 @@ export function references<Brand>(
|
|
|
761
797
|
|
|
762
798
|
return {
|
|
763
799
|
configFile: project.configFile,
|
|
764
|
-
references:
|
|
800
|
+
references:
|
|
801
|
+
consumer === undefined
|
|
802
|
+
? found
|
|
803
|
+
: found.map((reference) => ({ ...reference, consumer: consumer.id })),
|
|
765
804
|
testFilePaths: tests.sort(compare),
|
|
766
805
|
cost: {
|
|
767
806
|
batched,
|
package/src/report.ts
CHANGED
|
@@ -130,6 +130,13 @@ export interface ReportConfiguration {
|
|
|
130
130
|
readonly project: string;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** One consumer the run loaded, at the directory a report names it by. */
|
|
134
|
+
interface ReportConsumer {
|
|
135
|
+
readonly id: string;
|
|
136
|
+
/** The consumer's directory, as the report names its target's: below the run directory. */
|
|
137
|
+
readonly path: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
/** One consumer the scope declared and the run did not load, with why. */
|
|
134
141
|
export interface UnavailableConsumer {
|
|
135
142
|
readonly id: string;
|
|
@@ -156,7 +163,11 @@ export interface Report {
|
|
|
156
163
|
readonly configurations_not_built: readonly never[];
|
|
157
164
|
readonly consumers: {
|
|
158
165
|
readonly declared: number;
|
|
159
|
-
readonly loaded: readonly
|
|
166
|
+
readonly loaded: readonly {
|
|
167
|
+
readonly id: string;
|
|
168
|
+
readonly role: "consumer";
|
|
169
|
+
readonly path: string;
|
|
170
|
+
}[];
|
|
160
171
|
readonly unavailable: readonly {
|
|
161
172
|
readonly id: string;
|
|
162
173
|
readonly role: "consumer";
|
|
@@ -180,6 +191,7 @@ export interface ReportInput {
|
|
|
180
191
|
readonly declaredGaps: readonly DeclaredGap[];
|
|
181
192
|
readonly target: { readonly kind: TargetKind; readonly root: string; readonly identity: string };
|
|
182
193
|
readonly configurations: readonly ReportConfiguration[];
|
|
194
|
+
readonly loaded: readonly ReportConsumer[];
|
|
183
195
|
readonly unavailable: readonly UnavailableConsumer[];
|
|
184
196
|
readonly result: PassResult;
|
|
185
197
|
readonly testFileRules: readonly TestFileRule[];
|
|
@@ -387,6 +399,9 @@ export function buildReport(input: ReportInput): Report {
|
|
|
387
399
|
const unavailable = [...new Map(input.unavailable.map((one) => [one.id, one])).values()]
|
|
388
400
|
.sort((a, b) => compare(a.id, b.id))
|
|
389
401
|
.map((one) => ({ id: one.id, role: "consumer" as const, reason: one.reason }));
|
|
402
|
+
const loaded = [...input.loaded]
|
|
403
|
+
.sort((a, b) => compare(a.id, b.id) || compare(a.path, b.path))
|
|
404
|
+
.map((one) => ({ id: one.id, role: "consumer" as const, path: one.path }));
|
|
390
405
|
return {
|
|
391
406
|
schema_version: SCHEMA_VERSION,
|
|
392
407
|
contract_version: input.contractVersion,
|
|
@@ -406,7 +421,7 @@ export function buildReport(input: ReportInput): Report {
|
|
|
406
421
|
.sort((a, b) => compare(a.id, b.id))
|
|
407
422
|
.map((one) => ({ id: one.id, project: one.project })),
|
|
408
423
|
configurations_not_built: [],
|
|
409
|
-
consumers: { declared: unavailable.length, loaded
|
|
424
|
+
consumers: { declared: loaded.length + unavailable.length, loaded, unavailable },
|
|
410
425
|
findings,
|
|
411
426
|
edge_evaluations: input.result.edgeEvaluations.map(wireEvaluation).sort(byEdge),
|
|
412
427
|
stale_suppressions: input.result.staleSuppressions.map(wireStale).sort(canonicalStale),
|
package/src/roots.ts
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
|
|
6
6
|
* that names a caller is live under reference counting as well, which the sweep
|
|
7
|
-
* decides;
|
|
8
|
-
*
|
|
9
|
-
* candidate under reference counting.
|
|
7
|
+
* decides; the published API only supposes a consumer, so a published export is live
|
|
8
|
+
* under reachability and still a candidate under reference counting.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
@@ -23,9 +22,9 @@ import { sourceFilesOf } from "./source-files.ts";
|
|
|
23
22
|
export type RootKind =
|
|
24
23
|
/** A file `ts.entry_files` names, and what it exports. */
|
|
25
24
|
| "entry-file"
|
|
26
|
-
/** A file the manifest names as an importable entry point, and what it exports. */
|
|
25
|
+
/** A file the manifest names as an importable entry point, and what it exports outside the published API. */
|
|
27
26
|
| "manifest-entry"
|
|
28
|
-
/** A file the manifest names as a command, and what it exports. */
|
|
27
|
+
/** A file the manifest names as a command, and what it exports outside the published API. */
|
|
29
28
|
| "manifest-binary"
|
|
30
29
|
/** A declaration a consumer of a library target can name. */
|
|
31
30
|
| "published-api"
|
|
@@ -168,6 +167,7 @@ function childrenOf(held: Inventory): ReadonlyMap<string, readonly InventorySymb
|
|
|
168
167
|
* is a root with everything its module exports: each name of the module's export
|
|
169
168
|
* table, and where a name is a re-export, every link of the chain to the declaration
|
|
170
169
|
* it carries, so a re-exported declaration is a root of the file that publishes it.
|
|
170
|
+
* In a library, what a published file exports is rooted by the published API alone.
|
|
171
171
|
* A file a runtime executes without reading its exports, a test file or a worker, is
|
|
172
172
|
* a root by itself.
|
|
173
173
|
*/
|
|
@@ -198,14 +198,19 @@ export function roots<Brand>(
|
|
|
198
198
|
|
|
199
199
|
const published: SourceFile[] = [];
|
|
200
200
|
const publishes = (entry: ManifestEntry): boolean =>
|
|
201
|
+
options.publishedAPI &&
|
|
201
202
|
entry.published &&
|
|
202
203
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
|
|
204
|
+
for (const entry of options.manifest.entries) {
|
|
205
|
+
if (publishes(entry)) {
|
|
206
|
+
published.push(...files.named(entry.path));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const publishedNames = new Set(published.map((file) => file.fileName));
|
|
203
210
|
for (const entry of options.manifest.entries) {
|
|
204
211
|
for (const file of files.named(entry.path)) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
published.push(file);
|
|
208
|
-
}
|
|
212
|
+
const kind = entry.role === "run" ? "manifest-binary" : "manifest-entry";
|
|
213
|
+
enter(file, kind, entry.member, !publishedNames.has(file.fileName));
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
for (const pattern of options.entryFiles) {
|
package/src/session.ts
CHANGED
|
@@ -193,6 +193,25 @@ function viewOf<Brand>(project: Project): ProjectView<Brand> {
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The same project, its own source files narrowed to the ones `keep` admits. A consumer's
|
|
198
|
+
* program holds the target's files it imports beside its own, and a pass that enumerates
|
|
199
|
+
* the target's declarations in that program reads those files alone.
|
|
200
|
+
*/
|
|
201
|
+
export function narrowedTo<Brand>(
|
|
202
|
+
project: ProjectView<Brand>,
|
|
203
|
+
keep: (file: SourceFile) => boolean,
|
|
204
|
+
): ProjectView<Brand> {
|
|
205
|
+
let kept: readonly SourceFile[] | undefined;
|
|
206
|
+
return {
|
|
207
|
+
...project,
|
|
208
|
+
ownSourceFiles: () => {
|
|
209
|
+
kept ??= project.ownSourceFiles().filter(keep);
|
|
210
|
+
return kept;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
196
215
|
/** Every diagnostic set a project is refused for carrying an error in. */
|
|
197
216
|
export interface ProjectDiagnostics {
|
|
198
217
|
readonly configFile: string;
|
package/src/sweep.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
10
|
-
import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
|
|
10
|
+
import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
|
|
11
11
|
import type { Position } from "./position.ts";
|
|
12
12
|
|
|
13
13
|
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
@@ -156,6 +156,14 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
|
156
156
|
return { liveUnder: live, candidates };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The references a loaded consumer makes that the mode counts. A production mode counts
|
|
161
|
+
* none a consumer's test file made, as it counts none of the target's.
|
|
162
|
+
*/
|
|
163
|
+
function callsOf(graph: Graph, mode: Mode): readonly Edge[] {
|
|
164
|
+
return graph.consumed.filter((edge) => !mode.production || !edge.test);
|
|
165
|
+
}
|
|
166
|
+
|
|
159
167
|
/** Per declaration of the graph, whether one of the identifiers names it. */
|
|
160
168
|
function flagged(graph: Graph, ids: readonly string[]): boolean[] {
|
|
161
169
|
const flags = graph.symbols.map(() => false);
|
|
@@ -218,6 +226,10 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
|
|
|
218
226
|
for (const root of graph.rooted) {
|
|
219
227
|
enter(root.at, false);
|
|
220
228
|
}
|
|
229
|
+
// A consumer's reference reaches what it names as one of the target's own does.
|
|
230
|
+
for (const edge of callsOf(graph, mode)) {
|
|
231
|
+
enter(edge.to, !edge.evaluation);
|
|
232
|
+
}
|
|
221
233
|
walk(false);
|
|
222
234
|
return reached;
|
|
223
235
|
}
|
package/src/verbs/analyze.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { runAnalysis, type RunProject } from "../analysis.ts";
|
|
2
2
|
import { ConfigError, type Format } from "../config.ts";
|
|
3
3
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
|
|
4
|
+
import { moduleIdentity } from "../consumers.ts";
|
|
4
5
|
import { analyzerProvenance, recordedFindings, verdictOf } from "../findings-pass.ts";
|
|
5
|
-
import { packageScope } from "../inventory.ts";
|
|
6
6
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
|
|
7
7
|
import type { Host } from "../host.ts";
|
|
8
8
|
import {
|
|
@@ -16,7 +16,6 @@ import { json } from "../reporters/json.ts";
|
|
|
16
16
|
import { RENDERINGS } from "../reporters/reporters.ts";
|
|
17
17
|
import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
|
|
18
18
|
import { resolve } from "../resolve.ts";
|
|
19
|
-
import type { Module } from "../scope.ts";
|
|
20
19
|
import { writeBaseline, type Recorded } from "../suppress-file.ts";
|
|
21
20
|
import { CONTRACT_VERSION } from "../version.ts";
|
|
22
21
|
import { EXIT_CLEAN, EXIT_FAILURE, type Verb, type VerbOptions } from "./verb.ts";
|
|
@@ -31,9 +30,6 @@ export const ANALYZE_OPTIONS: VerbOptions = {
|
|
|
31
30
|
const EXIT_CODE_ON = "on";
|
|
32
31
|
const EXIT_CODE_OFF = "off";
|
|
33
32
|
|
|
34
|
-
/** Why a declared consumer is not loaded, which every report names it with. */
|
|
35
|
-
const NOT_LOADED = "this analyzer loads no consumer beside the target";
|
|
36
|
-
|
|
37
33
|
/** A path of the run no report can name, which ends the run with the failure code. */
|
|
38
34
|
class ReportPathError extends Error {
|
|
39
35
|
constructor(message: string) {
|
|
@@ -101,11 +97,6 @@ function configurationsOf(
|
|
|
101
97
|
});
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
/** The name one module publishes itself under: the scope's, else its manifest's. */
|
|
105
|
-
function identityOf(module: Module, host: Host): string {
|
|
106
|
-
return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
100
|
/** A count with its noun, so a message reads for one record as well as for several. */
|
|
110
101
|
function counted(n: number, noun: string): string {
|
|
111
102
|
return `${String(n)} ${noun}${n === 1 ? "" : "s"}`;
|
|
@@ -221,12 +212,13 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
221
212
|
version,
|
|
222
213
|
conformance: CONFORMANCE,
|
|
223
214
|
declaredGaps: DECLARED_GAPS,
|
|
224
|
-
target: { kind, root, identity:
|
|
215
|
+
target: { kind, root, identity: moduleIdentity(host, scoped.target) },
|
|
225
216
|
configurations: configurationsOf(run.projects, targetRoot),
|
|
226
|
-
|
|
227
|
-
id:
|
|
228
|
-
|
|
217
|
+
loaded: run.consumers.map((consumer) => ({
|
|
218
|
+
id: consumer.id,
|
|
219
|
+
path: runRelative(invoked, consumer.path),
|
|
229
220
|
})),
|
|
221
|
+
unavailable: [],
|
|
230
222
|
result,
|
|
231
223
|
testFileRules: run.testFileRules,
|
|
232
224
|
});
|
package/src/verbs/explain.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { ConfigError } from "../config.ts";
|
|
|
5
5
|
import type { CompletedFinding } from "../finding.ts";
|
|
6
6
|
import { OUTSIDE, swept as judged } from "../graph.ts";
|
|
7
7
|
import type { InventorySymbol } from "../inventory.ts";
|
|
8
|
-
import type
|
|
8
|
+
import { referenceKey, type Configured } from "../matrix.ts";
|
|
9
9
|
import { positionKey } from "../position.ts";
|
|
10
10
|
import type { Reference } from "../references.ts";
|
|
11
11
|
import { resolve } from "../resolve.ts";
|
|
@@ -108,9 +108,7 @@ function referencesOf(configured: readonly Configured[]): readonly Reference[] {
|
|
|
108
108
|
const seen = new Set<string>();
|
|
109
109
|
const held: Reference[] = [];
|
|
110
110
|
for (const reference of configured.flatMap((one) => one.references)) {
|
|
111
|
-
const key =
|
|
112
|
-
"\u0000",
|
|
113
|
-
);
|
|
111
|
+
const key = referenceKey(reference);
|
|
114
112
|
if (!seen.has(key)) {
|
|
115
113
|
seen.add(key);
|
|
116
114
|
held.push(reference);
|
|
@@ -142,9 +140,15 @@ type Hop =
|
|
|
142
140
|
| { readonly k: "reference"; readonly reference: Reference }
|
|
143
141
|
| { readonly k: "export"; readonly reference: Reference; readonly to: string };
|
|
144
142
|
|
|
143
|
+
/** Where a path starts: a root of the run, or a reference a loaded consumer makes. */
|
|
144
|
+
type Origin =
|
|
145
|
+
| { readonly k: "root"; readonly root: Root }
|
|
146
|
+
| { readonly k: "consumer"; readonly consumer: string };
|
|
147
|
+
|
|
145
148
|
/**
|
|
146
|
-
* The shortest path from a root to one declaration, walked breadth first
|
|
147
|
-
* once in the order the run names them,
|
|
149
|
+
* The shortest path from a root or a loaded consumer to one declaration, walked breadth first
|
|
150
|
+
* from every root at once in the order the run names them, a consumer's reference being the
|
|
151
|
+
* first hop of a path of its own, and following the references the sweep follows: a
|
|
148
152
|
* reference that reads a module reaches what the module exports, and an evaluation or a root
|
|
149
153
|
* reaches the module alone. Under `production` no reference a test file made is followed,
|
|
150
154
|
* which is the set the report's sweep counts.
|
|
@@ -153,13 +157,19 @@ function shortestPath(
|
|
|
153
157
|
analysis: RunAnalysis,
|
|
154
158
|
to: string,
|
|
155
159
|
production: boolean,
|
|
156
|
-
): { readonly
|
|
160
|
+
): { readonly origin: Origin; readonly path: readonly Hop[] } | undefined {
|
|
157
161
|
const graph = analysis.swept.matrix.union;
|
|
158
162
|
const out = new Map<string, Reference[]>();
|
|
163
|
+
const consumed: (Reference & { readonly consumer: string })[] = [];
|
|
159
164
|
for (const reference of referencesOf(analysis.configured)) {
|
|
160
165
|
if (production && reference.test) {
|
|
161
166
|
continue;
|
|
162
167
|
}
|
|
168
|
+
const { consumer } = reference;
|
|
169
|
+
if (consumer !== undefined) {
|
|
170
|
+
consumed.push({ ...reference, consumer });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
163
173
|
const held = out.get(reference.from);
|
|
164
174
|
if (held === undefined) {
|
|
165
175
|
out.set(reference.from, [reference]);
|
|
@@ -171,7 +181,7 @@ function shortestPath(
|
|
|
171
181
|
readonly at: string;
|
|
172
182
|
readonly hop: Hop | undefined;
|
|
173
183
|
readonly previous: number;
|
|
174
|
-
readonly
|
|
184
|
+
readonly origin: Origin;
|
|
175
185
|
}
|
|
176
186
|
const steps: Step[] = [];
|
|
177
187
|
const seen = new Set<string>();
|
|
@@ -182,10 +192,32 @@ function shortestPath(
|
|
|
182
192
|
steps.push(step);
|
|
183
193
|
}
|
|
184
194
|
};
|
|
195
|
+
const follow = (reference: Reference, previous: number, origin: Origin): void => {
|
|
196
|
+
enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
|
|
197
|
+
const target = graph.at(reference.to);
|
|
198
|
+
if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
expanded.add(reference.to);
|
|
202
|
+
for (const exported of graph.exportsOf[target] ?? []) {
|
|
203
|
+
const id = graph.symbols[exported]?.id;
|
|
204
|
+
if (id !== undefined) {
|
|
205
|
+
enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
185
209
|
for (const root of rootsOf(analysis.configured)) {
|
|
186
|
-
enter({ at: root.id, hop: undefined, previous: -1, root });
|
|
210
|
+
enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
|
|
187
211
|
}
|
|
188
|
-
|
|
212
|
+
// A consumer's reference is one hop long, so it joins the walk where the roots' first
|
|
213
|
+
// hops do.
|
|
214
|
+
const firstHops = steps.length;
|
|
215
|
+
for (let head = 0; ; head += 1) {
|
|
216
|
+
if (head === firstHops) {
|
|
217
|
+
for (const reference of consumed) {
|
|
218
|
+
follow(reference, -1, { k: "consumer", consumer: reference.consumer });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
189
221
|
const step = steps[head];
|
|
190
222
|
if (step === undefined) {
|
|
191
223
|
break;
|
|
@@ -195,36 +227,20 @@ function shortestPath(
|
|
|
195
227
|
for (let at: Step | undefined = step; at?.hop !== undefined; at = steps[at.previous]) {
|
|
196
228
|
path.unshift(at.hop);
|
|
197
229
|
}
|
|
198
|
-
return {
|
|
230
|
+
return { origin: step.origin, path };
|
|
199
231
|
}
|
|
200
232
|
for (const reference of out.get(step.at) ?? []) {
|
|
201
|
-
|
|
202
|
-
at: reference.to,
|
|
203
|
-
hop: { k: "reference", reference },
|
|
204
|
-
previous: head,
|
|
205
|
-
root: step.root,
|
|
206
|
-
});
|
|
207
|
-
const target = graph.at(reference.to);
|
|
208
|
-
if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
expanded.add(reference.to);
|
|
212
|
-
for (const exported of graph.exportsOf[target] ?? []) {
|
|
213
|
-
const id = graph.symbols[exported]?.id;
|
|
214
|
-
if (id !== undefined) {
|
|
215
|
-
enter({
|
|
216
|
-
at: id,
|
|
217
|
-
hop: { k: "export", reference, to: id },
|
|
218
|
-
previous: head,
|
|
219
|
-
root: step.root,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
233
|
+
follow(reference, head, step.origin);
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
return undefined;
|
|
226
237
|
}
|
|
227
238
|
|
|
239
|
+
/** What made a reference: a declaration of the target, or a loaded consumer. */
|
|
240
|
+
function madeBy(reference: Reference): string {
|
|
241
|
+
return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
228
244
|
function referenceLine(reference: Reference, tail: string): string {
|
|
229
245
|
return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${reference.test ? "\tfrom a test file" : ""}\n`;
|
|
230
246
|
}
|
|
@@ -244,19 +260,20 @@ function liveness(analysis: RunAnalysis, subject: InventorySymbol, cascade: Casc
|
|
|
244
260
|
const found =
|
|
245
261
|
shortestPath(analysis, subject.id, true) ?? shortestPath(analysis, subject.id, false);
|
|
246
262
|
if (found !== undefined && found.path.length > 0) {
|
|
247
|
-
|
|
263
|
+
const { origin } = found;
|
|
264
|
+
text += ` reached from: ${origin.k === "root" ? `root ${origin.root.kind} ${origin.root.id}` : `consumer ${origin.consumer}`}\n`;
|
|
248
265
|
for (const hop of found.path) {
|
|
249
|
-
text += referenceLine(hop.reference, `${hop.reference
|
|
266
|
+
text += referenceLine(hop.reference, `${madeBy(hop.reference)} -> ${hop.reference.to}`);
|
|
250
267
|
if (hop.k === "export") {
|
|
251
268
|
text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
|
|
252
269
|
}
|
|
253
270
|
}
|
|
254
271
|
} else if (found === undefined) {
|
|
255
272
|
const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
|
|
256
|
-
text += " unreachable: no path of references reaches it from a root\n";
|
|
273
|
+
text += " unreachable: no path of references reaches it from a root or a loaded consumer\n";
|
|
257
274
|
text += ` references: ${String(references.length)}\n`;
|
|
258
275
|
for (const reference of references) {
|
|
259
|
-
text += referenceLine(reference, `from ${reference
|
|
276
|
+
text += referenceLine(reference, `from ${madeBy(reference)}`);
|
|
260
277
|
}
|
|
261
278
|
}
|
|
262
279
|
if (candidate !== undefined) {
|
|
@@ -337,9 +354,9 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
|
|
|
337
354
|
* Explains one symbol from the analysis the report is built from, so an explanation cannot
|
|
338
355
|
* disagree with a finding: why a reported symbol is reported, which exemption classes held a
|
|
339
356
|
* retained one back, and for any other declaration the relations that hold it live and the
|
|
340
|
-
* shortest path of references from a root, or the references it has where
|
|
341
|
-
* A request naming no one declaration exits with the usage code and names
|
|
342
|
-
* matches it.
|
|
357
|
+
* shortest path of references from a root or a loaded consumer, or the references it has where
|
|
358
|
+
* no path reaches it. A request naming no one declaration exits with the usage code and names
|
|
359
|
+
* what partially matches it.
|
|
343
360
|
*/
|
|
344
361
|
export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
345
362
|
const named = namedSymbol(option);
|