@cplieger/deadset-ts 5.1.0 → 5.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +79 -37
  3. package/dist/src/config.js +3 -0
  4. package/dist/src/configuration-files.js +24 -25
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/contextual-uses.js +46 -18
  7. package/dist/src/convention-rows.js +7 -0
  8. package/dist/src/conventions.js +97 -33
  9. package/dist/src/dependencies.js +8 -0
  10. package/dist/src/entry-point-gaps.js +10 -26
  11. package/dist/src/entry-points.js +4 -0
  12. package/dist/src/exempt.js +23 -8
  13. package/dist/src/file-facts.js +23 -20
  14. package/dist/src/findings/completion.js +27 -11
  15. package/dist/src/findings/interfaces.js +2 -6
  16. package/dist/src/findings/intra-function.js +15 -3
  17. package/dist/src/findings/unused-declarations.js +5 -6
  18. package/dist/src/findings/visibility-narrowing.js +6 -1
  19. package/dist/src/generated-file.js +72 -12
  20. package/dist/src/graph.js +6 -5
  21. package/dist/src/html-entries.js +137 -0
  22. package/dist/src/inventory.js +13 -3
  23. package/dist/src/kinds.js +2 -2
  24. package/dist/src/manifest.js +34 -5
  25. package/dist/src/matrix.js +9 -3
  26. package/dist/src/namespace-objects.js +158 -0
  27. package/dist/src/outside-tests.js +139 -0
  28. package/dist/src/overrides.js +104 -0
  29. package/dist/src/paths.js +17 -0
  30. package/dist/src/references.js +150 -4
  31. package/dist/src/roots.js +172 -39
  32. package/dist/src/session.js +4 -3
  33. package/dist/src/sweep.js +17 -0
  34. package/dist/src/template-field.js +60 -3
  35. package/dist/src/test-support.js +35 -23
  36. package/dist/src/version.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +122 -49
  40. package/src/config.ts +3 -0
  41. package/src/configuration-files.ts +29 -25
  42. package/src/conformance-record.ts +2 -2
  43. package/src/contextual-uses.ts +51 -20
  44. package/src/convention-rows.ts +15 -0
  45. package/src/conventions.ts +119 -30
  46. package/src/dependencies.ts +9 -0
  47. package/src/entry-point-gaps.ts +11 -28
  48. package/src/entry-points.ts +7 -1
  49. package/src/exempt.ts +29 -12
  50. package/src/file-facts.ts +25 -21
  51. package/src/findings/completion.ts +31 -11
  52. package/src/findings/interfaces.ts +2 -9
  53. package/src/findings/intra-function.ts +17 -3
  54. package/src/findings/unused-declarations.ts +5 -8
  55. package/src/findings/visibility-narrowing.ts +7 -1
  56. package/src/generated-file.ts +80 -12
  57. package/src/graph.ts +9 -8
  58. package/src/html-entries.ts +170 -0
  59. package/src/inventory.ts +15 -3
  60. package/src/kinds.ts +2 -2
  61. package/src/manifest.ts +49 -7
  62. package/src/matrix.ts +21 -3
  63. package/src/namespace-objects.ts +216 -0
  64. package/src/outside-tests.ts +191 -0
  65. package/src/overrides.ts +136 -0
  66. package/src/paths.ts +18 -0
  67. package/src/references.ts +206 -9
  68. package/src/roots.ts +210 -53
  69. package/src/session.ts +4 -2
  70. package/src/sweep.ts +19 -0
  71. package/src/template-field.ts +65 -3
  72. package/src/test-support.ts +34 -22
  73. package/src/version.ts +1 -1
@@ -5,7 +5,7 @@
5
5
  * directory a configuration property moves read from the property's literal value.
6
6
  * No configuration file is run.
7
7
  */
8
- import { isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isClassDeclaration, isComputedPropertyName, isExportAssignment, isExportDeclaration, isExpressionStatement, isFunctionDeclaration, isIdentifier, isImportDeclaration, isNamedExports, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isNumericLiteral, isObjectBindingPattern, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, isVariableStatement, SyntaxKind, } from "@typescript/native/unstable/ast";
9
9
  import { lineOf } from "./component-files.js";
