@saptools/service-flow 0.1.66 → 0.1.68

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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +20 -8
  3. package/TECHNICAL-NOTE.md +27 -0
  4. package/dist/{chunk-TOVX4WYH.js → chunk-AEM4JY22.js} +12676 -6714
  5. package/dist/chunk-AEM4JY22.js.map +1 -0
  6. package/dist/cli.js +2325 -413
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +61 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/003-doctor-package-resolution.ts +68 -0
  12. package/src/cli/doctor.ts +25 -14
  13. package/src/db/000-call-fact-repository.ts +475 -342
  14. package/src/db/001-fact-lifecycle.ts +60 -30
  15. package/src/db/002-fact-json-inventory.ts +169 -0
  16. package/src/db/003-current-fact-semantics.ts +698 -0
  17. package/src/db/004-package-target-invalidation.ts +173 -0
  18. package/src/db/005-schema-structure.ts +201 -0
  19. package/src/db/006-relative-symbol-resolution.ts +443 -0
  20. package/src/db/007-package-fact-semantics.ts +573 -0
  21. package/src/db/008-relative-fact-semantics.ts +207 -0
  22. package/src/db/009-binding-fact-semantics.ts +347 -0
  23. package/src/db/010-package-symbol-surface-semantics.ts +320 -0
  24. package/src/db/011-symbol-call-semantics.ts +144 -0
  25. package/src/db/012-binding-reference-proof.ts +264 -0
  26. package/src/db/migrations.ts +16 -3
  27. package/src/db/repositories.ts +113 -6
  28. package/src/db/schema.ts +4 -2
  29. package/src/index.ts +12 -0
  30. package/src/indexer/repository-indexer.ts +72 -11
  31. package/src/indexer/workspace-indexer.ts +123 -32
  32. package/src/linker/003-package-import-symbol-resolver.ts +363 -131
  33. package/src/linker/004-event-subscription-handler-linker.ts +2 -0
  34. package/src/linker/005-odata-path-structure.ts +371 -0
  35. package/src/linker/cross-repo-linker.ts +2 -1
  36. package/src/linker/odata-path-normalizer.ts +273 -180
  37. package/src/linker/service-resolver.ts +197 -77
  38. package/src/parsers/002-symbol-import-bindings.ts +516 -0
  39. package/src/parsers/003-package-public-surface.ts +661 -0
  40. package/src/parsers/004-fact-identity.ts +108 -0
  41. package/src/parsers/005-event-subscription-facts.ts +281 -0
  42. package/src/parsers/006-binding-identity.ts +343 -0
  43. package/src/parsers/007-source-fact-reconciliation.ts +105 -0
  44. package/src/parsers/008-package-surface-publication.ts +82 -0
  45. package/src/parsers/009-symbol-call-facts.ts +528 -0
  46. package/src/parsers/010-package-public-surface-analysis.ts +352 -0
  47. package/src/parsers/011-binding-lexical-scope.ts +583 -0
  48. package/src/parsers/012-package-fact-contract.ts +306 -0
  49. package/src/parsers/013-executable-body-eligibility.ts +35 -0
  50. package/src/parsers/014-service-binding-helper-flow.ts +240 -0
  51. package/src/parsers/015-service-binding-collector.ts +693 -0
  52. package/src/parsers/016-local-symbol-reference.ts +261 -0
  53. package/src/parsers/017-symbol-derived-contexts.ts +268 -0
  54. package/src/parsers/018-package-commonjs-syntax.ts +142 -0
  55. package/src/parsers/019-binding-assignment-targets.ts +76 -0
  56. package/src/parsers/020-stable-local-value.ts +217 -0
  57. package/src/parsers/021-binding-visibility.ts +152 -0
  58. package/src/parsers/operation-path-analysis.ts +6 -1
  59. package/src/parsers/outbound-call-parser.ts +19 -6
  60. package/src/parsers/package-json-parser.ts +45 -3
  61. package/src/parsers/service-binding-parser-helpers.ts +86 -15
  62. package/src/parsers/service-binding-parser.ts +147 -597
  63. package/src/parsers/symbol-parser.ts +482 -353
  64. package/src/trace/002-trace-diagnostics.ts +36 -1
  65. package/src/trace/016-compact-projector.ts +16 -17
  66. package/src/trace/020-compact-field-projection.ts +48 -37
  67. package/src/trace/021-compact-decision-normalization.ts +105 -0
  68. package/src/trace/022-trace-fact-preflight.ts +21 -0
  69. package/src/trace/023-nested-event-scopes.ts +23 -0
  70. package/src/trace/024-compact-observation-decision.ts +76 -0
  71. package/src/trace/025-trace-implementation-scope.ts +123 -0
  72. package/src/trace/026-trace-start-scope.ts +335 -0
  73. package/src/trace/027-trace-scope-execution.ts +566 -0
  74. package/src/trace/028-trace-operation-execution.ts +336 -0
  75. package/src/trace/029-trace-start-implementation.ts +172 -0
  76. package/src/trace/trace-engine.ts +122 -624
  77. package/src/types.ts +56 -0
  78. package/src/utils/001-placeholders.ts +188 -10
  79. package/src/version.ts +1 -1
  80. package/dist/chunk-TOVX4WYH.js.map +0 -1
