@cplieger/deadset-ts 8.0.0-dev.20 → 8.0.0-dev.21

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 (68) hide show
  1. package/dist/src/alias-chain.js +33 -2
  2. package/dist/src/analysis.js +18 -77
  3. package/dist/src/conformance-record.js +2 -2
  4. package/dist/src/contract/contract.generated.js +4 -3
  5. package/dist/src/dependencies.js +1 -1
  6. package/dist/src/exemptions/argument-places.js +131 -0
  7. package/dist/src/{calls.js → exemptions/calls.js} +126 -179
  8. package/dist/src/{exempt-classes.js → exemptions/classes.js} +1 -1
  9. package/dist/src/{decorator.js → exemptions/decorator.js} +19 -30
  10. package/dist/src/exemptions/detectors.js +48 -0
  11. package/dist/src/{enum-group.js → exemptions/enum-group.js} +7 -7
  12. package/dist/src/{erased-stores.js → exemptions/erased-stores.js} +4 -6
  13. package/dist/src/{framework-lifecycle.js → exemptions/framework-lifecycle.js} +31 -36
  14. package/dist/src/{generated-file.js → exemptions/generated-file.js} +4 -4
  15. package/dist/src/{injection-container.js → exemptions/injection-container.js} +19 -25
  16. package/dist/src/exemptions/installed-globals.js +173 -0
  17. package/dist/src/{interface-satisfaction.js → exemptions/interface-satisfaction.js} +52 -121
  18. package/dist/src/exemptions/lifecycle.js +150 -0
  19. package/dist/src/{reflective-lookup.js → exemptions/reflective-lookup.js} +3 -3
  20. package/dist/src/{serialization-contract.js → exemptions/serialization-contract.js} +415 -329
  21. package/dist/src/exemptions/sites.js +182 -0
  22. package/dist/src/{template-field.js → exemptions/template-field.js} +7 -7
  23. package/dist/src/{value-reach.js → exemptions/value-reach.js} +14 -7
  24. package/dist/src/index/sites.js +1 -1
  25. package/dist/src/index/values.js +124 -0
  26. package/dist/src/liveness/sweep.js +2 -2
  27. package/dist/src/query.js +48 -0
  28. package/dist/src/roots/configured-declarations.js +1 -1
  29. package/dist/src/verbs/print-retained.js +23 -1
  30. package/jsr.json +1 -1
  31. package/package.json +1 -1
  32. package/src/alias-chain.ts +38 -2
  33. package/src/analysis.ts +19 -100
  34. package/src/bindings.ts +1 -1
  35. package/src/conformance-record.ts +2 -2
  36. package/src/contract/contract.generated.ts +4 -3
  37. package/src/dependencies.ts +1 -1
  38. package/src/exemptions/argument-places.ts +198 -0
  39. package/src/{calls.ts → exemptions/calls.ts} +190 -224
  40. package/src/{exempt-classes.ts → exemptions/classes.ts} +1 -1
  41. package/src/exemptions/decorator.ts +56 -0
  42. package/src/exemptions/detectors.ts +75 -0
  43. package/src/{enum-group.ts → exemptions/enum-group.ts} +9 -9
  44. package/src/{erased-stores.ts → exemptions/erased-stores.ts} +5 -8
  45. package/src/{framework-lifecycle.ts → exemptions/framework-lifecycle.ts} +39 -48
  46. package/src/{generated-file.ts → exemptions/generated-file.ts} +6 -6
  47. package/src/{injection-container.ts → exemptions/injection-container.ts} +28 -33
  48. package/src/exemptions/installed-globals.ts +255 -0
  49. package/src/{interface-satisfaction.ts → exemptions/interface-satisfaction.ts} +69 -160
  50. package/src/exemptions/lifecycle.ts +286 -0
  51. package/src/{reflective-lookup.ts → exemptions/reflective-lookup.ts} +5 -5
  52. package/src/{serialization-contract.ts → exemptions/serialization-contract.ts} +563 -429
  53. package/src/exemptions/sites.ts +293 -0
  54. package/src/{template-field.ts → exemptions/template-field.ts} +11 -11
  55. package/src/{value-reach.ts → exemptions/value-reach.ts} +16 -8
  56. package/src/facts.ts +1 -1
  57. package/src/index/sites.ts +1 -1
  58. package/src/index/values.ts +170 -0
  59. package/src/liveness/sweep.ts +2 -2
  60. package/src/query.ts +109 -10
  61. package/src/roots/configured-declarations.ts +1 -2
  62. package/src/stores.ts +1 -1
  63. package/src/verbs/print-retained.ts +29 -1
  64. package/dist/src/exempt.js +0 -164
  65. package/dist/src/value-flow.js +0 -74
  66. package/src/decorator.ts +0 -78
  67. package/src/exempt.ts +0 -295
  68. package/src/value-flow.ts +0 -104
@@ -8,14 +8,12 @@
8
8
  */
9
9
 
