@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
@@ -1,554 +1,204 @@
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
- import type { OutboundCallFact } 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';
4
+ import type { OutboundCallFact, ServiceBindingFact } from '../types.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 './023-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 './022-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
- }
464
- export function containsSupportedOutboundCall(node: ts.Node): boolean {
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
+
35
+ export function containsSupportedOutboundCall(
36
+ node: ts.Node,
37
+ classified?: readonly ClassifiedOutboundCall[],
38
+ ): boolean {
465
39
  const source = node.getSourceFile();
466
40
  const start = node.getFullStart();
467
41
  const end = node.getEnd();
468
- return classifyOutboundCallsInSource(source, source.fileName).some((call) => call.node.getStart(source) >= start && call.node.getEnd() <= end);
42
+ const calls = classified
43
+ ?? classifyOutboundCallsInSource(source, source.fileName);
44
+ return calls.some((call) =>
45
+ call.node.getStart(source) >= start && call.node.getEnd() <= end);
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
+ );
469
58
  }
59
+
470
60
  export async function parseOutboundCalls(
471
61
  repoPath: string,
472
62
  filePath: string,
473
63
  context?: RepositorySourceContext,
64
+ classified?: readonly ClassifiedOutboundCall[],
65
+ preparedBindings?: readonly ServiceBindingFact[],
474
66
  ): Promise<OutboundCallFact[]> {
475
67
  const snapshot = context?.get(filePath);
476
68
  const text = snapshot?.text
477
69
  ?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
478
- const source = snapshot?.sourceFile() ?? ts.createSourceFile(
479
- filePath, text, ts.ScriptTarget.Latest, true,
480
- filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
481
- );
482
- const bindingNames = new Set((await parseServiceBindings(
70
+ const source = parsedSource(filePath, text, context);
71
+ const bindings = preparedBindings ?? await parseServiceBindings(
483
72
  repoPath, filePath, context,
484
- )).map((binding) => binding.variableName));
73
+ );
74
+ const bindingNames = new Set(bindings.map((binding) =>
75
+ binding.variableName));
485
76
  const importedWrappers = await parseImportedWrapperCalls(
486
77
  repoPath, filePath, source, bindingNames, context,
487
78
  );
488
- return [...classifyOutboundCallsInSource(source, filePath).map((call) => call.fact), ...importedWrappers, ...parseLocalServiceCalls(text, filePath, source)];
79
+ const nativeCalls = classified
80
+ ?? classifyOutboundCallsInSource(source, filePath);
81
+ return [
82
+ ...nativeCalls.map((call) => call.fact),
83
+ ...importedWrappers,
84
+ ...parseLocalServiceCalls(text, filePath, source),
85
+ ];
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
+ };
489
120
  }
121
+
490
122
  function parseLocalServiceCalls(
491
123
  text: string,
492
124
  filePath: string,
493
- source = ts.createSourceFile(
494
- filePath, text, ts.ScriptTarget.Latest, true,
495
- filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS,
496
- ),
125
+ source: ts.SourceFile,
497
126
  ): OutboundCallFact[] {
498
- const aliases = new Map<string, { service: string; lookup: string; chain: string[] }>();
127
+ const aliases = new Map<string, LocalServiceOrigin>();
499
128
  const calls: OutboundCallFact[] = [];
500
129
  const visit = (node: ts.Node): void => {
501
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
130
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)
131
+ && node.initializer) {
502
132
  const origin = serviceLookup(node.initializer, aliases);
503
- 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
+ });
504
136
  }
505
137
  if (ts.isCallExpression(node)) {
506
138
  const parsed = serviceOperationCall(node, aliases);
507
- if (parsed && parsed.operation !== 'entities') calls.push({
508
- callType: 'local_service_call',
509
- operationPathExpr: `/${parsed.operation}`,
510
- payloadSummary: parsed.service,
511
- localServiceName: parsed.service,
512
- localServiceLookup: parsed.lookup,
513
- aliasChain: parsed.chain,
514
- sourceFile: normalizePath(filePath),
515
- sourceLine: lineOf(text, node.getStart(source)),
516
- callSiteStartOffset: node.getStart(source),
517
- callSiteEndOffset: node.getEnd(),
518
- confidence: 0.9,
519
- unresolvedReason: ['send', 'emit', 'publish', 'on'].includes(parsed.operation) ? 'transport_client_method' : undefined,
520
- evidence: parserEvidence(source, node, {
521
- classifier: parsed.classifier,
522
- parserCallType: parsed.operation === 'send' ? 'transport_client_method' : parsed.classifier,
523
- localServiceLookup: parsed.lookup,
524
- localServiceName: parsed.service,
525
- operation: parsed.operation,
526
- aliasChain: parsed.chain,
527
- }),
528
- });
139
+ if (parsed && parsed.operation !== 'entities')
140
+ calls.push(localServiceCallFact(
141
+ parsed, node, text, filePath, source,
142
+ ));
529
143
  }
530
144
  ts.forEachChild(node, visit);
531
145
  };
532
146
  visit(source);
533
147
  return calls;
534
148
  }
535
- function serviceLookup(expr: ts.Expression, aliases: Map<string, { service: string; lookup: string; chain: string[] }>): { service: string; lookup: string; chain: string[] } | undefined {
536
- if (ts.isIdentifier(expr)) return aliases.get(expr.text);
537
- if (ts.isPropertyAccessExpression(expr) && expr.expression.getText() === 'cds.services') return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
538
- if (ts.isElementAccessExpression(expr) && expr.expression.getText() === 'cds.services' && ts.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
539
- 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
+ };
540
169
  }
541
- 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 {
542
- const expr = node.expression;
543
- if (!ts.isPropertyAccessExpression(expr)) return undefined;
544
- 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);
545
195
  if (!origin) return undefined;
546
- if (expr.name.text === 'send') {
547
- const first = literalText(node.arguments[0]);
548
- const second = literalText(node.arguments[1]);
549
- const method = first?.toUpperCase();
550
- if (method && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ''), classifier: 'cap_service_send_method_path' };
551
- if (first) return { ...origin, operation: first.replace(/^\//, ''), classifier: 'cap_service_send_local_dispatch' };
552
- }
553
- 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
+ };
554
204
  }