@cplieger/deadset-ts 8.0.0-dev.14 → 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 (149) hide show
  1. package/dist/src/alias-chain.js +2 -2
  2. package/dist/src/analysis.js +43 -38
  3. package/dist/src/commands/read.js +6 -7
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/consumers.js +4 -2
  6. package/dist/src/contract/config.generated.js +1 -1
  7. package/dist/src/contract/contract.generated.js +120 -2
  8. package/dist/src/dependencies.js +81 -49
  9. package/dist/src/entries/build-scripts.js +222 -0
  10. package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
  11. package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
  12. package/dist/src/entries/conventions.js +739 -0
  13. package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
  14. package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
  15. package/dist/src/entries/run-directories.js +77 -0
  16. package/dist/src/entries/sources.js +144 -0
  17. package/dist/src/entries/static-module.js +669 -0
  18. package/dist/src/entries/test-runners.js +684 -0
  19. package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
  20. package/dist/src/extensions.js +37 -0
  21. package/dist/src/facts.js +38 -0
  22. package/dist/src/file-facts.js +1 -1
  23. package/dist/src/findings/emitters.js +5 -8
  24. package/dist/src/findings/self-check.js +36 -20
  25. package/dist/src/findings-pass.js +5 -6
  26. package/dist/src/framework-lifecycle.js +1 -1
  27. package/dist/src/fs/manifest.js +15 -20
  28. package/dist/src/generated-file.js +172 -29
  29. package/dist/src/graph.js +2 -5
  30. package/dist/src/index.js +1 -1
  31. package/dist/src/injection-container.js +1 -1
  32. package/dist/src/inventory/commonjs.js +507 -0
  33. package/dist/src/inventory/enumerate.js +167 -0
  34. package/dist/src/inventory/syntax.js +236 -0
  35. package/dist/src/inventory.js +305 -705
  36. package/dist/src/javascript-file.js +26 -11
  37. package/dist/src/load/classify.js +28 -9
  38. package/dist/src/load/diagnostics.js +8 -1
  39. package/dist/src/load/discover.js +16 -8
  40. package/dist/src/load/package-scope.js +56 -0
  41. package/dist/src/load/plan.js +2 -2
  42. package/dist/src/load/synthesize.js +5 -5
  43. package/dist/src/load/workspace-manifests.js +1 -1
  44. package/dist/src/matrix.js +6 -13
  45. package/dist/src/outside-tests.js +1 -1
  46. package/dist/src/paths.js +16 -0
  47. package/dist/src/platform-calls.js +78 -0
  48. package/dist/src/platform-symbols.js +140 -0
  49. package/dist/src/position.js +41 -0
  50. package/dist/src/ref.js +11 -20
  51. package/dist/src/references.js +281 -50
  52. package/dist/src/resolve.js +1 -1
  53. package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
  54. package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
  55. package/dist/src/{manifest.js → roots/manifest.js} +4 -4
  56. package/dist/src/roots/published-reach.js +412 -0
  57. package/dist/src/roots/root-set.js +489 -0
  58. package/dist/src/roots/unseen-loads.js +208 -0
  59. package/dist/src/serialization-contract.js +1 -1
  60. package/dist/src/session.js +3 -0
  61. package/dist/src/source-files.js +5 -9
  62. package/dist/src/suppress-inline.js +33 -81
  63. package/dist/src/suppress.js +30 -32
  64. package/dist/src/test-support.js +4 -2
  65. package/dist/src/verbs/analyze.js +13 -1
  66. package/dist/src/verbs/describe.js +5 -3
  67. package/jsr.json +1 -1
  68. package/package.json +3 -3
  69. package/src/alias-chain.ts +2 -2
  70. package/src/analysis.ts +77 -53
  71. package/src/automatic-types.ts +1 -1
  72. package/src/commands/read.ts +6 -8
  73. package/src/conformance-record.ts +1 -1
  74. package/src/consumers.ts +5 -1
  75. package/src/contract/config.generated.ts +1 -1
  76. package/src/contract/contract.generated.ts +139 -2
  77. package/src/dependencies.ts +98 -50
  78. package/src/entries/build-scripts.ts +292 -0
  79. package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
  80. package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
  81. package/src/{conventions.ts → entries/conventions.ts} +347 -522
  82. package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
  83. package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
  84. package/src/entries/run-directories.ts +108 -0
  85. package/src/entries/sources.ts +266 -0
  86. package/src/entries/static-module.ts +857 -0
  87. package/src/entries/test-runners.ts +868 -0
  88. package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
  89. package/src/exempt.ts +3 -2
  90. package/src/extensions.ts +45 -0
  91. package/src/facts.ts +44 -0
  92. package/src/file-facts.ts +2 -2
  93. package/src/findings/emitters.ts +14 -10
  94. package/src/findings/self-check.ts +41 -22
  95. package/src/findings/visibility-narrowing.ts +1 -1
  96. package/src/findings-pass.ts +5 -12
  97. package/src/framework-lifecycle.ts +1 -1
  98. package/src/fs/manifest.ts +25 -21
  99. package/src/generated-file.ts +240 -36
  100. package/src/graph.ts +3 -6
  101. package/src/index.ts +1 -1
  102. package/src/injection-container.ts +1 -1
  103. package/src/inventory/commonjs.ts +696 -0
  104. package/src/inventory/enumerate.ts +203 -0
  105. package/src/inventory/syntax.ts +311 -0
  106. package/src/inventory.ts +424 -878
  107. package/src/javascript-file.ts +29 -12
  108. package/src/load/classify.ts +49 -13
  109. package/src/load/diagnostics.ts +11 -1
  110. package/src/load/discover.ts +28 -12
  111. package/src/load/package-scope.ts +74 -0
  112. package/src/load/plan.ts +11 -2
  113. package/src/load/synthesize.ts +6 -6
  114. package/src/load/test-globals.ts +1 -1
  115. package/src/load/workspace-manifests.ts +1 -1
  116. package/src/load/workspace-run.ts +1 -1
  117. package/src/matrix.ts +7 -14
  118. package/src/outside-tests.ts +1 -1
  119. package/src/paths.ts +18 -0
  120. package/src/platform-calls.ts +119 -0
  121. package/src/platform-symbols.ts +205 -0
  122. package/src/position.ts +42 -0
  123. package/src/ref.ts +11 -20
  124. package/src/references.ts +383 -73
  125. package/src/report.ts +1 -1
  126. package/src/resolve.ts +1 -1
  127. package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
  128. package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
  129. package/src/{manifest.ts → roots/manifest.ts} +5 -5
  130. package/src/roots/published-reach.ts +481 -0
  131. package/src/{roots.ts → roots/root-set.ts} +203 -630
  132. package/src/roots/unseen-loads.ts +307 -0
  133. package/src/serialization-contract.ts +1 -1
  134. package/src/session.ts +17 -1
  135. package/src/source-files.ts +5 -10
  136. package/src/suppress-inline.ts +53 -89
  137. package/src/suppress.ts +48 -33
  138. package/src/test-support.ts +8 -3
  139. package/src/verbs/analyze.ts +17 -4
  140. package/src/verbs/describe.ts +5 -2
  141. package/src/verbs/explain.ts +1 -1
  142. package/dist/src/build-scripts.js +0 -418
  143. package/dist/src/conventions.js +0 -907
  144. package/dist/src/entry-points.js +0 -513
  145. package/dist/src/roots.js +0 -865
  146. package/dist/src/unseen-loads.js +0 -100
  147. package/src/build-scripts.ts +0 -536
  148. package/src/entry-points.ts +0 -661
  149. package/src/unseen-loads.ts +0 -134
