@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,371 @@
1
+ import ts from "typescript";
2
+ import { isFunctionLike, unwrapExpression } from "./functions.js";
3
+
4
+ const FEATURE_PACKAGE = "@jskit-ai/kernel";
5
+ const CRUD_PACKAGE = "@jskit-ai/crud-core";
6
+ const CRUD_RESOURCE_PACKAGE = "@jskit-ai/resource-crud-core";
7
+ // How many arrays, spreads, conditionals, calls and functions an action
8
+ // definition may sit under in a feature's actions() result: enough for a
9
+ // flagged spread of a create*Actions call whose definitions are each frozen.
10
+ const MAX_ACTION_DEPTH = 10;
11
+ const MAX_OBJECT_DEPTH = 8;
12
+
13
+ function memberNamed(objectLiteral, name) {
14
+ return objectLiteral.properties.find((property) =>
15
+ property.name && !ts.isComputedPropertyName(property.name) && property.name.text === name);
16
+ }
17
+
18
+ function isActionObject(node) {
19
+ return Boolean(node) && ts.isObjectLiteralExpression(node) &&
20
+ Boolean(memberNamed(node, "id")) && Boolean(memberNamed(node, "execute"));
21
+ }
22
+
23
+ /**
24
+ * JSKIT's defineFeature dependency injection, read statically:
25
+ * requires/optional tokens, provides tokens, setup outputs and the actions()
26
+ * definitions. Feature setup and actions parameters are bound to the
27
+ * expressions their providers return. One provider is exact; several are
28
+ * ambiguous; none leaves the parameter unbound.
29
+ * @param {object} dependencies
30
+ * @param {import("typescript").CallExpression[]} dependencies.calls
31
+ * @param {import("./types.js").CallSymbols} dependencies.symbols
32
+ * @param {import("./types.js").CallEvaluator} dependencies.evaluator
33
+ */
34
+ function bindFeatures({ calls, symbols, evaluator }) {
35
+ const features = [];
36
+ const crudDeclarations = [];
37
+ for (const call of calls) {
38
+ const callee = unwrapExpression(call.expression);
39
+ if (ts.isIdentifier(callee) && symbols.isImportOf(callee, CRUD_PACKAGE, ["defineCrudJsonApiFeature"])) {
40
+ const config = objectLiteral(call.arguments[0], symbols);
41
+ if (config) {
42
+ crudDeclarations.push(config);
43
+ }
44
+ }
45
+ if (!ts.isIdentifier(callee) || !symbols.isImportOf(callee, FEATURE_PACKAGE, ["defineFeature"])) {
46
+ continue;
47
+ }
48
+ const definition = unwrapExpression(call.arguments[0]);
49
+ if (definition && ts.isObjectLiteralExpression(definition)) {
50
+ features.push(readFeature(definition));
51
+ }
52
+ }
53
+
54
+ function memberFunctions(member) {
55
+ if (!member) {
56
+ return [];
57
+ }
58
+ if (isFunctionLike(member)) {
59
+ return [member];
60
+ }
61
+ if (ts.isPropertyAssignment(member)) {
62
+ return symbols.expressionImplementations(member.initializer, 0).filter(isFunctionLike);
63
+ }
64
+ if (ts.isShorthandPropertyAssignment(member)) {
65
+ return symbols.symbolImplementations(symbols.symbolAt(member.name)).filter(isFunctionLike);
66
+ }
67
+ return [];
68
+ }
69
+
70
+ function tokens(member) {
71
+ const result = new Map();
72
+ const value = member && ts.isPropertyAssignment(member) ? unwrapExpression(member.initializer) : null;
73
+ if (!value || !ts.isObjectLiteralExpression(value)) {
74
+ return result;
75
+ }
76
+ for (const property of value.properties) {
77
+ const initializer = ts.isPropertyAssignment(property) ? property.initializer
78
+ : ts.isShorthandPropertyAssignment(property) ? property.name : null;
79
+ const token = initializer && evaluator.evaluateLiteral(initializer);
80
+ if (token && property.name && !ts.isComputedPropertyName(property.name)) {
81
+ result.set(property.name.text, token);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ function returnedObject(expression) {
88
+ let value = unwrapExpression(expression);
89
+ if (value && ts.isIdentifier(value)) {
90
+ const declaration = symbols.declarationOf(value);
91
+ value = declaration && ts.isVariableDeclaration(declaration) ? unwrapExpression(declaration.initializer) : null;
92
+ }
93
+ return value && ts.isObjectLiteralExpression(value) ? value : null;
94
+ }
95
+
96
+ function setupOutputs(setups) {
97
+ const outputs = new Map();
98
+ for (const setup of setups) {
99
+ for (const expression of evaluator.functionReturns(setup)) {
100
+ const object = returnedObject(expression);
101
+ for (const property of object?.properties || []) {
102
+ if (ts.isShorthandPropertyAssignment(property)) {
103
+ outputs.set(property.name.text, [...(outputs.get(property.name.text) || []), property.name]);
104
+ } else if (ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name)) {
105
+ outputs.set(property.name.text, [...(outputs.get(property.name.text) || []), property.initializer]);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return outputs;
111
+ }
112
+
113
+ function readFeature(definition) {
114
+ const setups = memberFunctions(memberNamed(definition, "setup"));
115
+ const actionsMember = memberNamed(definition, "actions");
116
+ return {
117
+ definition,
118
+ requires: new Map([...tokens(memberNamed(definition, "optional")), ...tokens(memberNamed(definition, "requires"))]),
119
+ provides: tokens(memberNamed(definition, "provides")),
120
+ setups,
121
+ actionFunctions: memberFunctions(actionsMember),
122
+ actionsMember,
123
+ outputs: setupOutputs(setups)
124
+ };
125
+ }
126
+
127
+ const providers = new Map();
128
+ for (const feature of features) {
129
+ for (const [name, token] of feature.provides) {
130
+ for (const expression of feature.outputs.get(name) || []) {
131
+ providers.set(token, [...(providers.get(token) || []), expression]);
132
+ }
133
+ }
134
+ }
135
+
136
+ const bindings = new Map();
137
+ function bindPattern(fn, valuesFor) {
138
+ const parameter = fn.parameters[0];
139
+ if (!parameter || !ts.isObjectBindingPattern(parameter.name)) {
140
+ return;
141
+ }
142
+ for (const element of parameter.name.elements) {
143
+ const key = element.propertyName || element.name;
144
+ if (!ts.isIdentifier(key) || !ts.isIdentifier(element.name)) {
145
+ continue;
146
+ }
147
+ const expressions = valuesFor(key.text);
148
+ if (expressions.length) {
149
+ bindings.set(element, { expressions, exact: expressions.length === 1 });
150
+ }
151
+ }
152
+ }
153
+ for (const feature of features) {
154
+ const provided = (name) => providers.get(feature.requires.get(name)) || [];
155
+ for (const setup of feature.setups) {
156
+ bindPattern(setup, provided);
157
+ }
158
+ for (const actions of feature.actionFunctions) {
159
+ bindPattern(actions, (name) => feature.outputs.get(name) || provided(name));
160
+ }
161
+ }
162
+
163
+ return {
164
+ features,
165
+ bindings,
166
+ actions: [...collectFeatureActions(features, { symbols, evaluator }), ...collectCrudActions(crudDeclarations, { symbols, evaluator })]
167
+ };
168
+ }
169
+
170
+ // The object literal an expression holds, through const bindings, imports and
171
+ // `Object.freeze`, or null.
172
+ function objectLiteral(expression, symbols, depth = 0) {
173
+ let value = unwrapExpression(expression);
174
+ if (value && ts.isCallExpression(value) && value.arguments.length === 1 && ts.isPropertyAccessExpression(value.expression) &&
175
+ value.expression.name.text === "freeze" && ts.isIdentifier(value.expression.expression) && value.expression.expression.text === "Object") {
176
+ value = unwrapExpression(value.arguments[0]);
177
+ }
178
+ if (!value || depth > MAX_OBJECT_DEPTH) {
179
+ return null;
180
+ }
181
+ if (ts.isObjectLiteralExpression(value)) {
182
+ return value;
183
+ }
184
+ const declaration = ts.isIdentifier(value) ? symbols.declarationOf(value) : null;
185
+ return declaration && ts.isVariableDeclaration(declaration) ? objectLiteral(declaration.initializer, symbols, depth + 1) : null;
186
+ }
187
+
188
+ function memberValue(member) {
189
+ if (!member) {
190
+ return null;
191
+ }
192
+ if (ts.isPropertyAssignment(member)) {
193
+ return member.initializer;
194
+ }
195
+ return ts.isShorthandPropertyAssignment(member) ? member.name : null;
196
+ }
197
+
198
+ /**
199
+ * The functions an action's `execute` member runs.
200
+ * @param {import("typescript").Node} member
201
+ * @param {import("./types.js").CallSymbols} symbols
202
+ */
203
+ function executeFunctions(member, symbols) {
204
+ if (isFunctionLike(member)) {
205
+ return [member];
206
+ }
207
+ if (ts.isPropertyAssignment(member)) {
208
+ return symbols.expressionImplementations(member.initializer, 0).filter(isFunctionLike);
209
+ }
210
+ if (ts.isShorthandPropertyAssignment(member)) {
211
+ return symbols.symbolImplementations(symbols.symbolAt(member.name)).filter(isFunctionLike);
212
+ }
213
+ return [];
214
+ }
215
+
216
+ // The custom actions a CRUD declaration adds (`actions: { name: { execute } }`).
217
+ // Each runs its own execute; its id is the declared `id`, or
218
+ // `crud.<resource namespace>.<name>` as crud-core names it.
219
+ function collectCrudActions(declarations, { symbols, evaluator }) {
220
+ const actions = [];
221
+ for (const config of declarations) {
222
+ const definitions = objectLiteral(memberValue(memberNamed(config, "actions")), symbols);
223
+ for (const property of definitions?.properties || []) {
224
+ const name = ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : "";
225
+ const object = name ? objectLiteral(property.initializer, symbols) : null;
226
+ const execute = object && memberNamed(object, "execute");
227
+ if (!execute) {
228
+ continue;
229
+ }
230
+ // A declared id that is not a literal stays unresolved.
231
+ const idMember = memberNamed(object, "id");
232
+ const declared = idMember ? evaluator.evaluateLiteral(memberValue(idMember))?.trim() : "";
233
+ const namespace = declared === "" ? crudNamespace(config, { symbols, evaluator }) : null;
234
+ const actionId = declared || (namespace ? `crud.${namespace}.${name}` : null);
235
+ actions.push({ actionId, object, execute: executeFunctions(execute, symbols) });
236
+ }
237
+ }
238
+ return actions;
239
+ }
240
+
241
+ // The literal namespace of a CRUD declaration's `defineCrudResource` resource.
242
+ function crudNamespace(config, { symbols, evaluator }) {
243
+ let resource = unwrapExpression(memberValue(memberNamed(config, "resource")));
244
+ for (let depth = 0; resource && ts.isIdentifier(resource) && depth < MAX_OBJECT_DEPTH; depth += 1) {
245
+ const declaration = symbols.declarationOf(resource);
246
+ resource = declaration && ts.isVariableDeclaration(declaration) ? unwrapExpression(declaration.initializer) : null;
247
+ }
248
+ const callee = resource && ts.isCallExpression(resource) ? unwrapExpression(resource.expression) : null;
249
+ if (!callee || !ts.isIdentifier(callee) || !symbols.isImportOf(callee, CRUD_RESOURCE_PACKAGE, ["defineCrudResource"])) {
250
+ return null;
251
+ }
252
+ const definition = objectLiteral(resource.arguments[0], symbols);
253
+ const namespace = definition && evaluator.evaluateLiteral(memberValue(memberNamed(definition, "namespace")));
254
+ return namespace?.trim() || null;
255
+ }
256
+
257
+ // Action definitions inside each feature's actions() result: object literals
258
+ // with `id` and `execute`, possibly wrapped by a pass-through call, through
259
+ // arrays, spreads, project factory functions and literal-array `.map`.
260
+ function collectFeatureActions(features, { symbols, evaluator }) {
261
+ const found = [];
262
+ const seenFunctions = new Set();
263
+
264
+ function collect(node, indexes, depth) {
265
+ const value = unwrapExpression(node);
266
+ if (!value || depth > MAX_ACTION_DEPTH) {
267
+ return;
268
+ }
269
+ if (ts.isObjectLiteralExpression(value)) {
270
+ if (isActionObject(value)) {
271
+ found.push({ object: value, indexes });
272
+ return;
273
+ }
274
+ for (const property of value.properties) {
275
+ if (ts.isSpreadAssignment(property)) {
276
+ collect(property.expression, indexes, depth + 1);
277
+ }
278
+ }
279
+ return;
280
+ }
281
+ if (ts.isArrayLiteralExpression(value)) {
282
+ for (const element of value.elements) {
283
+ collect(ts.isSpreadElement(element) ? element.expression : element, indexes, depth + 1);
284
+ }
285
+ return;
286
+ }
287
+ if (ts.isConditionalExpression(value)) {
288
+ collect(value.whenTrue, indexes, depth + 1);
289
+ collect(value.whenFalse, indexes, depth + 1);
290
+ return;
291
+ }
292
+ if (isFunctionLike(value)) {
293
+ for (const expression of evaluator.functionReturns(value)) {
294
+ collect(expression, indexes, depth + 1);
295
+ }
296
+ return;
297
+ }
298
+ if (ts.isIdentifier(value)) {
299
+ const declaration = symbols.declarationOf(value);
300
+ if (declaration && ts.isVariableDeclaration(declaration) && declaration.initializer) {
301
+ collect(declaration.initializer, indexes, depth + 1);
302
+ }
303
+ return;
304
+ }
305
+ if (ts.isCallExpression(value)) {
306
+ collectCall(value, indexes, depth);
307
+ }
308
+ }
309
+
310
+ function collectCall(call, indexes, depth) {
311
+ const passedThrough = call.arguments.map(unwrapExpression).filter(isActionObject);
312
+ if (passedThrough.length) {
313
+ for (const object of passedThrough) {
314
+ found.push({ object, indexes });
315
+ }
316
+ return;
317
+ }
318
+ const callee = unwrapExpression(call.expression);
319
+ const callback = unwrapExpression(call.arguments[0]);
320
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === "map" && isFunctionLike(callback)) {
321
+ const elements = evaluator.literalArray(callee.expression);
322
+ const returns = evaluator.functionReturns(callback);
323
+ if (!elements) {
324
+ returns.forEach((expression) => collect(expression, indexes, depth + 1));
325
+ return;
326
+ }
327
+ elements.forEach((_, index) => {
328
+ const elementIndexes = new Map(indexes).set(callback, index);
329
+ returns.forEach((expression) => collect(expression, elementIndexes, depth + 1));
330
+ });
331
+ return;
332
+ }
333
+ const implementations = symbols.expressionImplementations(callee, 0).filter(isFunctionLike);
334
+ if (implementations.length) {
335
+ for (const fn of implementations) {
336
+ if (!seenFunctions.has(fn)) {
337
+ seenFunctions.add(fn);
338
+ collect(fn, indexes, depth + 1);
339
+ seenFunctions.delete(fn);
340
+ }
341
+ }
342
+ return;
343
+ }
344
+ if (ts.isPropertyAccessExpression(callee)) {
345
+ collect(callee.expression, indexes, depth + 1);
346
+ }
347
+ for (const argument of call.arguments) {
348
+ collect(argument, indexes, depth + 1);
349
+ }
350
+ }
351
+
352
+ const actions = [];
353
+ for (const feature of features) {
354
+ found.length = 0;
355
+ if (feature.actionFunctions.length) {
356
+ feature.actionFunctions.forEach((fn) => collect(fn, new Map(), 0));
357
+ } else if (feature.actionsMember && ts.isPropertyAssignment(feature.actionsMember)) {
358
+ collect(feature.actionsMember.initializer, new Map(), 0);
359
+ }
360
+ for (const { object, indexes } of found) {
361
+ const idMember = memberNamed(object, "id");
362
+ const idExpression = ts.isPropertyAssignment(idMember) ? idMember.initializer
363
+ : ts.isShorthandPropertyAssignment(idMember) ? idMember.name : null;
364
+ const actionId = idExpression && evaluator.evaluateLiteral(idExpression, { indexes, values: new Map() });
365
+ actions.push({ actionId, object, execute: executeFunctions(memberNamed(object, "execute"), symbols) });
366
+ }
367
+ }
368
+ return actions;
369
+ }
370
+
371
+ export { bindFeatures };
@@ -0,0 +1,277 @@
1
+ import ts from "typescript";
2
+
3
+ const SKIPPED_MODIFIERS = new Set([ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.DefaultKeyword, ts.SyntaxKind.DeclareKeyword]);
4
+
5
+ function isFunctionLike(node) {
6
+ return Boolean(node) && (
7
+ ts.isFunctionDeclaration(node) ||
8
+ ts.isFunctionExpression(node) ||
9
+ ts.isArrowFunction(node) ||
10
+ ts.isMethodDeclaration(node) ||
11
+ ts.isConstructorDeclaration(node) ||
12
+ ts.isGetAccessorDeclaration(node) ||
13
+ ts.isSetAccessorDeclaration(node)
14
+ ) && Boolean(node.body);
15
+ }
16
+
17
+ function unwrapExpression(node) {
18
+ let current = node;
19
+ while (current && (
20
+ ts.isParenthesizedExpression(current) ||
21
+ ts.isAsExpression(current) ||
22
+ ts.isNonNullExpression(current) ||
23
+ ts.isSatisfiesExpression(current) ||
24
+ ts.isTypeAssertionExpression(current)
25
+ )) {
26
+ current = current.expression;
27
+ }
28
+ return current;
29
+ }
30
+
31
+ // The canonical start (spec Decision 6): the first token of the tree-sitter
32
+ // function node, excluding decorators and the export/default wrapper that
33
+ // tree-sitter places outside the function node. Leading comments are trivia.
34
+ function canonicalStart(node) {
35
+ const sourceFile = node.getSourceFile();
36
+ const modifiers = node.modifiers || [];
37
+ let skippedEnd = -1;
38
+ for (const modifier of modifiers) {
39
+ if (ts.isDecorator(modifier) || SKIPPED_MODIFIERS.has(modifier.kind)) {
40
+ skippedEnd = modifier.end;
41
+ continue;
42
+ }
43
+ return modifier.getStart(sourceFile);
44
+ }
45
+ return skippedEnd < 0 ? node.getStart(sourceFile) : ts.skipTrivia(sourceFile.text, skippedEnd);
46
+ }
47
+
48
+ function point(sourceFile, position) {
49
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
50
+ return { line: line + 1, column: character + 1 };
51
+ }
52
+
53
+ function functionKind(node) {
54
+ if (ts.isConstructorDeclaration(node)) {
55
+ return "constructor";
56
+ }
57
+ if (ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
58
+ return "method";
59
+ }
60
+ if (ts.isArrowFunction(node)) {
61
+ return "arrow";
62
+ }
63
+ return ts.isFunctionExpression(node) ? "closure" : "function";
64
+ }
65
+
66
+ function propertyNameText(name, sourceFile) {
67
+ if (!name) {
68
+ return "";
69
+ }
70
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
71
+ return name.text;
72
+ }
73
+ return name.getText(sourceFile);
74
+ }
75
+
76
+ // The name a function is declared or bound under; "" when anonymous.
77
+ function functionName(node) {
78
+ const sourceFile = node.getSourceFile();
79
+ if (ts.isConstructorDeclaration(node)) {
80
+ return "constructor";
81
+ }
82
+ if (node.name) {
83
+ return propertyNameText(node.name, sourceFile);
84
+ }
85
+ let parent = node.parent;
86
+ while (parent && (ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent) || ts.isSatisfiesExpression(parent))) {
87
+ parent = parent.parent;
88
+ }
89
+ if (!parent) {
90
+ return "";
91
+ }
92
+ if ((ts.isVariableDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isPropertyAssignment(parent)) && parent.name) {
93
+ return ts.isObjectBindingPattern(parent.name) || ts.isArrayBindingPattern(parent.name) ? "" : propertyNameText(parent.name, sourceFile);
94
+ }
95
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && unwrapExpression(parent.right) === node) {
96
+ const target = parent.left;
97
+ if (ts.isIdentifier(target)) {
98
+ return target.text;
99
+ }
100
+ if (ts.isPropertyAccessExpression(target)) {
101
+ return target.name.text;
102
+ }
103
+ }
104
+ if (ts.isExportAssignment(parent)) {
105
+ return "default";
106
+ }
107
+ return "";
108
+ }
109
+
110
+ // The enclosing class, or the variable an enclosing object literal is bound to.
111
+ function functionContainer(node) {
112
+ const parent = ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) ? node.parent.parent : node.parent;
113
+ if (parent && (ts.isClassDeclaration(parent) || ts.isClassExpression(parent))) {
114
+ return parent.name?.text || (ts.isVariableDeclaration(parent.parent) && ts.isIdentifier(parent.parent.name) ? parent.parent.name.text : undefined);
115
+ }
116
+ if (parent && ts.isObjectLiteralExpression(parent)) {
117
+ let holder = parent.parent;
118
+ while (holder && (ts.isParenthesizedExpression(holder) || ts.isAsExpression(holder) || ts.isCallExpression(holder))) {
119
+ holder = holder.parent;
120
+ }
121
+ if (holder && ts.isVariableDeclaration(holder) && ts.isIdentifier(holder.name)) {
122
+ return holder.name.text;
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ function isTopLevelDeclaration(node) {
129
+ if (ts.isFunctionDeclaration(node)) {
130
+ return ts.isSourceFile(node.parent);
131
+ }
132
+ const declaration = node.parent;
133
+ return Boolean(declaration) && ts.isVariableDeclaration(declaration) &&
134
+ declaration.initializer === node &&
135
+ ts.isVariableStatement(declaration.parent?.parent) &&
136
+ ts.isSourceFile(declaration.parent.parent.parent);
137
+ }
138
+
139
+ // Every function-like with a body in one script, plus the file's top-level
140
+ // `module` function (`fn:<path>#module`, 1:1 to the end of the file).
141
+ // Positions are 1-based and cover the original file.
142
+ function collectFileFunctions({ file, hash, sourceFile, checker }) {
143
+ const records = [];
144
+ const nodes = [];
145
+ const end = point(sourceFile, Math.max(1, sourceFile.text.length - (sourceFile.text.endsWith("\n") ? 1 : 0)));
146
+ const moduleRecord = {
147
+ id: `fn:${file}#module`,
148
+ resolution: "resolved",
149
+ file,
150
+ name: "",
151
+ kind: "module",
152
+ start: { line: 1, column: 1 },
153
+ end,
154
+ evidence: [{ path: file, hash, start: { line: 1, column: 1 }, end }]
155
+ };
156
+ const exported = new Set();
157
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
158
+ for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : []) {
159
+ const target = symbol.flags & ts.SymbolFlags.Alias ? safeAliased(checker, symbol) : symbol;
160
+ for (const declaration of target?.declarations || []) {
161
+ exported.add(declaration);
162
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
163
+ exported.add(unwrapExpression(declaration.initializer));
164
+ }
165
+ }
166
+ }
167
+ const visit = (node) => {
168
+ if (isFunctionLike(node)) {
169
+ const startPosition = canonicalStart(node);
170
+ const start = point(sourceFile, startPosition);
171
+ const finish = point(sourceFile, node.end);
172
+ const container = functionContainer(node);
173
+ const record = {
174
+ id: `fn:${file}#${start.line}:${start.column}`,
175
+ resolution: "resolved",
176
+ file,
177
+ name: functionName(node),
178
+ kind: functionKind(node),
179
+ ...(container ? { container } : {}),
180
+ ...(isTopLevelDeclaration(node) && exported.has(node) ? { exported: true } : {}),
181
+ start,
182
+ end: finish,
183
+ evidence: [{ path: file, hash, start, end: finish }]
184
+ };
185
+ records.push(record);
186
+ nodes.push({ node, record, startPosition });
187
+ }
188
+ ts.forEachChild(node, visit);
189
+ };
190
+ visit(sourceFile);
191
+ // An empty file has no code, so no module function either.
192
+ const module = sourceFile.text.length ? moduleRecord : null;
193
+ return { module, functions: module ? [module, ...records] : records, nodes };
194
+ }
195
+
196
+ // A function that runs while the code around it runs: one passed straight to
197
+ // a call, or called in place.
198
+ function isCalledInPlace(fn) {
199
+ let child = fn;
200
+ let parent = fn.parent;
201
+ while (parent && ts.isParenthesizedExpression(parent)) {
202
+ child = parent;
203
+ parent = parent.parent;
204
+ }
205
+ return Boolean(parent) && (ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
206
+ (parent.expression === child || Boolean(parent.arguments?.includes(child)));
207
+ }
208
+
209
+ // Whether code at `node` runs while `fn` runs: no function between them is
210
+ // stored, returned or declared to run later.
211
+ function runsDuring(node, fn) {
212
+ for (let current = node.parent; current; current = current.parent) {
213
+ if (current === fn) {
214
+ return true;
215
+ }
216
+ if (isFunctionLike(current) && !isCalledInPlace(current)) {
217
+ return false;
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+
223
+ // The names a function's parameters bind, destructured ones included.
224
+ function parameterNames(fn) {
225
+ const names = new Set();
226
+ const visit = (name) => {
227
+ if (ts.isIdentifier(name)) {
228
+ names.add(name.text);
229
+ } else if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
230
+ name.elements.forEach((element) => ts.isBindingElement(element) && visit(element.name));
231
+ }
232
+ };
233
+ fn.parameters.forEach((parameter) => visit(parameter.name));
234
+ return names;
235
+ }
236
+
237
+ // The function (and parameter position) a parameter or destructured
238
+ // parameter element belongs to, with its property path.
239
+ function owningParameter(declaration) {
240
+ const pathParts = [];
241
+ let current = declaration;
242
+ while (current && ts.isBindingElement(current)) {
243
+ if (!ts.isObjectBindingPattern(current.parent)) {
244
+ return null;
245
+ }
246
+ const key = current.propertyName || current.name;
247
+ if (!ts.isIdentifier(key) && !ts.isStringLiteralLike(key)) {
248
+ return null;
249
+ }
250
+ pathParts.unshift(key.text);
251
+ current = current.parent.parent;
252
+ }
253
+ if (!current || !ts.isParameter(current) || !isFunctionLike(current.parent)) {
254
+ return null;
255
+ }
256
+ return { fn: current.parent, index: current.parent.parameters.indexOf(current), path: pathParts };
257
+ }
258
+
259
+ function safeAliased(checker, symbol) {
260
+ try {
261
+ return checker.getAliasedSymbol(symbol);
262
+ } catch {
263
+ return symbol;
264
+ }
265
+ }
266
+
267
+ export {
268
+ collectFileFunctions,
269
+ isCalledInPlace,
270
+ isFunctionLike,
271
+ owningParameter,
272
+ parameterNames,
273
+ point,
274
+ runsDuring,
275
+ safeAliased,
276
+ unwrapExpression
277
+ };