@saptools/service-flow 0.1.38 → 0.1.39

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/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  stripQuotes,
17
17
  substituteVariables,
18
18
  trace
19
- } from "./chunk-WE3A6TOJ.js";
19
+ } from "./chunk-SAZ5OK7R.js";
20
20
 
21
21
  // src/parsers/generated-constants-parser.ts
22
22
  import fs from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saptools/service-flow",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -92,9 +92,48 @@ function nameOfProperty(name: ts.PropertyName): string | undefined {
92
92
  function staticExpressionText(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string | undefined {
93
93
  if (!expr) return undefined;
94
94
  if (isStringLike(expr)) return expr.text;
95
+ if (ts.isTemplateExpression(expr)) return stripQuotes(expr.getText(expr.getSourceFile()));
95
96
  if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
96
97
  return undefined;
97
98
  }
99
+ interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string }
100
+ function staticPathExpression(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): StaticPathResolution {
101
+ if (!expr) return { sourceKind: 'dynamic' };
102
+ const rawExpression = expr.getText(expr.getSourceFile());
103
+ if (isStringLike(expr)) return { text: expr.text, sourceKind: 'literal', rawExpression };
104
+ if (ts.isTemplateExpression(expr)) return { text: stripQuotes(rawExpression), sourceKind: 'template', rawExpression };
105
+ if (ts.isIdentifier(expr) && initializers.has(expr.text)) {
106
+ const resolved = staticPathExpression(initializers.get(expr.text), initializers);
107
+ return resolved.text ? { ...resolved, sourceKind: 'const', rawExpression, constName: expr.text } : { sourceKind: 'dynamic', rawExpression };
108
+ }
109
+ return { sourceKind: 'dynamic', rawExpression };
110
+ }
111
+ function operationPathFromStatic(text: string | undefined): string | undefined {
112
+ return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
113
+ }
114
+ interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean }
115
+ function staticPathCandidates(source: ts.SourceFile, identifier: string, initializers: Map<string, ts.Expression>): PathCandidateEvidence | undefined {
116
+ const paths: string[] = [];
117
+ let hasDynamicAssignments = false;
118
+ const visit = (node: ts.Node): void => {
119
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier && node.initializer) {
120
+ const resolved = staticPathExpression(node.initializer, initializers);
121
+ if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
122
+ else hasDynamicAssignments = true;
123
+ }
124
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left) && node.left.text === identifier) {
125
+ const resolved = staticPathExpression(node.right, initializers);
126
+ if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
127
+ else hasDynamicAssignments = true;
128
+ }
129
+ ts.forEachChild(node, visit);
130
+ };
131
+ visit(source);
132
+ const candidatePaths = [...new Set(paths)];
133
+ if (candidatePaths.length === 0) return undefined;
134
+ const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, 'POST').topLevelOperationName).filter((value): value is string => Boolean(value)))];
135
+ return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: 'static candidates observed', candidateIdentifier: identifier, hasDynamicAssignments };
136
+ }
98
137
  function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
99
138
  if (!expr) return undefined;
100
139
  if (ts.isIdentifier(expr)) return 'identifier';
@@ -287,16 +326,17 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
287
326
  const query = objectPropertyText(objectArg, 'query');
288
327
  const method = stripQuotes(objectPropertyText(objectArg, 'method') ?? 'POST');
289
328
  const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
290
- const op = pathExpr ? staticExpressionText(pathExpr, initializers) ?? pathExpr.getText(source) : undefined;
329
+ const resolvedPath = staticPathExpression(pathExpr, initializers);
330
+ const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : undefined;
291
331
  const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
292
- const staticPath = staticExpressionText(pathExpr, initializers);
293
- const literalPath = isStringLike(pathExpr) ? pathExpr.text : pathExpr && ts.isTemplateExpression(pathExpr) ? pathExpr.getText(source) : undefined;
294
- const operationPathExpr = staticPath ? `/${stripQuotes(staticPath).replace(/^\//, '')}` : literalPath ? `/${stripQuotes(literalPath).replace(/^\//, '')}` : undefined;
332
+ const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(source, pathExpr.text, initializers) : undefined;
333
+ const candidateOperationPath = candidateEvidence && !candidateEvidence.hasDynamicAssignments && candidateEvidence.normalizedCandidateOperations.length === 1 && candidateEvidence.candidatePaths.length > 0 ? candidateEvidence.candidatePaths[0] : undefined;
334
+ const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
295
335
  const intent = classifyODataPathIntent(operationPathExpr, method);
296
336
  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' };
297
337
  const entityCallType = entityCallTypes[intent.kind];
298
338
  const isODataQueryRead = method.toUpperCase() === 'GET' && ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
299
- add(node, { callType: query ? 'remote_query' : entityCallType ?? (isODataQueryRead ? 'remote_query' : 'remote_action'), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : undefined, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, literalPathSource: shorthandPath && staticPath ? 'same_scope_const_initializer' : undefined, odataPathIntent: operationPathExpr ? intent : undefined, parserWarning: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined });
339
+ add(node, { callType: query ? 'remote_query' : entityCallType ?? (isODataQueryRead ? 'remote_query' : 'remote_action'), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : undefined, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? (resolvedPath.sourceKind === 'const' ? 'same_scope_const_initializer' : resolvedPath.sourceKind) : undefined, odataPathIntent: operationPathExpr ? intent : undefined, staticPathCandidates: candidateEvidence, parserWarning: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined });
300
340
  }
301
341
  } else if (((ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) || (ts.isIdentifier(expr) && wrapperSpecs.has(expr.text)))) {
302
342
  const wrapperName = ts.isIdentifier(expr) ? expr.text : ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) ? expr.expression.text : '';
@@ -307,10 +347,13 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
307
347
  const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
308
348
  const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
309
349
  const method = stripQuotes(staticExpressionText(methodArg, initializers) ?? spec?.methodLiteral ?? 'POST');
310
- if (spec && receiver && isStringLike(pathArg)) {
311
- add(node, { callType: 'remote_action', serviceVariableName: receiver, method, operationPathExpr: `/${pathArg.text.replace(/^\//, '')}`, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: 'higher_order_wrapper_literal_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), literalPathSource: 'wrapper_call_argument', literalCallerArgumentDetected: true });
350
+ const resolvedPath = staticPathExpression(pathArg, initializers);
351
+ const operationPathExpr = operationPathFromStatic(resolvedPath.text);
352
+ const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
353
+ if (spec && receiver && operationPathExpr) {
354
+ add(node, { callType: 'remote_action', serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: resolvedPath.sourceKind === 'literal' ? 'higher_order_wrapper_literal_path' : 'higher_order_wrapper_static_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, normalizedOperationPath, literalPathSource: resolvedPath.sourceKind === 'const' ? 'same_scope_const_initializer' : `wrapper_call_${resolvedPath.sourceKind}`, literalCallerArgumentDetected: true });
312
355
  } else if (spec && receiver) {
313
- add(node, { callType: 'remote_action', serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: 'dynamic_operation_path_identifier' }, { receiver, classifier: 'higher_order_wrapper_dynamic_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), parserWarning: 'dynamic_operation_path_identifier' });
356
+ add(node, { callType: 'remote_action', serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: 'dynamic_operation_path_identifier' }, { receiver, classifier: 'higher_order_wrapper_dynamic_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: 'dynamic_operation_path_identifier' });
314
357
  }
315
358
  } else if (ts.isPropertyAccessExpression(expr) && ['emit', 'publish', 'on'].includes(expr.name.text)) {
316
359
  const receiver = receiverName(expr.expression);