@cplieger/deadset-ts 3.0.0 → 3.2.0

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.
@@ -1,5 +1,5 @@
1
- import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isForInStatement, isForOfStatement, isNamespaceExport, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
2
- import { SymbolFlags } from "@typescript/native/unstable/sync";
1
+ import { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isDeleteExpression, isElementAccessExpression, isExportDeclaration, isExternalModuleReference, isForInStatement, isForOfStatement, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
2
+ import { aliasChains } from "./alias-chain.js";
3
3
  import { globExpression } from "./glob.js";
4
4
  import { nodeKey } from "./inventory.js";
5
5
  import { byPosition, renderPosition } from "./position.js";
@@ -126,6 +126,38 @@ function markStores(node, writes) {
126
126
  }
127
127
  }
128
128
  }
129
+ /** Whether one node is a string literal or a template literal with no substitution. */
130
+ function isLiteralText(node) {
131
+ return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
132
+ }
133
+ /**
134
+ * The module specifier of the module one node evaluates, if it evaluates one: an import
135
+ * declaration, an import-equals declaration of an external module, an export
136
+ * declaration that names a module, and an `import()` call whose argument is literal
137
+ * text. A type-only form evaluates nothing. An `import()` of a computed argument names
138
+ * no module this pass can know.
139
+ */
140
+ function evaluatedBy(node) {
141
+ if (isImportDeclaration(node)) {
142
+ return node.importClause?.phaseModifier === SyntaxKind.TypeKeyword
143
+ ? undefined
144
+ : node.moduleSpecifier;
145
+ }
146
+ if (isExportDeclaration(node)) {
147
+ return node.isTypeOnly ? undefined : node.moduleSpecifier;
148
+ }
149
+ if (isImportEqualsDeclaration(node)) {
150
+ const reference = node.moduleReference;
151
+ return !node.isTypeOnly && isExternalModuleReference(reference)
152
+ ? reference.expression
153
+ : undefined;
154
+ }
155
+ if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
156
+ const [argument] = node.arguments;
157
+ return isLiteralText(argument) ? argument : undefined;
158
+ }
159
+ return undefined;
160
+ }
129
161
  /**
130
162
  * The re-exports one node writes, if it is an export declaration. A specifier and a
131
163
  * namespace export are declarations whose names are where they are written, so no use is
@@ -174,6 +206,7 @@ function linksOf(file, node, declarations, enclosing) {
174
206
  function sitesOf(file, declarations, fileId) {
175
207
  const found = [];
176
208
  const links = [];
209
+ const evaluations = [];
177
210
  const writes = new Set();
178
211
  const declared = new Set();
179
212
  const shorthands = new Map();
@@ -210,6 +243,10 @@ function sitesOf(file, declarations, fileId) {
210
243
  markNames(node);
211
244
  markStores(node, writes);
212
245
  links.push(...linksOf(file, node, declarations, enclosing));
246
+ const evaluated = evaluatedBy(node);
247
+ if (evaluated !== undefined) {
248
+ evaluations.push({ node: evaluated, from: enclosing });
249
+ }
213
250
  const held = declarations.get(nodeKey(file, node));
214
251
  const outer = enclosing;
215
252
  if (held !== undefined) {
@@ -219,7 +256,7 @@ function sitesOf(file, declarations, fileId) {
219
256
  enclosing = outer;
220
257
  };
221
258
  file.forEachChild(visit);
222
- return { uses: found, links };
259
+ return { uses: found, links, evaluations };
223
260
  }
224
261
  /**
225
262
  * The name one test-file rule takes. A rule's name is a word list the report's own
@@ -247,7 +284,8 @@ function compare(a, b) {
247
284
  * The order is by the referencing position, then by the declaration named, the
248
285
  * declaration referencing it and the use. That is a total order because one name
249
286
  * references one declaration once from one declaration, while a chain of aliases reaches
250
- * several declarations at one position.
287
+ * several declarations at one position and a bare star re-export's specifier both reads
288
+ * and evaluates the module it names.
251
289
  */
