@cplieger/deadset-ts 5.2.3 → 6.0.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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +28 -5
  3. package/dist/src/augmentations.js +50 -0
  4. package/dist/src/components.js +15 -15
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/convention-rows.js +17 -2
  7. package/dist/src/conventions.js +82 -1
  8. package/dist/src/dependencies.js +42 -26
  9. package/dist/src/entry-points.js +28 -14
  10. package/dist/src/exempt.js +1 -0
  11. package/dist/src/findings/completion.js +9 -4
  12. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  13. package/dist/src/findings/emitters.js +64 -25
  14. package/dist/src/findings/interfaces.js +4 -3
  15. package/dist/src/findings/intra-function.js +15 -2
  16. package/dist/src/findings/reads-and-writes.js +3 -2
  17. package/dist/src/findings/unused-declarations.js +13 -1
  18. package/dist/src/findings/visibility-narrowing.js +2 -1
  19. package/dist/src/findings-pass.js +1 -0
  20. package/dist/src/graph.js +6 -6
  21. package/dist/src/implementations.js +1 -1
  22. package/dist/src/indexed-access.js +215 -0
  23. package/dist/src/interface-satisfaction.js +85 -23
  24. package/dist/src/intra-function-parts.js +139 -30
  25. package/dist/src/inventory.js +6 -1
  26. package/dist/src/kinds.js +13 -12
  27. package/dist/src/matrix.js +34 -9
  28. package/dist/src/module-declarations.js +180 -0
  29. package/dist/src/outside-tests.js +17 -15
  30. package/dist/src/overrides.js +70 -9
  31. package/dist/src/query.js +7 -0
  32. package/dist/src/ref.js +9 -0
  33. package/dist/src/references.js +6 -0
  34. package/dist/src/report.js +6 -1
  35. package/dist/src/reporters/sarif.js +6 -2
  36. package/dist/src/reporters/text.js +23 -3
  37. package/dist/src/roots.js +50 -10
  38. package/dist/src/suppress-file.js +3 -5
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/workflow-steps.js +82 -0
  41. package/dist/src/workspace-programs.js +6 -31
  42. package/jsr.json +1 -1
  43. package/package.json +8 -2
  44. package/src/analysis.ts +40 -4
  45. package/src/augmentations.ts +72 -0
  46. package/src/components.ts +15 -13
  47. package/src/conformance-record.ts +2 -2
  48. package/src/convention-rows.ts +25 -3
  49. package/src/conventions.ts +106 -2
  50. package/src/dependencies.ts +51 -31
  51. package/src/entry-points.ts +32 -19
  52. package/src/exempt.ts +4 -1
  53. package/src/findings/completion.ts +11 -4
  54. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  55. package/src/findings/emitters.ts +89 -27
  56. package/src/findings/interfaces.ts +7 -6
  57. package/src/findings/intra-function.ts +20 -2
  58. package/src/findings/reads-and-writes.ts +7 -2
  59. package/src/findings/unused-declarations.ts +16 -1
  60. package/src/findings/visibility-narrowing.ts +2 -1
  61. package/src/findings-pass.ts +4 -1
  62. package/src/graph.ts +6 -6
  63. package/src/implementations.ts +1 -1
  64. package/src/indexed-access.ts +295 -0
  65. package/src/interface-satisfaction.ts +118 -27
  66. package/src/intra-function-parts.ts +196 -35
  67. package/src/inventory.ts +8 -0
  68. package/src/kinds.ts +14 -12
  69. package/src/matrix.ts +51 -8
  70. package/src/module-declarations.ts +241 -0
  71. package/src/outside-tests.ts +18 -16
  72. package/src/overrides.ts +80 -14
  73. package/src/query.ts +28 -0
  74. package/src/ref.ts +10 -0
  75. package/src/references.ts +6 -0
  76. package/src/report.ts +11 -1
  77. package/src/reporters/sarif.ts +9 -2
  78. package/src/reporters/text.ts +25 -3
  79. package/src/roots.ts +56 -7
  80. package/src/suppress-file.ts +3 -6
  81. package/src/sweep.ts +12 -0
  82. package/src/version.ts +1 -1
  83. package/src/workflow-steps.ts +104 -0
  84. package/src/workspace-programs.ts +6 -35