@@ -12,7 +12,7 @@ import { SignatureKind, SymbolFlags, TypeFlags, } from "@typescript/native/sync"
12
12
  import { aliasChains } from "./alias-chain.js";
13
13
  import { calleeName, resolvedTargets, valuesPassed } from "./calls.js";
14
14
  import { classMembers, memberComponent, typesAt } from "./class-members.js";
15
- import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./configured-declarations.js";
15
+ import { names, namesADeclaration, resolveEntries, spelledEntry, } from "./roots/configured-declarations.js";
16
16
  import { nodeKey } from "./inventory.js";
17
17
  import { renderPosition } from "./position.js";
18
18
  import { must, Unanswerable } from "./query.js";
@@ -138,6 +138,9 @@ function viewOf(project, configFile, engine, log) {
138
138
  ownKeys ??= new Set(ownSourceFiles().map((file) => file.path));
139
139
  return ownKeys;
140
140
  },
141
+ resolvedFileNames: (specifiers) => batched(engine, specifiers, (node) => project.program.getResolvedModuleFromModuleSpecifier.gen(node, node.getSourceFile().fileName)).map((resolved) => resolved === undefined || resolved.resolvedFileName === ""
142
+ ? undefined
143
+ : resolved.resolvedFileName),
141
144
  handle: (node) => ({ node }),
