@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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 (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -12,6 +12,7 @@ import { consumerReferences } from "./consumers.js";
12
12
  import { configuredDeclarations, configuredRef, declarationsNamed, } from "./roots/configured-declarations.js";
13
13
  import { decorator } from "./decorator.js";
14
14
  import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.js";
15
+ import { carriersOf, NO_CARRIERS } from "./exposure.js";
15
16
  import { deprecatedDeclarations } from "./deprecation.js";
16
17
  import { moduleIdentity } from "./load/discover.js";
17
18
  import { loadRun } from "./load/load.js";
@@ -23,7 +24,7 @@ import { bindingsOf, distinctBindings } from "./bindings.js";
23
24
  import { generatedFile, generatedFilesOf } from "./generated-file.js";
24
25
  import { inOperandNames } from "./findings/in-operands.js";
25
26
  import { sourceFilesOf } from "./source-files.js";
26
- import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, writtenRecords, } from "./exempt.js";
27
+ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, isUnansweredRecord, writtenRecords, } from "./exempt.js";
27
28
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
28
29
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
29
30
  import { unmatchedRootFindings } from "./findings/self-check.js";
@@ -35,7 +36,7 @@ import { intraFunctionFacts, projectParts } from "./intra-function-parts.js";
35
36
  import {} from "./inventory.js";
36
37
  import { inventory } from "./inventory/enumerate.js";
37
38
  import { readManifest } from "./roots/manifest.js";
38
- import { liveFilesAt, matrixOf, referenceKey, sweepMatrix, } from "./matrix.js";
39
+ import { matrixOf, referenceKey } from "./liveness/matrix.js";
39
40
  import { ancestors, dirnamePath, extensionOf, joinPath, relativePath, resolvePath, } from "./paths.js";
40
41
  import { byPosition, isComponentFile } from "./position.js";
41
42
  import { skippedUnits } from "./type-errors.js";
@@ -52,6 +53,7 @@ import { accessorsOf, storesOf } from "./stores.js";
52
53
  import { isPartKind } from "./suppress.js";
53
54
  import { bindRows, readBaseline, readIgnoreFile } from "./suppress-file.js";
54
55
  import { inlineDirectives, inlineSuppressions } from "./suppress-inline.js";
56
+ import { sweep } from "./liveness/sweep.js";
55
57
  import { readTemplates, templateField } from "./template-field.js";
56
58
  import { bytewise } from "./text/compare.js";
57
59
  import { holdsManifest, manifestAt } from "./fs/manifest.js";
@@ -454,6 +456,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
454
456
  ],
455
457
  projects,
456
458
  consumers: read.plan.discovery.consumers,
459
+ declaresConsumers: scope.consumers.length > 0,
457
460
  notBuilt: read.classified.notBuilt,
458
461
  unanswered: read.unanswered,
459
462
  componentsUnread: read.plan.workspace?.componentsUnread ?? [],
@@ -467,29 +470,6 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
467
470
  workflows: read.result.workflows,
468
471
  };
469
472
  }
