@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.3

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 (57) hide show
  1. package/dist/src/analysis.js +27 -5
  2. package/dist/src/augmentations.js +62 -0
  3. package/dist/src/build-scripts.js +437 -0
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/convention-rows.js +39 -2
  6. package/dist/src/conventions.js +15 -0
  7. package/dist/src/dependencies.js +89 -0
  8. package/dist/src/entry-points.js +9 -18
  9. package/dist/src/findings/completion.js +62 -26
  10. package/dist/src/findings/intra-function.js +4 -2
  11. package/dist/src/findings/reads-and-writes.js +7 -6
  12. package/dist/src/findings/unused-declarations.js +4 -1
  13. package/dist/src/graph.js +5 -3
  14. package/dist/src/intra-function-parts.js +45 -17
  15. package/dist/src/kinds.js +7 -7
  16. package/dist/src/manifest.js +18 -0
  17. package/dist/src/matrix.js +38 -4
  18. package/dist/src/report.js +23 -4
  19. package/dist/src/reporters/sarif.js +12 -8
  20. package/dist/src/roots.js +138 -11
  21. package/dist/src/serialization-contract.js +258 -8
  22. package/dist/src/sweep.js +7 -9
  23. package/dist/src/test-globals.js +234 -0
  24. package/dist/src/type-errors.js +11 -3
  25. package/dist/src/unseen-loads.js +107 -0
  26. package/dist/src/verbs/analyze.js +24 -2
  27. package/dist/src/yaml-document.js +355 -0
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +45 -7
  31. package/src/augmentations.ts +76 -1
  32. package/src/build-scripts.ts +564 -0
  33. package/src/conformance-record.ts +1 -1
  34. package/src/convention-rows.ts +47 -2
  35. package/src/conventions.ts +16 -0
  36. package/src/dependencies.ts +96 -0
  37. package/src/entry-points.ts +10 -25
  38. package/src/file-facts.ts +3 -0
  39. package/src/findings/completion.ts +72 -28
  40. package/src/findings/intra-function.ts +4 -2
  41. package/src/findings/reads-and-writes.ts +7 -6
  42. package/src/findings/unused-declarations.ts +6 -1
  43. package/src/graph.ts +5 -3
  44. package/src/intra-function-parts.ts +55 -16
  45. package/src/kinds.ts +7 -7
  46. package/src/manifest.ts +21 -3
  47. package/src/matrix.ts +51 -4
  48. package/src/report.ts +56 -16
  49. package/src/reporters/sarif.ts +16 -8
  50. package/src/roots.ts +162 -12
  51. package/src/serialization-contract.ts +309 -7
  52. package/src/sweep.ts +21 -9
  53. package/src/test-globals.ts +275 -0
  54. package/src/type-errors.ts +14 -3
  55. package/src/unseen-loads.ts +145 -0
  56. package/src/verbs/analyze.ts +28 -2
  57. package/src/yaml-document.ts +377 -0
@@ -146,8 +146,8 @@ const SCRIPT = "{js,jsx,ts,tsx}";
146
146
  const MODULE = "{js,mjs,ts,mts}";
147
147
  const ANY_MODULE = "{js,mjs,cjs,ts,mts,cts}";
148
148
  const VITE_CONFIG = `vite.config.${ANY_MODULE}`;
149
- /** The legacy configuration files of ESLint that hold JSON or a CommonJS module. */
150
- const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{js,cjs}"];
149
+ /** The legacy configuration files of ESLint: JSON, YAML or a CommonJS module. */
150
+ const ESLINTRC = [".eslintrc", ".eslintrc.json", ".eslintrc.{yml,yaml}", ".eslintrc.{js,cjs}"];
151
151
 
152
152
  const SVELTEKIT_ROUTE_FILES = "+{page,layout,server,page.server,layout.server,error}";
153
153
 
@@ -209,6 +209,35 @@ const ANGULAR_PACKAGE_KEYS: readonly PackageKey[] = [
209
209
  "scripts",
210
210
  ].map((key) => ({ key, files: ["angular.json"] }));
211
211
 
