@cplieger/deadset-ts 5.0.0 → 5.1.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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +145 -39
  3. package/dist/src/configuration-files.js +108 -4
  4. package/dist/src/conformance-record.js +3 -9
  5. package/dist/src/contextual-uses.js +247 -0
  6. package/dist/src/convention-rows.js +6 -0
  7. package/dist/src/conventions.js +123 -14
  8. package/dist/src/dependencies.js +8 -2
  9. package/dist/src/destructuring.js +6 -6
  10. package/dist/src/entry-points.js +8 -5
  11. package/dist/src/exempt-classes.js +6 -1
  12. package/dist/src/exempt.js +35 -16
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -3
  15. package/dist/src/findings/interfaces.js +6 -2
  16. package/dist/src/findings/reads-and-writes.js +53 -9
  17. package/dist/src/findings/unused-declarations.js +40 -1
  18. package/dist/src/generated-file.js +24 -0
  19. package/dist/src/graph.js +1 -1
  20. package/dist/src/interface-satisfaction.js +2 -68
  21. package/dist/src/inventory.js +17 -4
  22. package/dist/src/kinds.js +4 -3
  23. package/dist/src/manifest.js +29 -4
  24. package/dist/src/outside-tests.js +139 -0
  25. package/dist/src/overrides.js +104 -0
  26. package/dist/src/paths.js +17 -0
  27. package/dist/src/query.js +15 -0
  28. package/dist/src/references.js +229 -11
  29. package/dist/src/resolve.js +12 -11
  30. package/dist/src/roots.js +121 -37
  31. package/dist/src/session.js +4 -3
  32. package/dist/src/stores.js +3 -0
  33. package/dist/src/template-field.js +60 -3
  34. package/dist/src/value-flow.js +74 -0
  35. package/dist/src/version.js +1 -1
  36. package/jsr.json +1 -1
  37. package/package.json +2 -2
  38. package/src/analysis.ts +194 -49
  39. package/src/configuration-files.ts +137 -2
  40. package/src/conformance-record.ts +3 -10
  41. package/src/contextual-uses.ts +358 -0
  42. package/src/convention-rows.ts +8 -0
  43. package/src/conventions.ts +161 -13
  44. package/src/dependencies.ts +14 -1
  45. package/src/destructuring.ts +16 -6
  46. package/src/entry-points.ts +16 -3
  47. package/src/exempt-classes.ts +6 -1
  48. package/src/exempt.ts +67 -16
  49. package/src/file-facts.ts +26 -20
  50. package/src/findings/completion.ts +31 -3
  51. package/src/findings/interfaces.ts +9 -2
  52. package/src/findings/reads-and-writes.ts +70 -9
  53. package/src/findings/unused-declarations.ts +51 -1
  54. package/src/generated-file.ts +27 -0
  55. package/src/graph.ts +4 -4
  56. package/src/interface-satisfaction.ts +1 -92
  57. package/src/inventory.ts +19 -4
  58. package/src/kinds.ts +5 -3
  59. package/src/manifest.ts +40 -4
  60. package/src/outside-tests.ts +191 -0
  61. package/src/overrides.ts +136 -0
  62. package/src/paths.ts +18 -0
  63. package/src/query.ts +26 -0
  64. package/src/references.ts +294 -22
  65. package/src/resolve.ts +14 -13
  66. package/src/roots.ts +152 -44
  67. package/src/session.ts +4 -2
  68. package/src/stores.ts +9 -0
  69. package/src/template-field.ts +65 -3
  70. package/src/value-flow.ts +104 -0
  71. package/src/version.ts +1 -1