10
10
  import { CONVENTION_ROWS, } from "./convention-rows.js";
11
11
  import { globExpression } from "./glob.js";
@@ -288,12 +288,32 @@ function objectOf(node, declared, throughCalls, seen = new Set()) {
288
288
  }
289
289
  return at;
290
290
  }
291
+ /** The text of a string literal or a template literal with no substitution, or undefined. */
292
+ function literalText(node) {
293
+ const at = unwrapped(node);
294
+ return isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at) ? at.text : undefined;
295
+ }
296
+ /**
297
+ * The literal texts one value at a path's end writes: the literal itself, or, where
298
+ * `elements`, each element of an array literal of literals. Undefined for any other value.
299
+ */
300
+ function literalTexts(held, elements) {
301
+ const one = literalText(held);
302
+ if (one !== undefined) {
303
+ return [one];
304
+ }
305
+ if (!elements || !isArrayLiteralExpression(held)) {
306
+ return undefined;
307
+ }
308
+ const texts = held.elements.map(literalText);
309
+ return texts.every((text) => text !== undefined) ? texts : undefined;
310
+ }
291
311
  /**
292
312
  * Reads the values at one path of keys below one object literal into `values`, and
293
313
  * answers the first member that does not write its value out: a spread, a value that is
294
314
  * not a literal at the path's end, or one that is not an object literal before it.
295
315
  */