212
+ /**
213
+ * The row of a code generator that writes its output below one directory its
214
+ * configuration's `property` sets, `fallback` where no configuration file sets it, so
215
+ * every file there is a generated file.
216
+ */
217
+ function outputDirectoryRow(row: {
218
+ readonly name: string;
219
+ readonly package: string;
220
+ readonly range: string;
221
+ readonly files: readonly string[];
222
+ readonly property: string;
223
+ readonly fallback: string;
224
+ }): ConventionRow {
225
+ return {
226
+ name: row.name,
227
+ package: row.package,
228
+ range: row.range,
229
+ entries: [],
230
+ generated: ["<output>"],
231
+ moves: [
232
+ {
233
+ id: "output",
234
+ defaults: [row.fallback],
235
+ readings: [{ property: row.property, files: row.files }],
236
+ },
237
+ ],
238
+ };
239
+ }
240
+
212
241
  /**
213
242
  * The test runners and the assertion, mock and runner libraries whose import is test
214
243
  * evidence beside each runner a row reads test files for. Each applies whether or not a
@@ -299,6 +328,7 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
299
328
  range: ">=4.0.0 <10.0.0",
300
329
  entries: [],
301
330
  moves: [],
331
+ packageKeys: [{ key: "parser", files: ESLINTRC }],
302
332
  prefixedNames: [
303
333
  { property: "plugins", files: ESLINTRC, prefix: "eslint-plugin" },
304
334
  { property: "extends", files: ESLINTRC, prefix: "eslint-plugin", marker: "plugin:" },
@@ -414,6 +444,14 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
414
444
  { key: "oclif.hooks", directory: false },
415
445
  ],
416
446
  },
447
+ outputDirectoryRow({
448
+ name: "panda",
449
+ package: "@pandacss/dev",
450
+ range: ">=0.1.0 <2.0.0",
451
+ files: [`panda.config.${ANY_MODULE}`],
452
+ property: "outdir",
453
+ fallback: "styled-system",
454
+ }),
417
455
  {
418
456
  name: "parcel",
419
457
  package: "parcel",
@@ -481,6 +519,13 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
481
519
  },
482
520
  ],
483
521
  },
522
+ {
523
+ name: "react-scripts",
524
+ package: "react-scripts",
525
+ range: ">=1.0.0 <6.0.0",
526
+ entries: [`src/index.${SCRIPT}`, "src/setupProxy.js", `src/setupTests.${SCRIPT}`],
527
+ moves: [],
528
+ },
484
529
  {
485
530
  name: "remix",
486
531
  package: "@remix-run/dev",
@@ -66,6 +66,7 @@ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from
66
66
  import type { SetupFailure } from "./setup-failure.ts";
67
67
  import type { SourceFiles } from "./source-files.ts";
68
68
  import { satisfies } from "./version-range.ts";
69
+ import { yamlDocument } from "./yaml-document.ts";
69
70
 
70
71
  /** One manifest the analysis reads. */
