@saptools/service-flow 0.1.68 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +11 -5
- package/TECHNICAL-NOTE.md +9 -0
- package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +317 -105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +45 -19
- package/src/db/003-current-fact-semantics.ts +1 -0
- package/src/db/004-package-target-invalidation.ts +10 -0
- package/src/db/006-relative-symbol-resolution.ts +28 -7
- package/src/db/008-relative-fact-semantics.ts +6 -3
- package/src/db/009-binding-fact-semantics.ts +5 -0
- package/src/db/012-binding-reference-proof.ts +4 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/repositories.ts +22 -5
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +66 -5
- package/src/indexer/workspace-indexer.ts +141 -29
- package/src/linker/004-event-subscription-handler-linker.ts +32 -11
- package/src/linker/006-event-template-link.ts +72 -0
- package/src/linker/007-call-edge-insertion.ts +568 -0
- package/src/linker/cross-repo-linker.ts +2 -165
- package/src/parsers/000-direct-query-execution.ts +11 -0
- package/src/parsers/006-binding-identity.ts +6 -1
- package/src/parsers/014-service-binding-helper-flow.ts +70 -4
- package/src/parsers/021-binding-visibility.ts +17 -1
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/outbound-call-parser.ts +146 -509
- package/src/parsers/symbol-parser.ts +37 -5
- package/src/trace/007-implementation-start-diagnostic.ts +1 -0
- package/src/trace/011-event-subscriber-traversal.ts +100 -8
- package/src/trace/013-trace-root-scopes.ts +2 -3
- package/src/trace/014-compact-contract.ts +6 -0
- package/src/trace/015-trace-edge-recorder.ts +61 -4
- package/src/trace/016-compact-projector.ts +2 -3
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +74 -1
- package/src/trace/021-compact-decision-normalization.ts +75 -9
- package/src/trace/024-compact-observation-decision.ts +7 -2
- package/src/trace/026-trace-start-scope.ts +1 -0
- package/src/trace/027-trace-scope-execution.ts +33 -33
- package/src/trace/030-event-runtime-resolution.ts +151 -0
- package/src/trace/031-local-call-expansion.ts +37 -0
- package/src/trace/implementation-hints.ts +9 -6
- package/src/trace/selectors.ts +1 -0
- package/src/types.ts +2 -1
- package/src/version.ts +1 -1
- package/dist/chunk-AEM4JY22.js.map +0 -1
|
@@ -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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from './000-direct-query-execution.js';
|
|
7
|
+
classifyOutboundCallsInSource,
|
|
8
|
+
type ClassifiedOutboundCall,
|
|
9
|
+
} from './023-outbound-call-classifier.js';
|
|
16
10
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from './
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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 =
|
|
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) =>
|
|
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
|
|
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
|
|
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,
|
|
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)
|
|
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, {
|
|
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')
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
}
|