@saptools/service-flow 0.1.68 → 0.1.70

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 (124) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +13 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-GSLFY6J2.js} +6986 -5503
  5. package/dist/chunk-GSLFY6J2.js.map +1 -0
  6. package/dist/cli.js +334 -122
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/{002-doctor-lifecycle.ts → doctor-lifecycle.ts} +1 -1
  12. package/src/cli/{001-doctor-projection.ts → doctor-projection.ts} +1 -1
  13. package/src/cli/doctor.ts +5 -5
  14. package/src/cli/index-summary.ts +22 -0
  15. package/src/cli.ts +155 -91
  16. package/src/db/{009-binding-fact-semantics.ts → binding-fact-semantics.ts} +6 -1
  17. package/src/db/binding-helper-provenance.ts +17 -0
  18. package/src/db/{012-binding-reference-proof.ts → binding-reference-proof.ts} +4 -0
  19. package/src/db/{000-call-fact-repository.ts → call-fact-repository.ts} +49 -23
  20. package/src/db/{003-current-fact-semantics.ts → current-fact-semantics.ts} +5 -4
  21. package/src/db/{001-fact-lifecycle.ts → fact-lifecycle.ts} +3 -3
  22. package/src/db/index-publication-failure.ts +91 -0
  23. package/src/db/{007-package-fact-semantics.ts → package-fact-semantics.ts} +6 -6
  24. package/src/db/{010-package-symbol-surface-semantics.ts → package-symbol-surface-semantics.ts} +2 -2
  25. package/src/db/{004-package-target-invalidation.ts → package-target-invalidation.ts} +11 -1
  26. package/src/db/{008-relative-fact-semantics.ts → relative-fact-semantics.ts} +9 -6
  27. package/src/db/{006-relative-symbol-resolution.ts → relative-symbol-resolution.ts} +28 -7
  28. package/src/db/repositories.ts +24 -7
  29. package/src/db/{011-symbol-call-semantics.ts → symbol-call-semantics.ts} +1 -1
  30. package/src/index.ts +3 -3
  31. package/src/indexer/cds-extension-resolver.ts +27 -3
  32. package/src/indexer/repository-indexer.ts +70 -9
  33. package/src/indexer/workspace-indexer.ts +142 -30
  34. package/src/linker/call-edge-insertion.ts +568 -0
  35. package/src/linker/{002-call-evidence.ts → call-evidence.ts} +2 -2
  36. package/src/linker/cross-repo-linker.ts +6 -169
  37. package/src/linker/dynamic-edge-resolver.ts +1 -1
  38. package/src/linker/{004-event-subscription-handler-linker.ts → event-subscription-handler-linker.ts} +32 -11
  39. package/src/linker/event-template-link.ts +72 -0
  40. package/src/linker/external-http-target.ts +1 -1
  41. package/src/linker/helper-package-linker.ts +1 -1
  42. package/src/linker/{000-implementation-candidates.ts → implementation-candidates.ts} +1 -1
  43. package/src/linker/{001-implementation-evidence-projection.ts → implementation-evidence-projection.ts} +1 -1
  44. package/src/linker/odata-path-normalizer.ts +1 -1
  45. package/src/linker/{005-odata-path-structure.ts → odata-path-structure.ts} +1 -1
  46. package/src/linker/{003-package-import-symbol-resolver.ts → package-import-symbol-resolver.ts} +3 -3
  47. package/src/linker/service-resolver.ts +2 -2
  48. package/src/output/{001-compact-json-output.ts → compact-json-output.ts} +1 -1
  49. package/src/parsers/{006-binding-identity.ts → binding-identity.ts} +9 -4
  50. package/src/parsers/{011-binding-lexical-scope.ts → binding-lexical-scope.ts} +1 -1
  51. package/src/parsers/{021-binding-visibility.ts → binding-visibility.ts} +18 -2
  52. package/src/parsers/{000-direct-query-execution.ts → direct-query-execution.ts} +11 -0
  53. package/src/parsers/{005-event-subscription-facts.ts → event-subscription-facts.ts} +2 -2
  54. package/src/parsers/{016-local-symbol-reference.ts → local-symbol-reference.ts} +2 -2
  55. package/src/parsers/operation-path-analysis.ts +1 -1
  56. package/src/parsers/outbound-call-classifier.ts +692 -0
  57. package/src/parsers/outbound-call-parser.ts +146 -509
  58. package/src/parsers/outbound-expression-analysis.ts +700 -0
  59. package/src/parsers/{018-package-commonjs-syntax.ts → package-commonjs-syntax.ts} +1 -1
  60. package/src/parsers/{012-package-fact-contract.ts → package-fact-contract.ts} +3 -3
  61. package/src/parsers/{010-package-public-surface-analysis.ts → package-public-surface-analysis.ts} +2 -2
  62. package/src/parsers/{003-package-public-surface.ts → package-public-surface.ts} +3 -3
  63. package/src/parsers/{008-package-surface-publication.ts → package-surface-publication.ts} +1 -1
  64. package/src/parsers/{001-query-entity-resolution.ts → query-entity-resolution.ts} +1 -1
  65. package/src/parsers/{015-service-binding-collector.ts → service-binding-collector.ts} +3 -3
  66. package/src/parsers/{014-service-binding-helper-flow.ts → service-binding-helper-flow.ts} +72 -6
  67. package/src/parsers/service-binding-parser-helpers.ts +1 -1
  68. package/src/parsers/service-binding-parser.ts +2 -2
  69. package/src/parsers/{007-source-fact-reconciliation.ts → source-fact-reconciliation.ts} +3 -3
  70. package/src/parsers/{020-stable-local-value.ts → stable-local-value.ts} +2 -2
  71. package/src/parsers/{009-symbol-call-facts.ts → symbol-call-facts.ts} +3 -3
  72. package/src/parsers/{017-symbol-derived-contexts.ts → symbol-derived-contexts.ts} +2 -2
  73. package/src/parsers/symbol-parser.ts +43 -11
  74. package/src/trace/{014-compact-contract.ts → compact-contract.ts} +6 -0
  75. package/src/trace/{021-compact-decision-normalization.ts → compact-decision-normalization.ts} +77 -11
  76. package/src/trace/{020-compact-field-projection.ts → compact-field-projection.ts} +77 -4
  77. package/src/trace/{024-compact-observation-decision.ts → compact-observation-decision.ts} +10 -5
  78. package/src/trace/{016-compact-projector.ts → compact-projector.ts} +6 -7
  79. package/src/trace/{018-compact-trace.ts → compact-trace.ts} +3 -3
  80. package/src/trace/{006-contextual-projection.ts → contextual-projection.ts} +1 -1
  81. package/src/trace/{008-contextual-runtime-state.ts → contextual-runtime-state.ts} +1 -1
  82. package/src/trace/{001-dynamic-identity.ts → dynamic-identity.ts} +2 -2
  83. package/src/trace/{003-dynamic-references.ts → dynamic-references.ts} +2 -2
  84. package/src/trace/dynamic-targets.ts +6 -6
  85. package/src/trace/event-runtime-resolution.ts +151 -0
  86. package/src/trace/{011-event-subscriber-traversal.ts → event-subscriber-traversal.ts} +101 -9
  87. package/src/trace/evidence.ts +2 -2
  88. package/src/trace/implementation-hints.ts +10 -7
  89. package/src/trace/{005-implementation-selection.ts → implementation-selection.ts} +2 -2
  90. package/src/trace/{007-implementation-start-diagnostic.ts → implementation-start-diagnostic.ts} +1 -0
  91. package/src/trace/local-call-expansion.ts +37 -0
  92. package/src/trace/{009-selected-handler-provenance.ts → selected-handler-provenance.ts} +1 -1
  93. package/src/trace/selectors.ts +2 -1
  94. package/src/trace/{017-trace-context.ts → trace-context.ts} +2 -2
  95. package/src/trace/{015-trace-edge-recorder.ts → trace-edge-recorder.ts} +62 -5
  96. package/src/trace/{019-trace-edge-semantics.ts → trace-edge-semantics.ts} +9 -13
  97. package/src/trace/trace-engine.ts +10 -10
  98. package/src/trace/{022-trace-fact-preflight.ts → trace-fact-preflight.ts} +2 -2
  99. package/src/trace/{025-trace-implementation-scope.ts → trace-implementation-scope.ts} +1 -1
  100. package/src/trace/{028-trace-operation-execution.ts → trace-operation-execution.ts} +10 -10
  101. package/src/trace/{013-trace-root-scopes.ts → trace-root-scopes.ts} +5 -6
  102. package/src/trace/{027-trace-scope-execution.ts → trace-scope-execution.ts} +44 -44
  103. package/src/trace/{029-trace-start-implementation.ts → trace-start-implementation.ts} +7 -7
  104. package/src/trace/{026-trace-start-scope.ts → trace-start-scope.ts} +6 -5
  105. package/src/trace/{010-traversal-scope.ts → traversal-scope.ts} +1 -1
  106. package/src/types.ts +2 -1
  107. package/src/version.ts +1 -1
  108. package/dist/chunk-AEM4JY22.js.map +0 -1
  109. /package/src/cli/{000-clean.ts → clean.ts} +0 -0
  110. /package/src/cli/{003-doctor-package-resolution.ts → doctor-package-resolution.ts} +0 -0
  111. /package/src/db/{002-fact-json-inventory.ts → fact-json-inventory.ts} +0 -0
  112. /package/src/db/{005-schema-structure.ts → schema-structure.ts} +0 -0
  113. /package/src/output/{000-stdout-policy.ts → stdout-policy.ts} +0 -0
  114. /package/src/parsers/{019-binding-assignment-targets.ts → binding-assignment-targets.ts} +0 -0
  115. /package/src/parsers/{013-executable-body-eligibility.ts → executable-body-eligibility.ts} +0 -0
  116. /package/src/parsers/{004-fact-identity.ts → fact-identity.ts} +0 -0
  117. /package/src/parsers/{002-symbol-import-bindings.ts → symbol-import-bindings.ts} +0 -0
  118. /package/src/trace/{004-dynamic-candidate-sources.ts → dynamic-candidate-sources.ts} +0 -0
  119. /package/src/trace/{000-dynamic-target-types.ts → dynamic-target-types.ts} +0 -0
  120. /package/src/trace/{023-nested-event-scopes.ts → nested-event-scopes.ts} +0 -0
  121. /package/src/trace/{002-trace-diagnostics.ts → trace-diagnostics.ts} +0 -0
  122. /package/src/trace/{012-trace-graph-lookups.ts → trace-graph-lookups.ts} +0 -0
  123. /package/src/utils/{000-bounded-projection.ts → bounded-projection.ts} +0 -0
  124. /package/src/utils/{001-placeholders.ts → placeholders.ts} +0 -0
