@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
@@ -1,466 +1,37 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import ts from 'typescript';
4
- import { externalHttpTarget } from '../linker/external-http-target.js';
5
4
  import type { OutboundCallFact, ServiceBindingFact } from '../types.js';
6
- import { normalizePath, stripQuotes } from '../utils/path-utils.js';
7
- import { summarizeExpression } from '../utils/redaction.js';
8
- import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
9
- import { parseServiceBindings } from './service-binding-parser.js';
10
- import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
5
+ import { normalizePath } from '../utils/path-utils.js';
11
6
  import {
12
- directQueryBuilderStatement,
13
- queryBuilderRoot,
14
- type DirectQueryBuilderStatement,
15
- } from './000-direct-query-execution.js';
7
+ classifyOutboundCallsInSource,
8
+ type ClassifiedOutboundCall,
9
+ } from './outbound-call-classifier.js';
16
10
  import {
17
- expressionName,
18
- maxAliasDepth,
19
- queryEntityFromAst,
20
- resolveBinding,
21
- variableInitializers,
22
- } from './001-query-entity-resolution.js';
11
+ lineOf,
12
+ literalText,
13
+ parserEvidence,
14
+ } from './outbound-expression-analysis.js';
15
+ import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
16
+ import { parseServiceBindings } from './service-binding-parser.js';
23
17
  import type { RepositorySourceContext } from './ts-project.js';
