@cplieger/deadset-ts 8.0.0-dev.15 → 8.0.0-dev.16

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 (134) hide show
  1. package/dist/src/analysis.js +41 -37
  2. package/dist/src/commands/read.js +6 -7
  3. package/dist/src/conformance-record.js +1 -1
  4. package/dist/src/consumers.js +3 -1
  5. package/dist/src/contract/config.generated.js +1 -1
  6. package/dist/src/contract/contract.generated.js +119 -2
  7. package/dist/src/dependencies.js +79 -48
  8. package/dist/src/entries/build-scripts.js +222 -0
  9. package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
  10. package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
  11. package/dist/src/entries/conventions.js +739 -0
  12. package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
  13. package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
  14. package/dist/src/entries/run-directories.js +77 -0
  15. package/dist/src/entries/sources.js +144 -0
  16. package/dist/src/entries/static-module.js +669 -0
  17. package/dist/src/entries/test-runners.js +684 -0
  18. package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
  19. package/dist/src/extensions.js +37 -0
  20. package/dist/src/facts.js +38 -0
  21. package/dist/src/findings/emitters.js +5 -8
  22. package/dist/src/findings/self-check.js +36 -20
  23. package/dist/src/findings-pass.js +5 -6
  24. package/dist/src/framework-lifecycle.js +1 -1
  25. package/dist/src/fs/manifest.js +15 -20
  26. package/dist/src/generated-file.js +172 -29
  27. package/dist/src/graph.js +2 -4
  28. package/dist/src/index.js +1 -1
  29. package/dist/src/injection-container.js +1 -1
  30. package/dist/src/inventory/commonjs.js +62 -17
  31. package/dist/src/inventory/enumerate.js +2 -1
  32. package/dist/src/javascript-file.js +26 -11
  33. package/dist/src/load/classify.js +28 -9
  34. package/dist/src/load/diagnostics.js +8 -1
  35. package/dist/src/load/discover.js +15 -7
  36. package/dist/src/load/plan.js +2 -2
  37. package/dist/src/load/synthesize.js +5 -5
  38. package/dist/src/load/workspace-manifests.js +1 -1
  39. package/dist/src/matrix.js +6 -13
  40. package/dist/src/outside-tests.js +1 -1
  41. package/dist/src/paths.js +16 -0
  42. package/dist/src/platform-calls.js +78 -0
  43. package/dist/src/platform-symbols.js +140 -0
  44. package/dist/src/references.js +202 -32
  45. package/dist/src/resolve.js +1 -1
  46. package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
  47. package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
  48. package/dist/src/{manifest.js → roots/manifest.js} +4 -4
  49. package/dist/src/roots/published-reach.js +412 -0
  50. package/dist/src/roots/root-set.js +489 -0
  51. package/dist/src/roots/unseen-loads.js +208 -0
  52. package/dist/src/serialization-contract.js +1 -1
  53. package/dist/src/source-files.js +5 -9
  54. package/dist/src/suppress.js +30 -32
  55. package/dist/src/test-support.js +4 -2
  56. package/dist/src/verbs/analyze.js +13 -1
  57. package/dist/src/verbs/describe.js +5 -3
  58. package/jsr.json +1 -1
  59. package/package.json +2 -2
  60. package/src/analysis.ts +75 -52
  61. package/src/automatic-types.ts +1 -1
  62. package/src/commands/read.ts +6 -8
  63. package/src/conformance-record.ts +1 -1
  64. package/src/consumers.ts +4 -0
  65. package/src/contract/config.generated.ts +1 -1
  66. package/src/contract/contract.generated.ts +138 -2
  67. package/src/dependencies.ts +96 -49
  68. package/src/entries/build-scripts.ts +292 -0
  69. package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
  70. package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
  71. package/src/{conventions.ts → entries/conventions.ts} +347 -522
  72. package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
  73. package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
  74. package/src/entries/run-directories.ts +108 -0
  75. package/src/entries/sources.ts +266 -0
  76. package/src/entries/static-module.ts +857 -0
  77. package/src/entries/test-runners.ts +868 -0
  78. package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
  79. package/src/exempt.ts +3 -2
  80. package/src/extensions.ts +45 -0
  81. package/src/facts.ts +44 -0
  82. package/src/file-facts.ts +1 -1
  83. package/src/findings/emitters.ts +14 -10
  84. package/src/findings/self-check.ts +41 -22
  85. package/src/findings/visibility-narrowing.ts +1 -1
  86. package/src/findings-pass.ts +5 -12
  87. package/src/framework-lifecycle.ts +1 -1
  88. package/src/fs/manifest.ts +25 -21
  89. package/src/generated-file.ts +240 -36
  90. package/src/graph.ts +3 -5
  91. package/src/index.ts +1 -1
  92. package/src/injection-container.ts +1 -1
  93. package/src/inventory/commonjs.ts +102 -17
  94. package/src/inventory/enumerate.ts +2 -1
  95. package/src/javascript-file.ts +29 -12
  96. package/src/load/classify.ts +49 -13
  97. package/src/load/diagnostics.ts +11 -1
  98. package/src/load/discover.ts +27 -11
  99. package/src/load/plan.ts +11 -2
  100. package/src/load/synthesize.ts +6 -6
  101. package/src/load/test-globals.ts +1 -1
  102. package/src/load/workspace-manifests.ts +1 -1
  103. package/src/load/workspace-run.ts +1 -1
  104. package/src/matrix.ts +7 -14
  105. package/src/outside-tests.ts +1 -1
  106. package/src/paths.ts +18 -0
  107. package/src/platform-calls.ts +119 -0
  108. package/src/platform-symbols.ts +205 -0
  109. package/src/references.ts +281 -34
  110. package/src/report.ts +1 -1
  111. package/src/resolve.ts +1 -1
  112. package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
  113. package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
  114. package/src/{manifest.ts → roots/manifest.ts} +5 -5
  115. package/src/roots/published-reach.ts +481 -0
  116. package/src/{roots.ts → roots/root-set.ts} +203 -630
  117. package/src/roots/unseen-loads.ts +307 -0
  118. package/src/serialization-contract.ts +1 -1
  119. package/src/source-files.ts +5 -10
  120. package/src/suppress.ts +48 -33
  121. package/src/test-support.ts +8 -3
  122. package/src/verbs/analyze.ts +17 -4
  123. package/src/verbs/describe.ts +5 -2
  124. package/src/verbs/explain.ts +1 -1
  125. package/dist/src/build-scripts.js +0 -418
  126. package/dist/src/conventions.js +0 -907
  127. package/dist/src/entry-points.js +0 -444
  128. package/dist/src/own-body.js +0 -28
  129. package/dist/src/roots.js +0 -865
  130. package/dist/src/unseen-loads.js +0 -189
  131. package/src/build-scripts.ts +0 -536
  132. package/src/entry-points.ts +0 -584
  133. package/src/own-body.ts +0 -42
  134. package/src/unseen-loads.ts +0 -282
