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