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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Which types of the target each of its declarations hands to code outside the package:
3
+ * the types each declaration a reference from outside reaches names, as written or as the
4
+ * checker reads it, as the precondition of `DS1501` in the Contract's kinds reads that
5
+ * reach.
6
+ */
7
+ import { SignatureKind, SymbolFlags, } from "@typescript/native/sync";
8
+ import { isArrowFunction, isAsExpression, isBindingElement, isCallSignatureDeclaration, isClassExpression, isConstructorTypeNode, isConstructSignatureDeclaration, isDecorator, isEnumMember, isFunctionExpression, isFunctionTypeNode, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isMethodSignatureDeclaration, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAssignment, isPropertyDeclaration, isPropertySignatureDeclaration, isSatisfiesExpression, isSetAccessorDeclaration, isSpreadAssignment, isTypeAssertion, isTypeReferenceNode, } from "@typescript/native/ast";
9
+ import { aliasChains } from "./alias-chain.js";
10
+ import { OUTSIDE } from "./liveness/graph.js";
11
+ import { nodeKey } from "./inventory.js";
12
+ import { isAnswered } from "./query.js";
13
+ import { typeNamesOf } from "./roots/published-reach.js";
14
+ /** The declarations whose values a reference holds without naming them. */
15
+ const TYPES = new Set(["class", "interface", "type", "enum"]);
16
+ /** The symbol flags of a type the source spells as a literal, whose members are its own. */
17
+ const LITERAL = SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral;
18
+ /** The members a type's definition names other types through, where exported. */
19
+ const MEMBERS = new Set([
20
+ "method",
21
+ "class-member",
22
+ "interface-method",
23
+ "type-member",
24
+ ]);
25
+ /** The declarations of a module or a namespace whose signature or type hands values on. */
26
+ const CARRYING = new Set(["function", "variable", ...TYPES]);
27
+ /** The containers whose own declarations a reference from outside can reach. */
28
+ const SCOPES = new Set(["file", "namespace"]);
29
+ /** What a project whose carriers no rule reads carries: nothing. */
30
+ export const NO_CARRIERS = { named: new Map(), unread: new Set() };
31
+ /** Whether a member is named from outside its type: neither `private` nor a private name. */
32
+ function exportedMember(symbol) {
33
+ return symbol.visibility === "public" || symbol.visibility === "protected";
34
+ }
35
+ /**
36
+ * The declarations of one project that carry values out: a function, a variable or a type
37
+ * a module or a namespace declares, and an exported member of a class, an interface or a
38
+ * type. Neither is filtered by its export: only a declaration the reach meets passes it
39
+ * on, and an unexported type an exported function returns passes it to what it names.
40
+ */
41
+ function carrying(held) {
42
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
43
+ return held.symbols.filter((symbol) => {
44
+ const parent = byId.get(symbol.parent);
45
+ if (MEMBERS.has(symbol.kind)) {
46
+ return parent !== undefined && TYPES.has(parent.kind) && exportedMember(symbol);
47
+ }
48
+ return CARRYING.has(symbol.kind) && parent !== undefined && SCOPES.has(parent.kind);
49
+ });
50
+ }
51
+ /** The node a declaration's symbol is resolved at: its name, or itself where it has none. */
52
+ function nameNode(node) {
53
+ return node.name ?? node;
54
+ }
55
+ /** Whether a value expression is one whose type the declaration output prints as written. */
56
+ function printedAsWritten(node) {
57
+ return (isParenthesizedExpression(node) ||
58
+ isAsExpression(node) ||
59
+ isTypeAssertion(node) ||
60
+ isSatisfiesExpression(node) ||
61
+ isObjectLiteralExpression(node) ||
62
+ isArrowFunction(node) ||
63
+ isFunctionExpression(node) ||
64
+ isClassExpression(node));
65
+ }
66
+ /** Whether a type node is the `const` of a const assertion, which names no type. */
67
+ function isConstName(node) {
68
+ return isTypeReferenceNode(node) && isIdentifier(node.typeName) && node.typeName.text === "const";
69
+ }
70
+ /**
71
+ * Whether the declaration output prints a type or a member it meets by this declaration's
72
+ * written types: a function type's, a signature's, a member's, or an expression's that
73
+ * {@link printedAsWritten} holds. A type literal or a mapped type it prints from the
74
+ * checker's members, so only each member's own declaration is read.
75
+ */
76
+ function reused(node) {
77
+ return (isFunctionTypeNode(node) ||
78
+ isConstructorTypeNode(node) ||
79
+ isPropertySignatureDeclaration(node) ||
80
+ isMethodSignatureDeclaration(node) ||
81
+ isCallSignatureDeclaration(node) ||
82
+ isConstructSignatureDeclaration(node) ||
83
+ isPropertyAssignment(node) ||
84
+ isPropertyDeclaration(node) ||
85
+ isMethodDeclaration(node) ||
86
+ isGetAccessorDeclaration(node) ||
87
+ isSetAccessorDeclaration(node) ||
88
+ printedAsWritten(node));
89
+ }
90
+ /**
91
+ * Whether the declaration output leaves out the types one node of a declaration writes.
92
+ * It prints an initializer only where its declaration has no annotation, and then as the
93
+ * checker types it, except through what {@link printedAsWritten} holds: an assertion's
94
+ * type in place of its operand, a function's or a class's signatures, an object literal's
95
+ * properties, and the operand of a `satisfies`. A decorator and an enum member's or a
96
+ * binding element's initializer are not printed.
97
+ */
98
+ function unprinted(node) {
99
+ const parent = node.parent;
100
+ if (parent === undefined) {
101
+ return false;
102
+ }
103
+ if (isDecorator(node)) {
104
+ return true;
105
+ }
106
+ const held = parent;
107
+ if (held.initializer === node) {
108
+ return (isEnumMember(parent) ||
109
+ isBindingElement(parent) ||
110
+ held.type !== undefined ||
111
+ !printedAsWritten(node));
112
+ }
113
+ if (isAsExpression(parent) || isTypeAssertion(parent)) {
114
+ return isConstName(parent.type)
115
+ ? node === parent.type || !printedAsWritten(node)
116
+ : node === parent.expression;
117
+ }
118
+ if (isSatisfiesExpression(parent)) {
119
+ return node === parent.type || !printedAsWritten(node);
120
+ }
121
+ if (isParenthesizedExpression(parent) || isSpreadAssignment(parent)) {
122
+ return !printedAsWritten(node);
123
+ }
124
+ return false;
125
+ }
126
+ /**
127
+ * The declarations of the target each declaration of one project carries: those it names
128
+ * as written, and those its type names as the checker reads it, so an inferred result names
129
+ * its type as an annotation does. The walk reads a type's constituents, type arguments,
130
+ * alias arguments, `partsOf`, literal members and index signatures, and each signature's
131
+ * parameters, result and `signaturePartsOf`. A type of the target ends the walk, since its
132
+ * own definition is read as its own.
133
+ */
134
+ export function carriersOf(project, held) {
135
+ const { queries } = project;
136
+ const chains = aliasChains(project, held);
137
+ const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
138
+ const parents = new Map(held.symbols.map((symbol) => [symbol.id, symbol.parent]));
139
+ const wanted = new Set(carrying(held).map((symbol) => symbol.id));
140
+ const declared = [];
141
+ // A nested carrier and any other member of a type are read on that member, not on the
142
+ // declaration that holds it; a type parameter is part of its declaration.
143
+ const readAloneIn = (file) => (nested) => {
144
+ const id = held.declarations.get(nodeKey(file, nested));
145
+ const kind = id === undefined ? undefined : kinds.get(id);
146
+ return (unprinted(nested) ||
147
+ (id !== undefined &&
148
+ kind !== "type-parameter" &&
149
+ (wanted.has(id) || TYPES.has(kinds.get(parents.get(id) ?? "") ?? "file"))));
150
+ };
151
+ for (const file of project.ownSourceFiles()) {
152
+ const readAlone = readAloneIn(file);
153
+ const visit = (node) => {
154
+ const id = held.declarations.get(nodeKey(file, node));
155
+ if (id !== undefined && wanted.has(id)) {
156
+ declared.push({
157
+ id,
158
+ handle: project.handle(nameNode(node)),
159
+ written: typeNamesOf(node, readAlone).map((name) => project.handle(name)),
160
+ });
161
+ }
162
+ node.forEachChild(visit);
163
+ };
164
+ file.forEachChild(visit);
165
+ }
166
+ const unread = new Set();
167
+ let reading = "";
168
+ const answer = (found, otherwise) => {
169
+ if (isAnswered(found)) {
170
+ return found;
171
+ }
172
+ unread.add(reading);
173
+ return otherwise;
174
+ };
175
+ const typesOf = (symbol) => chains.declarationsOf(symbol).filter((id) => TYPES.has(kinds.get(id) ?? "file"));
176
+ // The declaration output prints an anonymous type or a literal's member by its own
177
+ // declaration's written types where it has them, which name an alias the checker erases,
178
+ // so the walk of one declaration keeps the target's own declarations of those it meets.
179
+ const own = project.ownPaths();
180
+ let meeting = new Map();
181
+ const meet = (symbol) => {
182
+ for (const handle of symbol.declarations) {
183
+ if (own.has(handle.path)) {
184
+ meeting.set(`${handle.path}#${String(handle.index)}`, handle);
185
+ }
186
+ }
187
+ };
188
+ const signatures = (type, into, seen) => {
189
+ for (const kind of [SignatureKind.Call, SignatureKind.Construct]) {
190
+ for (const signature of answer(queries.signaturesOf(type, kind), [])) {
191
+ signature.parameters.forEach((_parameter, index) => {
192
+ walk(answer(queries.parameterType(signature, index), undefined), into, seen);
193
+ });
194
+ walk(answer(queries.returnTypeOf(signature), undefined), into, seen);
195
+ for (const part of answer(queries.signaturePartsOf(signature), [])) {
196
+ walk(part, into, seen);
197
+ }
198
+ }
199
+ }
200
+ };
201
+ // The parts of a type that spell other types, not counting the type itself.
202
+ const structure = (type, into, seen) => {
203
+ for (const part of answer(queries.constituents(type), [])) {
204
+ if (part.id !== type.id) {
205
+ walk(part, into, seen);
206
+ }
207
+ }
208
+ for (const argument of answer(queries.typeArguments(type), [])) {
209
+ walk(argument, into, seen);
210
+ }
211
+ for (const part of answer(queries.partsOf(type), [])) {
212
+ walk(part, into, seen);
213
+ }
214
+ const symbol = answer(queries.symbolOfType(type), undefined);
215
+ if (symbol !== undefined && (symbol.flags & LITERAL) !== 0) {
216
+ for (const property of answer(queries.propertiesOf(type), [])) {
217
+ meet(property);
218
+ walk(answer(queries.typeOfSymbol(property), undefined), into, seen);
219
+ }
220
+ for (const info of answer(queries.indexInfosOf(type), [])) {
221
+ walk(info.valueType, into, seen);
222
+ }
223
+ }
224
+ signatures(type, into, seen);
225
+ };
226
+ const walk = (type, into, seen) => {
227
+ if (type === undefined || seen.has(type.id)) {
228
+ return;
229
+ }
230
+ seen.add(type.id);
231
+ const alias = answer(queries.aliasSymbolOf(type), undefined);
232
+ if (alias !== undefined) {
233
+ for (const id of typesOf(alias)) {
234
+ into.add(id);
235
+ }
236
+ for (const argument of answer(queries.aliasArgumentsOf(type), [])) {
237
+ walk(argument, into, seen);
238
+ }
239
+ if (typesOf(alias).length > 0) {
240
+ return;
241
+ }
242
+ }
243
+ const symbol = answer(queries.symbolOfType(type), undefined);
244
+ const named = symbol === undefined ? [] : typesOf(symbol);
245
+ if (named.length > 0) {
246
+ for (const id of named) {
247
+ into.add(id);
248
+ }
249
+ for (const argument of answer(queries.typeArguments(type), [])) {
250
+ walk(argument, into, seen);
251
+ }
252
+ return;
253
+ }
254
+ if (symbol !== undefined) {
255
+ meet(symbol);
256
+ }
257
+ structure(type, into, seen);
258
+ };
259
+ // What one declaration's checker type names, by its kind.
260
+ const definition = (symbol, kind, into) => {
261
+ const seen = new Set();
262
+ // A class's or an interface's own definition: its properties are members, which carry
263
+ // on their own, so only its bases, signatures and index signatures are read here.
264
+ const own = (type) => {
265
+ for (const info of answer(queries.indexInfosOf(type), [])) {
266
+ walk(info.valueType, into, seen);
267
+ }
268
+ signatures(type, into, seen);
269
+ };
270
+ if (kind === "class" || kind === "interface") {
271
+ const instance = answer(queries.declaredTypeOf(symbol), undefined);
272
+ const statics = kind === "class" ? answer(queries.typeOfSymbol(symbol), undefined) : undefined;
273
+ const sides = [instance, statics].filter((side) => side !== undefined);
274
+ for (const side of sides) {
275
+ seen.add(side.id);
276
+ }
277
+ for (const base of instance === undefined ? [] : answer(queries.baseTypes(instance), [])) {
278
+ walk(base, into, seen);
279
+ }
280
+ sides.forEach(own);
281
+ }
282
+ else if (kind === "type") {
283
+ const aliased = answer(queries.declaredTypeOf(symbol), undefined);
284
+ if (aliased !== undefined) {
285
+ seen.add(aliased.id);
286
+ structure(aliased, into, seen);
287
+ }
288
+ }
289
+ else if (kind !== "enum") {
290
+ walk(answer(queries.typeOfSymbol(symbol), undefined), into, seen);
291
+ }
292
+ };
293
+ const symbols = project.symbolsAt(declared.map((one) => one.handle));
294
+ const spelled = project.symbolsAt(declared.flatMap((one) => one.written));
295
+ const reached = [];
296
+ const add = (answered, into) => {
297
+ const name = answer(answered, undefined);
298
+ for (const target of name === undefined ? [] : chains.chainOf(name)) {
299
+ if (TYPES.has(kinds.get(target.id) ?? "file")) {
300
+ into.add(target.id);
301
+ }
302
+ }
303
+ };
304
+ let from = 0;
305
+ declared.forEach(({ id, written }, index) => {
306
+ reading = id;
307
+ const into = new Set();
308
+ for (const answered of spelled.slice(from, from + written.length)) {
309
+ add(answered, into);
310
+ }
311
+ from += written.length;
312
+ meeting = new Map();
313
+ const symbol = answer(symbols[index], undefined);
314
+ if (symbol !== undefined) {
315
+ definition(symbol, kinds.get(id), into);
316
+ }
317
+ reached.push({ id, into, met: [...meeting.values()] });
318
+ });
319
+ const namesAt = new Map();
320
+ const writtenAt = (handle) => {
321
+ const key = `${handle.path}#${String(handle.index)}`;
322
+ let names = namesAt.get(key);
323
+ if (names === undefined) {
324
+ const node = project.declarationAt(handle)?.node;
325
+ names =
326
+ node === undefined || !reused(node)
327
+ ? []
328
+ : typeNamesOf(node, readAloneIn(node.getSourceFile())).map((name) => project.handle(name));
329
+ namesAt.set(key, names);
330
+ }
331
+ return names;
332
+ };
333
+ const metNames = reached.map((one) => one.met.flatMap(writtenAt));
334
+ const metSpelled = project.symbolsAt(metNames.flat());
335
+ const named = new Map();
336
+ from = 0;
337
+ reached.forEach(({ id, into }, index) => {
338
+ reading = id;
339
+ const count = metNames[index]?.length ?? 0;
340
+ for (const answered of metSpelled.slice(from, from + count)) {
341
+ add(answered, into);
342
+ }
343
+ from += count;
344
+ into.delete(id);
345
+ named.set(id, [...into]);
346
+ });
347
+ return { named, unread };
348
+ }
349
+ /**
350
+ * The types whose values a reference from outside the package reaches through the
351
+ * package's own declarations, or every type where such a reference reaches a declaration
352
+ * whose surface the checker left unread. `outside` flags each declaration of the graph a
353
+ * reference from outside names; each passes the reach on to what it carries, a type to
354
+ * each member it exports, a namespace to each declaration it exports and a member to its
355
+ * type, until no declaration joins.
356
+ */
357
+ export function exposedTypes(graph, outside, carriers) {
358
+ const { symbols, parent } = graph;
359
+ const members = symbols.map(() => []);
360
+ symbols.forEach((symbol, at) => {
361
+ const container = parent[at] ?? OUTSIDE;
362
+ const holder = symbols[container];
363
+ if (holder !== undefined &&
364
+ ((TYPES.has(holder.kind) && MEMBERS.has(symbol.kind) && exportedMember(symbol)) ||
365
+ (holder.kind === "namespace" && symbol.exported))) {
366
+ members[container]?.push(at);
367
+ }
368
+ });
369
+ const reached = symbols.map((_symbol, at) => outside[at] === true);
370
+ const queue = reached.flatMap((one, at) => (one ? [at] : []));
371
+ const join = (at) => {
372
+ if (at !== OUTSIDE && reached[at] !== true) {
373
+ reached[at] = true;
374
+ queue.push(at);
375
+ }
376
+ };
377
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
378
+ const symbol = symbols[at];
379
+ if (symbol === undefined) {
380
+ continue;
381
+ }
382
+ if (carriers?.unread.has(symbol.id) === true) {
383
+ return "every";
384
+ }
385
+ for (const id of carriers?.named.get(symbol.id) ?? []) {
386
+ join(graph.at(id));
387
+ }
388
+ for (const member of members[at] ?? []) {
389
+ join(member);
390
+ }
391
+ if (MEMBERS.has(symbol.kind)) {
392
+ join(parent[at] ?? OUTSIDE);
393
+ }
394
+ }
395
+ return new Set(symbols.flatMap((symbol, at) => reached[at] === true && TYPES.has(symbol.kind) ? [symbol.id] : []));
396
+ }
397
+ /** Whether code outside the package holds values of a type through the package's declarations. */
398
+ export function exposes(exposed, symbol) {
399
+ return TYPES.has(symbol.kind) && (exposed === "every" || exposed.has(symbol.id));
400
+ }
@@ -4,9 +4,9 @@
4
4
  * together with every finding that falls with a root they withhold.