252
290
  export function references(project, held, targetRoot, options) {
253
291
  const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
@@ -256,85 +294,15 @@ export function references(project, held, targetRoot, options) {
256
294
  const matched = options.testFiles.map(() => 0);
257
295
  const found = [];
258
296
  const tests = [];
259
- const reached = new Map();
260
- const steps = new Map();
297
+ const chains = aliasChains(project, held);
261
298
  let batched = 0;
262
299
  let fileBatches = 0;
263
300
  let residueFallbacks = 0;
264
301
  let shorthandLookups = 0;
265
- let aliasSteps = 0;
266
302
  const files = project.ownSourceFiles();
267
- const ownFiles = new Set(files.map((file) => file.fileName));
303
+ const fileIds = new Set(held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id));
268
304
  /** The identifier of the declaration one node of this program is, where it is one. */
269
305
  const declaredAt = (node) => declarations.get(nodeKey(node.getSourceFile(), node));
270
- /**
271
- * The declarations of the inventory one symbol is.
272
- *
273
- * A declaration handle carries the file it is in, so the question is answered before
274
- * the handle is resolved: a declaration outside the project's own files belongs to
275
- * another program and is never read, which is what keeps the pass from fetching a
276
- * library file to discover that the inventory does not hold what is in it.
277
- */
278
- const declarationsOf = (symbol) => {
279
- const ids = [];
280
- for (const handle of symbol.declarations) {
281
- if (!ownFiles.has(handle.path)) {
282
- continue;
283
- }
284
- const node = project.declarationAt(handle)?.node;
285
- const id = node === undefined ? undefined : declaredAt(node);
286
- if (id !== undefined && !ids.includes(id)) {
287
- ids.push(id);
288
- }
289
- }
290
- return ids;
291
- };
292
- /**
293
- * What one alias names, one link along, asked once per alias whatever the number of
294
- * chains it is a link of. Only a symbol the binder flagged an alias is asked, which is
295
- * the condition the step asserts on: a default export of an expression is written in
296
- * an alias form and names nothing declared elsewhere, so its chain ends at itself.
297
- */
298
- const stepOf = (symbol) => {
299
- if ((symbol.flags & SymbolFlags.Alias) === 0) {
300
- return undefined;
301
- }
302
- if (steps.has(symbol.id)) {
303
- return steps.get(symbol.id);
304
- }
305
- const next = project.aliasStepOf(symbol);
306
- aliasSteps += 1;
307
- steps.set(symbol.id, next);
308
- return next;
309
- };
310
- /**
311
- * The declarations one symbol names: the declaration it is, and every link of the
312
- * alias chain it stands at the head of. A re-export something imports is used and so
313
- * is the declaration behind it, so both are named; a link this project does not
314
- * declare is stepped over rather than recorded.
315
- */
316
- const targetsOf = (symbol) => {
317
- const cached = reached.get(symbol.id);
318
- if (cached !== undefined) {
319
- return cached;
320
- }
321
- const targets = [];
322
- const walked = new Set();
323
- let at = symbol;
324
- let stepped = false;
325
- while (at !== undefined && !walked.has(at.id)) {
326
- walked.add(at.id);
327
- for (const id of declarationsOf(at)) {
328
- if (!targets.some((target) => target.id === id)) {
329
- targets.push({ id, stepped });
330
- }
331
- }
332
- at = stepOf(at);
333
- stepped = true;
334
- }
335
- reached.set(symbol.id, targets);
336
- return targets;
337
- };
338
306
  /**
339
307
  * The declarations one re-export carries forward: for an alias, the first link along
340
308
  * its chain that the inventory declares, stepping over the links it does not, such as
@@ -342,22 +310,7 @@ export function references(project, held, targetRoot, options) {
342
310
  * Each later link is a declaration of its own that references the next, so one link
343
311
  * per re-export is what lets a chain be followed one declaration at a time.
344
312
  */
345
- const carriedBy = (link, symbol) => {
346
- if (!link.alias) {
347
- return declarationsOf(symbol);
348
- }
349
- const walked = new Set([symbol.id]);
350
- let at = stepOf(symbol);
351
- while (at !== undefined && !walked.has(at.id)) {
352
- walked.add(at.id);
353
- const ids = declarationsOf(at);
354
- if (ids.length > 0) {
355
- return ids;
356
- }
357
- at = stepOf(at);
358
- }
359
- return [];
360
- };
313
+ const carriedBy = (link, symbol) => link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
361
314
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
362
315
  const symbolFor = (site, answered) => {
363
316
  if (site.shorthand !== undefined) {
@@ -383,14 +336,18 @@ export function references(project, held, targetRoot, options) {
383
336
  if (test) {
384
337
  tests.push(path);
385
338
  }
386
- const { uses, links } = sitesOf(file, declarations, declaredAt(file) ?? "");
339
+ const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
387
340
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
388
- // the uses. A shorthand is left out of the batch: its name resolves to the property
389
- // the literal declares, so the batch's answer for it would name something this
390
- // project's inventory never holds.
341
+ // the uses and the evaluated modules' specifiers last, each node once: a bare star
342
+ // re-export's specifier is a link and an evaluation both. A shorthand is left out
343
+ // of the batch: its name resolves to the property the literal declares, so the
344
+ // batch's answer for it would name something this project's inventory never holds.
391
345
  const batching = [
392
- ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
393
- ...links.map((link) => link.node),
346
+ ...new Set([
347
+ ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
348
+ ...links.map((link) => link.node),
349
+ ...evaluations.map((evaluation) => evaluation.node),
350
+ ]),
394
351
  ];
395
352
  const answered = new Map();
396
353
  batched += batching.length;
@@ -422,6 +379,25 @@ export function references(project, held, targetRoot, options) {
422
379
  });
423
380
  }
424
381
  }
382
+ for (const evaluation of evaluations) {
383
+ const module = answered.get(evaluation.node);
384
+ if (module === undefined) {
385
+ continue;
386
+ }
387
+ const position = renderPosition(file, targetRoot, evaluation.node.getStart());
388
+ for (const id of chains.declarationsOf(module)) {
389
+ if (fileIds.has(id)) {
390
+ found.push({
391
+ from: evaluation.from,
392
+ to: id,
393
+ position,
394
+ use: "evaluation",
395
+ resolution: "batch",
396
+ test,
397
+ });
398
+ }
399
+ }
400
+ }
425
401
  for (const site of uses) {
426
402
  const direct = site.shorthand !== undefined
427
403
  ? "shorthand"
@@ -433,7 +409,9 @@ export function references(project, held, targetRoot, options) {
433
409
  continue;
434
410
  }
435
411
  const position = renderPosition(file, targetRoot, site.node.getStart());
436
- for (const target of targetsOf(symbol)) {
412
+ // A re-export something imports is used and so is the declaration behind it, so
413
+ // the use names every link of the chain its name stands at the head of.
414
+ for (const target of chains.chainOf(symbol)) {
437
415
  found.push({
438
416
  from: site.from,
439
417
  to: target.id,
@@ -456,6 +434,6 @@ export function references(project, held, targetRoot, options) {
456
434
  .map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
457
435
  .sort((a, b) => compare(a.rule, b.rule)),
458
436
  testFilePaths: tests.sort(compare),
459
- cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
437
+ cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
460
438
  };
461
439
  }
package/dist/src/roots.js CHANGED
@@ -2,12 +2,13 @@
2
2
  * The root set of one project: every declaration the analysis keeps live without a
3
3
  * reference, and why.
4
4
  *
5
- * A root makes its declaration live under REACHABILITY and plays no part in
6
- * reference counting, which asks only whether some declaration of the loaded graph
7
- * names it. The two relations are different sets, so an exported declaration that
8
- * something outside the program reaches is live under the first and still a
9
- * candidate under the second.
5
+ * Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
6
+ * that names a caller is live under reference counting as well, which the sweep
7
+ * decides; a root that only supposes a consumer is not, so an exported declaration
8
+ * something outside the program reaches is live under reachability and still a
9
+ * candidate under reference counting.
10
10
  */
11
+ import { aliasChains } from "./alias-chain.js";
11
12
  import { REPOSITORY_DOCUMENT } from "./config.js";
12
13
  import { entryPoints } from "./entry-points.js";
13
14
  import { globExpression } from "./glob.js";
@@ -143,7 +144,10 @@ export function roots(project, held, targetRoot, options) {
143
144
  for (const point of entryPoints(project, files, options.testFiles)) {
144
145
  enter(point.file, point.rule, point.source, point.exports);
145
146
  }
146
- const exportsOf = exportTables(project, held, [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published], files.byName);
147
+ const exportsOf = exportTables(project, held, [
148
+ ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
149
+ ...published,
150
+ ]);
147
151
  for (const entry of entered) {
148
152
  const id = fileIds.get(entry.file.fileName);
149
153
  if (id === undefined) {
@@ -181,41 +185,14 @@ export function roots(project, held, targetRoot, options) {
181
185
  * its export table, star re-exports resolved into it. A file that is no module
182
186
  * exports nothing.
183
187
  */
184
- function exportTables(project, held, wanted, ownFiles) {
188
+ function exportTables(project, held, wanted) {
185
189
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
186
190
  const tables = new Map();
187
191
  if (unique.length === 0) {
188
192
  return tables;
189
193
  }
190
194
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
191
- const chains = new Map();
192
- const chainOf = (symbol) => {
193
- const cached = chains.get(symbol.id);
194
- if (cached !== undefined) {
195
- return cached;
196
- }
197
- const ids = [];
198
- const walked = new Set();
199
- let at = symbol;
200
- while (at !== undefined && !walked.has(at.id)) {
201
- walked.add(at.id);
202
- for (const handle of at.declarations) {
203
- if (!ownFiles.has(handle.path)) {
204
- continue;
205
- }
206
- const node = project.declarationAt(handle)?.node;
207
- const id = node === undefined
208
- ? undefined
209
- : held.declarations.get(nodeKey(node.getSourceFile(), node));
210
- if (id !== undefined && !ids.includes(id)) {
211
- ids.push(id);
212
- }
213
- }
214
- at = project.aliasStepOf(at);
215
- }
216
- chains.set(symbol.id, ids);
217
- return ids;
218
- };
195
+ const chains = aliasChains(project, held);
219
196
  unique.forEach((file, index) => {
220
197
  const module = modules[index];
221
198
  if (module === undefined) {
@@ -224,8 +201,8 @@ function exportTables(project, held, wanted, ownFiles) {
224
201
  }
225
202
  const ids = new Set();
226
203
  for (const exported of project.checker.getExportsOfModule(module)) {
227
- for (const id of chainOf(exported)) {
228
- ids.add(id);
204
+ for (const target of chains.chainOf(exported)) {
205
+ ids.add(target.id);
229
206
  }
230
207
  }
231
208
  tables.set(file.fileName, [...ids]);
package/dist/src/run.js CHANGED
@@ -1,6 +1,7 @@
1
- import { runRoots } from "./analysis.js";
1
+ import { runRoots, runSweep } from "./analysis.js";
2
2
  import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
3
3
  import { diagnosticErrors, discoverProjects, DiscoveryError, renderDiagnostic, } from "./discover.js";
4
+ import { retainedLines } from "./exempt.js";
4
5
  import { joinPath, resolvePath } from "./paths.js";
5
6
  import { printConfig } from "./print.js";
6
7
  import { resolve, resolveMatrix } from "./resolve.js";
@@ -174,6 +175,9 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
174
175
  if (verb === "print-roots") {
175
176
  return printRootsVerb(args.slice(1), out, err, host, openClient);
176
177
  }
178
+ if (verb === "print-retained") {
179
+ return printRetainedVerb(args.slice(1), out, err, host, openClient);
180
+ }
177
181
  if (verb !== undefined && VERBS.includes(verb)) {
178
182
  err.write(`deadset-ts: ${verb} is not implemented\n`);
179
183
  return EXIT_USAGE;
@@ -276,6 +280,34 @@ function printRootsVerb(args, out, err, host, openClient) {
276
280
  return refuse(error, err);
277
281
  }
278
282
  }
283
+ /**
284
+ * Writes every declaration an exemption held back in the production sweep a report is
285
+ * built from, one per line, and nothing where no exemption held one back. One fact
286
+ * several configurations found is one record, so the line names no configuration. The
287
+ * line shape is this command's own rather than a Contract format, so that sort and cut
288
+ * read it.
289
+ */
290
+ function printRetainedVerb(args, out, err, host, openClient) {
291
+ let engine;
292
+ try {
293
+ const options = readOptions(args);
294
+ const { config } = resolve(inputsOf(host, options));
295
+ const scope = scopeOf(host, options);
296
+ engine = openClient(false);
297
+ const swept = runSweep(engine, host, scope, config, {
298
+ marked: [],
299
+ mode: { production: true },
300
+ });
301
+ for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
302
+ out.write(`${line}\n`);
303
+ }
304
+ return EXIT_CLEAN;
305
+ }
306
+ catch (error) {
307
+ engine?.close();
308
+ return refuse(error, err);
309
+ }
310
+ }
279
311
  /** Turns one refusal into the exit code the Contract's table gives it. */
280
312
  function refuse(error, err) {
281
313
  if (error instanceof ConfigError) {
@@ -1,5 +1,6 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import { isIdentifier } from "@typescript/native/unstable/ast";
3
+ import { DiscoveryError } from "./discover.js";
3
4
  /**
4
5
  * Opens the compiler client the analysis reads through.
5
6
  *
@@ -81,19 +82,29 @@ export function diagnosticsOf(project) {
81
82
  }
82
83
  /**
83
84
  * Runs `visit` over every project the compiler configurations name, inside one
84
- * snapshot of one client.
85
+ * snapshot of one client, in the order they are named, and answers in that order.
85
86
  *
86
87
  * The snapshot is opened once with every configuration, so each becomes a project
87
88
  * with its own program and checker; it is never updated, because the analysis
88
- * reads and the tree does not change under it. It is disposed once when the last
89
- * project has been visited, and the client is released after it, whether the visit
90
- * completed or threw.
89
+ * reads and the tree does not change under it. The order the snapshot lists its
90
+ * projects in is the compiler's own, so each named configuration is looked up in it
91
+ * instead, and a configuration the snapshot opened no project for ends the run: the
92
+ * run would otherwise answer for fewer projects than it was asked to analyze. The
93
+ * snapshot is disposed once when the last project has been visited, and the client
94
+ * is released after it, whether the visit completed or threw.
91
95
  */
92
96
  export function runSession(engine, configFiles, visit) {
93
97
  try {
94
98
  const snapshot = engine.updateSnapshot(configFiles);
95
99
  try {
96
- const projects = snapshot.getProjects().map((project) => visit(viewOf(project)));
100
+ const opened = new Map(snapshot.getProjects().map((project) => [project.configFileName, project]));
101
+ const projects = configFiles.map((configFile) => {
102
+ const project = opened.get(configFile);
103
+ if (project === undefined) {
104
+ throw new DiscoveryError(`${configFile} opened no project`, []);
105
+ }
106
+ return visit(viewOf(project));
107
+ });
97
108
  return { projects, timing: engine.getTimingInfo() };
98
109
  }
99
110
  finally {
package/dist/src/sweep.js CHANGED
@@ -1,31 +1,26 @@
1
1
  /**
2
- * Which declarations of one graph are dead, under which liveness relation, and which
3
- * dead component each belongs to. Reference counting holds a declaration live when any
4
- * declaration references it, whatever that one's own liveness, and no root plays a part;
2
+ * Which declarations of one graph are dead, and under which liveness relation.
3
+ * Reference counting holds a declaration live when any declaration references it,
4
+ * whatever that one's own liveness, or when it is a root of a kind that names a caller;
5
5
  * reachability holds it live when a path of references reaches it from a root. So a
6
6
  * declaration only unreachable ones reference is live under the first, dead under the second.
7
7
  */
8
- import { componentsOf } from "./components.js";
9
8
  import { namesACaller, OUTSIDE } from "./graph.js";
10
9
  /**
11
10
  * Answers which declarations of one graph are dead under the input's mode.
12
11
  *
13
12
  * The order is the order the answers depend on: the callers and the marks are live
14
13
  * before either relation runs, the candidate set is every judged declaration at least
15
- * one relation does not hold live, the tests of dead code join it, and the components
16
- * are computed over the set that results.
14
+ * one relation does not hold live and no exemption holds back, and the tests of dead
15
+ * code join it.
17
16
  */
18
17
  export function sweep(graph, input) {
19
- const marked = graph.symbols.map(() => false);
20
- for (const id of input.marked) {
21
- const at = graph.at(id);
22
- if (at !== OUTSIDE) {
23
- marked[at] = true;
24
- }
25
- }
26
- // Every root and every mark seeds reachability. A root of a kind that names a caller,
27
- // and a mark, are live under reference counting as well; a root that only supposes a
28
- // caller is not.
18
+ const marked = flagged(graph, input.marked);
19
+ const exempt = flagged(graph, (input.exempt ?? []).map((record) => record.id));
20
+ const held = marked.map((mark, at) => mark || exempt[at] === true);
21
+ // Every root, every mark and every exemption seeds reachability. A root of a kind that
22
+ // names a caller, and a mark, are live under reference counting as well; a root that
23
+ // only supposes a caller is not, and neither is an exemption.
29
24
  const called = graph.symbols.map(() => false);
30
25
  for (const root of graph.rooted) {
31
26
  if (namesACaller(root.kind)) {
@@ -39,7 +34,7 @@ export function sweep(graph, input) {
39
34
  }
40
35
  return input.mode.production ? made.production : made.production + made.test;
41
36
  };
42
- const reached = reachable(graph, marked, input.mode);
37
+ const reached = reachable(graph, held, input.mode);
43
38
  const liveUnder = graph.symbols.map((_symbol, at) => {
44
39
  const relations = [];
45
40
  if (marked[at] === true || called[at] === true || counted(at) > 0) {
@@ -50,8 +45,8 @@ export function sweep(graph, input) {
50
45
  }
51
46
  return relations;
52
47
  });
53
- const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2);
54
- const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
48
+ const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && exempt[at] !== true && (liveUnder[at]?.length ?? 0) < 2);
49
+ const testOfDeadCode = admitTestsOfDeadCode(graph, dead, held);
55
50
  const candidates = [];
56
51
  const live = new Map();
57
52
  graph.symbols.forEach((symbol, at) => {
@@ -70,31 +65,41 @@ export function sweep(graph, input) {
70
65
  testOfDeadCode: testOfDeadCode[at] === true,
71
66
  });
72
67
  });
73
- return {
74
- liveUnder: live,
75
- candidates,
76
- components: componentsOf(graph, dead, testOfDeadCode),
77
- };
68
+ return { liveUnder: live, candidates };
69
+ }
70
+ /** Per declaration of the graph, whether one of the identifiers names it. */
71
+ function flagged(graph, ids) {
72
+ const flags = graph.symbols.map(() => false);
73
+ for (const id of ids) {
74
+ const at = graph.at(id);
75
+ if (at !== OUTSIDE) {
76
+ flags[at] = true;
77
+ }
78
+ }
79
+ return flags;
78
80
  }
79
81
  /**
80
82
  * Per declaration, whether a seed reaches it. The two seeds reach through different
81
- * reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
82
- * no mode withholds a reference it makes, while a root reaches through the references the
83
- * mode counts. The marks run first, so what they reach is expanded under the wider rule.
83
+ * reference sets: a held declaration, marked or exempt, is live by a mechanism the
84
+ * analysis cannot see, so no mode withholds a reference it makes, a test declaration's
85
+ * included, while a root reaches through the references the mode counts. The held seed
86
+ * runs first, so what it reaches is expanded under the wider rule.
84
87
  */
85
- function reachable(graph, marked, mode) {
88
+ function reachable(graph, held, mode) {
86
89
  const reached = graph.symbols.map(() => false);
87
90
  const expanded = graph.symbols.map(() => false);
88
91
  const queue = [];
89
- // A reference to a file is a reference to the module's namespace, which reaches what
90
- // the module exports as well as its top level. A root does not expand: a file a
91
- // runtime runs without reading its exports is a root by itself.
92
- const enter = (at, viaReference) => {
92
+ // Reaching a file reaches its top level: the references written outside every
93
+ // declaration it holds. A reference that reads a file reads the module's namespace,
94
+ // which reaches what the module exports as well. An evaluation runs the top level
95
+ // and reads no export, and a root does not expand either: a file a runtime runs
96
+ // without reading its exports is a root by itself.
97
+ const enter = (at, expands) => {
93
98
  if (!reached[at]) {
94
99
  reached[at] = true;
95
100
  queue.push(at);
96
101
  }
97
- if (!viaReference || expanded[at] === true) {
102
+ if (!expands || expanded[at] === true) {
98
103
  return;
99
104
  }
100
105
  expanded[at] = true;
@@ -102,18 +107,18 @@ function reachable(graph, marked, mode) {
102
107
  enter(exported, false);
103
108
  }
104
109
  };
105
- const walk = (held) => {
110
+ const walk = (holding) => {
106
111
  for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
107
112
  for (const edge of graph.out[at] ?? []) {
108
- if (!held && mode.production && edge.test) {
113
+ if (!holding && mode.production && edge.test) {
109
114
  continue;
110
115
  }
111
- enter(edge.to, true);
116
+ enter(edge.to, !edge.evaluation);
112
117
  }
113
118
  }
114
119
  };
115
- marked.forEach((mark, at) => {
116
- if (mark) {
120
+ held.forEach((hold, at) => {
121
+ if (hold) {
117
122
  enter(at, false);
118
123
  }
119
124
  });
@@ -128,12 +133,13 @@ function reachable(graph, marked, mode) {
128
133
  * Admits every test declaration whose set of referenced production declarations is
129
134
  * non-empty and wholly dead, and returns which it admitted. The set is read from every
130
135
  * reference the test makes, not the ones the mode counts: a production sweep counts none,
131
- * which is what makes the targets dead. A test referencing one live target is never admitted.
136
+ * which is what makes the targets dead. A test referencing one live target is never
137
+ * admitted, and neither is a marked or exempt test.
132
138
  */
133
- function admitTestsOfDeadCode(graph, dead, marked) {
139
+ function admitTestsOfDeadCode(graph, dead, held) {
134
140
  const admitted = graph.symbols.map(() => false);
135
141
  graph.symbols.forEach((_symbol, at) => {
136
- if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
142
+ if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
137
143
  return;
138
144
  }
139
145
  let targets = 0;
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "2.2.0";
7
+ export const CONTRACT_VERSION = "3.0.0";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
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": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -32,13 +32,13 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@eslint/js": "10.0.1",
35
- "@types/node": "24.13.5",
35
+ "@types/node": "24.19.0",
36
36
  "eslint": "10.11.0",
37
- "fast-check": "4.10.1",
38
- "prettier": "3.9.8",
37
+ "fast-check": "4.10.2",
38
+ "prettier": "3.9.9",
39
39
  "typescript": "npm:@typescript/typescript6@6.0.2",
40
- "typescript-eslint": "8.70.0",
41
- "vitest": "5.0.1"
40
+ "typescript-eslint": "8.70.1",
41
+ "vitest": "5.0.2"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"