142
145
  symbolsAt: (handles, cap) => queries.symbolsAt(handles.map(({ node }) => node), cap),
143
146
  resolvedSymbolAt: ({ node }) => queries.resolvedSymbol(node),
@@ -5,12 +5,11 @@
5
5
  */
6
6
  import { commonDirectory, emittedFrom } from "./load/emit-map.js";
7
7
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
8
+ import { OUTPUT_EXTENSIONS } from "./extensions.js";
8
9
  /** The one character a subpath pattern of a manifest's `exports` holds. */
9
10
  const SUBPATH_WILDCARD = "*";
10
11
  /** The extension a source file's name drops when the project writes it out. */
11
12
  const SOURCE_EXTENSION = /\.[cm]?[jt]sx?$/u;
12
- /** The extensions the project may write a source file's output under. */
13
- const OUTPUT_EXTENSIONS = [".js", ".mjs", ".cjs", ".jsx", ".d.ts", ".d.mts", ".d.cts"];
14
13
  /**
15
14
  * Whether one path is what a subpath pattern stands for: the text before the
16
15
  * wildcard, any run of characters including the solidus, then the text after it.
@@ -43,13 +42,10 @@ function layoutOf(project, fileNames) {
43
42
  };
44
43
  }
45
44
  /**
46
- * The lookups over one project's own files.
47
- *
48
- * A path that names an emitted file is read back to the source the project would
49
- * emit it from through the project's own emit mapping and nothing else: the
50
- * extension is one the compiler emits from a source extension, and a path below the
51
- * output directory is read below the directory the compiler emits from, which the
52
- * configuration declares or which is the directory every input file shares.
45
+ * The lookups over one project's own files. A path naming an emitted file is read back to its
46
+ * source through the project's own emit mapping alone: the extension is one the compiler emits
47
+ * from a source extension, and a path below the output directory is read below the directory
48
+ * the compiler emits from, which the configuration declares or every input file shares.
53
49
  */
