@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/analysis.ts
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Diagnostic } from "@typescript/native/unstable/sync";
|
|
12
12
|
import type { Config, Provenance } from "./config.ts";
|
|
13
|
+
import {
|
|
14
|
+
consumerLoads,
|
|
15
|
+
consumerReferences,
|
|
16
|
+
moduleIdentity,
|
|
17
|
+
type LoadedConsumer,
|
|
18
|
+
} from "./consumers.ts";
|
|
13
19
|
import { decorator } from "./decorator.ts";
|
|
14
20
|
import { dependenciesOf, projectNeeds } from "./dependencies.ts";
|
|
15
21
|
import { deprecatedDeclarations } from "./deprecation.ts";
|
|
@@ -41,11 +47,18 @@ import { injectionContainer } from "./injection-container.ts";
|
|
|
41
47
|
import { interfaceSatisfaction } from "./interface-satisfaction.ts";
|
|
42
48
|
import { inventory, type Inventory } from "./inventory.ts";
|
|
43
49
|
import { readManifest } from "./manifest.ts";
|
|
44
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
matrixOf,
|
|
52
|
+
referenceKey,
|
|
53
|
+
sweepMatrix,
|
|
54
|
+
type Configured,
|
|
55
|
+
type Matrix,
|
|
56
|
+
type SweepResult,
|
|
57
|
+
} from "./matrix.ts";
|
|
45
58
|
import { relativePath, resolvePath } from "./paths.ts";
|
|
46
59
|
import { byPosition, type Position } from "./position.ts";
|
|
47
60
|
import { reflectiveLookup } from "./reflective-lookup.ts";
|
|
48
|
-
import { references, testFileRulesOf, type TestFileRule } from "./references.ts";
|
|
61
|
+
import { references, testFileRulesOf, type Reference, type TestFileRule } from "./references.ts";
|
|
49
62
|
import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
|
|
50
63
|
import type { Scope } from "./scope.ts";
|
|
51
64
|
import { serializationContract } from "./serialization-contract.ts";
|
|
@@ -125,11 +138,27 @@ interface ProjectRead {
|
|
|
125
138
|
readonly rooted: Roots;
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
/** What a run read of the consumers its scope declares. */
|
|
142
|
+
interface ConsumerReads {
|
|
143
|
+
/** Every declared consumer, each loaded, in the scope's order. */
|
|
144
|
+
readonly loaded: readonly LoadedConsumer[];
|
|
145
|
+
/** Every reference a consumer makes to a declaration of the target, each once. */
|
|
146
|
+
readonly references: readonly Reference[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What a run read of its projects: each target project's answer, and its consumers. */
|
|
150
|
+
interface ReadProjects<Answer> {
|
|
151
|
+
readonly answers: readonly Answer[];
|
|
152
|
+
readonly consumers: ConsumerReads;
|
|
153
|
+
}
|
|
154
|
+
|
|
128
155
|
/**
|
|
129
|
-
* Reads every project the scope discovers,
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
156
|
+
* Reads every project the scope discovers, and every project of every consumer it
|
|
157
|
+
* declares, in one snapshot. Each is checked for errors first. A target project is then
|
|
158
|
+
* enumerated and rooted, and `stage` reads the rest of what it needs from the project
|
|
159
|
+
* while its view is open; a consumer project is read for its references to the target's
|
|
160
|
+
* declarations. A project carrying an error fails the run with no answer, as every verb
|
|
161
|
+
* that reads declarations does, and so does a declared consumer that cannot be loaded.
|
|
133
162
|
*/
|
|
134
163
|
function readProjects<Answer>(
|
|
135
164
|
engine: Engine,
|
|
@@ -137,10 +166,16 @@ function readProjects<Answer>(
|
|
|
137
166
|
scope: Scope,
|
|
138
167
|
config: Config,
|
|
139
168
|
stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
|
|
140
|
-
):
|
|
169
|
+
): ReadProjects<Answer> {
|
|
141
170
|
const targetRoot = scope.target.path;
|
|
142
171
|
const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
|
|
143
172
|
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
173
|
+
const loads = consumerLoads(engine, host, scope, discovered.configFiles);
|
|
174
|
+
const consumerOf = new Map(
|
|
175
|
+
loads.flatMap((load) =>
|
|
176
|
+
load.configFiles.map((configFile) => [configFile, load.consumer] as const),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
144
179
|
const options = {
|
|
145
180
|
manifest: readManifest(host, targetRoot),
|
|
146
181
|
patterns: config.rootPatterns,
|
|
@@ -149,24 +184,51 @@ function readProjects<Answer>(
|
|
|
149
184
|
publishedAPI: config.targetKind === "library",
|
|
150
185
|
};
|
|
151
186
|
const failures: Diagnostic[] = [];
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
const consumed = new Map<string, Reference>();
|
|
188
|
+
const { projects } = runSession(
|
|
189
|
+
engine,
|
|
190
|
+
[...discovered.configFiles, ...consumerOf.keys()],
|
|
191
|
+
(project) => {
|
|
192
|
+
const errors = diagnosticErrors(diagnosticsOf(project));
|
|
193
|
+
if (errors.length > 0) {
|
|
194
|
+
failures.push(...errors);
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const consumer = consumerOf.get(project.configFile);
|
|
198
|
+
if (consumer !== undefined) {
|
|
199
|
+
for (const reference of consumerReferences(
|
|
200
|
+
project,
|
|
201
|
+
host,
|
|
202
|
+
targetRoot,
|
|
203
|
+
{
|
|
204
|
+
id: consumer.id,
|
|
205
|
+
root: consumer.path,
|
|
206
|
+
},
|
|
207
|
+
config,
|
|
208
|
+
)) {
|
|
209
|
+
consumed.set(referenceKey(reference), reference);
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const held = inventory(project, host, targetRoot);
|
|
214
|
+
const rooted = roots(project, held, targetRoot, options);
|
|
215
|
+
const configuration = ids.get(project.configFile) ?? project.configFile;
|
|
216
|
+
return stage(project, { configuration, configFile: project.configFile, held, rooted });
|
|
217
|
+
},
|
|
218
|
+
);
|
|
163
219
|
if (failures.length > 0) {
|
|
164
220
|
throw new DiscoveryError(
|
|
165
221
|
`${String(failures.length)} error(s); no answer was produced`,
|
|
166
222
|
failures,
|
|
167
223
|
);
|
|
168
224
|
}
|
|
169
|
-
return
|
|
225
|
+
return {
|
|
226
|
+
answers: projects.filter((answer): answer is Answer => answer !== undefined),
|
|
227
|
+
consumers: {
|
|
228
|
+
loaded: loads.map((load) => load.consumer),
|
|
229
|
+
references: [...consumed.values()],
|
|
230
|
+
},
|
|
231
|
+
};
|
|
170
232
|
}
|
|
171
233
|
|
|
172
234
|
/**
|
|
@@ -183,7 +245,7 @@ export function runRoots(
|
|
|
183
245
|
provenance: Provenance,
|
|
184
246
|
): RunRoots {
|
|
185
247
|
const targetRoot = scope.target.path;
|
|
186
|
-
const projects = readProjects(engine, host, scope, config, (_project, read) => read);
|
|
248
|
+
const projects = readProjects(engine, host, scope, config, (_project, read) => read).answers;
|
|
187
249
|
|
|
188
250
|
const configurations: string[] = [];
|
|
189
251
|
const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
|
|
@@ -266,12 +328,15 @@ interface ReadRun<Extra> {
|
|
|
266
328
|
readonly matrix: Matrix;
|
|
267
329
|
readonly exempt: readonly Exemption[];
|
|
268
330
|
readonly projects: readonly SweptProject<Extra>[];
|
|
331
|
+
/** Every declared consumer, each loaded, in the scope's order. */
|
|
332
|
+
readonly consumers: readonly LoadedConsumer[];
|
|
269
333
|
}
|
|
270
334
|
|
|
271
335
|
/**
|
|
272
336
|
* Reads every project of the run, its references and exemptions, and `extra`'s answer,
|
|
273
337
|
* while the project's view is open, and merges the projects into one matrix in the run's
|
|
274
|
-
* order.
|
|
338
|
+
* order. Every configuration holds every consumer's references beside its own, because a
|
|
339
|
+
* consumer calls the target whichever configuration the target is built under.
|
|
275
340
|
*/
|
|
276
341
|
function readRun<Extra>(
|
|
277
342
|
engine: Engine,
|
|
@@ -288,7 +353,7 @@ function readRun<Extra>(
|
|
|
288
353
|
? { delimiters: config.analysis.templateDelimiters, files: [] }
|
|
289
354
|
: readTemplates(host, targetRoot, config.analysis);
|
|
290
355
|
const consumers = scope.consumers.map((consumer) => consumer.path);
|
|
291
|
-
const
|
|
356
|
+
const read = readProjects(engine, host, scope, config, (project, projectRead) => {
|
|
292
357
|
const resolved = references(project, projectRead.held, targetRoot, {
|
|
293
358
|
testFiles: config.ts.testFiles,
|
|
294
359
|
});
|
|
@@ -315,10 +380,23 @@ function readRun<Extra>(
|
|
|
315
380
|
extra: extra(project, projectRead),
|
|
316
381
|
};
|
|
317
382
|
});
|
|
383
|
+
const consumed = read.consumers.references;
|
|
384
|
+
const projects = read.answers.map((one) =>
|
|
385
|
+
consumed.length === 0
|
|
386
|
+
? one
|
|
387
|
+
: {
|
|
388
|
+
...one,
|
|
389
|
+
configured: {
|
|
390
|
+
...one.configured,
|
|
391
|
+
references: [...one.configured.references, ...consumed],
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
);
|
|
318
395
|
return {
|
|
319
396
|
matrix: matrixOf(projects.map((one) => one.configured)),
|
|
320
397
|
exempt: exemptionsOf(projects.flatMap((one) => one.exempt)),
|
|
321
398
|
projects,
|
|
399
|
+
consumers: read.consumers.loaded,
|
|
322
400
|
};
|
|
323
401
|
}
|
|
324
402
|
|
|
@@ -430,7 +508,10 @@ function emitterInputOver(
|
|
|
430
508
|
heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
|
|
431
509
|
},
|
|
432
510
|
boundary: {
|
|
433
|
-
consumers: {
|
|
511
|
+
consumers: {
|
|
512
|
+
declared: scope.consumers.map((consumer) => moduleIdentity(host, consumer)),
|
|
513
|
+
loaded: read.consumers.map((consumer) => consumer.id),
|
|
514
|
+
},
|
|
434
515
|
encapsulated: readManifest(host, targetRoot).declaresExports,
|
|
435
516
|
edges: readEdgeSides(host, targetRoot),
|
|
436
517
|
},
|
|
@@ -442,8 +523,8 @@ function emitterInputOver(
|
|
|
442
523
|
* What every emitter reads of the run the scope and the configuration describe, swept
|
|
443
524
|
* under the caller's marks: the run's sweep, the facts beside it each kind family reads,
|
|
444
525
|
* and the sides of the target's declared cross-language edges that name a symbol of this
|
|
445
|
-
* language. The run
|
|
446
|
-
*
|
|
526
|
+
* language. The run reads no suppression document, so the self-check family has nothing
|
|
527
|
+
* to report.
|
|
447
528
|
*/
|
|
448
529
|
export function runEmitterInput(
|
|
449
530
|
engine: Engine,
|
|
@@ -526,6 +607,7 @@ function runAnalysisInputs(
|
|
|
526
607
|
config.ts.testFiles,
|
|
527
608
|
read.projects.flatMap((one) => one.configured.testFiles),
|
|
528
609
|
),
|
|
610
|
+
consumers: read.consumers,
|
|
529
611
|
},
|
|
530
612
|
configured: read.projects.map((one) => one.configured),
|
|
531
613
|
};
|
|
@@ -544,6 +626,8 @@ export interface RunFacts {
|
|
|
544
626
|
readonly projects: readonly RunProject[];
|
|
545
627
|
/** The rules that classified files as test files, each counted over every project at once. */
|
|
546
628
|
readonly testFileRules: readonly TestFileRule[];
|
|
629
|
+
/** Every consumer the run loaded beside the target, in the scope's order. */
|
|
630
|
+
readonly consumers: readonly LoadedConsumer[];
|
|
547
631
|
}
|
|
548
632
|
|
|
549
633
|
/** The findings of one run, beside what a report states about the run itself. */
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
5
|
corpusVersion: "1.9.0",
|
|
6
|
-
result: "
|
|
7
|
-
digest: "sha256:
|
|
6
|
+
result: "pass",
|
|
7
|
+
digest: "sha256:3e9e40876551faa2c196311bf06d47d220ab5127a9fb8921969aae552c42e824",
|
|
8
8
|
},
|
|
9
9
|
gaps: [
|
|
10
10
|
{
|
|
@@ -13,17 +13,5 @@ export const RECORD = {
|
|
|
13
13
|
reason:
|
|
14
14
|
"The reference pass resolves a string-literal element access to the member it names, so the member is live by that reference and the retained listing names no class for it.",
|
|
15
15
|
},
|
|
16
|
-
{
|
|
17
|
-
fixture: "redundant-export-keyword",
|
|
18
|
-
capability: "DS1104",
|
|
19
|
-
reason:
|
|
20
|
-
"No consumer is loaded beside the target, so the published surface is unknown and no export is narrowed; review an export only the target uses by hand.",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
fixture: "unused-exported-consumer",
|
|
24
|
-
capability: "DS1001",
|
|
25
|
-
reason:
|
|
26
|
-
"No consumer is loaded beside the target, so an export nothing in the target references is reported at the possible class rather than the certain one.",
|
|
27
|
-
},
|
|
28
16
|
],
|
|
29
17
|
} as const;
|
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
|
}
|