@@ -1,5 +1,6 @@
1
1
  import { evaluateEdges } from "../edges.js";
2
2
  import { liveFilesAt } from "../matrix.js";
3
+ import { declarationKey } from "../ref.js";
3
4
  import { ledgerOf } from "../suppress.js";
4
5
  import { insideSkipped } from "../type-errors.js";
5
6
  import { completed, dialsOf, reportable } from "./completion.js";
@@ -23,24 +24,24 @@ export const EMITTERS = new Map([
23
24
  ["intra-function", intraFunction],
24
25
  ]);
25
26
  /**
26
- * The references of every declaration an unanswered question could have kept live, which
27
- * the run reports nothing about: a finding about one would be computed from the gap.
27
+ * The keys of every declaration an unanswered question could have kept live, which the run
28
+ * reports nothing about: a finding about one would be computed from the gap.
28
29
  */
29
- function heldRefs(input) {
30
+ function heldKeys(input) {
30
31
  const held = new Set(input.swept.heldByUnanswered);
31
32
  if (held.size === 0) {
32
33
  return held;
33
34
  }
34
35
  return new Set(input.swept.matrix.union.symbols
35
36
  .filter((symbol) => held.has(symbol.id))
36
- .map((symbol) => symbol.ref));
37
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
37
38
  }
38
39
  /**
39
- * The references of every top-level declaration of a live component file. The file's
40
+ * The keys of every top-level declaration of a live component file. The file's
40
41
  * markup may use each of them, which no analysis of the markup can rule out, so the run
41
42
  * reports nothing about one while the file is live.
42
43
  */
43
- function markupRefs(input) {
44
+ function markupKeys(input) {
44
45
  const { swept } = input;
45
46
  if (swept.componentFiles.length === 0) {
46
47
  return new Set();
@@ -48,7 +49,7 @@ function markupRefs(input) {
48
49
  const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
49
50
  return new Set(swept.matrix.union.symbols
50
51
  .filter((symbol) => live.has(symbol.parent))
51
- .map((symbol) => symbol.ref));
52
+ .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
52
53
  }
53
54
  /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
54
55
  function completedOver(input, ...more) {
@@ -68,32 +69,70 @@ function dialsOver(input, found, suppressed, unmarked) {
68
69
  };
69
70
  }
70
71
  /**
71
- * Every finding of one run swept under its suppressions' marks, decided in one pass: every
72
- * family's findings completed by one step, so every component number is minted once, a
73
- * pending finding's among them; the finding each record claims withheld, whatever its
74
- * kind; the rest withheld by the dials, so a root one family reports withholds what
75
- * another family reports in its component; and every declared edge evaluated over what
76
- * remains, so a finding it holds pending is in its evaluation and nowhere else.
72
+ * Every finding of one run swept under its suppressions' marks, decided in one pass: each
73
+ * family's findings completed by one step, so every component number is minted once, the
74
+ * finding each record claims withheld, the rest withheld by the dials, so a root one family
75
+ * reports withholds what another reports in its component, and every declared edge
76
+ * evaluated over what remains, so a pending finding is in its evaluation alone. What the
77
+ * minimum confidence withheld is what the same decision at the lowest minimum adds.
77
78
  */
78
79
  export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
79
80
  const found = completedOver(input);
80
81
  const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
81
82
  const would = [...unmarked, ...found];
82
- const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
83
- const unanswered = heldRefs(input);
84
- const markup = markupRefs(input);
85
- const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) &&
86
- !unanswered.has(finding.symbol.ref) &&
87
- !markup.has(finding.symbol.ref) &&
88
- !insideSkipped(input.swept.skipped, finding.position)));
89
- const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
83
+ const decided = dialledOver(input, suppressed, found, unmarked);
84
+ const shown = input.config.analysis.minConfidence === "possible"
85
+ ? decided
86
+ : dialledOver(atPossible(input), {
87
+ unmarked: atPossible(suppressed.unmarked),
88
+ records: suppressed.records,
89
+ }, found, unmarked);
90
+ return { ...decided, would, withheld: withheldOf(decided.findings, shown.findings) };
91
+ }
92
+ /** The run's input with the minimum confidence at its lowest, which withholds nothing. */
93
+ function atPossible(input) {
94
+ const { config } = input;
90
95
  return {
91
- findings: evaluated.findings,
92
- evaluations: evaluated.evaluations,
93
- ledger,
94
- would,
96
+ ...input,
97
+ config: { ...config, analysis: { ...config.analysis, minConfidence: "possible" } },
95
98
  };
96
99
  }
