@jskit-ai/architecture-inspector 0.1.0 → 0.1.2

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,658 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { Stats } from "node:fs";
3
+ import { Duplex } from "node:stream";
4
+ import ts from "typescript";
5
+ import { isCalledInPlace, isFunctionLike, owningParameter, parameterNames, runsDuring, unwrapExpression } from "./functions.js";
6
+ import { isClassLike } from "./symbols.js";
7
+ import { isLiteralPattern } from "./evaluate.js";
8
+ import { NODE_GLOBALS, createExternalValues, isComponentFile } from "./external.js";
9
+
10
+ const MAX_TARGETS = 16;
11
+ const MAX_TYPE_DEPTH = 6;
12
+ // Vue's `<script setup>` compiler macros are globals of component scripts.
13
+ const VUE_MACROS = new Set(["defineProps", "defineEmits", "defineExpose", "defineOptions", "defineSlots", "defineModel", "withDefaults"]);
14
+ // Node objects untyped receivers most often hold; TypeScript's libraries do
15
+ // not declare their members, so the names are read from Node itself.
16
+ const NODE_MEMBER_SOURCES = Object.freeze([EventEmitter.prototype, Stats.prototype, Duplex.prototype]);
17
+ const VIA_ORDER = Object.freeze(["checker", "self", "di-binding", "argument-flow"]);
18
+
19
+ function weakerVia(left, right) {
20
+ return VIA_ORDER.indexOf(left) >= VIA_ORDER.indexOf(right) ? left : right;
21
+ }
22
+
23
+ function isAnyType(type) {
24
+ return Boolean(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown));
25
+ }
26
+
27
+ // JSKIT's package layout keeps client and server code apart, so a name never
28
+ // matches a candidate on the other side.
29
+ function codeSide(file) {
30
+ const parts = file.split("/");
31
+ if (parts.includes("server")) {
32
+ return "server";
33
+ }
34
+ if (parts.includes("client") || parts[0] === "src" || file.endsWith(".vue")) {
35
+ return "client";
36
+ }
37
+ return "shared";
38
+ }
39
+
40
+ function compatibleSides(left, right) {
41
+ return left === "shared" || right === "shared" || left === right;
42
+ }
43
+
44
+ // Member names of TypeScript's libraries and of common Node objects.
45
+ function libraryMemberNames(program) {
46
+ const names = new Set();
47
+ for (const source of NODE_MEMBER_SOURCES) {
48
+ for (let prototype = source; prototype && prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)) {
49
+ Object.getOwnPropertyNames(prototype).forEach((name) => names.add(name));
50
+ }
51
+ }
52
+ for (const sourceFile of program.getSourceFiles()) {
53
+ if (!program.isSourceFileDefaultLibrary(sourceFile)) {
54
+ continue;
55
+ }
56
+ const visit = (node) => {
57
+ if ((ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isMethodDeclaration(node) ||
58
+ ts.isPropertyDeclaration(node) || ts.isGetAccessorDeclaration(node)) && node.name && ts.isIdentifier(node.name)) {
59
+ names.add(node.name.text);
60
+ }
61
+ ts.forEachChild(node, visit);
62
+ };
63
+ visit(sourceFile);
64
+ }
65
+ return names;
66
+ }
67
+
68
+ function constructorOf(implementation) {
69
+ if (!isClassLike(implementation)) {
70
+ return implementation;
71
+ }
72
+ return implementation.members.find((member) => ts.isConstructorDeclaration(member) && member.body) || null;
73
+ }
74
+
75
+ /**
76
+ * Call resolution over one program: TypeScript checker targets, defineFeature
77
+ * injection, argument flow, literal-array unrolling and receiver names, each
78
+ * labelled with its certainty. Provably external calls are dropped.
79
+ * @param {object} dependencies
80
+ * @param {import("typescript").Program} dependencies.program
81
+ * @param {import("typescript").TypeChecker} dependencies.checker
82
+ * @param {import("./types.js").CallSymbols} dependencies.symbols
83
+ * @param {import("./types.js").CallEvaluator} dependencies.evaluator
84
+ * @param {import("./types.js").FeatureBindings["bindings"]} dependencies.bindings
85
+ * @param {Map<string, import("./types.js").FeatureBindings["actions"]>} dependencies.actionsById
86
+ * @param {import("./types.js").FileOf} dependencies.fileOf
87
+ * @param {Map<string, import("typescript").Node[]>} dependencies.functionNames
88
+ */
89
+ function createResolver({ program, checker, symbols, evaluator, bindings, actionsById, fileOf, functionNames }) {
90
+ const builtinMembers = libraryMemberNames(program);
91
+ const { isExternalValue, isExternalDeclaration } = createExternalValues({ checker, symbols, fileOf });
92
+ /** @type {Map<import("typescript").Node, import("typescript").CallExpression[]>} */
93
+ const callSites = new Map();
94
+ const direct = new Map();
95
+ // The functions each indexed call reaches, and the calls whose index entry
96
+ // came from a guess (several values or an ambiguous injection).
97
+ /** @type {Map<import("typescript").CallExpression, import("typescript").Node[]>} */
98
+ const indexedTargets = new Map();
99
+ /** @type {Set<import("typescript").CallExpression>} */
100
+ const probableSites = new Set();
101
+
102
+ function implementationsFor(symbol) {
103
+ const found = symbols.symbolImplementations(symbol).map(constructorOf).filter(Boolean);
104
+ return found.length ? found : declaredMemberImplementations(symbol);
105
+ }
106
+
107
+ // A member a type declares without a value (`{ run: typeof run }` or
108
+ // `@property {typeof run} run`) holds the function or class its type names.
109
+ function declaredMemberImplementations(symbol) {
110
+ const declarations = symbol?.declarations || [];
111
+ if (!declarations.length || !declarations.every((declaration) => ts.isPropertySignature(declaration) || ts.isJSDocPropertyTag(declaration))) {
112
+ return [];
113
+ }
114
+ const type = checker.getTypeOfSymbol(symbol);
115
+ if (!type.getCallSignatures().length && !type.getConstructSignatures().length) {
116
+ return [];
117
+ }
118
+ return symbols.symbolImplementations(type.getSymbol()).map(constructorOf).filter(Boolean);
119
+ }
120
+
121
+ // Pass 1: what the checker resolves on its own, and the call-site index
122
+ // argument flow reads.
123
+ function directResolution(call) {
124
+ const callee = unwrapExpression(call.expression);
125
+ if (ts.isIdentifier(callee)) {
126
+ const symbol = symbols.symbolAt(callee);
127
+ if (!symbol) {
128
+ return { kind: "undeclared", name: callee.text };
129
+ }
130
+ if (symbols.isPackageImport(callee) || symbols.isLibrarySymbol(symbol)) {
131
+ return { kind: "external" };
132
+ }
133
+ const targets = implementationsFor(symbol);
134
+ return targets.length
135
+ ? { kind: "targets", targets, via: "checker" }
136
+ : { kind: "value", declaration: symbols.declarationOf(callee) };
137
+ }
138
+ if (ts.isPropertyAccessExpression(callee)) {
139
+ const symbol = checker.getSymbolAtLocation(callee.name);
140
+ if (symbol && symbols.isLibrarySymbol(symbol)) {
141
+ return { kind: "external" };
142
+ }
143
+ const targets = symbol ? implementationsFor(symbol) : [];
144
+ return targets.length ? { kind: "targets", targets, via: "checker" } : { kind: "member", callee };
145
+ }
146
+ if (ts.isElementAccessExpression(callee)) {
147
+ return { kind: "computed", callee };
148
+ }
149
+ if (callee.kind === ts.SyntaxKind.ImportKeyword) {
150
+ return { kind: "external" };
151
+ }
152
+ const declaration = checker.getResolvedSignature(call)?.declaration;
153
+ if (declaration && isFunctionLike(declaration) && fileOf(declaration)) {
154
+ return { kind: "targets", targets: [declaration], via: "checker" };
155
+ }
156
+ if (declaration && !fileOf(declaration)) {
157
+ return { kind: "external" };
158
+ }
159
+ return { kind: "other" };
160
+ }
161
+
162
+ // Records that a call reaches these functions, for argument flow. A
163
+ // probable site's values are never exact.
164
+ function indexSite(call, targets, probable = false) {
165
+ const known = indexedTargets.get(call) || [];
166
+ for (const target of targets) {
167
+ if (known.includes(target)) {
168
+ continue;
169
+ }
170
+ known.push(target);
171
+ if (!callSites.has(target)) {
172
+ callSites.set(target, []);
173
+ }
174
+ callSites.get(target).push(call);
175
+ }
176
+ if (known.length) {
177
+ indexedTargets.set(call, known);
178
+ }
179
+ if (probable) {
180
+ probableSites.add(call);
181
+ }
182
+ }
183
+
184
+ // The checker's own targets of each call; returns them per call.
185
+ function indexCalls(calls) {
186
+ return calls.map((call) => {
187
+ const resolution = directResolution(call);
188
+ direct.set(call, resolution);
189
+ indexSite(call, resolution.targets || []);
190
+ return resolution.targets || [];
191
+ });
192
+ }
193
+
194
+ // The value one call gives a parameter: the argument, or the property of an
195
+ // object literal argument a destructured parameter takes, with the rest of
196
+ // its property path.
197
+ function argumentValue(call, owner) {
198
+ let value = call.arguments?.[owner.index];
199
+ if (!value || ts.isSpreadElement(value)) {
200
+ return null;
201
+ }
202
+ const remaining = [...owner.path];
203
+ while (remaining.length && ts.isObjectLiteralExpression(unwrapExpression(value))) {
204
+ value = evaluator.propertyInitializer(unwrapExpression(value), remaining[0]);
205
+ remaining.shift();
206
+ if (!value) {
207
+ return null;
208
+ }
209
+ }
210
+ return { expression: value, path: remaining };
211
+ }
212
+
213
+ function argumentValues(declaration) {
214
+ const owner = owningParameter(declaration);
215
+ if (!owner) {
216
+ return [];
217
+ }
218
+ return (callSites.get(owner.fn) || []).map((call) => {
219
+ const value = argumentValue(call, owner);
220
+ return value && { ...value, probable: probableSites.has(call) };
221
+ }).filter(Boolean);
222
+ }
223
+
224
+ function propertyValueExpressions(property) {
225
+ const expressions = [];
226
+ for (const declaration of property.declarations || []) {
227
+ if ((ts.isPropertyAssignment(declaration) || ts.isPropertyDeclaration(declaration)) && declaration.initializer) {
228
+ expressions.push(declaration.initializer);
229
+ } else if (ts.isShorthandPropertyAssignment(declaration)) {
230
+ expressions.push(declaration.name);
231
+ } else if ((ts.isPropertyAccessExpression(declaration) || ts.isBinaryExpression(declaration.parent)) &&
232
+ ts.isBinaryExpression(declaration.parent) && declaration.parent.left === declaration) {
233
+ expressions.push(declaration.parent.right);
234
+ }
235
+ }
236
+ return expressions;
237
+ }
238
+
239
+ function propertyTypes(entries, name, location, depth, seen) {
240
+ const result = [];
241
+ for (const entry of entries) {
242
+ for (const part of entry.type.isUnion() ? entry.type.types : [entry.type]) {
243
+ const property = part.getProperty(name);
244
+ if (!property) {
245
+ continue;
246
+ }
247
+ const type = checker.getTypeOfSymbolAtLocation(property, location);
248
+ if (!isAnyType(type)) {
249
+ result.push({ ...entry, type });
250
+ continue;
251
+ }
252
+ for (const expression of propertyValueExpressions(property)) {
253
+ for (const found of typesOf(expression, depth + 1, seen)) {
254
+ result.push({ type: found.type, exact: entry.exact && found.exact, via: weakerVia(entry.via, found.via) });
255
+ }
256
+ }
257
+ }
258
+ }
259
+ return result;
260
+ }
261
+
262
+ function valueTypes(value, depth, seen) {
263
+ let entries = typesOf(value.expression, depth + 1, seen);
264
+ for (const name of value.path) {
265
+ entries = propertyTypes(entries, name, value.expression, depth, seen);
266
+ }
267
+ return entries;
268
+ }
269
+
270
+ // Types an untyped expression takes through injection and argument flow.
271
+ function typesOf(expression, depth = 0, seen = new Set()) {
272
+ const value = unwrapExpression(expression);
273
+ if (!value || depth > MAX_TYPE_DEPTH || seen.has(value)) {
274
+ return [];
275
+ }
276
+ seen.add(value);
277
+ const direct = checker.getTypeAtLocation(value);
278
+ if (!isAnyType(direct)) {
279
+ return [{ type: direct, exact: true, via: "checker" }];
280
+ }
281
+ if (ts.isIdentifier(value)) {
282
+ const declaration = symbols.declarationOf(value);
283
+ if (!declaration || !fileOf(declaration)) {
284
+ return [];
285
+ }
286
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
287
+ return typesOf(declaration.initializer, depth + 1, seen);
288
+ }
289
+ if (ts.isBindingElement(declaration) || ts.isParameter(declaration)) {
290
+ const binding = bindings.get(declaration);
291
+ if (binding) {
292
+ return binding.expressions.flatMap((bound) => typesOf(bound, depth + 1, seen).map((entry) => ({
293
+ type: entry.type,
294
+ exact: entry.exact && binding.exact,
295
+ via: weakerVia(entry.via, "di-binding")
296
+ })));
297
+ }
298
+ const values = argumentValues(declaration);
299
+ return values.flatMap((bound) => valueTypes(bound, depth, seen).map((entry) => ({
300
+ type: entry.type,
301
+ exact: entry.exact && values.length === 1,
302
+ via: "argument-flow"
303
+ })));
304
+ }
305
+ return [];
306
+ }
307
+ if (ts.isPropertyAccessExpression(value)) {
308
+ return propertyTypes(typesOf(value.expression, depth + 1, seen), value.name.text, value, depth, seen);
309
+ }
310
+ if (ts.isCallExpression(value)) {
311
+ const signatures = typesOf(value.expression, depth + 1, seen).flatMap((entry) =>
312
+ entry.type.getCallSignatures().map((signature) => ({ ...entry, type: signature.getReturnType() })));
313
+ return signatures.filter((entry) => !isAnyType(entry.type));
314
+ }
315
+ if (ts.isAwaitExpression(value)) {
316
+ return typesOf(value.expression, depth + 1, seen).map((entry) => ({ ...entry, type: checker.getAwaitedType(entry.type) || entry.type }));
317
+ }
318
+ return [];
319
+ }
320
+
321
+ function targetsFromTypes(entries, names) {
322
+ const targets = new Map();
323
+ let external = false;
324
+ for (const entry of entries) {
325
+ for (const part of entry.type.isUnion() ? entry.type.types : [entry.type]) {
326
+ for (const name of names) {
327
+ const property = part.getProperty(name);
328
+ if (!property) {
329
+ continue;
330
+ }
331
+ if (symbols.isLibrarySymbol(property)) {
332
+ external = true;
333
+ continue;
334
+ }
335
+ for (const target of implementationsFor(property)) {
336
+ const previous = targets.get(target);
337
+ targets.set(target, previous
338
+ ? { exact: previous.exact && entry.exact, via: weakerVia(previous.via, entry.via) }
339
+ : { exact: entry.exact, via: entry.via });
340
+ }
341
+ }
342
+ }
343
+ }
344
+ return { targets, external };
345
+ }
346
+
347
+ function fromTargets(targets, { exactAllowed = true, method }) {
348
+ const list = [...targets.keys()];
349
+ if (!list.length) {
350
+ return null;
351
+ }
352
+ if (list.length > MAX_TARGETS) {
353
+ return unresolved();
354
+ }
355
+ const facts = [...targets.values()];
356
+ const via = method || facts.map((entry) => entry.via).reduce(weakerVia);
357
+ if (list.length === 1) {
358
+ const exact = exactAllowed && facts[0].exact;
359
+ return { resolution: "resolved", certainty: exact ? "exact" : "probable", method: via, targets: list };
360
+ }
361
+ return { resolution: "ambiguous", certainty: "probable", method: via, targets: list };
362
+ }
363
+
364
+ function unresolved() {
365
+ return { resolution: "unresolved", certainty: "unresolved", method: "none", targets: [] };
366
+ }
367
+
368
+ // The functions a passed value can be, added to `targets`. With `passedOn`,
369
+ // the path of parameters followed so far, a parameter the value passes on
370
+ // holds what its own callers give it; a callback a helper runs (passedCall)
371
+ // is never followed to another caller's value.
372
+ function addValueTargets(value, targets, exact = false, passedOn = null) {
373
+ const found = value.path.length
374
+ ? [...targetsFromTypes(valueTypes({ expression: value.expression, path: value.path.slice(0, -1) }, 0, new Set()), [value.path.at(-1)]).targets.keys()]
375
+ : symbols.expressionImplementations(value.expression, 0).map(constructorOf).filter(Boolean);
376
+ const expression = unwrapExpression(value.expression);
377
+ const parameter = passedOn && !found.length && !value.path.length && ts.isIdentifier(expression) ? symbols.declarationOf(expression) : null;
378
+ if (parameter && (ts.isParameter(parameter) || ts.isBindingElement(parameter)) &&
379
+ !passedOn.includes(parameter) && passedOn.length < MAX_TYPE_DEPTH) {
380
+ const values = argumentValues(parameter);
381
+ for (const passed of values) {
382
+ addValueTargets(passed, targets, exact && values.length === 1 && !passed.probable, [...passedOn, parameter]);
383
+ }
384
+ return;
385
+ }
386
+ for (const target of found) {
387
+ targets.set(target, { exact: exact && found.length === 1 && !value.path.length, via: "argument-flow" });
388
+ }
389
+ }
390
+
391
+ // Values of an injected or passed-in function, called directly. A parameter
392
+ // called while its function runs is left to each caller (passedCall), so
393
+ // one caller's callback is never reached through another caller.
394
+ function boundFunctionTargets(declaration, call) {
395
+ const targets = new Map();
396
+ const binding = bindings.get(declaration);
397
+ if (binding) {
398
+ for (const expression of binding.expressions) {
399
+ for (const target of symbols.expressionImplementations(expression, 0).map(constructorOf).filter(Boolean)) {
400
+ targets.set(target, { exact: binding.exact, via: "di-binding" });
401
+ }
402
+ }
403
+ return targets;
404
+ }
405
+ const owner = owningParameter(declaration);
406
+ if (owner && ts.isCallExpression(call) && runsDuring(call, owner.fn)) {
407
+ return targets;
408
+ }
409
+ const values = argumentValues(declaration);
410
+ for (const value of values) {
411
+ addValueTargets(value, targets, values.length === 1 && !value.probable, []);
412
+ }
413
+ return targets;
414
+ }
415
+
416
+ function checkerTargets(call) {
417
+ if (!direct.has(call)) {
418
+ direct.set(call, directResolution(call));
419
+ }
420
+ return direct.get(call).targets || [];
421
+ }
422
+
423
+ // The functions a call reaches as the index knows them: the checker's, or
424
+ // those injection or argument flow resolved.
425
+ function siteTargets(call) {
426
+ return indexedTargets.get(call) || checkerTargets(call);
427
+ }
428
+
429
+ // The calls whose callee is a parameter or a destructured name: those
430
+ // injection or argument flow can resolve. What a call's callee is declared
431
+ // as depends only on its own file.
432
+ function boundCandidates(calls) {
433
+ return calls.filter((call) => {
434
+ checkerTargets(call);
435
+ const declaration = direct.get(call).declaration;
436
+ return declaration && (ts.isParameter(declaration) || ts.isBindingElement(declaration));
437
+ });
438
+ }
439
+
440
+ // Calls the checker cannot resolve but injection or argument flow can are
441
+ // call sites too, so argument flow and the callbacks a call passes reach
442
+ // through them. Each round can expose more; the targets and guesses only
443
+ // grow, so the rounds stop. Takes the candidates of every file, since one
444
+ // file's arguments decide another's targets, and returns each candidate's
445
+ // targets and whether they are a guess.
446
+ function indexBoundCalls(candidates) {
447
+ for (let changed = true; changed;) {
448
+ changed = false;
449
+ for (const call of candidates) {
450
+ const found = boundFunctionTargets(direct.get(call).declaration, call);
451
+ const targets = found.size > MAX_TARGETS ? [] : [...found.keys()];
452
+ const probable = targets.length !== 1 || !found.get(targets[0]).exact;
453
+ const known = indexedTargets.get(call) || [];
454
+ if (targets.some((target) => !known.includes(target)) || (probable && targets.length && !probableSites.has(call))) {
455
+ indexSite(call, targets, probable);
456
+ changed = true;
457
+ }
458
+ }
459
+ }
460
+ return candidates.map((call) => ({ targets: indexedTargets.get(call) || [], probable: probableSites.has(call) }));
461
+ }
462
+
463
+ // The parameters ({ index, path }) a function calls while it runs: in its
464
+ // own body, in callbacks it passes on, or by forwarding them to a function
465
+ // that does. A cycle adds nothing.
466
+ const invoked = new Map();
467
+ function invokedParameters(fn) {
468
+ if (invoked.has(fn)) {
469
+ return [...invoked.get(fn).values()];
470
+ }
471
+ const found = new Map();
472
+ invoked.set(fn, found);
473
+ const names = parameterNames(fn);
474
+ const own = (expression) => {
475
+ const value = unwrapExpression(expression);
476
+ if (!value || !ts.isIdentifier(value) || !names.has(value.text)) {
477
+ return null;
478
+ }
479
+ const owner = owningParameter(symbols.declarationOf(value));
480
+ return owner?.fn === fn ? owner : null;
481
+ };
482
+ const add = (index, path) => found.set(`${index}:${path.join(".")}`, { index, path });
483
+ const visit = (node) => {
484
+ if (isFunctionLike(node) && !isCalledInPlace(node)) {
485
+ return;
486
+ }
487
+ if (ts.isCallExpression(node)) {
488
+ const called = own(node.expression);
489
+ if (called) {
490
+ add(called.index, called.path);
491
+ }
492
+ node.arguments.forEach((argument, position) => {
493
+ const forwarded = own(argument);
494
+ for (const target of forwarded ? siteTargets(node) : []) {
495
+ for (const inner of invokedParameters(target)) {
496
+ if (inner.index === position) {
497
+ add(forwarded.index, [...forwarded.path, ...inner.path]);
498
+ }
499
+ }
500
+ }
501
+ });
502
+ }
503
+ ts.forEachChild(node, visit);
504
+ };
505
+ if (names.size) {
506
+ visit(fn.body);
507
+ }
508
+ return [...found.values()];
509
+ }
510
+
511
+ // What a call reaches through the functions it passes: each value it gives
512
+ // a parameter its callee calls while it runs. Null when there is none.
513
+ function passedCall(call) {
514
+ if (!call.arguments?.length) {
515
+ return null;
516
+ }
517
+ const targets = new Map();
518
+ for (const callee of siteTargets(call)) {
519
+ for (const parameter of invokedParameters(callee)) {
520
+ const value = argumentValue(call, parameter);
521
+ if (value) {
522
+ addValueTargets(value, targets, !probableSites.has(call));
523
+ }
524
+ }
525
+ }
526
+ const result = fromTargets(targets, {});
527
+ return result?.resolution === "unresolved" ? null : result;
528
+ }
529
+
530
+ function receiverHint(receiver, candidates) {
531
+ const text = receiver?.getText().split(".").pop()?.replace(/Service$/u, "").toLowerCase();
532
+ if (!text || !/^[a-z][\w]*$/u.test(text)) {
533
+ return [];
534
+ }
535
+ const singular = text.replace(/s$/u, "");
536
+ return candidates.filter((candidate) => fileOf(candidate).toLowerCase().split(/[/.-]/u)
537
+ .some((token) => token === text || token === singular));
538
+ }
539
+
540
+ // A member call on an untyped receiver reaches a project function only
541
+ // when the name is that function's alone: no builtin member has it, and
542
+ // exactly one function on this side declares it (or exactly one in a file
543
+ // the receiver names). A shared name is a guess, so it stays unresolved.
544
+ function byName(name, receiver, file) {
545
+ const side = codeSide(file);
546
+ const candidates = (functionNames.get(name) || []).filter((candidate) => compatibleSides(side, codeSide(fileOf(candidate))));
547
+ if (!candidates.length) {
548
+ return builtinMembers.has(name) ? null : unresolved();
549
+ }
550
+ const hinted = candidates.length > 1 ? receiverHint(receiver, candidates) : [];
551
+ const chosen = hinted.length === 1 ? hinted : candidates;
552
+ if (chosen.length !== 1 || builtinMembers.has(name)) {
553
+ return unresolved();
554
+ }
555
+ return { resolution: "resolved", certainty: "probable", method: "receiver-name", targets: chosen };
556
+ }
557
+
558
+ function enclosingClass(node) {
559
+ let current = node.parent;
560
+ while (current && !isClassLike(current)) {
561
+ current = current.parent;
562
+ }
563
+ return current;
564
+ }
565
+
566
+ function selfMethod(callee) {
567
+ if (callee.expression.kind !== ts.SyntaxKind.ThisKeyword) {
568
+ return null;
569
+ }
570
+ const owner = enclosingClass(callee);
571
+ const method = owner?.members.find((member) => ts.isMethodDeclaration(member) && member.body &&
572
+ member.name && ts.isIdentifier(member.name) && member.name.text === callee.name.text);
573
+ return method ? { resolution: "resolved", certainty: "exact", method: "self", targets: [method] } : null;
574
+ }
575
+
576
+ // `request.executeAction({ actionId: "literal" })` runs that action.
577
+ function actionCall(call, callee) {
578
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "executeAction") {
579
+ return null;
580
+ }
581
+ const options = unwrapExpression(call.arguments[0]);
582
+ const idExpression = options && ts.isObjectLiteralExpression(options) ? evaluator.propertyInitializer(options, "actionId") : null;
583
+ const actionId = idExpression && evaluator.evaluateLiteral(idExpression);
584
+ const definitions = actionId ? actionsById.get(actionId) || [] : [];
585
+ const executes = definitions.flatMap((definition) => definition.execute);
586
+ if (definitions.length !== 1 || executes.length !== 1) {
587
+ return null;
588
+ }
589
+ return { resolution: "resolved", certainty: "exact", method: "route-action", targets: executes };
590
+ }
591
+
592
+ function memberResolution(call, callee, file) {
593
+ const action = actionCall(call, callee);
594
+ if (action) {
595
+ return action;
596
+ }
597
+ if (isExternalValue(callee.expression)) {
598
+ return null;
599
+ }
600
+ const found = targetsFromTypes(typesOf(callee.expression), [callee.name.text]);
601
+ if (found.targets.size) {
602
+ return fromTargets(found.targets, {});
603
+ }
604
+ if (found.external) {
605
+ return null;
606
+ }
607
+ return selfMethod(callee) || byName(callee.name.text, callee.expression, file);
608
+ }
609
+
610
+ function computedResolution(callee) {
611
+ if (isExternalValue(callee.expression)) {
612
+ return null;
613
+ }
614
+ const names = evaluator.evaluate(callee.argumentExpression);
615
+ if (!names.every(isLiteralPattern)) {
616
+ return unresolved();
617
+ }
618
+ const found = targetsFromTypes(typesOf(callee.expression), names);
619
+ if (found.targets.size) {
620
+ return fromTargets(found.targets, {});
621
+ }
622
+ return found.external ? null : unresolved();
623
+ }
624
+
625
+ // Pass 2: the final certainty of one call, or null when it is external.
626
+ function resolve(call, file) {
627
+ const resolution = direct.get(call) || directResolution(call);
628
+ const callee = unwrapExpression(call.expression);
629
+ switch (resolution.kind) {
630
+ case "targets":
631
+ return fromTargets(new Map(resolution.targets.map((target) => [target, { exact: true, via: "checker" }])), {});
632
+ case "external":
633
+ return null;
634
+ case "undeclared":
635
+ return NODE_GLOBALS.has(resolution.name) || (isComponentFile(file) && VUE_MACROS.has(resolution.name)) ? null : unresolved();
636
+ case "value": {
637
+ const declaration = resolution.declaration;
638
+ const bound = declaration && (ts.isParameter(declaration) || ts.isBindingElement(declaration))
639
+ ? fromTargets(boundFunctionTargets(declaration, call), {})
640
+ : null;
641
+ if (bound) {
642
+ return bound;
643
+ }
644
+ return declaration && isExternalDeclaration(declaration, 0) ? null : unresolved();
645
+ }
646
+ case "member":
647
+ return memberResolution(call, resolution.callee, file);
648
+ case "computed":
649
+ return computedResolution(callee);
650
+ default:
651
+ return unresolved();
652
+ }
653
+ }
654
+
655
+ return { indexCalls, indexSite, boundCandidates, indexBoundCalls, resolve, passedCall, builtinMembers };
656
+ }
657
+
658
+ export { codeSide, createResolver };