@cplieger/deadset-ts 7.0.0-dev.2 → 7.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 (75) hide show
  1. package/dist/src/analysis.js +89 -44
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/components.js +62 -20
  4. package/dist/src/configuration-files.js +5 -5
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +71 -14
  7. package/dist/src/convention-rows.js +6 -2
  8. package/dist/src/conventions.js +3 -1
  9. package/dist/src/dependencies.js +80 -26
  10. package/dist/src/element-access.js +341 -0
  11. package/dist/src/erased-stores.js +317 -0
  12. package/dist/src/findings/completion.js +46 -15
  13. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  14. package/dist/src/findings/emitters.js +1 -1
  15. package/dist/src/findings/self-check.js +17 -4
  16. package/dist/src/findings-pass.js +6 -5
  17. package/dist/src/in-operands.js +30 -0
  18. package/dist/src/indexed-access.js +264 -125
  19. package/dist/src/intra-function-parts.js +25 -11
  20. package/dist/src/inventory.js +25 -0
  21. package/dist/src/keyof-comparisons.js +170 -0
  22. package/dist/src/kinds.js +17 -7
  23. package/dist/src/manifest.js +6 -16
  24. package/dist/src/matrix.js +20 -2
  25. package/dist/src/query.js +3 -0
  26. package/dist/src/references.js +12 -1
  27. package/dist/src/report.js +1 -1
  28. package/dist/src/reporters/sarif.js +4 -0
  29. package/dist/src/serialization-contract.js +43 -14
  30. package/dist/src/session.js +12 -7
  31. package/dist/src/suppress-file.js +58 -6
  32. package/dist/src/suppress-inline.js +13 -4
  33. package/dist/src/suppress.js +54 -24
  34. package/dist/src/unimplemented-callees.js +312 -0
  35. package/dist/src/value-reach.js +7 -7
  36. package/dist/src/verbs/analyze.js +11 -9
  37. package/jsr.json +1 -1
  38. package/package.json +2 -2
  39. package/src/analysis.ts +135 -52
  40. package/src/augmentations.ts +14 -11
  41. package/src/components.ts +103 -21
  42. package/src/configuration-files.ts +5 -4
  43. package/src/conformance-record.ts +1 -1
  44. package/src/contextual-uses.ts +81 -17
  45. package/src/convention-rows.ts +6 -2
  46. package/src/conventions.ts +10 -1
  47. package/src/dependencies.ts +107 -30
  48. package/src/element-access.ts +446 -0
  49. package/src/erased-stores.ts +413 -0
  50. package/src/file-facts.ts +2 -0
  51. package/src/findings/completion.ts +54 -20
  52. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  53. package/src/findings/emitters.ts +1 -1
  54. package/src/findings/self-check.ts +18 -4
  55. package/src/findings-pass.ts +12 -5
  56. package/src/in-operands.ts +45 -0
  57. package/src/indexed-access.ts +303 -145
  58. package/src/intra-function-parts.ts +41 -16
  59. package/src/inventory.ts +32 -0
  60. package/src/keyof-comparisons.ts +211 -0
  61. package/src/kinds.ts +20 -7
  62. package/src/manifest.ts +6 -21
  63. package/src/matrix.ts +34 -2
  64. package/src/query.ts +10 -0
  65. package/src/references.ts +23 -1
  66. package/src/report.ts +3 -1
  67. package/src/reporters/sarif.ts +4 -0
  68. package/src/serialization-contract.ts +53 -14
  69. package/src/session.ts +34 -29
  70. package/src/suppress-file.ts +68 -5
  71. package/src/suppress-inline.ts +14 -4
  72. package/src/suppress.ts +63 -28
  73. package/src/unimplemented-callees.ts +411 -0
  74. package/src/value-reach.ts +11 -7
  75. package/src/verbs/analyze.ts +15 -9
@@ -426,7 +426,7 @@ export function configurationFiles(
426
426
  outside:
427
427
  parse === undefined
428
428
  ? []
429
- : outsideModules(host, [...dirs], files, holdsManifest, parse, named),
429
+ : outsideModules(host, root, [...dirs], files, holdsManifest, parse, named),
430
430
  };
431
431
  }
432
432
 
