@saptools/service-flow 0.1.67 → 0.1.69

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