@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
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { externalHttpTarget } from '../linker/external-http-target.js';
|
|
3
|
+
import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
|
|
4
|
+
import type { OutboundCallFact } from '../types.js';
|
|
5
|
+
import { normalizePath, stripQuotes } from '../utils/path-utils.js';
|
|
6
|
+
import { summarizeExpression } from '../utils/redaction.js';
|
|
7
|
+
import { directQueryBuilderStatement } from './000-direct-query-execution.js';
|
|
8
|
+
import {
|
|
9
|
+
expressionName,
|
|
10
|
+
queryEntityFromAst,
|
|
11
|
+
variableInitializers,
|
|
12
|
+
} from './001-query-entity-resolution.js';
|
|
13
|
+
import {
|
|
14
|
+
CDS_LIFECYCLE_EVENTS,
|
|
15
|
+
calledWrapperNames,
|
|
16
|
+
collectServiceVariables,
|
|
17
|
+
externalHttpEvidence,
|
|
18
|
+
isSupportedEventReceiver,
|
|
19
|
+
legacyPathCandidates,
|
|
20
|
+
lineOf,
|
|
21
|
+
literalPathSource,
|
|
22
|
+
objectPropertyIsShorthand,
|
|
23
|
+
parserEvidence,
|
|
24
|
+
propertyInitializer,
|
|
25
|
+
queryBuilderEvidence,
|
|
26
|
+
queryRunEvidence,
|
|
27
|
+
queryWarning,
|
|
28
|
+
receiverName,
|
|
29
|
+
resolveExpression,
|
|
30
|
+
rootReceiverName,
|
|
31
|
+
safeOperationName,
|
|
32
|
+
supportedHttpMethods,
|
|
33
|
+
wrapperSourceKind,
|
|
34
|
+
wrapperSpec,
|
|
35
|
+
type ExpressionResolution,
|
|
36
|
+
type WrapperSpec,
|
|
37
|
+
} from './022-outbound-expression-analysis.js';
|
|
38
|
+
import {
|
|
39
|
+
analyzeOperationPath,
|
|
40
|
+
operationPathExpression,
|
|
41
|
+
pathUnresolvedReason,
|
|
42
|
+
type OperationPathAnalysis,
|
|
43
|
+
} from './operation-path-analysis.js';
|
|
44
|
+
|
|
45
|
+
export interface ClassifiedOutboundCall {
|
|
46
|
+
fact: OutboundCallFact;
|
|
47
|
+
node: ts.CallExpression;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function namedFunctionLike(
|
|
51
|
+
node: ts.Node,
|
|
52
|
+
): { name: string; fn: ts.FunctionLikeDeclaration } | undefined {
|
|
53
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
54
|
+
return { name: node.name.text, fn: node };
|
|
55
|
+
if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name)
|
|
56
|
+
|| !node.initializer) return undefined;
|
|
57
|
+
return ts.isArrowFunction(node.initializer)
|
|
58
|
+
|| ts.isFunctionExpression(node.initializer)
|
|
59
|
+
? { name: node.name.text, fn: node.initializer } : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function collectWrapperSpecs(
|
|
63
|
+
source: ts.SourceFile,
|
|
64
|
+
): Map<string, WrapperSpec> {
|
|
65
|
+
const specs = new Map<string, WrapperSpec>();
|
|
66
|
+
const calledNames = calledWrapperNames(source);
|
|
67
|
+
const serviceVariables = collectServiceVariables(source);
|
|
68
|
+
const visit = (node: ts.Node): void => {
|
|
69
|
+
const named = namedFunctionLike(node);
|
|
70
|
+
if (named) {
|
|
71
|
+
const spec = wrapperSpec(
|
|
72
|
+
named.name, named.fn, source, calledNames, serviceVariables, specs,
|
|
73
|
+
);
|
|
74
|
+
if (spec) specs.set(named.name, spec);
|
|
75
|
+
}
|
|
76
|
+
ts.forEachChild(node, visit);
|
|
77
|
+
};
|
|
78
|
+
visit(source);
|
|
79
|
+
return specs;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface EventCallClassification {
|
|
83
|
+
fact: Pick<OutboundCallFact,
|
|
84
|
+
'callType' | 'serviceVariableName' | 'eventNameExpr'
|
|
85
|
+
| 'confidence' | 'unresolvedReason'>;
|
|
86
|
+
evidence: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface EventReceiver {
|
|
90
|
+
effectiveReceiver: string;
|
|
91
|
+
receiver?: string;
|
|
92
|
+
rootReceiver?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function eventReceiver(
|
|
96
|
+
expression: ts.PropertyAccessExpression,
|
|
97
|
+
serviceVariables: Set<string>,
|
|
98
|
+
): EventReceiver | undefined {
|
|
99
|
+
const receiver = receiverName(expression.expression);
|
|
100
|
+
const rootReceiver = rootReceiverName(expression.expression);
|
|
101
|
+
if (!isSupportedEventReceiver(receiver, rootReceiver, serviceVariables))
|
|
102
|
+
return undefined;
|
|
103
|
+
const effectiveReceiver = rootReceiver ?? receiver;
|
|
104
|
+
return effectiveReceiver
|
|
105
|
+
? { receiver, rootReceiver, effectiveReceiver } : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function eventUnresolvedReason(
|
|
109
|
+
resolved: ExpressionResolution,
|
|
110
|
+
): string | undefined {
|
|
111
|
+
if (resolved.status === 'static') return undefined;
|
|
112
|
+
return resolved.value !== undefined && resolved.placeholderKeys.length > 0
|
|
113
|
+
? 'dynamic_event_name_identifier'
|
|
114
|
+
: 'dynamic_event_name_unsupported_expression';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function eventConfidence(reason: string | undefined): number {
|
|
118
|
+
if (!reason) return 0.8;
|
|
119
|
+
return reason === 'dynamic_event_name_identifier' ? 0.6 : 0.3;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function eventClassificationEvidence(
|
|
123
|
+
expression: ts.PropertyAccessExpression,
|
|
124
|
+
receiver: EventReceiver,
|
|
125
|
+
resolved: ExpressionResolution,
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
return {
|
|
128
|
+
receiver: receiver.receiver,
|
|
129
|
+
rootReceiver: receiver.rootReceiver,
|
|
130
|
+
classifier: expression.name.text === 'on'
|
|
131
|
+
? 'cap_service_event_subscription' : 'cap_service_event_emit',
|
|
132
|
+
receiverClassification: 'cap_evidence',
|
|
133
|
+
...(resolved.status === 'static' ? {} : {
|
|
134
|
+
eventNameStatus: resolved.status,
|
|
135
|
+
eventNameSourceKind: resolved.sourceKind,
|
|
136
|
+
eventNamePlaceholderKeys: resolved.placeholderKeys,
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface EventNameState {
|
|
142
|
+
eventName: string;
|
|
143
|
+
resolved: ExpressionResolution;
|
|
144
|
+
unresolvedReason?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function eventNameState(node: ts.CallExpression): EventNameState | undefined {
|
|
148
|
+
const expression = node.arguments[0];
|
|
149
|
+
const resolved = resolveExpression(expression, node, 'operation_path');
|
|
150
|
+
let eventName = resolved.value;
|
|
151
|
+
if (eventName === undefined) eventName = resolved.rawExpression;
|
|
152
|
+
if (eventName === undefined && expression)
|
|
153
|
+
eventName = expression.getText(node.getSourceFile());
|
|
154
|
+
if (!eventName) return undefined;
|
|
155
|
+
return {
|
|
156
|
+
eventName,
|
|
157
|
+
resolved,
|
|
158
|
+
unresolvedReason: eventUnresolvedReason(resolved),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function excludedEventName(
|
|
163
|
+
expression: ts.PropertyAccessExpression,
|
|
164
|
+
receiver: EventReceiver,
|
|
165
|
+
state: EventNameState,
|
|
166
|
+
): boolean {
|
|
167
|
+
if (state.resolved.status !== 'static') return false;
|
|
168
|
+
if (receiver.effectiveReceiver === 'cds'
|
|
169
|
+
&& CDS_LIFECYCLE_EVENTS.has(state.eventName)) return true;
|
|
170
|
+
return expression.name.text === 'on' && state.eventName === 'error';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function eventCallClassification(
|
|
174
|
+
node: ts.CallExpression,
|
|
175
|
+
expression: ts.PropertyAccessExpression,
|
|
176
|
+
serviceVariables: Set<string>,
|
|
177
|
+
): EventCallClassification | undefined {
|
|
178
|
+
const receiver = eventReceiver(expression, serviceVariables);
|
|
179
|
+
if (!receiver) return undefined;
|
|
180
|
+
const state = eventNameState(node);
|
|
181
|
+
if (!state || excludedEventName(expression, receiver, state))
|
|
182
|
+
return undefined;
|
|
183
|
+
return {
|
|
184
|
+
fact: {
|
|
185
|
+
callType: expression.name.text === 'on'
|
|
186
|
+
? 'async_subscribe' : 'async_emit',
|
|
187
|
+
serviceVariableName: receiver.effectiveReceiver,
|
|
188
|
+
eventNameExpr: state.eventName,
|
|
189
|
+
confidence: eventConfidence(state.unresolvedReason),
|
|
190
|
+
unresolvedReason: state.unresolvedReason,
|
|
191
|
+
},
|
|
192
|
+
evidence: eventClassificationEvidence(
|
|
193
|
+
expression, receiver, state.resolved,
|
|
194
|
+
),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type OutboundFactInput = Omit<
|
|
199
|
+
OutboundCallFact, 'sourceFile' | 'sourceLine' | 'confidence'
|
|
200
|
+
> & { confidence?: number };
|
|
201
|
+
type AddOutboundCall = (
|
|
202
|
+
node: ts.CallExpression,
|
|
203
|
+
fact: OutboundFactInput,
|
|
204
|
+
extra?: Record<string, unknown>,
|
|
205
|
+
) => void;
|
|
206
|
+
|
|
207
|
+
interface OutboundCallContext {
|
|
208
|
+
source: ts.SourceFile;
|
|
209
|
+
initializers: Map<string, ts.Expression>;
|
|
210
|
+
serviceVariables: Set<string>;
|
|
211
|
+
wrapperSpecs: Map<string, WrapperSpec>;
|
|
212
|
+
add: AddOutboundCall;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function classifyQueryDispatch(
|
|
216
|
+
node: ts.CallExpression,
|
|
217
|
+
context: OutboundCallContext,
|
|
218
|
+
): boolean {
|
|
219
|
+
const { source, initializers, add } = context;
|
|
220
|
+
if (expressionName(node.expression) === 'cds.run') {
|
|
221
|
+
const argument = node.arguments[0];
|
|
222
|
+
const entity = argument
|
|
223
|
+
? queryEntityFromAst(argument, initializers) : undefined;
|
|
224
|
+
const payload = argument?.getText(source) ?? '';
|
|
225
|
+
add(node, {
|
|
226
|
+
callType: 'local_db_query',
|
|
227
|
+
queryEntity: entity,
|
|
228
|
+
payloadSummary: summarizeExpression(payload),
|
|
229
|
+
confidence: entity ? 0.9 : 0.55,
|
|
230
|
+
unresolvedReason: entity ? undefined : queryWarning(payload),
|
|
231
|
+
}, queryRunEvidence(source, argument));
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
const direct = directQueryBuilderStatement(node);
|
|
235
|
+
if (!direct) return false;
|
|
236
|
+
const entity = queryEntityFromAst(direct.logicalCall, initializers);
|
|
237
|
+
const payload = direct.logicalCall.getText(source);
|
|
238
|
+
add(direct.logicalCall, {
|
|
239
|
+
callType: 'local_db_query',
|
|
240
|
+
queryEntity: entity,
|
|
241
|
+
payloadSummary: summarizeExpression(payload),
|
|
242
|
+
confidence: entity ? 0.9 : 0.55,
|
|
243
|
+
unresolvedReason: entity ? undefined : queryWarning(payload),
|
|
244
|
+
}, queryBuilderEvidence(source, direct));
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sendExpression(
|
|
249
|
+
node: ts.CallExpression,
|
|
250
|
+
): ts.PropertyAccessExpression | undefined {
|
|
251
|
+
const expression = node.expression;
|
|
252
|
+
if (!ts.isPropertyAccessExpression(expression)
|
|
253
|
+
|| expression.name.text !== 'send') return undefined;
|
|
254
|
+
return ts.isIdentifier(expression.expression)
|
|
255
|
+
|| ts.isPropertyAccessExpression(expression.expression)
|
|
256
|
+
? expression : undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function objectSendMethod(
|
|
260
|
+
object: ts.ObjectLiteralExpression,
|
|
261
|
+
node: ts.CallExpression,
|
|
262
|
+
): { method: string; dynamicDefaulted: boolean } {
|
|
263
|
+
const expression = propertyInitializer(object, 'method');
|
|
264
|
+
const resolution = resolveExpression(expression, node, 'literal');
|
|
265
|
+
return {
|
|
266
|
+
method: stripQuotes(resolution.value ?? 'POST'),
|
|
267
|
+
dynamicDefaulted: Boolean(expression && resolution.value === undefined),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function objectSendCallType(
|
|
272
|
+
query: ts.Expression | undefined,
|
|
273
|
+
entityType: OutboundCallFact['callType'] | undefined,
|
|
274
|
+
queryRead: boolean,
|
|
275
|
+
): OutboundCallFact['callType'] {
|
|
276
|
+
if (query) return 'remote_query';
|
|
277
|
+
if (entityType) return entityType;
|
|
278
|
+
return queryRead ? 'remote_query' : 'remote_action';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function objectSendQueryEntity(
|
|
282
|
+
query: ts.Expression | undefined,
|
|
283
|
+
initializers: Map<string, ts.Expression>,
|
|
284
|
+
queryRead: boolean,
|
|
285
|
+
entitySegment: string | undefined,
|
|
286
|
+
): string | undefined {
|
|
287
|
+
if (query) return queryEntityFromAst(query, initializers);
|
|
288
|
+
return queryRead ? entitySegment : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function objectSendUnresolvedReason(
|
|
292
|
+
query: ts.Expression | undefined,
|
|
293
|
+
queryEntity: string | undefined,
|
|
294
|
+
pathExpression: ts.Expression | undefined,
|
|
295
|
+
pathAnalysis: OperationPathAnalysis,
|
|
296
|
+
source: ts.SourceFile,
|
|
297
|
+
): string | undefined {
|
|
298
|
+
if (query)
|
|
299
|
+
return queryEntity ? undefined : queryWarning(query.getText(source));
|
|
300
|
+
return pathExpression ? pathUnresolvedReason(pathAnalysis) : undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const objectSendEntityTypes: Readonly<Record<
|
|
304
|
+
string, OutboundCallFact['callType']
|
|
305
|
+
>> = {
|
|
306
|
+
entity_mutation: 'remote_entity_mutation',
|
|
307
|
+
entity_delete: 'remote_entity_delete',
|
|
308
|
+
entity_media: 'remote_entity_media',
|
|
309
|
+
entity_candidate: 'remote_entity_candidate',
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
function objectSendEvidence(
|
|
313
|
+
object: ts.ObjectLiteralExpression,
|
|
314
|
+
receiver: string | undefined,
|
|
315
|
+
rawOperation: string | undefined,
|
|
316
|
+
operationPath: string | undefined,
|
|
317
|
+
intent: ReturnType<typeof classifyODataPathIntent>,
|
|
318
|
+
pathAnalysis: OperationPathAnalysis,
|
|
319
|
+
unresolvedReason: string | undefined,
|
|
320
|
+
dynamicMethodDefaulted: boolean,
|
|
321
|
+
): Record<string, unknown> {
|
|
322
|
+
return {
|
|
323
|
+
receiver,
|
|
324
|
+
classifier: 'service_client_send_object',
|
|
325
|
+
operationPathExpression: objectPropertyIsShorthand(object, 'path')
|
|
326
|
+
? rawOperation : undefined,
|
|
327
|
+
rawPathExpression: pathAnalysis.rawExpression,
|
|
328
|
+
literalPathSource: literalPathSource(pathAnalysis),
|
|
329
|
+
odataPathIntent: operationPath ? intent : undefined,
|
|
330
|
+
pathAnalysis,
|
|
331
|
+
staticPathCandidates: legacyPathCandidates(pathAnalysis),
|
|
332
|
+
parserWarning: unresolvedReason,
|
|
333
|
+
...(dynamicMethodDefaulted ? { dynamicMethodDefaulted: true } : {}),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function classifyObjectSend(
|
|
338
|
+
node: ts.CallExpression,
|
|
339
|
+
expression: ts.PropertyAccessExpression,
|
|
340
|
+
object: ts.ObjectLiteralExpression,
|
|
341
|
+
context: OutboundCallContext,
|
|
342
|
+
): void {
|
|
343
|
+
const { source, initializers, add } = context;
|
|
344
|
+
const receiver = receiverName(expression.expression);
|
|
345
|
+
const query = propertyInitializer(object, 'query');
|
|
346
|
+
const methodState = objectSendMethod(object, node);
|
|
347
|
+
const path = propertyInitializer(object, 'path')
|
|
348
|
+
?? propertyInitializer(object, 'event');
|
|
349
|
+
const pathAnalysis = analyzeOperationPath(path, node, methodState.method);
|
|
350
|
+
const operationPath = operationPathExpression(pathAnalysis);
|
|
351
|
+
const intent = classifyODataPathIntent(operationPath, methodState.method);
|
|
352
|
+
const queryRead = methodState.method.toUpperCase() === 'GET'
|
|
353
|
+
&& ['entity_query', 'entity_key_read', 'entity_navigation_query']
|
|
354
|
+
.includes(intent.kind);
|
|
355
|
+
const queryEntity = objectSendQueryEntity(
|
|
356
|
+
query, initializers, queryRead, intent.entitySegment,
|
|
357
|
+
);
|
|
358
|
+
const unresolvedReason = objectSendUnresolvedReason(
|
|
359
|
+
query, queryEntity, path, pathAnalysis, source,
|
|
360
|
+
);
|
|
361
|
+
const rawOperation = path
|
|
362
|
+
? operationPath ?? path.getText(source) : undefined;
|
|
363
|
+
add(node, {
|
|
364
|
+
callType: objectSendCallType(
|
|
365
|
+
query, objectSendEntityTypes[intent.kind], queryRead,
|
|
366
|
+
),
|
|
367
|
+
serviceVariableName: receiver,
|
|
368
|
+
method: methodState.method,
|
|
369
|
+
operationPathExpr: operationPath,
|
|
370
|
+
queryEntity,
|
|
371
|
+
payloadSummary: summarizeExpression(object.getText(source)),
|
|
372
|
+
confidence: rawOperation || query ? 0.8 : 0.4,
|
|
373
|
+
unresolvedReason,
|
|
374
|
+
}, objectSendEvidence(
|
|
375
|
+
object, receiver, rawOperation, operationPath, intent, pathAnalysis,
|
|
376
|
+
unresolvedReason, methodState.dynamicDefaulted,
|
|
377
|
+
));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function classifyPositionalSend(
|
|
381
|
+
node: ts.CallExpression,
|
|
382
|
+
expression: ts.PropertyAccessExpression,
|
|
383
|
+
context: OutboundCallContext,
|
|
384
|
+
): void {
|
|
385
|
+
const { source, serviceVariables, add } = context;
|
|
386
|
+
const receiver = receiverName(expression.expression);
|
|
387
|
+
const rootReceiver = rootReceiverName(expression.expression);
|
|
388
|
+
const serviceVariable = rootReceiver ?? receiver;
|
|
389
|
+
if (!receiver || !serviceVariable
|
|
390
|
+
|| !serviceVariables.has(serviceVariable)) return;
|
|
391
|
+
const first = resolveExpression(node.arguments[0], node, 'literal');
|
|
392
|
+
const method = first.value?.toUpperCase();
|
|
393
|
+
if (!method || !supportedHttpMethods.has(method)) {
|
|
394
|
+
const operationPath = safeOperationName(first.value);
|
|
395
|
+
addUnsupportedPositionalSend(
|
|
396
|
+
node, receiver, rootReceiver, first, operationPath, context,
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const pathAnalysis = analyzeOperationPath(node.arguments[1], node, method);
|
|
401
|
+
const operationPath = operationPathExpression(pathAnalysis);
|
|
402
|
+
const intent = classifyODataPathIntent(operationPath, method);
|
|
403
|
+
const unresolvedReason = pathUnresolvedReason(pathAnalysis);
|
|
404
|
+
add(node, {
|
|
405
|
+
callType: 'remote_action',
|
|
406
|
+
serviceVariableName: serviceVariable,
|
|
407
|
+
method,
|
|
408
|
+
operationPathExpr: operationPath,
|
|
409
|
+
payloadSummary: summarizeExpression(node.getText(source)),
|
|
410
|
+
confidence: operationPath ? 0.8 : 0.45,
|
|
411
|
+
unresolvedReason,
|
|
412
|
+
}, {
|
|
413
|
+
receiver, rootReceiver,
|
|
414
|
+
classifier: 'service_client_send_method_path',
|
|
415
|
+
rawPathExpression: pathAnalysis.rawExpression,
|
|
416
|
+
literalPathSource: literalPathSource(pathAnalysis),
|
|
417
|
+
odataPathIntent: operationPath ? intent : undefined,
|
|
418
|
+
pathAnalysis,
|
|
419
|
+
staticPathCandidates: legacyPathCandidates(pathAnalysis),
|
|
420
|
+
parserWarning: unresolvedReason,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function addUnsupportedPositionalSend(
|
|
425
|
+
node: ts.CallExpression,
|
|
426
|
+
receiver: string,
|
|
427
|
+
rootReceiver: string | undefined,
|
|
428
|
+
first: ExpressionResolution,
|
|
429
|
+
operationPath: string | undefined,
|
|
430
|
+
context: OutboundCallContext,
|
|
431
|
+
): void {
|
|
432
|
+
const unresolvedReason = operationPath
|
|
433
|
+
? undefined : 'unsupported_cap_send_signature';
|
|
434
|
+
context.add(node, {
|
|
435
|
+
callType: 'remote_action',
|
|
436
|
+
serviceVariableName: rootReceiver ?? receiver,
|
|
437
|
+
operationPathExpr: operationPath,
|
|
438
|
+
payloadSummary: summarizeExpression(node.getText(context.source)),
|
|
439
|
+
confidence: operationPath ? 0.75 : 0.35,
|
|
440
|
+
unresolvedReason,
|
|
441
|
+
}, {
|
|
442
|
+
receiver, rootReceiver,
|
|
443
|
+
classifier: operationPath
|
|
444
|
+
? 'service_client_send_operation_event'
|
|
445
|
+
: 'service_client_send_unsupported_signature',
|
|
446
|
+
rawOperationExpression: first.rawExpression,
|
|
447
|
+
literalOperationSource: first.value ? first.sourceKind : undefined,
|
|
448
|
+
parserWarning: unresolvedReason,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function classifySendCall(
|
|
453
|
+
node: ts.CallExpression,
|
|
454
|
+
context: OutboundCallContext,
|
|
455
|
+
): boolean {
|
|
456
|
+
const expression = sendExpression(node);
|
|
457
|
+
if (!expression) return false;
|
|
458
|
+
const object = node.arguments[0];
|
|
459
|
+
if (object && ts.isObjectLiteralExpression(object))
|
|
460
|
+
classifyObjectSend(node, expression, object, context);
|
|
461
|
+
else classifyPositionalSend(node, expression, context);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
interface WrapperInvocation {
|
|
466
|
+
name: string;
|
|
467
|
+
arguments: readonly ts.Expression[];
|
|
468
|
+
spec: WrapperSpec;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function wrapperInvocation(
|
|
472
|
+
expression: ts.LeftHandSideExpression,
|
|
473
|
+
specs: Map<string, WrapperSpec>,
|
|
474
|
+
): WrapperInvocation | undefined {
|
|
475
|
+
if (ts.isIdentifier(expression)) {
|
|
476
|
+
const spec = specs.get(expression.text);
|
|
477
|
+
return spec
|
|
478
|
+
? { name: expression.text, arguments: [], spec } : undefined;
|
|
479
|
+
}
|
|
480
|
+
if (!ts.isCallExpression(expression)
|
|
481
|
+
|| !ts.isIdentifier(expression.expression)) return undefined;
|
|
482
|
+
const spec = specs.get(expression.expression.text);
|
|
483
|
+
return spec ? {
|
|
484
|
+
name: expression.expression.text,
|
|
485
|
+
arguments: expression.arguments,
|
|
486
|
+
spec,
|
|
487
|
+
} : undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function wrapperArguments(
|
|
491
|
+
node: ts.CallExpression,
|
|
492
|
+
invocation: WrapperInvocation,
|
|
493
|
+
): readonly ts.Expression[] {
|
|
494
|
+
return invocation.arguments.length > 0
|
|
495
|
+
? invocation.arguments : node.arguments;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function classifyWrapperCall(
|
|
499
|
+
node: ts.CallExpression,
|
|
500
|
+
context: OutboundCallContext,
|
|
501
|
+
): boolean {
|
|
502
|
+
const invocation = wrapperInvocation(node.expression, context.wrapperSpecs);
|
|
503
|
+
if (!invocation) return false;
|
|
504
|
+
const args = wrapperArguments(node, invocation);
|
|
505
|
+
const client = invocation.spec.clientIndex === undefined
|
|
506
|
+
? undefined : args[invocation.spec.clientIndex];
|
|
507
|
+
const path = args[invocation.spec.pathIndex];
|
|
508
|
+
const methodArgument = invocation.spec.methodIndex === undefined
|
|
509
|
+
? undefined : args[invocation.spec.methodIndex];
|
|
510
|
+
const receiver = client && ts.isIdentifier(client)
|
|
511
|
+
? client.text : invocation.spec.clientName;
|
|
512
|
+
const method = stripQuotes(resolveExpression(
|
|
513
|
+
methodArgument, node, 'literal',
|
|
514
|
+
).value ?? invocation.spec.methodLiteral ?? 'POST');
|
|
515
|
+
const analysis = analyzeOperationPath(path, node, method);
|
|
516
|
+
if (receiver) addWrapperCall(
|
|
517
|
+
node, receiver, method, analysis, invocation, context,
|
|
518
|
+
);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function addWrapperCall(
|
|
523
|
+
node: ts.CallExpression,
|
|
524
|
+
receiver: string,
|
|
525
|
+
method: string,
|
|
526
|
+
analysis: OperationPathAnalysis,
|
|
527
|
+
invocation: WrapperInvocation,
|
|
528
|
+
context: OutboundCallContext,
|
|
529
|
+
): void {
|
|
530
|
+
const operationPath = operationPathExpression(analysis);
|
|
531
|
+
const unresolvedReason = pathUnresolvedReason(analysis);
|
|
532
|
+
const staticPath = Boolean(operationPath);
|
|
533
|
+
context.add(node, {
|
|
534
|
+
callType: 'remote_action',
|
|
535
|
+
serviceVariableName: receiver,
|
|
536
|
+
method,
|
|
537
|
+
operationPathExpr: operationPath,
|
|
538
|
+
payloadSummary: summarizeExpression(node.getText(context.source)),
|
|
539
|
+
confidence: staticPath ? 0.75 : 0.45,
|
|
540
|
+
unresolvedReason,
|
|
541
|
+
}, wrapperEvidence(
|
|
542
|
+
node, receiver, method, analysis, invocation, context, staticPath,
|
|
543
|
+
));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function wrapperEvidence(
|
|
547
|
+
node: ts.CallExpression,
|
|
548
|
+
receiver: string,
|
|
549
|
+
method: string,
|
|
550
|
+
analysis: OperationPathAnalysis,
|
|
551
|
+
invocation: WrapperInvocation,
|
|
552
|
+
context: OutboundCallContext,
|
|
553
|
+
staticPath: boolean,
|
|
554
|
+
): Record<string, unknown> {
|
|
555
|
+
const operationPath = operationPathExpression(analysis);
|
|
556
|
+
return {
|
|
557
|
+
receiver,
|
|
558
|
+
classifier: staticPath
|
|
559
|
+
? analysis.sourceKind.includes('string_literal')
|
|
560
|
+
? 'higher_order_wrapper_literal_path'
|
|
561
|
+
: 'higher_order_wrapper_static_path'
|
|
562
|
+
: analysis.status === 'ambiguous'
|
|
563
|
+
? 'higher_order_wrapper_ambiguous_path'
|
|
564
|
+
: 'higher_order_wrapper_dynamic_path',
|
|
565
|
+
wrapperFunction: invocation.name,
|
|
566
|
+
nestedWrapperFunction: invocation.spec.nestedWrapperFunction,
|
|
567
|
+
wrapperDefinitionLine: invocation.spec.definitionLine,
|
|
568
|
+
callerLine: lineOf(context.source.text, node.getStart(context.source)),
|
|
569
|
+
wrapperPathSourceKind: wrapperSourceKind(analysis.sourceKind),
|
|
570
|
+
rawPathExpression: analysis.rawExpression,
|
|
571
|
+
normalizedOperationPath: operationPath
|
|
572
|
+
? classifyODataPathIntent(operationPath, method).topLevelOperationName
|
|
573
|
+
: undefined,
|
|
574
|
+
literalPathSource: staticPath
|
|
575
|
+
? analysis.sourceKind.includes('const_alias')
|
|
576
|
+
? 'same_scope_const_initializer'
|
|
577
|
+
: `wrapper_call_${wrapperSourceKind(analysis.sourceKind)}`
|
|
578
|
+
: undefined,
|
|
579
|
+
literalCallerArgumentDetected: staticPath || undefined,
|
|
580
|
+
pathAnalysis: analysis,
|
|
581
|
+
parserWarning: staticPath ? undefined : pathUnresolvedReason(analysis),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function classifyEventCall(
|
|
586
|
+
node: ts.CallExpression,
|
|
587
|
+
context: OutboundCallContext,
|
|
588
|
+
): boolean {
|
|
589
|
+
const expression = node.expression;
|
|
590
|
+
if (!ts.isPropertyAccessExpression(expression)
|
|
591
|
+
|| !['emit', 'publish', 'on'].includes(expression.name.text)) return false;
|
|
592
|
+
const event = eventCallClassification(
|
|
593
|
+
node, expression, context.serviceVariables,
|
|
594
|
+
);
|
|
595
|
+
if (event) context.add(node, event.fact, event.evidence);
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function classifyExternalCall(
|
|
600
|
+
node: ts.CallExpression,
|
|
601
|
+
context: OutboundCallContext,
|
|
602
|
+
): void {
|
|
603
|
+
const external = externalHttpEvidence(node, context.source);
|
|
604
|
+
if (!external) return;
|
|
605
|
+
const evidenceTarget = {
|
|
606
|
+
...external.externalTarget,
|
|
607
|
+
method: external.method,
|
|
608
|
+
parserClassifier: external.classifier,
|
|
609
|
+
sourceCallShape: external.sourceCallShape,
|
|
610
|
+
};
|
|
611
|
+
const safeTarget = externalHttpTarget({
|
|
612
|
+
method: external.method,
|
|
613
|
+
evidence_json: JSON.stringify({ externalTarget: evidenceTarget }),
|
|
614
|
+
});
|
|
615
|
+
context.add(node, {
|
|
616
|
+
callType: 'external_http',
|
|
617
|
+
method: external.method,
|
|
618
|
+
payloadSummary: undefined,
|
|
619
|
+
confidence: 0.7,
|
|
620
|
+
unresolvedReason:
|
|
621
|
+
'External HTTP destination is outside indexed CAP services',
|
|
622
|
+
externalTarget: {
|
|
623
|
+
kind: safeTarget.kind,
|
|
624
|
+
stableId: safeTarget.toId,
|
|
625
|
+
label: safeTarget.label,
|
|
626
|
+
dynamic: safeTarget.dynamic,
|
|
627
|
+
},
|
|
628
|
+
}, {
|
|
629
|
+
classifier: external.classifier,
|
|
630
|
+
externalTarget: safeTarget,
|
|
631
|
+
sourceCallShape: external.sourceCallShape,
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function classifyOutboundCall(
|
|
636
|
+
node: ts.CallExpression,
|
|
637
|
+
context: OutboundCallContext,
|
|
638
|
+
): void {
|
|
639
|
+
if (classifyQueryDispatch(node, context)) return;
|
|
640
|
+
if (classifySendCall(node, context)) return;
|
|
641
|
+
if (classifyWrapperCall(node, context)) return;
|
|
642
|
+
if (classifyEventCall(node, context)) return;
|
|
643
|
+
classifyExternalCall(node, context);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function visitOutboundCalls(
|
|
647
|
+
source: ts.SourceFile,
|
|
648
|
+
internalRanges: readonly { start: number; end: number }[],
|
|
649
|
+
classify: (node: ts.CallExpression) => void,
|
|
650
|
+
): void {
|
|
651
|
+
const visit = (node: ts.Node): void => {
|
|
652
|
+
if (ts.isCallExpression(node) && internalRanges.some((range) =>
|
|
653
|
+
node.getStart(source) >= range.start && node.getEnd() <= range.end))
|
|
654
|
+
return;
|
|
655
|
+
if (ts.isCallExpression(node)) classify(node);
|
|
656
|
+
ts.forEachChild(node, visit);
|
|
657
|
+
};
|
|
658
|
+
visit(source);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function classifyOutboundCallsInSource(
|
|
662
|
+
source: ts.SourceFile,
|
|
663
|
+
filePath: string,
|
|
664
|
+
): ClassifiedOutboundCall[] {
|
|
665
|
+
const calls: ClassifiedOutboundCall[] = [];
|
|
666
|
+
const sourceFile = normalizePath(filePath);
|
|
667
|
+
const initializers = variableInitializers(source);
|
|
668
|
+
const serviceVariables = collectServiceVariables(source);
|
|
669
|
+
const wrapperSpecs = collectWrapperSpecs(source);
|
|
670
|
+
const internalRanges = [...wrapperSpecs.values()].map((spec) => ({
|
|
671
|
+
start: spec.internalStart, end: spec.internalEnd,
|
|
672
|
+
}));
|
|
673
|
+
const add: AddOutboundCall = (node, fact, extra): void => {
|
|
674
|
+
calls.push({ node, fact: {
|
|
675
|
+
...fact,
|
|
676
|
+
sourceFile,
|
|
677
|
+
sourceLine: lineOf(source.text, node.getStart(source)),
|
|
678
|
+
callSiteStartOffset: node.getStart(source),
|
|
679
|
+
callSiteEndOffset: node.getEnd(),
|
|
680
|
+
confidence: fact.confidence ?? 0.8,
|
|
681
|
+
evidence: parserEvidence(source, node, extra),
|
|
682
|
+
} });
|
|
683
|
+
};
|
|
684
|
+
const context = {
|
|
685
|
+
source, initializers, serviceVariables, wrapperSpecs, add,
|
|
686
|
+
};
|
|
687
|
+
visitOutboundCalls(
|
|
688
|
+
source, internalRanges,
|
|
689
|
+
(node) => classifyOutboundCall(node, context),
|
|
690
|
+
);
|
|
691
|
+
return calls;
|
|
692
|
+
}
|