@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.1

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 (47) hide show
  1. package/dist/src/analysis.js +39 -16
  2. package/dist/src/conformance-record.js +2 -2
  3. package/dist/src/contextual-uses.js +27 -3
  4. package/dist/src/convention-rows.js +42 -0
  5. package/dist/src/conventions.js +48 -0
  6. package/dist/src/dependencies.js +109 -38
  7. package/dist/src/findings/completion.js +6 -7
  8. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  9. package/dist/src/html-entries.js +29 -14
  10. package/dist/src/indexed-access.js +371 -81
  11. package/dist/src/intra-function-parts.js +57 -11
  12. package/dist/src/kinds.js +6 -6
  13. package/dist/src/references.js +60 -21
  14. package/dist/src/roots.js +96 -6
  15. package/dist/src/session.js +6 -1
  16. package/dist/src/source-files.js +1 -0
  17. package/dist/src/sweep.js +29 -17
  18. package/dist/src/symbol-keys.js +114 -0
  19. package/dist/src/test-joins.js +101 -0
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/workflow-steps.js +21 -15
  22. package/dist/src/workspace-programs.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +45 -17
  26. package/src/conformance-record.ts +2 -2
  27. package/src/contextual-uses.ts +36 -4
  28. package/src/convention-rows.ts +57 -0
  29. package/src/conventions.ts +59 -0
  30. package/src/dependencies.ts +134 -46
  31. package/src/finding.ts +2 -0
  32. package/src/findings/completion.ts +7 -7
  33. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  34. package/src/html-entries.ts +33 -15
  35. package/src/indexed-access.ts +468 -102
  36. package/src/intra-function-parts.ts +84 -12
  37. package/src/kinds.ts +7 -6
  38. package/src/references.ts +86 -27
  39. package/src/roots.ts +129 -6
  40. package/src/session.ts +7 -1
  41. package/src/source-files.ts +3 -0
  42. package/src/sweep.ts +29 -17
  43. package/src/symbol-keys.ts +172 -0
  44. package/src/test-joins.ts +120 -0
  45. package/src/version.ts +1 -1
  46. package/src/workflow-steps.ts +23 -16
  47. package/src/workspace-programs.ts +1 -1
@@ -32,7 +32,7 @@ import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
32
32
  import { inventory } from "./inventory.js";
33
33
  import { packageEntries, readManifest } from "./manifest.js";
34
34
  import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
35
- import { joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
35
+ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
36
36
  import { byPosition, isComponentFile } from "./position.js";
37
37
  import { SetupError, setupLine } from "./setup-failure.js";
38
38
  import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
@@ -41,6 +41,7 @@ import { references, testFileRulesOf } from "./references.js";
41
41
  import { roots, unmatchedEverywhere } from "./roots.js";
42
42
  import { serializationContract } from "./serialization-contract.js";
43
43
  import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
44
+ import { joinedTestFiles, withJoinedTests } from "./test-joins.js";
44
45
  import { supportReferences, testSupportFiles } from "./test-support.js";
45
46
  import { diagnosticsOf, ownedAs, runSession } from "./session.js";
46
47
  import { accessorsOf, storesOf } from "./stores.js";
@@ -119,6 +120,7 @@ function readProjects(engine, host, scope, config, stage) {
119
120
  testFiles: config.ts.testFiles,
120
121
  parse: (fileName, text) => engine.parseSourceFile(fileName, text),
121
122
  publishedAPI: config.targetKind === "library",
123
+ publishedMembers: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
122
124
  };
123
125
  const notBuilt = [...discovered.notBuilt];
124
126
  const dropped = (configFile, error) => {
@@ -144,6 +146,10 @@ function readProjects(engine, host, scope, config, stage) {
144
146
  unbuildable.push(...met);
145
147
  }
146
148
  };
149
+ const testPatterns = config.ts.testFiles.map(globExpression);
150
+ const joins = discovered.derived
151
+ ? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
152
+ : new Map();
147
153
  const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (opened) => {
148
154
  const consumer = consumerOf.get(opened.configFile);
149
155
  const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
@@ -243,9 +249,10 @@ function readProjects(engine, host, scope, config, stage) {
243
249
  heldFiles.add(relativePath(absoluteRoot, file.fileName) ?? file.fileName);
244
250
  }
245
251
  return stage(project, { configuration, read });
246
- }, discovered.derived ? "record" : "refuse", workspace?.programs, () => {
247
- const patterns = config.ts.testFiles.map(globExpression);
248
- outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && patterns.some((pattern) => pattern.test(path))), options.parse);
252
+ }, discovered.derived ? "record" : "refuse", joins.size === 0
253
+ ? workspace?.programs
254
+ : withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins), () => {
255
+ outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), options.parse);
249
256
  });