54
50
  export function sourceFilesOf(project, targetRoot) {
55
51
  const byName = new Map();
@@ -4,12 +4,12 @@
4
4
  * below it, or on its own line where it trails a token there.
5
5
  */
6
6
  import { CODE } from "./contract/contract.generated.js";
7
- import { isConstructorDeclaration, SyntaxKind, } from "@typescript/native/ast";
7
+ import { SyntaxKind } from "@typescript/native/ast";
8
8
  import { importBindings } from "./import-bindings.js";
9
- import { isParameterProperty, nodeKey } from "./inventory.js";
10
- import { byPosition, positionKey, renderPosition } from "./position.js";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { relativePath } from "./paths.js";
11
+ import { byPosition, lineEnd, positionKey, renderPosition, startLine, tokenStart, } from "./position.js";
11
12
  import { importRef } from "./ref.js";
12
- import { signatureOf } from "./signatures.js";
13
13
  import { isPartKind, SuppressionError, siteText, } from "./suppress.js";
14
14
  /**
15
15
  * The three expressions the grammar applies to the text of each line comment, in this
@@ -55,43 +55,6 @@ const LITERALS = new Set([
55
55
  SyntaxKind.RegularExpressionLiteral,
56
56
  SyntaxKind.JsxText,
57
57
  ]);
58
- /** Whether one character ends a line, under the language's line terminators. */
59
- function endsLine(char) {
60
- return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
61
- }
62
- /** The offset where the line holding `from` ends. */
63
- function lineEnd(text, from) {
64
- let at = from;
65
- while (at < text.length && !endsLine(text[at])) {
66
- at += 1;
67
- }
68
- return at;
69
- }
70
- /**
71
- * The offset of the first token at or after `from`: whitespace, comments and a leading
72
- * hashbang are trivia.
73
- */
74
- export function tokenStart(text, from) {
75
- let at = from;
76
- if (at === 0 && text.startsWith("#!")) {
77
- at = lineEnd(text, 0);
78
- }
79
- for (;;) {
80
- if (/\s/u.test(text[at] ?? "")) {
81
- at += 1;
82
- }
83
- else if (text.startsWith("//", at)) {
84
- at = lineEnd(text, at);
85
- }
86
- else if (text.startsWith("/*", at)) {
87
- const close = text.indexOf("*/", at + 2);
88
- at = close < 0 ? text.length : close + 2;
89
- }
90
- else {
91
- return at;
92
- }
93
- }
94
- }
95
58
  /**
96
59
  * Every `//` line comment of one file, in file order. The text between tokens is read as
97
60
  * the language reads it: a block comment holds no line comment, and the literal tokens the
@@ -140,55 +103,42 @@ function lineComments(file) {
140
103
  function bindingKey(one) {
141
104
  return `${one.declared?.id ?? ""}\u0000${one.part.id}`;
142
105
  }
106
+ /** The placed declarations of a project by the path of the file each is in. */
107
+ function placedByPath(held, symbols) {
108
+ const byPath = new Map();
109
+ for (const [id, placed] of held.placement) {
110
+ const one = symbols.get(id);
111
+ const holder = symbols.get(placed.holder);
112
+ if (one === undefined || holder === undefined) {
113
+ continue;
114
+ }
115
+ const at = byPath.get(one.position.path) ?? [];
116
+ at.push({ one, holder, startLines: placed.startLines, partLines: placed.partLines });
117
+ byPath.set(one.position.path, at);
118
+ }
119
+ return byPath;
120
+ }
143
121
  /**
144
- * The declarations of one file by the line each begins on, the line of its first token: a
145
- * decorator, a modifier or the keyword. A variable's first token is its statement's
146
- * keyword, and a variable written on a later line of the statement also begins there. A
147
- * function also begins, for a directive, on the line of each of its parameters, and a
148
- * class, for a part kind, on the lines of its constructor and its constructor's parameters.
122
+ * The declarations of one file by the line a directive binds each on, as the inventory
123
+ * places them, and each import binding of the file on its statement's lines.
149
124
  */
150
- function declarationLines(file, held, symbols, targetRoot) {
125
+ function declarationLines(file, placed, held, targetRoot) {
151
126
  const below = new Map();
152
- const add = (offset, one) => {
153
- const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
127
+ const add = (line, one) => {
154
128
  const at = below.get(line) ?? [];
155
129
  if (!at.some((known) => bindingKey(known) === bindingKey(one))) {
156
130
  at.push(one);
157
131
  }
158
132
  below.set(line, at);
159
133
  };
160
- const declaredAt = (node) => {
161
- const id = held.declarations.get(nodeKey(file, node));
162
- return id === undefined ? undefined : symbols.get(id);
163
- };
164
- const visit = (node) => {
165
- const one = declaredAt(node);
166
- const owner = isConstructorDeclaration(node) ? declaredAt(node.parent) : undefined;
167
- if (owner !== undefined && isConstructorDeclaration(node)) {
168
- for (const at of [node, ...node.parameters.filter((one) => !isParameterProperty(one))]) {
169
- add(at.pos, { declared: undefined, part: owner });
170
- }
171
- }
172
- const holder = one !== undefined && isParameterProperty(node) ? declaredAt(node.parent.parent) : one;
173
- if (one !== undefined && holder !== undefined) {
174
- add(node.pos, { declared: one, part: holder });
134
+ for (const { one, holder, startLines, partLines } of placed) {
135
+ for (const line of startLines) {
136
+ add(line, { declared: one, part: holder });
175
137
  }
176
- if (one !== undefined) {
177
- // A parameter on a later line of a wrapped signature takes its directive on the
178
- // line above it, inside the parameter list, and the directive binds the function.
179
- for (const parameter of signatureOf(node)?.parameters ?? []) {
180
- add(parameter.pos, { declared: one, part: one });
181
- }
182
- const list = node.parent;
183
- if (node.kind === SyntaxKind.VariableDeclaration &&
184
- list.kind === SyntaxKind.VariableDeclarationList &&
185
- list.parent.kind === SyntaxKind.VariableStatement) {
186
- add(list.parent.pos, { declared: one, part: one });
187
- }
138
+ for (const line of partLines) {
139
+ add(line, { declared: undefined, part: one });
188
140
  }
189
- node.forEachChild(visit);
190
- };
191
- file.forEachChild(visit);
141
+ }
192
142
  // An import binding is a subject of its own, bound on its line as a declaration is.
193
143
  const fileId = held.declarations.get(nodeKey(file, file));
194
144
  const moduleRef = held.symbols.find((symbol) => symbol.id === fileId)?.ref ?? "";
@@ -200,7 +150,7 @@ function declarationLines(file, held, symbols, targetRoot) {
200
150
  statement = statement.parent;
201
151
  }
202
152
  for (const at of [statement.pos, name.getStart()]) {
203
- add(at, { declared: one, part: one });
153
+ add(startLine(file, at), { declared: one, part: one });
204
154
  }
205
155
  }
206
156
  return below;
@@ -221,13 +171,15 @@ export function inlineDirectives(project, held, targetRoot) {
221
171
  const symbols = new Map(held.symbols
222
172
  .filter((symbol) => symbol.kind !== "file")
223
173
  .map((symbol) => [symbol.id, { id: symbol.id, ref: symbol.ref, position: symbol.position }]));
174
+ const placed = placedByPath(held, symbols);
224
175
  const found = [];
225
176
  for (const file of project.ownSourceFiles()) {
226
177
  const comments = lineComments(file).filter((comment) => CANDIDATE.test(comment.text));
227
178
  if (comments.length === 0) {
228
179
  continue;
229
180
  }
230
- const below = declarationLines(file, held, symbols, targetRoot);
181
+ const path = relativePath(targetRoot, file.fileName) ?? "";
182
+ const below = declarationLines(file, placed.get(path) ?? [], held, targetRoot);
231
183
  for (const comment of comments) {
232
184
  const site = renderPosition(file, targetRoot, comment.offset);
233
185
  const target = trails(file, comment.offset) ? site.line : site.line + 1;
@@ -4,9 +4,9 @@
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
- import { KIND_ROWS, SYMBOL_SUBJECTS, } from "./contract/contract.generated.js";
9
+ import { KIND_ROWS, REMEDIES, SYMBOL_SUBJECTS, } from "./contract/contract.generated.js";
10
10
  import { UsageError } from "./usage.js";
11
11
  import { positionKey } from "./position.js";
12
12
  import { isRef } from "./ref.js";
@@ -66,6 +66,8 @@ export function isSymbolRef(value) {
66
66
  export function blankReason(reason) {
67
67
  return reason === undefined || /^[ \t\r\n]*$/u.test(reason);
68
68
  }
69
+ /** No hold. */
70
+ const NO_HOLDS = { withholds: () => false, holdsRecord: () => false };
69
71
  /** The subject of the kind each live code names. */
70
72
  const SUBJECT_OF_CODE = new Map(KIND_ROWS.map((row) => [row.code, row.subject]));
71
73
  /** The subject a finding about one symbol kind is about. */
@@ -78,32 +80,25 @@ function subjectOf(kind) {
78
80
  function rowOf(subject) {
79
81
  return subject === "file" || subject === "dependency" || subject === "record";
80
82
  }
81
- /**
82
- * Whether a finding's subject is a row of a document rather than a declaration. Apart
83
- * from a dependency row, nothing withholds a finding about one.
84
- */
83
+ /** Whether a finding's subject is a row of a document rather than a declaration. */
85
84
  export function isRowSubject(kind) {
86
85
  return rowOf(subjectOf(kind));
87
86
  }
88
- /** Whether a code reports a row of a document. */
89
- export function isRowCode(code) {
90
- return rowOf(SUBJECT_OF_CODE.get(code));
87
+ /** The subject a code reports, which kinds.json's remedies table answers for. */
88
+ export function subjectOfCode(code) {
89
+ return SUBJECT_OF_CODE.get(code);
91
90
  }
92
91
  /**
93
- * Whether a finding's subject is a dependency row. An ignore entry or a baseline row
94
- * naming the row's reference and file withholds its finding and marks nothing, and an
95
- * inline directive never binds one.
92
+ * Whether a record of `mechanism` can name a finding about `subject`, as kinds.json's
93
+ * remedies table lists the suppressions of each subject. A subject the table does not
94
+ * know is a declaration's.
96
95
  */
97
- export function isDependencySubject(kind) {
98
- return subjectOf(kind) === "dependency";
99
- }
100
- /** Whether a code reports a dependency row, which only an ignore entry or a baseline row names. */
101
- export function isDependencyCode(code) {
102
- return SUBJECT_OF_CODE.get(code) === "dependency";
96
+ export function names(subject, mechanism) {
97
+ return REMEDIES[subject ?? "declaration"].suppressions.includes(mechanism);
103
98
  }
104
- /** Whether a code reports a row of a document that no record of any mechanism can name. */
105
- export function isUnsuppressibleCode(code) {
106
- return isRowCode(code) && !isDependencyCode(code);
99
+ /** Whether a finding about a symbol of `kind` is one a record of `mechanism` can name. */
100
+ export function namesKind(kind, mechanism) {
101
+ return names(subjectOf(kind), mechanism);
107
102
  }
108
103
  /**
109
104
  * Whether a finding's subject is a part of the declaration its reference names: a part
@@ -136,14 +131,13 @@ export function pairs(record, finding) {
136
131
  if (record.code !== finding.code) {
137
132
  return false;
138
133
  }
139
- if (isDependencySubject(finding.symbol.kind)) {
140
- return (record.mechanism !== "inline" &&
141
- record.symbol === finding.symbol.ref &&
142
- record.path === finding.position.path);
143
- }
144
- if (isRowSubject(finding.symbol.kind)) {
134
+ const subject = subjectOf(finding.symbol.kind);
135
+ if (!names(subject, record.mechanism)) {
145
136
  return false;
146
137
  }
138
+ if (subject === "dependency") {
139
+ return record.symbol === finding.symbol.ref && record.path === finding.position.path;
140
+ }
147
141
  if (record.bound !== "" && record.bound === positionKey(finding.position)) {
148
142
  return true;
149
143
  }
@@ -155,10 +149,10 @@ export function pairs(record, finding) {
155
149
  * there: swept with no mark for a liveness kind, and as swept otherwise. The first record
156
150
  * in reading order claims a finding, so a second naming it is stale; a part-kind record
157
151
  * claims every finding of its code about a part of its declaration. A record is dormant
158
- * when the dials withhold every finding it claims, read from those findings alone, or when
159
- * it claims none and its code's severity is `allow`.
152
+ * when the dials or `holds` withhold every finding it claims, or when it claims none and
153
+ * its code is at `allow` or a hold keeps its declaration live.
160
154
  */
161
- export function ledgerOf(records, would, dials) {
155
+ export function ledgerOf(records, would, dials, holds = NO_HOLDS) {
162
156
  const taken = new Set();
163
157
  const verdicts = [];
164
158
  for (const record of records) {
@@ -166,13 +160,17 @@ export function ledgerOf(records, would, dials) {
166
160
  const first = would.find(open);
167
161
  const claimed = first === undefined ? [] : isPartKind(record.code) ? would.filter(open) : [first];
168
162
  if (claimed.length === 0) {
169
- verdicts.push(record.bound !== "" && dials.allows(record.code) ? "dormant" : "stale");
163
+ verdicts.push(record.bound !== "" && (dials.allows(record.code) || holds.holdsRecord(record))
164
+ ? "dormant"
165
+ : "stale");
170
166
  continue;
171
167
  }
172
168
  for (const finding of claimed) {
173
169
  taken.add(claimKey(finding));
174
170
  }
175
- verdicts.push(claimed.every((finding) => dials.withholds(finding)) ? "dormant" : "in-effect");
171
+ verdicts.push(claimed.every((finding) => dials.withholds(finding) || holds.withholds(finding))
172
+ ? "dormant"
173
+ : "in-effect");
176
174
  }
177
175
  return {
178
176
  verdicts,
@@ -5,7 +5,7 @@
5
5
  * another such file, so it is judged with the tests: what it references is referenced by
6
6
  * test code.
7
7
  */
8
- import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./convention-rows.js";
8
+ import { CONVENTION_ROWS, TEST_LIBRARIES, TEST_LIBRARY_SCOPES } from "./entries/convention-rows.js";
9
9
  import { packageOfSpecifier } from "./dependencies.js";
10
10
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
11
11
  const SKIP_ROOT = "type-error";
@@ -14,7 +14,9 @@ const SKIP_ROOT = "type-error";
14
14
  * convention row reads test files for, and the convention table's test libraries.
15
15
  */
16
16
  const TEST_PACKAGES = new Set([
17
- ...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
17
+ ...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined ||
18
+ row.testDirectories !== undefined ||
19
+ row.testRunner !== undefined).map((row) => row.package),
18
20
  ...TEST_LIBRARIES,
19
21
  ]);
20
22
  /**
@@ -3,6 +3,7 @@ import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js
3
3
  import { kindOf } from "../config.js";
4
4
  import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
5
5
  import { analyzerProvenance, verdictOf } from "../findings-pass.js";
6
+ import { factsDocument } from "../facts.js";
6
7
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
7
8
  import { moduleIdentity } from "../load/discover.js";
8
9
  import { partialNotes } from "../partial.js";
@@ -21,7 +22,7 @@ import { diagnose, message } from "../product.js";
21
22
  import { ownVersion } from "./version.js";
22
23
  /** The options `analyze` takes beside the ones every verb takes. */
23
24
  export const ANALYZE_OPTIONS = {
24
- plain: ["report", "baseline-write", "exit-code", "template"],
25
+ plain: ["report", "facts", "baseline-write", "exit-code", "template"],
25
26
  repeatable: ["format"],
26
27
  };
27
28
  /** The two values `--exit-code` takes; on, the default, puts the run's verdict in the exit code. */
@@ -211,8 +212,13 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
211
212
  const { config, provenance } = resolve(inputs());
212
213
  const invoked = host.workingDirectory();
213
214
  const baseline = option("baseline-write");
215
+ const facts = option("facts");
216
+ if (facts === "") {
217
+ throw new UsageError("--facts names the path the facts document is written to, and named none");
218
+ }
214
219
  const documents = {
215
220
  report: resolvePath(invoked, reportPath),
221
+ facts: facts === undefined ? undefined : resolvePath(invoked, facts),
216
222
  formats: formatsOf(repeated("format"), config.reporters.formats),
217
223
  baseline: baseline === undefined ? undefined : resolvePath(invoked, baseline),
218
224
  };
@@ -281,6 +287,12 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
281
287
  throw productFault(`built a report that breaks the report schema at ${refused.pointer || "the document"}: ${refused.keyword}: ${refused.reason}`);
282
288
  }
283
289
  writeDocument(host, documents.report, "name a path the run can write", () => json(report));
290
+ const factsPath = documents.facts;
291
+ if (factsPath !== undefined) {
292
+ writeDocument(host, factsPath, "name a path the run can write", () => [
293
+ factsDocument(version, run.generated),
294
+ ]);
295
+ }
284
296
  const options = {
285
297
  readSource: (path) => host.readFile(joinPath(targetRoot, path)),
286
298
  template,
@@ -1,12 +1,13 @@
1
- import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_USAGE, SCHEMA_VERSIONS, } from "../contract/contract.generated.js";
1
+ import { CONTRACT_VERSION, EXIT_CLEAN, EXIT_USAGE, FACTS_VERSION, SCHEMA_VERSIONS, } from "../contract/contract.generated.js";
2
2
  import { CONFORMANCE } from "../conformance.js";
3
3
  import {} from "./verb.js";
4
4
  import { diagnose, LANGUAGE, PRODUCT } from "../product.js";
5
5
  import { ownVersion } from "./version.js";
6
6
  /**
7
7
  * The document `describe` writes: the analyzer, the Contract version it implements, the
8
- * report schema versions it reads, the languages it claims, and its recorded result over
9
- * the conformance corpus, a member the document omits where no run is recorded.
8
+ * report schema versions it reads, the languages it claims, the facts version it writes,
9
+ * and its recorded result over the conformance corpus, a member the document omits where
10
+ * no run is recorded.
10
11
  */
11
12
  export function describeDocument(version, recorded) {
12
13
  const document = {
@@ -15,6 +16,7 @@ export function describeDocument(version, recorded) {
15
16
  contract_version: CONTRACT_VERSION,
16
17
  schema_versions_accepted: SCHEMA_VERSIONS,
17
18
  languages: [LANGUAGE],
19
+ facts_version_written: FACTS_VERSION,
18
20
  ...(recorded === undefined
19
21
  ? {}
20
22
  : {
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.14",
3
+ "version": "8.0.0-dev.16",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.14",
3
+ "version": "8.0.0-dev.16",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -32,7 +32,7 @@
32
32
  "lint:fix": "eslint . --fix && prettier --write ."
33
33
  },
34
34
  "dependencies": {
35
- "@typescript/native": "npm:typescript@7.1.0-dev.20261029.1"
35
+ "@typescript/native": "npm:typescript@7.1.0-dev.20261103.1"
36
36
  },
37
37
  "overrides": {
38
38
  "qs": "^6.15.2"
@@ -43,7 +43,7 @@
43
43
  "@stryker-mutator/typescript-checker": "10.1.1",
44
44
  "@stryker-mutator/vitest-runner": "10.1.1",
45
45
  "@types/node": "24.19.2",
46
- "eslint": "10.13.0",
46
+ "eslint": "10.14.0",
47
47
  "fast-check": "4.10.2",
48
48
  "prettier": "3.9.10",
49
49
  "typescript": "npm:@typescript/typescript6@6.0.2",
@@ -14,7 +14,7 @@ import {
14
14
  isNamespaceImport,
15
15
  } from "@typescript/native/ast";
16
16
  import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/sync";
17
- import { declarationsByName, isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
17
+ import { isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
18
18
  import { UNANSWERED, type Answer } from "./query.ts";
19
19
  import type { ProjectView } from "./session.ts";
20
20
 
@@ -132,7 +132,7 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
132
132
  names.add(node.expression.text);
133
133
  }
134
134
  }
135
- const byName = declarationsByName(held);
135
+ const byName = held.byName;
136
136
  const ids = [...names].flatMap((name) => byName.get(name) ?? []);
137
137
  return [...new Set([...ids, ...(module ? files : [])])].map((id) => ({
138
138
  id,