100
+ /**
101
+ * The run's completed findings decided under its dials: the finding each record claims
102
+ * withheld, the rest withheld by the dials, and every declared edge evaluated over what
103
+ * remains.
104
+ */
105
+ function dialledOver(input, suppressed, found, unmarked) {
106
+ const would = [...unmarked, ...found];
107
+ const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
108
+ const unanswered = heldKeys(input);
109
+ const markup = markupKeys(input);
110
+ const kept = reportable(input, found.filter((finding) => {
111
+ const key = declarationKey(finding.symbol.ref, finding.position.path);
112
+ return (!ledger.withheld(finding) &&
113
+ !unanswered.has(key) &&
114
+ !markup.has(key) &&
115
+ !insideSkipped(input.swept.skipped, finding.position));
116
+ }));
117
+ const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
118
+ return { findings: evaluated.findings, evaluations: evaluated.evaluations, ledger };
119
+ }
120
+ /** The findings the lowest minimum would report that the run does not, counted by confidence. */
121
+ function withheldOf(reported, shown) {
122
+ const kept = new Set(reported.map(findingKey));
123
+ const counts = { certain: 0, probable: 0, possible: 0 };
124
+ for (const finding of shown) {
125
+ if (!kept.has(findingKey(finding))) {
126
+ counts[finding.confidence] += 1;
127
+ }
128
+ }
129
+ return counts;
130
+ }
131
+ /** A finding's identity across two decisions of one run: its position, code and subject. */
132
+ function findingKey(finding) {
133
+ const { path, line, column } = finding.position;
134
+ return JSON.stringify([path, line, column, finding.code, finding.symbol.ref]);
135
+ }
97
136
  /** Every finding the run reports, each family's in table order. */