71
72
  export interface ConventionManifest {
@@ -151,6 +152,13 @@ type ParseSource = (fileName: string, text: string) => SourceFile;
151
152
 
152
153
  const JSON_EXTENSION = ".json";
153
154
 
155
+ /**
156
+ * A configuration file its tool reads as a YAML document, read as the JSON document it
157
+ * denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
158
+ * line would name the wrong line.
159
+ */
160
+ const YAML_NAME = /\.ya?ml$/u;
161
+
154
162
  /** A configuration file with no extension that its tool reads as a JSON document. */
155
163
  const JSON_RC = /^\.[\w-]+rc$/u;
156
164
 
@@ -883,6 +891,14 @@ function readingFiles(
883
891
  } catch {
884
892
  continue;
885
893
  }
894
+ if (YAML_NAME.test(name)) {
895
+ const value = yamlDocument(text);
896
+ if (value !== undefined) {
897
+ const json = JSON.stringify(value);
898
+ found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
899
+ }
900
+ continue;
901
+ }
886
902
  found.push({
887
903
  name,
888
904
  text,
@@ -871,6 +871,98 @@ function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string>
871
871
  return found;
872
872
  }
873
873
 
874
+ /**
875
+ * The runtime flags whose value is a module loaded before the entry, as `--flag value` or
876
+ * `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
877
+ * https://nodejs.org/api/cli.html#--experimental-loadermodule,
878
+ * https://nodejs.org/api/cli.html#-r---require-module
879
+ */
880
+ const PRELOAD_FLAGS: ReadonlySet<string> = new Set([
881
+ "--experimental-loader",
882
+ "--import",
883
+ "--loader",
884
+ "--require",
885
+ "-r",
886
+ ]);
887
+
888
+ /**
889
+ * The programs that pass a preload flag to the runtime they start: Node itself, and the
890
+ * runners that forward Node's flags or read the same `--require`:
891
+ * https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
892
+ * https://typestrong.org/ts-node/docs/options#require,
893
+ * https://github.com/remy/nodemon#running-non-node-scripts
894
+ */
895
+ const PRELOADING_PROGRAMS: ReadonlySet<string> = new Set([
896
+ "mocha",
897
+ "node",
898
+ "nodemon",
899
+ "ts-node",
900
+ "tsx",
901
+ ]);
902
+
903
+ /** The programs that run the command after them, and an environment assignment before it. */
904
+ const LAUNCHERS: ReadonlySet<string> = new Set(["cross-env", "env", "npx"]);
905
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
906
+
907
+ /** The commands one script string runs, split at the shell's list and pipeline operators. */
908
+ const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
909
+
910
+ /**
911
+ * The packages one manifest's scripts preload by a runtime flag's bare module specifier,
912
+ * a flag counting only in a command whose program passes it to the runtime, and for Node
913
+ * only before the script it runs, whose own arguments come after it.
914
+ */
915
+ function scriptPreloadsOf(manifest: Record<string, unknown>): ReadonlySet<string> {
916
+ const scripts = manifest["scripts"];
917
+ const found = new Set<string>();
918
+ if (!isRecord(scripts)) {
919
+ return found;
920
+ }
921
+ for (const script of Object.values(scripts)) {
922
+ if (typeof script !== "string") {
923
+ continue;
924
+ }
925
+ for (const command of script.split(COMMAND_SEPARATORS)) {
926
+ const tokens = scriptTokens(command);
927
+ let at = 0;
928
+ while (
929
+ at < tokens.length &&
930
+ (ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))
931
+ ) {
932
+ at += 1;
933
+ }
934
+ const path = tokens[at] ?? "";
935
+ const program = path.slice(path.lastIndexOf("/") + 1);
936
+ if (!PRELOADING_PROGRAMS.has(program)) {
937
+ continue;
938
+ }
939
+ for (at += 1; at < tokens.length; at += 1) {
940
+ const token = tokens[at] ?? "";
941
+ if (!token.startsWith("-")) {
942
+ if (program === "node") {
943
+ break;
944
+ }
945
+ continue;
946
+ }
947
+ const equals = token.indexOf("=");
948
+ const flag = equals < 0 ? token : token.slice(0, equals);
949
+ if (!PRELOAD_FLAGS.has(flag)) {
950
+ continue;
951
+ }
952
+ if (equals < 0) {
953
+ at += 1;
954
+ }
955
+ const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
956
+ const name = value === undefined ? undefined : packageOfSpecifier(value);
957
+ if (name !== undefined) {
958
+ found.add(name);
959
+ }
960
+ }
961
+ }
962
+ }
963
+ return found;
964
+ }
965
+
874
966
  /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
875
967
  function isInside(path: string, dir: string): boolean {
876
968
  return dir === "" || path.startsWith(`${dir}/`);
@@ -976,12 +1068,16 @@ export function dependenciesOf(
976
1068
  continue;
977
1069
  }
978
1070
  const tokens = scriptTokensOf(read.value);
1071
+ const preloads = scriptPreloadsOf(read.value);
979
1072
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
980
1073
  const found = installedOf(host, dir, dependency.name);
981
1074
  if (found === undefined && uninstalledHere.has(dir)) {
982
1075
  continue;
983
1076
  }
984
1077
  declared.push(dependency);
1078
+ if (preloads.has(dependency.name)) {
1079
+ ran.add(declarationKey(dependency));
1080
+ }
985
1081
  if (found === undefined) {
986
1082
  continue;
987
1083
  }
@@ -14,7 +14,6 @@
14
14
  import {
15
15
  isArrayLiteralExpression,
16
16
  isAsExpression,
17
- isBinaryExpression,
18
17
  isCallExpression,
19
18
  isExportAssignment,
20
19
  isIdentifier,
@@ -27,7 +26,6 @@ import {
27
26
  isPropertyAssignment,
28
27
  isSatisfiesExpression,
29
28
  isStringLiteral,
30
- isTemplateExpression,
31
29
  SyntaxKind,
32
30
  type Node,
33
31
  type SourceFile,
@@ -40,6 +38,7 @@ import {
40
38
  type ParseFile,
41
39
  } from "./configuration-files.ts";
42
40
  import type { Selected } from "./conventions.ts";
41
+ import { buildScriptEntries } from "./build-scripts.ts";
43
42
  import { globExpression } from "./glob.ts";
44
43
  import type { Host } from "./host.ts";
45
44
  import { htmlEntries } from "./html-entries.ts";
@@ -48,6 +47,7 @@ import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
48
47
  import { UNANSWERED } from "./query.ts";
49
48
  import type { ProjectView } from "./session.ts";
50
49
  import type { SourceFiles } from "./source-files.ts";
50
+ import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.ts";
51
51
  import { workflowEntries } from "./workflow-steps.ts";
52
52
 
53
53
  /** Which rule entered a file. */
@@ -56,6 +56,8 @@ export type EntryRule =
56
56
  | "configuration-file"
57
57
  /** A file a relative string of a configuration module or a JSON configuration file names. */
58
58
  | "configuration-string"
59
+ /** An entry a build script's bundler call names, whose exports the bundle exposes. */
60
+ | "bundler-entry"
59
61
  /** The test runner: its workspace configuration, its setup files and the test files. */
60
62
  | "test-runner"
61
63
  /** The test files below the browser-test runner's test directory. */
@@ -324,10 +326,7 @@ function configurationEntries(
324
326
  strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
325
327
  }
326
328
  for (const { file } of outside) {
327
- strings(
328
- dirnamePath(file.fileName),
329
- moduleStrings(file).map((one) => one.text),
330
- );
329
+ strings(dirnamePath(file.fileName), unselected(file));
331
330
  }
332
331
  return found;
333
332
  }
@@ -541,18 +540,6 @@ function workersOf<Brand>(
541
540
  return found;
542
541
  }
543
542
 
544
- /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
545
- function leadingLiteral(node: Node): string | undefined {
546
- const at = unwrapped(node);
547
- if (isTemplateExpression(at)) {
548
- return at.head.text;
549
- }
550
- if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
551
- return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
552
- }
553
- return undefined;
554
- }
555
-
556
543
  /** Whether one call is `import.meta.glob(...)`. */
557
544
  function isMetaGlob(node: Node): boolean {
558
545
  if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
@@ -579,13 +566,8 @@ function computedImportsOf(file: SourceFile, files: SourceFiles): readonly Entry
579
566
  const dir = dirnamePath(file.fileName);
580
567
  const found: EntryPoint[] = [];
581
568
  const below = (prefix: string): void => {
582
- const target = resolvePath(dir, prefix);
583
- if (
584
- !isModuleRelative(prefix) ||
585
- !prefix.endsWith("/") ||
586
- target === dir ||
587
- relativePath(target, dir) !== undefined
588
- ) {
569
+ const target = computedLoadDirectory(dir, prefix);
570
+ if (target === undefined) {
589
571
  return;
590
572
  }
591
573
  for (const candidate of files.byName.values()) {
@@ -680,6 +662,9 @@ export function entryPoints<Brand>(
680
662
  ...(names === undefined ? {} : { names }),
681
663
  });
682
664
  }
665
+ for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
666
+ found.push({ file, rule: "bundler-entry", source, exports: true });
667
+ }
683
668
  for (const { file, source } of workflowEntries(host, targetRoot, files)) {
684
669
  found.push({ file, rule: "workflow-step", source, exports: true });
685
670
  }
package/src/file-facts.ts CHANGED
@@ -11,6 +11,7 @@ import { packageScope } from "./inventory.ts";
11
11
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
12
12
  import { renderRef } from "./ref.ts";
13
13
  import type { ProjectView } from "./session.ts";
14
+ import type { UnseenLoad } from "./unseen-loads.ts";
14
15
 
15
16
  /** A file name the compiler reads as a source file. */
16
17
  const SOURCE_FILE = /\.(?:ts|tsx|mts|cts)$/u;
@@ -51,6 +52,8 @@ export interface FileFacts {
51
52
  readonly generated?: ReadonlySet<string>;
52
53
  /** The property names an `in` expression of a production file tests by a literal. */
53
54
  readonly inOperands?: ReadonlySet<string>;
55
+ /** The loads of production files whose target the analysis cannot resolve. */
56
+ readonly unseenLoads?: readonly UnseenLoad[];
54
57
  }
55
58
 
56
59
  /** The line of the last character of one text, counted from one; an empty text is one line. */
@@ -132,6 +132,20 @@ function reachabilityClasses(input: EmitterInput): (id: string) => Confidence {
132
132
  };
133
133
  }
134
134
 
135
+ /**
136
+ * Whether one test of dead code references a target symbol that is published API of a run
137
+ * that declares no consumer, which outside programs the run cannot see may use, so the
138
+ * target counts as live for the test and the test is no test of dead code.
139
+ */
140
+ export function testsUnseenAPI(input: EmitterInput): (id: string) => boolean {
141
+ if (input.boundary.consumers.declared.length > 0) {
142
+ return () => false;
143
+ }
144
+ const union = input.swept.matrix.union;
145
+ const published = publishedIds(union);
146
+ return (id) => testTargets(union, id).some((target) => published.has(target));
147
+ }
148
+
135
149
  /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
136
150
  function testTargets(union: Graph, id: string): readonly string[] {
137
151
  return (union.out[union.at(id)] ?? []).flatMap((edge) => {
@@ -197,17 +211,21 @@ function testUsedSubjects(input: EmitterInput): {
197
211
 
198
212
  /**
199
213
  * The ceiling the roots the run found put on one dead subject: a subject other declarations
200
- * reference, found dead only because no root reaches them, is `probable` unless
201
- * the configuration declares the roots complete, because a root the analysis cannot see
202
- * would make it live; every other declaration has none below `certain`.
214
+ * reference, found dead only because no root reaches them, is `probable` unless the
215
+ * configuration declares the roots complete and no configuration holding the subject loads
216
+ * a module the analysis cannot resolve, because a root the analysis cannot see would make
217
+ * it live; every other declaration has none below `certain`.
203
218
  */
204
219
  function rootsCeiling(
205
220
  input: EmitterInput,
206
221
  finding: Finding,
207
222
  candidate: Candidate | undefined,
223
+ configurations: readonly string[],
208
224
  ): Confidence {
209
225
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
210
- return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
226
+ const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
227
+ const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
228
+ return unreachable && !complete ? "probable" : "certain";
211
229
  }
212
230
 
213
231
  /**
@@ -225,9 +243,28 @@ function findingComponents(
225
243
  held.set(member, component);
226
244
  }
227
245
  }
246
+ const listed = membersListed(swept);
247
+ return (id) => {
248
+ const component = held.get(id);
249
+ return component === undefined
250
+ ? undefined
251
+ : {
252
+ id: component.id,
253
+ root: component.roots.includes(id),
254
+ symbolCount: component.members.length,
255
+ deletableLines: component.deletableLines,
256
+ ...(cascade === "full" ? { members: listed(component.members) } : {}),
257
+ };
258
+ };
259
+ }
260
+
261
+ /** The members of a component as the run lists them, positioned and in the canonical order. */
262
+ function membersListed(
263
+ swept: RunSweep,
264
+ ): (members: readonly string[]) => readonly PositionedSymbol[] {
228
265
  const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
229
- const listed = (component: Component): readonly PositionedSymbol[] =>
230
- component.members
266
+ return (members) =>
267
+ members
231
268
  .flatMap((member) => {
232
269
  const symbol = symbols.get(member);
233
270
  return symbol === undefined
@@ -244,18 +281,6 @@ function findingComponents(
244
281
  ];
245
282
  })
246
283
  .sort((a, b) => byPosition(a.position, b.position));
247
- return (id) => {
248
- const component = held.get(id);
249
- return component === undefined
250
- ? undefined
251
- : {
252
- id: component.id,
253
- root: component.roots.includes(id),
254
- symbolCount: component.members.length,
255
- deletableLines: component.deletableLines,
256
- ...(cascade === "full" ? { members: listed(component) } : {}),
257
- };
258
- };
259
284
  }
260
285
 
261
286
  /**
@@ -357,12 +382,13 @@ export function completed(
357
382
  // A join keeps its first component's identifier, so a minted one counts past every computed one.
358
383
  let minted = computed;
359
384
  const dead = new Set(components.map((component) => component.id));
360
- const fallingWith = new Map<string, number>();
361
- for (const { heldByTests } of swept.sweep.candidates) {
385
+ const fallingWith = new Map<string, string[]>();
386
+ for (const { id, heldByTests } of swept.sweep.candidates) {
362
387
  if (heldByTests !== undefined) {
363
- fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
388
+ fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
364
389
  }
365
390
  }
391
+ const listed = membersListed(swept);
366
392
 
367
393
  const done = findings.map((finding): CompletedFinding => {
368
394
  const row = kinds.get(finding.code);
@@ -413,11 +439,25 @@ export function completed(
413
439
  let component = found;
414
440
  if (component === undefined) {
415
441
  minted += 1;
442
+ const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
416
443
  component = {
417
444
  id: componentId(minted),
418
445
  root: true,
419
- symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
446
+ symbolCount: falling?.length ?? 1,
420
447
  deletableLines: 0,
448
+ ...(input.config.reporters.cascade !== "full"
449
+ ? {}
450
+ : falling === undefined
451
+ ? {
452
+ members: [
453
+ {
454
+ ref: finding.symbol.ref,
455
+ name: finding.symbol.name,
456
+ position: finding.position,
457
+ },
458
+ ],
459
+ }
460
+ : { members: listed(falling) }),
421
461
  };
422
462
  }
423
463
  const counts =
@@ -431,8 +471,16 @@ export function completed(
431
471
  ? candidate !== undefined && testUsed.declarations.has(judged.id)
432
472
  : finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
433
473
  const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
474
+ const configurations =
475
+ candidate?.configurations ??
476
+ (judged === undefined && !part
477
+ ? swept.matrix.configurations
478
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
434
479
  const untested = capped(
435
- capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate)),
480
+ capped(
481
+ capped(ownClass, row.maxClass),
482
+ rootsCeiling(input, finding, candidate, configurations),
483
+ ),
436
484
  judged === undefined ? "certain" : ceilingOf(judged.id),
437
485
  );
438
486
  const generated = input.files.generated?.has(finding.position.path) === true;
@@ -466,11 +514,7 @@ export function completed(
466
514
  generated,
467
515
  component,
468
516
  retainedBy: [],
469
- configurations:
470
- candidate?.configurations ??
471
- (judged === undefined && !part
472
- ? swept.matrix.configurations
473
- : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
517
+ configurations,
474
518
  consumersLoaded: input.boundary.consumers.loaded,
475
519
  fixability,
476
520
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -42,7 +42,8 @@ type ParameterVerdict =
42
42
  * caller may pass a function used as a value more arguments than it declares, so there one
43
43
  * a read parameter follows is renamed to an underscore name, which the compilers read as
44
44
  * declared unread, and one no read parameter follows is deleted: kept where the call's
45
- * inference may read its annotation, at `probable` where nothing reads the annotation.
45
+ * inference may read its annotation, at `probable` where nothing reads the annotation or
46
+ * where it is a name a contextually typed object pattern binds.
46
47
  */
47
48
  function parameterVerdict(
48
49
  part: PartFact,
@@ -63,7 +64,8 @@ function parameterVerdict(
63
64
  if (place?.annotated === "steers") {
64
65
  return undefined;
65
66
  }
66
- return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
67
+ const unseen = place?.annotated === "free" || place?.contextualPattern === true;
68
+ return { fix: "delete", confidence: unseen ? "probable" : "certain" };
67
69
  }
68
70
 
69
71
  /**
@@ -8,8 +8,8 @@ import type { Emitter, EmitterInput } from "./emitter.ts";
8
8
  /** The codes of this family. */
9
9
  type ReadsAndWritesCode = "DS1301" | "DS1302" | "DS1303";
10
10
 
11
- /** The containers whose type parameters `DS1303` reports: a function and a class method. */
12
- const SIGNATURES: ReadonlySet<string> = new Set(["function", "method"]);
11
+ /** The containers whose type parameters `DS1303` reports: a function, a class method and a type alias. */
12
+ const SIGNATURES: ReadonlySet<string> = new Set(["function", "method", "type"]);
13
13
 
14
14
  /** What the family reads of one run, indexed by declaration. */
15
15
  interface Run {
@@ -225,10 +225,11 @@ function unusedEnumMembers(run: Run): readonly Finding[] {
225
225
  }
226
226
 
227
227
  /**
228
- * `DS1303`: a type parameter of a function or a class method that neither the
229
- * declaration's signature nor its body names. A type parameter of a class, an interface,
230
- * a type alias or a method signature belongs to a type declaration and is never
231
- * reported, because a phantom parameter makes two instantiations distinct types.
228
+ * `DS1303`: a type parameter of a function, a class method or a type alias that neither
229
+ * the declaration's signature nor its body names. An alias names no type of its own, so
230
+ * a parameter its type never names makes no two instantiations distinct. One of a class,
231
+ * an interface or a method signature is never reported: a class or an interface merges
232
+ * only with declarations of the same parameters.
232
233
  */
233
234
  function unusedTypeParameters(run: Run): readonly Finding[] {
234
235
  return unnamed(run, "type-parameter")
@@ -2,6 +2,7 @@ import type { Finding } from "../finding.ts";
2
2
  import type { InventorySymbol, SymbolKind } from "../inventory.ts";
3
3
  import type { MatrixCandidate } from "../matrix.ts";
4
4
  import type { Emitter, EmitterInput } from "./emitter.ts";
5
+ import { testsUnseenAPI } from "./completion.ts";
5
6
  import { unreachableExports } from "./visibility-narrowing.ts";
6
7
 
7
8
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
@@ -55,6 +56,8 @@ interface Judged {
55
56
  readonly deprecated: boolean;
56
57
  /** Whether the unreachable-export kind reports it. */
57
58
  readonly unreachable: boolean;
59
+ /** Whether it is a test with a target that is published API no declared consumer judges. */
60
+ readonly testsUnseenAPI: boolean;
58
61
  }
59
62
 
60
63
  /**
@@ -68,7 +71,7 @@ interface Judged {
68
71
  function codeOf(judged: Judged): string | undefined {
69
72
  const { candidate, symbol } = judged;
70
73
  if (candidate.testOfDeadCode) {
71
- return "DS1005";
74
+ return judged.testsUnseenAPI ? undefined : "DS1005";
72
75
  }
73
76
  if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
74
77
  return undefined;
@@ -241,6 +244,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
241
244
  const union = input.swept.matrix.union;
242
245
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
243
246
  const unreachable = unreachableExports(input);
247
+ const unseenAPI = testsUnseenAPI(input);
244
248
  let brands: ReadonlySet<string> | undefined;
245
249
 
246
250
  const found: Finding[] = [];
@@ -260,6 +264,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
260
264
  test: union.test[at] === true,
261
265
  deprecated: input.deprecated.has(symbol.id),
262
266
  unreachable: unreachable.has(symbol.id),
267
+ testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
263
268
  };
264
269
  const code = codeOf(judged);
265
270
  if (
package/src/graph.ts CHANGED
@@ -41,9 +41,9 @@ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
41
41
  * Whether a root of each kind names a caller, which holds it live under both relations: a
42
42
  * tool reads what it loads and what its strings name, a runtime runs a test, setup or
43
43
  * worker file, a page's module script, a command, a script or a workflow step, the manifest
44
- * names what enters the package, an alias global stands for its export, and an entry file,
45
- * a configured root and a pattern assert a caller. A library's published API only supposes
46
- * an unseen consumer, so it is live under reachability alone.
44
+ * names what enters the package, an alias global stands for its export, an outside
45
+ * declaration names a global interface merging into it, and an entry file, a configured root
46
+ * and a pattern assert a caller. A published API supposes a consumer: reachability alone.
47
47
  */
48
48
  const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
49
49
  "entry-file": true,
@@ -56,8 +56,10 @@ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
56
56
  pattern: true,
57
57
  "type-error": true,
58
58
  "type-query-alias": true,
59
+ "global-augmentation": true,
59
60
  "configuration-file": true,
60
61
  "configuration-string": true,
62
+ "bundler-entry": true,
61
63
  "test-runner": true,
62
64
  "browser-tests": true,
63
65
  worker: true,