250
257
  for (const configFile of session.unopened) {
251
258
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -638,23 +645,38 @@ function readEmitterRun(engine, host, scope, config, mode, detectors) {
638
645
  : { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
639
646
  }
640
647
  /**
641
- * The workspace members whose manifest the run reads whole enough to judge its
642
- * dependencies: a built project holds a file below the member's directory, and no derived
643
- * configuration at or below it was dropped. A file no built project holds may import any
644
- * dependency the member declares.
648
+ * The workspace members whose dependencies the run judges: `read` holds those a built
649
+ * project holds a file below, `unbuilt` the others whose own directory, the nearest
650
+ * manifest's above the configuration, holds a configuration the run could not build. A
651
+ * member no configuration holds a file of is in neither.
645
652
  */
646
- function membersWhollyRead(host, targetRoot, read) {
653
+ function judgedMembers(host, targetRoot, read) {
647
654
  const root = resolvePath(host.workingDirectory(), targetRoot);
648
655
  const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
649
- const dropped = read.notBuilt.map((one) => relativePath(root, one.configFile) ?? "");
650
- return read.memberDirs.filter((dir) => {
656
+ const owners = new Set(read.notBuilt.map((one) => nearestManifestDir(host, root, one.configFile)));
657
+ const judged = { read: [], unbuilt: [] };
658
+ for (const dir of read.memberDirs) {
651
659
  const below = relativePath(root, dir);
652
660
  if (below === undefined) {
653
- return false;
661
+ continue;
654
662
  }
655
- const inside = (path) => path.startsWith(`${below}/`);
656
- return [...held].some(inside) && !dropped.some(inside);
657
- });
663
+ if ([...held].some((path) => path.startsWith(`${below}/`))) {
664
+ judged.read.push(dir);
665
+ }
666
+ else if (owners.has(dir)) {
667
+ judged.unbuilt.push(dir);
668
+ }
669
+ }
670
+ return judged;
671
+ }
672
+ /** The directory of the nearest `package.json` at or above a file, inside `root`, else `root`. */
673
+ function nearestManifestDir(host, root, file) {
674
+ for (let dir = dirnamePath(file); relativePath(root, dir) !== undefined; dir = dirnamePath(dir)) {
675
+ if (host.kindOf(joinPath(dir, "package.json")) === "file") {
676
+ return dir;
677
+ }
678
+ }
679
+ return root;
658
680
  }
659
681
  /**
660
682
  * The input every emitter reads over one sweep of a read run: the facts beside the sweep
@@ -664,6 +686,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
664
686
  const targetRoot = scope.target.path;
665
687
  const swept = sweptOf(read, input);
666
688
  const { projects } = read;
689
+ const members = judgedMembers(host, targetRoot, read);
667
690
  return {
668
691
  config,
669
692
  swept,
@@ -672,7 +695,7 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
672
695
  references: one.configured.references,
673
696
  accessors: one.extra.accessors,
674
697
  })), swept.exempt, input.mode),
675
- dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), membersWhollyRead(host, targetRoot, read)),
698
+ dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), members.read, members.unbuilt),
676
699
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
677
700
  files: {
678
701
  tree: sourceTree(host, targetRoot),
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "4.1.0",
5
+ corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:167f770cdff2776fa7ab28b7ff6121c4d7bdb01d83c5ba0057d9303281614008",
7
+ digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
8
8
  },
9
9
  gaps: [],
10
10
  };
@@ -11,7 +11,10 @@ import { SignatureKind } from "@typescript/native/unstable/sync";
11
11
  import { isAnswered } from "./query.js";
12
12
  /** The member kinds a contextual type writes and reads: data members and the members of object types. */
13
13
  const MEMBER_KINDS = new Set(["type-member", "class-member"]);
14
- /** The name one literal property is written under, where the name is literal text. */
14
+ /**
15
+ * The name one literal property is written under: its literal text, or the computed name
16
+ * node whose key the checker names, a unique symbol among them.
17
+ */
15
18
  function propertyName(property) {
16
19
  if (!isPropertyAssignment(property) &&
17
20
  !isShorthandPropertyAssignment(property) &&
@@ -29,6 +32,7 @@ function propertyName(property) {
29
32
  if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
30
33
  return { node: name, text: key.text };
31
34
  }
35
+ return { node: name, text: undefined };
32
36
  }
33
37
  return undefined;
34
38
  }
@@ -88,6 +92,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
88
92
  const writes = [];
89
93
  batches += batchesOf(literals.length);
90
94
  const contexts = queries.contextualTypes(literals.map((one) => one.literal), cap);
95
+ // A computed name that is not literal text names the property the literal declares
96
+ // under the checker's own key, which is how a type declares a member a unique symbol
97
+ // keys: asked once, for the literals that have a contextual type.
98
+ const computed = literals.flatMap(({ literal }, index) => contexts[index] === undefined || !isAnswered(contexts[index])
99
+ ? []
100
+ : literal.properties.flatMap((property) => {
101
+ const name = propertyName(property);
102
+ return name !== undefined && name.text === undefined ? [name.node] : [];
103
+ }));
104
+ batches += batchesOf(computed.length);
105
+ const keys = new Map();
106
+ (computed.length === 0
107
+ ? []
108
+ : project.symbolsAt(computed.map((node) => project.handle(node)), cap)).forEach((symbol, index) => {
109
+ const node = computed[index];
110
+ if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
111
+ keys.set(node, symbol.name);
112
+ }
113
+ });
91
114
  literals.forEach(({ literal, from }, index) => {
92
115
  const context = contexts[index];
93
116
  if (context === undefined || !isAnswered(context)) {
@@ -96,10 +119,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
96
119
  const tablesOf = partsOf(context).map(membersOf);
97
120
  for (const property of literal.properties) {
98
121
  const name = propertyName(property);
99
- if (name === undefined) {
122
+ const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
123
+ if (name === undefined || text === undefined) {
100
124
  continue;
101
125
  }
102
- const ids = [...new Set(tablesOf.flatMap((table) => table.get(name.text)?.ids ?? []))];
126
+ const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
103
127
  if (ids.length > 0) {
104
128
  writes.push({ at: name.node, from, ids });
105
129
  }
@@ -39,8 +39,38 @@ const BABEL_CONFIG = [
39
39
  const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
40
40
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
41
41
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
42
+ /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
43
+ const ANGULAR_ENTRIES = [
44
+ "src/{main,main.server,server,polyfills,test}.ts",
45
+ "server.ts",
46
+ "projects/*/src/{main,main.server,server,polyfills,test,public-api}.ts",
47
+ "projects/*/server.ts",
48
+ ];
49
+ /** The `angular.json` members whose strings name the packages a build loads. */
50
+ const ANGULAR_PACKAGE_KEYS = [
51
+ "builder",
52
+ "polyfills",
53
+ "styles",
54
+ "scripts",
55
+ ].map((key) => ({ key, files: ["angular.json"] }));
42
56
  /** Every row, ordered by name and then by range. */
43
57
  export const CONVENTION_ROWS = [
58
+ {
59
+ name: "angular",
60
+ package: "@angular/build",
61
+ range: ">=17.0.0 <22.0.0",
62
+ entries: ANGULAR_ENTRIES,
63
+ moves: [],
64
+ packageKeys: ANGULAR_PACKAGE_KEYS,
65
+ },
66
+ {
67
+ name: "angular-devkit",
68
+ package: "@angular-devkit/build-angular",
69
+ range: ">=12.0.0 <22.0.0",
70
+ entries: ANGULAR_ENTRIES,
71
+ moves: [],
72
+ packageKeys: ANGULAR_PACKAGE_KEYS,
73
+ },
44
74
  {
45
75
  name: "astro",
46
76
  package: "astro",
@@ -472,6 +502,18 @@ export const CONVENTION_ROWS = [
472
502
  },
473
503
  ],
474
504
  },
505
+ {
506
+ name: "vitepress",
507
+ package: "vitepress",
508
+ range: ">=1.0.0 <2.0.0-0 || >=2.0.0-alpha.0 <3.0.0",
509
+ entries: [
510
+ `.vitepress/config.${ANY_MODULE}`,
511
+ `.vitepress/theme/index.${ANY_MODULE}`,
512
+ `**/*.data.${ANY_MODULE}`,
513
+ "**/*.vue",
514
+ ],
515
+ moves: [],
516
+ },
475
517
  {
476
518
  name: "vitest",
477
519
  package: "vitest",
@@ -602,6 +602,50 @@ function helperUses(host, parse, dir, manifest, readings) {
602
602
  readingFiles(host, parse, dir, reading.files).some(({ file }) => valuesAt(file, reading.property, reading.call).some((held) => holdsValue(held, reading.value))))
603
603
  .map((reading) => reading.package);
604
604
  }
605
+ /**
606
+ * The dependencies one manifest declares that an applied row's package keys use: each
607
+ * whose name a string under a member of a key's name equals, or begins followed by `/` or
608
+ * by `:` and a name. A member's own name is no such string.
609
+ */
610
+ function packageKeyUses(host, parse, dir, manifest, keys) {
611
+ const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
612
+ const held = isRecord(manifest) ? manifest[section] : undefined;
613
+ return isRecord(held) ? Object.keys(held) : [];
614
+ });
615
+ const strings = [];
616
+ const collect = (node) => {
617
+ const text = literalText(node);
618
+ if (text !== undefined) {
619
+ strings.push(text);
620
+ }
621
+ else if (isPropertyAssignment(node)) {
622
+ collect(node.initializer);
623
+ }
624
+ else {
625
+ node.forEachChild(collect);
626
+ }
627
+ };
628
+ for (const { key, files } of keys) {
629
+ const visit = (node) => {
630
+ if (isPropertyAssignment(node) && keyOf(node) === key) {
631
+ collect(node.initializer);
632
+ }
633
+ node.forEachChild(visit);
634
+ };
635
+ for (const { file } of readingFiles(host, parse, dir, files)) {
636
+ const values = declaredValues(file);
637
+ for (const one of defaultExports(file)) {
638
+ const object = objectOf(one, values, true);
639
+ if (object !== undefined) {
640
+ visit(object);
641
+ }
642
+ }
643
+ }
644
+ }
645
+ return declared.filter((name) => strings.some((text) => text === name ||
646
+ text.startsWith(`${name}/`) ||
647
+ (text.startsWith(`${name}:`) && text.length > name.length + 1)));
648
+ }
605
649
  /** Two strings ordered bytewise. */
606
650
  function compare(a, b) {
607
651
  if (a === b) {
@@ -680,6 +724,10 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
680
724
  for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
681
725
  uses.add(name);
682
726
  }
727
+ const keys = row.packageKeys ?? [];
728
+ for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
729
+ uses.add(name);
730
+ }
683
731
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
684
732
  uses.add(name);
685
733
  }
@@ -1,17 +1,19 @@
1
1
  /**
2
- * The dependencies the target's manifest and its workspace members' manifests declare, and
3
- * what the projects' own files need from outside the target. A file needs a package it
4
- * imports, re-exports from, augments or references the types of, by name or through the
5
- * file the specifier resolves to; a compiler configuration needs one it names, and a
6
- * configuration file a tool loads needs each dependency its strings spell. A package one
7
- * dependency pulls in for itself is that dependency's need, not the target's.
2
+ * The dependencies the target's manifest and its workspace members' manifests declare, and what the
3
+ * projects' own files need from outside the target. A file needs a package it imports, re-exports
4
+ * from, augments, references the types of or asks the runtime to resolve, by name or through the
5
+ * file the specifier resolves to; a compiler configuration needs one it names, and a configuration
6
+ * file a tool loads needs each dependency its strings spell. A package one dependency pulls in for
7
+ * itself is that dependency's need, not the target's.
8
8
  */
9
- import { isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isNamespaceExport, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { isCallExpression, isExportDeclaration, isExternalModuleReference, isIdentifier, isImportDeclaration, isImportEqualsDeclaration, isMetaProperty, isNamespaceExport, isPropertyAccessExpression, isShorthandPropertyAssignment, isStringLiteral, isNoSubstitutionTemplateLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
10
10
  import { configurationFiles, declaredIn as declaredBeside, dependenciesNamed, moduleStrings, } from "./configuration-files.js";
11
11
  import { nodeKey, packageScope } from "./inventory.js";
12
+ import { scriptTokens } from "./manifest.js";
12
13
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
13
14
  import { UNANSWERED } from "./query.js";
14
15
  import { sourceFilesOf } from "./source-files.js";
16
+ import { workflowTokens } from "./workflow-steps.js";
15
17
  const MANIFEST = "package.json";
16
18
  const MODULES_DIR = "node_modules";
17
19
  const MODULES_SEGMENT = `/${MODULES_DIR}/`;
@@ -29,7 +31,7 @@ export function configurationNeeds(packages) {
29
31
  return {
30
32
  packages: new Set(packages),
31
33
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
32
- unanswered: false,
34
+ unanswered: [],
33
35
  };
34
36
  }
35
37
  function isRecord(value) {
@@ -219,9 +221,11 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
219
221
  }
220
222
  };
221
223
  use([PROJECT_OWNER], packages);
222
- let unanswered = false;
224
+ const unanswered = [];
223
225
  for (const file of project.ownSourceFiles()) {
224
- unanswered = fileNeeds(project, held, file, programPackages, use) || unanswered;
226
+ if (fileNeeds(project, held, file, programPackages, use)) {
227
+ unanswered.push(file.fileName);
228
+ }
225
229
  }
226
230
  const beside = new Map();
227
231
  const declaredAt = (path) => {
@@ -365,8 +369,40 @@ function fileNeeds(project, held, file, programPackages, use) {
365
369
  for (const reference of file.typeReferenceDirectives) {
366
370
  use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
367
371
  }
372
+ for (const specifier of resolvedSpecifiers(file)) {
373
+ const named = packageOfSpecifier(specifier.text);
374
+ if (named !== undefined) {
375
+ use(ownersOf(file, held, specifier), [named]);
376
+ }
377
+ }
368
378
  return resolved.slice(0, specifiers.length).includes(UNANSWERED);
369
379
  }
380
+ /**
381
+ * The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
382
+ * or `require.resolve`, each of which needs the package it names installed.
383
+ */
384
+ function resolvedSpecifiers(file) {
385
+ const found = [];
386
+ if (!file.text.includes(".resolve(")) {
387
+ return found;
388
+ }
389
+ const visit = (node) => {
390
+ if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
391
+ const callee = node.expression;
392
+ const [argument] = node.arguments;
393
+ const receiver = callee.expression;
394
+ const resolving = callee.name.text === "resolve" &&
395
+ ((isMetaProperty(receiver) && receiver.name.text === "meta") ||
396
+ (isIdentifier(receiver) && receiver.text === "require"));
397
+ if (resolving && argument !== undefined && isLiteralText(argument)) {
398
+ found.push(argument);
399
+ }
400
+ }
401
+ node.forEachChild(visit);
402
+ };
403
+ visit(file);
404
+ return found;
405
+ }
370
406
  /** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
371
407
  function valueEnd(text, at) {
372
408
  const first = text.charAt(at);
@@ -455,20 +491,16 @@ function installedOf(host, root, name) {
455
491
  for (let dir = root;; dir = dirnamePath(dir)) {
456
492
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
457
493
  if (read !== undefined && isRecord(read.value)) {
458
- const manifest = read.value;
459
- const bin = manifest["bin"];
460
- const peers = manifest["peerDependencies"];
461
- const meta = isRecord(manifest["peerDependenciesMeta"])
462
- ? manifest["peerDependenciesMeta"]
463
- : {};
494
+ const bin = read.value["bin"];
495
+ // A string `bin` names one command, the package's name without its scope.
464
496
  return {
465
- command: (typeof bin === "string" && bin !== "") || (isRecord(bin) && Object.keys(bin).length > 0),
466
- peers: isRecord(peers)
467
- ? Object.keys(peers).filter((peer) => {
468
- const about = meta[peer];
469
- return !(isRecord(about) && about["optional"] === true);
470
- })
471
- : [],
497
+ commands: typeof bin === "string"
498
+ ? bin === ""
499
+ ? []
500
+ : [name.slice(name.indexOf("/") + 1)]
501
+ : isRecord(bin)
502
+ ? Object.keys(bin)
503
+ : [],
472
504
  };
473
505
  }
474
506
  if (dirnamePath(dir) === dir) {
@@ -501,37 +533,70 @@ function declaredIn(text, manifest, path, scope) {
501
533
  }
502
534
  return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
503
535
  }
536
+ /** The key one declared dependency is known by: its manifest's path and its name. */
537
+ export function declarationKey(dependency) {
538
+ return `${dependency.position.path}\u0000${dependency.name}`;
539
+ }
540
+ /** Every token of every value of one manifest's `scripts` member. */
541
+ function scriptTokensOf(manifest) {
542
+ const scripts = manifest["scripts"];
543
+ const found = new Set();
544
+ if (isRecord(scripts)) {
545
+ for (const command of Object.values(scripts)) {
546
+ if (typeof command === "string") {
547
+ scriptTokens(command).forEach((token) => found.add(token));
548
+ }
549
+ }
550
+ }
551
+ return found;
552
+ }
553
+ /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
554
+ function isInside(path, dir) {
555
+ return dir === "" || path.startsWith(`${dir}/`);
556
+ }
504
557
  /**
505
558
  * The run's dependency evidence: the target's manifest and each workspace member's in
506
559
  * `members` below the target root, read with the position of every dependency each
507
- * declares, the installed copy of each found from its manifest's directory, what the
508
- * projects need, and the last uses each deletion candidate holds. A manifest the
509
- * program cannot read declares nothing.
560
+ * declares, what the projects and the commands its scripts and workflow steps run need,
561
+ * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
562
+ * in configurations the run could not build. A manifest the program cannot read declares
563
+ * nothing.
510
564
  */
511
- export function dependenciesOf(host, targetRoot, perProject, candidates, members = []) {
565
+ export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
512
566
  const root = resolvePath(host.workingDirectory(), targetRoot);
513
567
  const scopeOf = packageScope(host, targetRoot);
514
568
  const manifests = new Map([[root, MANIFEST]]);
515
- for (const dir of members) {
569
+ const unbuilt = new Set();
570
+ for (const dir of [...members, ...unbuiltMembers]) {
516
571
  const below = relativePath(root, dir);
517
572
  if (below !== undefined && !manifests.has(dir)) {
518
573
  manifests.set(dir, `${below}/${MANIFEST}`);
519
574
  }
520
575
  }
576
+ for (const dir of unbuiltMembers) {
577
+ const path = manifests.get(dir);
578
+ if (path !== undefined && dir !== root) {
579
+ unbuilt.add(path);
580
+ }
581
+ }
582
+ const workflow = new Set(workflowTokens(host, targetRoot));
521
583
  const declared = [];
522
- const installed = new Map();
584
+ const ran = new Set();
523
585
  for (const [dir, path] of manifests) {
524
586
  const read = readJSON(host, joinPath(dir, MANIFEST));
525
587
  if (read === undefined || !isRecord(read.value)) {
526
588
  continue;
527
589
  }
590
+ const tokens = scriptTokensOf(read.value);
528
591
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
529
592
  declared.push(dependency);
530
- if (!installed.has(dependency.name)) {
531
- const found = installedOf(host, dir, dependency.name);
532
- if (found !== undefined) {
533
- installed.set(dependency.name, found);
534
- }
593
+ const found = installedOf(host, dir, dependency.name);
594
+ if (found === undefined) {
595
+ continue;
596
+ }
597
+ const runs = (command) => tokens.has(command) || (dir === root && workflow.has(command));
598
+ if (found.commands.some(runs)) {
599
+ ran.add(declarationKey(dependency));
535
600
  }
536
601
  }
537
602
  }
@@ -540,9 +605,15 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
540
605
  const needed = new Set();
541
606
  const uses = new Map();
542
607
  for (const project of perProject) {
543
- // A specifier the checker left unanswered may resolve to any declared dependency,
544
- // and what the configuration needs is never deleted, so it is needed there.
545
- const unknown = project.unanswered ? declared.map((dependency) => dependency.name) : [];
608
+ // A specifier the checker left unanswered may resolve to any dependency a manifest at
609
+ // or above its file declares, and what the configuration needs is never deleted.
610
+ const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
611
+ const unknown = declared
612
+ .filter((dependency) => {
613
+ const dir = dirnamePath(dependency.position.path);
614
+ return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
615
+ })
616
+ .map((dependency) => dependency.name);
546
617
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
547
618
  for (const [id, used] of [...project.uses, ...held]) {
548
619
  const merged = uses.get(id) ?? new Set();
@@ -563,5 +634,5 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
563
634
  lastUses.set(id, sole.sort((a, b) => (a < b ? -1 : 1)));
564
635
  }
565
636
  }
566
- return { declared, installed, needed, lastUses };
637
+ return { declared, needed, ran, unbuilt, lastUses };
567
638
  }
@@ -108,12 +108,11 @@ function findingComponents(swept, cascade) {
108
108
  }
109
109
  /**
110
110
  * Every finding of the run completed, in the order given. A dead declaration carries its
111
- * candidate's relation, counts, configurations and component; a live one carries no
112
- * relation and holds where it is declared; a file or a manifest row is `certain` and
113
- * holds in every configuration. A subject in no dead component gets one of its own,
114
- * numbered past the computed ones in finding order, so no two families mint one; nothing
115
- * falls with it, so its deletion removes no line. `kinds` is the vocabulary each code's
116
- * row is read from, the shipped one unless a caller supplies its own.
111
+ * candidate's relation, counts, configurations and component; a live one carries no relation and
112
+ * holds where it is declared; a file or a manifest row carries the class its emitter states,
113
+ * `certain` by default, and holds in every configuration. A subject in no dead component gets one
114
+ * of its own, numbered past the computed ones in finding order so no two families mint one, whose
115
+ * deletion removes no line. `kinds` defaults to the shipped vocabulary.
117
116
  */
118
117
  export function completed(input, findings, kinds = KINDS) {
119
118
  const { swept } = input;
@@ -172,7 +171,7 @@ export function completed(input, findings, kinds = KINDS) {
172
171
  const counts = candidate === undefined
173
172
  ? union.made[at]
174
173
  : { production: candidate.productionRefs, test: candidate.testRefs };
175
- const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
174
+ const reachabilityClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
176
175
  const generated = input.files.generated?.has(finding.position.path) === true;
177
176
  return {
178
177
  code: finding.code,
@@ -1,3 +1,4 @@
1
+ import { declarationKey } from "../dependencies.js";
1
2
  import { renderRef } from "../ref.js";
2
3
  const UNUSED_DEPENDENCY = "DS1601";
3
4
  const MESSAGES = {
@@ -5,38 +6,14 @@ const MESSAGES = {
5
6
  "dev-dependency": "no import in the target needs the declared development dependency",
6
7
  "peer-dependency": "no import in the target needs the declared peer dependency",
7
8
  };
8
- /**
9
- * The dependencies the target needs: every package the projects' uses name, every
10
- * declared dependency that ships a command, which a script, a workflow or a shell
11
- * runs outside anything the analysis reads, and every peer a needed package
12
- * requires, which the target satisfies by declaring it.
13
- */
14
- function neededBy(dependencies) {
15
- const needed = new Set(dependencies.needed);
16
- for (const [name, installed] of dependencies.installed) {
17
- if (installed.command) {
18
- needed.add(name);
19
- }
20
- }
21
- const pending = [...needed];
22
- for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
23
- for (const peer of dependencies.installed.get(name)?.peers ?? []) {
24
- if (!needed.has(peer)) {
25
- needed.add(peer);
26
- pending.push(peer);
27
- }
28
- }
29
- }
30
- return needed;
31
- }
32
9
  /**
33
10
  * A dependency, development dependency or peer dependency of a manifest the run reads
34
11
  * that the target does not need, at the position of its key.
35
12
  */
36
13
  function unusedDependencies(dependencies) {
37
- const needed = neededBy(dependencies);
38
14
  return dependencies.declared
39
- .filter((dependency) => !needed.has(dependency.name))
15
+ .filter((dependency) => !dependencies.needed.has(dependency.name) &&
16
+ !dependencies.ran.has(declarationKey(dependency)))
40
17
  .map((dependency) => ({
41
18
  code: UNUSED_DEPENDENCY,
42
19
  position: dependency.position,
@@ -52,6 +29,9 @@ function unusedDependencies(dependencies) {
52
29
  },
53
30
  message: MESSAGES[dependency.section],
54
31
  details: { dependencyClass: dependency.section },
32
+ ...(dependencies.unbuilt.has(dependency.position.path)
33
+ ? { reachabilityClass: "possible" }
34
+ : {}),
55
35
  }));
56
36
  }
57
37
  /** The findings of the dependencies-and-module-machinery family, `DS1600` to `DS1699`. */