@@ -0,0 +1,693 @@
1
+ import ts from 'typescript';
2
+ import type { ServiceBindingFact } from '../types.js';
3
+ import { normalizePath } from '../utils/path-utils.js';
4
+ import {
5
+ connectFactFromCall,
6
+ lineOf,
7
+ transactionReceiverName,
8
+ unwrapCall,
9
+ unwrapIdentityExpression,
10
+ type ClassHelperReturn,
11
+ type HelperBinding,
12
+ type ImportBinding,
13
+ } from './service-binding-parser-helpers.js';
14
+ import {
15
+ createBindingLexicalIndex,
16
+ type BindingLexicalIndex,
17
+ type BindingSiteCandidate,
18
+ } from './011-binding-lexical-scope.js';
19
+ import { selectVisibleBinding } from './021-binding-visibility.js';
20
+ import {
21
+ arrayAssignmentName,
22
+ arrayBindingName,
23
+ bindingValueAtSite,
24
+ collectReturnedObjectBindings,
25
+ directConnectFactFromFunctionLike,
26
+ functionLikeInitializer,
27
+ } from './014-service-binding-helper-flow.js';
28
+
29
+ type ResolvedHelper = { helper: HelperBinding; imp?: ImportBinding };
30
+ type BindingEvent = {
31
+ pos: number;
32
+ node: ts.VariableDeclaration | ts.BinaryExpression;
33
+ };
34
+
35
+ export interface ServiceBindingCollectorInput {
36
+ source: ts.SourceFile;
37
+ filePath: string;
38
+ imports: ImportBinding[];
39
+ classHelpers: ClassHelperReturn[];
40
+ loadHelperBindings: (filePath: string) => Promise<HelperBinding[]>;
41
+ }
42
+
43
+ function bindingAtSite(
44
+ fact: ServiceBindingFact,
45
+ node: ts.Node,
46
+ source: ts.SourceFile,
47
+ ): ServiceBindingFact {
48
+ return {
49
+ ...fact,
50
+ bindingSiteStartOffset: node.getStart(source),
51
+ bindingSiteEndOffset: node.getEnd(),
52
+ };
53
+ }
54
+
55
+ function bindingEvents(source: ts.SourceFile): BindingEvent[] {
56
+ const events: BindingEvent[] = [];
57
+ function visit(node: ts.Node): void {
58
+ if (ts.isVariableDeclaration(node))
59
+ events.push({ pos: node.getStart(source), node });
60
+ if (ts.isBinaryExpression(node)
61
+ && node.operatorToken.kind === ts.SyntaxKind.EqualsToken)
62
+ events.push({ pos: node.getStart(source), node });
63
+ ts.forEachChild(node, visit);
64
+ }
65
+ visit(source);
66
+ return events.sort((left, right) => left.pos - right.pos);
67
+ }
68
+
69
+ function destructuredTarget(
70
+ property: ts.ObjectLiteralElementLike,
71
+ ): { propertyName?: string; targetName?: string } {
72
+ if (ts.isShorthandPropertyAssignment(property))
73
+ return {
74
+ propertyName: property.name.text,
75
+ targetName: property.name.text,
76
+ };
77
+ if (!ts.isPropertyAssignment(property)) return {};
78
+ const propertyName = ts.isIdentifier(property.name)
79
+ || ts.isStringLiteralLike(property.name)
80
+ ? property.name.text
81
+ : undefined;
82
+ return {
83
+ propertyName,
84
+ targetName: ts.isIdentifier(property.initializer)
85
+ ? property.initializer.text
86
+ : undefined,
87
+ };
88
+ }
89
+
90
+ class ServiceBindingCollector {
91
+ private readonly sourceFile: string;
92
+ private readonly lexicalIndex: BindingLexicalIndex;
93
+ private readonly out: ServiceBindingFact[] = [];
94
+ private readonly helperCache = new Map<string, HelperBinding[]>();
95
+ private readonly localObjectHelpers = new Map<string, HelperBinding[]>();
96
+ private readonly localDirectHelpers = new Map<string, HelperBinding>();
97
+ private readonly objectHelperVariables: Array<BindingSiteCandidate<
98
+ ResolvedHelper[]
99
+ >> = [];
100
+
101
+ constructor(private readonly input: ServiceBindingCollectorInput) {
102
+ this.sourceFile = normalizePath(input.filePath);
103
+ this.lexicalIndex = createBindingLexicalIndex(input.source);
104
+ }
105
+
106
+ async collect(): Promise<ServiceBindingFact[]> {
107
+ this.collectLocalHelpers();
108
+ for (const event of bindingEvents(this.input.source))
109
+ if (ts.isVariableDeclaration(event.node))
110
+ await this.recordDeclaration(event.node);
111
+ else
112
+ await this.recordAssignment(event.node);
113
+ return this.out;
114
+ }
115
+
116
+ private helperFact(
117
+ name: string,
118
+ node: ts.Node,
119
+ helper: ts.FunctionLikeDeclaration,
120
+ ): void {
121
+ const direct = directConnectFactFromFunctionLike(helper);
122
+ if (direct)
123
+ this.localDirectHelpers.set(name, {
124
+ ...direct,
125
+ exportedName: name,
126
+ sourceFile: this.sourceFile,
127
+ sourceLine: lineOf(this.input.source, node),
128
+ });
129
+ const rows = [...collectReturnedObjectBindings(helper)]
130
+ .map(([returnedProperty, fact]): HelperBinding => ({
131
+ ...fact,
132
+ exportedName: name,
133
+ returnedProperty,
134
+ sourceFile: this.sourceFile,
135
+ sourceLine: lineOf(this.input.source, node),
136
+ }));
137
+ if (rows.length > 0) this.localObjectHelpers.set(name, rows);
138
+ }
139
+
140
+ private collectLocalHelpers(): void {
141
+ for (const statement of this.input.source.statements) {
142
+ if (ts.isFunctionDeclaration(statement) && statement.name)
143
+ this.helperFact(statement.name.text, statement, statement);
144
+ if (!ts.isVariableStatement(statement)) continue;
145
+ for (const declaration of statement.declarationList.declarations) {
146
+ if (!ts.isIdentifier(declaration.name)) continue;
147
+ const helper = functionLikeInitializer(declaration.initializer);
148
+ if (helper) this.helperFact(
149
+ declaration.name.text, declaration, helper,
150
+ );
151
+ }
152
+ }
153
+ }
154
+
155
+ private async importedHelpers(localName: string): Promise<ResolvedHelper[]> {
156
+ const imported = this.input.imports.find((item) =>
157
+ item.localName === localName && item.sourceFile);
158
+ if (!imported?.sourceFile) return [];
159
+ if (!this.helperCache.has(imported.sourceFile))
160
+ this.helperCache.set(
161
+ imported.sourceFile,
162
+ await this.input.loadHelperBindings(imported.sourceFile),
163
+ );
164
+ return (this.helperCache.get(imported.sourceFile) ?? [])
165
+ .filter((helper) => helper.exportedName === imported.exportedName)
166
+ .map((helper) => ({ imp: imported, helper }));
167
+ }
168
+
169
+ private async importedHelper(localName: string): Promise<ResolvedHelper | undefined> {
170
+ return (await this.importedHelpers(localName))
171
+ .find((row) => !row.helper.returnedProperty);
172
+ }
173
+
174
+ private bindingCandidates(): Array<BindingSiteCandidate<ServiceBindingFact>> {
175
+ return this.out.map((fact) => ({
176
+ variableName: fact.variableName,
177
+ bindingSiteStartOffset: fact.bindingSiteStartOffset,
178
+ bindingSiteEndOffset: fact.bindingSiteEndOffset,
179
+ value: fact,
180
+ }));
181
+ }
182
+
183
+ private bindingForVariable(
184
+ variableName: string,
185
+ node: ts.Node,
186
+ ): ServiceBindingFact | undefined {
187
+ const selected = selectVisibleBinding(
188
+ this.lexicalIndex, this.bindingCandidates(), variableName, node,
189
+ );
190
+ if (!selected.site?.deterministic
191
+ || selected.declarationSite?.declarationKind === 'var') return undefined;
192
+ return selected.candidate?.value;
193
+ }
194
+
195
+ private cloneAlias(
196
+ targetName: string,
197
+ sourceName: string,
198
+ aliasKind: 'identity' | 'identity-assignment' | 'transaction',
199
+ node: ts.Node,
200
+ ): void {
201
+ const existing = this.bindingForVariable(sourceName, node);
202
+ if (!existing) return;
203
+ const helperChain = [...(existing.helperChain ?? []), {
204
+ callerVariable: targetName,
205
+ aliasOf: sourceName,
206
+ aliasKind,
207
+ scopeRule: 'exact_lexical_scope',
208
+ ...(aliasKind === 'transaction'
209
+ ? { transactionAliasSource: sourceName }
210
+ : {}),
211
+ }];
212
+ this.out.push(bindingAtSite({
213
+ ...existing,
214
+ variableName: targetName,
215
+ sourceLine: lineOf(this.input.source, node),
216
+ helperChain,
217
+ }, node, this.input.source));
218
+ }
219
+
220
+ private recordIdentityAlias(declaration: ts.VariableDeclaration): void {
221
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
222
+ const source = unwrapIdentityExpression(declaration.initializer);
223
+ if (ts.isIdentifier(source))
224
+ this.cloneAlias(
225
+ declaration.name.text, source.text, 'identity', declaration,
226
+ );
227
+ }
228
+
229
+ private directBinding(
230
+ targetName: string,
231
+ call: ts.CallExpression,
232
+ node: ts.Node,
233
+ flow: 'declaration' | 'assignment',
234
+ ): boolean {
235
+ const direct = connectFactFromCall(call);
236
+ if (!direct) return false;
237
+ const helperChain = flow === 'assignment'
238
+ ? [{
239
+ callerVariable: targetName,
240
+ assignedFrom: call.expression.getText(this.input.source),
241
+ aliasKind: flow,
242
+ scopeRule: 'exact_lexical_scope',
243
+ }]
244
+ : undefined;
245
+ this.out.push(bindingAtSite({
246
+ variableName: targetName,
247
+ ...direct,
248
+ sourceFile: this.sourceFile,
249
+ sourceLine: lineOf(this.input.source, node),
250
+ helperChain,
251
+ }, node, this.input.source));
252
+ return true;
253
+ }
254
+
255
+ private helperBindingFact(
256
+ targetName: string,
257
+ resolved: ResolvedHelper,
258
+ call: ts.CallExpression,
259
+ node: ts.Node,
260
+ flow: 'declaration' | 'assignment',
261
+ ): ServiceBindingFact {
262
+ const assignment = flow === 'assignment'
263
+ ? {
264
+ assignedFrom: call.expression.getText(this.input.source),
265
+ aliasKind: flow,
266
+ scopeRule: 'exact_lexical_scope',
267
+ }
268
+ : {};
269
+ return {
270
+ variableName: targetName,
271
+ alias: resolved.helper.alias,
272
+ aliasExpr: resolved.helper.aliasExpr,
273
+ destinationExpr: resolved.helper.destinationExpr,
274
+ servicePathExpr: resolved.helper.servicePathExpr,
275
+ isDynamic: resolved.helper.isDynamic,
276
+ placeholders: resolved.helper.placeholders,
277
+ sourceFile: this.sourceFile,
278
+ sourceLine: lineOf(this.input.source, node),
279
+ helperChain: [...(resolved.helper.helperChain ?? []), {
280
+ callerVariable: targetName,
281
+ ...assignment,
282
+ importedHelper: call.expression.getText(this.input.source),
283
+ importSource: resolved.imp?.sourceFile,
284
+ exportedSymbol: resolved.imp?.exportedName
285
+ ?? resolved.helper.exportedName,
286
+ helperSourceFile: resolved.helper.sourceFile,
287
+ helperSourceLine: resolved.helper.sourceLine,
288
+ }],
289
+ };
290
+ }
291
+
292
+ private async recordExpression(
293
+ targetName: string,
294
+ expression: ts.Expression,
295
+ node: ts.Node,
296
+ flow: 'declaration' | 'assignment',
297
+ ): Promise<void> {
298
+ const call = unwrapCall(expression);
299
+ if (!call || this.directBinding(targetName, call, node, flow)) return;
300
+ if (!ts.isIdentifier(call.expression)) return;
301
+ const local = this.localDirectHelpers.get(call.expression.text);
302
+ const resolved = local
303
+ ? { helper: local }
304
+ : await this.importedHelper(call.expression.text);
305
+ if (resolved)
306
+ this.out.push(bindingAtSite(
307
+ this.helperBindingFact(targetName, resolved, call, node, flow),
308
+ node,
309
+ this.input.source,
310
+ ));
311
+ }
312
+
313
+ private async helpersForCall(call: ts.CallExpression): Promise<ResolvedHelper[]> {
314
+ if (!ts.isIdentifier(call.expression)) return [];
315
+ const local = this.localObjectHelpers.get(call.expression.text) ?? [];
316
+ const imported = await this.importedHelpers(call.expression.text);
317
+ return [...local.map((helper) => ({ helper })), ...imported];
318
+ }
319
+
320
+ private async rememberObjectHelper(declaration: ts.VariableDeclaration): Promise<void> {
321
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
322
+ const call = unwrapCall(declaration.initializer);
323
+ if (!call) return;
324
+ const helpers = (await this.helpersForCall(call))
325
+ .filter((row) => row.helper.returnedProperty);
326
+ if (helpers.length > 0)
327
+ this.objectHelperVariables.push(bindingValueAtSite(
328
+ declaration.name.text, declaration, this.input.source, helpers,
329
+ ));
330
+ }
331
+
332
+ private visibleObjectHelpers(variableName: string, node: ts.Node): ResolvedHelper[] {
333
+ const selected = selectVisibleBinding(
334
+ this.lexicalIndex, this.objectHelperVariables, variableName, node,
335
+ );
336
+ if (!selected.site?.deterministic
337
+ || selected.declarationSite?.declarationKind === 'var') return [];
338
+ return selected.candidate?.value ?? [];
339
+ }
340
+
341
+ private objectPropertyFact(
342
+ targetName: string,
343
+ expression: ts.PropertyAccessExpression,
344
+ resolved: ResolvedHelper,
345
+ node: ts.Node,
346
+ ): ServiceBindingFact {
347
+ return {
348
+ variableName: targetName,
349
+ alias: resolved.helper.alias,
350
+ aliasExpr: resolved.helper.aliasExpr,
351
+ destinationExpr: resolved.helper.destinationExpr,
352
+ servicePathExpr: resolved.helper.servicePathExpr,
353
+ isDynamic: resolved.helper.isDynamic,
354
+ placeholders: resolved.helper.placeholders,
355
+ sourceFile: this.sourceFile,
356
+ sourceLine: lineOf(this.input.source, node),
357
+ helperChain: [...(resolved.helper.helperChain ?? []), {
358
+ callerVariable: targetName,
359
+ sourceVariable: expression.expression.getText(this.input.source),
360
+ returnedProperty: expression.name.text,
361
+ assignedFromProperty: expression.getText(this.input.source),
362
+ importSource: resolved.imp?.sourceFile,
363
+ exportedSymbol: resolved.imp?.exportedName,
364
+ helperSourceFile: resolved.helper.sourceFile,
365
+ helperSourceLine: resolved.helper.sourceLine,
366
+ }],
367
+ };
368
+ }
369
+
370
+ private recordObjectProperty(
371
+ targetName: string,
372
+ expression: ts.Expression,
373
+ node: ts.Node,
374
+ ): boolean {
375
+ const unwrapped = unwrapIdentityExpression(expression);
376
+ if (!ts.isPropertyAccessExpression(unwrapped)
377
+ || !ts.isIdentifier(unwrapped.expression)) return false;
378
+ const matches = this.visibleObjectHelpers(
379
+ unwrapped.expression.text, node,
380
+ ).filter((row) => row.helper.returnedProperty === unwrapped.name.text);
381
+ const resolved = matches.length === 1 ? matches[0] : undefined;
382
+ if (!resolved) return false;
383
+ this.out.push(bindingAtSite(
384
+ this.objectPropertyFact(targetName, unwrapped, resolved, node),
385
+ node,
386
+ this.input.source,
387
+ ));
388
+ return true;
389
+ }
390
+
391
+ private returnedHelperFact(
392
+ targetName: string,
393
+ propertyName: string,
394
+ resolved: ResolvedHelper,
395
+ call: ts.CallExpression,
396
+ node: ts.Node,
397
+ assignment: boolean,
398
+ ): ServiceBindingFact {
399
+ return {
400
+ variableName: targetName,
401
+ alias: resolved.helper.alias,
402
+ aliasExpr: resolved.helper.aliasExpr,
403
+ destinationExpr: resolved.helper.destinationExpr,
404
+ servicePathExpr: resolved.helper.servicePathExpr,
405
+ isDynamic: resolved.helper.isDynamic,
406
+ placeholders: resolved.helper.placeholders,
407
+ sourceFile: this.sourceFile,
408
+ sourceLine: lineOf(this.input.source, node),
409
+ helperChain: [...(resolved.helper.helperChain ?? []), {
410
+ callerVariable: targetName,
411
+ ...(assignment ? {
412
+ assignedFrom: call.expression.getText(this.input.source),
413
+ aliasKind: 'assignment',
414
+ scopeRule: 'exact_lexical_scope',
415
+ } : { helperFunction: call.expression.getText(this.input.source) }),
416
+ returnedProperty: propertyName,
417
+ importSource: resolved.imp?.sourceFile,
418
+ exportedSymbol: resolved.imp?.exportedName,
419
+ helperSourceFile: resolved.helper.sourceFile,
420
+ helperSourceLine: resolved.helper.sourceLine,
421
+ }],
422
+ };
423
+ }
424
+
425
+ private async recordDestructuredHelper(
426
+ declaration: ts.VariableDeclaration,
427
+ ): Promise<void> {
428
+ if (!ts.isObjectBindingPattern(declaration.name)
429
+ || !declaration.initializer) return;
430
+ const call = unwrapCall(declaration.initializer);
431
+ if (!call) return;
432
+ const helpers = await this.helpersForCall(call);
433
+ for (const element of declaration.name.elements) {
434
+ if (!ts.isIdentifier(element.name)) continue;
435
+ const propertyName = element.propertyName
436
+ && ts.isIdentifier(element.propertyName)
437
+ ? element.propertyName.text
438
+ : element.name.text;
439
+ const matches = helpers.filter((row) =>
440
+ row.helper.returnedProperty === propertyName);
441
+ const resolved = matches.length === 1 ? matches[0] : undefined;
442
+ if (resolved)
443
+ this.out.push(bindingAtSite(
444
+ this.returnedHelperFact(
445
+ element.name.text, propertyName, resolved,
446
+ call, declaration, false,
447
+ ),
448
+ declaration,
449
+ this.input.source,
450
+ ));
451
+ }
452
+ }
453
+
454
+ private async recordDestructuredAssignment(
455
+ pattern: ts.ObjectLiteralExpression,
456
+ expression: ts.Expression,
457
+ node: ts.Node,
458
+ ): Promise<void> {
459
+ const call = unwrapCall(expression);
460
+ if (!call) return;
461
+ const helpers = await this.helpersForCall(call);
462
+ for (const property of pattern.properties) {
463
+ const target = destructuredTarget(property);
464
+ if (!target.propertyName || !target.targetName) continue;
465
+ const matches = helpers.filter((row) =>
466
+ row.helper.returnedProperty === target.propertyName);
467
+ const resolved = matches.length === 1 ? matches[0] : undefined;
468
+ if (resolved)
469
+ this.out.push(bindingAtSite(
470
+ this.returnedHelperFact(
471
+ target.targetName, target.propertyName, resolved,
472
+ call, node, true,
473
+ ),
474
+ node,
475
+ this.input.source,
476
+ ));
477
+ }
478
+ }
479
+
480
+ private recordClassHelper(declaration: ts.VariableDeclaration): void {
481
+ if (!ts.isObjectBindingPattern(declaration.name)
482
+ || !declaration.initializer) return;
483
+ const call = unwrapCall(declaration.initializer);
484
+ if (!call || !ts.isPropertyAccessExpression(call.expression)
485
+ || call.expression.expression.kind !== ts.SyntaxKind.ThisKeyword) return;
486
+ for (const element of declaration.name.elements)
487
+ this.recordClassHelperElement(
488
+ declaration, element, call.expression.name.text,
489
+ );
490
+ }
491
+
492
+ private recordClassHelperElement(
493
+ declaration: ts.VariableDeclaration,
494
+ element: ts.BindingElement,
495
+ helperName: string,
496
+ ): void {
497
+ if (!ts.isIdentifier(element.name)) return;
498
+ const propertyName = element.propertyName
499
+ && ts.isIdentifier(element.propertyName)
500
+ ? element.propertyName.text
501
+ : element.name.text;
502
+ const helper = this.input.classHelpers.find((row) =>
503
+ row.helperName === helperName && row.propertyName === propertyName);
504
+ if (!helper) return;
505
+ this.out.push(bindingAtSite({
506
+ variableName: element.name.text,
507
+ ...helper.fact,
508
+ sourceFile: this.sourceFile,
509
+ sourceLine: lineOf(this.input.source, declaration),
510
+ helperChain: [{
511
+ callerVariable: element.name.text,
512
+ className: helper.className,
513
+ classHelper: helper.helperName,
514
+ returnedProperty: helper.propertyName,
515
+ helperVariable: helper.variableName,
516
+ helperSourceFile: this.sourceFile,
517
+ helperSourceLine: helper.sourceLine,
518
+ }],
519
+ }, declaration, this.input.source));
520
+ }
521
+
522
+ private arrayElements(expression: ts.Expression): {
523
+ elements: ts.NodeArray<ts.Expression>;
524
+ promiseAll: boolean;
525
+ } | undefined {
526
+ const unwrapped = unwrapIdentityExpression(expression);
527
+ if (ts.isArrayLiteralExpression(unwrapped))
528
+ return { elements: unwrapped.elements, promiseAll: false };
529
+ const call = unwrapCall(expression);
530
+ if (!call || !ts.isPropertyAccessExpression(call.expression)
531
+ || call.expression.name.text !== 'all'
532
+ || call.expression.expression.getText(this.input.source) !== 'Promise')
533
+ return undefined;
534
+ const first = call.arguments[0];
535
+ const container = first ? unwrapIdentityExpression(first) : undefined;
536
+ return container && ts.isArrayLiteralExpression(container)
537
+ ? { elements: container.elements, promiseAll: true }
538
+ : undefined;
539
+ }
540
+
541
+ private appendArrayStep(
542
+ fact: ServiceBindingFact,
543
+ targetName: string,
544
+ arrayIndex: number,
545
+ promiseAll: boolean,
546
+ sourceVariable?: string,
547
+ ): void {
548
+ fact.helperChain = [...(fact.helperChain ?? []), {
549
+ callerVariable: targetName,
550
+ targetVariable: targetName,
551
+ ...(sourceVariable ? {
552
+ sourceVariable,
553
+ aliasKind: 'array-destructuring',
554
+ } : {}),
555
+ arrayIndex,
556
+ promiseAll,
557
+ arrayContainer: promiseAll ? 'Promise.all' : 'array_literal',
558
+ }];
559
+ }
560
+
561
+ private async recordArrayElement(
562
+ targetName: string,
563
+ expression: ts.Expression,
564
+ node: ts.Node,
565
+ arrayIndex: number,
566
+ promiseAll: boolean,
567
+ ): Promise<void> {
568
+ const before = this.out.length;
569
+ await this.recordExpression(targetName, expression, node, 'declaration');
570
+ const direct = this.out.length > before ? this.out.at(-1) : undefined;
571
+ if (direct) {
572
+ this.appendArrayStep(direct, targetName, arrayIndex, promiseAll);
573
+ return;
574
+ }
575
+ const unwrapped = unwrapIdentityExpression(expression);
576
+ if (!ts.isIdentifier(unwrapped)) return;
577
+ const existing = this.bindingForVariable(unwrapped.text, node);
578
+ if (!existing) return;
579
+ const fact = bindingAtSite({
580
+ ...existing,
581
+ variableName: targetName,
582
+ sourceLine: lineOf(this.input.source, node),
583
+ }, node, this.input.source);
584
+ this.appendArrayStep(
585
+ fact, targetName, arrayIndex, promiseAll, unwrapped.text,
586
+ );
587
+ this.out.push(fact);
588
+ }
589
+
590
+ private async recordArrayDeclaration(
591
+ declaration: ts.VariableDeclaration,
592
+ ): Promise<void> {
593
+ if (!ts.isArrayBindingPattern(declaration.name)
594
+ || !declaration.initializer) return;
595
+ const container = this.arrayElements(declaration.initializer);
596
+ if (!container) return;
597
+ for (let index = 0; index < declaration.name.elements.length; index += 1) {
598
+ const targetName = arrayBindingName(
599
+ declaration.name.elements[index],
600
+ );
601
+ const expression = container.elements[index];
602
+ if (targetName && expression && !ts.isOmittedExpression(expression))
603
+ await this.recordArrayElement(
604
+ targetName, expression, declaration,
605
+ index, container.promiseAll,
606
+ );
607
+ }
608
+ }
609
+
610
+ private async recordArrayAssignment(
611
+ pattern: ts.ArrayLiteralExpression,
612
+ expression: ts.Expression,
613
+ node: ts.Node,
614
+ ): Promise<void> {
615
+ const container = this.arrayElements(expression);
616
+ if (!container) return;
617
+ for (let index = 0; index < pattern.elements.length; index += 1) {
618
+ const targetName = arrayAssignmentName(pattern.elements[index]);
619
+ const source = container.elements[index];
620
+ if (!targetName || !source || ts.isOmittedExpression(source)) continue;
621
+ await this.recordArrayElement(
622
+ targetName, source, node, index, container.promiseAll,
623
+ );
624
+ }
625
+ }
626
+
627
+ private async recordDeclaration(
628
+ declaration: ts.VariableDeclaration,
629
+ ): Promise<void> {
630
+ await this.recordDestructuredHelper(declaration);
631
+ await this.recordArrayDeclaration(declaration);
632
+ this.recordClassHelper(declaration);
633
+ await this.rememberObjectHelper(declaration);
634
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
635
+ this.recordObjectProperty(
636
+ declaration.name.text, declaration.initializer, declaration,
637
+ );
638
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
639
+ await this.recordExpression(
640
+ declaration.name.text, declaration.initializer,
641
+ declaration, 'declaration',
642
+ );
643
+ this.recordIdentityAlias(declaration);
644
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
645
+ const source = transactionReceiverName(declaration.initializer);
646
+ if (source)
647
+ this.cloneAlias(
648
+ declaration.name.text, source, 'transaction', declaration,
649
+ );
650
+ }
651
+
652
+ private async recordIdentifierAssignment(
653
+ assignment: ts.BinaryExpression,
654
+ ): Promise<void> {
655
+ if (!ts.isIdentifier(assignment.left)) return;
656
+ const right = unwrapIdentityExpression(assignment.right);
657
+ if (ts.isIdentifier(right)) {
658
+ this.cloneAlias(
659
+ assignment.left.text, right.text,
660
+ 'identity-assignment', assignment,
661
+ );
662
+ return;
663
+ }
664
+ if (this.recordObjectProperty(
665
+ assignment.left.text, assignment.right, assignment,
666
+ )) return;
667
+ await this.recordExpression(
668
+ assignment.left.text, assignment.right, assignment, 'assignment',
669
+ );
670
+ }
671
+
672
+ private async recordAssignment(assignment: ts.BinaryExpression): Promise<void> {
673
+ if (ts.isIdentifier(assignment.left)) {
674
+ await this.recordIdentifierAssignment(assignment);
675
+ return;
676
+ }
677
+ const left = ts.isParenthesizedExpression(assignment.left)
678
+ ? assignment.left.expression
679
+ : assignment.left;
680
+ if (ts.isObjectLiteralExpression(left))
681
+ await this.recordDestructuredAssignment(
682
+ left, assignment.right, assignment,
683
+ );
684
+ if (ts.isArrayLiteralExpression(left))
685
+ await this.recordArrayAssignment(left, assignment.right, assignment);
686
+ }
687
+ }
688
+
689
+ export async function collectServiceBindings(
690
+ input: ServiceBindingCollectorInput,
691
+ ): Promise<ServiceBindingFact[]> {
692
+ return new ServiceBindingCollector(input).collect();
693
+ }