@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/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 { matrixOf, sweepMatrix, type Configured, type Matrix, type SweepResult } from "./matrix.ts";
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, in one snapshot: each is checked for errors
130
- * first, then enumerated, then rooted, and `stage` reads the rest of what it needs from
131
- * the project while its view is open. A project carrying an error fails the run with no
132
- * answer, as every verb that reads declarations does.
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
- ): readonly Answer[] {
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 { projects } = runSession(engine, discovered.configFiles, (project) => {
153
- const errors = diagnosticErrors(diagnosticsOf(project));
154
- if (errors.length > 0) {
155
- failures.push(...errors);
156
- return undefined;
157
- }
158
- const held = inventory(project, host, targetRoot);
159
- const rooted = roots(project, held, targetRoot, options);
160
- const configuration = ids.get(project.configFile) ?? project.configFile;
161
- return stage(project, { configuration, configFile: project.configFile, held, rooted });
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 projects.filter((answer): answer is Answer => answer !== undefined);
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 projects = readProjects(engine, host, scope, config, (project, projectRead) => {
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: { declared: scope.consumers.map((consumer) => consumer.id), loaded: [] },
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 loads no consumer, and it reads no suppression document, so the
446
- * self-check family has nothing to report.
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: "fail",
7
- digest: "sha256:8c29f19fc189f4f5d9ea210d556b62d12c3701c221eb7a8adde499b7060bec0d",
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;
@@ -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 files the scope names directly: one per module the scope
164
- * declares whose path names a configuration file rather than a directory. A module
165
- * naming a directory is reached by the walk over that directory instead.
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, ...scope.consumers]
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 seen = new Set<string>();
261
- const projects: DiscoveredProject[] = [];
262
- const pending = [...fromScope, ...configFilesUnder(host, scope.target.path)];
263
- while (pending.length > 0) {
264
- const configFile = pending.shift();
265
- if (configFile === undefined) {
266
- break;
267
- }
268
- const path = resolvePath(host.workingDirectory(), configFile);
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, Config, Severity } from "../config.ts";
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(config: Config, code: string, fallback: Severity): Severity {
30
- return (
31
- config.severity.get(code) ?? config.severity.get(code.slice(0, FAMILY_KEY_LENGTH)) ?? fallback
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, and the run loads no consumer that
38
- * would name them, so it is `possible`; every other declaration has every reference
39
- * in the loaded program, a private member and a `#private` name among them, and is
40
- * `certain`.
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(swept: RunSweep): (id: string) => Confidence {
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
- return (id) => (published.has(id) ? "possible" : "certain");
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(swept);
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 = KINDS.get(finding.code);
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.config, finding.code, row.defaultSeverity),
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.swept);
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.config, code, row.defaultSeverity),
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 reference, so an import from any
67
- * file reaches what it names, whatever reaches the importing file.
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
- if (reference.use !== "write") {
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
  }