10
10
  import {
11
- isArrayLiteralExpression,
12
11
  isArrowFunction,
13
12
  isAsExpression,
14
13
  isAwaitExpression,
15
14
  isCallExpression,
16
15
  isClassDeclaration,
17
16
  isClassExpression,
18
- isClassStaticBlockDeclaration,
19
17
  isElementAccessExpression,
20
18
  isExpressionStatement,
21
19
  isFunctionDeclaration,
@@ -43,17 +41,14 @@ import {
43
41
  isTypeAssertion,
44
42
  isTypeLiteralNode,
45
43
  isVariableDeclaration,
46
- ModifierFlags,
47
44
  NodeFlags,
48
45
  SyntaxKind,
49
46
  type CallExpression,
50
47
  type Expression,
51
48
  type Node,
52
- type ParameterDeclaration,
53
49
  type SourceFile,
54
50
  type SpreadAssignment,
55
51
  } from "@typescript/native/ast";
56
- import { parenthesesRemoved } from "./unwrap.ts";
57
52
  import {
58
53
  SignatureKind,
59
54
  SymbolFlags,
@@ -63,36 +58,50 @@ import {
63
58
  type Symbol as TSSymbol,
64
59
  type Type,
65
60
  } from "@typescript/native/sync";
66
- import { aliasChains, type AliasChains } from "./alias-chain.ts";
67
- import { calleeName, resolvedTargets, valuesPassed } from "./calls.ts";
68
- import { classMembers, memberComponent, typesAt } from "./class-members.ts";
69
- import type { DeclarationEntry } from "./config.ts";
61
+ import { aliasChains, type AliasChains } from "../alias-chain.ts";
62
+ import { carriedValues, valuesOf } from "../index/values.ts";
63
+ import { parenthesesRemoved } from "../unwrap.ts";
64
+ import {
65
+ fills,
66
+ knownElements,
67
+ layoutOf,
68
+ placesIn,
69
+ spansOf,
70
+ type Layout,
71
+ type Placed,
72
+ type Span,
73
+ } from "./argument-places.ts";
74
+ import type { Invocation } from "./calls.ts";
75
+ import { classMembers, memberComponent, typesAt } from "../class-members.ts";
76
+ import type { DeclarationEntry } from "../config.ts";
70
77
  import {
71
78
  names,
72
79
  namesADeclaration,
73
80
  resolveEntries,
74
81
  spelledEntry,
75
- } from "./roots/configured-declarations.ts";
76
- import type { DetectorInput, Evidence } from "./exempt.ts";
77
- import { nodeKey, type Inventory } from "./inventory.ts";
78
- import { renderPosition, type Position } from "./position.ts";
79
- import { must, Unanswerable } from "./query.ts";
80
- import type { ProjectView } from "./session.ts";
82
+ } from "../roots/configured-declarations.ts";
83
+ import type { DetectorInput, Evidence } from "./lifecycle.ts";
84
+ import type { Inventory } from "../inventory.ts";
85
+ import { renderPosition, type Position } from "../position.ts";
86
+ import { must, Unanswerable } from "../query.ts";
87
+ import type { ProjectView } from "../session.ts";
81
88
  import { erasedStores, type ErasedStores } from "./erased-stores.ts";
82
- import { checkedTypes, CONVERSION_MEMBERS, valueReach, type Reached } from "./value-reach.ts";
89
+ import type { CallSite, ParameterSite, ProjectSites } from "./sites.ts";
90
+ import {
91
+ checkedTypes,
92
+ CONVERSION_MEMBERS,
93
+ CONVERSION_NAMES,
94
+ ERASED,
95
+ valueReach,
96
+ type Reached,
97
+ } from "./value-reach.ts";
83
98
 
84
99
  /** The serializer every run reads, whatever the configuration names. */
85
100
  const JSON_STRINGIFY: DeclarationEntry = { global: "JSON.stringify" };
86
101
 
87
- /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
88
- const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
89
-
90
102
  /** The method a serializer calls on a value that carries one, wherever the value goes. */
91
103
  const SERIALIZED_CONVERSION = "toJSON";
92
104
 
93
- /** The parameter types that keep nothing of the value they are given. */
94
- const ERASED = TypeFlags.Any | TypeFlags.Unknown;
95
-
96
105
  /** The standard library's promise types, whose value is their one type argument. */
97
106
  const PROMISES: ReadonlySet<string> = new Set(["Promise", "PromiseLike"]);
98
107
 
@@ -105,25 +114,10 @@ const MENTION_DEPTH = 3;
105
114
  */
106
115
  type Reach = "properties" | "conversions";
107
116
 
108
- /** One call of the project's own files, and the class `this` is a value of where it is written. */
109
- interface Site {
110
- readonly call: CallExpression;
111
- readonly self: string | undefined;
112
- }
113
-
114
- /** One parameter of a function of the project that may hand on what it is given. */
115
- interface Parameter {
116
- /** The inventory's identifier of the function's declaration. */
117
- readonly fn: string;
118
- readonly index: number;
119
- readonly rest: boolean;
120
- readonly name: Node;
121
- }
122
-
123
117
  /** One place a function hands a parameter of its own to the argument of a call. */
124
118
  interface Forwarded {
125
- readonly parameter: Parameter;
126
- readonly site: Site;
119
+ readonly parameter: ParameterSite;
120
+ readonly site: CallSite;
127
121
  readonly argument: number;
128
122
  }
129
123
 
@@ -138,51 +132,6 @@ function wider(a: Reach | undefined, b: Reach | undefined): Reach | undefined {
138
132
  return a === "conversions" || b === "conversions" ? "conversions" : (a ?? b);
139
133
  }
140
134
 
141
- /** Whether one node declares a static member, whose `this` is the class itself. */
142
- function isStatic(node: Node): boolean {
143
- const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
144
- return (flags & ModifierFlags.Static) !== 0;
145
- }
146
-
147
- /** Whether one node opens a body whose `this` is not an instance of the enclosing class. */
148
- function resetsThis(node: Node): boolean {
149
- return (
150
- isFunctionDeclaration(node) ||
151
- isFunctionExpression(node) ||
152
- isClassStaticBlockDeclaration(node) ||
153
- isStatic(node) ||
154
- ((isMethodDeclaration(node) ||
155
- isGetAccessorDeclaration(node) ||
156
- isSetAccessorDeclaration(node)) &&
157
- isObjectLiteralExpression(node.parent))
158
- );
159
- }
160
-
161
- /** The parameters and declaring node of one function-like declaration of the project. */
162
- function functionOf(
163
- node: Node,
164
- ): { readonly declared: Node; readonly parameters: readonly ParameterDeclaration[] } | undefined {
165
- if (
166
- isFunctionDeclaration(node) ||
167
- (isMethodDeclaration(node) && !isObjectLiteralExpression(node.parent))
168
- ) {
169
- return { declared: node, parameters: node.parameters };
170
- }
171
- if (
172
- isVariableDeclaration(node) &&
173
- node.initializer !== undefined &&
174
- (isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))
175
- ) {
176
- return { declared: node, parameters: node.initializer.parameters };
177
- }
178
- return undefined;
179
- }
180
-
181
- /** Whether one parameter is a `this` parameter, which types `this` and binds no argument. */
182
- function isThisParameter(parameter: ParameterDeclaration): boolean {
183
- return isIdentifier(parameter.name) && parameter.name.text === "this";
184
- }
185
-
186
135
  /** Whether an argument is written in a form that holds no class instance, which saves a type. */
187
136
  function holdsNoInstance(argument: Expression): boolean {
188
137
  return (
@@ -195,137 +144,6 @@ function holdsNoInstance(argument: Expression): boolean {
195
144
  );
196
145
  }
197
146
 
198
- /** The expression an argument evaluates, through parentheses and a spread. */
199
- function evaluated(argument: Expression): Expression {
200
- const value = isSpreadElement(argument) ? argument.expression : argument;
201
- return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
202
- }
203
-
204
- /** The methods of `Function.prototype` that invoke a function with arguments of their own. */
205
- const FUNCTION_METHODS: ReadonlySet<string> = new Set(["call", "apply", "bind"]);
206
-
207
- /** The interfaces of the standard library that declare those methods for a function value. */
208
- const FUNCTION_INTERFACES: ReadonlySet<string> = new Set([
209
- "Function",
210
- "CallableFunction",
211
- "NewableFunction",
212
- ]);
213
-
214
- /**
215
- * One call as the function it invokes sees it: the expression naming that function,
216
- * and the argument each of its parameters binds, in order.
217
- */
218
- interface Invocation {
219
- readonly callee: Expression;
220
- readonly arguments: readonly Expression[];
221
- /** Whether the call reaches the callee through a method of `Function.prototype`. */
222
- readonly indirect: boolean;
223
- }
224
-
225
- /** A call through a method of `Function.prototype`: the method's name and the invocation. */
226
- interface MethodForm {
227
- readonly method: Node;
228
- readonly invocation: Invocation;
229
- }
230
-
231
- /**
232
- * The invocation a call writes through a method of `Function.prototype`, read from
233
- * the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
234
- * binds `a`, and `f.bind(self, a)(b)`, which passes `b` after it. An `apply` argument
235
- * that is no array literal is the value of the first parameter, whose elements it
236
- * carries. Whether the method is the prototype's is resolution's to say.
237
- */
238
- function throughMethod(call: CallExpression): MethodForm | undefined {
239
- const expression = parenthesesRemoved(call.expression);
240
- if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
241
- const [, ...rest] = call.arguments;
242
- let passed: readonly Expression[] = rest;
243
- if (expression.name.text === "apply") {
244
- const list = rest[0] === undefined ? undefined : parenthesesRemoved(rest[0]);
245
- passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
246
- }
247
- return {
248
- method: expression.name,
249
- invocation: { callee: expression.expression, arguments: passed, indirect: true },
250
- };
251
- }
252
- if (isCallExpression(expression)) {
253
- const bound = parenthesesRemoved(expression.expression);
254
- if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
255
- return {
256
- method: bound.name,
257
- invocation: {
258
- callee: bound.expression,
259
- arguments: [...expression.arguments.slice(1), ...call.arguments],
260
- indirect: true,
261
- },
262
- };
263
- }
264
- }
265
- return undefined;
266
- }
267
-
268
- /** What one walk of the project's own files found: its calls and its functions' parameters. */
269
- interface Walked {
270
- readonly sites: readonly Site[];
271
- readonly parameters: readonly Parameter[];
272
- /** The class properties whose declaration writes an initializer, by identifier. */
273
- readonly initialized: ReadonlySet<string>;
274
- }
275
-
276
- /**
277
- * Every call of the project's own files with the class `this` stands for there, and
278
- * every identifier-named parameter of every function, method and function-valued
279
- * variable the project declares, at the position of the argument it binds.
280
- */
281
- function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walked {
282
- const sites: Site[] = [];
283
- const parameters: Parameter[] = [];
284
- const initialized = new Set<string>();
285
- for (const file of project.ownSourceFiles()) {
286
- const visit = (node: Node, self: string | undefined): void => {
287
- let inner = self;
288
- if (isPropertyDeclaration(node) && node.initializer !== undefined) {
289
- const id = held.declarations.get(nodeKey(file, node));
290
- if (id !== undefined) {
291
- initialized.add(id);
292
- }
293
- }
294
- if (isClassDeclaration(node) || isClassExpression(node)) {
295
- inner = held.declarations.get(nodeKey(file, node));
296
- } else if (resetsThis(node)) {
297
- inner = undefined;
298
- }
299
- if (node.kind === SyntaxKind.CallExpression) {
300
- sites.push({ call: node as CallExpression, self });
301
- }
302
- const fn = functionOf(node);
303
- const id = fn === undefined ? undefined : held.declarations.get(nodeKey(file, fn.declared));
304
- if (fn !== undefined && id !== undefined) {
305
- fn.parameters
306
- .filter((parameter) => !isThisParameter(parameter))
307
- .forEach((parameter, index) => {
308
- if (isIdentifier(parameter.name)) {
309
- parameters.push({
310
- fn: id,
311
- index,
312
- rest: parameter.dotDotDotToken !== undefined,
313
- name: parameter.name,
314
- });
315
- }
316
- });
317
- }
318
- node.forEachChild((child) => {
319
- visit(child, inner);
320
- });
321
- };
322
- file.forEachChild((child) => {
323
- visit(child, undefined);
324
- });
325
- }
326
- return { sites, parameters, initialized };
327
- }
328
-
329
147
  /** The calls of `Object` that read every own property of their argument by name. */