98
137
  export function findingsOf(input) {
99
138
  return decidedFindings(input).findings;
@@ -54,15 +54,16 @@ function findingOf(code, symbol, message, implementations) {
54
54
  * The findings of the interfaces family, `DS1200` to `DS1299`: `DS1201`, a dead
55
55
  * interface, whose members fall with it unreported; and `DS1203`, a method of a live
56
56
  * interface no reference names, unless it is a marker, a method whose every
57
- * implementation has an empty body. A test file's declaration is neither kind's
58
- * subject, because a production sweep counts none of the references it can have.
57
+ * implementation has an empty body. A test file's declaration is neither kind's subject,
58
+ * because a production sweep counts none of the references it can have, save an interface
59
+ * the run that counts test references does not reach, which is `DS1201`'s.
59
60
  */
60
61
  export const interfaces = (input) => {
61
62
  const run = indexed(input);
62
63
  const found = [];
63
64
  for (const candidate of input.swept.sweep.candidates) {
64
65
  const symbol = run.symbolOf(candidate.id);
65
- if (symbol === undefined || run.inTest(candidate.id)) {
66
+ if (symbol === undefined || (run.inTest(candidate.id) && !candidate.unreferencedTest)) {
66
67
  continue;
67
68
  }
68
69
  if (symbol.kind === "interface") {
@@ -14,6 +14,19 @@ function overlapClause(overlap) {
14
14
  ? "no other rule is known to report it"
15
15
  : `also reported by ${spelled(overlap)}`;
16
16
  }
17
+ /**
18
+ * Whether one parameter's name is reported: every unread one of a function whose
19
+ * signature is free, and of a function used as a value only one no read parameter follows,
20
+ * because a caller may pass such a function more arguments than it declares.
21
+ */
22
+ function parameterReported(part, free, freeButValued) {
23
+ const place = part.parameter;
24
+ if (place?.inValue !== true && free.has(part.declaration)) {
25
+ return true;
26
+ }
27
+ const valued = place?.inValue === true || freeButValued.has(part.declaration);
28
+ return valued && place?.beforeARead !== true;
29
+ }
17
30
  /**
18
31
  * Whether one part is reported. A parameter needs its function's signature free to
19
32
  * change; a result needs that, every call in the loaded program to discard it, and every
@@ -25,10 +38,10 @@ function reported(input, part, callersUnknown) {
25
38
  if (part.used) {
26
39
  return false;
27
40
  }
28
- const { free, discardedEverywhere } = input.intraFunction;
41
+ const { free, freeButValued, discardedEverywhere } = input.intraFunction;
29
42
  switch (part.code) {
30
43
  case UNUSED_PARAMETER:
31
- return free.has(part.declaration);
44
+ return parameterReported(part, free, freeButValued);
32
45
  case UNUSED_RESULT:
33
46
  return (free.has(part.declaration) &&
34
47
  discardedEverywhere.has(part.declaration) &&
@@ -1,4 +1,5 @@
1
1
  import { positionKey } from "../position.js";
2
+ import { declarationKey } from "../ref.js";
2
3
  /** The containers whose type parameters `DS1303` reports: a function and a class method. */
3
4
  const SIGNATURES = new Set(["function", "method"]);
4
5
  function indexed(input) {
@@ -175,12 +176,12 @@ function unusedTypeParameters(run) {
175
176
  .map((symbol) => findingAbout(symbol, "DS1303", `type parameter ${symbol.name} is named in neither its signature nor its body`));
176
177
  }
177
178
  /**
178
- * The references of the declarations the write-only kind reports, whatever the
179
+ * The keys of the declarations the write-only kind reports, whatever the
179
180
  * severity: a narrowing kind yields each to it, since deleting the declaration
180
181
  * supersedes narrowing it.
181
182
  */
182
183
  export function writeOnlyDeclarations(input) {
183
- return new Set(writeOnlySymbols(indexed(input)).map((finding) => finding.symbol.ref));
184
+ return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
184
185
  }
185
186
  /**
186
187
  * The findings of the reads-and-writes family, `DS1300` to `DS1399`, in code order and
@@ -49,9 +49,14 @@ function codeOf(judged) {
49
49
  if (candidate.testOfDeadCode) {
50
50
  return "DS1005";
51
51
  }
52
- if (judged.test || judged.containerDead || NOT_THIS_FAMILY.has(symbol.kind)) {
52
+ if ((judged.test && !candidate.unreferencedTest) ||
53
+ judged.containerDead ||
54
+ NOT_THIS_FAMILY.has(symbol.kind)) {
53
55
  return undefined;
54
56
  }
57
+ if (candidate.unreferencedTest) {
58
+ return unreferencedCode(symbol);
59
+ }
55
60
  const referenced = candidate.productionRefs + candidate.testRefs > 0;
56
61
  if (symbol.kind === "enum-member" && !referenced) {
57
62
  return undefined;
@@ -73,6 +78,13 @@ function codeOf(judged) {
73
78
  }
74
79
  return judged.unreachable ? undefined : "DS1001";
75
80
  }
81
+ /** The code a declaration nothing references carries, by its shape and its visibility. */
82
+ function unreferencedCode(symbol) {
83
+ if (MEMBERS.has(symbol.kind)) {
84
+ return "DS1003";
85
+ }
86
+ return symbol.exported ? "DS1001" : "DS1002";
87
+ }
76
88
  /** The visibility a member's message names, where it narrows the member's reach. */
77
89
  function visibilityWord(symbol) {
78
90
  switch (symbol.visibility) {
@@ -1,4 +1,5 @@
1
1
  import { OUTSIDE } from "../graph.js";
2
+ import { declarationKey } from "../ref.js";
2
3
  import { writeOnlyDeclarations } from "./reads-and-writes.js";
3
4
  const UNNECESSARY_EXPORT = "DS1101";
4
5
  const UNREACHABLE_EXPORT = "DS1103";
@@ -119,7 +120,7 @@ function narrowing(facts, at, symbol, world) {
119
120
  const { closed } = world;
120
121
  const file = facts.fileOf[at] ?? OUTSIDE;
121
122
  if (!topLevelExport(facts, at, symbol) ||
122
- world.writeOnly.has(symbol.ref) ||
123
+ world.writeOnly.has(declarationKey(symbol.ref, symbol.position.path)) ||
123
124
  symbol.kind === "export-alias" ||
124
125
  entersByName(facts.rootedBy[at]) ||
125
126
  entersByName(facts.rootedBy[file]) ||
@@ -55,6 +55,7 @@ export function findingsPass(inputs) {
55
55
  reasonsRecorded: counts.reasonsRecorded,
56
56
  staleSuppressions: records.length,
57
57
  pending: decided.evaluations.filter((one) => one.finding !== undefined).length,
58
+ withheld: decided.withheld,
58
59
  },
59
60
  ledger,
60
61
  };
package/dist/src/graph.js CHANGED
@@ -32,11 +32,11 @@ const SWEPT = {
32
32
  };
33
33
  /**
34
34
  * Whether a root of each kind names a caller, which holds it live under both relations: a
35
- * tool reads the configuration it loads and the files its strings name, a runtime runs a
36
- * test file, a setup file, a worker, a page's module script, a command and a script, the
37
- * manifest names what enters the package, an alias global stands for the export it names,
38
- * and an entry file, a configured root and a pattern assert a caller. A library's
39
- * published API only supposes an unseen consumer, so it is live under reachability alone.
35
+ * tool reads what it loads and what its strings name, a runtime runs a test, setup or
36
+ * worker file, a page's module script, a command, a script or a workflow step, the manifest
37
+ * names what enters the package, an alias global stands for its export, and an entry file,
38
+ * a configured root and a pattern assert a caller. A library's published API only supposes
39
+ * an unseen consumer, so it is live under reachability alone.
40
40
  */
41
41
  const NAMES_A_CALLER = {
42
42
  "entry-file": true,
@@ -52,10 +52,10 @@ const NAMES_A_CALLER = {
52
52
  "configuration-file": true,
53
53
  "configuration-string": true,
54
54
  "test-runner": true,
55
- "mutation-testing": true,
56
55
  "browser-tests": true,
57
56
  worker: true,
58
57
  "html-entry": true,
58
+ "workflow-step": true,
59
59
  };
60
60
  /** Whether the sweep judges a declaration of one kind. */
61
61
  export function swept(kind) {
@@ -54,7 +54,7 @@ function typesAt(project, nodes) {
54
54
  return typeAt;
55
55
  }
56
56
  /** Whether one class member is a method whose every written body holds no statement. */
57
- function emptyImplementation(project, member) {
57
+ export function emptyImplementation(project, member) {
58
58
  const own = project.ownPaths();
59
59
  let bodies = 0;
60
60
  for (const handle of member.declarations) {
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The members an indexed-access type names. `T["a" | "b"]` reads the members `a` and `b`
3
+ * of `T` as a property access by either name would. Where the index is a type parameter
4
+ * a call fixes, the call names the members the literal type argument names, written or
5
+ * inferred, which is read off the parameter the type parameter annotates whole. The type
6
+ * query of a type-query alias is no reference.
7
+ */
8
+ import { isArrowFunction, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isIndexedAccessTypeNode, isLiteralTypeNode, isMethodDeclaration, isNewExpression, isNoSubstitutionTemplateLiteral, isNumericLiteral, isParenthesizedTypeNode, isPropertyAccessExpression, isStringLiteral, isTypeReferenceNode, isUnionTypeNode, SyntaxKind, } from "@typescript/native/unstable/ast";
9
+ import { nodeKey } from "./inventory.js";
10
+ import { renderPosition } from "./position.js";
11
+ import { UNANSWERED } from "./query.js";
12
+ import { typeQueryAliases } from "./type-query-alias.js";
13
+ function isGeneric(node) {
14
+ return ((isFunctionDeclaration(node) ||
15
+ isMethodDeclaration(node) ||
16
+ isArrowFunction(node) ||
17
+ isFunctionExpression(node)) &&
18
+ node.typeParameters !== undefined);
19
+ }
20
+ function unwrappedType(node) {
21
+ return isParenthesizedTypeNode(node) ? unwrappedType(node.type) : node;
22
+ }
23
+ /** The names a literal index type spells, or none where the index is any other type. */
24
+ function literalNames(index) {
25
+ const node = unwrappedType(index);
26
+ if (isUnionTypeNode(node)) {
27
+ const names = node.types.map(literalNames);
28
+ return names.every((one) => one !== undefined) ? names.flat() : undefined;
29
+ }
30
+ if (!isLiteralTypeNode(node)) {
31
+ return undefined;
32
+ }
33
+ const literal = node.literal;
34
+ if (isStringLiteral(literal) || isNoSubstitutionTemplateLiteral(literal)) {
35
+ return [literal.text];
36
+ }
37
+ return isNumericLiteral(literal) ? [String(Number(literal.text))] : undefined;
38
+ }
39
+ /** The name a literal type carries, a union's each, or none where a constituent is no literal. */
40
+ function literalTypeNames(type, parts) {
41
+ const names = [];
42
+ for (const part of parts(type)) {
43
+ if (part.isStringLiteralType()) {
44
+ names.push(part.value);
45
+ }
46
+ else if (part.isNumberLiteralType()) {
47
+ names.push(String(part.value));
48
+ }
49
+ else {
50
+ return undefined;
51
+ }
52
+ }
53
+ return names;
54
+ }
55
+ /**
56
+ * The references the indexed-access types of one project's own files make to the members
57
+ * they name, each made by the declaration that holds the type or the call, or by the file.
58
+ */
59
+ export function indexedAccessReferences(project, held, chains, root, testFiles) {
60
+ const queries = project.queries;
61
+ const accesses = [];
62
+ const generics = new Map();
63
+ const calls = [];
64
+ const names = new Set();
65
+ for (const file of project.ownSourceFiles()) {
66
+ const fileId = held.declarations.get(nodeKey(file, file));
67
+ if (fileId === undefined) {
68
+ continue;
69
+ }
70
+ const aliasTypes = new Set(typeQueryAliases(file).map((alias) => alias.type));
71
+ const visit = (node, from, enclosing) => {
72
+ const own = held.declarations.get(nodeKey(file, node));
73
+ const holder = own ?? from;
74
+ const inside = isGeneric(node) ? [...enclosing, node] : enclosing;
75
+ if (isIndexedAccessTypeNode(node) && !aliasTypes.has(node)) {
76
+ const literal = literalNames(node.indexType);
77
+ if (literal !== undefined) {
78
+ accesses.push({ file, from: holder, at: node, object: node.objectType, names: literal });
79
+ }
80
+ else {
81
+ parameterIndex(node, inside, file, generics, names);
82
+ }
83
+ }
84
+ if (isCallExpression(node) || isNewExpression(node)) {
85
+ const callee = node.expression;
86
+ const name = isIdentifier(callee)
87
+ ? callee.text
88
+ : isPropertyAccessExpression(callee) && isIdentifier(callee.name)
89
+ ? callee.name.text
90
+ : undefined;
91
+ if (name !== undefined) {
92
+ calls.push({ file, from: holder, call: node, name });
93
+ }
94
+ }
95
+ node.forEachChild((child) => {
96
+ visit(child, holder, inside);
97
+ });
98
+ };
99
+ file.forEachChild((child) => {
100
+ visit(child, fileId, []);
101
+ });
102
+ }
103
+ for (const { file, from, call, name } of calls) {
104
+ if (!names.has(name)) {
105
+ continue;
106
+ }
107
+ const resolved = queries.resolvedSignature(call);
108
+ if (resolved === UNANSWERED || resolved?.declaration === undefined) {
109
+ continue;
110
+ }
111
+ const declaration = project.declarationAt(resolved.declaration)?.node;
112
+ const byParameter = declaration === undefined
113
+ ? undefined
114
+ : generics.get(nodeKey(declaration.getSourceFile(), declaration));
115
+ if (byParameter === undefined) {
116
+ continue;
117
+ }
118
+ for (const { object, parameter } of byParameter) {
119
+ const fixed = queries.typeAtPosition(resolved, parameter);
120
+ const literal = fixed === UNANSWERED ? undefined : literalTypeNames(fixed, (type) => partsOf(type));
121
+ if (literal !== undefined) {
122
+ accesses.push({ file, from, at: call, object, names: literal });
123
+ }
124
+ }
125
+ }
126
+ function partsOf(type) {
127
+ const parts = queries.constituents(type);
128
+ return parts === UNANSWERED ? [type] : parts;
129
+ }
130
+ const objectNodes = accesses.flatMap((one) => ("kind" in one.object ? [one.object] : []));
131
+ const typeOf = new Map();
132
+ queries.typesAt(objectNodes).forEach((answer, index) => {
133
+ const node = objectNodes[index];
134
+ if (answer !== UNANSWERED && answer !== undefined && node !== undefined) {
135
+ typeOf.set(node, answer);
136
+ }
137
+ });
138
+ const found = [];
139
+ for (const access of accesses) {
140
+ const type = "kind" in access.object ? typeOf.get(access.object) : access.object;
141
+ if (type === undefined) {
142
+ continue;
143
+ }
144
+ const position = renderPosition(access.file, root, access.at.getStart());
145
+ for (const part of partsOf(type)) {
146
+ const properties = queries.propertiesOf(part);
147
+ if (properties === UNANSWERED) {
148
+ continue;
149
+ }
150
+ for (const property of properties) {
151
+ if (!access.names.includes(property.escapedName)) {
152
+ continue;
153
+ }
154
+ for (const to of chains.declarationsOf(property)) {
155
+ found.push({
156
+ from: access.from,
157
+ to,
158
+ position,
159
+ use: "read",
160
+ resolution: "batch",
161
+ test: testFiles.has(position.path),
162
+ });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ return found;
168
+ }
169
+ /**
170
+ * Records an indexed-access type whose index names a type parameter of an enclosing
171
+ * function that a parameter of that function is annotated with whole, so a call's
172
+ * argument for that parameter fixes the index.
173
+ */
174
+ function parameterIndex(node, enclosing, file, generics, names) {
175
+ const index = unwrappedType(node.indexType);
176
+ if (!isTypeReferenceNode(index) || !isIdentifier(index.typeName)) {
177
+ return;
178
+ }
179
+ const spelled = index.typeName.text;
180
+ for (const generic of [...enclosing].reverse()) {
181
+ const declares = (generic.typeParameters ?? []).some((parameter) => isIdentifier(parameter.name) && parameter.name.text === spelled);
182
+ if (!declares) {
183
+ continue;
184
+ }
185
+ const at = generic.parameters.findIndex((parameter) => {
186
+ const type = parameter.type === undefined ? undefined : unwrappedType(parameter.type);
187
+ return (type !== undefined &&
188
+ isTypeReferenceNode(type) &&
189
+ isIdentifier(type.typeName) &&
190
+ type.typeName.text === spelled);
191
+ });
192
+ if (at >= 0) {
193
+ const key = nodeKey(file, generic);
194
+ generics.set(key, [...(generics.get(key) ?? []), { object: node.objectType, parameter: at }]);
195
+ const name = calledName(generic);
196
+ if (name !== undefined) {
197
+ names.add(name);
198
+ }
199
+ }
200
+ return;
201
+ }
202
+ }
203
+ /** The name a call writes to call one generic function: its own, or its variable's. */
204
+ function calledName(generic) {
205
+ const own = generic.name;
206
+ if (own !== undefined && isIdentifier(own)) {
207
+ return own.text;
208
+ }
209
+ const parent = generic.parent;
210
+ return parent.kind === SyntaxKind.VariableDeclaration &&
211
+ parent.name !== undefined &&
212
+ isIdentifier(parent.name)
213
+ ? parent.name.text
214
+ : undefined;
215
+ }