@saptools/service-flow 0.1.37 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cli.js +3 -2
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +3 -2
  4. package/src/cli.ts +865 -0
  5. package/src/config/defaults.ts +14 -0
  6. package/src/config/workspace-config.ts +61 -0
  7. package/src/db/connection.ts +131 -0
  8. package/src/db/migrations.ts +81 -0
  9. package/src/db/repositories.ts +353 -0
  10. package/src/db/schema.ts +24 -0
  11. package/src/discovery/classify-repository.ts +50 -0
  12. package/src/discovery/discover-repositories.ts +58 -0
  13. package/src/index.ts +13 -0
  14. package/src/indexer/incremental-index.ts +25 -0
  15. package/src/indexer/repository-indexer.ts +137 -0
  16. package/src/indexer/workspace-indexer.ts +29 -0
  17. package/src/linker/cross-repo-linker.ts +406 -0
  18. package/src/linker/dynamic-edge-resolver.ts +45 -0
  19. package/src/linker/external-http-target.ts +38 -0
  20. package/src/linker/helper-package-linker.ts +57 -0
  21. package/src/linker/odata-path-normalizer.ts +236 -0
  22. package/src/linker/operation-decorator-normalizer.ts +47 -0
  23. package/src/linker/remote-query-target.ts +39 -0
  24. package/src/linker/service-resolver.ts +223 -0
  25. package/src/output/json-output.ts +7 -0
  26. package/src/output/mermaid-output.ts +16 -0
  27. package/src/output/table-output.ts +21 -0
  28. package/src/parsers/cds-parser.ts +147 -0
  29. package/src/parsers/decorator-parser.ts +76 -0
  30. package/src/parsers/generated-constants-parser.ts +23 -0
  31. package/src/parsers/handler-registration-parser.ts +177 -0
  32. package/src/parsers/outbound-call-parser.ts +398 -0
  33. package/src/parsers/package-json-parser.ts +63 -0
  34. package/src/parsers/service-binding-parser.ts +826 -0
  35. package/src/parsers/symbol-parser.ts +328 -0
  36. package/src/parsers/ts-project.ts +13 -0
  37. package/src/trace/selectors.ts +20 -0
  38. package/src/trace/trace-engine.ts +647 -0
  39. package/src/trace/traversal.ts +4 -0
  40. package/src/types.ts +186 -0
  41. package/src/utils/diagnostics.ts +10 -0
  42. package/src/utils/hashing.ts +10 -0
  43. package/src/utils/path-utils.ts +13 -0
  44. package/src/utils/redaction.ts +20 -0
  45. package/src/version.ts +4 -0