330
148
  const ENUMERATIONS: ReadonlySet<string> = new Set([
331
149
  "keys",
@@ -354,12 +172,14 @@ function keepsNoName<Brand>(project: ProjectView<Brand>, type: Type): boolean {
354
172
 
355
173
  /**
356
174
  * The parameters that keep no member name of the value they are given, as `keepsNoName`
357
- * reads it, and a rest parameter whose elements keep none, each by its symbol.
175
+ * reads it, and a rest parameter whose number index holds a type that keeps none, an
176
+ * element of an array, a tuple, an array-like interface or a union or an intersection of
177
+ * them, each by its symbol. A rest a type parameter types is left to its calls' flows.
358
178
  */
359
179
  function erasedParameters<Brand>(
360
180
  project: ProjectView<Brand>,
361
- parameters: readonly Parameter[],
362
- ): ReadonlyMap<number, Parameter> {
181
+ parameters: readonly ParameterSite[],
182
+ ): ReadonlyMap<number, ParameterSite> {
363
183
  const types = typesAt(
364
184
  project,
365
185
  parameters.map((parameter) => parameter.name),
@@ -372,10 +192,16 @@ function erasedParameters<Brand>(
372
192
  if (!parameter.rest) {
373
193
  return keepsNoName(project, type);
374
194
  }
375
- const [element] = must(project.queries.typeArguments(type));
376
- return element !== undefined && keepsNoName(project, element);
195
+ const element =
196
+ (type.flags & TypeFlags.Instantiable) === 0
197
+ ? must(project.queries.numberIndexOf(type))
198
+ : undefined;
199
+ return (
200
+ element !== undefined &&
201
+ must(project.queries.constituents(element)).some((part) => keepsNoName(project, part))
202
+ );
377
203
  });
378
- const bySymbol = new Map<number, Parameter>();
204
+ const bySymbol = new Map<number, ParameterSite>();
379
205
  project
380
206
  .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
381
207
  .map(must)
@@ -388,6 +214,15 @@ function erasedParameters<Brand>(
388
214
  return bySymbol;
389
215
  }
390
216
 
217
+ /**
218
+ * The type any place of a signature's rest may take, the number index of the rest's type;
219
+ * none where it has no rest or the type no number index.
220
+ */
221
+ function restElementOf<Brand>(project: ProjectView<Brand>, signature: Signature): Type | undefined {
222
+ const rest = must(project.queries.restTypeOf(signature));
223
+ return rest === undefined ? undefined : must(project.queries.numberIndexOf(rest));
224
+ }
225
+
391
226
  /** Whether one node is a function, whose body runs only where something calls it. */
392
227
  function isFunctionNode(node: Node): boolean {
393
228
  return (
@@ -437,11 +272,11 @@ function namesWritten(fn: Node): ReadonlySet<string> {
437
272
  */
438
273
  function enumeratedParameters<Brand>(
439
274
  project: ProjectView<Brand>,
440
- erased: ReadonlyMap<number, Parameter>,
441
- ): readonly Parameter[] {
275
+ erased: ReadonlyMap<number, ParameterSite>,
276
+ ): readonly ParameterSite[] {
442
277
  const asked: {
443
278
  readonly node: Node;
444
- readonly parameter: Parameter;
279
+ readonly parameter: ParameterSite;
445
280
  readonly object: Node | undefined;
446
281
  }[] = [];
447
282
  for (const parameter of erased.values()) {
@@ -472,7 +307,7 @@ function enumeratedParameters<Brand>(
472
307
  } else if (isForInStatement(node)) {
473
308
  read.push(node.expression);
474
309
  }
475
- for (const value of read.map(evaluated)) {
310
+ for (const value of read.flatMap(valuesOf)) {
476
311
  if (isIdentifier(value) && value.text === spelled) {
477
312
  asked.push({ node: value, parameter, object });
478
313
  }
@@ -509,17 +344,20 @@ function enumeratedParameters<Brand>(
509
344
  /** Each place a call's argument passes one of the erased parameters it can see. */
510
345
  function handedOn<Brand>(
511
346
  project: ProjectView<Brand>,
512
- sites: readonly Site[],
513
- erased: ReadonlyMap<number, Parameter>,
514
- invocationOf: (call: CallExpression) => Invocation,
347
+ sites: ProjectSites,
348
+ erased: ReadonlyMap<number, ParameterSite>,
515
349
  ): readonly Forwarded[] {
516
350
  const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
517
- const candidates = sites.flatMap((site) =>
518
- invocationOf(site.call).arguments.flatMap((argument, index) =>
519
- valuesPassed(argument)
520
- .filter((value) => spellings.has(value.name.getText()))
521
- .map((value) => ({ site, argument: index, value })),
522
- ),
351
+ const candidates = sites.calls.flatMap((site) =>
352
+ sites.invocations
353
+ .of(site.call)
354
+ .arguments.flatMap((argument, index) =>
355
+ carriedValues(argument).flatMap((value) =>
356
+ value.name !== undefined && spellings.has(value.name.getText())
357
+ ? [{ site, argument: index, value: { ...value, name: value.name } }]
358
+ : [],
359
+ ),
360
+ ),
523
361
  );
524
362
  const plain = candidates.filter((one) => !one.value.shorthand);
525
363
  const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name))).map(must);
@@ -566,12 +404,12 @@ function implementedOwners<Brand>(
566
404
  (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
567
405
  node.type !== undefined &&
568
406
  node.initializer !== undefined &&
569
- isObjectLiteralExpression(evaluated(node.initializer))
407
+ isObjectLiteralExpression(parenthesesRemoved(node.initializer))
570
408
  ) {
571
409
  naming.push(node.type);
572
410
  } else if (
573
411
  (isSatisfiesExpression(node) || isAsExpression(node)) &&
574
- isObjectLiteralExpression(evaluated(node.expression))
412
+ isObjectLiteralExpression(parenthesesRemoved(node.expression))
575
413
  ) {
576
414
  naming.push(node.type);
577
415
  }
@@ -603,9 +441,14 @@ function implementedOwners<Brand>(
603
441
  /** The destination of each call's arguments, with the project's forwarding functions settled. */
604
442
  interface Destinations {
605
443
  /** The function one call invokes and the arguments its parameters bind. */
606
- invocation(call: CallExpression): Invocation;
607
- /** The destination one argument of one call is, if it is one. */
608
- at(call: CallExpression, argument: number): Destination | undefined;
444
+ readonly invocation: (call: CallExpression) => Invocation;
445
+ /** The signatures each call binds. */
446
+ readonly bindings: Bindings;
447
+ /**
448
+ * The destination one argument of one call is, if it is one; with `element`, the
449
+ * destination of that element of a spread of known length, at the one place it fills.
450
+ */
451
+ at(call: CallExpression, argument: number, element?: number): Destination | undefined;
609
452
  /** Whether a call can be a destination at any argument, read without a round trip. */
610
453
  mayReceive(call: CallExpression): boolean;
611
454
  }
@@ -618,33 +461,16 @@ interface Destinations {
618
461
  * parameter the function enumerates or hands on to a destination, holding back the widest
619
462
  * of what those hold back, settled by widening until no parameter moves.
620
463
  */
621
- function destinations<Brand>(
622
- input: DetectorInput<Brand>,
623
- chains: AliasChains,
624
- walked: Walked,
625
- ): Destinations {
626
- const { project, held } = input;
464
+ function destinations<Brand>(input: DetectorInput<Brand>, chains: AliasChains): Destinations {
465
+ const { project, held, sites } = input;
627
466
  const queries = project.queries;
628
467
  const own = project.ownPaths();
629
468
  const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(
630
469
  namesADeclaration,
631
470
  );
632
- const forms = new Map<CallExpression, MethodForm>();
633
- for (const site of walked.sites) {
634
- const form = throughMethod(site.call);
635
- if (form !== undefined) {
636
- forms.set(site.call, form);
637
- }
638
- }
639
- const callees = resolvedTargets(project, [
640
- ...walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []),
641
- ...[...forms.values()].flatMap((form) => {
642
- const name = calleeName(form.invocation.callee);
643
- return name === undefined ? [form.method] : [form.method, name];
644
- }),
645
- ]);
646
- const erased = erasedParameters(project, walked.parameters);
647
- const byFunction = new Map<string, Parameter[]>();
471
+ const { of: invocation, callee: calleeOf } = sites.invocations;
472
+ const erased = erasedParameters(project, sites.parameters);
473
+ const byFunction = new Map<string, ParameterSite[]>();
648
474
  for (const parameter of erased.values()) {
649
475
  byFunction.set(parameter.fn, [...(byFunction.get(parameter.fn) ?? []), parameter]);
650
476
  }
@@ -685,39 +511,16 @@ function destinations<Brand>(
685
511
  */
686
512
  const mayBeOutside = (target: TSSymbol): boolean =>
687
513
  target.declarations.some((handle) => outside(target, false, [handle]));
688
- const invocation = (call: CallExpression): Invocation => {
689
- const form = forms.get(call);
690
- const method = form === undefined ? undefined : callees.get(form.method);
691
- // Only the standard library's function methods pass their arguments on: the
692
- // program's own `call` and `Reflect.apply` invoke nothing here.
693
- if (
694
- form !== undefined &&
695
- method !== undefined &&
696
- mayBeOutside(method) &&
697
- FUNCTION_INTERFACES.has(must(project.queries.parentOf(method))?.name ?? "")
698
- ) {
699
- return form.invocation;
700
- }
701
- return { callee: call.expression, arguments: call.arguments, indirect: false };
702
- };
703
- const calleeOf = (call: CallExpression): TSSymbol | undefined => {
704
- const name = calleeName(invocation(call).callee);
705
- return name === undefined ? undefined : callees.get(name);
706
- };
707
- const signatures = new Map<CallExpression, Signature | undefined>();
708
- const resolvedOf = (call: CallExpression): Signature | undefined => {
709
- if (!signatures.has(call)) {
710
- signatures.set(call, must(queries.resolvedSignature(call)));
711
- }
712
- return signatures.get(call);
713
- };
514
+ const bindings = callBindings(project, invocation);
714
515
  /**
715
516
  * The declarations of a callee that the declaration of a direct call's resolved
716
517
  * signature lies in, or all of them where it lies in none: a declaration merged into
717
518
  * the callee's symbol that the call does not resolve to does not decide where it runs.
718
519
  */
719
520
  const calledDeclarations = (call: CallExpression, target: TSSymbol): readonly NodeHandle[] => {
720
- const declaration = invocation(call).indirect ? undefined : resolvedOf(call)?.declaration;
521
+ const declaration = invocation(call).indirect
522
+ ? undefined
523
+ : bindings.of(call)[0]?.signature.declaration;
721
524
  const signature =
722
525
  declaration === undefined ? undefined : project.declarationAt(declaration)?.node;
723
526
  if (declaration === undefined || signature === undefined) {
@@ -754,61 +557,125 @@ function destinations<Brand>(
754
557
  ];
755
558
  return inner.some((one) => mentions(one, parameter, depth + 1));
756
559
  };
757
- const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
758
- const erasedAt = (call: CallExpression, argument: number): boolean => {
759
- let indexes = erasedArguments.get(call);
760
- if (indexes === undefined) {
761
- const invoked = invocation(call);
762
- // Through a method of the prototype the resolved signature is the method's, so
763
- // the parameters are read off the callee's own signatures, any one erasing.
764
- const type = invoked.indirect ? must(queries.typeAt(invoked.callee)) : undefined;
765
- const signatures = invoked.indirect
766
- ? type === undefined
767
- ? []
768
- : must(queries.signaturesOf(type, SignatureKind.Call))
769
- : [resolvedOf(call)].filter((one) => one !== undefined);
770
- // A type parameter of the callee that nothing constrains and its result does not name
771
- // keeps nothing of the value either: the instantiated signature names the value's own
772
- // type there instead, and a result that names it hands the value back.
773
- const generic = invoked.indirect ? undefined : resolvedOf(call);
774
- const declared = generic === undefined ? undefined : must(queries.genericOf(generic));
775
- const unconstrained = (index: number): boolean => {
776
- const parameter =
777
- declared === undefined ? undefined : must(queries.parameterType(declared, index));
778
- const parts =
779
- parameter === undefined
780
- ? []
781
- : parameter.isUnionType()
782
- ? must(queries.constituents(parameter))
783
- : [parameter];
784
- const result = declared === undefined ? undefined : must(queries.returnTypeOf(declared));
785
- return parts.some((part) => {
786
- if (!part.isTypeParameter() || (result !== undefined && mentions(result, part, 0))) {
787
- return false;
788
- }
789
- const constraint = must(queries.constraintOf(part));
790
- return constraint === undefined || (constraint.flags & ERASED) !== 0;
791
- });
560
+ const erasedPlaces = new Map<CallExpression, ReadonlySet<string>>();
561
+ /**
562
+ * Whether an argument of a call is erased: where `element` is given, that element of a
563
+ * span of known length at the one place it fills, and otherwise at any place the
564
+ * argument fills, in any signature the call binds. An argument no signature the call
565
+ * binds has a place for says nothing of what it keeps, so it counts as erased.
566
+ */
567
+ const erasedAt = (call: CallExpression, argument: number, element?: number): boolean => {
568
+ let erased = erasedPlaces.get(call);
569
+ if (erased === undefined) {
570
+ // A type parameter or an indexed access over one keeps nothing of the value where its
571
+ // base constraint is erased or absent, unless the result names the type parameter:
572
+ // the instantiated result is then the value's own type, handed back. A mapped type
573
+ // over type parameters keeps nothing where any of theirs is erased, whatever the
574
+ // result names, because the checker infers through it a copy without the class's
575
+ // non-public members, and which keys the mapping keeps is not read.
576
+ const erasedConstraint = (generic: Type): boolean => {
577
+ const constraint = must(queries.baseConstraintOf(generic));
578
+ return constraint === undefined || (constraint.flags & ERASED) !== 0;
792
579
  };
793
- indexes = new Set(
794
- invoked.arguments.flatMap((_unused, index) =>
795
- unconstrained(index) ||
796
- signatures.some((signature) => {
797
- const parameter = must(queries.parameterType(signature, index));
798
- return parameter !== undefined && (parameter.flags & ERASED) !== 0;
799
- })
800
- ? [index]
801
- : [],
802
- ),
803
- );
804
- erasedArguments.set(call, indexes);
580
+ const keepsNothing = (part: Type, result: Type | undefined): boolean => {
581
+ const mapped = must(queries.mappedParametersOf(part));
582
+ if (mapped.length > 0) {
583
+ return mapped.some(erasedConstraint);
584
+ }
585
+ if ((part.flags & TypeFlags.Instantiable) === 0) {
586
+ return false;
587
+ }
588
+ const free = part.isTypeParameter() ? part : must(queries.indexedAccessOf(part))?.object;
589
+ if (free?.isTypeParameter() === true && result !== undefined && mentions(result, free, 0)) {
590
+ return false;
591
+ }
592
+ return erasedConstraint(part);
593
+ };
594
+ // At a rest placement the declaration's type is its rest's element, the number index,
595
+ // which the checker reads through the base constraint of a rest a type parameter
596
+ // types, so an element erased there is one the declaration leaves generic.
597
+ const unconstrained = (declared: Signature | undefined, placed: Placed): boolean => {
598
+ if (declared === undefined) {
599
+ return false;
600
+ }
601
+ const result = must(queries.returnTypeOf(declared));
602
+ if (!placed.rest) {
603
+ const parameter = must(queries.parameterType(declared, placed.place));
604
+ return (parameter === undefined ? [] : must(queries.constituents(parameter))).some(
605
+ (part) => keepsNothing(part, result),
606
+ );
607
+ }
608
+ const rest = must(queries.restTypeOf(declared));
609
+ const element = restElementOf(project, declared);
610
+ if (rest?.isTypeParameter() === true && result !== undefined && mentions(result, rest, 0)) {
611
+ return false;
612
+ }
613
+ return (
614
+ element === undefined ||
615
+ must(queries.constituents(element)).some(
616
+ (part) => (part.flags & ERASED) !== 0 || keepsNothing(part, result),
617
+ )
618
+ );
619
+ };
620
+ const found = new Set<string>();
621
+ const placedAt = new Set<string>();
622
+ const unplaced = new Map<string, number>();
623
+ for (const one of bindings.of(call)) {
624
+ const asked = new Set<string>();
625
+ for (const placed of one.placed()) {
626
+ const { argument: at, element: given, place, uncovered, rest } = placed;
627
+ const key = `${String(at)}:${String(given)}`;
628
+ if (uncovered) {
629
+ unplaced.set(key, at);
630
+ continue;
631
+ }
632
+ placedAt.add(key);
633
+ const where = `${key}:${rest ? "rest" : String(place)}`;
634
+ if (found.has(key) || asked.has(where)) {
635
+ continue;
636
+ }
637
+ asked.add(where);
638
+ // A signature that is its own declaration, as one an indirect call binds, has its
639
+ // rest's element judged as the declaration's alone.
640
+ const own = rest && one.signature === one.declared;
641
+ const parameter = own
642
+ ? undefined
643
+ : rest
644
+ ? restElementOf(project, one.signature)
645
+ : must(queries.parameterType(one.signature, place));
646
+ if (
647
+ (rest && !own && parameter === undefined) ||
648
+ (parameter !== undefined && (parameter.flags & ERASED) !== 0) ||
649
+ unconstrained(one.declared, placed)
650
+ ) {
651
+ found.add(key);
652
+ found.add(String(at));
653
+ }
654
+ }
655
+ }
656
+ for (const [key, at] of unplaced) {
657
+ if (!placedAt.has(key)) {
658
+ found.add(key);
659
+ found.add(String(at));
660
+ }
661
+ }
662
+ erased = found;
663
+ erasedPlaces.set(call, erased);
805
664
  }
806
- return indexes.has(argument);
665
+ const span = bindings.spans(call).find((one) => one.argument === argument);
666
+ return element === undefined || span?.open !== false
667
+ ? erased.has(String(argument))
668
+ : erased.has(`${String(argument)}:${String(element)}`);
807
669
  };
808
670
 
809
671
  const forwards = new Map<string, Reach>();
810
- const keyOf = (parameter: Parameter): string => `${parameter.fn}\u0000${String(parameter.index)}`;
811
- const at = (call: CallExpression, argument: number): Destination | undefined => {
672
+ const keyOf = (parameter: ParameterSite): string =>
673
+ `${parameter.fn}\u0000${String(parameter.index)}`;
674
+ const at = (
675
+ call: CallExpression,
676
+ argument: number,
677
+ element?: number,
678
+ ): Destination | undefined => {
812
679
  const target = calleeOf(call);
813
680
  if (target === undefined) {
814
681
  return undefined;
@@ -818,18 +685,21 @@ function destinations<Brand>(
818
685
  return { reach: "properties", callee: spelledEntry(entry.entry, held) };
819
686
  }
820
687
  if (outside(target, true, calledDeclarations(call, target))) {
821
- return erasedAt(call, argument)
688
+ return erasedAt(call, argument, element)
822
689
  ? {
823
690
  reach: "conversions",
824
691
  callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
825
692
  }
826
693
  : undefined;
827
694
  }
695
+ const spans = bindings.spans(call).filter((span) => span.argument === argument);
828
696
  for (const fn of chains.declarationsOf(target)) {
829
- const parameter = (byFunction.get(fn) ?? []).find(
830
- (one) => one.index === argument || (one.rest && one.index <= argument),
831
- );
832
- const reach = parameter === undefined ? undefined : forwards.get(keyOf(parameter));
697
+ const reach = (byFunction.get(fn) ?? [])
698
+ .filter((one) => spans.some((span) => fills(span, one.index, one.rest, element)))
699
+ .reduce<Reach | undefined>(
700
+ (widest, one) => wider(widest, forwards.get(keyOf(one))),
701
+ undefined,
702
+ );
833
703
  if (reach !== undefined) {
834
704
  return { reach, callee: held.symbols.find((symbol) => symbol.id === fn)?.name ?? fn };
835
705
  }
@@ -840,7 +710,7 @@ function destinations<Brand>(
840
710
  for (const parameter of enumeratedParameters(project, erased)) {
841
711
  forwards.set(keyOf(parameter), "properties");
842
712
  }
843
- const forwarded = handedOn(project, walked.sites, erased, invocation);
713
+ const forwarded = handedOn(project, sites, erased);
844
714
  for (let moved = true; moved;) {
845
715
  moved = false;
846
716
  for (const one of forwarded) {
@@ -866,7 +736,7 @@ function destinations<Brand>(
866
736
  .declarationsOf(target)
867
737
  .some((fn) => (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))));
868
738
  };
869
- return { invocation, at, mayReceive };
739
+ return { invocation, bindings, at, mayReceive };
870
740
  }
871
741
 
872
742
  /** One value that carries classes and object types of the target into a destination. */
@@ -889,17 +759,20 @@ function joined(a: Reached, b: Reached): Reached {
889
759
  function flowsOf<Brand>(
890
760
  project: ProjectView<Brand>,
891
761
  held: Inventory,
892
- sites: readonly Site[],
762
+ sites: readonly CallSite[],
893
763
  to: Destinations,
894
764
  stores: ErasedStores,
895
765
  ): readonly Flow[] {
896
766
  const reach = valueReach(project, held, stores.inside);
897
767
  const arguments_ = sites.flatMap((site) =>
898
768
  to.mayReceive(site.call)
899
- ? to.invocation(site.call).arguments.flatMap((argument, index) => {
900
- const value = evaluated(argument);
901
- return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
902
- })
769
+ ? to
770
+ .invocation(site.call)
771
+ .arguments.flatMap((argument, index) =>
772
+ valuesOf(argument).flatMap((value) =>
773
+ holdsNoInstance(value) ? [] : [{ site, argument: index, value }],
774
+ ),
775
+ )
903
776
  : [],
904
777
  );
905
778
  const asked = arguments_.filter((one) => one.value.kind !== SyntaxKind.ThisKeyword);
@@ -914,45 +787,91 @@ function flowsOf<Brand>(
914
787
  project,
915
788
  arguments_.map((one) => one.value),
916
789
  );
917
- const passed = arguments_.flatMap((one, index) => {
918
- const type = typeOf.get(one.value);
919
- let reached: Reached = { classes: new Set<string>(), members: [] };
920
- if (one.value.kind === SyntaxKind.ThisKeyword) {
921
- reached = {
922
- classes: new Set(one.site.self === undefined ? [] : [one.site.self]),
923
- members: [],
924
- };
925
- } else if (type !== undefined) {
926
- reached = reach.reachedFrom(type);
790
+ /**
791
+ * The types one argument's value carries, each with the element of the argument's span
792
+ * it fills. A spread's value is its elements, which a container other than an array does
793
+ * not walk to, so the span's element types join. A spread of known length gives each
794
+ * element at its own place, as does any type read for it that is a tuple of that length,
795
+ * and every other type fills every place the argument does.
796
+ */
797
+ const partsOf = (
798
+ one: (typeof arguments_)[number],
799
+ types: readonly Type[],
800
+ ): readonly { readonly element: number | undefined; readonly types: readonly Type[] }[] => {
801
+ const span = to.bindings.spans(one.site.call).find((at) => at.argument === one.argument);
802
+ const given = span?.spread === true ? span.given.filter((type) => type !== undefined) : [];
803
+ if (span === undefined || !span.spread || span.open) {
804
+ return [{ element: undefined, types: [...types, ...given] }];
927
805
  }
928
- for (const stored of [...(checked[index] ?? []), ...(stores.ofVariable(one.value) ?? [])]) {
929
- reached = joined(reached, reach.reachedFrom(stored));
806
+ const whole: Type[] = [];
807
+ const elements = span.given.map((type) => (type === undefined ? [] : [type]));
808
+ for (const type of types) {
809
+ const known = knownElements(project, type, elements.length);
810
+ if (known === undefined) {
811
+ whole.push(type);
812
+ } else {
813
+ known.forEach((part, element) => {
814
+ if (part !== undefined) {
815
+ elements[element]?.push(part);
816
+ }
817
+ });
818
+ }
930
819
  }
931
- const carries = reached.classes.size > 0 || reached.members.length > 0;
932
- const destination = carries ? to.at(one.site.call, one.argument) : undefined;
933
- return destination === undefined
934
- ? []
935
- : [
936
- {
937
- site: one.value,
938
- classes: reached.classes,
939
- members: reached.members,
940
- to: destination,
941
- detail: `passed to ${destination.callee}`,
942
- },
943
- ];
820
+ return [
821
+ { element: undefined, types: whole },
822
+ ...elements.map((parts, element) => ({ element, types: parts })),
823
+ ];
824
+ };
825
+ const passed = arguments_.flatMap((one, index) => {
826
+ const type = typeOf.get(one.value);
827
+ const self: Reached = {
828
+ classes: new Set(
829
+ one.value.kind === SyntaxKind.ThisKeyword && one.site.self !== undefined
830
+ ? [one.site.self]
831
+ : [],
832
+ ),
833
+ members: [],
834
+ };
835
+ const types = [
836
+ ...(type === undefined ? [] : [type]),
837
+ ...(checked[index] ?? []),
838
+ ...(stores.ofVariable(one.value) ?? []),
839
+ ];
840
+ return partsOf(one, types).flatMap(({ element, types: parts }) => {
841
+ const reached = parts.reduce<Reached>(
842
+ (all, part) => joined(all, reach.reachedFrom(part)),
843
+ element === undefined ? self : { classes: new Set<string>(), members: [] },
844
+ );
845
+ const carries = reached.classes.size > 0 || reached.members.length > 0;
846
+ const destination = carries ? to.at(one.site.call, one.argument, element) : undefined;
847
+ return destination === undefined
848
+ ? []
849
+ : [
850
+ {
851
+ site: one.value,
852
+ classes: reached.classes,
853
+ members: reached.members,
854
+ to: destination,
855
+ detail: `passed to ${destination.callee}`,
856
+ },
857
+ ];
858
+ });
944
859
  });
945
860
  const generic = typeParameterFlows(
946
861
  project,
947
862
  reach,
948
863
  sites,
864
+ to.bindings,
949
865
  arguments_.flatMap((one) => {
950
866
  const type = typeOf.get(one.value);
951
- const parameters = type === undefined ? [] : typeParametersOf(project, type, 0);
952
- const destination = parameters.length === 0 ? undefined : to.at(one.site.call, one.argument);
953
- return destination === undefined
954
- ? []
955
- : parameters.map((parameter) => ({ parameter, to: destination }));
867
+ return partsOf(one, type === undefined ? [] : [type]).flatMap(({ element, types }) => {
868
+ const parameters = types.flatMap((part) => typeParametersOf(project, part, 0));
869
+ const destination =
870
+ parameters.length === 0 ? undefined : to.at(one.site.call, one.argument, element);
871
+ return destination === undefined
872
+ ? []
873
+ : parameters.map((parameter) => ({ parameter, to: destination }));
874
+ });
956
875
  }),
957
876
  );
958
877
  const left = stores.outside.flatMap((store) => {
@@ -972,10 +891,14 @@ function flowsOf<Brand>(
972
891
  return [...passed, ...generic, ...left];
973
892
  }
974
893
 
975
- /** The type parameters one type names, through constituents and type arguments. */
894
+ /**
895
+ * The type parameters one type names, through constituents, type arguments and the type
896
+ * parameters a mapped type is over. A class's polymorphic `this` type is no parameter a
897
+ * call binds: the reach walks it as its class.
898
+ */
976
899
  function typeParametersOf<Brand>(project: ProjectView<Brand>, type: Type, depth: number): Type[] {
977
900
  if (type.isTypeParameter()) {
978
- return [type];
901
+ return type.isThisType === true ? [] : [type];
979
902
  }
980
903
  if (depth >= MENTION_DEPTH) {
981
904
  return [];
@@ -984,6 +907,7 @@ function typeParametersOf<Brand>(project: ProjectView<Brand>, type: Type, depth:
984
907
  return [
985
908
  ...(type.isUnionType() || type.isIntersectionType() ? must(queries.constituents(type)) : []),
986
909
  ...must(queries.typeArguments(type)),
910
+ ...must(queries.mappedParametersOf(type)),
987
911
  ].flatMap((one) => typeParametersOf(project, one, depth + 1));
988
912
  }
989
913
 
@@ -1039,6 +963,11 @@ function bound<Brand>(
1039
963
  return [];
1040
964
  }
1041
965
  const { queries } = project;
966
+ // A place of a rest the parameter types is declared as an indexed access over it, and
967
+ // what the call passes there is one element of the type it binds.
968
+ if (must(queries.indexedAccessOf(declared))?.object.id === parameter.id) {
969
+ return [instantiated];
970
+ }
1042
971
  if (declared.isUnionType()) {
1043
972
  const parts = must(queries.constituents(declared));
1044
973
  if (parts.some((part) => part.id === parameter.id)) {
@@ -1059,6 +988,223 @@ function bound<Brand>(
1059
988
  });
1060
989
  }
1061
990
 
991
+ /** A declared type at a place and the type a call puts there. */
992
+ interface Placing {
993
+ readonly from: Type;
994
+ readonly into: Type;
995
+ }
996
+
997
+ /**
998
+ * One signature a call binds its arguments to: the one the checker resolves for a direct
999
+ * call, and each call signature of the value the method is read from for one through
1000
+ * `call`, `apply` or `bind`, whose resolved signature is the method's.
1001
+ */
1002
+ interface Binding {
1003
+ readonly call: CallExpression;
1004
+ /** The signature whose parameter types the arguments meet. */
1005
+ readonly signature: Signature;
1006
+ /** The generic signature the call instantiates: the resolved one's, or the callee's own. */
1007
+ readonly declared: Signature | undefined;
1008
+ /** The parameter place each argument binds in `signature`. */
1009
+ readonly placed: () => readonly Placed[];
1010
+ /**
1011
+ * The declared and the instantiated type at each place an argument binds, and an open
1012
+ * rest's element type.
1013
+ */
1014
+ readonly places: () => readonly Placing[];
1015
+ /** The type arguments the call writes for the declared signature, if it writes them. */
1016
+ readonly written: readonly Node[] | undefined;
1017
+ }
1018
+
1019
+ /** The signatures each call binds, each call read once. */
1020
+ interface Bindings {
1021
+ of(call: CallExpression): readonly Binding[];
1022
+ /** Those of every call, asked of the checker in batches. */
1023
+ all(calls: readonly CallExpression[]): readonly Binding[];
1024
+ /** The places each argument of one call fills, read as `argument-places.ts` reads them. */
1025
+ spans(call: CallExpression): readonly Span[];
1026
+ }
1027
+
1028
+ /** One value read once, at its first use. */
1029
+ function once<T>(read: () => T): () => T {
1030
+ let held: { readonly value: T } | undefined;
1031
+ return () => (held ??= { value: read() }).value;
1032
+ }
1033
+
1034
+ /**
1035
+ * The signatures a project's calls bind, and the place each argument binds in each. A
1036
+ * direct call binds the signature the checker resolves, its places taking the types the
1037
+ * checker instantiates there. Through `call`, `apply` or `bind` a call binds every call
1038
+ * signature of the function, a place taking the type of the argument that fills it: the
1039
+ * checker instantiates those methods from the arguments, not from the function's own
1040
+ * parameters, so no instantiation of them says what the function keeps.
1041
+ */
1042
+ function callBindings<Brand>(
1043
+ project: ProjectView<Brand>,
1044
+ invocation: (call: CallExpression) => Invocation,
1045
+ ): Bindings {
1046
+ const { queries } = project;
1047
+ const known = new Map<CallExpression, readonly Binding[]>();
1048
+ const passed = new Map<CallExpression, readonly (Type | undefined)[]>();
1049
+ const spanned = new Map<CallExpression, readonly Span[]>();
1050
+ const layouts = new Map<number, Layout>();
1051
+ const valueOf = (argument: Expression): Expression =>
1052
+ isSpreadElement(argument) ? argument.expression : argument;
1053
+ // An indirect call's places take its arguments' types, and a spread's type says how
1054
+ // many places it fills; a direct call with neither reads no argument's type.
1055
+ const typed = (call: CallExpression): boolean => {
1056
+ const invoked = invocation(call);
1057
+ return invoked.indirect || invoked.arguments.some(isSpreadElement);
1058
+ };
1059
+ const typesPassed = (call: CallExpression): readonly (Type | undefined)[] => {
1060
+ let types = passed.get(call);
1061
+ if (types === undefined) {
1062
+ types = typesAt(project, invocation(call).arguments.map(valueOf));
1063
+ passed.set(call, types);
1064
+ }
1065
+ return types;
1066
+ };
1067
+ const spans = (call: CallExpression): readonly Span[] => {
1068
+ let found = spanned.get(call);
1069
+ if (found === undefined) {
1070
+ const invoked = invocation(call);
1071
+ found = spansOf(
1072
+ project,
1073
+ invoked.arguments,
1074
+ invoked.listed,
1075
+ typed(call) ? typesPassed(call) : [],
1076
+ );
1077
+ spanned.set(call, found);
1078
+ }
1079
+ return found;
1080
+ };
1081
+ const layout = (signature: Signature): Layout => {
1082
+ let found = layouts.get(signature.id);
1083
+ if (found === undefined) {
1084
+ found = layoutOf(project, signature);
1085
+ layouts.set(signature.id, found);
1086
+ }
1087
+ return found;
1088
+ };
1089
+ const distinct = (pairs: readonly Placing[]): readonly Placing[] => [
1090
+ ...new Map(
1091
+ pairs.map((pair) => [`${String(pair.from.id)}:${String(pair.into.id)}`, pair]),
1092
+ ).values(),
1093
+ ];
1094
+ // The checker infers a type parameter a mapped type is over by reversing the mapping,
1095
+ // into a copy that names no class, so at a place declared as one the value passed is
1096
+ // what each of its type parameters carries.
1097
+ const mappedFrom = (from: Type, value: Type | undefined): readonly Placing[] =>
1098
+ value === undefined
1099
+ ? []
1100
+ : must(queries.constituents(from)).flatMap((part) =>
1101
+ must(queries.mappedParametersOf(part)).map((source) => ({ from: source, into: value })),
1102
+ );
1103
+ const direct = (call: CallExpression, resolved: Signature | undefined): readonly Binding[] => {
1104
+ if (resolved === undefined) {
1105
+ return [];
1106
+ }
1107
+ const declared = must(queries.genericOf(resolved));
1108
+ const placed = once(() => placesIn(spans(call), layout(resolved)));
1109
+ const places = () => {
1110
+ if (declared === undefined) {
1111
+ return [];
1112
+ }
1113
+ return distinct(
1114
+ placed().flatMap((one) => {
1115
+ const from = one.uncovered ? undefined : must(queries.parameterType(declared, one.place));
1116
+ if (from === undefined) {
1117
+ return [];
1118
+ }
1119
+ const into = must(queries.parameterType(resolved, one.place));
1120
+ const element = one.rest ? restElementOf(project, declared) : undefined;
1121
+ const elements = one.rest ? restElementOf(project, resolved) : undefined;
1122
+ return [
1123
+ ...(into === undefined ? [] : [{ from, into }]),
1124
+ ...(element === undefined || elements === undefined
1125
+ ? []
1126
+ : [{ from: element, into: elements }]),
1127
+ ...mappedFrom(from, one.given ?? typesPassed(call)[one.argument]),
1128
+ ];
1129
+ }),
1130
+ );
1131
+ };
1132
+ return [{ call, signature: resolved, declared, placed, places, written: call.typeArguments }];
1133
+ };
1134
+ const indirect = (call: CallExpression, callee: Type | undefined): readonly Binding[] =>
1135
+ (callee === undefined ? [] : must(queries.signaturesOf(callee, SignatureKind.Call))).map(
1136
+ (signature) => {
1137
+ const placed = once(() => placesIn(spans(call), layout(signature)));
1138
+ return {
1139
+ call,
1140
+ signature,
1141
+ declared: signature,
1142
+ placed,
1143
+ places: () =>
1144
+ distinct(
1145
+ placed().flatMap(({ place, given, uncovered, rest }) => {
1146
+ const from = uncovered ? undefined : must(queries.parameterType(signature, place));
1147
+ const element = rest ? restElementOf(project, signature) : undefined;
1148
+ return from === undefined || given === undefined
1149
+ ? []
1150
+ : [
1151
+ { from, into: given },
1152
+ ...(element === undefined ? [] : [{ from: element, into: given }]),
1153
+ ...mappedFrom(from, given),
1154
+ ];
1155
+ }),
1156
+ ),
1157
+ written: undefined,
1158
+ };
1159
+ },
1160
+ );
1161
+ const of = (call: CallExpression): readonly Binding[] => {
1162
+ let bound = known.get(call);
1163
+ if (bound === undefined) {
1164
+ const invoked = invocation(call);
1165
+ bound = invoked.indirect
1166
+ ? indirect(call, must(queries.typeAt(invoked.callee)))
1167
+ : direct(call, must(queries.resolvedSignature(call)));
1168
+ known.set(call, bound);
1169
+ }
1170
+ return bound;
1171
+ };
1172
+ const all = (calls: readonly CallExpression[]): readonly Binding[] => {
1173
+ const asked = calls.filter((call) => !known.has(call));
1174
+ const plain = asked.filter((call) => !invocation(call).indirect);
1175
+ queries
1176
+ .resolvedSignatures(plain)
1177
+ .map(must)
1178
+ .forEach((resolved, at) => {
1179
+ const call = plain[at];
1180
+ if (call !== undefined) {
1181
+ known.set(call, direct(call, resolved));
1182
+ }
1183
+ });
1184
+ const through = asked.filter((call) => invocation(call).indirect);
1185
+ const callees = typesAt(
1186
+ project,
1187
+ through.map((call) => invocation(call).callee),
1188
+ );
1189
+ const typing = asked.filter((call) => typed(call) && !passed.has(call));
1190
+ const values = typesAt(
1191
+ project,
1192
+ typing.flatMap((call) => invocation(call).arguments.map(valueOf)),
1193
+ );
1194
+ let offset = 0;
1195
+ for (const call of typing) {
1196
+ const count = invocation(call).arguments.length;
1197
+ passed.set(call, values.slice(offset, offset + count));
1198
+ offset += count;
1199
+ }
1200
+ through.forEach((call, at) => {
1201
+ known.set(call, indirect(call, callees[at]));
1202
+ });
1203
+ return calls.flatMap(of);
1204
+ };
1205
+ return { of, all, spans };
1206
+ }
1207
+
1062
1208
  /**
1063
1209
  * The flows a type parameter of a function of the project carries where a value its type
1064
1210
  * names reaches a destination: each call of the function passes its type argument there,
@@ -1068,68 +1214,56 @@ function bound<Brand>(
1068
1214
  function typeParameterFlows<Brand>(
1069
1215
  project: ProjectView<Brand>,
1070
1216
  reach: ReturnType<typeof valueReach>,
1071
- sites: readonly Site[],
1217
+ sites: readonly CallSite[],
1218
+ bindings: Bindings,
1072
1219
  pending: { readonly parameter: Type; readonly to: Destination }[],
1073
1220
  ): readonly Flow[] {
1074
1221
  const { queries } = project;
1075
- const byName = new Map<string, Site[]>();
1076
- for (const site of sites) {
1077
- const name = calleeName(site.call.expression)?.getText();
1078
- if (name !== undefined) {
1079
- byName.set(name, [...(byName.get(name) ?? []), site]);
1222
+ let calls: ReadonlyMap<Node, readonly Binding[]> | undefined;
1223
+ const byDeclaration = (): ReadonlyMap<Node, readonly Binding[]> => {
1224
+ const found = new Map<Node, Binding[]>();
1225
+ for (const one of bindings.all(sites.map((site) => site.call))) {
1226
+ const handle = one.declared?.declaration;
1227
+ const node = handle === undefined ? undefined : project.declarationAt(handle)?.node;
1228
+ if (node !== undefined) {
1229
+ found.set(node, [...(found.get(node) ?? []), one]);
1230
+ }
1080
1231
  }
1081
- }
1232
+ return found;
1233
+ };
1082
1234
  const found: Flow[] = [];
1083
- const seen = new Set<number>();
1235
+ // One type parameter reaches each of its destinations, each holding back its own.
1236
+ const seen = new Set<string>();
1084
1237
  for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
1085
1238
  const { parameter, to } = next;
1086
- if (seen.has(parameter.id)) {
1239
+ const key = `${String(parameter.id)}\u0000${to.reach}\u0000${to.callee}`;
1240
+ if (seen.has(key)) {
1087
1241
  continue;
1088
1242
  }
1089
- seen.add(parameter.id);
1243
+ seen.add(key);
1090
1244
  const symbol = must(queries.symbolOfType(parameter));
1091
1245
  const [handle] = symbol?.declarations ?? [];
1092
1246
  const declaration = handle === undefined ? undefined : project.declarationAt(handle)?.node;
1093
1247
  const fn = declaration?.parent;
1094
- const index =
1095
- fn === undefined || !isFunctionLikeDeclaration(fn)
1096
- ? -1
1097
- : (fn.typeParameters ?? []).findIndex((one) => one === declaration);
1098
- const name =
1099
- fn === undefined
1100
- ? undefined
1101
- : isMethodDeclaration(fn) && isIdentifier(fn.name)
1102
- ? fn.name.text
1103
- : nestedName(fn);
1104
- if (symbol === undefined || fn === undefined || index < 0 || name === undefined) {
1248
+ if (symbol === undefined || fn === undefined || !isFunctionLikeDeclaration(fn)) {
1105
1249
  continue;
1106
1250
  }
1107
- for (const site of byName.get(name) ?? []) {
1108
- const resolved = must(queries.resolvedSignature(site.call));
1109
- const declared = resolved === undefined ? undefined : must(queries.genericOf(resolved));
1110
- const called =
1111
- declared?.declaration === undefined
1112
- ? undefined
1113
- : project.declarationAt(declared.declaration)?.node;
1114
- if (resolved === undefined || declared === undefined || called !== fn) {
1115
- continue;
1116
- }
1117
- const written = site.call.typeArguments?.[index];
1251
+ const index = (fn.typeParameters ?? []).findIndex((one) => one === declaration);
1252
+ if (index < 0) {
1253
+ continue;
1254
+ }
1255
+ calls ??= byDeclaration();
1256
+ for (const one of calls.get(fn) ?? []) {
1257
+ const written = one.written?.[index];
1118
1258
  const given =
1119
1259
  written === undefined
1120
- ? site.call.arguments.flatMap((_argument, at) => {
1121
- const from = must(queries.parameterType(declared, at));
1122
- const into = must(queries.parameterType(resolved, at));
1123
- return from === undefined || into === undefined
1124
- ? []
1125
- : bound(project, from, into, parameter, 0);
1126
- })
1260
+ ? one.places().flatMap(({ from, into }) => bound(project, from, into, parameter, 0))
1127
1261
  : typesAt(project, [written]).filter((type) => type !== undefined);
1128
1262
  for (const type of given) {
1129
1263
  const reached = reach.reachedFrom(type);
1130
1264
  if (reached.classes.size > 0 || reached.members.length > 0) {
1131
1265
  found.push({
1132
- site: site.call,
1266
+ site: one.call,
1133
1267
  classes: reached.classes,
1134
1268
  members: reached.members,
1135
1269
  to,
@@ -1150,14 +1284,13 @@ function typeParameterFlows<Brand>(
1150
1284
  * the callee named.
1151
1285
  */
1152
1286
  export function serializationContract<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
1153
- const { project, held, targetRoot } = input;
1154
- const walked = walkProject(project, held);
1287
+ const { project, held, sites, targetRoot } = input;
1155
1288
  const chains = aliasChains(project, held);
1156
1289
  const flows = flowsOf(
1157
1290
  project,
1158
1291
  held,
1159
- walked.sites,
1160
- destinations(input, chains, walked),
1292
+ sites.calls,
1293
+ destinations(input, chains),
1161
1294
  erasedStores(project),
1162
1295
  );
1163
1296
  if (flows.length === 0) {
@@ -1175,10 +1308,11 @@ export function serializationContract<Brand>(input: DetectorInput<Brand>): reado
1175
1308
  const holdsBack = (flags: SymbolFlags, name: string, to: Destination): boolean =>
1176
1309
  (flags & SymbolFlags.Property) !== 0 ||
1177
1310
  ((flags & CONVERSION_MEMBERS) !== 0 &&
1178
- (name === SERIALIZED_CONVERSION || (to.reach === "conversions" && CONVERSIONS.has(name))));
1311
+ (name === SERIALIZED_CONVERSION ||
1312
+ (to.reach === "conversions" && CONVERSION_NAMES.has(name))));
1179
1313
  // The receiver reads only what the program wrote, so a data member holds while written.
1180
1314
  const evidence = (id: string, flags: SymbolFlags, detail: string, site: Position): Evidence =>
1181
- (flags & SymbolFlags.Property) !== 0 && !walked.initialized.has(id)
1315
+ (flags & SymbolFlags.Property) !== 0 && !sites.initialized.has(id)
1182
1316
  ? { id, detail, site, whileWritten: true }
1183
1317
  : { id, detail, site };
1184
1318
  const found: Evidence[] = [];
@@ -1489,7 +1623,7 @@ function readTypes<Brand>(
1489
1623
  const parse = parses[at];
1490
1624
  const parsed = parse !== undefined && standard.has(parse);
1491
1625
  const type = valueTypes[at];
1492
- const untyped = type !== undefined && (type.flags & (TypeFlags.Any | TypeFlags.Unknown)) !== 0;
1626
+ const untyped = type !== undefined && (type.flags & ERASED) !== 0;
1493
1627
  return parsed || untyped ? [{ ...readAs(value), parsed }] : [];
1494
1628
  });
1495
1629
  // Each type node and each outermost expression, with whether a `JSON.parse` call is read as it.