5
5
  */
6
6
  import { CODE, FAMILY_KEY_LENGTH, KIND_ROWS, } from "../contract/contract.generated.js";
7
- import { componentId } from "../components.js";
8
- import { deadComponents } from "../matrix.js";
9
- import { OUTSIDE } from "../graph.js";
7
+ import { componentId } from "../liveness/components.js";
8
+ import { deadComponents } from "../liveness/sweep.js";
9
+ import { OUTSIDE } from "../liveness/graph.js";
10
10
  import { KINDS } from "../kinds.js";
11
11
  import { byPosition, positionKey } from "../position.js";
12
12
  import { declarationKey } from "../ref.js";
@@ -82,7 +82,7 @@ function testedByIn(input) {
82
82
  * consumer set. A member an `in` expression tests by name, one a function outside the
83
83
  * analysis fills, and one {@link possiblyLoaded} names, is `possible`.
84
84
  */
85
- function reachabilityClasses(input) {
85
+ export function reachabilityClasses(input) {
86
86
  const tested = testedByIn(input);
87
87
  const filled = input.boundary.filledOutside ?? new Set();
88
88
  const possible = possiblyLoaded(input);
@@ -100,19 +100,6 @@ function reachabilityClasses(input) {
100
100
  return published.has(id) ? outside : "certain";
101
101
  };
102
102
  }
103
- /**
104
- * Whether one test of dead code references a target symbol that is published API of a run
105
- * that declares no consumer, which outside programs the run cannot see may use, so the
106
- * target counts as live for the test and the test is no test of dead code.
107
- */
108
- export function testsUnseenAPI(input) {
109
- if (input.boundary.consumers.declared.length > 0) {
110
- return () => false;
111
- }
112
- const union = input.swept.matrix.union;
113
- const published = publishedIds(union);
114
- return (id) => testTargets(union, id).some((target) => published.has(target));
115
- }
116
103
  /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
117
104
  function testTargets(union, id) {
118
105
  return (union.out[union.at(id)] ?? []).flatMap((edge) => {
@@ -1,5 +1,4 @@
1
1
  import { evaluateEdges } from "../edges.js";
2
- import { liveFilesAt } from "../matrix.js";
3
2
  import { declarationKey } from "../ref.js";
4
3
  import { ledgerOf } from "../suppress.js";
5
4
  import { insideSkipped } from "../type-errors.js";
@@ -8,7 +7,7 @@ import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machin
8
7
  import { interfaces } from "./interfaces.js";
9
8
  import { intraFunction } from "./intra-function.js";
10
9
  import { nonCodeArtifacts } from "./non-code-artifacts.js";
11
- import { readsAndWrites } from "./reads-and-writes.js";
10
+ import { fallingWithWrites, readsAndWrites } from "./reads-and-writes.js";
12
11
  import { selfCheck } from "./self-check.js";
13
12
  import { fallenMembers, unusedDeclarations } from "./unused-declarations.js";
14
13
  import { visibilityNarrowing } from "./visibility-narrowing.js";
@@ -23,37 +22,25 @@ export const EMITTERS = new Map([
23
22
  ["self-check", selfCheck],
24
23
  ["intra-function", intraFunction],
25
24
  ]);
26
- /**
27
- * The keys of every declaration an unanswered question could have kept live, which the run
28
- * reports nothing about: a finding about one would be computed from the gap.
29
- */
30
- function heldKeys(input) {
31
- const held = new Set(input.swept.heldByUnanswered);
32
- if (held.size === 0) {
33
- return held;
34
- }
25
+ /** The keys of the declarations the identifiers name, as {@link findingKey} keys a finding. */
26
+ function keysOf(input, ids) {
27
+ const named = new Set(ids);
35
28
  return new Set(input.swept.matrix.union.symbols
36
- .filter((symbol) => held.has(symbol.id))
29
+ .filter((symbol) => named.has(symbol.id))
37
30
  .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
38
31
  }
32
+ /** The key of the declaration a finding is about, or of the one holding the part it is about. */
33
+ function findingKey(finding) {
34
+ return declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
35
+ }
39
36
  /**
40
- * The keys of every top-level declaration of a live component file. The file's
41
- * markup may use each of them, which no analysis of the markup can rule out, so the run
42
- * reports nothing about one while the file is live.
37
+ * Every emitter's findings over one sweep, and those of any further emitter, completed by
38
+ * one step, but none about a declaration that falls with a write-only subject: that
39
+ * subject's finding names it, so no finding about it exists for a record to match.
43
40
  */
44
- function markupKeys(input) {
45
- const { swept } = input;
46
- if (swept.componentFiles.length === 0) {
47
- return new Set();
48
- }
49
- const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
50
- return new Set(swept.matrix.union.symbols
51
- .filter((symbol) => live.has(symbol.parent))
52
- .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
53
- }
54
- /** Every emitter's findings over one sweep, and those of any further emitter, completed by one step. */
55
41
  function completedOver(input, ...more) {
56
- return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
42
+ const falls = keysOf(input, fallingWithWrites(input));
43
+ return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input))).filter((finding) => !falls.has(findingKey(finding)));
57
44
  }
58
45
  /**
59
46
  * The dials over the findings a record could name, each read against the sweep that
@@ -96,13 +83,15 @@ function atPossible(input) {
96
83
  */
97
84
  function dialledOver(input, suppressed, found, unmarked) {
98
85
  const would = [...unmarked, ...found];
99
- const unanswered = heldKeys(input);
100
- const heldIds = new Set(input.swept.heldByUnanswered);
101
- const keyOf = (finding) => declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
102
- const held = (finding) => unanswered.has(keyOf(finding)) || insideSkipped(input.swept.skipped, finding.position);
86
+ const { sweep } = input.swept;
87
+ // A hold is a gap of the analysis, which leaves a record it touches dormant; a live
88
+ // component file's markup is no gap, so a record on one of its bindings stays in effect.
89
+ const unanswered = keysOf(input, sweep.held);
90
+ const markup = keysOf(input, sweep.bindings);
91
+ const heldIds = new Set(sweep.held);
92
+ const held = (finding) => unanswered.has(findingKey(finding)) || insideSkipped(input.swept.skipped, finding.position);
103
93
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked), { withholds: held, holdsRecord: (record) => heldIds.has(record.bound) });
104
- const markup = markupKeys(input);
105
- const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(keyOf(finding)));
94
+ const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(findingKey(finding)));
106
95
  const kept = reportable(input, candidates);
107
96
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
108
97
  return {
@@ -2,7 +2,7 @@ import { CODE } from "../contract/contract.generated.js";
2
2
  import { KINDS } from "../kinds.js";
3
3
  import { parameterRef } from "../ref.js";
4
4
  import {} from "../intra-function-parts.js";
5
- import { reachedWithTheTests } from "../sweep.js";
5
+ import { reachedWithTheTests } from "../liveness/sweep.js";
6
6
  /** A list of rules as a sentence names them. */
7
7
  function spelled(rules) {
8
8
  if (rules.length < 2) {
@@ -84,8 +84,8 @@ export const intraFunction = (input) => {
84
84
  const union = input.swept.matrix.union;
85
85
  const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
86
86
  const reached = reachedWithTheTests(union, [
87
- ...input.swept.exempt.map((record) => record.id),
88
- ...input.swept.heldByUnanswered,
87
+ ...input.swept.sweep.exempt.map((record) => record.id),
88
+ ...input.swept.sweep.held,
89
89
  ]);
90
90
  const testsOfDeadCode = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
91
91
  const judged = (id) => {
@@ -130,8 +130,14 @@ function fileNeverImported(input) {
130
130
  : fileFinding(CODE.fileNeverImported, file, "no import reaches this file and no root names it or a declaration in it");
131
131
  });
132
132
  }
133
- /** The findings of the files family, `DS1600` to `DS1699`. */
134
- export const nonCodeArtifacts = (input) => [
135
- ...fileNeverBuilt(input),
136
- ...fileNeverImported(input),
137
- ];
133
+ /**
134
+ * The findings of the files family, `DS1600` to `DS1699`, each at the class `possible` in a
135
+ * package a command of which may run a file whose path no word of it writes, since that
136
+ * command may run the file (confidence.35).
137
+ */
138
+ export const nonCodeArtifacts = (input) => {
139
+ const unwritten = [...(input.files.unwrittenRuns ?? [])];
140
+ return [...fileNeverBuilt(input), ...fileNeverImported(input)].map((finding) => unwritten.some((dir) => dir === "." || finding.position.path === dir || finding.position.path.startsWith(`${dir}/`))
141
+ ? { ...finding, reachabilityClass: "possible" }
142
+ : finding);
143
+ };