@@ -0,0 +1,307 @@
1
+ /**
2
+ * The loads by a computed path: those whose files the analysis resolves, and those it
3
+ * cannot, whose files are roots the analysis does not see, so the configuration's roots
4
+ * are never complete.
5
+ */
6
+
7
+ import {
8
+ isBinaryExpression,
9
+ isCallExpression,
10
+ isIdentifier,
11
+ isMetaProperty,
12
+ isNoSubstitutionTemplateLiteral,
13
+ isPropertyAccessExpression,
14
+ isStringLiteral,
15
+ isTemplateExpression,
16
+ SyntaxKind,
17
+ type CallExpression,
18
+ type Identifier,
19
+ type Node,
20
+ type SourceFile,
21
+ } from "@typescript/native/ast";
22
+ import { membersNamed, textsOf, writtenValue, type Value } from "../entries/static-module.ts";
23
+ import { writtenName } from "../platform-calls.ts";
24
+ import { dirnamePath, relativePath, resolvePath } from "../paths.ts";
25
+ import type { Position } from "../position.ts";
26
+ import { unwrapped } from "../unwrap.ts";
27
+ import { globExpression } from "../text/glob.ts";
28
+
29
+ /** One load the analysis cannot resolve, in the configuration whose production file writes it. */
30
+ export interface UnseenLoad {
31
+ readonly configuration: string;
32
+ /** The position of the call. */
33
+ readonly position: Position;
34
+ }
35
+
36
+ /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
37
+ function leadingLiteral(node: Node): string | undefined {
38
+ const at = unwrapped(node);
39
+ if (isTemplateExpression(at)) {
40
+ return at.head.text;
41
+ }
42
+ if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
43
+ const left = unwrapped(at.left);
44
+ return isStringLiteral(left) || isNoSubstitutionTemplateLiteral(left)
45
+ ? left.text
46
+ : leadingLiteral(at.left);
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * The directory below `dir` a computed load's leading literal names, every file of which
53
+ * the load reaches, or none where it names a package, `dir` itself or a directory holding
54
+ * it, each of which would name every sibling.
55
+ */
56
+ function computedLoadDirectory(dir: string, prefix: string): string | undefined {
57
+ if (!(prefix.startsWith("./") || prefix.startsWith("../")) || !prefix.endsWith("/")) {
58
+ return undefined;
59
+ }
60
+ const target = resolvePath(dir, prefix);
61
+ return target === dir || relativePath(target, dir) !== undefined ? undefined : target;
62
+ }
63
+
64
+ /** One `import()`, `require()` or `import.meta.glob()` call, and the declaration whose code writes it. */
65
+ export interface LoadCall {
66
+ readonly call: CallExpression;
67
+ readonly from: string;
68
+ }
69
+
70
+ /** One file a computed load may load, by its id. */
71
+ interface Loadable {
72
+ readonly file: SourceFile;
73
+ readonly fileId: string;
74
+ }
75
+
76
+ /** One load by a computed path whose files the analysis resolves. */
77
+ interface ComputedLoad {
78
+ readonly call: CallExpression;
79
+ readonly from: string;
80
+ /** The name a `require()` call is written with; {@link callsPlatform} owns which one loads. */
81
+ readonly require: Identifier | undefined;
82
+ readonly files: readonly Loadable[];
83
+ /** Whether the load reads each file as text or as a URL, which references no declaration of it. */
84
+ readonly asset: boolean;
85
+ /**
86
+ * Whether the analysis names every file the load may load. An unresolved load names none,
87
+ * and leaves the configuration's roots incomplete (confidence.26).
88
+ */
89
+ readonly resolved: boolean;
90
+ /**
91
+ * Whether the load is an eager glob, which Vite turns into static imports at the top of
92
+ * the calling module (https://vite.dev/guide/features.html#glob-import), so the file
93
+ * evaluates what it loads wherever the call is written.
94
+ */
95
+ readonly eager: boolean;
96
+ }
97
+
98
+ /** Whether one node is a call that may load a module by a computed path. */
99
+ export function isLoadCall(node: Node): node is CallExpression {
100
+ if (!isCallExpression(node)) {
101
+ return false;
102
+ }
103
+ return (
104
+ node.expression.kind === SyntaxKind.ImportKeyword ||
105
+ writtenName(node.expression) !== undefined ||
106
+ isMetaGlob(node)
107
+ );
108
+ }
109
+
110
+ /**
111
+ * The files one `import.meta.glob` call loads, as Vite's `toAbsoluteGlob` reads its globs
112
+ * (https://vite.dev/guide/features.html#glob-import): `/` against the project root, `./`
113
+ * and `../` against a literal `base` (read against the root where it starts with `/`) or the
114
+ * calling file's directory, `**` as written, and `!` removing what it matches. Undefined
115
+ * where Vite reads a glob through an alias, the root is unknown, or a glob or base is computed.
116
+ */
117
+ function globbed(
118
+ file: SourceFile,
119
+ first: Node,
120
+ options: Node | undefined,
121
+ others: readonly Loadable[],
122
+ rootOf: (fileName: string) => string | undefined,
123
+ ): readonly Loadable[] | undefined {
124
+ const patterns = textsOf(writtenValue(first), true);
125
+ const bases = globOption(options, "base");
126
+ const [written, ...more] = bases ?? [];
127
+ const base = written === undefined ? undefined : textsOf(written, false);
128
+ if (
129
+ patterns === undefined ||
130
+ bases === undefined ||
131
+ more.length > 0 ||
132
+ (written !== undefined && base === undefined)
133
+ ) {
134
+ return undefined;
135
+ }
136
+ const dir = dirnamePath(file.fileName);
137
+ const root = (): string | undefined => rootOf(file.fileName);
138
+ const against = (at: string): string | undefined => {
139
+ if (!at.startsWith("/")) {
140
+ return resolvePath(dir, at);
141
+ }
142
+ const top = root();
143
+ return top === undefined ? undefined : resolvePath(top, `.${at}`);
144
+ };
145
+ const from = base?.[0] === undefined ? dir : against(base[0]);
146
+ const keep: RegExp[] = [];
147
+ const drop: RegExp[] = [];
148
+ for (const written of patterns) {
149
+ const negated = written.startsWith("!");
150
+ const pattern = negated ? written.slice(1) : written;
151
+ let path: string | undefined;
152
+ if (pattern.startsWith("/")) {
153
+ path = against(pattern);
154
+ } else if (pattern.startsWith("./") || pattern.startsWith("../")) {
155
+ path = from === undefined ? undefined : resolvePath(from, pattern);
156
+ } else if (pattern.startsWith("**")) {
157
+ path = pattern;
158
+ }
159
+ if (path === undefined) {
160
+ return undefined;
161
+ }
162
+ (negated ? drop : keep).push(globExpression(path));
163
+ }
164
+ return others.filter(
165
+ (candidate) =>
166
+ keep.some((glob) => glob.test(candidate.file.fileName)) &&
167
+ !drop.some((glob) => glob.test(candidate.file.fileName)),
168
+ );
169
+ }
170
+
171
+ /**
172
+ * The computed loads among one file's load calls, each loading the `candidates` but the
173
+ * file itself: an `import()` or `require()` whose path starts with a relative literal naming
174
+ * a directory below the file's loads every candidate below it, as a bundler's context module
175
+ * reads it, and an `import.meta.glob` every candidate its globs match ({@link globbed}). A
176
+ * call of a computed path that names no such directory and no package is an unresolved load.
177
+ */
178
+ export function computedLoadsOf(
179
+ file: SourceFile,
180
+ calls: readonly LoadCall[],
181
+ candidates: readonly Loadable[],
182
+ rootOf: (fileName: string) => string | undefined,
183
+ ): readonly ComputedLoad[] {
184
+ const dir = dirnamePath(file.fileName);
185
+ const others = candidates.filter((candidate) => candidate.file !== file);
186
+ return calls.flatMap(({ call, from }): ComputedLoad[] => {
187
+ const [first, options] = call.arguments;
188
+ if (first === undefined) {
189
+ return [];
190
+ }
191
+ if (isMetaGlob(call)) {
192
+ const files = globbed(file, first, options, others, rootOf);
193
+ const asset = readsAsAsset(options);
194
+ return [
195
+ {
196
+ call,
197
+ from,
198
+ require: undefined,
199
+ files: files ?? [],
200
+ resolved: files !== undefined,
201
+ asset,
202
+ eager: isEager(options),
203
+ },
204
+ ];
205
+ }
206
+ const argument = unwrapped(first);
207
+ const require =
208
+ call.expression.kind === SyntaxKind.ImportKeyword ? undefined : writtenName(call.expression);
209
+ if (!mayNamePath(argument)) {
210
+ return [];
211
+ }
212
+ const prefix = leadingLiteral(argument);
213
+ if (namesPackage(prefix)) {
214
+ return [];
215
+ }
216
+ const target = prefix === undefined ? undefined : computedLoadDirectory(dir, prefix);
217
+ const files =
218
+ target === undefined
219
+ ? []
220
+ : others.filter((candidate) => relativePath(target, candidate.file.fileName) !== undefined);
221
+ return [
222
+ { call, from, require, files, resolved: target !== undefined, asset: false, eager: false },
223
+ ];
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Whether a computed argument may be a module path: anything but a string, which names its
229
+ * module, and a value no path is, a number, a boolean, `null`, an object, an array, a
230
+ * function or a class.
231
+ */
232
+ function mayNamePath(argument: Node): boolean {
233
+ return !NO_PATH.has(argument.kind);
234
+ }
235
+
236
+ const NO_PATH: ReadonlySet<SyntaxKind> = new Set([
237
+ SyntaxKind.StringLiteral,
238
+ SyntaxKind.NoSubstitutionTemplateLiteral,
239
+ SyntaxKind.NumericLiteral,
240
+ SyntaxKind.BigIntLiteral,
241
+ SyntaxKind.TrueKeyword,
242
+ SyntaxKind.FalseKeyword,
243
+ SyntaxKind.NullKeyword,
244
+ SyntaxKind.ObjectLiteralExpression,
245
+ SyntaxKind.ArrayLiteralExpression,
246
+ SyntaxKind.ArrowFunction,
247
+ SyntaxKind.FunctionExpression,
248
+ SyntaxKind.ClassExpression,
249
+ SyntaxKind.RegularExpressionLiteral,
250
+ ]);
251
+
252
+ /** Whether one call is `import.meta.glob(...)`. */
253
+ function isMetaGlob(node: CallExpression): boolean {
254
+ if (!isPropertyAccessExpression(node.expression)) {
255
+ return false;
256
+ }
257
+ const { expression, name } = node.expression;
258
+ return (
259
+ isIdentifier(name) &&
260
+ name.text === "glob" &&
261
+ isMetaProperty(expression) &&
262
+ expression.keywordToken === SyntaxKind.ImportKeyword
263
+ );
264
+ }
265
+
266
+ /** The values the options of an `import.meta.glob` write for one key, undefined where a spread may hide one. */
267
+ function globOption(options: Node | undefined, key: string): readonly Value[] | undefined {
268
+ return options === undefined ? [] : membersNamed(writtenValue(options), key);
269
+ }
270
+
271
+ /** Whether the options of an `import.meta.glob` set `eager` to `true`. */
272
+ function isEager(options: Node | undefined): boolean {
273
+ return (
274
+ globOption(options, "eager")?.some((value) => value.kind === "boolean" && value.value) === true
275
+ );
276
+ }
277
+
278
+ /** The query parameters that make Vite load a file as its text or its URL, each written bare. */
279
+ const ASSET_QUERY = /(?:^\?|&)(?:raw|url|inline)(?:&|$)/u;
280
+
281
+ /**
282
+ * Whether the options of an `import.meta.glob` read each file as text or as a URL: a
283
+ * `query` string holding a bare `raw`, `url` or `inline`, or an `as` of `raw` or `url`,
284
+ * which Vite reads as that query (https://vite.dev/guide/features.html#glob-import). Any
285
+ * other query is read as loading the module, which keeps more than it may.
286
+ */
287
+ function readsAsAsset(options: Node | undefined): boolean {
288
+ const texts = (key: string): readonly string[] =>
289
+ (globOption(options, key) ?? []).flatMap((value) =>
290
+ value.kind === "text" ? [value.text] : [],
291
+ );
292
+ return (
293
+ texts("as").some((text) => text === "raw" || text === "url") ||
294
+ texts("query").some((text) => ASSET_QUERY.test(text.startsWith("?") ? text : `?${text}`))
295
+ );
296
+ }
297
+
298
+ /** Whether a computed path's leading literal names a package, whose files are none of the target's. */
299
+ function namesPackage(prefix: string | undefined): boolean {
300
+ return (
301
+ prefix !== undefined &&
302
+ prefix !== "" &&
303
+ !prefix.startsWith(".") &&
304
+ !prefix.startsWith("/") &&
305
+ !/^[A-Za-z][A-Za-z\d+.-]*:/u.test(prefix)
306
+ );
307
+ }
@@ -72,7 +72,7 @@ import {
72
72
  namesADeclaration,
73
73
  resolveEntries,
74
74
  spelledEntry,
75
- } from "./configured-declarations.ts";
75
+ } from "./roots/configured-declarations.ts";
76
76
  import type { DetectorInput, Evidence } from "./exempt.ts";
77
77
  import { nodeKey, type Inventory } from "./inventory.ts";
78
78
  import { renderPosition, type Position } from "./position.ts";
@@ -8,6 +8,7 @@ import type { SourceFile } from "@typescript/native/ast";
8
8
  import { commonDirectory, emittedFrom } from "./load/emit-map.ts";
9
9
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
10
10
  import type { ProjectView } from "./session.ts";
11
+ import { OUTPUT_EXTENSIONS } from "./extensions.ts";
11
12
 
12
13
  /** The one character a subpath pattern of a manifest's `exports` holds. */
13
14
  const SUBPATH_WILDCARD = "*";
@@ -15,9 +16,6 @@ const SUBPATH_WILDCARD = "*";
15
16
  /** The extension a source file's name drops when the project writes it out. */
16
17
  const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
17
18
 
18
- /** The extensions the project may write a source file's output under. */
19
- const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"] as const;
20
-
21
19
  /**
22
20
  * Whether one path is what a subpath pattern stands for: the text before the
23
21
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -90,13 +88,10 @@ function layoutOf<Brand>(
90
88
  }
91
89
 
92
90
  /**
93
- * The lookups over one project's own files.
94
- *
95
- * A path that names an emitted file is read back to the source the project would
96
- * emit it from through the project's own emit mapping and nothing else: the
97
- * extension is one the compiler emits from a source extension, and a path below the
98
- * output directory is read below the directory the compiler emits from, which the
99
- * configuration declares or which is the directory every input file shares.
91
+ * The lookups over one project's own files. A path naming an emitted file is read back to its
92
+ * source through the project's own emit mapping alone: the extension is one the compiler emits
93
+ * from a source extension, and a path below the output directory is read below the directory
94
+ * the compiler emits from, which the configuration declares or every input file shares.
100
95
  */
101
96
  export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: string): SourceFiles {
102
97
  const byName = new Map<string, SourceFile>();
package/src/suppress.ts CHANGED
@@ -4,12 +4,13 @@
4
4
  * ignore entry or a baseline row. A bound record marks its declaration live before the
5
5
  * sweep, unless its code reports a part or a dependency row, and is in effect when it
6
6
  * withholds a finding its code would have produced there, dormant when the configuration
7
- * withholds every such finding anyway, and stale otherwise.
7
+ * or a hold withholds every such finding anyway, and stale otherwise.
8
8
  */
9
9
 
10
10
  import {
11
11
  type CODE,
12
12
  KIND_ROWS,
13
+ REMEDIES,
13
14
  SYMBOL_SUBJECTS,
14
15
  type Subject,
15
16
  } from "./contract/contract.generated.ts";
@@ -147,6 +148,20 @@ export interface Dials<F extends Finding = Finding> {
147
148
  readonly withholds: (finding: F) => boolean;
148
149
  }
149
150
 
151
+ /**
152
+ * What the analysis holds because it cannot decide it, as the ledger reads it: a hold is a
153
+ * gap, not evidence that the code changed, so a record it touches is dormant.
154
+ */
155
+ export interface Holds<F extends Finding = Finding> {
156
+ /** Whether a hold withholds one finding: a type error's skip, or a hold of its declaration. */
157
+ readonly withholds: (finding: F) => boolean;
158
+ /** Whether a hold keeps the declaration one record is bound to live, so no finding about it is computed. */
159
+ readonly holdsRecord: (record: SuppressionRecord) => boolean;
160
+ }
161
+
162
+ /** No hold. */
163
+ const NO_HOLDS: Holds = { withholds: () => false, holdsRecord: () => false };
164
+
150
165
  /** What every record did, and the findings the records withhold. */
151
166
  export interface Ledger {
152
167
  /** One verdict per record, in reading order. */
@@ -172,36 +187,30 @@ function rowOf(subject: Subject | undefined): boolean {
172
187
  return subject === "file" || subject === "dependency" || subject === "record";
173
188
  }
174
189
 
175
- /**
176
- * Whether a finding's subject is a row of a document rather than a declaration. Apart
177
- * from a dependency row, nothing withholds a finding about one.
178
- */
190
+ /** Whether a finding's subject is a row of a document rather than a declaration. */
179
191
  export function isRowSubject(kind: string): boolean {
180
192
  return rowOf(subjectOf(kind));
181
193
  }
182
194
 
183
- /** Whether a code reports a row of a document. */
184
- export function isRowCode(code: string): boolean {
185
- return rowOf(SUBJECT_OF_CODE.get(code));
195
+ /** The subject a code reports, which kinds.json's remedies table answers for. */
196
+ export function subjectOfCode(code: string): Subject | undefined {
197
+ return SUBJECT_OF_CODE.get(code);
186
198
  }
187
199
 
188
200
  /**
189
- * Whether a finding's subject is a dependency row. An ignore entry or a baseline row
190
- * naming the row's reference and file withholds its finding and marks nothing, and an
191
- * inline directive never binds one.
201
+ * Whether a record of `mechanism` can name a finding about `subject`, as kinds.json's
202
+ * remedies table lists the suppressions of each subject. A subject the table does not
203
+ * know is a declaration's.
192
204
  */
193
- export function isDependencySubject(kind: string): boolean {
194
- return subjectOf(kind) === "dependency";
195
- }
196
-
197
- /** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
198
- export function isDependencyCode(code: string): boolean {
199
- return SUBJECT_OF_CODE.get(code) === "dependency";
205
+ export function names(subject: Subject | undefined, mechanism: Mechanism): boolean {
206
+ return (REMEDIES[subject ?? "declaration"].suppressions as readonly Mechanism[]).includes(
207
+ mechanism,
208
+ );
200
209
  }
201
210
 
202
- /** Whether a code reports a row of a document that no record of any mechanism can name. */
203
- export function isUnsuppressibleCode(code: string): boolean {
204
- return isRowCode(code) && !isDependencyCode(code);
211
+ /** Whether a finding about a symbol of `kind` is one a record of `mechanism` can name. */
212
+ export function namesKind(kind: string, mechanism: Mechanism): boolean {
213
+ return names(subjectOf(kind), mechanism);
205
214
  }
206
215
 
207
216
  /**
@@ -238,16 +247,13 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
238
247
  if (record.code !== finding.code) {
239
248
  return false;
240
249
  }
241
- if (isDependencySubject(finding.symbol.kind)) {
242
- return (
243
- record.mechanism !== "inline" &&
244
- record.symbol === finding.symbol.ref &&
245
- record.path === finding.position.path
246
- );
247
- }
248
- if (isRowSubject(finding.symbol.kind)) {
250
+ const subject = subjectOf(finding.symbol.kind);
251
+ if (!names(subject, record.mechanism)) {
249
252
  return false;
250
253
  }
254
+ if (subject === "dependency") {
255
+ return record.symbol === finding.symbol.ref && record.path === finding.position.path;
256
+ }
251
257
  if (record.bound !== "" && record.bound === positionKey(finding.position)) {
252
258
  return true;
253
259
  }
@@ -262,13 +268,14 @@ export function pairs(record: SuppressionRecord, finding: Finding): boolean {
262
268
  * there: swept with no mark for a liveness kind, and as swept otherwise. The first record
263
269
  * in reading order claims a finding, so a second naming it is stale; a part-kind record
264
270
  * claims every finding of its code about a part of its declaration. A record is dormant
265
- * when the dials withhold every finding it claims, read from those findings alone, or when
266
- * it claims none and its code's severity is `allow`.
271
+ * when the dials or `holds` withhold every finding it claims, or when it claims none and
272
+ * its code is at `allow` or a hold keeps its declaration live.
267
273
  */
268
274
  export function ledgerOf<F extends Finding>(
269
275
  records: readonly SuppressionRecord[],
270
276
  would: readonly F[],
271
277
  dials: Dials<F>,
278
+ holds: Holds<F> = NO_HOLDS,
272
279
  ): Ledger {
273
280
  const taken = new Set<string>();
274
281
  const verdicts: Verdict[] = [];
@@ -278,13 +285,21 @@ export function ledgerOf<F extends Finding>(
278
285
  const claimed =
279
286
  first === undefined ? [] : isPartKind(record.code) ? would.filter(open) : [first];
280
287
  if (claimed.length === 0) {
281
- verdicts.push(record.bound !== "" && dials.allows(record.code) ? "dormant" : "stale");
288
+ verdicts.push(
289
+ record.bound !== "" && (dials.allows(record.code) || holds.holdsRecord(record))
290
+ ? "dormant"
291
+ : "stale",
292
+ );
282
293
  continue;
283
294
  }
284
295
  for (const finding of claimed) {
285
296
  taken.add(claimKey(finding));
286
297
  }
287
- verdicts.push(claimed.every((finding) => dials.withholds(finding)) ? "dormant" : "in-effect");
298
+ verdicts.push(
299
+ claimed.every((finding) => dials.withholds(finding) || holds.withholds(finding))
300
+ ? "dormant"
301
+ : "in-effect",
302
+ );
288
303
  }
289
304
  return {
290
305
  verdicts,
@@ -6,11 +6,11 @@
6
6
  * test code.
7
7
  */
8
8
 
9
- import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./convention-rows.ts";
9
+ import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./entries/convention-rows.ts";
10
10
  import { packageOfSpecifier } from "./dependencies.ts";
11
11
  import type { InventorySymbol } from "./inventory.ts";
12
12
  import type { Reference } from "./references.ts";
13
- import type { Root } from "./roots.ts";
13
+ import type { Root } from "./roots/root-set.ts";
14
14
 
15
15
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
16
16
  const SKIP_ROOT = "type-error";
@@ -20,7 +20,12 @@ const SKIP_ROOT = "type-error";
20
20
  * convention row reads test files for, and the convention table's test libraries.
21
21
  */
22
22
  const TEST_PACKAGES: ReadonlySet<string> = new Set([
23
- ...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
23
+ ...CONVENTION_ROWS.filter(
24
+ (row) =>
25
+ row.testFiles !== undefined ||
26
+ row.testDirectories !== undefined ||
27
+ row.testRunner !== undefined,
28
+ ).map((row) => row.package),
24
29
  ...TEST_LIBRARIES,
25
30
  ]);
26
31
 
@@ -9,10 +9,11 @@ import {
9
9
  import { kindOf } from "../config.ts";
10
10
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
11
11
  import { analyzerProvenance, verdictOf } from "../findings-pass.ts";
12
+ import { factsDocument } from "../facts.ts";
12
13
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.ts";
13
14
  import type { Host } from "../host.ts";
14
- import type { ManifestEntry } from "../manifest.ts";
15
- import type { HeldScripts } from "../roots.ts";
15
+ import type { ManifestEntry } from "../roots/manifest.ts";
16
+ import type { HeldScripts } from "../roots/root-set.ts";
16
17
  import { moduleIdentity, type NotBuilt } from "../load/discover.ts";
17
18
  import { partialNotes } from "../partial.ts";
18
19
  import {
@@ -29,7 +30,7 @@ import { RENDERINGS } from "../reporters/reporters.ts";
29
30
  import { parseTemplate, TemplateError, type Template } from "../reporters/template.ts";
30
31
  import { resolve } from "../resolve.ts";
31
32
  import { otherSchemeRows, rowReason, writeBaseline, type Recorded } from "../suppress-file.ts";
32
- import type { UnseenLoad } from "../unseen-loads.ts";
33
+ import type { UnseenLoad } from "../roots/unseen-loads.ts";
33
34
  import type { UnreadInput } from "../load/plan.ts";
34
35
  import type { UnresolvedSpecifier } from "../load/specifiers.ts";
35
36
 
@@ -42,7 +43,7 @@ import { ownVersion } from "./version.ts";
42
43
 
43
44
  /** The options `analyze` takes beside the ones every verb takes. */
44
45
  export const ANALYZE_OPTIONS: VerbOptions = {
45
- plain: ["report", "baseline-write", "exit-code", "template"],
46
+ plain: ["report", "facts", "baseline-write", "exit-code", "template"],
46
47
  repeatable: ["format"],
47
48
  };
48
49
 
@@ -258,6 +259,7 @@ function templateOf(
258
259
  /** Every document one invocation asked for: the report, each rendering beside it, a baseline. */
259
260
  interface Documents {
260
261
  readonly report: string;
262
+ readonly facts: string | undefined;
261
263
  readonly formats: readonly Format[];
262
264
  readonly baseline: string | undefined;
263
265
  }
@@ -286,8 +288,13 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
286
288
  const { config, provenance } = resolve(inputs());
287
289
  const invoked = host.workingDirectory();
288
290
  const baseline = option("baseline-write");
291
+ const facts = option("facts");
292
+ if (facts === "") {
293
+ throw new UsageError("--facts names the path the facts document is written to, and named none");
294
+ }
289
295
  const documents: Documents = {
290
296
  report: resolvePath(invoked, reportPath),
297
+ facts: facts === undefined ? undefined : resolvePath(invoked, facts),
291
298
  formats: formatsOf(repeated("format"), config.reporters.formats),
292
299
  baseline: baseline === undefined ? undefined : resolvePath(invoked, baseline),
293
300
  };
@@ -378,6 +385,12 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
378
385
  );
379
386
  }
380
387
  writeDocument(host, documents.report, "name a path the run can write", () => json(report));
388
+ const factsPath = documents.facts;
389
+ if (factsPath !== undefined) {
390
+ writeDocument(host, factsPath, "name a path the run can write", () => [
391
+ factsDocument(version, run.generated),
392
+ ]);
393
+ }
381
394
  const options = {
382
395
  readSource: (path: string) => host.readFile(joinPath(targetRoot, path)),
383
396
  template,
@@ -2,6 +2,7 @@ import {
2
2
  CONTRACT_VERSION,
3
3
  EXIT_CLEAN,
4
4
  EXIT_USAGE,
5
+ FACTS_VERSION,
5
6
  SCHEMA_VERSIONS,
6
7
  } from "../contract/contract.generated.ts";
7
8
  import { CONFORMANCE, type Conformance } from "../conformance.ts";
@@ -11,8 +12,9 @@ import { ownVersion } from "./version.ts";
11
12
 
12
13
  /**
13
14
  * The document `describe` writes: the analyzer, the Contract version it implements, the
14
- * report schema versions it reads, the languages it claims, and its recorded result over
15
- * the conformance corpus, a member the document omits where no run is recorded.
15
+ * report schema versions it reads, the languages it claims, the facts version it writes,
16
+ * and its recorded result over the conformance corpus, a member the document omits where
17
+ * no run is recorded.
16
18
  */
17
19
  export function describeDocument(version: string, recorded: Conformance | undefined): string {
18
20
  const document = {
@@ -21,6 +23,7 @@ export function describeDocument(version: string, recorded: Conformance | undefi
21
23
  contract_version: CONTRACT_VERSION,
22
24
  schema_versions_accepted: SCHEMA_VERSIONS,
23
25
  languages: [LANGUAGE],
26
+ facts_version_written: FACTS_VERSION,
24
27
  ...(recorded === undefined
25
28
  ? {}
26
29
  : {
@@ -11,7 +11,7 @@ import { resolvePath } from "../paths.ts";
11
11
  import { positionKey } from "../position.ts";
12
12
  import type { Reference } from "../references.ts";
13
13
  import { resolve } from "../resolve.ts";
14
- import type { Root } from "../roots.ts";
14
+ import type { Root } from "../roots/root-set.ts";
15
15
  import { type Verb, type VerbOptions } from "./verb.ts";
16
16
  import { UsageError } from "../usage.ts";
17
17
  import { bytewise } from "../text/compare.ts";