@cplieger/deadset-ts 3.3.1 → 3.5.0

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.
@@ -0,0 +1,610 @@
1
+ /**
2
+ * The serialization-contract class: the properties of a class whose values reach
3
+ * `JSON.stringify` or a call `ts.serializers` names, and those and `toJSON` and
4
+ * `toString` where a value is passed to an `unknown` or `any` parameter of a function
5
+ * outside the analysis. A function of the project handing such a parameter of its own
6
+ * to a destination is one, holding back what that destination does, to a fixpoint. A
7
+ * function invoked through `Function.prototype.call`, `apply` or `bind` is the callee,
8
+ * given the arguments those methods pass it.
9
+ */
10
+
11
+ import {
12
+ isArrayLiteralExpression,
13
+ isArrowFunction,
14
+ isCallExpression,
15
+ isClassDeclaration,
16
+ isClassExpression,
17
+ isClassStaticBlockDeclaration,
18
+ isFunctionDeclaration,
19
+ isFunctionExpression,
20
+ isGetAccessorDeclaration,
21
+ isIdentifier,
22
+ isMethodDeclaration,
23
+ isNoSubstitutionTemplateLiteral,
24
+ isNumericLiteral,
25
+ isObjectLiteralExpression,
26
+ isParenthesizedExpression,
27
+ isPropertyAccessExpression,
28
+ isSetAccessorDeclaration,
29
+ isSpreadElement,
30
+ isStringLiteral,
31
+ isVariableDeclaration,
32
+ ModifierFlags,
33
+ SyntaxKind,
34
+ type CallExpression,
35
+ type Expression,
36
+ type Node,
37
+ type ParameterDeclaration,
38
+ } from "@typescript/native/unstable/ast";
39
+ import {
40
+ SignatureKind,
41
+ SymbolFlags,
42
+ TypeFlags,
43
+ type Symbol as TSSymbol,
44
+ type Type,
45
+ } from "@typescript/native/unstable/sync";
46
+ import { aliasChains, type AliasChains } from "./alias-chain.ts";
47
+ import { calleeName, resolvedTargets, valuesPassed } from "./calls.ts";
48
+ import { classMembers, memberComponent, typesAt } from "./class-members.ts";
49
+ import type { DeclarationEntry } from "./config.ts";
50
+ import {
51
+ names,
52
+ namesADeclaration,
53
+ resolveEntries,
54
+ spelledEntry,
55
+ } from "./configured-declarations.ts";
56
+ import type { DetectorInput, Evidence } from "./exempt.ts";
57
+ import { nodeKey, type Inventory } from "./inventory.ts";
58
+ import { renderPosition } from "./position.ts";
59
+ import type { ProjectView } from "./session.ts";
60
+ import { valueReach } from "./value-reach.ts";
61
+
62
+ /** The serializer every run reads, whatever the configuration names. */
63
+ const JSON_STRINGIFY: DeclarationEntry = { shape: "global", global: "JSON.stringify" };
64
+
65
+ /** The methods a serializer and a formatter resolve by name on a value that left the analysis. */
66
+ const CONVERSIONS: ReadonlySet<string> = new Set(["toJSON", "toString"]);
67
+
68
+ /** The parameter types that keep nothing of the value they are given. */
69
+ const ERASED = TypeFlags.Any | TypeFlags.Unknown;
70
+
71
+ /**
72
+ * What a destination holds back of a class: its properties, or its properties and its
73
+ * two conversion methods. The second holds back everything the first does.
74
+ */
75
+ type Reach = "properties" | "conversions";
76
+
77
+ /** One call of the project's own files, and the class `this` is a value of where it is written. */
78
+ interface Site {
79
+ readonly call: CallExpression;
80
+ readonly self: string | undefined;
81
+ }
82
+
83
+ /** One parameter of a function of the project that may hand on what it is given. */
84
+ interface Parameter {
85
+ /** The inventory's identifier of the function's declaration. */
86
+ readonly fn: string;
87
+ readonly index: number;
88
+ readonly rest: boolean;
89
+ readonly name: Node;
90
+ }
91
+
92
+ /** One place a function hands a parameter of its own to the argument of a call. */
93
+ interface Forwarded {
94
+ readonly parameter: Parameter;
95
+ readonly site: Site;
96
+ readonly argument: number;
97
+ }
98
+
99
+ /** A destination at one argument: what it holds back, and the callee a record names. */
100
+ interface Destination {
101
+ readonly reach: Reach;
102
+ readonly callee: string;
103
+ }
104
+
105
+ /** The wider of two reaches, where either may be absent. */
106
+ function wider(a: Reach | undefined, b: Reach | undefined): Reach | undefined {
107
+ return a === "conversions" || b === "conversions" ? "conversions" : (a ?? b);
108
+ }
109
+
110
+ /** Whether one node declares a static member, whose `this` is the class itself. */
111
+ function isStatic(node: Node): boolean {
112
+ const flags = (node as { readonly modifierFlags?: number }).modifierFlags ?? 0;
113
+ return (flags & ModifierFlags.Static) !== 0;
114
+ }
115
+
116
+ /** Whether one node opens a body whose `this` is not an instance of the enclosing class. */
117
+ function resetsThis(node: Node): boolean {
118
+ return (
119
+ isFunctionDeclaration(node) ||
120
+ isFunctionExpression(node) ||
121
+ isClassStaticBlockDeclaration(node) ||
122
+ isStatic(node) ||
123
+ ((isMethodDeclaration(node) ||
124
+ isGetAccessorDeclaration(node) ||
125
+ isSetAccessorDeclaration(node)) &&
126
+ isObjectLiteralExpression(node.parent))
127
+ );
128
+ }
129
+
130
+ /** The parameters and declaring node of one function-like declaration of the project. */
131
+ function functionOf(
132
+ node: Node,
133
+ ): { readonly declared: Node; readonly parameters: readonly ParameterDeclaration[] } | undefined {
134
+ if (
135
+ isFunctionDeclaration(node) ||
136
+ (isMethodDeclaration(node) && !isObjectLiteralExpression(node.parent))
137
+ ) {
138
+ return { declared: node, parameters: node.parameters };
139
+ }
140
+ if (
141
+ isVariableDeclaration(node) &&
142
+ node.initializer !== undefined &&
143
+ (isArrowFunction(node.initializer) || isFunctionExpression(node.initializer))
144
+ ) {
145
+ return { declared: node, parameters: node.initializer.parameters };
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ /** Whether one parameter is a `this` parameter, which types `this` and binds no argument. */
151
+ function isThisParameter(parameter: ParameterDeclaration): boolean {
152
+ return isIdentifier(parameter.name) && parameter.name.text === "this";
153
+ }
154
+
155
+ /** Whether an argument is written in a form that holds no class instance, which saves a type. */
156
+ function holdsNoInstance(argument: Expression): boolean {
157
+ return (
158
+ isArrowFunction(argument) ||
159
+ isFunctionExpression(argument) ||
160
+ isStringLiteral(argument) ||
161
+ isNumericLiteral(argument) ||
162
+ isNoSubstitutionTemplateLiteral(argument) ||
163
+ argument.kind === SyntaxKind.TemplateExpression
164
+ );
165
+ }
166
+
167
+ /** The expression an argument evaluates, through parentheses and a spread. */
168
+ function evaluated(argument: Expression): Expression {
169
+ const value = isSpreadElement(argument) ? argument.expression : argument;
170
+ return isParenthesizedExpression(value) ? evaluated(value.expression) : value;
171
+ }
172
+
173
+ /** The methods of `Function.prototype` that invoke a function with arguments of their own. */
174
+ const FUNCTION_METHODS: ReadonlySet<string> = new Set(["call", "apply", "bind"]);
175
+
176
+ /** The interfaces of the standard library that declare those methods for a function value. */
177
+ const FUNCTION_INTERFACES: ReadonlySet<string> = new Set([
178
+ "Function",
179
+ "CallableFunction",
180
+ "NewableFunction",
181
+ ]);
182
+
183
+ /**
184
+ * One call as the function it invokes sees it: the expression naming that function,
185
+ * and the argument each of its parameters binds, in order.
186
+ */
187
+ interface Invocation {
188
+ readonly callee: Expression;
189
+ readonly arguments: readonly Expression[];
190
+ /** Whether the call reaches the callee through a method of `Function.prototype`. */
191
+ readonly indirect: boolean;
192
+ }
193
+
194
+ /** A call through a method of `Function.prototype`: the method's name and the invocation. */
195
+ interface MethodForm {
196
+ readonly method: Node;
197
+ readonly invocation: Invocation;
198
+ }
199
+
200
+ function unparenthesized(expression: Expression): Expression {
201
+ return isParenthesizedExpression(expression)
202
+ ? unparenthesized(expression.expression)
203
+ : expression;
204
+ }
205
+
206
+ /**
207
+ * The invocation a call writes through a method of `Function.prototype`, read from
208
+ * the syntax alone: `f.call(self, a)`, `f.apply(self, [a])`, `f.bind(self, a)`, which
209
+ * binds `a`, and `f.bind(self, a)(b)`, which passes `b` after it. An `apply` argument
210
+ * that is no array literal is the value of the first parameter, whose elements it
211
+ * carries. Whether the method is the prototype's is resolution's to say.
212
+ */
213
+ function throughMethod(call: CallExpression): MethodForm | undefined {
214
+ const expression = unparenthesized(call.expression);
215
+ if (isPropertyAccessExpression(expression) && FUNCTION_METHODS.has(expression.name.text)) {
216
+ const [, ...rest] = call.arguments;
217
+ let passed: readonly Expression[] = rest;
218
+ if (expression.name.text === "apply") {
219
+ const list = rest[0] === undefined ? undefined : unparenthesized(rest[0]);
220
+ passed = list === undefined ? [] : isArrayLiteralExpression(list) ? list.elements : [list];
221
+ }
222
+ return {
223
+ method: expression.name,
224
+ invocation: { callee: expression.expression, arguments: passed, indirect: true },
225
+ };
226
+ }
227
+ if (isCallExpression(expression)) {
228
+ const bound = unparenthesized(expression.expression);
229
+ if (isPropertyAccessExpression(bound) && bound.name.text === "bind") {
230
+ return {
231
+ method: bound.name,
232
+ invocation: {
233
+ callee: bound.expression,
234
+ arguments: [...expression.arguments.slice(1), ...call.arguments],
235
+ indirect: true,
236
+ },
237
+ };
238
+ }
239
+ }
240
+ return undefined;
241
+ }
242
+
243
+ /** What one walk of the project's own files found: its calls and its functions' parameters. */
244
+ interface Walked {
245
+ readonly sites: readonly Site[];
246
+ readonly parameters: readonly Parameter[];
247
+ }
248
+
249
+ /**
250
+ * Every call of the project's own files with the class `this` stands for there, and
251
+ * every identifier-named parameter of every function, method and function-valued
252
+ * variable the project declares, at the position of the argument it binds.
253
+ */
254
+ function walkProject<Brand>(project: ProjectView<Brand>, held: Inventory): Walked {
255
+ const sites: Site[] = [];
256
+ const parameters: Parameter[] = [];
257
+ for (const file of project.ownSourceFiles()) {
258
+ const visit = (node: Node, self: string | undefined): void => {
259
+ let inner = self;
260
+ if (isClassDeclaration(node) || isClassExpression(node)) {
261
+ inner = held.declarations.get(nodeKey(file, node));
262
+ } else if (resetsThis(node)) {
263
+ inner = undefined;
264
+ }
265
+ if (node.kind === SyntaxKind.CallExpression) {
266
+ sites.push({ call: node as CallExpression, self });
267
+ }
268
+ const fn = functionOf(node);
269
+ const id = fn === undefined ? undefined : held.declarations.get(nodeKey(file, fn.declared));
270
+ if (fn !== undefined && id !== undefined) {
271
+ fn.parameters
272
+ .filter((parameter) => !isThisParameter(parameter))
273
+ .forEach((parameter, index) => {
274
+ if (isIdentifier(parameter.name)) {
275
+ parameters.push({
276
+ fn: id,
277
+ index,
278
+ rest: parameter.dotDotDotToken !== undefined,
279
+ name: parameter.name,
280
+ });
281
+ }
282
+ });
283
+ }
284
+ node.forEachChild((child) => {
285
+ visit(child, inner);
286
+ });
287
+ };
288
+ file.forEachChild((child) => {
289
+ visit(child, undefined);
290
+ });
291
+ }
292
+ return { sites, parameters };
293
+ }
294
+
295
+ /**
296
+ * The parameters that keep nothing of the value they are given: one typed `unknown` or
297
+ * `any`, and a rest parameter whose elements are, each by its symbol.
298
+ */
299
+ function erasedParameters<Brand>(
300
+ project: ProjectView<Brand>,
301
+ parameters: readonly Parameter[],
302
+ ): ReadonlyMap<number, Parameter> {
303
+ const types = typesAt(
304
+ project,
305
+ parameters.map((parameter) => parameter.name),
306
+ );
307
+ const erased = parameters.filter((parameter, index) => {
308
+ const type = types[index];
309
+ if (type === undefined) {
310
+ return false;
311
+ }
312
+ if (!parameter.rest) {
313
+ return (type.flags & ERASED) !== 0;
314
+ }
315
+ const [element] = type.isTypeReference() ? project.checker.getTypeArguments(type) : [];
316
+ return element !== undefined && (element.flags & ERASED) !== 0;
317
+ });
318
+ const bySymbol = new Map<number, Parameter>();
319
+ project
320
+ .symbolsAt(erased.map((parameter) => project.handle(parameter.name)))
321
+ .forEach((symbol, index) => {
322
+ const parameter = erased[index];
323
+ if (symbol !== undefined && parameter !== undefined) {
324
+ bySymbol.set(symbol.id, parameter);
325
+ }
326
+ });
327
+ return bySymbol;
328
+ }
329
+
330
+ /** Each place a call's argument passes one of the erased parameters it can see. */
331
+ function handedOn<Brand>(
332
+ project: ProjectView<Brand>,
333
+ sites: readonly Site[],
334
+ erased: ReadonlyMap<number, Parameter>,
335
+ invocationOf: (call: CallExpression) => Invocation,
336
+ ): readonly Forwarded[] {
337
+ const spellings = new Set([...erased.values()].map((parameter) => parameter.name.getText()));
338
+ const candidates = sites.flatMap((site) =>
339
+ invocationOf(site.call).arguments.flatMap((argument, index) =>
340
+ valuesPassed(argument)
341
+ .filter((value) => spellings.has(value.name.getText()))
342
+ .map((value) => ({ site, argument: index, value })),
343
+ ),
344
+ );
345
+ const plain = candidates.filter((one) => !one.value.shorthand);
346
+ const resolved = project.symbolsAt(plain.map((one) => project.handle(one.value.name)));
347
+ const symbolOf = new Map(plain.map((one, index) => [one, resolved[index]]));
348
+ return candidates.flatMap((one) => {
349
+ const symbol = one.value.shorthand
350
+ ? project.shorthandValueAt(project.handle(one.value.name))
351
+ : symbolOf.get(one);
352
+ const parameter = symbol === undefined ? undefined : erased.get(symbol.id);
353
+ return parameter === undefined ? [] : [{ parameter, site: one.site, argument: one.argument }];
354
+ });
355
+ }
356
+
357
+ /** The destination of each call's arguments, with the project's forwarding functions settled. */
358
+ interface Destinations {
359
+ /** The function one call invokes and the arguments its parameters bind. */
360
+ invocation(call: CallExpression): Invocation;
361
+ /** The destination one argument of one call is, if it is one. */
362
+ at(call: CallExpression, argument: number): Destination | undefined;
363
+ /** Whether a call can be a destination at any argument, read without a round trip. */
364
+ mayReceive(call: CallExpression): boolean;
365
+ }
366
+
367
+ /**
368
+ * The destinations of one project's calls. A call is one at every argument where its
369
+ * callee is a serializer an entry names; at the arguments its resolved signature types
370
+ * `unknown` or `any` where its callee is declared outside the analysis; and, where its
371
+ * callee is a function of the project, at each parameter the function hands on to a
372
+ * destination, holding back the widest of what those destinations hold back, which is
373
+ * settled by widening until no parameter moves.
374
+ */
375
+ function destinations<Brand>(
376
+ input: DetectorInput<Brand>,
377
+ chains: AliasChains,
378
+ walked: Walked,
379
+ ): Destinations {
380
+ const { project, held } = input;
381
+ const checker = project.checker;
382
+ const own = new Set(project.ownSourceFiles().map((file) => file.fileName));
383
+ const named = resolveEntries(project, held, [JSON_STRINGIFY, ...input.ts.serializers]).filter(
384
+ namesADeclaration,
385
+ );
386
+ const forms = new Map<CallExpression, MethodForm>();
387
+ for (const site of walked.sites) {
388
+ const form = throughMethod(site.call);
389
+ if (form !== undefined) {
390
+ forms.set(site.call, form);
391
+ }
392
+ }
393
+ const callees = resolvedTargets(project, [
394
+ ...walked.sites.flatMap((site) => calleeName(site.call.expression) ?? []),
395
+ ...[...forms.values()].flatMap((form) => {
396
+ const name = calleeName(form.invocation.callee);
397
+ return name === undefined ? [form.method] : [form.method, name];
398
+ }),
399
+ ]);
400
+ const erased = erasedParameters(project, walked.parameters);
401
+ const byFunction = new Map<string, Parameter[]>();
402
+ for (const parameter of erased.values()) {
403
+ byFunction.set(parameter.fn, [...(byFunction.get(parameter.fn) ?? []), parameter]);
404
+ }
405
+ const outside = (target: TSSymbol): boolean =>
406
+ target.declarations.length > 0 &&
407
+ target.declarations.every(
408
+ (handle) =>
409
+ !own.has(handle.path) &&
410
+ !input.consumers.some((root) => handle.path === root || handle.path.startsWith(`${root}/`)),
411
+ );
412
+ const invocation = (call: CallExpression): Invocation => {
413
+ const form = forms.get(call);
414
+ const method = form === undefined ? undefined : callees.get(form.method);
415
+ // Only the standard library's function methods pass their arguments on: the
416
+ // program's own `call` and `Reflect.apply` invoke nothing here.
417
+ if (
418
+ form !== undefined &&
419
+ method !== undefined &&
420
+ outside(method) &&
421
+ FUNCTION_INTERFACES.has(method.getParent()?.name ?? "")
422
+ ) {
423
+ return form.invocation;
424
+ }
425
+ return { callee: call.expression, arguments: call.arguments, indirect: false };
426
+ };
427
+ const calleeOf = (call: CallExpression): TSSymbol | undefined => {
428
+ const name = calleeName(invocation(call).callee);
429
+ return name === undefined ? undefined : callees.get(name);
430
+ };
431
+ const erasedArguments = new Map<CallExpression, ReadonlySet<number>>();
432
+ const erasedAt = (call: CallExpression, argument: number): boolean => {
433
+ let indexes = erasedArguments.get(call);
434
+ if (indexes === undefined) {
435
+ const invoked = invocation(call);
436
+ // Through a method of the prototype the resolved signature is the method's, so
437
+ // the parameters are read off the callee's own signatures, any one erasing.
438
+ const type = invoked.indirect ? checker.getTypeAtLocation(invoked.callee) : undefined;
439
+ const signatures = invoked.indirect
440
+ ? type === undefined
441
+ ? []
442
+ : checker.getSignaturesOfType(type, SignatureKind.Call)
443
+ : [checker.getResolvedSignature(call)].filter((one) => one !== undefined);
444
+ indexes = new Set(
445
+ invoked.arguments.flatMap((_unused, index) =>
446
+ signatures.some((signature) => {
447
+ const parameter = checker.getParameterType(signature, index);
448
+ return parameter !== undefined && (parameter.flags & ERASED) !== 0;
449
+ })
450
+ ? [index]
451
+ : [],
452
+ ),
453
+ );
454
+ erasedArguments.set(call, indexes);
455
+ }
456
+ return indexes.has(argument);
457
+ };
458
+
459
+ const forwards = new Map<string, Reach>();
460
+ const keyOf = (parameter: Parameter): string => `${parameter.fn}\u0000${String(parameter.index)}`;
461
+ const at = (call: CallExpression, argument: number): Destination | undefined => {
462
+ const target = calleeOf(call);
463
+ if (target === undefined) {
464
+ return undefined;
465
+ }
466
+ const entry = named.find((one) => names(one, target, chains));
467
+ if (entry !== undefined) {
468
+ return { reach: "properties", callee: spelledEntry(entry.entry, held) };
469
+ }
470
+ const declared = chains.declarationsOf(target);
471
+ if (declared.length === 0) {
472
+ return outside(target) && erasedAt(call, argument)
473
+ ? {
474
+ reach: "conversions",
475
+ callee: invocation(call).callee.getText().replace(/\s+/gu, ""),
476
+ }
477
+ : undefined;
478
+ }
479
+ for (const fn of declared) {
480
+ const parameter = (byFunction.get(fn) ?? []).find(
481
+ (one) => one.index === argument || (one.rest && one.index <= argument),
482
+ );
483
+ const reach = parameter === undefined ? undefined : forwards.get(keyOf(parameter));
484
+ if (reach !== undefined) {
485
+ return { reach, callee: held.symbols.find((symbol) => symbol.id === fn)?.name ?? fn };
486
+ }
487
+ }
488
+ return undefined;
489
+ };
490
+
491
+ const forwarded = handedOn(project, walked.sites, erased, invocation);
492
+ for (let moved = true; moved;) {
493
+ moved = false;
494
+ for (const one of forwarded) {
495
+ const key = keyOf(one.parameter);
496
+ const before = forwards.get(key);
497
+ const after = wider(before, at(one.site.call, one.argument)?.reach);
498
+ if (after !== undefined && after !== before) {
499
+ forwards.set(key, after);
500
+ moved = true;
501
+ }
502
+ }
503
+ }
504
+
505
+ const mayReceive = (call: CallExpression): boolean => {
506
+ const target = calleeOf(call);
507
+ if (target === undefined) {
508
+ return false;
509
+ }
510
+ const declared = chains.declarationsOf(target);
511
+ if (named.some((one) => names(one, target, chains))) {
512
+ return true;
513
+ }
514
+ if (declared.length === 0) {
515
+ return outside(target);
516
+ }
517
+ return declared.some((fn) =>
518
+ (byFunction.get(fn) ?? []).some((parameter) => forwards.has(keyOf(parameter))),
519
+ );
520
+ };
521
+ return { invocation, at, mayReceive };
522
+ }
523
+
524
+ /** One argument whose value carries classes of the target into a destination. */
525
+ interface Flow {
526
+ readonly site: Node;
527
+ readonly classes: ReadonlySet<string>;
528
+ readonly to: Destination;
529
+ }
530
+
531
+ /** Each argument of a call that can receive one whose value carries a class of the target. */
532
+ function flowsOf<Brand>(
533
+ project: ProjectView<Brand>,
534
+ held: Inventory,
535
+ sites: readonly Site[],
536
+ to: Destinations,
537
+ ): readonly Flow[] {
538
+ const reach = valueReach(project, held);
539
+ const arguments_ = sites.flatMap((site) =>
540
+ to.mayReceive(site.call)
541
+ ? to.invocation(site.call).arguments.flatMap((argument, index) => {
542
+ const value = evaluated(argument);
543
+ return holdsNoInstance(value) ? [] : [{ site, argument: index, value }];
544
+ })
545
+ : [],
546
+ );
547
+ const asked = arguments_.filter((one) => one.value.kind !== SyntaxKind.ThisKeyword);
548
+ const types = typesAt(
549
+ project,
550
+ asked.map((one) => one.value),
551
+ );
552
+ const typeOf = new Map<Expression, Type | undefined>(
553
+ asked.map((one, index) => [one.value, types[index]]),
554
+ );
555
+ return arguments_.flatMap((one) => {
556
+ const type = typeOf.get(one.value);
557
+ let classes: ReadonlySet<string> = new Set<string>();
558
+ if (one.value.kind === SyntaxKind.ThisKeyword) {
559
+ classes = new Set(one.site.self === undefined ? [] : [one.site.self]);
560
+ } else if (type !== undefined) {
561
+ classes = reach.classesOf(type);
562
+ }
563
+ const destination = classes.size === 0 ? undefined : to.at(one.site.call, one.argument);
564
+ return destination === undefined ? [] : [{ site: one.value, classes, to: destination }];
565
+ });
566
+ }
567
+
568
+ /**
569
+ * The serialization-contract detector: each property of each class whose values reach
570
+ * a destination, and the two conversion methods where the destination is outside the
571
+ * analysis, recorded at the argument with the callee named.
572
+ */
573
+ export function serializationContract<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
574
+ const { project, held, targetRoot } = input;
575
+ const walked = walkProject(project, held);
576
+ const flows = flowsOf(
577
+ project,
578
+ held,
579
+ walked.sites,
580
+ destinations(input, aliasChains(project, held), walked),
581
+ );
582
+ if (flows.length === 0) {
583
+ return [];
584
+ }
585
+ const members = classMembers(
586
+ project,
587
+ held,
588
+ new Set(flows.flatMap((flow) => [...flow.classes])),
589
+ "instance",
590
+ );
591
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
592
+ const found: Evidence[] = [];
593
+ for (const flow of flows) {
594
+ const site = renderPosition(flow.site.getSourceFile(), targetRoot, flow.site.getStart());
595
+ const detail = `passed to ${flow.to.callee}`;
596
+ for (const member of [...flow.classes].flatMap((id) => members.get(id) ?? [])) {
597
+ const symbol = byId.get(member.id);
598
+ const isProperty = (member.flags & SymbolFlags.Property) !== 0;
599
+ const converts =
600
+ flow.to.reach === "conversions" &&
601
+ (member.flags & SymbolFlags.Method) !== 0 &&
602
+ symbol !== undefined &&
603
+ CONVERSIONS.has(memberComponent(symbol, byId) ?? "");
604
+ if (isProperty || converts) {
605
+ found.push({ id: member.id, detail, site });
606
+ }
607
+ }
608
+ }
609
+ return found;
610
+ }