@@ -0,0 +1,700 @@
1
+ import ts from 'typescript';
2
+ import {
3
+ chainIncludesForUpdate,
4
+ queryBuilderRoot,
5
+ type DirectQueryBuilderStatement,
6
+ } from './direct-query-execution.js';
7
+ import {
8
+ expressionName,
9
+ maxAliasDepth,
10
+ resolveBinding,
11
+ } from './query-entity-resolution.js';
12
+ import type { OperationPathAnalysis } from './operation-path-analysis.js';
13
+ import { stripQuotes } from '../utils/path-utils.js';
14
+ export function lineOf(text: string, index: number): number {
15
+ return text.slice(0, index).split('\n').length;
16
+ }
17
+ export function queryBuilderEvidence(
18
+ source: ts.SourceFile,
19
+ statement: DirectQueryBuilderStatement,
20
+ ): Record<string, unknown> {
21
+ const hasForUpdate = chainIncludesForUpdate(statement.logicalCall);
22
+ return {
23
+ classifier: 'cap_query_builder_direct',
24
+ queryDispatch: 'direct_query_builder',
25
+ queryRoot: expressionName(statement.root.expression),
26
+ queryRootStartOffset: statement.root.getStart(source),
27
+ queryRootEndOffset: statement.root.getEnd(),
28
+ queryStatementStartOffset: statement.statement.getStart(source),
29
+ queryStatementEndOffset: statement.statement.getEnd(),
30
+ queryExecutionContext: statement.executionContext,
31
+ ...(hasForUpdate ? { hasForUpdate: true } : {}),
32
+ };
33
+ }
34
+ export function queryRunEvidence(
35
+ source: ts.SourceFile,
36
+ argument: ts.Expression | undefined,
37
+ ): Record<string, unknown> {
38
+ const root = argument ? queryBuilderRoot(argument) : undefined;
39
+ const hasForUpdate = argument
40
+ ? chainIncludesForUpdate(argument) : false;
41
+ return {
42
+ classifier: 'cap_query_run_wrapper',
43
+ queryDispatch: 'cds_run_wrapper',
44
+ ...(root ? {
45
+ queryRoot: expressionName(root.expression),
46
+ queryRootStartOffset: root.getStart(source),
47
+ queryRootEndOffset: root.getEnd(),
48
+ } : {}),
49
+ ...(hasForUpdate ? { hasForUpdate: true } : {}),
50
+ };
51
+ }
52
+ export function queryWarning(expression: string): string {
53
+ if (/^\s*[`'"]/.test(expression)) return 'raw_sql_or_cql_expression';
54
+ if (/^\s*\w+\s*$/.test(expression))
55
+ return 'query_variable_without_static_initializer';
56
+ return 'dynamic_entity_expression';
57
+ }
58
+
59
+ export function parserEvidence(
60
+ source: ts.SourceFile,
61
+ node: ts.CallExpression,
62
+ extra?: Record<string, unknown>,
63
+ ): Record<string, unknown> {
64
+ return {
65
+ parser: 'typescript_ast',
66
+ startOffset: node.getStart(source),
67
+ endOffset: node.getEnd(),
68
+ ...extra,
69
+ };
70
+ }
71
+
72
+ function isStringLike(
73
+ expression: ts.Expression | undefined,
74
+ ): expression is ts.StringLiteral | ts.NoSubstitutionTemplateLiteral {
75
+ return Boolean(expression && (
76
+ ts.isStringLiteral(expression)
77
+ || ts.isNoSubstitutionTemplateLiteral(expression)
78
+ ));
79
+ }
80
+
81
+ export function literalText(
82
+ expression: ts.Expression | undefined,
83
+ ): string | undefined {
84
+ return isStringLike(expression) ? expression.text : undefined;
85
+ }
86
+
87
+ export const CDS_LIFECYCLE_EVENTS = new Set([
88
+ 'bootstrap', 'loaded', 'connect', 'serving', 'served', 'listening',
89
+ 'shutdown',
90
+ ]);
91
+
92
+ export function objectPropertyIsShorthand(
93
+ object: ts.ObjectLiteralExpression,
94
+ key: string,
95
+ ): boolean {
96
+ return object.properties.some((property) =>
97
+ ts.isShorthandPropertyAssignment(property)
98
+ && property.name.text === key);
99
+ }
100
+
101
+ function nameOfProperty(name: ts.PropertyName): string | undefined {
102
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)
103
+ || ts.isNumericLiteral(name)) return name.text;
104
+ return undefined;
105
+ }
106
+
107
+ type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
108
+ type ExpressionSourceKind =
109
+ | 'string_literal'
110
+ | 'no_substitution_template'
111
+ | 'template_with_substitutions'
112
+ | 'const_alias'
113
+ | 'conditional_candidates'
114
+ | 'dynamic_expression';
115
+
116
+ export interface ExpressionResolution {
117
+ status: ExpressionStatus;
118
+ sourceKind: ExpressionSourceKind;
119
+ value?: string;
120
+ rawExpression?: string;
121
+ placeholderKeys: string[];
122
+ evidence: string[];
123
+ constName?: string;
124
+ }
125
+
126
+ function safeRaw(expression: ts.Expression): string | undefined {
127
+ if (ts.isStringLiteral(expression)
128
+ || ts.isNoSubstitutionTemplateLiteral(expression)
129
+ || ts.isIdentifier(expression)
130
+ || ts.isTemplateExpression(expression))
131
+ return expression.getText(expression.getSourceFile());
132
+ return undefined;
133
+ }
134
+
135
+ function staticResolution(
136
+ expression: ts.Expression,
137
+ ): ExpressionResolution | undefined {
138
+ if (ts.isStringLiteral(expression)) return {
139
+ status: 'static', sourceKind: 'string_literal',
140
+ value: expression.text, rawExpression: safeRaw(expression),
141
+ placeholderKeys: [], evidence: ['string_literal'],
142
+ };
143
+ if (!ts.isNoSubstitutionTemplateLiteral(expression)) return undefined;
144
+ return {
145
+ status: 'static', sourceKind: 'no_substitution_template',
146
+ value: expression.text, rawExpression: safeRaw(expression),
147
+ placeholderKeys: [], evidence: ['no_substitution_template'],
148
+ };
149
+ }
150
+
151
+ function templateResolution(
152
+ expression: ts.TemplateExpression,
153
+ policy: 'operation_path' | 'external' | 'literal',
154
+ ): ExpressionResolution {
155
+ const placeholderKeys = expression.templateSpans.map((span) =>
156
+ span.expression.getText(expression.getSourceFile()));
157
+ if (policy === 'operation_path') return {
158
+ status: 'dynamic',
159
+ sourceKind: 'template_with_substitutions',
160
+ value: stripQuotes(expression.getText(expression.getSourceFile())),
161
+ rawExpression: safeRaw(expression),
162
+ placeholderKeys,
163
+ evidence: ['operation_path_template_placeholders_retained'],
164
+ };
165
+ return {
166
+ status: 'dynamic',
167
+ sourceKind: 'template_with_substitutions',
168
+ placeholderKeys,
169
+ evidence: ['template_substitutions_not_static_external_target'],
170
+ };
171
+ }
172
+
173
+ function identifierResolution(
174
+ expression: ts.Identifier,
175
+ use: ts.Node,
176
+ policy: 'operation_path' | 'external' | 'literal',
177
+ depth: number,
178
+ seen: Set<ts.Node>,
179
+ ): ExpressionResolution {
180
+ if (depth >= maxAliasDepth) return {
181
+ status: 'unknown', sourceKind: 'const_alias',
182
+ rawExpression: safeRaw(expression), placeholderKeys: [],
183
+ evidence: ['alias_depth_exceeded'], constName: expression.text,
184
+ };
185
+ const binding = resolveBinding(expression, use);
186
+ if (!binding.declaration || !binding.initializer || !binding.immutable)
187
+ return {
188
+ status: 'dynamic', sourceKind: 'dynamic_expression',
189
+ rawExpression: safeRaw(expression), placeholderKeys: [],
190
+ evidence: binding.evidence, constName: expression.text,
191
+ };
192
+ if (seen.has(binding.declaration)) return {
193
+ status: 'unknown', sourceKind: 'const_alias',
194
+ rawExpression: safeRaw(expression), placeholderKeys: [],
195
+ evidence: ['alias_cycle_detected'], constName: expression.text,
196
+ };
197
+ seen.add(binding.declaration);
198
+ const resolved = resolveExpression(
199
+ binding.initializer, binding.declaration, policy, depth + 1, seen,
200
+ );
201
+ return {
202
+ ...resolved,
203
+ sourceKind: 'const_alias',
204
+ rawExpression: safeRaw(expression),
205
+ constName: expression.text,
206
+ evidence: [...binding.evidence, ...resolved.evidence],
207
+ };
208
+ }
209
+
210
+ export function resolveExpression(
211
+ expression: ts.Expression | undefined,
212
+ use: ts.Node,
213
+ policy: 'operation_path' | 'external' | 'literal',
214
+ depth = 0,
215
+ seen = new Set<ts.Node>(),
216
+ ): ExpressionResolution {
217
+ if (!expression) return {
218
+ status: 'unknown', sourceKind: 'dynamic_expression',
219
+ placeholderKeys: [], evidence: ['expression_missing'],
220
+ };
221
+ const literal = staticResolution(expression);
222
+ if (literal) return literal;
223
+ if (ts.isTemplateExpression(expression))
224
+ return templateResolution(expression, policy);
225
+ if (ts.isIdentifier(expression))
226
+ return identifierResolution(expression, use, policy, depth, seen);
227
+ return {
228
+ status: 'dynamic', sourceKind: 'dynamic_expression',
229
+ rawExpression: safeRaw(expression), placeholderKeys: [],
230
+ evidence: [
231
+ `unsupported_${ts.SyntaxKind[expression.kind] ?? 'expression'}`,
232
+ ],
233
+ };
234
+ }
235
+
236
+ function staticExpressionText(
237
+ expression: ts.Expression | undefined,
238
+ initializers: Map<string, ts.Expression>,
239
+ ): string | undefined {
240
+ if (!expression) return undefined;
241
+ if (isStringLike(expression)) return expression.text;
242
+ if (ts.isIdentifier(expression) && initializers.has(expression.text))
243
+ return staticExpressionText(initializers.get(expression.text), initializers);
244
+ return undefined;
245
+ }
246
+
247
+ function operationPathFromStatic(
248
+ text: string | undefined,
249
+ ): string | undefined {
250
+ return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
251
+ }
252
+
253
+ function destinationExpressionShape(
254
+ expression: ts.Expression | undefined,
255
+ ): string | undefined {
256
+ if (!expression) return undefined;
257
+ if (ts.isIdentifier(expression)) return 'identifier';
258
+ if (ts.isPropertyAccessExpression(expression)
259
+ || ts.isElementAccessExpression(expression)) return 'property_read';
260
+ if (ts.isCallExpression(expression)) return 'function_call';
261
+ if (ts.isConditionalExpression(expression)) return 'conditional';
262
+ if (ts.isBinaryExpression(expression)) return 'binary_expression';
263
+ if (ts.isTemplateExpression(expression)) return 'template_expression';
264
+ return ts.SyntaxKind[expression.kind] ?? 'expression';
265
+ }
266
+
267
+ function staticConditionalCandidates(
268
+ expression: ts.Expression | undefined,
269
+ initializers: Map<string, ts.Expression>,
270
+ ): string[] | undefined {
271
+ const resolved = expression && ts.isIdentifier(expression)
272
+ && initializers.has(expression.text)
273
+ ? initializers.get(expression.text) : expression;
274
+ if (!resolved || !ts.isConditionalExpression(resolved)) return undefined;
275
+ const left = staticExpressionText(resolved.whenTrue, initializers);
276
+ const right = staticExpressionText(resolved.whenFalse, initializers);
277
+ return left && right ? [...new Set([left, right])] : undefined;
278
+ }
279
+
280
+ export function propertyInitializer(
281
+ object: ts.ObjectLiteralExpression,
282
+ key: string,
283
+ ): ts.Expression | undefined {
284
+ for (const property of object.properties) {
285
+ if (ts.isPropertyAssignment(property)
286
+ && nameOfProperty(property.name) === key) return property.initializer;
287
+ if (ts.isShorthandPropertyAssignment(property)
288
+ && property.name.text === key) return property.name;
289
+ }
290
+ return undefined;
291
+ }
292
+
293
+ function httpMethodFromObject(
294
+ object: ts.ObjectLiteralExpression,
295
+ use: ts.Node,
296
+ ): string | undefined {
297
+ const text = resolveExpression(
298
+ propertyInitializer(object, 'method'), use, 'literal',
299
+ ).value;
300
+ return text ? stripQuotes(text).toUpperCase() : undefined;
301
+ }
302
+
303
+ export const supportedHttpMethods = new Set([
304
+ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD',
305
+ ]);
306
+
307
+ export function safeOperationName(
308
+ value: string | undefined,
309
+ ): string | undefined {
310
+ if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value))
311
+ return undefined;
312
+ return operationPathFromStatic(value);
313
+ }
314
+
315
+ export function wrapperSourceKind(sourceKind: string): string {
316
+ if (sourceKind.includes('const_alias')) return 'const';
317
+ if (sourceKind.includes('template')) return 'template';
318
+ if (sourceKind.includes('string_literal')) return 'literal';
319
+ return sourceKind.includes('conditional') ? 'ambiguous' : 'dynamic';
320
+ }
321
+
322
+ export function literalPathSource(
323
+ analysis: OperationPathAnalysis,
324
+ ): string | undefined {
325
+ if (analysis.status !== 'static') return undefined;
326
+ if (analysis.sourceKind.includes('const_alias'))
327
+ return 'same_scope_const_initializer';
328
+ if (analysis.sourceKind.includes('no_substitution_template'))
329
+ return 'template';
330
+ return analysis.sourceKind.includes('string_literal')
331
+ ? 'literal' : analysis.sourceKind;
332
+ }
333
+
334
+ export function legacyPathCandidates(
335
+ analysis: OperationPathAnalysis,
336
+ ): Record<string, unknown> | undefined {
337
+ if (analysis.candidateRawPaths.length < 2
338
+ && analysis.dynamicReassignments.length === 0) return undefined;
339
+ return {
340
+ candidatePaths: analysis.candidateRawPaths,
341
+ normalizedCandidateOperations: analysis.candidateNormalizedOperationPaths
342
+ .map((value) => value.replace(/^\//, '')),
343
+ candidateSourceKind: analysis.sourceKind,
344
+ candidateIdentifier: analysis.candidateIdentifier,
345
+ hasDynamicAssignments: analysis.dynamicReassignments.length > 0,
346
+ conservativeReason: analysis.dynamicReassignments.length > 0
347
+ ? 'dynamic_assignment_observed' : 'candidate_tie',
348
+ };
349
+ }
350
+
351
+ function hasTemplatePlaceholder(value: string): boolean {
352
+ return /\$\{|%7B|%7D/i.test(value);
353
+ }
354
+
355
+ function urlTargetFromExpression(
356
+ expression: ts.Expression | undefined,
357
+ use: ts.Node,
358
+ ): Record<string, unknown> {
359
+ const resolved = resolveExpression(expression, use, 'external');
360
+ if (resolved.status === 'static' && resolved.value
361
+ && !hasTemplatePlaceholder(resolved.value)) return {
362
+ kind: 'static_url', expression: resolved.value, dynamic: false,
363
+ sourceKind: resolved.sourceKind,
364
+ };
365
+ if (expression) return {
366
+ kind: 'url_expression', dynamic: true,
367
+ expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`,
368
+ expressionShape: resolved.sourceKind,
369
+ placeholderKeys: resolved.placeholderKeys,
370
+ };
371
+ return { kind: 'unknown', dynamic: false };
372
+ }
373
+
374
+ function destinationTargetFromExpression(
375
+ expression: ts.Expression | undefined,
376
+ use: ts.Node,
377
+ ): Record<string, unknown> | undefined {
378
+ const resolved = resolveExpression(expression, use, 'external');
379
+ if (resolved.status === 'static' && resolved.value
380
+ && !hasTemplatePlaceholder(resolved.value)) return {
381
+ kind: 'destination', expression: resolved.value, dynamic: false,
382
+ sourceKind: resolved.sourceKind,
383
+ };
384
+ const candidates = staticConditionalCandidates(
385
+ expression, new Map<string, ts.Expression>(),
386
+ );
387
+ if (candidates) return {
388
+ kind: 'destination', dynamic: true,
389
+ expressionShape: 'conditional', candidateLiterals: candidates,
390
+ };
391
+ const shape = destinationExpressionShape(expression);
392
+ return shape
393
+ ? { kind: 'destination', dynamic: true, expressionShape: shape }
394
+ : undefined;
395
+ }
396
+
397
+ export interface ExternalHttpEvidence {
398
+ method?: string;
399
+ externalTarget: Record<string, unknown>;
400
+ classifier: string;
401
+ sourceCallShape: string;
402
+ }
403
+
404
+ function destinationLookupEvidence(
405
+ node: ts.CallExpression,
406
+ ): ExternalHttpEvidence | undefined {
407
+ const object = node.arguments[0];
408
+ if (!object || !ts.isObjectLiteralExpression(object)) return undefined;
409
+ const destination = destinationTargetFromExpression(
410
+ propertyInitializer(object, 'destinationName'), node,
411
+ );
412
+ return {
413
+ externalTarget: destination ?? { kind: 'unknown', dynamic: false },
414
+ classifier: 'sap_destination_lookup',
415
+ sourceCallShape: 'useOrFetchDestination',
416
+ };
417
+ }
418
+
419
+ function executeHttpEvidence(node: ts.CallExpression): ExternalHttpEvidence {
420
+ const destination = destinationTargetFromExpression(node.arguments[0], node);
421
+ const config = node.arguments[1];
422
+ const object = config && ts.isObjectLiteralExpression(config)
423
+ ? config : undefined;
424
+ const method = object ? httpMethodFromObject(object, node) : undefined;
425
+ const url = object
426
+ ? urlTargetFromExpression(propertyInitializer(object, 'url'), node)
427
+ : { kind: 'unknown', dynamic: false };
428
+ return {
429
+ method,
430
+ externalTarget: destination ? { ...url, destination } : url,
431
+ classifier: 'sap_execute_http_request',
432
+ sourceCallShape: 'executeHttpRequest',
433
+ };
434
+ }
435
+
436
+ function axiosEvidence(node: ts.CallExpression): ExternalHttpEvidence {
437
+ const config = node.arguments[0];
438
+ if (!config || !ts.isObjectLiteralExpression(config)) return {
439
+ externalTarget: { kind: 'unknown', dynamic: false },
440
+ classifier: 'axios_unknown_call',
441
+ sourceCallShape: 'axios(...)',
442
+ };
443
+ return {
444
+ method: httpMethodFromObject(config, node),
445
+ externalTarget: urlTargetFromExpression(
446
+ propertyInitializer(config, 'url'), node,
447
+ ),
448
+ classifier: 'axios_config_call',
449
+ sourceCallShape: 'axios(config)',
450
+ };
451
+ }
452
+
453
+ function fetchEvidence(node: ts.CallExpression): ExternalHttpEvidence {
454
+ const init = node.arguments[1];
455
+ const object = init && ts.isObjectLiteralExpression(init) ? init : undefined;
456
+ return {
457
+ method: object ? httpMethodFromObject(object, node) : undefined,
458
+ externalTarget: urlTargetFromExpression(node.arguments[0], node),
459
+ classifier: 'fetch_call',
460
+ sourceCallShape: 'fetch',
461
+ };
462
+ }
463
+
464
+ function axiosMemberEvidence(
465
+ node: ts.CallExpression,
466
+ source: ts.SourceFile,
467
+ ): ExternalHttpEvidence | undefined {
468
+ const expression = node.expression;
469
+ if (!ts.isPropertyAccessExpression(expression)
470
+ || !['get', 'post', 'put', 'patch', 'delete', 'head']
471
+ .includes(expression.name.text)
472
+ || expression.expression.getText(source) !== 'axios') return undefined;
473
+ return {
474
+ method: expression.name.text.toUpperCase(),
475
+ externalTarget: urlTargetFromExpression(node.arguments[0], node),
476
+ classifier: 'axios_member_call',
477
+ sourceCallShape: `axios.${expression.name.text}`,
478
+ };
479
+ }
480
+
481
+ export function externalHttpEvidence(
482
+ node: ts.CallExpression,
483
+ source: ts.SourceFile,
484
+ ): ExternalHttpEvidence | undefined {
485
+ const expression = node.expression.getText(source);
486
+ if (expression === 'useOrFetchDestination')
487
+ return destinationLookupEvidence(node);
488
+ if (expression === 'executeHttpRequest') return executeHttpEvidence(node);
489
+ if (expression === 'axios') return axiosEvidence(node);
490
+ if (expression === 'fetch') return fetchEvidence(node);
491
+ return axiosMemberEvidence(node, source);
492
+ }
493
+
494
+ export function collectServiceVariables(source: ts.SourceFile): Set<string> {
495
+ const variables = new Set([
496
+ 'cds', 'messaging', 'messageClient', 'eventClient',
497
+ ]);
498
+ const visit = (node: ts.Node): void => {
499
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
500
+ && node.initializer
501
+ && /cds\.connect\.(to|messaging)\s*\(/.test(
502
+ node.initializer.getText(source),
503
+ )) variables.add(node.name.text);
504
+ ts.forEachChild(node, visit);
505
+ };
506
+ visit(source);
507
+ return variables;
508
+ }
509
+
510
+ export function receiverName(expression: ts.Expression): string | undefined {
511
+ if (ts.isIdentifier(expression)) return expression.text;
512
+ return ts.isPropertyAccessExpression(expression)
513
+ ? expression.getText(expression.getSourceFile()) : undefined;
514
+ }
515
+
516
+ export function rootReceiverName(
517
+ expression: ts.Expression,
518
+ ): string | undefined {
519
+ if (ts.isIdentifier(expression)) return expression.text;
520
+ if (ts.isPropertyAccessExpression(expression))
521
+ return rootReceiverName(expression.expression);
522
+ if (ts.isCallExpression(expression))
523
+ return rootReceiverName(expression.expression);
524
+ return undefined;
525
+ }
526
+
527
+ export function isSupportedEventReceiver(
528
+ receiver: string | undefined,
529
+ rootReceiver: string | undefined,
530
+ serviceVariables: Set<string>,
531
+ ): boolean {
532
+ const candidate = rootReceiver ?? receiver;
533
+ if (!candidate) return false;
534
+ if (candidate === 'cds' || serviceVariables.has(candidate)) return true;
535
+ if (receiver && serviceVariables.has(receiver)) return true;
536
+ return /^(srv|service|serviceClient|messaging|messageClient|eventClient)$/
537
+ .test(candidate);
538
+ }
539
+
540
+ export interface WrapperSpec {
541
+ clientIndex?: number;
542
+ clientName?: string;
543
+ pathIndex: number;
544
+ methodIndex?: number;
545
+ methodName?: string;
546
+ methodLiteral?: string;
547
+ nestedWrapperFunction?: string;
548
+ definitionLine: number;
549
+ internalStart: number;
550
+ internalEnd: number;
551
+ }
552
+
553
+ interface WrapperSend {
554
+ client: string;
555
+ path: string;
556
+ method?: string;
557
+ methodLiteral?: string;
558
+ nestedWrapperFunction?: string;
559
+ start: number;
560
+ end: number;
561
+ }
562
+
563
+ export function calledWrapperNames(source: ts.SourceFile): Set<string> {
564
+ const names = new Set<string>();
565
+ const visit = (node: ts.Node): void => {
566
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression))
567
+ names.add(node.expression.text);
568
+ const expression = ts.isCallExpression(node) ? node.expression : undefined;
569
+ if (expression && ts.isCallExpression(expression)
570
+ && ts.isIdentifier(expression.expression))
571
+ names.add(expression.expression.text);
572
+ ts.forEachChild(node, visit);
573
+ };
574
+ visit(source);
575
+ return names;
576
+ }
577
+
578
+ function directWrapperSend(
579
+ node: ts.CallExpression,
580
+ source: ts.SourceFile,
581
+ ): WrapperSend | undefined {
582
+ const expression = node.expression;
583
+ if (!ts.isPropertyAccessExpression(expression)
584
+ || expression.name.text !== 'send'
585
+ || !ts.isIdentifier(expression.expression)) return undefined;
586
+ const object = node.arguments[0];
587
+ if (!object || !ts.isObjectLiteralExpression(object)) return undefined;
588
+ const path = propertyInitializer(object, 'path');
589
+ if (!path || !ts.isIdentifier(path)) return undefined;
590
+ const method = propertyInitializer(object, 'method');
591
+ return {
592
+ client: expression.expression.text,
593
+ path: path.text,
594
+ method: method && ts.isIdentifier(method) ? method.text : undefined,
595
+ methodLiteral: resolveExpression(method, node, 'literal').value,
596
+ start: node.getStart(source),
597
+ end: node.getEnd(),
598
+ };
599
+ }
600
+
601
+ function nestedWrapperSend(
602
+ node: ts.CallExpression,
603
+ source: ts.SourceFile,
604
+ specs: Map<string, WrapperSpec>,
605
+ ): WrapperSend | undefined {
606
+ if (!ts.isIdentifier(node.expression)) return undefined;
607
+ const nested = specs.get(node.expression.text);
608
+ if (!nested) return undefined;
609
+ const path = node.arguments[nested.pathIndex];
610
+ const client = nested.clientIndex === undefined
611
+ ? undefined : node.arguments[nested.clientIndex];
612
+ const pathName = path && ts.isIdentifier(path) ? path.text : undefined;
613
+ const clientName = client && ts.isIdentifier(client)
614
+ ? client.text : nested.clientName;
615
+ if (!pathName || !clientName) return undefined;
616
+ return {
617
+ client: clientName,
618
+ path: pathName,
619
+ method: nested.methodName,
620
+ methodLiteral: nested.methodLiteral,
621
+ nestedWrapperFunction: node.expression.text,
622
+ start: node.getStart(source),
623
+ end: node.getEnd(),
624
+ };
625
+ }
626
+
627
+ function wrapperSends(
628
+ fn: ts.FunctionLikeDeclaration,
629
+ source: ts.SourceFile,
630
+ specs: Map<string, WrapperSpec>,
631
+ ): WrapperSend[] {
632
+ const sends: WrapperSend[] = [];
633
+ const visit = (node: ts.Node): void => {
634
+ if (ts.isCallExpression(node)) {
635
+ const send = directWrapperSend(node, source)
636
+ ?? nestedWrapperSend(node, source, specs);
637
+ if (send) sends.push(send);
638
+ }
639
+ ts.forEachChild(node, visit);
640
+ };
641
+ visit(fn);
642
+ return sends;
643
+ }
644
+
645
+ function isExportedWrapper(fn: ts.FunctionLikeDeclaration): boolean {
646
+ const declaration = ts.isFunctionDeclaration(fn)
647
+ ? fn
648
+ : ts.isVariableDeclaration(fn.parent)
649
+ ? fn.parent.parent.parent : undefined;
650
+ if (!declaration || !ts.canHaveModifiers(declaration)) return false;
651
+ return ts.getModifiers(declaration)?.some((modifier) =>
652
+ modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
653
+ }
654
+
655
+ function optionalIndex(index: number): number | undefined {
656
+ return index >= 0 ? index : undefined;
657
+ }
658
+
659
+ function knownWrapperClient(
660
+ client: string,
661
+ serviceVariables: Set<string>,
662
+ ): boolean {
663
+ if (serviceVariables.has(client)) return true;
664
+ return /^(srv|service|serviceClient|client|.*Client)$/.test(client);
665
+ }
666
+
667
+ export function wrapperSpec(
668
+ name: string,
669
+ fn: ts.FunctionLikeDeclaration,
670
+ source: ts.SourceFile,
671
+ calledNames: Set<string>,
672
+ serviceVariables: Set<string>,
673
+ specs: Map<string, WrapperSpec>,
674
+ ): WrapperSpec | undefined {
675
+ if (!calledNames.has(name) && !isExportedWrapper(fn)) return undefined;
676
+ const sends = wrapperSends(fn, source, specs);
677
+ if (sends.length !== 1) return undefined;
678
+ const found = sends[0];
679
+ if (!found) return undefined;
680
+ const parameters = fn.parameters.map((parameter) =>
681
+ ts.isIdentifier(parameter.name) ? parameter.name.text : undefined);
682
+ const clientIndex = parameters.indexOf(found.client);
683
+ const pathIndex = parameters.indexOf(found.path);
684
+ const methodIndex = parameters.indexOf(found.method ?? '');
685
+ if (pathIndex < 0) return undefined;
686
+ if (clientIndex < 0
687
+ && !knownWrapperClient(found.client, serviceVariables)) return undefined;
688
+ return {
689
+ clientIndex: optionalIndex(clientIndex),
690
+ clientName: clientIndex >= 0 ? undefined : found.client,
691
+ pathIndex,
692
+ methodIndex: optionalIndex(methodIndex),
693
+ methodName: found.method,
694
+ methodLiteral: found.methodLiteral,
695
+ nestedWrapperFunction: found.nestedWrapperFunction,
696
+ definitionLine: lineOf(source.text, fn.getStart(source)),
697
+ internalStart: found.start,
698
+ internalEnd: found.end,
699
+ };
700
+ }
@@ -1,7 +1,7 @@
1
1
  import ts from 'typescript';
2
2
  import {
3
3
  lexicalIdentifierDeclarations,
4
- } from './002-symbol-import-bindings.js';
4
+ } from './symbol-import-bindings.js';
5
5
 
6
6
  interface AccessPath {
7
7
  root: ts.Identifier;