@@ -22,6 +22,8 @@ import {
22
22
  isNamespaceImport,
23
23
  isNoSubstitutionTemplateLiteral,
24
24
  isNonNullExpression,
25
+ isNumericLiteral,
26
+ isObjectBindingPattern,
25
27
  isObjectLiteralExpression,
26
28
  isParenthesizedExpression,
27
29
  isPropertyAccessExpression,
@@ -33,7 +35,9 @@ import {
33
35
  isVariableStatement,
34
36
  SyntaxKind,
35
37
  type AsExpression,
38
+ type Identifier,
36
39
  type Node,
40
+ type ObjectBindingPattern,
37
41
  type NonNullExpression,
38
42
  type ObjectLiteralExpression,
39
43
  type ParenthesizedExpression,
@@ -81,6 +85,14 @@ interface ConventionGlob {
81
85
  readonly excludes: readonly RegExp[];
82
86
  }
83
87
 
88
+ /** One directory an applied row names as generated, read against its manifest's directory. */
89
+ interface GeneratedDirectory {
90
+ readonly dir: string;
91
+ readonly row: string;
92
+ /** The directory below the manifest's directory, `/`-separated. */
93
+ readonly below: string;
94
+ }
95
+
84
96
  /** A setup failure a row met, at the directory of the manifest that declared it. */
85
97
  interface ConventionFailure {
86
98
  readonly dir: string;
@@ -92,6 +104,7 @@ export interface Conventions {
92
104
  /** The rows applied, ordered by name, then manifest, then their compact encoding. */
93
105
  readonly applied: readonly AppliedConvention[];
94
106
  readonly globs: readonly ConventionGlob[];
107
+ readonly generated: readonly GeneratedDirectory[];
95
108
  readonly failures: readonly ConventionFailure[];
96
109
  }
97
110
 
@@ -105,9 +118,11 @@ interface ConventionEntry {
105
118
  /** Parses one file's text with no program. */
106
119
  type ParseSource = (fileName: string, text: string) => SourceFile;
107
120
 
121
+ /** The sections an installed package is required by, so its absence fails the run. */
122
+ const REQUIRED_SECTIONS = ["dependencies", "devDependencies"] as const;
123
+
108
124
  const DEPENDENCY_SECTIONS = [
109
- "dependencies",
110
- "devDependencies",
125
+ ...REQUIRED_SECTIONS,
111
126
  "peerDependencies",
112
127
  "optionalDependencies",
113
128
  ] as const;
@@ -124,11 +139,15 @@ function readJSON(host: Host, path: string): unknown {
124
139
  }
125
140
  }
126
141
 
127
- function declares(manifest: unknown, name: string): boolean {
142
+ function declares(
143
+ manifest: unknown,
144
+ name: string,
145
+ sections: readonly string[] = DEPENDENCY_SECTIONS,
146
+ ): boolean {
128
147
  if (!isRecord(manifest)) {
129
148
  return false;
130
149
  }
131
- return DEPENDENCY_SECTIONS.some((section) => {
150
+ return sections.some((section) => {
132
151
  const held = manifest[section];
133
152
  return isRecord(held) && Object.hasOwn(held, name);
134
153
  });
@@ -174,21 +193,33 @@ function unwrapped(node: Node): Node {
174
193
  return at;
175
194
  }
176
195
 
177
- /** The key one property is written with, where it is written as a name or a literal. */
196
+ /** The key one property is written with, where it is written as a name or as literal text. */
178
197
  function keyOf(property: Node): string | undefined {
179
198
  if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
180
199
  return undefined;
181
200
  }
182
201
  const { name } = property;
183
- if (isComputedPropertyName(name)) {
184
- return undefined;
185
- }
186
- if (isIdentifier(name) || isStringLiteral(name) || isNoSubstitutionTemplateLiteral(name)) {
187
- return name.text;
202
+ const key = isComputedPropertyName(name) ? name.expression : name;
203
+ if (
204
+ (isIdentifier(key) && key === name) ||
205
+ isStringLiteral(key) ||
206
+ isNoSubstitutionTemplateLiteral(key)
207
+ ) {
208
+ return key.text;
188
209
  }
189
210
  return undefined;
190
211
  }
191
212
 
213
+ /** Whether one member's computed name is no literal, so it may set any key. */
214
+ function computesKey(member: Node): boolean {
215
+ const name = (member as { readonly name?: Node }).name;
216
+ if (name === undefined || !isComputedPropertyName(name)) {
217
+ return false;
218
+ }
219
+ const key = name.expression;
220
+ return !(isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key));
221
+ }
222
+
192
223
  /** The values one file's top-level variable declarations bind, by name. */
193
224
  function declaredValues(file: SourceFile): ReadonlyMap<string, Node> {
194
225
  const values = new Map<string, Node>();
@@ -247,6 +278,79 @@ function defaultExports(file: SourceFile): readonly Node[] {
247
278
  return found;
248
279
  }
249
280
 
281
+ /** The declarations whose name is a binding of the scope they are written in. */
282
+ const BINDING_KINDS: ReadonlySet<SyntaxKind> = new Set([
283
+ SyntaxKind.VariableDeclaration,
284
+ SyntaxKind.Parameter,
285
+ SyntaxKind.BindingElement,
286
+ SyntaxKind.FunctionDeclaration,
287
+ SyntaxKind.ClassDeclaration,
288
+ SyntaxKind.ImportClause,
289
+ SyntaxKind.ImportSpecifier,
290
+ SyntaxKind.NamespaceImport,
291
+ SyntaxKind.ImportEqualsDeclaration,
292
+ ]);
293
+
294
+ /** Whether one file declares a binding named `require` at any depth, which shadows the global. */
295
+ function declaresRequire(file: SourceFile): boolean {
296
+ let found = false;
297
+ const visit = (node: Node): void => {
298
+ const name = (node as { readonly name?: Node }).name;
299
+ if (
300
+ name !== undefined &&
301
+ isIdentifier(name) &&
302
+ name.text === "require" &&
303
+ BINDING_KINDS.has(node.kind)
304
+ ) {
305
+ found = true;
306
+ }
307
+ if (!found) {
308
+ node.forEachChild(visit);
309
+ }
310
+ };
311
+ file.forEachChild(visit);
312
+ return found;
313
+ }
314
+
315
+ /**
316
+ * The top-level `const`, `let` and `var` declarations that bind a `require` of one module,
317
+ * written with the module's name as its one string-literal argument, in a file that
318
+ * declares no binding named `require`: each binds as an import of the module does.
319
+ */
320
+ function requireBindings(
321
+ file: SourceFile,
322
+ module: string,
323
+ ): readonly (Identifier | ObjectBindingPattern)[] {
324
+ const found: (Identifier | ObjectBindingPattern)[] = [];
325
+ for (const statement of file.statements) {
326
+ if (!isVariableStatement(statement)) {
327
+ continue;
328
+ }
329
+ for (const declaration of statement.declarationList.declarations) {
330
+ const value =
331
+ declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
332
+ const [argument, ...rest] =
333
+ value !== undefined && isCallExpression(value) ? value.arguments : [];
334
+ if (
335
+ value === undefined ||
336
+ !isCallExpression(value) ||
337
+ !isIdentifier(value.expression) ||
338
+ value.expression.text !== "require" ||
339
+ argument === undefined ||
340
+ rest.length > 0 ||
341
+ !isStringLiteral(argument) ||
342
+ argument.text !== module
343
+ ) {
344
+ continue;
345
+ }
346
+ if (isIdentifier(declaration.name) || isObjectBindingPattern(declaration.name)) {
347
+ found.push(declaration.name);
348
+ }
349
+ }
350
+ }
351
+ return found.length > 0 && declaresRequire(file) ? [] : found;
352
+ }
353
+
250
354
  /**
251
355
  * The first argument of every call one file makes to one module's export, through a
252
356
  * named or a namespace import; `undefined` for a call with no argument.
@@ -254,6 +358,26 @@ function defaultExports(file: SourceFile): readonly Node[] {
254
358
  function optionsArguments(file: SourceFile, call: OptionsCall): readonly (Node | undefined)[] {
255
359
  const names = new Set<string>();
256
360
  const namespaces = new Set<string>();
361
+ for (const name of requireBindings(file, call.module)) {
362
+ if (isIdentifier(name)) {
363
+ namespaces.add(name.text);
364
+ continue;
365
+ }
366
+ for (const element of name.elements) {
367
+ const local = element.name;
368
+ const exported = element.propertyName ?? local;
369
+ if (
370
+ element.dotDotDotToken === undefined &&
371
+ local !== undefined &&
372
+ isIdentifier(local) &&
373
+ exported !== undefined &&
374
+ (isIdentifier(exported) || isStringLiteral(exported)) &&
375
+ exported.text === call.export
376
+ ) {
377
+ names.add(local.text);
378
+ }
379
+ }
380
+ }
257
381
  for (const statement of file.statements) {
258
382
  const bindings = isImportDeclaration(statement)
259
383
  ? statement.importClause?.namedBindings
@@ -333,7 +457,7 @@ function readPath(
333
457
  ): Node | undefined {
334
458
  const [key, ...rest] = keys;
335
459
  for (const member of object.properties) {
336
- if (isSpreadAssignment(member)) {
460
+ if (isSpreadAssignment(member) || computesKey(member)) {
337
461
  return member;
338
462
  }
339
463
  if (keyOf(member) !== key) {
@@ -518,6 +642,7 @@ export function readConventions(
518
642
  ): Conventions {
519
643
  const applied: AppliedConvention[] = [];
520
644
  const globs: ConventionGlob[] = [];
645
+ const generated: GeneratedDirectory[] = [];
521
646
  const failures: ConventionFailure[] = [];
522
647
  for (const manifest of manifests) {
523
648
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
@@ -532,7 +657,7 @@ export function readConventions(
532
657
  }
533
658
  const version = versions.get(row.package);
534
659
  if (version === undefined) {
535
- if (missing.has(row.package)) {
660
+ if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
536
661
  continue;
537
662
  }
538
663
  missing.add(row.package);
@@ -580,6 +705,9 @@ export function readConventions(
580
705
  const excludes = [
581
706
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
582
707
  ].map(globExpression);
708
+ for (const below of new Set((row.generated ?? []).flatMap((one) => expand(one, resolved)))) {
709
+ generated.push({ dir: manifest.dir, row: row.name, below });
710
+ }
583
711
  for (const glob of new Set(row.entries.flatMap((entry) => expand(entry, resolved)))) {
584
712
  globs.push({
585
713
  dir: manifest.dir,
@@ -597,7 +725,27 @@ export function readConventions(
597
725
  compare(a.manifest, b.manifest) ||
598
726
  compare(JSON.stringify(a), JSON.stringify(b)),
599
727
  );
600
- return { applied: unique, globs, failures };
728
+ return { applied: unique, globs, generated, failures };
729
+ }
730
+
731
+ /**
732
+ * Every own file of one project below a directory an applied row names as generated, by
733
+ * its path below the target root, with the name of the first such row.
734
+ */
735
+ export function generatedFiles(
736
+ conventions: Conventions,
737
+ files: SourceFiles,
738
+ ): ReadonlyMap<string, string> {
739
+ const found = new Map<string, string>();
740
+ for (const { dir, row, below } of conventions.generated) {
741
+ const root = joinPath(dir, below);
742
+ for (const [path, file] of files.byPath) {
743
+ if (!found.has(path) && relativePath(root, file.fileName) !== undefined) {
744
+ found.set(path, row);
745
+ }
746
+ }
747
+ }
748
+ return found;
601
749
  }
602
750
 
603
751
  /** Every own file of one project an applied row's globs match, once per row. */
@@ -27,6 +27,7 @@ import {
27
27
  declaredIn as declaredBeside,
28
28
  dependenciesNamed,
29
29
  moduleStrings,
30
+ type ParseFile,
30
31
  } from "./configuration-files.ts";
31
32
  import type { FindingPosition } from "./finding.ts";
32
33
  import type { Host } from "./host.ts";
@@ -284,6 +285,7 @@ export function projectNeeds<Brand>(
284
285
  held: Inventory,
285
286
  host: Host,
286
287
  targetRoot: string,
288
+ parse?: ParseFile,
287
289
  ): ProjectNeeds {
288
290
  const programPackages = new Set<string>();
289
291
  for (const name of project.program.getSourceFileNames()) {
@@ -329,7 +331,12 @@ export function projectNeeds<Brand>(
329
331
  }
330
332
  return names;
331
333
  };
332
- const configuration = configurationFiles(host, targetRoot, sourceFilesOf(project, targetRoot));
334
+ const configuration = configurationFiles(
335
+ host,
336
+ targetRoot,
337
+ sourceFilesOf(project, targetRoot),
338
+ parse,
339
+ );
333
340
  for (const { file } of configuration.modules) {
334
341
  const names = declaredAt(file.fileName);
335
342
  for (const { node, text } of moduleStrings(file)) {
@@ -342,6 +349,12 @@ export function projectNeeds<Brand>(
342
349
  use([PROJECT_OWNER], dependenciesNamed(text, names));
343
350
  }
344
351
  }
352
+ for (const { file, manifestDir } of configuration.outside) {
353
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
354
+ for (const { text } of moduleStrings(file)) {
355
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
356
+ }
357
+ }
345
358
  return { packages, uses, unanswered };
346
359
  }
347
360
 
@@ -25,7 +25,13 @@ import {
25
25
  type Node,
26
26
  } from "@typescript/native/unstable/ast";
27
27
  import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
28
- import { isAnswered, UNANSWERED, type Answer } from "./query.ts";
28
+ import {
29
+ isAnswered,
30
+ propertyTables,
31
+ UNANSWERED,
32
+ type Answer,
33
+ type PropertyTables,
34
+ } from "./query.ts";
29
35
  import type { ProjectView } from "./session.ts";
30
36
 
31
37
  /** One property one object pattern reads. */
@@ -228,7 +234,11 @@ type Found = Answer<Type | undefined>;
228
234
  * read once per type for the whole project, since one type is destructured in many
229
235
  * places and a table answers every name of it.
230
236
  */
231
- export function destructuring<Brand>(project: ProjectView<Brand>, cap: number): Destructuring {
237
+ export function destructuring<Brand>(
238
+ project: ProjectView<Brand>,
239
+ cap: number,
240
+ properties: PropertyTables = propertyTables(project.queries),
241
+ ): Destructuring {
232
242
  const queries = project.queries;
233
243
  const tables = new Map<number, Answer<ReadonlyMap<string, TSSymbol>>>();
234
244
  let patternBatches = 0;
@@ -241,10 +251,10 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
241
251
  }
242
252
  let table: Answer<ReadonlyMap<string, TSSymbol>> = new Map<string, TSSymbol>();
243
253
  if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
244
- patternLookups += 1;
245
- const properties = queries.propertiesOf(type);
246
- table = isAnswered(properties)
247
- ? new Map(properties.map((property) => [property.name, property]))
254
+ const read = properties.of(type);
255
+ patternLookups += read.asked ? 1 : 0;
256
+ table = isAnswered(read.properties)
257
+ ? new Map(read.properties.map((property) => [property.name, property]))
248
258
  : UNANSWERED;
249
259
  }
250
260
  tables.set(type.id, table);
@@ -30,7 +30,12 @@ import {
30
30
  type SourceFile,
31
31
  } from "@typescript/native/unstable/ast";
32
32
  import { SymbolFlags } from "@typescript/native/unstable/sync";
33
- import { configurationFiles, filesNamedBy, moduleStrings } from "./configuration-files.ts";
33
+ import {
34
+ configurationFiles,
35
+ filesNamedBy,
36
+ moduleStrings,
37
+ type ParseFile,
38
+ } from "./configuration-files.ts";
34
39
  import { globExpression } from "./glob.ts";
35
40
  import type { Host } from "./host.ts";
36
41
  import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
@@ -260,8 +265,9 @@ function configurationEntries(
260
265
  host: Host,
261
266
  targetRoot: string,
262
267
  files: SourceFiles,
268
+ parse: ParseFile | undefined,
263
269
  ): readonly EntryPoint[] {
264
- const { modules, documents } = configurationFiles(host, targetRoot, files);
270
+ const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
265
271
  const found: EntryPoint[] = [];
266
272
  const strings = (dir: string, texts: readonly string[]): void => {
267
273
  for (const text of texts) {
@@ -280,6 +286,12 @@ function configurationEntries(
280
286
  for (const document of documents) {
281
287
  strings(dirnamePath(document.path), document.strings);
282
288
  }
289
+ for (const { file } of outside) {
290
+ strings(
291
+ dirnamePath(file.fileName),
292
+ moduleStrings(file).map((one) => one.text),
293
+ );
294
+ }
283
295
  return found;
284
296
  }
285
297
 
@@ -507,8 +519,9 @@ export function entryPoints<Brand>(
507
519
  testFiles: readonly string[],
508
520
  host: Host,
509
521
  targetRoot: string,
522
+ parse?: ParseFile,
510
523
  ): readonly EntryPoint[] {
511
- const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files)];
524
+ const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
512
525
  const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
513
526
  for (const [path, file] of files.byPath) {
514
527
  const name = basename(path);
@@ -49,7 +49,12 @@ export const EXEMPTION_CLASSES = [
49
49
  confidence: "certain",
50
50
  typescriptVisibility: { private: false, privateName: false },
51
51
  },
52
- { class: "generated-file", languages: ["go"], confidence: "certain" },
52
+ {
53
+ class: "generated-file",
54
+ languages: ["go", "ts"],
55
+ confidence: "certain",
56
+ typescriptVisibility: { private: true, privateName: false },
57
+ },
53
58
  { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
54
59
  {
55
60
  class: "template-field",
package/src/exempt.ts CHANGED
@@ -48,6 +48,11 @@ export interface DetectorInput<Brand> {
48
48
  readonly ts: TSSection;
49
49
  /** The absolute path of each declared consumer, whose modules are part of the analysis. */
50
50
  readonly consumers: readonly string[];
51
+ /**
52
+ * The project's generated files, by path below the target root, each with the
53
+ * convention row that names its directory as generated.
54
+ */
55
+ readonly generated?: ReadonlyMap<string, string>;
51
56
  }
52
57
 
53
58
  /**
@@ -161,12 +166,44 @@ export function computeExemptions<Brand>(
161
166
  input: DetectorInput<Brand>,
162
167
  detectors: Detectors,
163
168
  holding: Holding,
169
+ ): readonly Exemption[] {
170
+ return detectExemptions(input, detectors, holding).records;
171
+ }
172
+
173
+ /** One project's records, and the evidence a production mode found in a test file. */
174
+ export interface Detected {
175
+ readonly records: readonly Exemption[];
176
+ /**
177
+ * Under a production mode, each record whose evidence is written in a test file. It
178
+ * holds nothing, and it is a test reference to its declaration, as a reference the
179
+ * test file wrote would be.
180
+ */
181
+ readonly inTestFiles: readonly Exemption[];
182
+ }
183
+
184
+ /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
185
+ export function detectExemptions<Brand>(
186
+ input: DetectorInput<Brand>,
187
+ detectors: Detectors,
188
+ holding: Holding,
189
+ ): Detected {
190
+ return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
191
+ }
192
+
193
+ /**
194
+ * Every record one project's detectors found, one per piece of evidence, before the
195
+ * mode decides which of them hold: what {@link heldRecords} reads.
196
+ */
197
+ export function evidenceRecords<Brand>(
198
+ input: DetectorInput<Brand>,
199
+ detectors: Detectors,
200
+ disabled: ReadonlySet<ExemptionClass>,
164
201
  ): readonly Exemption[] {
165
202
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
166
203
  const found: Exemption[] = [];
167
204
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
168
205
  const detect = detectors.get(exemptionClass);
169
- if (detect === undefined || holding.disabled.has(exemptionClass)) {
206
+ if (detect === undefined || disabled.has(exemptionClass)) {
170
207
  continue;
171
208
  }
172
209
  const allowed = typescriptVisibilityOf(exemptionClass);
@@ -174,9 +211,6 @@ export function computeExemptions<Brand>(
174
211
  if (!retainable(allowed, visibility.get(evidence.id))) {
175
212
  continue;
176
213
  }
177
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
178
- continue;
179
- }
180
214
  found.push({
181
215
  id: evidence.id,
182
216
  class: exemptionClass,
@@ -186,7 +220,21 @@ export function computeExemptions<Brand>(
186
220
  });
187
221
  }
188
222
  }
189
- return exemptionsOf(found);
223
+ return found;
224
+ }
225
+
226
+ /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
227
+ export function heldRecords(records: readonly Exemption[], holding: Holding): Detected {
228
+ const found: Exemption[] = [];
229
+ const inTestFiles: Exemption[] = [];
230
+ for (const record of records) {
231
+ if (holding.mode.production && holding.testFiles.has(record.site.path)) {
232
+ inTestFiles.push(record);
233
+ } else {
234
+ found.push(record);
235
+ }
236
+ }
237
+ return { records: exemptionsOf(found), inTestFiles };
190
238
  }
191
239
 
192
240
  /** Two strings ordered bytewise. */
@@ -247,17 +295,18 @@ function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, reado
247
295
  }
248
296
 
249
297
  /**
250
- * The records that held back a declaration some configuration of the run would
251
- * otherwise judge dead: each configuration's graph is swept once more with no
252
- * exemption and the marks it was given, and a record is retained when one of those
253
- * sweeps judged its declaration a candidate. A record that holds a declaration back in
254
- * one configuration is retained whatever another configuration answers about it, as a
255
- * run of that configuration alone would report the declaration without it. A record
256
- * on a declaration that a relation or a mark holds live in every configuration held
257
- * nothing back. The order is the declarations' site order, and for one declaration
258
- * the input's.
298
+ * The records that held back a declaration: one some configuration's graph, swept
299
+ * again with no exemption and its marks, judges a candidate, whatever another
300
+ * configuration answers, or one in `writeOnly`, which the run would report as written
301
+ * and never read without the record's read. A declaration a relation or a mark holds
302
+ * live in every configuration is held back by no record. The order is the
303
+ * declarations' site order, and for one declaration the input's.
259
304
  */
260
- export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemption[] {
305
+ export function retainedIn(
306
+ matrix: Matrix,
307
+ input: SweepInput,
308
+ writeOnly: ReadonlySet<string> = new Set(),
309
+ ): readonly Exemption[] {
261
310
  const exempt = input.exempt ?? [];
262
311
  if (exempt.length === 0) {
263
312
  return [];
@@ -268,7 +317,9 @@ export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemptio
268
317
  sweep(graph, bare).candidates.map((candidate) => candidate.id),
269
318
  ),
270
319
  );
271
- const held = byDeclaration(exempt.filter((record) => dead.has(record.id)));
320
+ const held = byDeclaration(
321
+ exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)),
322
+ );
272
323
  return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
273
324
  }
274
325
 
package/src/file-facts.ts CHANGED
@@ -47,6 +47,8 @@ export interface FileFacts {
47
47
  * reference names is included by the file that names it.
48
48
  */
49
49
  readonly heldByInclusion: ReadonlySet<string>;
50
+ /** The paths below the target root of the files below a directory an applied row names as generated. */
51
+ readonly generated?: ReadonlySet<string>;
50
52
  }
51
53
 
52
54
  /** The line of the last character of one text, counted from one; an empty text is one line. */
@@ -63,13 +65,11 @@ function lastLine(text: string): number {
63
65
  }
64
66
 
65
67
  /**
66
- * Every source file below the target root, in path order. A directory that cannot be
67
- * read is skipped, as discovery skips it, and a file that cannot be read is skipped
68
- * with it: a tree the analysis may not enter holds no file it can report.
68
+ * The path below the target root of every source file there, in path order. A directory
69
+ * that cannot be read is skipped, as discovery skips it.
69
70
  */
70
- export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
71
- const scopeOf = packageScope(host, targetRoot);
72
- const found: TreeFile[] = [];
71
+ export function sourcePaths(host: Host, targetRoot: string): readonly string[] {
72
+ const found: string[] = [];
73
73
  const pending = [""];
74
74
  for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
75
75
  let entries: readonly { readonly name: string; readonly directory: boolean }[];
@@ -86,23 +86,29 @@ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[]
86
86
  }
87
87
  continue;
88
88
  }
89
- if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
90
- continue;
89
+ if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
90
+ found.push(path);
91
91
  }
92
- let text: string;
93
- try {
94
- text = host.readFile(joinPath(targetRoot, path));
95
- } catch {
96
- continue;
97
- }
98
- found.push({
99
- path,
100
- ref: renderRef(scopeOf(path), { of: "module" }),
101
- endLine: lastLine(text),
102
- });
103
92
  }
104
93
  }
105
- return found.sort((a, b) => (a.path < b.path ? -1 : 1));
94
+ return found.sort((a, b) => (a < b ? -1 : 1));
95
+ }
96
+
97
+ /**
98
+ * Every source file below the target root, in path order. A file that cannot be read is
99
+ * skipped: a tree the analysis may not enter holds no file it can report.
100
+ */
101
+ export function sourceTree(host: Host, targetRoot: string): readonly TreeFile[] {
102
+ const scopeOf = packageScope(host, targetRoot);
103
+ return sourcePaths(host, targetRoot).flatMap((path) => {
104
+ let text: string;
105
+ try {
106
+ text = host.readFile(joinPath(targetRoot, path));
107
+ } catch {
108
+ return [];
109
+ }
110
+ return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
111
+ });
106
112
  }
107
113
 
108
114
  /** Whether one file changes what the program sees by being included in it. */