296
- function readPath(object, keys, values) {
316
+ function readPath(object, keys, values, elements) {
297
317
  const [key, ...rest] = keys;
298
318
  for (const member of object.properties) {
299
319
  if (isSpreadAssignment(member) || computesKey(member)) {
@@ -304,16 +324,17 @@ function readPath(object, keys, values) {
304
324
  }
305
325
  const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
306
326
  if (rest.length === 0) {
307
- if (held === undefined || !(isStringLiteral(held) || isNoSubstitutionTemplateLiteral(held))) {
327
+ const texts = held === undefined ? undefined : literalTexts(held, elements);
328
+ if (texts === undefined) {
308
329
  return member;
309
330
  }
310
- values.push(held.text);
331
+ values.push(...texts);
311
332
  }
312
333
  else {
313
334
  if (held === undefined || !isObjectLiteralExpression(held)) {
314
335
  return member;
315
336
  }
316
- const failed = readPath(held, rest, values);
337
+ const failed = readPath(held, rest, values, elements);
317
338
  if (failed !== undefined) {
318
339
  return failed;
319
340
  }
@@ -324,11 +345,12 @@ function readPath(object, keys, values) {
324
345
  /**
325
346
  * Every value one file writes at one property's path inside the object it is read from:
326
347
  * the options argument of every call `call` names, or the file's default export. A string
327
- * literal or a template literal with no substitution is the value. A read object, or an
328
- * object along the path, that is not written out as an object literal, or that spreads
329
- * another object into itself, is not a literal, and neither is any other value at the path.
348
+ * literal or a template literal with no substitution is the value, and so is each element
349
+ * of an array literal of them where `elements`. A read object, or an object along the
350
+ * path, that is not written out as an object literal, or that spreads another object into
351
+ * itself, is not a literal, and neither is any other value at the path.
330
352
  */
331
- export function readProperty(file, property, call) {
353
+ export function readProperty(file, property, call, elements = false) {
332
354
  const declared = declaredValues(file);
333
355
  const objects = call === undefined
334
356
  ? defaultExports(file).map((one) => objectOf(one, declared, true))
@@ -339,7 +361,7 @@ export function readProperty(file, property, call) {
339
361
  continue;
340
362
  }
341
363
  const notLiteral = isObjectLiteralExpression(object)
342
- ? readPath(object, property.split("."), values)
364
+ ? readPath(object, property.split("."), values, elements)
343
365
  : object;
344
366
  if (notLiteral !== undefined) {
345
367
  return { values, notLiteral };
@@ -392,29 +414,18 @@ function configurationFiles(host, dir, glob) {
392
414
  function resolveMove(host, parse, dir, row, move, resolved) {
393
415
  const set = [];
394
416
  for (const reading of move.readings) {
395
- for (const glob of reading.files) {
396
- for (const name of configurationFiles(host, dir, glob)) {
397
- const path = joinPath(dir, name);
398
- let text;
399
- try {
400
- text = host.readFile(path);
401
- }
402
- catch {
403
- continue;
404
- }
405
- const file = parse(path, text);
406
- const read = readProperty(file, reading.property, reading.call);
407
- if (read.notLiteral !== undefined) {
408
- const line = lineOf(text, read.notLiteral.getStart());
409
- return {
410
- failure: `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
411
- `the convention row ${row.name} reads, to a value that is not a string literal: ` +
412
- `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
413
- `files in ts.entry_files`,
414
- };
415
- }
416
- set.push(...read.values);
417
+ for (const { name, text, file } of readingFiles(host, parse, dir, reading.files)) {
418
+ const read = readProperty(file, reading.property, reading.call);
419
+ if (read.notLiteral !== undefined) {
420
+ const line = lineOf(text, read.notLiteral.getStart());
421
+ return {
422
+ failure: `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
423
+ `the convention row ${row.name} reads, to a value that is not a string literal: ` +
424
+ `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
425
+ `files in ts.entry_files`,
426
+ };
417
427
  }
428
+ set.push(...read.values);
418
429
  }
419
430
  }
420
431
  const dirs = [];
@@ -427,6 +438,55 @@ function resolveMove(host, parse, dir, row, move, resolved) {
427
438
  }
428
439
  return { dirs: [...new Set(dirs)] };
429
440
  }
441
+ /** The texts each configuration file a reading names holds, parsed, in name order. */
442
+ function readingFiles(host, parse, dir, files) {
443
+ const found = [];
444
+ for (const glob of files) {
445
+ for (const name of configurationFiles(host, dir, glob)) {
446
+ const path = joinPath(dir, name);
447
+ let text;
448
+ try {
449
+ text = host.readFile(path);
450
+ }
451
+ catch {
452
+ continue;
453
+ }
454
+ found.push({ name, text, file: parse(path, text) });
455
+ }
456
+ }
457
+ return found;
458
+ }
459
+ /**
460
+ * The dependencies one manifest declares that an applied row's short names use: the
461
+ * package each literal value stands for, and, for a value that is not a literal, every
462
+ * declared dependency the row's package name stands for with some value.
463
+ */
464
+ function shortNameUses(host, parse, dir, manifest, readings) {
465
+ const declared = new Set(DEPENDENCY_SECTIONS.flatMap((section) => {
466
+ const held = isRecord(manifest) ? manifest[section] : undefined;
467
+ return isRecord(held) ? Object.keys(held) : [];
468
+ }));
469
+ const used = new Set();
470
+ for (const reading of readings) {
471
+ const [before = "", after = ""] = reading.package.split("{}");
472
+ for (const { file } of readingFiles(host, parse, dir, reading.files)) {
473
+ const read = readProperty(file, reading.property, reading.call, true);
474
+ for (const value of read.values) {
475
+ used.add(`${before}${value}${after}`);
476
+ }
477
+ if (read.notLiteral !== undefined) {
478
+ for (const name of declared) {
479
+ if (name.length >= before.length + after.length &&
480
+ name.startsWith(before) &&
481
+ name.endsWith(after)) {
482
+ used.add(name);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ return [...used].filter((name) => declared.has(name)).sort(compare);
489
+ }
430
490
  /** Two strings ordered bytewise. */
431
491
  function compare(a, b) {
432
492
  if (a === b) {
@@ -443,6 +503,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
443
503
  const globs = [];
444
504
  const generated = [];
445
505
  const failures = [];
506
+ const uses = new Set();
446
507
  for (const manifest of manifests) {
447
508
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
448
509
  const versions = new Map();
@@ -500,6 +561,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
500
561
  version,
501
562
  manifest: manifest.path,
502
563
  });
564
+ for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
565
+ uses.add(name);
566
+ }
503
567
  const excludes = [
504
568
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
505
569
  ].map(globExpression);
@@ -520,7 +584,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
520
584
  unique.sort((a, b) => compare(a.name, b.name) ||
521
585
  compare(a.manifest, b.manifest) ||
522
586
  compare(JSON.stringify(a), JSON.stringify(b)));
523
- return { applied: unique, globs, generated, failures };
587
+ return { applied: unique, globs, generated, failures, uses: [...uses].sort(compare) };
524
588
  }
525
589
  /**
526
590
  * Every own file of one project below a directory an applied row names as generated, by
@@ -24,6 +24,14 @@ const SECTIONS = [
24
24
  ];
25
25
  /** The owner of a use no declaration of the inventory holds: a configuration's. */
26
26
  const PROJECT_OWNER = "";
27
+ /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
28
+ export function configurationNeeds(packages) {
29
+ return {
30
+ packages: new Set(packages),
31
+ uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
32
+ unanswered: false,
33
+ };
34
+ }
27
35
  function isRecord(value) {
28
36
  return typeof value === "object" && value !== null && !Array.isArray(value);
29
37
  }
@@ -13,9 +13,9 @@ const EVERY_CONVENTION = "every file it loads by its own convention beyond its c
13
13
  const UNREAD = "No rule reads it beyond a configuration file the file-name form or a string enters, so a " +
14
14
  "file only it loads is reported as never imported unless ts.entry_files or roots.patterns " +
15
15
  "names it.";
16
- /** Why a configuration's file names that are not relative literals are declined. */
17
- const NOT_RELATIVE = "A string of a configuration file names a file only where it is a literal starting with ./ " +
18
- "or ../, so a file named otherwise is reported as never imported.";
16
+ /** Why a configuration's file names that are not literals are declined. */
17
+ const NOT_LITERAL = "A string of a configuration file names a file only where it is a literal, so a file a " +
18
+ "computed value names is reported as never imported.";
19
19
  /** The tools whose own conventions are not read. */
20
20
  const UNREAD_TOOLS = [
21
21
  "angular",
@@ -211,11 +211,6 @@ const PARTLY_ENTERED = [
211
211
  reason: "A testMatch that is not a string literal leaves the test-file set unknown, and an " +
212
212
  "unknown set enters nothing.",
213
213
  },
214
- {
215
- tool: "playwright",
216
- convention: "a globalSetup or globalTeardown file named without a leading ./ or ../",
217
- reason: NOT_RELATIVE,
218
- },
219
214
  {
220
215
  tool: "playwright",
221
216
  convention: "the testDir and testMatch of an entry in projects",
@@ -272,20 +267,14 @@ const PARTLY_ENTERED = [
272
267
  },
273
268
  {
274
269
  tool: "vite",
275
- convention: "an entry point its configuration names without a leading ./ or ../, or computes",
276
- reason: NOT_RELATIVE,
277
- },
278
- {
279
- tool: "vite",
280
- convention: "the module scripts an index.html loads",
281
- reason: "No rule reads an HTML file, so a script only index.html loads is reported as never " +
282
- "imported unless ts.entry_files names it.",
270
+ convention: "an entry point its configuration computes",
271
+ reason: NOT_LITERAL,
283
272
  },
284
273
  {
285
274
  tool: "vitest",
286
- convention: "the benchmark and type-test files",
287
- reason: "A test file is one ts.test_files classifies, so a *.bench.* or *.test-d.* file is " +
288
- "entered only where a configured pattern names it.",
275
+ convention: "the benchmark files",
276
+ reason: "A test file is one ts.test_files classifies, so a *.bench.* file is entered only " +
277
+ "where a configured pattern names it.",
289
278
  },
290
279
  {
291
280
  tool: "vitest",
@@ -293,11 +282,6 @@ const PARTLY_ENTERED = [
293
282
  reason: "A string is read against the configuration file's directory, so a setup file named " +
294
283
  "against a root the configuration moves is not entered.",
295
284
  },
296
- {
297
- tool: "vitest",
298
- convention: "a setup file named without a leading ./ or ../",
299
- reason: NOT_RELATIVE,
300
- },
301
285
  {
302
286
  tool: "vitest",
303
287
  convention: "the test files a configuration's include member names",
@@ -306,8 +290,8 @@ const PARTLY_ENTERED = [
306
290
  },
307
291
  {
308
292
  tool: "webpack",
309
- convention: "an entry point its configuration names without a leading ./ or ../, or computes",
310
- reason: NOT_RELATIVE,
293
+ convention: "an entry point its configuration computes",
294
+ reason: NOT_LITERAL,
311
295
  },
312
296
  ];
313
297
  /** Two strings ordered bytewise. */
@@ -14,6 +14,7 @@ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAss
14
14
  import { SymbolFlags } from "@typescript/native/unstable/sync";
15
15
  import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
16
  import { globExpression } from "./glob.js";
17
+ import { htmlEntries } from "./html-entries.js";
17
18
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
18
19
  import { UNANSWERED } from "./query.js";
19
20
  /** The extensions the test runner and the browser-test runner load a configuration from. */
@@ -406,5 +407,8 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse)
406
407
  }
407
408
  found.push(...workersOf(project, file, files));
408
409
  }
410
+ for (const { file, source } of htmlEntries(host, targetRoot, files, parse)) {
411
+ found.push({ file, rule: "html-entry", source, exports: true });
412
+ }
409
413
  return found;
410
414
  }
@@ -92,12 +92,18 @@ export function computeExemptions(input, detectors, holding) {
92
92
  }
93
93
  /** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
94
94
  export function detectExemptions(input, detectors, holding) {
95
+ return heldRecords(evidenceRecords(input, detectors, holding.disabled), holding);
96
+ }
97
+ /**
98
+ * Every record one project's detectors found, one per piece of evidence, before the
99
+ * mode decides which of them hold: what {@link heldRecords} reads.
100
+ */
101
+ export function evidenceRecords(input, detectors, disabled) {
95
102
  const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
96
103
  const found = [];
97
- const inTestFiles = [];
98
104
  for (const exemptionClass of TS_EXEMPTION_CLASSES) {
99
105
  const detect = detectors.get(exemptionClass);
100
- if (detect === undefined || holding.disabled.has(exemptionClass)) {
106
+ if (detect === undefined || disabled.has(exemptionClass)) {
101
107
  continue;
102
108
  }
103
109
  const allowed = typescriptVisibilityOf(exemptionClass);
@@ -105,17 +111,26 @@ export function detectExemptions(input, detectors, holding) {
105
111
  if (!retainable(allowed, visibility.get(evidence.id))) {
106
112
  continue;
107
113
  }
108
- const record = {
114
+ found.push({
109
115
  id: evidence.id,
110
116
  class: exemptionClass,
111
117
  detail: evidence.detail,
112
118
  site: evidence.site,
113
119
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
114
- };
115
- if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
116
- inTestFiles.push(record);
117
- continue;
118
- }
120
+ });
121
+ }
122
+ }
123
+ return found;
124
+ }
125
+ /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
126
+ export function heldRecords(records, holding) {
127
+ const found = [];
128
+ const inTestFiles = [];
129
+ for (const record of records) {
130
+ if (holding.mode.production && holding.testFiles.has(record.site.path)) {
131
+ inTestFiles.push(record);
132
+ }
133
+ else {
119
134
  found.push(record);
120
135
  }
121
136
  }
@@ -26,12 +26,10 @@ function lastLine(text) {
26
26
  return line;
27
27
  }
28
28
  /**
29
- * Every source file below the target root, in path order. A directory that cannot be
30
- * read is skipped, as discovery skips it, and a file that cannot be read is skipped
31
- * with it: a tree the analysis may not enter holds no file it can report.
29
+ * The path below the target root of every source file there, in path order. A directory
30
+ * that cannot be read is skipped, as discovery skips it.
32
31
  */
33
- export function sourceTree(host, targetRoot) {
34
- const scopeOf = packageScope(host, targetRoot);
32
+ export function sourcePaths(host, targetRoot) {
35
33
  const found = [];
36
34
  const pending = [""];
37
35
  for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
@@ -50,24 +48,29 @@ export function sourceTree(host, targetRoot) {
50
48
  }
51
49
  continue;
52
50
  }
53
- if (!SOURCE_FILE.test(entry.name) || DECLARATION_FILE.test(entry.name)) {
54
- continue;
55
- }
56
- let text;
57
- try {
58
- text = host.readFile(joinPath(targetRoot, path));
51
+ if (SOURCE_FILE.test(entry.name) && !DECLARATION_FILE.test(entry.name)) {
52
+ found.push(path);
59
53
  }
60
- catch {
61
- continue;
62
- }
63
- found.push({
64
- path,
65
- ref: renderRef(scopeOf(path), { of: "module" }),
66
- endLine: lastLine(text),
67
- });
68
54
  }
69
55
  }
70
- return found.sort((a, b) => (a.path < b.path ? -1 : 1));
56
+ return found.sort((a, b) => (a < b ? -1 : 1));
57
+ }
58
+ /**
59
+ * Every source file below the target root, in path order. A file that cannot be read is
60
+ * skipped: a tree the analysis may not enter holds no file it can report.
61
+ */
62
+ export function sourceTree(host, targetRoot) {
63
+ const scopeOf = packageScope(host, targetRoot);
64
+ return sourcePaths(host, targetRoot).flatMap((path) => {
65
+ let text;
66
+ try {
67
+ text = host.readFile(joinPath(targetRoot, path));
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ return [{ path, ref: renderRef(scopeOf(path), { of: "module" }), endLine: lastLine(text) }];
73
+ });
71
74
  }
72
75
  /** Whether one file changes what the program sees by being included in it. */
73
76
  function actsByInclusion(file) {
@@ -39,23 +39,29 @@ function publishedIds(union) {
39
39
  }
40
40
  /**
41
41
  * The reachability class of one declaration of the run. A declaration of a library's
42
- * published API, a public member of a published type included, has callers outside the
43
- * target: it is `certain` where the run loaded every consumer the scope declared and
44
- * declared at least one, `probable` where it declared consumers and did not load them
45
- * all, and `possible` where it declared none. Whether the configuration declares the
46
- * consumer set complete decides nothing here. Every other declaration has every reference
47
- * in the loaded program, a private member and a `#private` name among them, and is
48
- * `certain`.
42
+ * published API, a public member of a published type included, is `certain` where the
43
+ * run loaded every consumer the scope declared and declared one, `probable` where one did
44
+ * not load, and `possible` where it declared none, whatever the configuration says of the
45
+ * consumer set. A declaration of test-support code that test code references, outside the
46
+ * test-of-dead-code rule, is `possible`. Every other declaration is `certain`.
49
47
  */
50
48
  function reachabilityClasses(input) {
51
49
  const published = publishedIds(input.swept.matrix.union);
50
+ const usedByTests = new Set(input.swept.sweep.candidates
51
+ .filter((candidate) => candidate.heldByTests === candidate.id)
52
+ .map((candidate) => candidate.id));
52
53
  const { declared } = input.boundary.consumers;
53
54
  const outside = declared.length === 0
54
55
  ? "possible"
55
56
  : everyDeclaredLoaded(input.boundary)
56
57
  ? "certain"
57
58
  : "probable";
58
- return (id) => (published.has(id) ? outside : "certain");
59
+ return (id) => {
60
+ if (usedByTests.has(id)) {
61
+ return "possible";
62
+ }
63
+ return published.has(id) ? outside : "certain";
64
+ };
59
65
  }
60
66
  /**
61
67
  * The component each dead declaration of the run falls in, and none for a live one, with
@@ -117,6 +123,12 @@ export function completed(input, findings, kinds = KINDS) {
117
123
  const componentOf = findingComponents(swept, input.config.reporters.cascade);
118
124
  let minted = swept.sweep.components.length;
119
125
  const dead = new Set(swept.sweep.components.map((component) => component.id));
126
+ const fallingWith = new Map();
127
+ for (const { heldByTests } of swept.sweep.candidates) {
128
+ if (heldByTests !== undefined) {
129
+ fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
130
+ }
131
+ }
120
132
  const done = findings.map((finding) => {
121
133
  const row = kinds.get(finding.code);
122
134
  if (row === undefined) {
@@ -134,8 +146,12 @@ export function completed(input, findings, kinds = KINDS) {
134
146
  const judged = !part && union.subject[at] === true ? symbol : undefined;
135
147
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
136
148
  const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
137
- // A test file's declaration is a component member only as a test of dead code.
138
- if (candidate !== undefined && found === undefined && union.test[at] !== true) {
149
+ // A test file's declaration is a component member only as a test of dead code, and
150
+ // test code keeps the test-support code it references out of every component.
151
+ if (candidate !== undefined &&
152
+ found === undefined &&
153
+ union.test[at] !== true &&
154
+ candidate.heldByTests === undefined) {
139
155
  throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
140
156
  }
141
157
  let component = found;
@@ -144,7 +160,7 @@ export function completed(input, findings, kinds = KINDS) {
144
160
  component = {
145
161
  id: componentId(minted),
146
162
  root: true,
147
- symbolCount: 1,
163
+ symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
148
164
  deletableLines: 0,
149
165
  };
150
166
  }
@@ -26,7 +26,6 @@ function indexed(input) {
26
26
  return {
27
27
  symbolOf,
28
28
  inTest: (id) => union.test[union.at(id)] === true,
29
- inSupport: (id) => union.support[union.at(id)] === true,
30
29
  candidateOf: (id) => candidates.get(id),
31
30
  implementationsOf: (id) => [...(input.implementations.classes.get(id) ?? [])]
32
31
  .map(symbolOf)
@@ -56,17 +55,14 @@ function findingOf(code, symbol, message, implementations) {
56
55
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
57
56
  * interface no reference names, unless it is a marker, a method whose every
58
57
  * implementation has an empty body. A test file's declaration is neither kind's
59
- * subject, because a production sweep counts none of the references it can have, and
60
- * neither is a test-support declaration test code references, which that code keeps live.
58
+ * subject, because a production sweep counts none of the references it can have.
61
59
  */
62
60
  export const interfaces = (input) => {
63
61
  const run = indexed(input);
64
62
  const found = [];
65
63
  for (const candidate of input.swept.sweep.candidates) {
66
64
  const symbol = run.symbolOf(candidate.id);
67
- if (symbol === undefined ||
68
- run.inTest(candidate.id) ||
69
- (run.inSupport(candidate.id) && candidate.testRefs > 0)) {
65
+ if (symbol === undefined || run.inTest(candidate.id)) {
70
66
  continue;
71
67
  }
72
68
  if (symbol.kind === "interface") {
@@ -1,5 +1,6 @@
1
1
  import { KINDS } from "../kinds.js";
2
2
  import { UNUSED_PARAMETER, UNUSED_RESULT } from "../intra-function-parts.js";
3
+ import { reachedWithTheTests } from "../sweep.js";
3
4
  /** A list of rules as a sentence names them. */
4
5
  function spelled(rules) {
5
6
  if (rules.length < 2) {
@@ -39,12 +40,23 @@ function reported(input, part, callersUnknown) {
39
40
  /**
40
41
  * The findings of the intra-function family, `DS1800` to `DS1899`, each about a part of
41
42
  * the declaration its reference names, at the part's own position, whatever the sweep
42
- * decided about that declaration. Each message names the other rules that report the
43
- * kind, as the vocabulary lists them, and so does the finding's details.
43
+ * decided about a production declaration. A part of test code is reported only where the
44
+ * run that counts test references reaches its declaration and that is no test of dead
45
+ * code. Each message and the finding's details name the other rules that report the kind.
44
46
  */
45
47
  export const intraFunction = (input) => {
46
48
  const union = input.swept.matrix.union;
47
49
  const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
50
+ const reached = reachedWithTheTests(union, [
51
+ ...input.swept.exempt.map((record) => record.id),
52
+ ...input.swept.heldByUnanswered,
53
+ ]);
54
+ const testsOfDeadCode = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
55
+ const judged = (id) => {
56
+ const at = union.at(id);
57
+ const testCode = union.test[at] === true || union.support[at] === true;
58
+ return !testCode || (reached[at] === true && !testsOfDeadCode.has(id));
59
+ };
48
60
  const unknown = new Set(union.rooted.flatMap((root) => {
49
61
  const symbol = union.symbols[root.at];
50
62
  return symbol === undefined ? [] : [symbol.id];
@@ -68,7 +80,7 @@ export const intraFunction = (input) => {
68
80
  for (const part of input.intraFunction.parts) {
69
81
  const declaration = byId.get(part.declaration);
70
82
  const overlap = KINDS.get(part.code)?.overlap ?? [];
71
- if (declaration === undefined || !reported(input, part, unknown)) {
83
+ if (declaration === undefined || !judged(declaration.id) || !reported(input, part, unknown)) {
72
84
  continue;
73
85
  }
74
86
  found.push({
@@ -40,9 +40,9 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
40
40
  /**
41
41
  * The code one candidate is reported under, or none, each symbol once under the most
42
42
  * specific code: a test of dead code first; no other test-file declaration, member of a
43
- * dead container or enum member nothing names, nor a test-support declaration test code
44
- * references; then deprecation, a test reference, and the unreferenced codes, an
45
- * unimportable unused export being the unreachable kind's.
43
+ * dead container or enum member nothing names; a test-support declaration test code
44
+ * references as test-only use; then deprecation, a test reference, and the unreferenced
45
+ * codes, an unimportable unused export being the unreachable kind's.
46
46
  */
47
47
  function codeOf(judged) {
48
48
  const { candidate, symbol } = judged;
@@ -56,8 +56,8 @@ function codeOf(judged) {
56
56
  if (symbol.kind === "enum-member" && !referenced) {
57
57
  return undefined;
58
58
  }
59
- if (judged.support && candidate.testRefs > 0) {
60
- return undefined;
59
+ if (candidate.heldByTests === candidate.id) {
60
+ return "DS1004";
61
61
  }
62
62
  if (candidate.productionRefs === 0 && judged.deprecated) {
63
63
  return "DS1006";
@@ -180,7 +180,6 @@ function familyFindings(input, fallen) {
180
180
  candidate,
181
181
  symbol,
182
182
  test: union.test[at] === true,
183
- support: union.support[at] === true,
184
183
  containerDead: containerDead && !fallen,
185
184
  deprecated: input.deprecated.has(symbol.id),
186
185
  unreachable: unreachable.has(symbol.id),
@@ -168,7 +168,8 @@ function unreachable(facts, at, symbol, candidate, input) {
168
168
  * The visibility-narrowing family over one run: `DS1101`, `DS1103` and `DS1104`. A finding
169
169
  * about a symbol a declared cross-language edge names is reported here like any other, and
170
170
  * the edge's evaluation then holds it pending, because the edge stands for a reference from
171
- * outside the file and the package whose use is decided in the other language.
171
+ * outside the file and the package whose use is decided in the other language. A
172
+ * declaration of a generated file is reported only where generated files are included.
172
173
  */
173
174
  export function narrowings(input) {
174
175
  const facts = factsOf(input.swept.matrix.union);
@@ -176,8 +177,12 @@ export function narrowings(input) {
176
177
  const closed = input.config.consumersComplete && declared.every((id) => loaded.includes(id));
177
178
  const world = { closed, writeOnly: writeOnlyDeclarations(input) };
178
179
  const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
180
+ const generated = input.config.analysis.generatedFiles === "include" ? undefined : input.files.generated;
179
181
  const findings = [];
180
182
  facts.graph.symbols.forEach((symbol, at) => {
183
+ if (generated?.has(symbol.position.path) === true) {
184
+ return;
185
+ }
181
186
  const candidate = candidates.get(symbol.id);
182
187
  const found = candidate === undefined
183
188
  ? narrowing(facts, at, symbol, world)