@cplieger/deadset-ts 3.11.0 → 3.12.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/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 +8 -5
- package/dist/src/matrix.js +14 -3
- package/dist/src/references.js +21 -9
- package/dist/src/report.js +4 -1
- 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 +14 -5
- package/src/matrix.ts +14 -5
- package/src/references.ts +48 -9
- package/src/report.ts +17 -2
- 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/consumers.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consumers a scope declares, loaded beside the target: each a directory on the
|
|
3
|
+
* local filesystem whose compiler configurations open as projects of the run's one
|
|
4
|
+
* snapshot, and whose references to the target's declarations count against them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Config } from "./config.ts";
|
|
8
|
+
import { consumerProjects } from "./discover.ts";
|
|
9
|
+
import type { Host } from "./host.ts";
|
|
10
|
+
import { inventory, packageScope } from "./inventory.ts";
|
|
11
|
+
import { relativePath } from "./paths.ts";
|
|
12
|
+
import { references, type ConsumerSide, type Reference } from "./references.ts";
|
|
13
|
+
import type { Module, Scope } from "./scope.ts";
|
|
14
|
+
import { narrowedTo, type Engine, type ProjectView } from "./session.ts";
|
|
15
|
+
|
|
16
|
+
/** One consumer the run loaded. */
|
|
17
|
+
export interface LoadedConsumer {
|
|
18
|
+
/** The name the consumer publishes itself under, which every finding names it by. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
/** The consumer's directory, absolute. */
|
|
21
|
+
readonly path: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One declared consumer and the compiler configurations it opens as. */
|
|
25
|
+
interface ConsumerLoad {
|
|
26
|
+
readonly consumer: LoadedConsumer;
|
|
27
|
+
/** The configuration files, absolute, in discovery order, none a target project's. */
|
|
28
|
+
readonly configFiles: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The name one module publishes itself under: the scope's, else its manifest's. */
|
|
32
|
+
export function moduleIdentity(host: Host, module: Module): string {
|
|
33
|
+
return module.id === "" ? packageScope(host, module.path)("index.ts").package : module.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Every consumer the scope declares, in its order, with the projects it opens as. A
|
|
38
|
+
* configuration that is also one of the target's projects, or an earlier consumer's, is
|
|
39
|
+
* that project's alone. A consumer whose directory is absent, is no directory or holds
|
|
40
|
+
* no configuration ends the run naming it, before any project opens.
|
|
41
|
+
*/
|
|
42
|
+
export function consumerLoads(
|
|
43
|
+
engine: Engine,
|
|
44
|
+
host: Host,
|
|
45
|
+
scope: Scope,
|
|
46
|
+
targetConfigFiles: readonly string[],
|
|
47
|
+
): readonly ConsumerLoad[] {
|
|
48
|
+
const opened = new Set(targetConfigFiles);
|
|
49
|
+
return scope.consumers.map((module) => {
|
|
50
|
+
const configFiles = consumerProjects(engine, host, module.path).filter((configFile) => {
|
|
51
|
+
const fresh = !opened.has(configFile);
|
|
52
|
+
opened.add(configFile);
|
|
53
|
+
return fresh;
|
|
54
|
+
});
|
|
55
|
+
return { consumer: { id: moduleIdentity(host, module), path: module.path }, configFiles };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Every reference one consumer project's own files make to a declaration of the target.
|
|
61
|
+
* The target's declarations are enumerated over the target's files the consumer's
|
|
62
|
+
* program holds, against the target root, so each carries the identifier the target's
|
|
63
|
+
* own projects give it. A reference from a consumer's test file is a test reference
|
|
64
|
+
* unless the configuration counts it as a production one.
|
|
65
|
+
*/
|
|
66
|
+
export function consumerReferences<Brand>(
|
|
67
|
+
project: ProjectView<Brand>,
|
|
68
|
+
host: Host,
|
|
69
|
+
targetRoot: string,
|
|
70
|
+
consumer: ConsumerSide,
|
|
71
|
+
config: Config,
|
|
72
|
+
): readonly Reference[] {
|
|
73
|
+
const held = inventory(
|
|
74
|
+
narrowedTo(project, (file) => relativePath(targetRoot, file.fileName) !== undefined),
|
|
75
|
+
host,
|
|
76
|
+
targetRoot,
|
|
77
|
+
);
|
|
78
|
+
const found = references(project, held, targetRoot, {
|
|
79
|
+
testFiles: config.ts.testFiles,
|
|
80
|
+
consumer,
|
|
81
|
+
}).references;
|
|
82
|
+
if (config.analysis.consumerTests !== "production") {
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
return found.map((reference) => (reference.test ? { ...reference, test: false } : reference));
|
|
86
|
+
}
|
package/src/discover.ts
CHANGED
|
@@ -160,17 +160,70 @@ function referencesOf(host: Host, configFile: string): string[] {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
* The configuration
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* The configuration file the scope names for the target directly, where its path
|
|
164
|
+
* names one rather than a directory. A target naming a directory is reached by the
|
|
165
|
+
* walk over that directory instead. A consumer's projects are a consumer's, never the
|
|
166
|
+
* target's, so no consumer path is read here.
|
|
166
167
|
*/
|
|
167
168
|
function scopeConfigFiles(host: Host, scope: Scope): string[] {
|
|
168
|
-
return [scope.target
|
|
169
|
-
.map((module) => module.path)
|
|
169
|
+
return [scope.target.path]
|
|
170
170
|
.filter((path) => path.endsWith(".json"))
|
|
171
171
|
.map((path) => resolvePath(host.workingDirectory(), path));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Every configuration file reached from `pending`, in the order discovery reads them:
|
|
176
|
+
* each once, each followed by the configurations it references, transitively. A
|
|
177
|
+
* configuration the compiler cannot read ends the run naming it.
|
|
178
|
+
*/
|
|
179
|
+
function derivedFrom(engine: Engine, host: Host, pending: string[]): string[] {
|
|
180
|
+
const seen = new Set<string>();
|
|
181
|
+
const found: string[] = [];
|
|
182
|
+
while (pending.length > 0) {
|
|
183
|
+
const configFile = pending.shift();
|
|
184
|
+
if (configFile === undefined) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
const path = resolvePath(host.workingDirectory(), configFile);
|
|
188
|
+
if (seen.has(path)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
seen.add(path);
|
|
192
|
+
parseOrRefuse(engine, path, path);
|
|
193
|
+
found.push(path);
|
|
194
|
+
pending.push(...referencesOf(host, path));
|
|
195
|
+
}
|
|
196
|
+
return found;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The compiler configurations one consumer is loaded through: every one under its
|
|
201
|
+
* root that is not inside an ignored directory, and the configurations each of those
|
|
202
|
+
* references, transitively, in that order. A root that is absent, that is no
|
|
203
|
+
* directory, or that holds no configuration ends the run naming it, because a
|
|
204
|
+
* finding computed without a declared consumer would claim a reference set the run
|
|
205
|
+
* never read.
|
|
206
|
+
*/
|
|
207
|
+
export function consumerProjects(engine: Engine, host: Host, root: string): readonly string[] {
|
|
208
|
+
const kind = host.kindOf(root);
|
|
209
|
+
if (kind !== "directory") {
|
|
210
|
+
throw new DiscoveryError(
|
|
211
|
+
kind === "absent"
|
|
212
|
+
? `the consumer ${root} does not exist`
|
|
213
|
+
: `the consumer ${root} is not a directory`,
|
|
214
|
+
[],
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const found = derivedFrom(engine, host, configFilesUnder(host, root));
|
|
218
|
+
if (found.length === 0) {
|
|
219
|
+
throw new DiscoveryError(
|
|
220
|
+
`the consumer ${root} holds no compiler configuration, so none of its references can be read`,
|
|
221
|
+
[],
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return found;
|
|
225
|
+
}
|
|
226
|
+
|
|
174
227
|
/**
|
|
175
228
|
* The name a derived project carries: its configuration file's path below the target
|
|
176
229
|
* root, or the absolute path of a file outside it.
|
|
@@ -257,24 +310,15 @@ function declaredProjects(
|
|
|
257
310
|
*/
|
|
258
311
|
function derivedProjects(engine: Engine, host: Host, scope: Scope): Discovery {
|
|
259
312
|
const fromScope = scopeConfigFiles(host, scope);
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (seen.has(path)) {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
seen.add(path);
|
|
273
|
-
parseOrRefuse(engine, path, path);
|
|
274
|
-
projects.push({ id: derivedId(scope.target.path, path), configFile: path });
|
|
275
|
-
pending.push(...referencesOf(host, path));
|
|
276
|
-
}
|
|
277
|
-
return { projects, configFiles: projects.map((project) => project.configFile), fromScope };
|
|
313
|
+
const configFiles = derivedFrom(engine, host, [
|
|
314
|
+
...fromScope,
|
|
315
|
+
...configFilesUnder(host, scope.target.path),
|
|
316
|
+
]);
|
|
317
|
+
const projects = configFiles.map((configFile) => ({
|
|
318
|
+
id: derivedId(scope.target.path, configFile),
|
|
319
|
+
configFile,
|
|
320
|
+
}));
|
|
321
|
+
return { projects, configFiles, fromScope };
|
|
278
322
|
}
|
|
279
323
|
|
|
280
324
|
/**
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import type { RunSweep } from "../analysis.ts";
|
|
8
8
|
import { componentId, type Component } from "../components.ts";
|
|
9
|
-
import type { Confidence,
|
|
9
|
+
import type { Confidence, Severity } from "../config.ts";
|
|
10
10
|
import type { CompletedFinding, Finding, FindingComponent } from "../finding.ts";
|
|
11
11
|
import { OUTSIDE } from "../graph.ts";
|
|
12
|
-
import { KINDS } from "../kinds.ts";
|
|
12
|
+
import { KINDS, type KindRow } from "../kinds.ts";
|
|
13
13
|
import { FAMILY_KEY_LENGTH } from "../resolve.ts";
|
|
14
14
|
import { isRowSubject, type Dials } from "../suppress.ts";
|
|
15
|
+
import type { Boundary } from "./boundary.ts";
|
|
15
16
|
import type { EmitterInput } from "./emitter.ts";
|
|
16
17
|
|
|
17
18
|
/** The classes ranked by the claim each makes, the strongest highest. */
|
|
@@ -22,31 +23,64 @@ function capped(found: Confidence, ceiling: Confidence): Confidence {
|
|
|
22
23
|
return RANK[found] <= RANK[ceiling] ? found : ceiling;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** The kind whose default turns on what the run knows about the target's consumers. */
|
|
27
|
+
const UNUSED_EXPORTED = "DS1001";
|
|
28
|
+
|
|
29
|
+
/** Whether the run loaded every consumer the scope declared, which holds of a scope declaring none. */
|
|
30
|
+
function everyDeclaredLoaded(boundary: Boundary): boolean {
|
|
31
|
+
const { declared, loaded } = boundary.consumers;
|
|
32
|
+
return declared.every((id) => loaded.includes(id));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a library's published API may have a caller the run cannot see: the target is
|
|
37
|
+
* a library, and the run either holds no consumer information or did not load a consumer
|
|
38
|
+
* the scope declared. A configuration declaring the consumer set complete holds consumer
|
|
39
|
+
* information whether or not it declares a consumer, because it says no other exists.
|
|
40
|
+
*/
|
|
41
|
+
function openWorld(input: EmitterInput): boolean {
|
|
42
|
+
const known = input.config.consumersComplete || input.boundary.consumers.declared.length > 0;
|
|
43
|
+
return input.config.targetKind === "library" && !(known && everyDeclaredLoaded(input.boundary));
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
/**
|
|
26
47
|
* The severity the configuration gives one code: the key naming the code, else the key
|
|
27
|
-
* naming its family, else the code's default.
|
|
48
|
+
* naming its family, else the code's default. The unused-exported kind defaults to
|
|
49
|
+
* `allow` while a library's published API is open to callers the run cannot see.
|
|
28
50
|
*/
|
|
29
|
-
function severityOf(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
51
|
+
function severityOf(input: EmitterInput, code: string, fallback: Severity): Severity {
|
|
52
|
+
const { config } = input;
|
|
53
|
+
const named = config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH));
|
|
54
|
+
if (named !== undefined) {
|
|
55
|
+
return named;
|
|
56
|
+
}
|
|
57
|
+
return code === UNUSED_EXPORTED && openWorld(input) ? "allow" : fallback;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
/**
|
|
36
61
|
* The reachability class of one declaration of the run. A declaration of a library's
|
|
37
|
-
* published API has callers outside the target
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
62
|
+
* published API has callers outside the target: it is `certain` where the run loaded
|
|
63
|
+
* every consumer the scope declared and declared at least one, `probable` where it
|
|
64
|
+
* declared consumers and did not load them all, and `possible` where it declared none.
|
|
65
|
+
* Whether the configuration declares the consumer set complete decides nothing here.
|
|
66
|
+
* Every other declaration has every reference in the loaded program, a private member
|
|
67
|
+
* and a `#private` name among them, and is `certain`.
|
|
41
68
|
*/
|
|
42
|
-
function reachabilityClasses(
|
|
43
|
-
const union = swept.matrix.union;
|
|
69
|
+
function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
|
|
70
|
+
const union = input.swept.matrix.union;
|
|
44
71
|
const published = new Set(
|
|
45
72
|
union.rooted
|
|
46
73
|
.filter((root) => root.kind === "published-api")
|
|
47
74
|
.map((root) => union.symbols[root.at]?.id),
|
|
48
75
|
);
|
|
49
|
-
|
|
76
|
+
const { declared } = input.boundary.consumers;
|
|
77
|
+
const outside: Confidence =
|
|
78
|
+
declared.length === 0
|
|
79
|
+
? "possible"
|
|
80
|
+
: everyDeclaredLoaded(input.boundary)
|
|
81
|
+
? "certain"
|
|
82
|
+
: "probable";
|
|
83
|
+
return (id) => (published.has(id) ? outside : "certain");
|
|
50
84
|
}
|
|
51
85
|
|
|
52
86
|
/** The component each dead declaration of the run falls in, and none for a live one. */
|
|
@@ -76,22 +110,24 @@ function findingComponents(swept: RunSweep): (id: string) => FindingComponent |
|
|
|
76
110
|
* relation and holds where it is declared; a file or a manifest row is `certain` and
|
|
77
111
|
* holds in every configuration. A subject in no dead component gets one of its own,
|
|
78
112
|
* numbered past the computed ones in finding order, so no two families mint one; nothing
|
|
79
|
-
* falls with it, so its deletion removes no line.
|
|
113
|
+
* falls with it, so its deletion removes no line. `kinds` is the vocabulary each code's
|
|
114
|
+
* row is read from, the shipped one unless a caller supplies its own.
|
|
80
115
|
*/
|
|
81
116
|
export function completed(
|
|
82
117
|
input: EmitterInput,
|
|
83
118
|
findings: readonly Finding[],
|
|
119
|
+
kinds: ReadonlyMap<string, KindRow> = KINDS,
|
|
84
120
|
): readonly CompletedFinding[] {
|
|
85
121
|
const { swept } = input;
|
|
86
122
|
const union = swept.matrix.union;
|
|
87
123
|
const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
|
|
88
124
|
const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
|
|
89
|
-
const classOf = reachabilityClasses(
|
|
125
|
+
const classOf = reachabilityClasses(input);
|
|
90
126
|
const componentOf = findingComponents(swept);
|
|
91
127
|
let minted = swept.sweep.components.length;
|
|
92
128
|
|
|
93
129
|
return findings.map((finding): CompletedFinding => {
|
|
94
|
-
const row =
|
|
130
|
+
const row = kinds.get(finding.code);
|
|
95
131
|
if (row === undefined) {
|
|
96
132
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
97
133
|
}
|
|
@@ -140,7 +176,7 @@ export function completed(
|
|
|
140
176
|
(judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
|
|
141
177
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
142
178
|
fixability: row.fixability,
|
|
143
|
-
severity: severityOf(input
|
|
179
|
+
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
144
180
|
message: finding.message,
|
|
145
181
|
details: finding.details ?? {},
|
|
146
182
|
};
|
|
@@ -168,7 +204,7 @@ export function dialsOf(
|
|
|
168
204
|
findings: readonly CompletedFinding[],
|
|
169
205
|
): Dials<CompletedFinding> {
|
|
170
206
|
const minConfidence = input.config.analysis.minConfidence;
|
|
171
|
-
const classOf = reachabilityClasses(input
|
|
207
|
+
const classOf = reachabilityClasses(input);
|
|
172
208
|
const components = new Set(
|
|
173
209
|
findings
|
|
174
210
|
.filter((finding) => finding.component.root && dialed(finding, minConfidence))
|
|
@@ -181,7 +217,7 @@ export function dialsOf(
|
|
|
181
217
|
row !== undefined &&
|
|
182
218
|
dialed(
|
|
183
219
|
{
|
|
184
|
-
severity: severityOf(input
|
|
220
|
+
severity: severityOf(input, code, row.defaultSeverity),
|
|
185
221
|
confidence: capped(classOf(id), row.maxClass),
|
|
186
222
|
},
|
|
187
223
|
minConfidence,
|
|
@@ -63,11 +63,18 @@ function fileNeverBuilt({ config, swept, files }: EmitterInput): readonly Findin
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* The paths of every file a reference written in another file reaches, naming the
|
|
66
|
-
* file or a declaration in it. An import is such a
|
|
67
|
-
* file reaches what it names, whatever reaches the
|
|
66
|
+
* file or a declaration in it, a loaded consumer's file included. An import is such a
|
|
67
|
+
* reference, so an import from any file reaches what it names, whatever reaches the
|
|
68
|
+
* importing file.
|
|
68
69
|
*/
|
|
69
70
|
function reachedFromOutside(union: Graph): ReadonlySet<string> {
|
|
70
71
|
const reached = new Set<string>();
|
|
72
|
+
for (const edge of union.consumed) {
|
|
73
|
+
const to = union.symbols[edge.to];
|
|
74
|
+
if (to !== undefined) {
|
|
75
|
+
reached.add(to.position.path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
71
78
|
union.out.forEach((edges, from) => {
|
|
72
79
|
const fromPath = union.symbols[from]?.position.path;
|
|
73
80
|
for (const edge of edges) {
|
|
@@ -94,7 +94,9 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
94
94
|
held = { writes: [], reads: 0 };
|
|
95
95
|
uses.set(reference.to, held);
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
// A consumer's store is written at a position no report of the target can name, so
|
|
98
|
+
// it counts as a use that keeps the declaration from this kind.
|
|
99
|
+
if (reference.use !== "write" || reference.consumer !== undefined) {
|
|
98
100
|
held.reads += 1;
|
|
99
101
|
continue;
|
|
100
102
|
}
|
package/src/graph.ts
CHANGED
|
@@ -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/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;
|