@@ -480,11 +480,12 @@ export function resolveRelativeSpecifier(
480
480
 
481
481
  /**
482
482
  * The configuration modules of the directories that hold a manifest which the program
483
- * does not hold, and the files they import by a relative specifier, until no further
484
- * file joins. A file that cannot be read is skipped.
483
+ * does not hold, and the project files below `root` they import by a relative specifier,
484
+ * until no further file joins. A file that cannot be read is skipped.
485
485
  */
486
486
  function outsideModules(
487
487
  host: Host,
488
+ root: string,
488
489
  dirs: readonly string[],
489
490
  files: SourceFiles,
490
491
  holdsManifest: (dir: string) => boolean,
@@ -533,7 +534,7 @@ function outsideModules(
533
534
  outside.manifestDirs.push(next.manifestDir);
534
535
  for (const specifier of relativeSpecifiers(outside.file)) {
535
536
  const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
536
- if (reached !== undefined) {
537
+ if (reached !== undefined && isProjectPath(root, reached)) {
537
538
  pending.push({ path: reached, manifestDir: next.manifestDir });
538
539
  }
539
540
  }
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
7
+ digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -8,10 +8,13 @@
8
8
  */
9
9
 
10
10
  import {
11
+ isClassDeclaration,
12
+ isClassExpression,
11
13
  isComputedPropertyName,
12
14
  isGetAccessorDeclaration,
13
15
  isIdentifier,
14
16
  isMethodDeclaration,
17
+ isMethodSignatureDeclaration,
15
18
  isNoSubstitutionTemplateLiteral,
16
19
  isNumericLiteral,
17
20
  isObjectLiteralExpression,
@@ -24,7 +27,7 @@ import {
24
27
  type Node,
25
28
  type ObjectLiteralExpression,
26
29
  } from "@typescript/native/unstable/ast";
27
- import { SignatureKind, SymbolFlags, type Type } from "@typescript/native/unstable/sync";
30
+ import { SignatureKind, type NodeHandle, type Type } from "@typescript/native/unstable/sync";
28
31
  import type { AliasChains } from "./alias-chain.ts";
29
32
  import type { Inventory, SymbolKind } from "./inventory.ts";
30
33
  import { isAnswered, type PropertyTables } from "./query.ts";
@@ -115,11 +118,30 @@ interface Checked {
115
118
  /** One member of an object type, by the inventory's declarations and the checker's declaration handles. */
116
119
  interface Member {
117
120
  readonly ids: readonly string[];
121
+ /** The inventory's interface methods it is, which a position reads but no literal writes. */
122
+ readonly methods: readonly string[];
118
123
  readonly handles: ReadonlySet<string>;
119
124
  /** Whether a declaration of it is in one of the target's own files. */
120
125
  readonly own: boolean;
121
- /** Whether the checker reads it as an accessor. */
122
- readonly accessor: boolean;
126
+ /** The checker's declarations of it in the target's own files. */
127
+ readonly declarations: readonly NodeHandle[];
128
+ }
129
+
130
+ /**
131
+ * Whether an object spread copies a member a declaration declares: a method, a method
132
+ * signature and a class's accessor are no own data property of the value, while an
133
+ * accessor of an interface or a type literal describes one. It is decided on the
134
+ * declaration, because a mapped type over a class keeps the class's declarations on
135
+ * plain property symbols.
136
+ */
137
+ function copiedBySpread(node: Node): boolean {
138
+ if (isMethodDeclaration(node) || isMethodSignatureDeclaration(node)) {
139
+ return false;
140
+ }
141
+ return !(
142
+ (isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) &&
143
+ (isClassDeclaration(node.parent) || isClassExpression(node.parent))
144
+ );
123
145
  }
124
146
 
125
147
  /**
@@ -182,11 +204,14 @@ export function contextualUses<Brand>(
182
204
  ids: chains
183
205
  .declarationsOf(property)
184
206
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
207
+ methods: chains
208
+ .declarationsOf(property)
209
+ .filter((id) => kinds.get(id) === "interface-method"),
185
210
  handles: new Set(
186
211
  property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
187
212
  ),
188
213
  own: property.declarations.some((handle) => ownPaths.has(handle.path)),
189
- accessor: (property.flags & SymbolFlags.Accessor) !== 0,
214
+ declarations: property.declarations.filter((handle) => ownPaths.has(handle.path)),
190
215
  });
191
216
  }
192
217
  tables.set(type.id, table);
@@ -250,8 +275,10 @@ export function contextualUses<Brand>(
250
275
  cap,
251
276
  );
252
277
  /** The members a position may read structurally: those of object types, not of classes. */
253
- const readable = (member: Member): readonly string[] =>
254
- member.ids.filter((id) => kinds.get(id) === "type-member");
278
+ const readable = (member: Member): readonly string[] => [
279
+ ...member.ids.filter((id) => kinds.get(id) === "type-member"),
280
+ ...member.methods,
281
+ ];
255
282
  const carries = (part: Type): boolean =>
256
283
  [...membersOf(part).values()].some((member) => readable(member).length > 0);
257
284
  /** Whether an object type has members and declares none of them in the target. */
@@ -278,6 +305,20 @@ export function contextualUses<Brand>(
278
305
  return element;
279
306
  };
280
307
 
308
+ const argumentsOf = new Map<number, readonly Type[]>();
309
+ /** The type arguments of an instantiation of a generic type, read once per type. */
310
+ const typeArgumentsOf = (type: Type): readonly Type[] => {
311
+ const known = argumentsOf.get(type.id);
312
+ if (known !== undefined) {
313
+ return known;
314
+ }
315
+ lookups += type.isTypeReference() ? 1 : 0;
316
+ const held = queries.typeArguments(type);
317
+ const found = isAnswered(held) ? held : [];
318
+ argumentsOf.set(type.id, found);
319
+ return found;
320
+ };
321
+
281
322
  const calls = new Map<number, Called | undefined>();
282
323
  /**
283
324
  * The parameter and return types of a type's one call signature, or of its one construct
@@ -315,10 +356,11 @@ export function contextualUses<Brand>(
315
356
 
316
357
  /**
317
358
  * Each object type a value of `source` is checked as against the object types of
318
- * `target`: the value's own, and through array elements and through a function's
319
- * parameters, checked the other way, and its result. A source that is one of the
320
- * target's own object types matches it as it stands. Each pair is compared once, which
321
- * ends the walk over a recursive type.
359
+ * `target`: the value's own, and through array elements, a function's parameters,
360
+ * checked the other way, and result, and the type arguments two instantiations share
361
+ * by place, an array among them, as an array reaches `Iterable<T>`.
362
+ * A source that is one of the target's own object types matches it as it stands. Each
363
+ * pair is compared once, which ends the walk over a recursive type.
322
364
  */
323
365
  const compared = (
324
366
  source: Type | undefined,
@@ -344,9 +386,20 @@ export function contextualUses<Brand>(
344
386
  }
345
387
  const element = elementOf(one);
346
388
  const called = element === undefined ? callOf(one) : undefined;
389
+ const sourceArguments = typeArgumentsOf(one);
347
390
  for (const part of targets) {
391
+ const partElement = elementOf(part);
392
+ if (element !== undefined && partElement !== undefined) {
393
+ compared(element, partElement, into, seen);
394
+ continue;
395
+ }
396
+ const targetArguments = typeArgumentsOf(part);
397
+ // A defaulted trailing parameter, as `Iterable<T, TReturn, TNext>` has, leaves
398
+ // the places two instantiations share comparable.
399
+ sourceArguments.slice(0, targetArguments.length).forEach((argument, index) => {
400
+ compared(argument, targetArguments[index], into, seen);
401
+ });
348
402
  if (element !== undefined) {
349
- compared(element, elementOf(part), into, seen);
350
403
  continue;
351
404
  }
352
405
  const other = called === undefined ? undefined : callOf(part);
@@ -378,7 +431,7 @@ export function contextualUses<Brand>(
378
431
  if (element !== undefined) {
379
432
  return mayCarry(element, seen);
380
433
  }
381
- return callOf(part) !== undefined;
434
+ return callOf(part) !== undefined || typeArgumentsOf(part).some((one) => mayCarry(one, seen));
382
435
  });
383
436
  };
384
437
 
@@ -433,6 +486,19 @@ export function contextualUses<Brand>(
433
486
  reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
434
487
  }
435
488
  });
489
+ const copies = new Map<Member, boolean>();
490
+ /** Whether a spread copies the member: where any declaration of it declares data. */
491
+ const copied = (member: Member): boolean => {
492
+ let known = copies.get(member);
493
+ if (known === undefined) {
494
+ known = member.declarations.some((handle) => {
495
+ const node = project.declarationAt(handle)?.node;
496
+ return node === undefined || copiedBySpread(node);
497
+ });
498
+ copies.set(member, known);
499
+ }
500
+ return known;
501
+ };
436
502
  const spreads = literals.flatMap(({ literal, from }) =>
437
503
  literal.properties.flatMap((property) =>
438
504
  isSpreadAssignment(property) ? [{ value: property.expression, from }] : [],
@@ -448,12 +514,10 @@ export function contextualUses<Brand>(
448
514
  if (type === undefined || !isAnswered(type)) {
449
515
  return;
450
516
  }
451
- // A spread gets every own enumerable property, an overwritten key's getter included.
452
- // So it reads each data member but a class's accessor, which sits on the prototype with
453
- // the class's methods (never data members), and a private name, which is no property
517
+ // A spread gets every own enumerable property, an overwritten key's getter included,
518
+ // and no private name, which is no property
454
519
  // (https://tc39.es/ecma262/#sec-copydataproperties).
455
- const own = (member: Member, id: string): boolean =>
456
- !privateNames.has(id) && !(member.accessor && kinds.get(id) === "class-member");
520
+ const own = (member: Member, id: string): boolean => !privateNames.has(id) && copied(member);
457
521
  const ids = [
458
522
  ...new Set(
459
523
  partsOf(type).flatMap((part) =>
@@ -158,7 +158,8 @@ const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
158
158
 
159
159
  /**
160
160
  * The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
161
- * resolves `setupFiles` against that root (https://vitest.dev/config/setupfiles).
161
+ * resolves `setupFiles` and `globalSetup` against that root
162
+ * (https://vitest.dev/config/setupfiles, https://vitest.dev/config/globalsetup).
162
163
  */
163
164
  const VITEST_ROOTS = ["test.root", "root"] as const;
164
165
 
@@ -660,6 +661,9 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
660
661
  { property: "test.coverage.include", files: [VITEST_CONFIG] },
661
662
  { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
662
663
  ],
663
- testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
664
+ testFiles: [
665
+ { property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
666
+ { property: "test.globalSetup", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
667
+ ],
664
668
  },
665
669
  ];
@@ -71,6 +71,11 @@ export interface ConventionManifest {
71
71
  readonly dir: string;
72
72
  /** The manifest's path below the target root, `/`-separated. */
73
73
  readonly path: string;
74
+ /**
75
+ * Whether the manifest is a nested package's, whose install no run requires: an
76
+ * enabling package it declares and nothing installs applies no row and fails nothing.
77
+ */
78
+ readonly nested?: boolean;
74
79
  }
75
80
 
76
81
  /** One row a run applied, as the report lists it. */
@@ -1007,7 +1012,11 @@ export function readConventions(
1007
1012
  }
1008
1013
  const version = versions.get(row.package);
1009
1014
  if (version === undefined) {
1010
- if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
1015
+ if (
1016
+ missing.has(row.package) ||
1017
+ manifest.nested === true ||
1018
+ !declares(declared, row.package, REQUIRED_SECTIONS)
1019
+ ) {
1011
1020
  continue;
1012
1021
  }
1013
1022
  missing.add(row.package);
@@ -71,28 +71,37 @@ interface DeclaredDependency {
71
71
  /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
72
72
  export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
73
73
  return {
74
- packages: new Set(packages),
75
74
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
75
+ at: new Map([[ANYWHERE, new Set(packages)]]),
76
76
  unanswered: [],
77
77
  };
78
78
  }
79
79
 
80
+ /** The key of {@link ProjectNeeds.at} for a use no file of the target governs. */
81
+ const ANYWHERE = "";
82
+
80
83
  /** What the installed copy of one declared dependency states about itself. */
81
84
  interface InstalledDependency {
82
85
  /** The commands its manifest's `bin` declares, by name. */
83
86
  readonly commands: readonly string[];
87
+ /** The packages its manifest's `peerDependencies` names, optional ones included. */
88
+ readonly peers: readonly string[];
84
89
  }
85
90
 
86
91
  /** What one project's own files and configuration need from outside the target. */
87
92
  export interface ProjectNeeds {
88
- /** Every package a use names or resolves to. */
89
- readonly packages: ReadonlySet<string>;
90
93
  /**
91
94
  * Per declaration of the inventory, the packages the uses written inside it name; a
92
95
  * use is held by the innermost declaration around it. The empty key holds what the
93
96
  * compiler configuration names, which no deletion removes.
94
97
  */
95
98
  readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
99
+ /**
100
+ * Per absolute path of the file a use is written in, the packages it names, which a
101
+ * dependency only a manifest at or above that file declares serves. The empty key holds
102
+ * the uses no one file holds, which every manifest's dependency of that name serves.
103
+ */
104
+ readonly at: ReadonlyMap<string, ReadonlySet<string>>;
96
105
  /**
97
106
  * The files writing a module specifier the checker left unanswered, each of which may
98
107
  * use any dependency a manifest at or above it declares.
@@ -104,11 +113,14 @@ export interface ProjectNeeds {
104
113
  export interface Dependencies {
105
114
  /** Every declared dependency, manifest by manifest, in the order each writes them. */
106
115
  readonly declared: readonly DeclaredDependency[];
107
- /** Every package some project needs. */
116
+ /**
117
+ * The declared dependencies, by {@link declarationKey}, a use written at or below their
118
+ * manifest's directory needs, and each declared peer of a needed one.
119
+ */
108
120
  readonly needed: ReadonlySet<string>;
109
121
  /**
110
122
  * The declared dependencies whose command a script entry of the manifest declaring them,
111
- * or a workflow step for the target's manifest, runs, each by {@link declarationKey}.
123
+ * or a workflow step of the target, runs, each by {@link declarationKey}.
112
124
  */
113
125
  readonly ran: ReadonlySet<string>;
114
126
  /**
@@ -322,9 +334,10 @@ export function projectNeeds<Brand>(
322
334
  programPackages,
323
335
  );
324
336
  const uses = new Map<string, Set<string>>();
325
- const use = (owners: readonly string[], named: Iterable<string>): void => {
337
+ const at = new Map<string, Set<string>>();
338
+ const use = (owners: readonly string[], named: Iterable<string>, path: string): void => {
326
339
  for (const one of named) {
327
- packages.add(one);
340
+ at.set(path, (at.get(path) ?? new Set()).add(one));
328
341
  for (const owner of owners) {
329
342
  let set = uses.get(owner);
330
343
  if (set === undefined) {
@@ -335,11 +348,14 @@ export function projectNeeds<Brand>(
335
348
  }
336
349
  }
337
350
  };
338
- use([PROJECT_OWNER], packages);
351
+ use([PROJECT_OWNER], packages, project.configFile);
339
352
 
340
353
  const unanswered: string[] = [];
341
354
  for (const file of project.ownSourceFiles()) {
342
- if (fileNeeds(project, held, file, programPackages, use)) {
355
+ const inFile = (owners: readonly string[], named: Iterable<string>): void => {
356
+ use(owners, named, file.fileName);
357
+ };
358
+ if (fileNeeds(project, held, file, programPackages, inFile)) {
343
359
  unanswered.push(file.fileName);
344
360
  }
345
361
  }
@@ -363,14 +379,14 @@ export function projectNeeds<Brand>(
363
379
  for (const { file } of configuration.modules) {
364
380
  const names = declaredAt(file.fileName);
365
381
  for (const { node, text } of moduleStrings(file)) {
366
- use(ownersOf(file, held, node), dependenciesNamed(text, names));
382
+ use(ownersOf(file, held, node), dependenciesNamed(text, names), file.fileName);
367
383
  }
368
384
  }
369
385
  for (const { strings, manifestDirs } of configuration.documents) {
370
386
  for (const manifestDir of manifestDirs) {
371
387
  const names = declaredAt(joinPath(manifestDir, "package.json"));
372
388
  for (const text of strings) {
373
- use([PROJECT_OWNER], dependenciesNamed(text, names));
389
+ use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
374
390
  }
375
391
  }
376
392
  }
@@ -378,11 +394,11 @@ export function projectNeeds<Brand>(
378
394
  for (const manifestDir of manifestDirs) {
379
395
  const names = declaredAt(joinPath(manifestDir, "package.json"));
380
396
  for (const { text } of moduleStrings(file)) {
381
- use([PROJECT_OWNER], dependenciesNamed(text, names));
397
+ use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
382
398
  }
383
399
  }
384
400
  }
385
- return { packages, uses, unanswered };
401
+ return { uses, at, unanswered };
386
402
  }
387
403
 
388
404
  /** The declarations of the inventory a use written at one node belongs to. */
@@ -638,8 +654,10 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
638
654
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
639
655
  if (read !== undefined && isRecord(read.value)) {
640
656
  const bin = read.value["bin"];
657
+ const peers = read.value["peerDependencies"];
641
658
  // A string `bin` names one command, the package's name without its scope.
642
659
  return {
660
+ peers: isRecord(peers) ? Object.keys(peers) : [],
643
661
  commands:
644
662
  typeof bin === "string"
645
663
  ? bin === ""
@@ -711,13 +729,53 @@ function isInside(path: string, dir: string): boolean {
711
729
  return dir === "" || path.startsWith(`${dir}/`);
712
730
  }
713
731
 
732
+ /** The directory below the target root a declared dependency's manifest is in, `""` being the root. */
733
+ function manifestDir(dependency: DeclaredDependency): string {
734
+ const dir = dirnamePath(dependency.position.path);
735
+ return dir === "." ? "" : dir;
736
+ }
737
+
738
+ /**
739
+ * Adds to `needed` each declared dependency that the installed manifest of a needed or
740
+ * run declared dependency names as a peer, until no further one joins: the declaration of
741
+ * that name in the needing dependency's manifest or in a manifest above it, where the
742
+ * installed peer is resolved from. `peers` holds each declaration's installed peers by
743
+ * {@link declarationKey}.
744
+ */
745
+ function addNeededPeers(
746
+ declared: readonly DeclaredDependency[],
747
+ peers: ReadonlyMap<string, readonly string[]>,
748
+ needed: Set<string>,
749
+ ran: ReadonlySet<string>,
750
+ ): void {
751
+ const pending = declared.filter(
752
+ (dependency) => needed.has(declarationKey(dependency)) || ran.has(declarationKey(dependency)),
753
+ );
754
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
755
+ const below = manifestDir(next);
756
+ for (const peer of peers.get(declarationKey(next)) ?? []) {
757
+ for (const dependency of declared) {
758
+ const key = declarationKey(dependency);
759
+ if (
760
+ dependency.name === peer &&
761
+ !needed.has(key) &&
762
+ (below === manifestDir(dependency) || isInside(below, manifestDir(dependency)))
763
+ ) {
764
+ needed.add(key);
765
+ pending.push(dependency);
766
+ }
767
+ }
768
+ }
769
+ }
770
+ }
771
+
714
772
  /**
715
- * The run's dependency evidence: the target's manifest and each workspace member's in
716
- * `members` below the target root, read with the position of every dependency each
717
- * declares, what the projects and the commands its scripts and workflow steps run need,
718
- * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
719
- * in configurations the run could not build. A manifest the program cannot read declares
720
- * nothing.
773
+ * The run's dependency evidence over the target's manifest and those of `members` and
774
+ * `nested` packages below the root: each declared dependency's position, what the projects,
775
+ * scripts and workflow steps need, and each deletion candidate's last uses. A member in
776
+ * `unbuilt` holds source only in configurations the run could not build. An unreadable
777
+ * manifest declares nothing, nor does a nested package's uninstalled dependency, because
778
+ * no run requires a nested package to be installed.
721
779
  */
722
780
  export function dependenciesOf(
723
781
  host: Host,
@@ -726,12 +784,13 @@ export function dependenciesOf(
726
784
  candidates: readonly string[],
727
785
  members: readonly string[] = [],
728
786
  unbuiltMembers: readonly string[] = [],
787
+ nested: readonly string[] = [],
729
788
  ): Dependencies {
730
789
  const root = resolvePath(host.workingDirectory(), targetRoot);
731
790
  const scopeOf = packageScope(host, targetRoot);
732
791
  const manifests = new Map<string, string>([[root, MANIFEST]]);
733
792
  const unbuilt = new Set<string>();
734
- for (const dir of [...members, ...unbuiltMembers]) {
793
+ for (const dir of [...members, ...unbuiltMembers, ...nested]) {
735
794
  const below = relativePath(root, dir);
736
795
  if (below !== undefined && !manifests.has(dir)) {
737
796
  manifests.set(dir, `${below}/${MANIFEST}`);
@@ -746,6 +805,8 @@ export function dependenciesOf(
746
805
  const workflow = new Set(workflowTokens(host, targetRoot));
747
806
  const declared: DeclaredDependency[] = [];
748
807
  const ran = new Set<string>();
808
+ const peers = new Map<string, readonly string[]>();
809
+ const uninstalledHere = new Set(nested);
749
810
  for (const [dir, path] of manifests) {
750
811
  const read = readJSON(host, joinPath(dir, MANIFEST));
751
812
  if (read === undefined || !isRecord(read.value)) {
@@ -753,13 +814,16 @@ export function dependenciesOf(
753
814
  }
754
815
  const tokens = scriptTokensOf(read.value);
755
816
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
756
- declared.push(dependency);
757
817
  const found = installedOf(host, dir, dependency.name);
818
+ if (found === undefined && uninstalledHere.has(dir)) {
819
+ continue;
820
+ }
821
+ declared.push(dependency);
758
822
  if (found === undefined) {
759
823
  continue;
760
824
  }
761
- const runs = (command: string): boolean =>
762
- tokens.has(command) || (dir === root && workflow.has(command));
825
+ peers.set(declarationKey(dependency), found.peers);
826
+ const runs = (command: string): boolean => tokens.has(command) || workflow.has(command);
763
827
  if (found.commands.some(runs)) {
764
828
  ran.add(declarationKey(dependency));
765
829
  }
@@ -769,25 +833,38 @@ export function dependenciesOf(
769
833
  // A declaration several projects compile is one user, and a use any project makes is
770
834
  // a use, so the uses are merged by declaration before anything is counted.
771
835
  const needed = new Set<string>();
836
+ const byName = new Map<string, DeclaredDependency[]>();
837
+ for (const dependency of declared) {
838
+ byName.set(dependency.name, [...(byName.get(dependency.name) ?? []), dependency]);
839
+ }
772
840
  const uses = new Map<string, Set<string>>();
773
841
  for (const project of perProject) {
774
842
  // A specifier the checker left unanswered may resolve to any dependency a manifest at
775
843
  // or above its file declares, and what the configuration needs is never deleted.
776
844
  const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
777
- const unknown = declared
778
- .filter((dependency) => {
779
- const dir = dirnamePath(dependency.position.path);
780
- return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
781
- })
782
- .map((dependency) => dependency.name);
845
+ const unknownDeclared = declared.filter((dependency) =>
846
+ unanswered.some((file) => isInside(file, manifestDir(dependency))),
847
+ );
848
+ unknownDeclared.forEach((dependency) => needed.add(declarationKey(dependency)));
849
+ const unknown = unknownDeclared.map((dependency) => dependency.name);
783
850
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
784
851
  for (const [id, used] of [...project.uses, ...held]) {
785
852
  const merged = uses.get(id) ?? new Set<string>();
786
853
  used.forEach((one) => merged.add(one));
787
854
  uses.set(id, merged);
788
855
  }
789
- [...project.packages, ...unknown].forEach((one) => needed.add(one));
856
+ for (const [path, named] of project.at) {
857
+ const below = path === ANYWHERE ? undefined : relativePath(root, path);
858
+ for (const one of named) {
859
+ for (const dependency of byName.get(one) ?? []) {
860
+ if (below === undefined || isInside(below, manifestDir(dependency))) {
861
+ needed.add(declarationKey(dependency));
862
+ }
863
+ }
864
+ }
865
+ }
790
866
  }
867
+ addNeededPeers(declared, peers, needed, ran);
791
868
  const users = new Map<string, number>();
792
869
  for (const used of uses.values()) {
793
870
  used.forEach((one) => users.set(one, (users.get(one) ?? 0) + 1));