470
- /**
471
- * The run swept under one set of marks with the records that hold. A record a component
472
- * file's markup holds counts only while the file is live, and what it holds may be what
473
- * keeps the file live, so each file's markup joins only once a sweep without it finds the
474
- * file live, until a sweep finds no further file live.
475
- */
476
- function holdingSweep(read, input) {
477
- const paths = new Set(read.exempt.flatMap((record) => record.whileLive ?? []));
478
- let live = new Set();
479
- for (;;) {
480
- const swept = {
481
- ...input,
482
- exempt: holdingWhile(read.exempt, live),
483
- unanswered: read.unansweredHeld,
484
- };
485
- const result = sweepMatrix(read.matrix, swept);
486
- const next = new Set(liveFilesAt(read.matrix, result, paths).map((one) => one.position.path));
487
- if (next.size === live.size) {
488
- return { swept, result };
489
- }
490
- live = next;
491
- }
492
- }
493
473
  /**
494
474
  * The test references exemption evidence written in a test file makes: each is a read of
495
475
  * the declaration it would retain, made by the test file at the evidence's site.
@@ -547,55 +527,22 @@ function writtenOnly(read, mode) {
547
527
  }
548
528
  return new Set([...written].filter((id) => !readSome.has(id)));
549
529
  }
550
- /**
551
- * The sweep of a run read once, under one set of marks. Where a question went unanswered,
552
- * the run is swept once more without what that question could have kept live and without
553
- * the references that stood in for its answer, and every declaration that sweep judges
554
- * dead and this one judges live, or dead by another rule, is held by the gap too, as is
555
- * every declaration that makes such a reference.
556
- */
530
+ /** The sweep of a run read once, under one set of marks. */
557
531
  function sweptOf(read, input) {
558
- const { swept, result } = holdingSweep(read, input);
559
- const exempt = swept.exempt ?? [];
560
- const held = new Set(read.unansweredHeld);
561
- if (held.size > 0) {
562
- // What a declaration reads where the checker did not answer is unknown, so a rule
563
- // over what it reads, the test of dead code among them, cannot judge it.
564
- for (const { configured } of read.projects) {
565
- for (const reference of configured.references) {
566
- if (reference.guessed === true) {
567
- held.add(reference.from);
568
- }
569
- }
570
- }
571
- const dead = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
572
- const answered = matrixOf(read.projects.map(({ configured }) => ({
573
- ...configured,
574
- references: configured.references.filter((one) => one.guessed !== true),
575
- })));
576
- const bare = {
577
- ...swept,
578
- exempt: exempt.filter((record) => !isUnansweredRecord(record)),
579
- unanswered: [],
580
- };
581
- for (const candidate of sweepMatrix(answered, bare).candidates) {
582
- const kept = dead.get(candidate.id);
583
- if (kept?.testOfDeadCode !== candidate.testOfDeadCode ||
584
- kept.relation !== candidate.relation) {
585
- held.add(candidate.id);
586
- }
587
- }
588
- }
532
+ const componentFiles = [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(bytewise);
589
533
  return {
590
534
  matrix: read.matrix,
591
- sweep: result,
592
- exempt,
593
- retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
535
+ sweep: sweep(read.matrix, {
536
+ ...input,
537
+ exempt: read.exempt,
538
+ unanswered: read.unansweredHeld,
539
+ componentFiles,
540
+ unseenCallers: !read.declaresConsumers,
541
+ writeOnly: writtenOnly(read, input.mode),
542
+ }),
594
543
  notBuilt: read.notBuilt,
595
544
  unanswered: read.unanswered,
596
- heldByUnanswered: [...held],
597
545
  skipped: read.projects.flatMap((one) => one.skipped),
598
- componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(bytewise),
599
546
  };
600
547
  }
601
548
  /**
@@ -631,6 +578,8 @@ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
631
578
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
632
579
  parts: projectParts(project, read.held, references, targetRoot),
633
580
  unseenLoads,
581
+ // Only a closed world reports an unnecessary export, which is all the carriers decide.
582
+ carriers: config.consumers.complete ? carriersOf(project, read.held) : NO_CARRIERS,
634
583
  }));
635
584
  // Whether a declaration carries the marker decides its code, so one whose answer went
636
585
  // unread is reported by nothing.
@@ -694,7 +643,7 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
694
643
  references: one.configured.references,
695
644
  support: new Set(one.configured.supportFiles ?? []),
696
645
  accessors: one.extra.accessors,
697
- })), swept.exempt, input.mode),
646
+ })), swept.sweep.exempt, input.mode),
698
647
  dependencies: dependenciesOf(host, targetRoot, read.workflows, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), members.read, members.unbuilt, read.nestedDirs),
699
648
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
700
649
  files: {
@@ -703,6 +652,9 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
703
652
  generated: new Set(projects.flatMap((one) => [...one.extra.generated.keys()])),
704
653
  inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
705
654
  unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
655
+ unwrittenRuns: new Set(projects.flatMap((one) => one.heldScripts
656
+ .filter((held) => held.unwritten)
657
+ .map((held) => relativePath(resolvePath(host.workingDirectory(), targetRoot), held.dir) ?? "."))),
706
658
  },
707
659
  boundary: {
708
660
  consumers: {
@@ -713,9 +665,15 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
713
665
  edges: readEdgeSides(host, targetRoot),
714
666
  filledOutside: new Set(projects.flatMap((one) => one.filledOutside)),
715
667
  possiblyLoaded: new Set(projects.flatMap((one) => one.possiblyLoaded)),
668
+ carriers: {
669
+ named: new Map([
670
+ ...Map.groupBy(projects.flatMap((one) => [...one.extra.carriers.named]), ([id]) => id),
671
+ ].map(([id, entries]) => [id, [...new Set(entries.flatMap(([, named]) => named))]])),
672
+ unread: new Set(projects.flatMap((one) => [...one.extra.carriers.unread])),
673
+ },
716
674
  },
717
675
  selfCheck,
718
- intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
676
+ intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.sweep.exempt.map((one) => one.id), ...swept.sweep.held])),
719
677
  };
720
678
  }
721
679
  /**
@@ -828,11 +786,11 @@ export function runAnalysis(engine, host, scope, config, provenance, mode, detec
828
786
  * declarations of its own the run holds for them.
829
787
  */
830
788
  export function unansweredCounts(analysis) {
831
- const held = new Set(analysis.swept.heldByUnanswered);
789
+ const held = new Set(analysis.swept.sweep.held);
832
790
  return analysis.run.projects.flatMap((project) => {
833
791
  const questions = analysis.run.unanswered.filter((one) => one.configFile === project.configFile).length +
834
792
  analysis.run.unread.filter((one) => one.kind === "manifest" && one.configFile === project.configFile).length +
835
- analysis.run.heldScripts.filter((one) => one.configFile === project.configFile).length;
793
+ analysis.run.heldScripts.filter((one) => one.configFile === project.configFile && one.files.length > 0).length;
836
794
  if (questions === 0) {
837
795
  return [];
838
796
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { isInterfaceDeclaration, isModuleBlock, isModuleDeclaration, } from "@typescript/native/ast";
10
10
  import { nodeKey } from "./inventory.js";
11
- import { referenceKey } from "./matrix.js";
11
+ import { referenceKey } from "./liveness/matrix.js";
12
12
  /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
13
13
  export function isGlobalBlock(symbol, held, files) {
14
14
  return (symbol.kind === "namespace" &&
package/dist/src/calls.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * The calls and decorators of one project that the configured classes read: what each
3
3
  * callee resolves to, and which of the target's classes a call passes or a decorator
4
- * is attached to. A class is passed as an argument, or as an element of an array
5
- * literal or a property value of an object literal written as one, at any depth, named
6
- * directly or through variables, and a decorator written as a call is a call.
4
+ * is attached to. A class is passed by each value an argument can be, at any depth inside
5
+ * the array and object literals it writes, named directly or through variables, and a
6
+ * decorator written as a call is a call.
7
7
  */
8
- import { isArrayLiteralExpression, isCallExpression, isClassDeclaration, isDecorator, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, isVariableDeclaration, } from "@typescript/native/ast";
9
- import { SymbolFlags } from "@typescript/native/sync";
8
+ import { isArrayLiteralExpression, isArrowFunction, isBinaryExpression, isBindingElement, isCallExpression, isClassDeclaration, isConditionalExpression, isDecorator, isFunctionDeclaration, isFunctionExpression, isIdentifier, isMethodDeclaration, isObjectLiteralExpression, isParameterDeclaration, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isPropertyDeclaration, isShorthandPropertyAssignment, isOmittedExpression, isSpreadAssignment, isSpreadElement, isVariableDeclaration, SyntaxKind, } from "@typescript/native/ast";
9
+ import { ObjectFlags, SymbolFlags, } from "@typescript/native/sync";
10
10
  import { nodeKey } from "./inventory.js";
11
11
  import { must } from "./query.js";
12
12
  import { unwrapped } from "./unwrap.js";
@@ -66,77 +66,296 @@ export function resolvedTargets(project, nodes) {
66
66
  });
67
67
  return targets;
68
68
  }
69
- /** The values one expression passes: itself, or what an array or object literal holds. */
70
- function passedIn(expression, found) {
71
- if (isParenthesizedExpression(expression)) {
72
- passedIn(expression.expression, found);
73
- return;
74
- }
75
- if (isArrayLiteralExpression(expression)) {
76
- for (const element of expression.elements) {
77
- passedIn(isSpreadElement(element) ? element.expression : element, found);
78
- }
79
- return;
69
+ /** The declaration a shorthand property's value resolves to, an import followed to what it names. */
70
+ function shorthandTarget(project, node) {
71
+ const value = must(project.shorthandValueAt(project.handle(node)));
72
+ if (value === undefined || (value.flags & SymbolFlags.Alias) === 0) {
73
+ return value;
80
74
  }
81
- if (isObjectLiteralExpression(expression)) {
82
- for (const property of expression.properties) {
83
- if (isPropertyAssignment(property)) {
84
- passedIn(property.initializer, found);
75
+ const aliased = must(project.queries.aliased(value));
76
+ return aliased.declarations.length === 0 ? undefined : aliased;
77
+ }
78
+ /**
79
+ * The values one expression passes: each value it can be ({@link leavesOf}), or what an
80
+ * array or object literal it can be holds, at any depth.
81
+ */
82
+ function passedIn(written, found) {
83
+ for (const expression of leavesOf(written)) {
84
+ if (isArrayLiteralExpression(expression)) {
85
+ for (const element of expression.elements) {
86
+ passedIn(element, found);
85
87
  }
86
- else if (isShorthandPropertyAssignment(property)) {
87
- found.push({ site: property.name, name: property, shorthand: true });
88
+ }
89
+ else if (isObjectLiteralExpression(expression)) {
90
+ for (const property of expression.properties) {
91
+ if (isPropertyAssignment(property)) {
92
+ passedIn(property.initializer, found);
93
+ }
94
+ else if (isSpreadAssignment(property)) {
95
+ passedIn(property.expression, found);
96
+ }
97
+ else if (isShorthandPropertyAssignment(property)) {
98
+ found.push({ site: property.name, name: property, shorthand: true });
99
+ }
88
100
  }
89
101
  }
90
- return;
102
+ else if (!isOmittedExpression(expression)) {
103
+ found.push({ site: expression, name: calleeName(expression), shorthand: false });
104
+ }
91
105
  }
92
- const name = calleeName(expression);
93
- if (name !== undefined) {
94
- found.push({ site: expression, name, shorthand: false });
106
+ }
107
+ /** The operators whose value is one of their operands. */
108
+ const CHOOSING = new Set([
109
+ SyntaxKind.QuestionQuestionToken,
110
+ SyntaxKind.BarBarToken,
111
+ SyntaxKind.AmpersandAmpersandToken,
112
+ ]);
113
+ /**
114
+ * The expressions one value can be: itself, or each branch of a conditional, each operand
115
+ * of `??`, `||` and `&&` and the last of a comma, inside any wrapper that changes no value.
116
+ * Each is read alone because the checker reduces a union by subtype, so the type at the
117
+ * whole drops a class structurally equal to, or a subtype of, another branch's.
118
+ */
119
+ function leavesOf(written) {
120
+ const expression = unwrapped(isSpreadElement(written) ? written.expression : written);
121
+ if (isConditionalExpression(expression)) {
122
+ return [...leavesOf(expression.whenTrue), ...leavesOf(expression.whenFalse)];
95
123
  }
124
+ if (isBinaryExpression(expression)) {
125
+ const operator = expression.operatorToken.kind;
126
+ if (CHOOSING.has(operator)) {
127
+ return [...leavesOf(expression.left), ...leavesOf(expression.right)];
128
+ }
129
+ if (operator === SyntaxKind.CommaToken) {
130
+ return leavesOf(expression.right);
131
+ }
132
+ }
133
+ return [expression];
96
134
  }
97
135
  /** The values one argument passes, a spread argument's included. */
98
- export function valuesPassed(argument) {
136
+ function leavesPassed(argument) {
99
137
  const found = [];
100
- passedIn(isSpreadElement(argument) ? argument.expression : argument, found);
138
+ passedIn(argument, found);
101
139
  return found;
102
140
  }
141
+ /** The values one argument passes that name their declaration. */
142
+ export function valuesPassed(argument) {
143
+ return leavesPassed(argument).filter((value) => value.name !== undefined);
144
+ }
103
145
  /**
104
- * The classes of the target each call passes, by the call. A name that is no class is
105
- * followed through the initializer of each variable it declares, so a class bound to a
106
- * variable first is passed. A value that names no class of the target, a class of
107
- * another program included, passes nothing here.
146
+ * What one passed value holds: the declaration its name resolves to, and the classes and
147
+ * functions the checker's type at it holds ({@link constructedAt}). `known` holds names
148
+ * already resolved in a batch.
149
+ */
150
+ function heldBy(project, value, known) {
151
+ const { name } = value;
152
+ if (name === undefined) {
153
+ return { named: undefined, typed: constructedAt(project, value.site) };
154
+ }
155
+ if (value.shorthand) {
156
+ return { named: shorthandTarget(project, name), typed: [] };
157
+ }
158
+ return {
159
+ named: known?.get(name) ?? resolvedTargets(project, [name]).get(name),
160
+ typed: constructedAt(project, value.site),
161
+ };
162
+ }
163
+ /**
164
+ * The classes of the target each call passes, by the call: each class a value an argument
165
+ * can be holds as a constructor, as the checker types it. A name whose type erases the
166
+ * class is followed through what its declaration binds ({@link boundTo}) and, for a
167
+ * parameter, through what each call of its function passes at its place, so a class handed
168
+ * to a wrapper is passed. A class is used at each value that passes it. A class of another
169
+ * program passes nothing.
108
170
  */
109
171
  export function classesPassed(project, held, chains, calls) {
110
- const passed = new Map(calls.map((call) => [call, call.arguments.flatMap((argument) => valuesPassed(argument))]));
111
- const all = [...passed.values()].flat();
112
- const targets = resolvedTargets(project, all.filter((value) => !value.shorthand).map((value) => value.name));
172
+ const passed = new Map(calls.map((call) => [call, call.arguments.flatMap(leavesPassed)]));
173
+ const targets = resolvedTargets(project, [...passed.values()].flat().flatMap((value) => (value.shorthand ? [] : (value.name ?? []))));
113
174
  const classes = new Set(held.symbols.filter((symbol) => symbol.kind === "class").map((symbol) => symbol.id));
175
+ let calling;
176
+ const callsOfFunction = (symbol) => {
177
+ calling ??= callsByDeclaration(project);
178
+ return symbol.declarations.flatMap((handle) => calling?.get(keyOf(handle)) ?? []);
179
+ };
180
+ const classesOf = (value, seen) => {
181
+ const { named, typed } = heldBy(project, value, targets);
182
+ return [
183
+ ...typed.flatMap((symbol) => chains.declarationsOf(symbol).filter((id) => classes.has(id))),
184
+ ...(named === undefined ? [] : reached(named, seen)),
185
+ ];
186
+ };
114
187
  const reached = (symbol, seen) => {
115
188
  const ids = chains.declarationsOf(symbol).filter((id) => classes.has(id));
116
189
  if (ids.length > 0 || seen.has(symbol.id)) {
117
190
  return ids;
118
191
  }
119
192
  seen.add(symbol.id);
193
+ const through = (written) => leavesPassed(written).flatMap((value) => classesOf(value, seen));
120
194
  return symbol.declarations.flatMap((handle) => {
121
195
  const node = project.declarationAt(handle)?.node;
122
- if (node === undefined || !isVariableDeclaration(node) || node.initializer === undefined) {
196
+ if (node === undefined) {
123
197
  return [];
124
198
  }
125
- const name = calleeName(unwrapped(node.initializer));
126
- const next = name === undefined ? undefined : resolvedTargets(project, [name]).get(name);
127
- return next === undefined ? [] : reached(next, seen);
199
+ if (isParameterDeclaration(node)) {
200
+ return argumentsAt(project, node, callsOfFunction).flatMap(through);
201
+ }
202
+ const bound = boundTo(project, node);
203
+ return [
204
+ ...(bound.written === undefined ? [] : through(bound.written)),
205
+ ...bound.symbols.flatMap((next) => reached(next, seen)),
206
+ ];
207
+ });
208
+ };
209
+ return new Map([...passed].map(([call, values]) => {
210
+ const uses = new Map();
211
+ for (const value of values) {
212
+ for (const id of classesOf(value, new Set())) {
213
+ uses.set(`${id}\u0000${String(value.site.pos)}`, { id, site: value.site });
214
+ }
215
+ }
216
+ return [call, [...uses.values()]];
217
+ }));
218
+ }
219
+ /**
220
+ * The symbols of the classes and functions the checker's type at one expression holds as
221
+ * values, each member of a union: the static side of a class or a function is an
222
+ * anonymous type its declaration's symbol names, which an instance's type is not.
223
+ */
224
+ function constructedAt(project, written) {
225
+ return heldIn(project, must(project.queries.typeAt(unwrapped(written))));
226
+ }
227
+ /** The classes and functions one type holds as values, as {@link constructedAt} reads them. */
228
+ function heldIn(project, type) {
229
+ const { queries } = project;
230
+ return (type === undefined ? [] : must(queries.constituents(type))).flatMap((part) => {
231
+ if (!part.isObjectType() || (part.objectFlags & ObjectFlags.Anonymous) === 0) {
232
+ return [];
233
+ }
234
+ return must(queries.symbolOfType(part)) ?? [];
235
+ });
236
+ }
237
+ /**
238
+ * What a variable, a property or a destructured binding binds its name to: a variable's
239
+ * or a property's initializer, the value a shorthand property names, or the property of
240
+ * the destructured value a binding element reads.
241
+ */
242
+ function boundTo(project, node) {
243
+ if (isShorthandPropertyAssignment(node)) {
244
+ const value = shorthandTarget(project, node);
245
+ return { symbols: value === undefined ? [] : [value] };
246
+ }
247
+ if (isBindingElement(node)) {
248
+ const key = node.propertyName ?? node.name;
249
+ if (key === undefined || !isIdentifier(key)) {
250
+ return { symbols: [] };
251
+ }
252
+ const source = must(project.queries.typeAt(node.parent));
253
+ const property = source === undefined
254
+ ? undefined
255
+ : must(project.queries.propertiesOf(source)).find((one) => one.name === key.text);
256
+ return { symbols: property === undefined ? [] : [property] };
257
+ }
258
+ const written = initializerOf(node);
259
+ return written === undefined ? { symbols: [] } : { written, symbols: [] };
260
+ }
261
+ /** The value a variable or a property declaration binds its name to, where it writes one. */
262
+ function initializerOf(node) {
263
+ return isVariableDeclaration(node) || isPropertyAssignment(node) || isPropertyDeclaration(node)
264
+ ? node.initializer
265
+ : undefined;
266
+ }
267
+ /** The key of one declaration a symbol names, the same for each symbol the checker makes of it. */
268
+ function keyOf(handle) {
269
+ return `${handle.path}#${String(handle.index)}`;
270
+ }
271
+ /**
272
+ * Every call of the project by the declarations its callee can be: the function or the
273
+ * method it names, or else each function the checker's type at it holds, wherever that
274
+ * value came from. A name is also followed through what its binding copies
275
+ * ({@link boundTo}), which is how a call reaches a function an annotation erases.
276
+ * Only the target's own declarations are keys, as only its own functions are asked for.
277
+ */
278
+ function callsByDeclaration(project) {
279
+ const calls = callsOf(project);
280
+ const own = project.ownPaths();
281
+ const callees = resolvedTargets(project, calls.flatMap((call) => calleeName(call.expression) ?? []));
282
+ const named = new Map();
283
+ const keysOf = (symbol, seen) => {
284
+ const known = named.get(symbol.id);
285
+ if (known !== undefined || seen.has(symbol.id)) {
286
+ return known ?? [];
287
+ }
288
+ seen.add(symbol.id);
289
+ const keys = symbol.declarations
290
+ .filter((handle) => own.has(handle.path))
291
+ .flatMap((handle) => {
292
+ const node = project.declarationAt(handle)?.node;
293
+ const bound = node === undefined ? { symbols: [] } : boundTo(project, node);
294
+ const copied = [
295
+ ...bound.symbols,
296
+ ...(bound.written === undefined ? [] : leavesPassed(bound.written)).flatMap((value) => {
297
+ const { named: next, typed } = heldBy(project, value);
298
+ return [...typed, ...(next === undefined ? [] : [next])];
299
+ }),
300
+ ];
301
+ return [keyOf(handle), ...copied.flatMap((next) => keysOf(next, seen))];
128
302
  });
303
+ named.set(symbol.id, keys);
304
+ return keys;
129
305
  };
130
- const classesOf = (value) => {
131
- const symbol = value.shorthand
132
- ? must(project.shorthandValueAt(project.handle(value.name)))
133
- : targets.get(value.name);
134
- return symbol === undefined ? [] : reached(symbol, new Set());
306
+ const calleeOf = (call) => {
307
+ const name = calleeName(call.expression);
308
+ return name === undefined ? undefined : callees.get(name);
135
309
  };
136
- return new Map([...passed].map(([call, values]) => [
137
- call,
138
- values.flatMap((value) => classesOf(value).map((id) => ({ id, site: value.site }))),
139
- ]));
310
+ const declared = (call) => ((calleeOf(call)?.flags ?? 0) & (SymbolFlags.Function | SymbolFlags.Method)) !== 0;
311
+ const typed = calls.filter((call) => !declared(call));
312
+ const types = new Map(project.queries
313
+ .typesAt(typed.map((call) => unwrapped(call.expression)))
314
+ .map((type, index) => [typed[index], must(type)]));
315
+ const found = new Map();
316
+ for (const call of calls) {
317
+ const callee = calleeOf(call);
318
+ const held = [
319
+ ...(callee === undefined ? [] : [callee]),
320
+ ...(types.has(call) ? heldIn(project, types.get(call)) : []),
321
+ ];
322
+ for (const key of new Set(held.flatMap((symbol) => keysOf(symbol, new Set())))) {
323
+ found.set(key, [...(found.get(key) ?? []), call]);
324
+ }
325
+ }
326
+ return found;
327
+ }
328
+ /**
329
+ * What each call of the function a parameter belongs to passes at the parameter's place,
330
+ * every argument from there on for a rest parameter. The function is the one the checker's
331
+ * type at its own declaration names, so a function with no name of its own is called too.
332
+ * A `this` parameter takes no place, and an argument at or after a spread argument passes
333
+ * at every place the spread's length can move it to.
334
+ */
335
+ function argumentsAt(project, parameter, callsOfFunction) {
336
+ const owner = parameter.parent;
337
+ if (!isFunctionDeclaration(owner) &&
338
+ !isMethodDeclaration(owner) &&
339
+ !isArrowFunction(owner) &&
340
+ !isFunctionExpression(owner)) {
341
+ return [];
342
+ }
343
+ const calls = new Set(constructedAt(project, owner).flatMap(callsOfFunction));
344
+ const [first] = owner.parameters;
345
+ const erased = first !== undefined && isIdentifier(first.name) && first.name.text === "this";
346
+ const place = owner.parameters.indexOf(parameter) - (erased ? 1 : 0);
347
+ const rest = parameter.dotDotDotToken !== undefined;
348
+ return [...calls].flatMap((call) => {
349
+ let spreads = 0;
350
+ return call.arguments.flatMap((argument, index) => {
351
+ const spread = isSpreadElement(argument);
352
+ const earliest = index - spreads;
353
+ spreads += spread ? 1 : 0;
354
+ const open = spread || earliest < index;
355
+ const lands = open ? rest || earliest <= place : rest ? index >= place : index === place;
356
+ return lands ? [spread ? argument.expression : argument] : [];
357
+ });
358
+ });
140
359
  }
141
360
  /** Every class declaration of the target that carries a decorator. */
142
361
  export function decoratedClasses(project, held) {