24
- import {
25
- analyzeOperationPath,
26
- operationPathExpression,
27
- pathUnresolvedReason,
28
- type OperationPathAnalysis,
29
- } from './operation-path-analysis.js';
30
- function lineOf(text: string, idx: number): number {
31
- return text.slice(0, idx).split('\n').length;
32
- }
33
- function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuilderStatement): Record<string, unknown> {
34
- return {
35
- classifier: 'cap_query_builder_direct',
36
- queryDispatch: 'direct_query_builder',
37
- queryRoot: expressionName(statement.root.expression),
38
- queryRootStartOffset: statement.root.getStart(source),
39
- queryRootEndOffset: statement.root.getEnd(),
40
- queryStatementStartOffset: statement.statement.getStart(source),
41
- queryStatementEndOffset: statement.statement.getEnd(),
42
- queryExecutionContext: statement.executionContext,
43
- };
44
- }
45
- function queryRunEvidence(
46
- source: ts.SourceFile,
47
- argument: ts.Expression | undefined,
48
- ): Record<string, unknown> {
49
- const root = argument ? queryBuilderRoot(argument) : undefined;
50
- return {
51
- classifier: 'cap_query_run_wrapper',
52
- queryDispatch: 'cds_run_wrapper',
53
- ...(root ? {
54
- queryRoot: expressionName(root.expression),
55
- queryRootStartOffset: root.getStart(source),
56
- queryRootEndOffset: root.getEnd(),
57
- } : {}),
58
- };
59
- }
60
- function queryWarning(expr: string): string {
61
- if (/^\s*[`'"]/.test(expr)) return 'raw_sql_or_cql_expression';
62
- if (/^\s*\w+\s*$/.test(expr)) return 'query_variable_without_static_initializer';
63
- return 'dynamic_entity_expression';
64
- }
65
- export interface ClassifiedOutboundCall {
66
- fact: OutboundCallFact;
67
- node: ts.CallExpression;
68
- }
69
- function parserEvidence(source: ts.SourceFile, node: ts.CallExpression, extra?: Record<string, unknown>): Record<string, unknown> {
70
- return { parser: 'typescript_ast', startOffset: node.getStart(source), endOffset: node.getEnd(), ...extra };
71
- }
72
- function isStringLike(expr: ts.Expression | undefined): expr is ts.StringLiteral | ts.NoSubstitutionTemplateLiteral {
73
- return Boolean(expr && (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)));
74
- }
75
- function literalText(expr: ts.Expression | undefined): string | undefined {
76
- if (isStringLike(expr)) return expr.text;
77
- return undefined;
78
- }
79
- const CDS_LIFECYCLE_EVENTS = new Set([
80
- 'bootstrap', 'loaded', 'connect', 'serving', 'served', 'listening', 'shutdown',
81
- ]);
82
- function objectPropertyIsShorthand(object: ts.ObjectLiteralExpression, key: string): boolean {
83
- return object.properties.some((property) => ts.isShorthandPropertyAssignment(property) && property.name.text === key);
84
- }
85
- function nameOfProperty(name: ts.PropertyName): string | undefined {
86
- if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
87
- return undefined;
88
- }
89
- type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
90
- type ExpressionSourceKind = 'string_literal' | 'no_substitution_template' | 'template_with_substitutions' | 'const_alias' | 'conditional_candidates' | 'dynamic_expression';
91
- interface ExpressionResolution { status: ExpressionStatus; sourceKind: ExpressionSourceKind; value?: string; rawExpression?: string; placeholderKeys: string[]; evidence: string[]; constName?: string }
92
- function safeRaw(expr: ts.Expression): string | undefined {
93
- if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr) || ts.isIdentifier(expr) || ts.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
94
- return undefined;
95
- }
96
- function placeholders(expr: ts.TemplateExpression): string[] {
97
- return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
98
- }
99
- function resolveExpression(expr: ts.Expression | undefined, use: ts.Node, policy: 'operation_path' | 'external' | 'literal', depth = 0, seen = new Set<ts.Node>()): ExpressionResolution {
100
- if (!expr) return { status: 'unknown', sourceKind: 'dynamic_expression', placeholderKeys: [], evidence: ['expression_missing'] };
101
- if (ts.isStringLiteral(expr)) return { status: 'static', sourceKind: 'string_literal', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['string_literal'] };
102
- if (ts.isNoSubstitutionTemplateLiteral(expr)) return { status: 'static', sourceKind: 'no_substitution_template', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['no_substitution_template'] };
103
- if (ts.isTemplateExpression(expr)) {
104
- const keys = placeholders(expr);
105
- if (policy === 'operation_path') return { status: 'dynamic', sourceKind: 'template_with_substitutions', value: stripQuotes(expr.getText(expr.getSourceFile())), rawExpression: safeRaw(expr), placeholderKeys: keys, evidence: ['operation_path_template_placeholders_retained'] };
106
- return { status: 'dynamic', sourceKind: 'template_with_substitutions', placeholderKeys: keys, evidence: ['template_substitutions_not_static_external_target'] };
107
- }
108
- if (ts.isIdentifier(expr)) {
109
- if (depth >= maxAliasDepth) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_depth_exceeded'], constName: expr.text };
110
- const binding = resolveBinding(expr, use);
111
- if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
112
- if (seen.has(binding.declaration)) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_cycle_detected'], constName: expr.text };
113
- seen.add(binding.declaration);
114
- const resolved = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
115
- return { ...resolved, sourceKind: 'const_alias', rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved.evidence] };
116
- }
117
- return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts.SyntaxKind[expr.kind] ?? 'expression'}`] };
118
- }
119
- function staticExpressionText(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string | undefined {
120
- if (!expr) return undefined;
121
- if (isStringLike(expr)) return expr.text;
122
- if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
123
- return undefined;
124
- }
125
- function operationPathFromStatic(text: string | undefined): string | undefined {
126
- return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
127
- }
128
- function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
129
- if (!expr) return undefined;
130
- if (ts.isIdentifier(expr)) return 'identifier';
131
- if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) return 'property_read';
132
- if (ts.isCallExpression(expr)) return 'function_call';
133
- if (ts.isConditionalExpression(expr)) return 'conditional';
134
- if (ts.isBinaryExpression(expr)) return 'binary_expression';
135
- if (ts.isTemplateExpression(expr)) return 'template_expression';
136
- return ts.SyntaxKind[expr.kind] ?? 'expression';
137
- }
138
- function staticConditionalCandidates(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string[] | undefined {
139
- const resolved = expr && ts.isIdentifier(expr) && initializers.has(expr.text) ? initializers.get(expr.text) : expr;
140
- if (!resolved || !ts.isConditionalExpression(resolved)) return undefined;
141
- const left = staticExpressionText(resolved.whenTrue, initializers);
142
- const right = staticExpressionText(resolved.whenFalse, initializers);
143
- if (!left || !right) return undefined;
144
- return [...new Set([left, right])];
145
- }
146
- function propertyInitializer(object: ts.ObjectLiteralExpression, key: string): ts.Expression | undefined {
147
- for (const property of object.properties) {
148
- if (ts.isPropertyAssignment(property) && nameOfProperty(property.name) === key) return property.initializer;
149
- if (ts.isShorthandPropertyAssignment(property) && property.name.text === key) return property.name;
150
- }
151
- return undefined;
152
- }
153
- function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node): string | undefined {
154
- const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
155
- return text ? stripQuotes(text).toUpperCase() : undefined;
156
- }
157
- const supportedHttpMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']);
158
- function safeOperationName(value: string | undefined): string | undefined {
159
- if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return undefined;
160
- return operationPathFromStatic(value);
161
- }
162
- function wrapperSourceKind(sourceKind: string): string {
163
- if (sourceKind.includes('const_alias')) return 'const';
164
- if (sourceKind.includes('template')) return 'template';
165
- if (sourceKind.includes('string_literal')) return 'literal';
166
- return sourceKind.includes('conditional') ? 'ambiguous' : 'dynamic';
167
- }
168
- function literalPathSource(analysis: OperationPathAnalysis): string | undefined {
169
- if (analysis.status !== 'static') return undefined;
170
- if (analysis.sourceKind.includes('const_alias')) return 'same_scope_const_initializer';
171
- if (analysis.sourceKind.includes('no_substitution_template')) return 'template';
172
- return analysis.sourceKind.includes('string_literal') ? 'literal' : analysis.sourceKind;
173
- }
174
- function legacyPathCandidates(analysis: OperationPathAnalysis): Record<string, unknown> | undefined {
175
- if (analysis.candidateRawPaths.length < 2 && analysis.dynamicReassignments.length === 0)
176
- return undefined;
177
- return {
178
- candidatePaths: analysis.candidateRawPaths,
179
- normalizedCandidateOperations: analysis.candidateNormalizedOperationPaths
180
- .map((value) => value.replace(/^\//, '')),
181
- candidateSourceKind: analysis.sourceKind,
182
- candidateIdentifier: analysis.candidateIdentifier,
183
- hasDynamicAssignments: analysis.dynamicReassignments.length > 0,
184
- conservativeReason: analysis.dynamicReassignments.length > 0
185
- ? 'dynamic_assignment_observed'
186
- : 'candidate_tie',
187
- };
188
- }
189
- function hasTemplatePlaceholder(value: string): boolean { return /\$\{|%7B|%7D/i.test(value); }
190
- function urlTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> {
191
- const resolved = resolveExpression(expr, use, 'external');
192
- if (resolved.status === 'static' && resolved.value && !hasTemplatePlaceholder(resolved.value)) return { kind: 'static_url', expression: resolved.value, dynamic: false, sourceKind: resolved.sourceKind };
193
- if (expr) return { kind: 'url_expression', dynamic: true, expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`, expressionShape: resolved.sourceKind, placeholderKeys: resolved.placeholderKeys };
194
- return { kind: 'unknown', dynamic: false };
195
- }
196
- function destinationTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> | undefined {
197
- const resolved = resolveExpression(expr, use, 'external');
198
- const text = resolved.value;
199
- if (resolved.status === 'static' && text && !hasTemplatePlaceholder(text)) return { kind: 'destination', expression: text, dynamic: false, sourceKind: resolved.sourceKind };
200
- const candidates = staticConditionalCandidates(expr, new Map<string, ts.Expression>());
201
- if (candidates) return { kind: 'destination', dynamic: true, expressionShape: 'conditional', candidateLiterals: candidates };
202
- const shape = destinationExpressionShape(expr);
203
- if (shape) return { kind: 'destination', dynamic: true, expressionShape: shape };
204
- return undefined;
205
- }
206
- function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile): { method?: string; externalTarget: Record<string, unknown>; classifier: string; sourceCallShape: string } | undefined {
207
- const expr = node.expression;
208
- const exprText = expr.getText(source);
209
- if (exprText === 'useOrFetchDestination') {
210
- const objectArg = node.arguments[0];
211
- if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
212
- const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), node);
213
- return { externalTarget: destination ?? { kind: 'unknown', dynamic: false }, classifier: 'sap_destination_lookup', sourceCallShape: 'useOrFetchDestination' };
214
- }
215
- }
216
- if (exprText === 'executeHttpRequest') {
217
- const destination = destinationTargetFromExpression(node.arguments[0], node);
218
- const config = node.arguments[1];
219
- const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : undefined;
220
- const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'), node) : { kind: 'unknown', dynamic: false };
221
- return { method, externalTarget: destination ? { ...url, destination } : url, classifier: 'sap_execute_http_request', sourceCallShape: 'executeHttpRequest' };
222
- }
223
- if (exprText === 'axios') {
224
- const config = node.arguments[0];
225
- if (config && ts.isObjectLiteralExpression(config)) {
226
- const method = httpMethodFromObject(config, node);
227
- return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'), node), classifier: 'axios_config_call', sourceCallShape: 'axios(config)' };
228
- }
229
- return { externalTarget: { kind: 'unknown', dynamic: false }, classifier: 'axios_unknown_call', sourceCallShape: 'axios(...)' };
230
- }
231
- if (exprText === 'fetch') {
232
- const init = node.arguments[1];
233
- const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : undefined;
234
- return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'fetch_call', sourceCallShape: 'fetch' };
235
- }
236
- if (ts.isPropertyAccessExpression(expr) && ['get','post','put','patch','delete','head'].includes(expr.name.text) && expr.expression.getText(source) === 'axios') {
237
- return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'axios_member_call', sourceCallShape: `axios.${expr.name.text}` };
238
- }
239
- return undefined;
240
- }
241
- function collectServiceVariables(source: ts.SourceFile): Set<string> {
242
- const vars = new Set<string>(['cds', 'messaging', 'messageClient', 'eventClient']);
243
- const visit = (node: ts.Node): void => {
244
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
245
- const text = node.initializer.getText(source);
246
- if (/cds\.connect\.(to|messaging)\s*\(/.test(text)) vars.add(node.name.text);
247
- }
248
- ts.forEachChild(node, visit);
249
- };
250
- visit(source);
251
- return vars;
252
- }
253
- function receiverName(expr: ts.Expression): string | undefined {
254
- if (ts.isIdentifier(expr)) return expr.text;
255
- if (ts.isPropertyAccessExpression(expr)) return expr.getText(sourceOf(expr));
256
- return undefined;
257
- }
258
- function sourceOf(node: ts.Node): ts.SourceFile {
259
- return node.getSourceFile();
260
- }
261
- function rootReceiverName(expr: ts.Expression): string | undefined {
262
- if (ts.isIdentifier(expr)) return expr.text;
263
- if (ts.isPropertyAccessExpression(expr)) return rootReceiverName(expr.expression);
264
- if (ts.isCallExpression(expr)) return rootReceiverName(expr.expression);
265
- return undefined;
266
- }
267
- function isSupportedEventReceiver(receiver: string | undefined, rootReceiver: string | undefined, serviceVariables: Set<string>): boolean {
268
- const candidate = rootReceiver ?? receiver;
269
- if (!candidate) return false;
270
- if (candidate === 'cds') return true;
271
- if (serviceVariables.has(candidate)) return true;
272
- if (receiver && serviceVariables.has(receiver)) return true;
273
- if (/^(srv|service|serviceClient|messaging|messageClient|eventClient)$/.test(candidate)) return true;
274
- return false;
275
- }
276
- interface WrapperSpec { clientIndex?: number; clientName?: string; pathIndex: number; methodIndex?: number; methodName?: string; methodLiteral?: string; nestedWrapperFunction?: string; definitionLine: number; internalStart: number; internalEnd: number }
277
- function collectWrapperSpecs(source: ts.SourceFile): Map<string, WrapperSpec> {
278
- const specs = new Map<string, WrapperSpec>();
279
- const serviceVariables = collectServiceVariables(source);
280
- const calledNames = new Set<string>();
281
- const collectCalls = (node: ts.Node): void => {
282
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression))
283
- calledNames.add(node.expression.text);
284
- if (ts.isCallExpression(node) && ts.isCallExpression(node.expression)
285
- && ts.isIdentifier(node.expression.expression))
286
- calledNames.add(node.expression.expression.text);
287
- ts.forEachChild(node, collectCalls);
288
- };
289
- collectCalls(source);
290
- const scanFunction = (name: string, fn: ts.FunctionLikeDeclaration): void => {
291
- if (!calledNames.has(name) && !isExportedWrapper(fn)) return;
292
- const params = fn.parameters.map((param) => ts.isIdentifier(param.name) ? param.name.text : undefined);
293
- const sends: Array<{ client: string; path: string; method?: string; methodLiteral?: string; nestedWrapperFunction?: string; start: number; end: number }> = [];
294
- const visit = (node: ts.Node): void => {
295
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'send' && ts.isIdentifier(node.expression.expression)) {
296
- const objectArg = node.arguments[0];
297
- if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
298
- const pathProp = propertyInitializer(objectArg, 'path');
299
- const methodProp = propertyInitializer(objectArg, 'method');
300
- const pathName = pathProp && ts.isIdentifier(pathProp) ? pathProp.text : undefined;
301
- const methodName = methodProp && ts.isIdentifier(methodProp) ? methodProp.text : undefined;
302
- const methodLiteral = resolveExpression(methodProp, node, 'literal').value;
303
- if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
304
- }
305
- }
306
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && specs.has(node.expression.text)) {
307
- const nested = specs.get(node.expression.text);
308
- const pathArg = nested ? node.arguments[nested.pathIndex] : undefined;
309
- const clientArg = nested?.clientIndex === undefined ? undefined : node.arguments[nested.clientIndex];
310
- const pathName = pathArg && ts.isIdentifier(pathArg) ? pathArg.text : undefined;
311
- const clientName = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : nested?.clientName;
312
- if (nested && pathName && clientName) sends.push({ client: clientName, path: pathName, method: nested.methodName, methodLiteral: nested.methodLiteral, nestedWrapperFunction: node.expression.text, start: node.getStart(source), end: node.getEnd() });
313
- }
314
- ts.forEachChild(node, visit);
315
- };
316
- visit(fn);
317
- if (sends.length !== 1) return;
318
- const found = sends[0];
319
- const clientIndex = params.indexOf(found.client);
320
- const pathIndex = params.indexOf(found.path);
321
- const methodIndex = found.method ? params.indexOf(found.method) : -1;
322
- const capturesKnownClient = serviceVariables.has(found.client) || /^(srv|service|serviceClient|client|.*Client)$/.test(found.client);
323
- if (pathIndex >= 0 && (clientIndex >= 0 || capturesKnownClient)) specs.set(name, { clientIndex: clientIndex >= 0 ? clientIndex : undefined, clientName: clientIndex >= 0 ? undefined : found.client, pathIndex, methodIndex: methodIndex >= 0 ? methodIndex : undefined, methodName: found.method, methodLiteral: found.methodLiteral, nestedWrapperFunction: found.nestedWrapperFunction, definitionLine: lineOf(source.text, fn.getStart(source)), internalStart: found.start, internalEnd: found.end });
324
- };
325
- const visitTop = (node: ts.Node): void => {
326
- if (ts.isFunctionDeclaration(node) && node.name) scanFunction(node.name.text, node);
327
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) scanFunction(node.name.text, node.initializer);
328
- ts.forEachChild(node, visitTop);
329
- };
330
- visitTop(source);
331
- return specs;
332
- }
333
- function isExportedWrapper(fn: ts.FunctionLikeDeclaration): boolean {
334
- const declaration = ts.isFunctionDeclaration(fn)
335
- ? fn
336
- : ts.isVariableDeclaration(fn.parent)
337
- ? fn.parent.parent.parent
338
- : undefined;
339
- if (!declaration || !ts.canHaveModifiers(declaration)) return false;
340
- return ts.getModifiers(declaration)?.some((modifier) =>
341
- modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
342
- }
343
- export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: string): ClassifiedOutboundCall[] {
344
- const calls: ClassifiedOutboundCall[] = [];
345
- const sourceFile = normalizePath(filePath);
346
- const initializers = variableInitializers(source);
347
- const serviceVariables = collectServiceVariables(source);
348
- const wrapperSpecs = collectWrapperSpecs(source);
349
- const wrapperInternalRanges = [...wrapperSpecs.values()].map((spec) => ({ start: spec.internalStart, end: spec.internalEnd }));
350
- const add = (node: ts.CallExpression, fact: Omit<OutboundCallFact, 'sourceFile' | 'sourceLine' | 'confidence'> & { confidence?: number }, extra?: Record<string, unknown>): void => {
351
- calls.push({ node, fact: {
352
- ...fact,
353
- sourceFile,
354
- sourceLine: lineOf(source.text, node.getStart(source)),
355
- callSiteStartOffset: node.getStart(source),
356
- callSiteEndOffset: node.getEnd(),
357
- confidence: fact.confidence ?? 0.8,
358
- evidence: parserEvidence(source, node, extra),
359
- } });
360
- };
361
- const visit = (node: ts.Node): void => {
362
- if (ts.isCallExpression(node)) {
363
- if (wrapperInternalRanges.some((range) => node.getStart(source) >= range.start && node.getEnd() <= range.end)) {
364
- return;
365
- }
366
- const expr = node.expression;
367
- const exprText = expr.getText(source);
368
- const directQuery = directQueryBuilderStatement(node);
369
- if (exprText === 'cds.run') {
370
- const arg = node.arguments[0];
371
- const entity = arg ? queryEntityFromAst(arg, initializers) : undefined;
372
- const payload = arg?.getText(source) ?? '';
373
- add(node, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) }, queryRunEvidence(source, arg));
374
- } else if (directQuery) {
375
- const entity = queryEntityFromAst(directQuery.logicalCall, initializers);
376
- const payload = directQuery.logicalCall.getText(source);
377
- add(directQuery.logicalCall, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) }, queryBuilderEvidence(source, directQuery));
378
- } else if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'send' && (ts.isIdentifier(expr.expression) || ts.isPropertyAccessExpression(expr.expression))) {
379
- const objectArg = node.arguments[0];
380
- if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
381
- const receiver = receiverName(expr.expression);
382
- const queryExpression = propertyInitializer(objectArg, 'query');
383
- const methodExpression = propertyInitializer(objectArg, 'method');
384
- const methodResolution = resolveExpression(methodExpression, node, 'literal');
385
- const method = stripQuotes(methodResolution.value ?? 'POST');
386
- const dynamicMethodDefaulted = Boolean(methodExpression && methodResolution.value === undefined);
387
- const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
388
- const pathAnalysis = analyzeOperationPath(pathExpr, node, method);
389
- const op = pathExpr ? operationPathExpression(pathAnalysis) ?? pathExpr.getText(source) : undefined;
390
- const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
391
- const operationPathExpr = operationPathExpression(pathAnalysis);
392
- const intent = classifyODataPathIntent(operationPathExpr, method);
393
- const entityCallTypes: Record<string, OutboundCallFact['callType']> = { entity_mutation: 'remote_entity_mutation', entity_delete: 'remote_entity_delete', entity_media: 'remote_entity_media', entity_candidate: 'remote_entity_candidate' };
394
- const entityCallType = entityCallTypes[intent.kind];
395
- const isODataQueryRead = method.toUpperCase() === 'GET' && ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
396
- const queryEntity = queryExpression
397
- ? queryEntityFromAst(queryExpression, initializers)
398
- : isODataQueryRead ? intent.entitySegment : undefined;
399
- const unresolvedReason = queryExpression
400
- ? queryEntity ? undefined : queryWarning(queryExpression.getText(source))
401
- : pathExpr ? pathUnresolvedReason(pathAnalysis) : undefined;
402
- add(node, { callType: queryExpression ? 'remote_query' : entityCallType ?? (isODataQueryRead ? 'remote_query' : 'remote_action'), serviceVariableName: receiver, method, operationPathExpr, queryEntity, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || queryExpression ? 0.8 : 0.4, unresolvedReason }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : undefined, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason, ...(dynamicMethodDefaulted ? { dynamicMethodDefaulted: true } : {}) });
403
- } else {
404
- const receiver = receiverName(expr.expression);
405
- const rootReceiver = rootReceiverName(expr.expression);
406
- const firstArg = resolveExpression(node.arguments[0], node, 'literal');
407
- const method = firstArg.value?.toUpperCase();
408
- const pathArg = node.arguments[1];
409
- const supported = method && supportedHttpMethods.has(method);
410
- if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
411
- const pathAnalysis = analyzeOperationPath(pathArg, node, method);
412
- const operationPathExpr = operationPathExpression(pathAnalysis);
413
- const intent = classifyODataPathIntent(operationPathExpr, method);
414
- const unresolvedReason = pathUnresolvedReason(pathAnalysis);
415
- add(node, { callType: 'remote_action', serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason }, { receiver, rootReceiver, classifier: 'service_client_send_method_path', rawPathExpression: pathAnalysis.rawExpression, literalPathSource: literalPathSource(pathAnalysis), odataPathIntent: operationPathExpr ? intent : undefined, pathAnalysis, staticPathCandidates: legacyPathCandidates(pathAnalysis), parserWarning: unresolvedReason });
416
- } else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
417
- const operationPathExpr = safeOperationName(firstArg.value);
418
- add(node, { callType: 'remote_action', serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? undefined : 'unsupported_cap_send_signature' }, { receiver, rootReceiver, classifier: operationPathExpr ? 'service_client_send_operation_event' : 'service_client_send_unsupported_signature', rawOperationExpression: firstArg.rawExpression, literalOperationSource: firstArg.value ? firstArg.sourceKind : undefined, parserWarning: operationPathExpr ? undefined : 'unsupported_cap_send_signature' });
419
- }
420
- }
421
- } else if (((ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) || (ts.isIdentifier(expr) && wrapperSpecs.has(expr.text)))) {
422
- const wrapperName = ts.isIdentifier(expr) ? expr.text : ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) ? expr.expression.text : '';
423
- const wrapperArgs = ts.isIdentifier(expr) ? node.arguments : ts.isCallExpression(expr) ? expr.arguments : node.arguments;
424
- const spec = wrapperSpecs.get(wrapperName);
425
- const clientArg = spec?.clientIndex === undefined ? undefined : wrapperArgs[spec.clientIndex];
426
- const pathArg = spec ? wrapperArgs[spec.pathIndex] : undefined;
427
- const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
428
- const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
429
- const method = stripQuotes(resolveExpression(methodArg, node, 'literal').value ?? spec?.methodLiteral ?? 'POST');
430
- const pathAnalysis = analyzeOperationPath(pathArg, node, method);
431
- const operationPathExpr = operationPathExpression(pathAnalysis);
432
- const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
433
- const unresolvedReason = pathUnresolvedReason(pathAnalysis);
434
- if (spec && receiver && operationPathExpr) {
435
- add(node, { callType: 'remote_action', serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75, unresolvedReason }, { receiver, classifier: pathAnalysis.sourceKind.includes('string_literal') ? 'higher_order_wrapper_literal_path' : 'higher_order_wrapper_static_path', wrapperFunction: wrapperName, nestedWrapperFunction: spec.nestedWrapperFunction, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, normalizedOperationPath, literalPathSource: pathAnalysis.sourceKind.includes('const_alias') ? 'same_scope_const_initializer' : `wrapper_call_${wrapperSourceKind(pathAnalysis.sourceKind)}`, literalCallerArgumentDetected: true, pathAnalysis });
436
- } else if (spec && receiver) {
437
- add(node, { callType: 'remote_action', serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason }, { receiver, classifier: pathAnalysis.status === 'ambiguous' ? 'higher_order_wrapper_ambiguous_path' : 'higher_order_wrapper_dynamic_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: wrapperSourceKind(pathAnalysis.sourceKind), rawPathExpression: pathAnalysis.rawExpression, pathAnalysis, parserWarning: unresolvedReason });
438
- }
439
- } else if (ts.isPropertyAccessExpression(expr) && ['emit', 'publish', 'on'].includes(expr.name.text)) {
440
- const receiver = receiverName(expr.expression);
441
- const rootReceiver = rootReceiverName(expr.expression);
442
- if (isSupportedEventReceiver(receiver, rootReceiver, serviceVariables)) {
443
- const eventName = literalText(node.arguments[0]);
444
- const effectiveReceiver = rootReceiver ?? receiver;
445
- const lifecycleHook = effectiveReceiver === 'cds'
446
- && CDS_LIFECYCLE_EVENTS.has(eventName ?? '');
447
- const errorHook = expr.name.text === 'on' && eventName === 'error';
448
- if (eventName && !lifecycleHook && !errorHook) add(node, { callType: expr.name.text === 'on' ? 'async_subscribe' : 'async_emit', serviceVariableName: effectiveReceiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === 'on' ? 'cap_service_event_subscription' : 'cap_service_event_emit', receiverClassification: 'cap_evidence' });
449
- }
450
- } else {
451
- const external = externalHttpEvidence(node, source);
452
- if (external) {
453
- const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
454
- const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
455
- add(node, { callType: 'external_http', method: external.method, payloadSummary: undefined, confidence: 0.7, unresolvedReason: 'External HTTP destination is outside indexed CAP services', externalTarget: { kind: safeTarget.kind, stableId: safeTarget.toId, label: safeTarget.label, dynamic: safeTarget.dynamic } }, { classifier: external.classifier, externalTarget: safeTarget, sourceCallShape: external.sourceCallShape });
456
- }
457
- }
458
- }
459
- ts.forEachChild(node, visit);
460
- };
461
- visit(source);
462
- return calls;
463
- }
18
+
19
+ export {
20
+ classifyOutboundCallsInSource,
21
+ type ClassifiedOutboundCall,
22
+ };
23
+
24
+ interface LocalServiceOrigin {
25
+ service: string;
26
+ lookup: string;
27
+ chain: string[];
28
+ }
29
+
30
+ interface LocalServiceOperation extends LocalServiceOrigin {
31
+ operation: string;
32
+ classifier: string;
33
+ }
34
+
464
35
  export function containsSupportedOutboundCall(
465
36
  node: ts.Node,
466
37
  classified?: readonly ClassifiedOutboundCall[],
@@ -468,10 +39,24 @@ export function containsSupportedOutboundCall(
468
39
  const source = node.getSourceFile();
469
40
  const start = node.getFullStart();
470
41
  const end = node.getEnd();
471
- const calls = classified ?? classifyOutboundCallsInSource(source, source.fileName);
42
+ const calls = classified
43
+ ?? classifyOutboundCallsInSource(source, source.fileName);
472
44
  return calls.some((call) =>
473
45
  call.node.getStart(source) >= start && call.node.getEnd() <= end);
474
46
  }
47
+
48
+ function parsedSource(
49
+ filePath: string,
50
+ text: string,
51
+ context?: RepositorySourceContext,
52
+ ): ts.SourceFile {
53
+ const snapshot = context?.get(filePath);
54
+ return snapshot?.sourceFile() ?? ts.createSourceFile(
55
+ filePath, text, ts.ScriptTarget.Latest, true,
56
+ filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
57
+ );
58
+ }
59
+
475
60
  export async function parseOutboundCalls(
476
61
  repoPath: string,
477
62
  filePath: string,
@@ -482,86 +67,138 @@ export async function parseOutboundCalls(
482
67
  const snapshot = context?.get(filePath);
483
68
  const text = snapshot?.text
484
69
  ?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
485
- const source = snapshot?.sourceFile() ?? ts.createSourceFile(
486
- filePath, text, ts.ScriptTarget.Latest, true,
487
- filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
488
- );
70
+ const source = parsedSource(filePath, text, context);
489
71
  const bindings = preparedBindings ?? await parseServiceBindings(
490
72
  repoPath, filePath, context,
491
73
  );
492
- const bindingNames = new Set(bindings.map((binding) => binding.variableName));
74
+ const bindingNames = new Set(bindings.map((binding) =>
75
+ binding.variableName));
493
76
  const importedWrappers = await parseImportedWrapperCalls(
494
77
  repoPath, filePath, source, bindingNames, context,
495
78
  );
496
- const nativeCalls = classified ?? classifyOutboundCallsInSource(source, filePath);
79
+ const nativeCalls = classified
80
+ ?? classifyOutboundCallsInSource(source, filePath);
497
81
  return [
498
82
  ...nativeCalls.map((call) => call.fact),
499
83
  ...importedWrappers,
500
84
  ...parseLocalServiceCalls(text, filePath, source),
501
85
  ];
502
86
  }
87
+
88
+ function localServiceCallFact(
89
+ parsed: LocalServiceOperation,
90
+ node: ts.CallExpression,
91
+ text: string,
92
+ filePath: string,
93
+ source: ts.SourceFile,
94
+ ): OutboundCallFact {
95
+ const transport = ['send', 'emit', 'publish', 'on']
96
+ .includes(parsed.operation);
97
+ return {
98
+ callType: 'local_service_call',
99
+ operationPathExpr: `/${parsed.operation}`,
100
+ payloadSummary: parsed.service,
101
+ localServiceName: parsed.service,
102
+ localServiceLookup: parsed.lookup,
103
+ aliasChain: parsed.chain,
104
+ sourceFile: normalizePath(filePath),
105
+ sourceLine: lineOf(text, node.getStart(source)),
106
+ callSiteStartOffset: node.getStart(source),
107
+ callSiteEndOffset: node.getEnd(),
108
+ confidence: 0.9,
109
+ unresolvedReason: transport ? 'transport_client_method' : undefined,
110
+ evidence: parserEvidence(source, node, {
111
+ classifier: parsed.classifier,
112
+ parserCallType: parsed.operation === 'send'
113
+ ? 'transport_client_method' : parsed.classifier,
114
+ localServiceLookup: parsed.lookup,
115
+ localServiceName: parsed.service,
116
+ operation: parsed.operation,
117
+ aliasChain: parsed.chain,
118
+ }),
119
+ };
120
+ }
121
+
503
122
  function parseLocalServiceCalls(
504
123
  text: string,
505
124
  filePath: string,
506
- source = ts.createSourceFile(
507
- filePath, text, ts.ScriptTarget.Latest, true,
508
- filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
509
- ),
125
+ source: ts.SourceFile,
510
126
  ): OutboundCallFact[] {
511
- const aliases = new Map<string, { service: string; lookup: string; chain: string[] }>();
127
+ const aliases = new Map<string, LocalServiceOrigin>();
512
128
  const calls: OutboundCallFact[] = [];
513
129
  const visit = (node: ts.Node): void => {
514
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
130
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
131
+ && node.initializer) {
515
132
  const origin = serviceLookup(node.initializer, aliases);
516
- if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
133
+ if (origin) aliases.set(node.name.text, {
134
+ ...origin, chain: [...origin.chain, node.name.text],
135
+ });
517
136
  }
518
137
  if (ts.isCallExpression(node)) {
519
138
  const parsed = serviceOperationCall(node, aliases);
520
- if (parsed && parsed.operation !== 'entities') calls.push({
521
- callType: 'local_service_call',
522
- operationPathExpr: `/${parsed.operation}`,
523
- payloadSummary: parsed.service,
524
- localServiceName: parsed.service,
525
- localServiceLookup: parsed.lookup,
526
- aliasChain: parsed.chain,
527
- sourceFile: normalizePath(filePath),
528
- sourceLine: lineOf(text, node.getStart(source)),
529
- callSiteStartOffset: node.getStart(source),
530
- callSiteEndOffset: node.getEnd(),
531
- confidence: 0.9,
532
- unresolvedReason: ['send', 'emit', 'publish', 'on'].includes(parsed.operation) ? 'transport_client_method' : undefined,
533
- evidence: parserEvidence(source, node, {
534
- classifier: parsed.classifier,
535
- parserCallType: parsed.operation === 'send' ? 'transport_client_method' : parsed.classifier,
536
- localServiceLookup: parsed.lookup,
537
- localServiceName: parsed.service,
538
- operation: parsed.operation,
539
- aliasChain: parsed.chain,
540
- }),
541
- });
139
+ if (parsed && parsed.operation !== 'entities')
140
+ calls.push(localServiceCallFact(
141
+ parsed, node, text, filePath, source,
142
+ ));
542
143
  }
543
144
  ts.forEachChild(node, visit);
544
145
  };
545
146
  visit(source);
546
147
  return calls;
547
148
  }
548
- function serviceLookup(expr: ts.Expression, aliases: Map<string, { service: string; lookup: string; chain: string[] }>): { service: string; lookup: string; chain: string[] } | undefined {
549
- if (ts.isIdentifier(expr)) return aliases.get(expr.text);
550
- if (ts.isPropertyAccessExpression(expr) && expr.expression.getText() === 'cds.services') return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
551
- if (ts.isElementAccessExpression(expr) && expr.expression.getText() === 'cds.services' && ts.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
552
- return undefined;
149
+
150
+ function serviceLookup(
151
+ expression: ts.Expression,
152
+ aliases: Map<string, LocalServiceOrigin>,
153
+ ): LocalServiceOrigin | undefined {
154
+ if (ts.isIdentifier(expression)) return aliases.get(expression.text);
155
+ if (ts.isPropertyAccessExpression(expression)
156
+ && expression.expression.getText() === 'cds.services') return {
157
+ service: expression.name.text,
158
+ lookup: expression.getText(),
159
+ chain: [expression.getText()],
160
+ };
161
+ if (!ts.isElementAccessExpression(expression)
162
+ || expression.expression.getText() !== 'cds.services'
163
+ || !ts.isStringLiteral(expression.argumentExpression)) return undefined;
164
+ return {
165
+ service: expression.argumentExpression.text,
166
+ lookup: expression.getText(),
167
+ chain: [expression.getText()],
168
+ };
553
169
  }
554
- function serviceOperationCall(node: ts.CallExpression, aliases: Map<string, { service: string; lookup: string; chain: string[] }>): { service: string; lookup: string; chain: string[]; operation: string; classifier: string } | undefined {
555
- const expr = node.expression;
556
- if (!ts.isPropertyAccessExpression(expr)) return undefined;
557
- const origin = serviceLookup(expr.expression, aliases);
170
+
171
+ function positionalSendOperation(
172
+ node: ts.CallExpression,
173
+ ): { operation: string; classifier: string } | undefined {
174
+ const first = literalText(node.arguments[0]);
175
+ const second = literalText(node.arguments[1]);
176
+ const method = first?.toUpperCase();
177
+ if (method && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']
178
+ .includes(method) && second) return {
179
+ operation: second.replace(/^\//, ''),
180
+ classifier: 'cap_service_send_method_path',
181
+ };
182
+ return first ? {
183
+ operation: first.replace(/^\//, ''),
184
+ classifier: 'cap_service_send_local_dispatch',
185
+ } : undefined;
186
+ }
187
+
188
+ function serviceOperationCall(
189
+ node: ts.CallExpression,
190
+ aliases: Map<string, LocalServiceOrigin>,
191
+ ): LocalServiceOperation | undefined {
192
+ const expression = node.expression;
193
+ if (!ts.isPropertyAccessExpression(expression)) return undefined;
194
+ const origin = serviceLookup(expression.expression, aliases);
558
195
  if (!origin) return undefined;
559
- if (expr.name.text === 'send') {
560
- const first = literalText(node.arguments[0]);
561
- const second = literalText(node.arguments[1]);
562
- const method = first?.toUpperCase();
563
- if (method && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ''), classifier: 'cap_service_send_method_path' };
564
- if (first) return { ...origin, operation: first.replace(/^\//, ''), classifier: 'cap_service_send_local_dispatch' };
565
- }
566
- return { ...origin, operation: expr.name.text, classifier: 'local_cap_service_call' };
196
+ const send = expression.name.text === 'send'
197
+ ? positionalSendOperation(node) : undefined;
198
+ if (send) return { ...origin, ...send };
199
+ return {
200
+ ...origin,
201
+ operation: expression.name.text,
202
+ classifier: 'local_cap_service_call',
203
+ };
567
204
  }