@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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.
Files changed (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
package/src/query.ts CHANGED
@@ -207,8 +207,22 @@ export interface Queries {
207
207
  constraintOf(type: Type): Answer<Type | undefined>;
208
208
  /** The object and the index of an indexed-access type; none for another type. */
209
209
  indexedAccessOf(type: Type): Answer<{ readonly object: Type; readonly index: Type } | undefined>;
210
+ /**
211
+ * The types one type is spelled from besides its constituents, type arguments and members:
212
+ * a type parameter's constraint and default, a conditional's check, extends, true
213
+ * and false types, a mapped type's constraint, name and template, an indexed access's
214
+ * object and index, the operand of `keyof` or of a string mapping, a template literal's
215
+ * spans and a substitution's base and constraint. None for another type.
216
+ */
217
+ partsOf(type: Type): Answer<readonly Type[]>;
218
+ /** The type parameters of a signature, its `this` parameter's type and its predicate's type. */
219
+ signaturePartsOf(signature: Signature): Answer<readonly Type[]>;
210
220
  /** The symbol a type is declared by. */
211
221
  symbolOfType(type: Type): Answer<TSSymbol | undefined>;
222
+ /** The type alias a type was instantiated from, where it was. */
223
+ aliasSymbolOf(type: Type): Answer<TSSymbol | undefined>;
224
+ /** The type arguments the alias a type was instantiated from was given. */
225
+ aliasArgumentsOf(type: Type): Answer<readonly Type[]>;
212
226
  signaturesOf(type: Type, kind: SignatureKind): Answer<readonly Signature[]>;
213
227
  resolvedSignature(node: Node): Answer<Signature | undefined>;
214
228
  /** The signature each call-like node resolves to. */
@@ -223,6 +237,37 @@ export interface Queries {
223
237
  typeToString(type: Type): Answer<string>;
224
238
  }
225
239
 
240
+ /** How to read the types one type is spelled from, as {@link Queries.partsOf} names them. */
241
+ function partQuestion(type: Type): (() => readonly (Type | undefined)[]) | undefined {
242
+ if (type.isTypeParameter()) {
243
+ return () => [type.getConstraint(), type.getDefault()];
244
+ }
245
+ if (type.isConditionalType()) {
246
+ return () => [
247
+ type.getCheckType(),
248
+ type.getExtendsType(),
249
+ type.getTrueType(),
250
+ type.getFalseType(),
251
+ ];
252
+ }
253
+ if (type.isMappedType()) {
254
+ return () => [type.getConstraintType(), type.getNameType(), type.getTemplateType()];
255
+ }
256
+ if (type.isIndexedAccessType()) {
257
+ return () => [type.getObjectType(), type.getIndexType()];
258
+ }
259
+ if (type.isIndexType() || type.isStringMappingType()) {
260
+ return () => [type.getTarget()];
261
+ }
262
+ if (type.isTemplateLiteralType()) {
263
+ return () => type.getTypes();
264
+ }
265
+ if (type.isSubstitutionType()) {
266
+ return () => [type.getBaseType(), type.getConstraint()];
267
+ }
268
+ return undefined;
269
+ }
270
+
226
271
  /**
227
272
  * The guarded questions over one project's checker. `configFile` names the project
228
273
  * in the log, `batch` sends many questions in one round trip, `log` is the run's,
@@ -528,12 +573,48 @@ export function queriesOf(
528
573
  () => ({ object: type.getObjectType(), index: type.getIndexType() }),
529
574
  )
530
575
  : undefined,
576
+ partsOf: (type) => {
577
+ const parts = partQuestion(type);
578
+ return parts === undefined
579
+ ? []
580
+ : one(
581
+ "getParts",
582
+ () => typeLocation(type),
583
+ () => parts().filter((part) => part !== undefined),
584
+ );
585
+ },
586
+ signaturePartsOf: (signature) =>
587
+ one(
588
+ "getSignatureParts",
589
+ () => `signature ${String(signature.id)}`,
590
+ () => {
591
+ const self =
592
+ signature.thisParameter === undefined ? undefined : signature.getThisParameter();
593
+ return [
594
+ ...((signature.typeParameters ?? []).length === 0 ? [] : signature.getTypeParameters()),
595
+ self === undefined ? undefined : checker.getTypeOfSymbol(self),
596
+ checker.getTypePredicateOfSignature(signature)?.type,
597
+ ].filter((part) => part !== undefined);
598
+ },
599
+ ),
531
600
  symbolOfType: (type) =>
532
601
  one(
533
602
  "getSymbol",
534
603
  () => typeLocation(type),
535
604
  () => type.getSymbol(),
536
605
  ),
606
+ aliasSymbolOf: (type) =>
607
+ one(
608
+ "getAliasSymbol",
609
+ () => typeLocation(type),
610
+ () => type.getAliasSymbol(),
611
+ ),
612
+ aliasArgumentsOf: (type) =>
613
+ one(
614
+ "getAliasTypeArguments",
615
+ () => typeLocation(type),
616
+ () => type.getAliasTypeArguments(),
617
+ ),
537
618
  signaturesOf: (type, kind) =>
538
619
  one(
539
620
  "getSignaturesOfType",
@@ -25,6 +25,13 @@ interface Valued {
25
25
  readonly value: Node;
26
26
  }
27
27
 
28
+ /** One record by its file, with the node that names its target and whether `from`'s own annotation holds it. */
29
+ interface Named {
30
+ readonly index: number;
31
+ readonly at: Node;
32
+ readonly annotated: boolean;
33
+ }
34
+
28
35
  /**
29
36
  * Where every reference of one pass is made: it renders the position against the side's
30
37
  * root, takes the writing file's test class from the file classes, marks a use only a
@@ -36,17 +43,21 @@ export class Sink {
36
43
  readonly #testFile: (file: SourceFile) => boolean;
37
44
  readonly #consumer: ConsumerSide | undefined;
38
45
  readonly #found: Reference[] = [];
46
+ readonly #declaredAt: (node: Node) => string | undefined;
39
47
  readonly #valued = new Map<SourceFile, Valued[]>();
48
+ readonly #named = new Map<SourceFile, Named[]>();
40
49
  readonly #held = new Set<string>();
41
50
 
42
51
  constructor(
43
52
  root: string,
44
53
  testFile: (file: SourceFile) => boolean,
45
54
  consumer: ConsumerSide | undefined,
55
+ declaredAt: (node: Node) => string | undefined,
46
56
  ) {
47
57
  this.#root = root;
48
58
  this.#testFile = testFile;
49
59
  this.#consumer = consumer;
60
+ this.#declaredAt = declaredAt;
50
61
  }
51
62
 
52
63
  /** Whether a test file wrote one walked file's uses. */
@@ -87,6 +98,11 @@ export class Sink {
87
98
  test,
88
99
  ...(target.guessed === true ? { guessed: true as const } : {}),
89
100
  });
101
+ if (this.#consumer === undefined && !("column" in at)) {
102
+ const named = this.#named.get(file) ?? [];
103
+ named.push({ index: this.#found.length - 1, at, annotated: this.#annotates(at, from) });
104
+ this.#named.set(file, named);
105
+ }
90
106
  if (use === "write" && value !== undefined) {
91
107
  const valued = this.#valued.get(file) ?? [];
92
108
  valued.push({ index: this.#found.length - 1, value });
@@ -95,6 +111,27 @@ export class Sink {
95
111
  }
96
112
  }
97
113
 
114
+ /**
115
+ * Whether a name sits in the annotation of `from`'s own declaration. An annotation holds
116
+ * no block, so the walk stops at the first declaration or block above the name.
117
+ */
118
+ #annotates(name: Node, from: string): boolean {
119
+ if (name.kind === SyntaxKind.SourceFile) {
120
+ return false;
121
+ }
122
+ let child = name;
123
+ let parent = name.parent;
124
+ while (parent.kind !== SyntaxKind.Block && parent.kind !== SyntaxKind.SourceFile) {
125
+ const id = this.#declaredAt(parent);
126
+ if (id !== undefined) {
127
+ return id === from && (parent as { readonly type?: Node }).type === child;
128
+ }
129
+ child = parent;
130
+ parent = parent.parent;
131
+ }
132
+ return false;
133
+ }
134
+
98
135
  /** Holds declarations an unanswered question could have made live, and the one whose read asked it. */
99
136
  hold(ids: Iterable<string>): void {
100
137
  for (const id of ids) {
@@ -130,6 +167,14 @@ export class Sink {
130
167
  }
131
168
  }
132
169
  }
170
+ for (const [file, named] of this.#named) {
171
+ for (const { index, carriers } of carriersIn(named, this.#valued.get(file) ?? [], found)) {
172
+ const reference = found[index];
173
+ if (reference !== undefined) {
174
+ found[index] = { ...reference, carriers };
175
+ }
176
+ }
177
+ }
133
178
  found.sort(
134
179
  (a, b) =>
135
180
  byPosition(a.position, b.position) ||
@@ -211,3 +256,59 @@ function aloneIn(
211
256
  }
212
257
  return result;
213
258
  }
259
+
260
+ /**
261
+ * The carriers of each named record of one file: `from` where its own annotation holds the
262
+ * name, then the target of each write whose stored value holds it and that names another
263
+ * declaration, the innermost value first. Values nest or are disjoint.
264
+ */
265
+ function carriersIn(
266
+ named: readonly Named[],
267
+ writes: readonly Valued[],
268
+ found: readonly Reference[],
269
+ ): readonly { index: number; carriers: readonly string[] }[] {
270
+ const byStart = [...named].sort((a, b) => a.at.pos - b.at.pos);
271
+ const holding = new Map<number, Valued[]>();
272
+ for (const write of writes) {
273
+ let low = 0;
274
+ let high = byStart.length;
275
+ while (low < high) {
276
+ const middle = (low + high) >>> 1;
277
+ if ((byStart[middle]?.at.pos ?? 0) < write.value.pos) {
278
+ low = middle + 1;
279
+ } else {
280
+ high = middle;
281
+ }
282
+ }
283
+ for (let at = low; at < byStart.length; at += 1) {
284
+ const one = byStart[at];
285
+ if (one === undefined || one.at.pos >= write.value.end) {
286
+ break;
287
+ }
288
+ if (one.at.end <= write.value.end) {
289
+ holding.set(one.index, [...(holding.get(one.index) ?? []), write]);
290
+ }
291
+ }
292
+ }
293
+ const result: { index: number; carriers: readonly string[] }[] = [];
294
+ for (const one of named) {
295
+ const reference = found[one.index];
296
+ if (reference === undefined) {
297
+ continue;
298
+ }
299
+ const carriers = one.annotated ? [reference.from] : [];
300
+ const values = [...(holding.get(one.index) ?? [])].sort(
301
+ (a, b) => a.value.end - a.value.pos - (b.value.end - b.value.pos),
302
+ );
303
+ for (const write of values) {
304
+ const to = found[write.index]?.to;
305
+ if (to !== undefined && to !== reference.to && !carriers.includes(to)) {
306
+ carriers.push(to);
307
+ }
308
+ }
309
+ if (carriers.length > 0) {
310
+ result.push({ index: one.index, carriers });
311
+ }
312
+ }
313
+ return result;
314
+ }
package/src/references.ts CHANGED
@@ -94,6 +94,13 @@ export interface Reference {
94
94
  * declaration with its writes deletes too.
95
95
  */
96
96
  readonly alone?: readonly string[];
97
+ /**
98
+ * The declarations whose deletion with their writes deletes the referencing name,
99
+ * innermost first: `from` where the name is in its own annotation, then each declaration
100
+ * a write of the same file stores into whose stored value holds the name. Absent where
101
+ * there is none, and on a consumer's reference.
102
+ */
103
+ readonly carriers?: readonly string[];
97
104
  }
98
105
 
99
106
  /** The consumer one reference pass reads the files of, beside the target it names. */
@@ -185,13 +192,13 @@ export function references<Brand>(
185
192
  relativePath(consumer.root, file.fileName) !== undefined &&
186
193
  relativePath(targetRoot, file.fileName) === undefined,
187
194
  );
188
- const sink = new Sink(consumer?.root ?? targetRoot, options.testFile, consumer);
189
195
  const ownPaths = project.ownPaths();
190
196
  const fileIds = new Set(
191
197
  held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
192
198
  );
193
199
  const declaredAt = (node: Node): string | undefined =>
194
200
  declarations.get(nodeKey(node.getSourceFile(), node));
201
+ const sink = new Sink(consumer?.root ?? targetRoot, options.testFile, consumer, declaredAt);
195
202
 
196
203
  let moduleBodies: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
197
204
  /**
@@ -9,6 +9,7 @@ import type { Host } from "../host.ts";
9
9
  import { isAbsolutePath, joinPath, relativePath, resolvePath } from "../paths.ts";
10
10
  import { isRecord } from "../json-document.ts";
11
11
  import { MANIFEST, manifestAt, readJSON } from "../fs/manifest.ts";
12
+ import type { HeldText } from "../commands/held.ts";
12
13
  import { pathsNamed, readCommands } from "../commands/read.ts";
13
14
 
14
15
  /** The three members that name an entry point beside `exports` and `bin`. */
@@ -41,12 +42,12 @@ export interface ManifestEntry {
41
42
  /** The directory of the manifest that names it, which `path` was resolved against. */
42
43
  readonly dir: string;
43
44
  /**
44
- * On an entry that names no entry point, whose path is the manifest's directory, the text
45
- * layer 3 holds: a whole command string the package runs, or the whole text of nodemon's
45
+ * On an entry that names no entry point, whose path is the manifest's directory, the texts
46
+ * layer 3 reads: a whole command string the package runs, or the values of nodemon's
46
47
  * configuration one is read from. Rooting enters no file for it and holds each file
47
- * `spelledFiles` reads the text to name.
48
+ * `spelledFiles` reads their words to name.
48
49
  */
49
- readonly held?: string;
50
+ readonly held?: readonly HeldText[];
50
51
  }
51
52
 
52
53
  /**
@@ -68,8 +69,9 @@ function below(member: string, key: string): string {
68
69
 
69
70
  /**
70
71
  * The whole text of nodemon's configuration, the manifest's `nodemonConfig` member and the
71
- * `nodemon.json` beside it, by the member each is. Layer 3 holds it whole, because nodemon runs
72
- * commands from keys besides `exec`, `events` among them: https://github.com/remy/nodemon#config-files
72
+ * `nodemon.json` beside it, by the member each is. The dependency rule reads it whole, and
73
+ * layer 3 each of its values, because nodemon runs commands from keys besides `exec`, `events`
74
+ * among them: https://github.com/remy/nodemon#config-files
73
75
  */
74
76
  export function nodemonTexts(
75
77
  host: Host,
@@ -89,6 +91,40 @@ export function nodemonTexts(
89
91
  ];
90
92
  }
91
93
 
94
+ /**
95
+ * The texts layer 3 reads of one text of nodemon's configuration: each value of it but its
96
+ * `exec` command, which is read as a command string of its own, those under `events` read as
97
+ * command strings too, and the whole text where it does not parse. Its keys are never read.
98
+ */
99
+ function nodemonValues(text: string): readonly HeldText[] {
100
+ let config: unknown;
101
+ try {
102
+ config = JSON.parse(text);
103
+ } catch {
104
+ return [{ text, shell: false }];
105
+ }
106
+ const found: HeldText[] = [];
107
+ const visit = (value: unknown, shell: boolean): void => {
108
+ if (Array.isArray(value) || isRecord(value)) {
109
+ for (const one of Object.values(value)) {
110
+ visit(one, shell);
111
+ }
112
+ } else if (
113
+ typeof value === "string" ||
114
+ typeof value === "number" ||
115
+ typeof value === "boolean"
116
+ ) {
117
+ found.push({ text: String(value), shell });
118
+ }
119
+ };
120
+ for (const [key, value] of Object.entries(isRecord(config) ? config : { "": config })) {
121
+ if (key !== "exec" || typeof value !== "string") {
122
+ visit(value, key === "events");
123
+ }
124
+ }
125
+ return found;
126
+ }
127
+
92
128
  /**
93
129
  * The command strings one manifest runs, each at the member it is written under: each of its
94
130
  * `scripts`, and the `exec` command of nodemon's configuration, in the manifest's `nodemonConfig`
@@ -219,10 +255,19 @@ export function readManifest(host: Host, targetRoot: string): Manifest {
219
255
  for (const path of new Set(pathsNamed(readCommands(command, ".")).map((one) => one.path))) {
220
256
  found.push({ member, path, role: "script", published: false });
221
257
  }
222
- found.push({ member, path: ".", role: "script", published: false, held: command });
258
+ found.push({
259
+ member,
260
+ path: ".",
261
+ role: "script",
262
+ published: false,
263
+ held: [{ text: command, shell: true, tested: member.startsWith("scripts[") }],
264
+ });
223
265
  }
224
266
  for (const { member, text } of nodemonTexts(host, targetRoot, manifest)) {
225
- found.push({ member, path: ".", role: "script", published: false, held: text });
267
+ const held = nodemonValues(text);
268
+ if (held.length > 0) {
269
+ found.push({ member, path: ".", role: "script", published: false, held });
270
+ }
226
271
  }
227
272
 
228
273
  return {
@@ -235,7 +235,9 @@ function declarationNodes(
235
235
  return {
236
236
  typeNamesOf: (symbol) => {
237
237
  const { byId, nodes } = nodesIn(symbol.position.path);
238
- return (byId.get(symbol.id) ?? []).flatMap((declaration) => typeNamesOf(declaration, nodes));
238
+ return (byId.get(symbol.id) ?? []).flatMap((declaration) =>
239
+ typeNamesOf(declaration, (nested) => nodes.has(nested)),
240
+ );
239
241
  },
240
242
  nodesOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id) ?? [],
241
243
  };
@@ -438,12 +440,15 @@ function inferredTypeSymbols<Brand>(
438
440
  /**
439
441
  * The names of types one declaration's own syntax writes: in a type reference, a
440
442
  * heritage clause or a type query. A function body is not read, and neither is a
441
- * declaration nested in this one, which `nested` holds and which is read on its own.
443
+ * declaration nested in this one that `readAlone` says is read on its own.
442
444
  */
443
- function typeNamesOf(declaration: Node, nested: ReadonlySet<Node>): readonly Node[] {
445
+ export function typeNamesOf(
446
+ declaration: Node,
447
+ readAlone: (nested: Node) => boolean,
448
+ ): readonly Node[] {
444
449
  const found: Node[] = [];
445
450
  const visit = (node: Node): void => {
446
- if (node !== declaration && nested.has(node)) {
451
+ if (node !== declaration && readAlone(node)) {
447
452
  return;
448
453
  }
449
454
  if (isTypeReferenceNode(node)) {
@@ -22,6 +22,7 @@ import { typeQueryAliases } from "../type-query-alias.ts";
22
22
  import { bytewise } from "../text/compare.ts";
23
23
  import { holdsManifest } from "../fs/manifest.ts";
24
24
  import { globExpression } from "../text/glob.ts";
25
+ import { heldWords, type HeldText } from "../commands/held.ts";
25
26
  import { spelledFiles } from "../text/named-files.ts";
26
27
  import type { Workflows } from "../entries/workflow-steps.ts";
27
28
  import { extensionOf, OUTPUT_EXTENSIONS } from "../extensions.ts";
@@ -104,6 +105,8 @@ export interface HeldScripts {
104
105
  readonly dir: string;
105
106
  /** Each file, by its path below the target root. */
106
107
  readonly files: readonly string[];
108
+ /** Whether a word that may write the file a command runs writes no path text (`roots.13`). */
109
+ readonly unwritten: boolean;
107
110
  }
108
111
 
109
112
  /** What decides the roots beside the declarations themselves. */
@@ -304,11 +307,12 @@ export function matchRef(pattern: string, ref: string): boolean {
304
307
  }
305
308
 
306
309
  /**
307
- * Per package, the own files its held texts name, as `spelledFiles` reads them, but those a
308
- * root enters with all they export: a command can use no more of such a file than the root
309
- * keeps live. A manifest's are its entries' held texts, and the target root's package has the
310
- * workflow steps, git hooks and makefile recipes beside its own, with the whole text of each
311
- * file they are read from.
310
+ * Per package, the own files the words of its held texts name, as `spelledFiles` reads them,
311
+ * but those a root enters with all they export: a command can use no more of such a file than
312
+ * the root keeps live. A manifest's are its entries' held texts, and the target root's package
313
+ * has the workflow steps, git hooks and makefile recipes beside its own, with the values of
314
+ * each file they are read from. A package is listed too where a word that may write the file
315
+ * a command runs writes no path text.
312
316
  */
313
317
  function scriptHolds(
314
318
  entries: readonly ManifestEntry[],
@@ -325,24 +329,22 @@ function scriptHolds(
325
329
  .filter((file) => !rooted.has(file.fileName))
326
330
  .map((file) => ({ file, paths: [file.fileName, ...files.writtenAs(file)] }));
327
331
  const cwd = host.workingDirectory();
328
- const commands = new Map<string, string[]>([
329
- [
330
- resolvePath(cwd, targetRoot),
331
- [...workflows.readings.map((reading) => reading.held), ...workflows.texts],
332
- ],
333
- ]);
332
+ const root = resolvePath(cwd, targetRoot);
333
+ const commands = new Map<string, HeldText[]>([[root, [...workflows.held]]]);
334
334
  for (const entry of entries) {
335
335
  if (entry.held !== undefined) {
336
336
  const dir = resolvePath(cwd, entry.dir);
337
- commands.set(dir, [...(commands.get(dir) ?? []), entry.held]);
337
+ commands.set(dir, [...(commands.get(dir) ?? []), ...entry.held]);
338
338
  }
339
339
  }
340
340
  const held: HeldScripts[] = [];
341
341
  for (const [dir, texts] of commands) {
342
- const named = spelledFiles(texts.join("\n"), dir, spelled);
343
- if (named.length > 0) {
344
- const paths = named.map((file) => relativePath(targetRoot, file.fileName) ?? file.fileName);
345
- held.push({ dir, files: paths });
342
+ const named = spelledFiles(heldWords(texts), dir, root, spelled);
343
+ if (named.files.length > 0 || named.unwritten) {
344
+ const paths = named.files.map(
345
+ (file) => relativePath(targetRoot, file.fileName) ?? file.fileName,
346
+ );
347
+ held.push({ dir, files: paths, unwritten: named.unwritten });
346
348
  }
347
349
  }
348
350
  return held;
package/src/stores.ts CHANGED
@@ -12,7 +12,8 @@ import {
12
12
  import { nodeKey, type Inventory } from "./inventory.ts";
13
13
  import type { Reference } from "./references.ts";
14
14
  import type { ProjectView } from "./session.ts";
15
- import type { Exemption, Mode } from "./sweep.ts";
15
+ import type { Exemption } from "./exempt.ts";
16
+ import type { Mode } from "./tests/file-class.ts";
16
17
  import { classedAsTest, madeByTestCode, withheldBy } from "./tests/file-class.ts";
17
18
 
18
19
  /** One run's stores and reads, over every configuration. */
@@ -8,14 +8,31 @@
8
8
  */
9
9
 
10
10
  import type { SourceFile } from "@typescript/native/ast";
11
- import type { Edge } from "../graph.ts";
11
+ import type { Edge } from "../liveness/graph.ts";
12
12
  import { renderPosition } from "../position.ts";
13
13
  import type { Reference } from "../references.ts";
14
14
  import type { ProjectView } from "../session.ts";
15
- import type { Mode } from "../sweep.ts";
16
15
  import { bytewise } from "../text/compare.ts";
17
16
  import { globExpression } from "../text/glob.ts";
18
17
 
18
+ /**
19
+ * The run's reference mode, decided once by the composition and read by every stage
20
+ * that counts references.
21
+ */
22
+ export interface Mode {
23
+ /**
24
+ * Counts no reference a test file made, under either relation. Every reference a root
25
+ * in a test file makes is such a reference, so nothing only a test reaches is live.
26
+ */
27
+ readonly production: boolean;
28
+ /**
29
+ * Classes a reference a loaded consumer's test file made as a production one rather than a
30
+ * test one, `analysis.consumer_tests` at `production`: a classification, not a filter, so it
31
+ * decides the split a candidate reports as well as what a production sweep counts.
32
+ */
33
+ readonly consumerTestsProduction?: boolean;
34
+ }
35
+
19
36
  /** One rule that classified files as test files, and how many files it matched. */
20
37
  export interface TestFileRule {
21
38
  /** The rule's name, as a report's own rule list spells one. */