@@ -0,0 +1,826 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
+ import type { ServiceBindingFact } from '../types.js';
5
+ import { normalizePath } from '../utils/path-utils.js';
6
+
7
+ interface HelperBinding {
8
+ exportedName: string;
9
+ returnedProperty?: string;
10
+ alias?: string;
11
+ aliasExpr?: string;
12
+ destinationExpr?: string;
13
+ servicePathExpr?: string;
14
+ isDynamic: boolean;
15
+ placeholders: string[];
16
+ helperChain?: Array<Record<string, unknown>>;
17
+ sourceFile: string;
18
+ sourceLine: number;
19
+ }
20
+ interface ImportBinding {
21
+ localName: string;
22
+ exportedName: string;
23
+ sourceFile?: string;
24
+ }
25
+ interface ClassHelperReturn {
26
+ className: string;
27
+ helperName: string;
28
+ propertyName: string;
29
+ variableName: string;
30
+ fact: Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>;
31
+ sourceLine: number;
32
+ }
33
+
34
+ function lineOf(sf: ts.SourceFile, node: ts.Node): number {
35
+ return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
36
+ }
37
+ function stringValue(node: ts.Expression | undefined): string | undefined {
38
+ if (!node) return undefined;
39
+ if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node))
40
+ return node.text;
41
+ if (ts.isTemplateExpression(node))
42
+ return node.getText().replace(/^`|`$/g, '');
43
+ return node.getText();
44
+ }
45
+ function placeholders(value?: string): string[] {
46
+ return [...(value ?? '').matchAll(/\$\{([^}]*)\}/g)]
47
+ .map((m) => (m[1] ?? '').trim())
48
+ .filter(Boolean);
49
+ }
50
+ function connectFactFromCall(
51
+ call: ts.CallExpression,
52
+ ):
53
+ | Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>
54
+ | undefined {
55
+ const expr = call.expression;
56
+ if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== 'to')
57
+ return undefined;
58
+ const inner = expr.expression;
59
+ if (
60
+ !ts.isPropertyAccessExpression(inner) ||
61
+ inner.name.text !== 'connect' ||
62
+ inner.expression.getText() !== 'cds'
63
+ )
64
+ return undefined;
65
+ const first = call.arguments[0];
66
+ if (!first) return undefined;
67
+ const second = call.arguments[1];
68
+ const objectArg = ts.isObjectLiteralExpression(first)
69
+ ? first
70
+ : second && ts.isObjectLiteralExpression(second)
71
+ ? second
72
+ : undefined;
73
+ let alias: string | undefined;
74
+ let aliasExpr: string | undefined;
75
+ if (ts.isStringLiteralLike(first) || ts.isNoSubstitutionTemplateLiteral(first))
76
+ alias = first.text;
77
+ else if (!ts.isObjectLiteralExpression(first))
78
+ aliasExpr = stringValue(first);
79
+ if (
80
+ (ts.isStringLiteralLike(first) || ts.isNoSubstitutionTemplateLiteral(first)) &&
81
+ !objectArg
82
+ )
83
+ return { alias: first.text, isDynamic: false, placeholders: [] };
84
+ if (!objectArg && aliasExpr)
85
+ return {
86
+ aliasExpr,
87
+ isDynamic: true,
88
+ placeholders: placeholders(aliasExpr),
89
+ };
90
+ let destinationExpr: string | undefined;
91
+ let servicePathExpr: string | undefined;
92
+ function visitObject(obj: ts.ObjectLiteralExpression): void {
93
+ for (const prop of obj.properties) {
94
+ if (!ts.isPropertyAssignment(prop)) continue;
95
+ const name =
96
+ ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)
97
+ ? prop.name.text
98
+ : undefined;
99
+ if (name === 'destination')
100
+ destinationExpr = stringValue(prop.initializer);
101
+ if (name === 'path' || name === 'servicePath')
102
+ servicePathExpr = stringValue(prop.initializer);
103
+ if (ts.isObjectLiteralExpression(prop.initializer))
104
+ visitObject(prop.initializer);
105
+ }
106
+ }
107
+ if (objectArg) visitObject(objectArg);
108
+ const ph = [
109
+ ...placeholders(aliasExpr ?? alias),
110
+ ...placeholders(destinationExpr),
111
+ ...placeholders(servicePathExpr),
112
+ ];
113
+ return {
114
+ alias,
115
+ aliasExpr,
116
+ destinationExpr,
117
+ servicePathExpr,
118
+ isDynamic: ph.length > 0 || (!destinationExpr && !servicePathExpr),
119
+ placeholders: ph,
120
+ };
121
+ }
122
+ function unwrapCall(expr: ts.Expression): ts.CallExpression | undefined {
123
+ if (ts.isAwaitExpression(expr)) return unwrapCall(expr.expression);
124
+ if (ts.isParenthesizedExpression(expr)) return unwrapCall(expr.expression);
125
+ if (ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) return unwrapCall(expr.expression);
126
+ if (ts.isTypeAssertionExpression(expr)) return unwrapCall(expr.expression);
127
+ if (ts.isCallExpression(expr)) return expr;
128
+ return undefined;
129
+ }
130
+ function unwrapIdentityExpression(expr: ts.Expression): ts.Expression {
131
+ if (ts.isAwaitExpression(expr)) return unwrapIdentityExpression(expr.expression);
132
+ if (ts.isParenthesizedExpression(expr)) return unwrapIdentityExpression(expr.expression);
133
+ if (ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) return unwrapIdentityExpression(expr.expression);
134
+ if (ts.isTypeAssertionExpression(expr)) return unwrapIdentityExpression(expr.expression);
135
+ return expr;
136
+ }
137
+
138
+ function transactionReceiverName(expr: ts.Expression): string | undefined {
139
+ const call = unwrapCall(expr);
140
+ if (call && ts.isPropertyAccessExpression(call.expression) && ['tx', 'transaction'].includes(call.expression.name.text) && ts.isIdentifier(call.expression.expression)) return call.expression.expression.text;
141
+ const unwrapped = unwrapIdentityExpression(expr);
142
+ if (ts.isConditionalExpression(unwrapped)) {
143
+ const left = transactionReceiverName(unwrapped.whenTrue);
144
+ const right = transactionReceiverName(unwrapped.whenFalse);
145
+ return left && left === right ? left : undefined;
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ function findConnectInExpression(
151
+ expr: ts.Expression,
152
+ ):
153
+ | Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>
154
+ | undefined {
155
+ const direct = unwrapCall(expr);
156
+ if (direct) {
157
+ const fact = connectFactFromCall(direct);
158
+ if (fact) return fact;
159
+ }
160
+ let found:
161
+ | Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>
162
+ | undefined;
163
+ function visit(node: ts.Node): void {
164
+ if (found) return;
165
+ if (ts.isCallExpression(node)) found = connectFactFromCall(node);
166
+ if (!found) ts.forEachChild(node, visit);
167
+ }
168
+ visit(expr);
169
+ return found;
170
+ }
171
+ async function readSource(abs: string): Promise<ts.SourceFile | undefined> {
172
+ try {
173
+ const text = await fs.readFile(abs, 'utf8');
174
+ return ts.createSourceFile(
175
+ abs,
176
+ text,
177
+ ts.ScriptTarget.Latest,
178
+ true,
179
+ ts.ScriptKind.TS,
180
+ );
181
+ } catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ async function resolveImport(
186
+ repoPath: string,
187
+ fromFile: string,
188
+ spec: string,
189
+ ): Promise<string | undefined> {
190
+ if (!spec.startsWith('.')) return undefined;
191
+ const rawBase = path.resolve(repoPath, path.dirname(fromFile), spec);
192
+ const parsed = path.parse(rawBase);
193
+ const base = ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(parsed.ext)
194
+ ? path.join(parsed.dir, parsed.name)
195
+ : rawBase;
196
+ for (const candidate of [
197
+ base,
198
+ `${base}.ts`,
199
+ `${base}.js`,
200
+ path.join(base, 'index.ts'),
201
+ path.join(base, 'index.js'),
202
+ ]) {
203
+ try {
204
+ const st = await fs.stat(candidate);
205
+ if (st.isFile()) return normalizePath(path.relative(repoPath, candidate));
206
+ } catch {
207
+ /* continue */
208
+ }
209
+ }
210
+ return undefined;
211
+ }
212
+ async function importsFor(
213
+ repoPath: string,
214
+ filePath: string,
215
+ sf: ts.SourceFile,
216
+ ): Promise<ImportBinding[]> {
217
+ const imports: ImportBinding[] = [];
218
+ for (const stmt of sf.statements) {
219
+ if (
220
+ !ts.isImportDeclaration(stmt) ||
221
+ !ts.isStringLiteralLike(stmt.moduleSpecifier)
222
+ )
223
+ continue;
224
+ const sourceFile = await resolveImport(
225
+ repoPath,
226
+ filePath,
227
+ stmt.moduleSpecifier.text,
228
+ );
229
+ const clause = stmt.importClause;
230
+ if (!clause) continue;
231
+ if (clause.name)
232
+ imports.push({
233
+ localName: clause.name.text,
234
+ exportedName: 'default',
235
+ sourceFile,
236
+ });
237
+ const bindings = clause.namedBindings;
238
+ if (bindings && ts.isNamedImports(bindings))
239
+ for (const el of bindings.elements)
240
+ imports.push({
241
+ localName: el.name.text,
242
+ exportedName: el.propertyName?.text ?? el.name.text,
243
+ sourceFile,
244
+ });
245
+ }
246
+ return imports;
247
+ }
248
+ function collectLocalBindingFacts(
249
+ fn: ts.FunctionLikeDeclaration,
250
+ ): Map<string, Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>> {
251
+ const bindings = new Map<string, Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>>();
252
+ function visit(node: ts.Node): void {
253
+ if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)))
254
+ return;
255
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
256
+ const fact = findConnectInExpression(node.initializer);
257
+ if (fact) bindings.set(node.name.text, fact);
258
+ const sourceName = transactionReceiverName(node.initializer);
259
+ if (sourceName) {
260
+ const source = bindings.get(sourceName);
261
+ if (source) bindings.set(node.name.text, { ...source, helperChain: [...(source.helperChain ?? []), { aliasOf: sourceName, callerVariable: node.name.text, aliasKind: 'transaction', transactionAliasSource: sourceName }] });
262
+ }
263
+ }
264
+ ts.forEachChild(node, visit);
265
+ }
266
+ visit(fn);
267
+ return bindings;
268
+ }
269
+
270
+ function collectReturnedObjectBindings(
271
+ fn: ts.FunctionLikeDeclaration,
272
+ ): Map<string, Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>> {
273
+ const bindings = collectLocalBindingFacts(fn);
274
+ const returns = new Map<string, string>();
275
+ function visit(node: ts.Node): void {
276
+ if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)))
277
+ return;
278
+ if (ts.isReturnStatement(node) && node.expression && ts.isObjectLiteralExpression(node.expression)) {
279
+ for (const prop of node.expression.properties) {
280
+ if (ts.isShorthandPropertyAssignment(prop)) returns.set(prop.name.text, prop.name.text);
281
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) {
282
+ const propertyName = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : undefined;
283
+ if (propertyName) returns.set(propertyName, prop.initializer.text);
284
+ }
285
+ }
286
+ }
287
+ ts.forEachChild(node, visit);
288
+ }
289
+ visit(fn);
290
+ const out = new Map<string, Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>>();
291
+ for (const [propertyName, variableName] of returns) {
292
+ const fact = bindings.get(variableName);
293
+ if (fact) out.set(propertyName, fact);
294
+ }
295
+ return out;
296
+ }
297
+
298
+ function functionLikeInitializer(
299
+ expr: ts.Expression | undefined,
300
+ ): ts.FunctionLikeDeclaration | undefined {
301
+ if (!expr) return undefined;
302
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) return expr;
303
+ return undefined;
304
+ }
305
+
306
+ function directReturnConnectFact(
307
+ fn: ts.FunctionLikeDeclaration,
308
+ ): Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'> | undefined {
309
+ const localBindings = collectLocalBindingFacts(fn);
310
+ let returned: ts.Expression | undefined;
311
+ function visit(node: ts.Node): void {
312
+ if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)))
313
+ return;
314
+ if (!returned && ts.isReturnStatement(node) && node.expression)
315
+ returned = node.expression;
316
+ if (!returned) ts.forEachChild(node, visit);
317
+ }
318
+ visit(fn);
319
+ if (!returned) return undefined;
320
+ if (ts.isIdentifier(returned)) return localBindings.get(returned.text);
321
+ return findConnectInExpression(returned);
322
+ }
323
+
324
+ function directConnectFactFromFunctionLike(
325
+ fn: ts.FunctionLikeDeclaration,
326
+ ): Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'> | undefined {
327
+ if (ts.isArrowFunction(fn) && fn.body && !ts.isBlock(fn.body))
328
+ return findConnectInExpression(fn.body);
329
+ return directReturnConnectFact(fn);
330
+ }
331
+
332
+ function exportedLocalNames(sf: ts.SourceFile): Map<string, string> {
333
+ const exports = new Map<string, string>();
334
+ for (const stmt of sf.statements) {
335
+ const direct = ts.canHaveModifiers(stmt)
336
+ ? (ts
337
+ .getModifiers(stmt)
338
+ ?.some(
339
+ (m: ts.ModifierLike) => m.kind === ts.SyntaxKind.ExportKeyword,
340
+ ) ?? false)
341
+ : false;
342
+ if (direct && ts.isFunctionDeclaration(stmt) && stmt.name)
343
+ exports.set(stmt.name.text, stmt.name.text);
344
+ if (direct && ts.isVariableStatement(stmt))
345
+ for (const decl of stmt.declarationList.declarations)
346
+ if (ts.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
347
+ if (!ts.isExportDeclaration(stmt) || !stmt.exportClause) continue;
348
+ if (!ts.isNamedExports(stmt.exportClause)) continue;
349
+ for (const el of stmt.exportClause.elements)
350
+ exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
351
+ }
352
+ return exports;
353
+ }
354
+ async function helperBindings(
355
+ repoPath: string,
356
+ filePath: string,
357
+ ): Promise<HelperBinding[]> {
358
+ const sf = await readSource(path.join(repoPath, filePath));
359
+ if (!sf) return [];
360
+ const sourceFileAst = sf;
361
+ const out: HelperBinding[] = [];
362
+ const exportedLocals = exportedLocalNames(sf);
363
+ const factsByLocal = new Map<
364
+ string,
365
+ Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'> & {
366
+ sourceLine: number;
367
+ }
368
+ >();
369
+ for (const stmt of sf.statements) {
370
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
371
+ const fact = directConnectFactFromFunctionLike(stmt);
372
+ if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf(sf, stmt) });
373
+ for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
374
+ factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf(sf, stmt) });
375
+ }
376
+ if (ts.isVariableStatement(stmt))
377
+ for (const decl of stmt.declarationList.declarations) {
378
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
379
+ const helper = functionLikeInitializer(decl.initializer);
380
+ if (helper) {
381
+ const directReturn = directConnectFactFromFunctionLike(helper);
382
+ if (directReturn)
383
+ factsByLocal.set(decl.name.text, {
384
+ ...directReturn,
385
+ sourceLine: lineOf(sourceFileAst, decl),
386
+ });
387
+ for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
388
+ factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
389
+ ...objectFact,
390
+ returnedProperty,
391
+ sourceLine: lineOf(sourceFileAst, decl),
392
+ });
393
+ continue;
394
+ }
395
+ const fact = findConnectInExpression(decl.initializer);
396
+ if (fact)
397
+ factsByLocal.set(decl.name.text, {
398
+ ...fact,
399
+ sourceLine: lineOf(sourceFileAst, decl),
400
+ });
401
+ }
402
+ }
403
+ for (const [exportedName, localName] of exportedLocals) {
404
+ const fact = factsByLocal.get(localName);
405
+ if (fact)
406
+ out.push({
407
+ ...fact,
408
+ exportedName,
409
+ sourceFile: normalizePath(filePath),
410
+ sourceLine: fact.sourceLine,
411
+ });
412
+ }
413
+ for (const [key, fact] of factsByLocal) {
414
+ const [localName, returnedProperty] = key.split('#');
415
+ if (!returnedProperty) continue;
416
+ for (const [exportedName, exportedLocal] of exportedLocals) {
417
+ if (exportedLocal !== localName) continue;
418
+ out.push({ ...fact, exportedName, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: fact.sourceLine });
419
+ }
420
+ }
421
+ return out;
422
+ }
423
+
424
+ export async function parseServiceBindings(
425
+ repoPath: string,
426
+ filePath: string,
427
+ ): Promise<ServiceBindingFact[]> {
428
+ const sf = await readSource(path.join(repoPath, filePath));
429
+ if (!sf) return [];
430
+ const sourceFileAst = sf;
431
+ const out: ServiceBindingFact[] = [];
432
+ const imports = await importsFor(repoPath, filePath, sf);
433
+ const helperCache = new Map<string, HelperBinding[]>();
434
+ const classHelpers = collectClassHelpers(sourceFileAst);
435
+ const localObjectHelpers = new Map<string, HelperBinding[]>();
436
+ const localDirectHelpers = new Map<string, HelperBinding>();
437
+ for (const stmt of sourceFileAst.statements) {
438
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
439
+ const directFact = directConnectFactFromFunctionLike(stmt);
440
+ if (directFact) localDirectHelpers.set(stmt.name.text, { ...directFact, exportedName: stmt.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf(sourceFileAst, stmt) });
441
+ const rows: HelperBinding[] = [];
442
+ for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
443
+ rows.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf(sourceFileAst, stmt) });
444
+ if (rows.length > 0) localObjectHelpers.set(stmt.name.text, rows);
445
+ }
446
+ if (ts.isVariableStatement(stmt)) {
447
+ for (const decl of stmt.declarationList.declarations) {
448
+ if (!ts.isIdentifier(decl.name)) continue;
449
+ const helper = functionLikeInitializer(decl.initializer);
450
+ if (!helper) continue;
451
+ const directFact = directConnectFactFromFunctionLike(helper);
452
+ if (directFact) localDirectHelpers.set(decl.name.text, { ...directFact, exportedName: decl.name.text, sourceFile: normalizePath(filePath), sourceLine: lineOf(sourceFileAst, decl) });
453
+ const rows: HelperBinding[] = [];
454
+ for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
455
+ rows.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf(sourceFileAst, decl) });
456
+ if (rows.length > 0) localObjectHelpers.set(decl.name.text, rows);
457
+ }
458
+ }
459
+ }
460
+ async function importedHelpers(
461
+ localName: string,
462
+ ): Promise<Array<{ imp: ImportBinding; helper: HelperBinding }>> {
463
+ const imp = imports.find((i) => i.localName === localName && i.sourceFile);
464
+ if (!imp?.sourceFile) return [];
465
+ if (!helperCache.has(imp.sourceFile))
466
+ helperCache.set(
467
+ imp.sourceFile,
468
+ await helperBindings(repoPath, imp.sourceFile),
469
+ );
470
+ return (helperCache.get(imp.sourceFile) ?? [])
471
+ .filter((h) => h.exportedName === imp.exportedName)
472
+ .map((helper) => ({ imp, helper }));
473
+ }
474
+ async function importedHelper(
475
+ localName: string,
476
+ ): Promise<{ imp: ImportBinding; helper: HelperBinding } | undefined> {
477
+ return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
478
+ }
479
+ function bindingForVariable(variableName: string): ServiceBindingFact | undefined {
480
+ const sourceFile = normalizePath(filePath);
481
+ return [...out]
482
+ .reverse()
483
+ .find((row) => row.variableName === variableName && row.sourceFile === sourceFile);
484
+ }
485
+ function cloneAliasBinding(targetName: string, sourceName: string, aliasKind: 'identity' | 'identity-assignment' | 'transaction', node: ts.Node): void {
486
+ const existing = bindingForVariable(sourceName);
487
+ if (!existing) return;
488
+ out.push({
489
+ ...existing,
490
+ variableName: targetName,
491
+ sourceLine: lineOf(sourceFileAst, node),
492
+ helperChain: [
493
+ ...(existing.helperChain ?? []),
494
+ {
495
+ callerVariable: targetName,
496
+ aliasOf: sourceName,
497
+ aliasKind,
498
+ scopeRule: 'same-file-source-order',
499
+ ...(aliasKind === 'transaction' ? { transactionAliasSource: sourceName } : {}),
500
+ },
501
+ ],
502
+ });
503
+ }
504
+ function recordIdentityAlias(decl: ts.VariableDeclaration): void {
505
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) return;
506
+ const unwrapped = unwrapIdentityExpression(decl.initializer);
507
+ if (!ts.isIdentifier(unwrapped)) return;
508
+ cloneAliasBinding(decl.name.text, unwrapped.text, 'identity', decl);
509
+ }
510
+
511
+ async function recordBindingFromExpression(targetName: string, expr: ts.Expression, node: ts.Node, aliasKind: 'declaration' | 'assignment'): Promise<void> {
512
+ const call = unwrapCall(expr);
513
+ if (!call) return;
514
+ const direct = connectFactFromCall(call);
515
+ if (direct)
516
+ out.push({
517
+ variableName: targetName,
518
+ ...direct,
519
+ sourceFile: normalizePath(filePath),
520
+ sourceLine: lineOf(sourceFileAst, node),
521
+ helperChain: aliasKind === 'assignment'
522
+ ? [{ callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind, scopeRule: 'same-file-source-order' }]
523
+ : undefined,
524
+ });
525
+ else if (ts.isIdentifier(call.expression)) {
526
+ const localDirect = localDirectHelpers.get(call.expression.text);
527
+ const resolved = localDirect ? { helper: localDirect, imp: undefined } : await importedHelper(call.expression.text);
528
+ if (resolved)
529
+ out.push({
530
+ variableName: targetName,
531
+ alias: resolved.helper.alias,
532
+ aliasExpr: resolved.helper.aliasExpr,
533
+ destinationExpr: resolved.helper.destinationExpr,
534
+ servicePathExpr: resolved.helper.servicePathExpr,
535
+ isDynamic: resolved.helper.isDynamic,
536
+ placeholders: resolved.helper.placeholders,
537
+ sourceFile: normalizePath(filePath),
538
+ sourceLine: lineOf(sourceFileAst, node),
539
+ helperChain: [
540
+ ...(resolved.helper.helperChain ?? []),
541
+ {
542
+ callerVariable: targetName,
543
+ ...(aliasKind === 'assignment' ? { assignedFrom: call.expression.text, aliasKind, scopeRule: 'same-file-source-order' } : {}),
544
+ importedHelper: call.expression.text,
545
+ importSource: resolved.imp?.sourceFile,
546
+ exportedSymbol: resolved.imp?.exportedName ?? resolved.helper.exportedName,
547
+ helperSourceFile: resolved.helper.sourceFile,
548
+ helperSourceLine: resolved.helper.sourceLine,
549
+ },
550
+ ],
551
+ });
552
+ }
553
+ }
554
+ async function recordVariable(decl: ts.VariableDeclaration): Promise<void> {
555
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) return;
556
+ await recordBindingFromExpression(decl.name.text, decl.initializer, decl, 'declaration');
557
+ }
558
+
559
+ async function helpersForCall(call: ts.CallExpression): Promise<Array<{ helper: HelperBinding; imp?: ImportBinding }>> {
560
+ if (!ts.isIdentifier(call.expression)) return [];
561
+ const local = localObjectHelpers.get(call.expression.text) ?? [];
562
+ const imported = await importedHelpers(call.expression.text);
563
+ return [...local.map((helper) => ({ helper })), ...imported];
564
+ }
565
+ async function recordDestructuredHelper(decl: ts.VariableDeclaration): Promise<void> {
566
+ if (!ts.isObjectBindingPattern(decl.name) || !decl.initializer) return;
567
+ const call = unwrapCall(decl.initializer);
568
+ if (!call) return;
569
+ const helpers = await helpersForCall(call);
570
+ if (helpers.length === 0) return;
571
+ for (const el of decl.name.elements) {
572
+ if (!ts.isIdentifier(el.name)) continue;
573
+ const propertyName = el.propertyName && ts.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
574
+ const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
575
+ if (matches.length !== 1) continue;
576
+ const resolved = matches[0];
577
+ out.push({
578
+ variableName: el.name.text,
579
+ alias: resolved.helper.alias,
580
+ aliasExpr: resolved.helper.aliasExpr,
581
+ destinationExpr: resolved.helper.destinationExpr,
582
+ servicePathExpr: resolved.helper.servicePathExpr,
583
+ isDynamic: resolved.helper.isDynamic,
584
+ placeholders: resolved.helper.placeholders,
585
+ sourceFile: normalizePath(filePath),
586
+ sourceLine: lineOf(sourceFileAst, decl),
587
+ helperChain: [...(resolved.helper.helperChain ?? []), { callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }],
588
+ });
589
+ }
590
+ }
591
+ async function recordDestructuredAssignment(pattern: ts.ObjectLiteralExpression, expr: ts.Expression, node: ts.Node): Promise<void> {
592
+ const call = unwrapCall(expr);
593
+ if (!call) return;
594
+ const helpers = await helpersForCall(call);
595
+ if (helpers.length === 0) return;
596
+ for (const prop of pattern.properties) {
597
+ let propertyName: string | undefined;
598
+ let targetName: string | undefined;
599
+ if (ts.isShorthandPropertyAssignment(prop)) {
600
+ propertyName = prop.name.text;
601
+ targetName = prop.name.text;
602
+ } else if (ts.isPropertyAssignment(prop)) {
603
+ propertyName = ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name) ? prop.name.text : undefined;
604
+ targetName = ts.isIdentifier(prop.initializer) ? prop.initializer.text : undefined;
605
+ }
606
+ if (!propertyName || !targetName) continue;
607
+ const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
608
+ if (matches.length !== 1) continue;
609
+ const resolved = matches[0];
610
+ out.push({
611
+ variableName: targetName,
612
+ alias: resolved.helper.alias,
613
+ aliasExpr: resolved.helper.aliasExpr,
614
+ destinationExpr: resolved.helper.destinationExpr,
615
+ servicePathExpr: resolved.helper.servicePathExpr,
616
+ isDynamic: resolved.helper.isDynamic,
617
+ placeholders: resolved.helper.placeholders,
618
+ sourceFile: normalizePath(filePath),
619
+ sourceLine: lineOf(sourceFileAst, node),
620
+ helperChain: [...(resolved.helper.helperChain ?? []), { callerVariable: targetName, assignedFrom: call.expression.getText(sourceFileAst), aliasKind: 'assignment', scopeRule: 'same-file-source-order', returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }],
621
+ });
622
+ }
623
+ }
624
+ function recordDestructuredClassHelper(decl: ts.VariableDeclaration): void {
625
+ if (!ts.isObjectBindingPattern(decl.name) || !decl.initializer) return;
626
+ const call = unwrapCall(decl.initializer);
627
+ if (!call || !ts.isPropertyAccessExpression(call.expression)) return;
628
+ const target = call.expression;
629
+ if (target.expression.kind !== ts.SyntaxKind.ThisKeyword) return;
630
+ for (const el of decl.name.elements) {
631
+ if (!ts.isIdentifier(el.name)) continue;
632
+ const propertyName = el.propertyName && ts.isIdentifier(el.propertyName)
633
+ ? el.propertyName.text
634
+ : el.name.text;
635
+ const helper = classHelpers.find(
636
+ (h) => h.helperName === target.name.text && h.propertyName === propertyName,
637
+ );
638
+ if (!helper) continue;
639
+ out.push({
640
+ variableName: el.name.text,
641
+ ...helper.fact,
642
+ sourceFile: normalizePath(filePath),
643
+ sourceLine: lineOf(sourceFileAst, decl),
644
+ helperChain: [
645
+ {
646
+ callerVariable: el.name.text,
647
+ className: helper.className,
648
+ classHelper: helper.helperName,
649
+ returnedProperty: helper.propertyName,
650
+ helperVariable: helper.variableName,
651
+ helperSourceFile: normalizePath(filePath),
652
+ helperSourceLine: helper.sourceLine,
653
+ },
654
+ ],
655
+ });
656
+ }
657
+ }
658
+
659
+ function arrayElementsFromExpression(expr: ts.Expression): { elements: ts.NodeArray<ts.Expression>; promiseAll: boolean } | undefined {
660
+ const unwrapped = unwrapIdentityExpression(expr);
661
+ if (ts.isArrayLiteralExpression(unwrapped)) return { elements: unwrapped.elements, promiseAll: false };
662
+ const call = unwrapCall(expr);
663
+ if (!call) return undefined;
664
+ if (!ts.isPropertyAccessExpression(call.expression) || call.expression.name.text !== 'all' || call.expression.expression.getText(sourceFileAst) !== 'Promise') return undefined;
665
+ const first = call.arguments[0];
666
+ if (!first) return undefined;
667
+ const container = unwrapIdentityExpression(first);
668
+ if (!ts.isArrayLiteralExpression(container)) return undefined;
669
+ return { elements: container.elements, promiseAll: true };
670
+ }
671
+
672
+ async function recordArrayElementBinding(targetName: string, expr: ts.Expression, node: ts.Node, arrayIndex: number, promiseAll: boolean): Promise<void> {
673
+ const before = out.length;
674
+ await recordBindingFromExpression(targetName, expr, node, 'declaration');
675
+ if (out.length > before) {
676
+ const row = out[out.length - 1];
677
+ row.helperChain = [
678
+ ...(row.helperChain ?? []),
679
+ { callerVariable: targetName, targetVariable: targetName, arrayIndex, promiseAll, arrayContainer: promiseAll ? 'Promise.all' : 'array_literal' },
680
+ ];
681
+ return;
682
+ }
683
+ const unwrapped = unwrapIdentityExpression(expr);
684
+ if (ts.isIdentifier(unwrapped)) {
685
+ const existing = bindingForVariable(unwrapped.text);
686
+ if (!existing) return;
687
+ out.push({
688
+ ...existing,
689
+ variableName: targetName,
690
+ sourceLine: lineOf(sourceFileAst, node),
691
+ helperChain: [
692
+ ...(existing.helperChain ?? []),
693
+ { callerVariable: targetName, targetVariable: targetName, sourceVariable: unwrapped.text, aliasKind: 'array-destructuring', arrayIndex, promiseAll, arrayContainer: promiseAll ? 'Promise.all' : 'array_literal' },
694
+ ],
695
+ });
696
+ }
697
+ }
698
+
699
+ async function recordArrayDestructuredVariable(decl: ts.VariableDeclaration): Promise<void> {
700
+ if (!ts.isArrayBindingPattern(decl.name) || !decl.initializer) return;
701
+ const container = arrayElementsFromExpression(decl.initializer);
702
+ if (!container) return;
703
+ for (let index = 0; index < decl.name.elements.length; index += 1) {
704
+ const el = decl.name.elements[index];
705
+ if (!el || ts.isOmittedExpression(el) || ts.isBindingElement(el) && el.dotDotDotToken) continue;
706
+ if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
707
+ const source = container.elements[index];
708
+ if (!source || ts.isOmittedExpression(source)) continue;
709
+ await recordArrayElementBinding(el.name.text, source, decl, index, container.promiseAll);
710
+ }
711
+ }
712
+
713
+ async function recordArrayDestructuredAssignment(pattern: ts.ArrayLiteralExpression, expr: ts.Expression, node: ts.Node): Promise<void> {
714
+ const container = arrayElementsFromExpression(expr);
715
+ if (!container) return;
716
+ for (let index = 0; index < pattern.elements.length; index += 1) {
717
+ const el = pattern.elements[index];
718
+ if (!el || ts.isOmittedExpression(el) || ts.isSpreadElement(el) || !ts.isIdentifier(el)) continue;
719
+ const source = container.elements[index];
720
+ if (!source || ts.isOmittedExpression(source)) continue;
721
+ await recordArrayElementBinding(el.text, source, node, index, container.promiseAll);
722
+ }
723
+ }
724
+
725
+ const events: Array<{ pos: number; node: ts.VariableDeclaration | ts.BinaryExpression }> = [];
726
+ function collectEvents(node: ts.Node): void {
727
+ if (ts.isVariableDeclaration(node)) events.push({ pos: node.getStart(sourceFileAst), node });
728
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken)
729
+ events.push({ pos: node.getStart(sourceFileAst), node });
730
+ ts.forEachChild(node, collectEvents);
731
+ }
732
+ collectEvents(sourceFileAst);
733
+ events.sort((a, b) => a.pos - b.pos);
734
+ for (const event of events) {
735
+ if (ts.isVariableDeclaration(event.node)) {
736
+ const decl = event.node;
737
+ await recordDestructuredHelper(decl);
738
+ await recordArrayDestructuredVariable(decl);
739
+ recordDestructuredClassHelper(decl);
740
+ await recordVariable(decl);
741
+ recordIdentityAlias(decl);
742
+ if (ts.isIdentifier(decl.name) && decl.initializer) {
743
+ const sourceName = transactionReceiverName(decl.initializer);
744
+ if (sourceName) cloneAliasBinding(decl.name.text, sourceName, 'transaction', decl);
745
+ }
746
+ continue;
747
+ }
748
+ const assignment = event.node;
749
+ if (ts.isIdentifier(assignment.left)) {
750
+ const rhs = unwrapIdentityExpression(assignment.right);
751
+ if (ts.isIdentifier(rhs)) {
752
+ cloneAliasBinding(assignment.left.text, rhs.text, 'identity-assignment', assignment);
753
+ continue;
754
+ }
755
+ await recordBindingFromExpression(assignment.left.text, assignment.right, assignment, 'assignment');
756
+ continue;
757
+ }
758
+ const left = ts.isParenthesizedExpression(assignment.left) ? assignment.left.expression : assignment.left;
759
+ if (ts.isObjectLiteralExpression(left))
760
+ await recordDestructuredAssignment(left, assignment.right, assignment);
761
+ if (ts.isArrayLiteralExpression(left))
762
+ await recordArrayDestructuredAssignment(left, assignment.right, assignment);
763
+ }
764
+ return out;
765
+ }
766
+
767
+ function collectClassHelpers(sf: ts.SourceFile): ClassHelperReturn[] {
768
+ const helpers: ClassHelperReturn[] = [];
769
+ for (const stmt of sf.statements) {
770
+ if (!ts.isClassDeclaration(stmt) || !stmt.name) continue;
771
+ for (const member of stmt.members) {
772
+ if (!ts.isPropertyDeclaration(member) || !member.initializer) continue;
773
+ if (!ts.isIdentifier(member.name)) continue;
774
+ const className = stmt.name.text;
775
+ const helperName = member.name.text;
776
+ const initializer = member.initializer;
777
+ if (!ts.isArrowFunction(initializer) && !ts.isFunctionExpression(initializer))
778
+ continue;
779
+ const bindings = new Map<
780
+ string,
781
+ Omit<HelperBinding, 'exportedName' | 'sourceFile' | 'sourceLine'>
782
+ >();
783
+ function visit(node: ts.Node): void {
784
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
785
+ const fact = findConnectInExpression(node.initializer);
786
+ if (fact) bindings.set(node.name.text, fact);
787
+ }
788
+ if (ts.isReturnStatement(node) && node.expression && ts.isObjectLiteralExpression(node.expression)) {
789
+ for (const prop of node.expression.properties) {
790
+ if (ts.isShorthandPropertyAssignment(prop)) {
791
+ const fact = bindings.get(prop.name.text);
792
+ if (fact)
793
+ helpers.push({
794
+ className,
795
+ helperName,
796
+ propertyName: prop.name.text,
797
+ variableName: prop.name.text,
798
+ fact,
799
+ sourceLine: lineOf(sf, prop),
800
+ });
801
+ }
802
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer)) {
803
+ const propertyName =
804
+ ts.isIdentifier(prop.name) || ts.isStringLiteralLike(prop.name)
805
+ ? prop.name.text
806
+ : undefined;
807
+ const fact = propertyName ? bindings.get(prop.initializer.text) : undefined;
808
+ if (propertyName && fact)
809
+ helpers.push({
810
+ className,
811
+ helperName,
812
+ propertyName,
813
+ variableName: prop.initializer.text,
814
+ fact,
815
+ sourceLine: lineOf(sf, prop),
816
+ });
817
+ }
818
+ }
819
+ }
820
+ ts.forEachChild(node, visit);
821
+ }
822
+ visit(initializer);
823
+ }
824
+ }
825
+ return helpers;
826
+ }