@saptools/service-flow 0.1.64 → 0.1.65

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
@@ -17,7 +17,7 @@ import {
17
17
  stripQuotes,
18
18
  substituteVariables,
19
19
  trace
20
- } from "./chunk-TBH33OYC.js";
20
+ } from "./chunk-OONNRIDL.js";
21
21
 
22
22
  // src/parsers/generated-constants-parser.ts
23
23
  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.64",
3
+ "version": "0.1.65",
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": {
@@ -2,7 +2,10 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import ts from 'typescript';
4
4
  import type { ExecutableSymbolFact, SymbolCallFact } from '../types.js';
5
- import { containsSupportedOutboundCall } from './outbound-call-parser.js';
5
+ import {
6
+ classifyOutboundCallsInSource,
7
+ containsSupportedOutboundCall,
8
+ } from './outbound-call-parser.js';
6
9
  import type { RepositorySourceContext } from './ts-project.js';
7
10
  import { normalizePath } from '../utils/path-utils.js';
8
11
 
@@ -38,6 +41,76 @@ function exportDeclarations(source: ts.SourceFile): Map<string, string> {
38
41
  function isRelativeImport(value: string | undefined): boolean {
39
42
  return Boolean(value?.startsWith('.'));
40
43
  }
44
+ type HandlerReferenceRelation =
45
+ | 'relative_import'
46
+ | 'package_import'
47
+ | 'indexed_local_symbol'
48
+ | 'relative_import_namespace_member';
49
+ interface HandlerReferenceTarget {
50
+ calleeExpression: string;
51
+ calleeLocalName: string;
52
+ importSource?: string;
53
+ relation: HandlerReferenceRelation;
54
+ wrapperFunction?: string;
55
+ }
56
+ function directHandlerReferenceTarget(
57
+ expression: ts.Expression,
58
+ source: ts.SourceFile,
59
+ imports: Map<string, string>,
60
+ namespaceImports: Set<string>,
61
+ ): HandlerReferenceTarget | undefined {
62
+ if (ts.isIdentifier(expression)) {
63
+ const importSource = imports.get(expression.text);
64
+ return {
65
+ calleeExpression: expression.text,
66
+ calleeLocalName: expression.text,
67
+ importSource,
68
+ relation: importSource
69
+ ? isRelativeImport(importSource) ? 'relative_import' : 'package_import'
70
+ : 'indexed_local_symbol',
71
+ };
72
+ }
73
+ if (!ts.isPropertyAccessExpression(expression) || expression.questionDotToken
74
+ || !ts.isIdentifier(expression.expression) || !ts.isIdentifier(expression.name))
75
+ return undefined;
76
+ const objectName = expression.expression.text;
77
+ const memberName = expression.name.text;
78
+ const importSource = imports.get(objectName);
79
+ if (namespaceImports.has(objectName)) return {
80
+ calleeExpression: expression.getText(source),
81
+ calleeLocalName: memberName,
82
+ importSource,
83
+ relation: 'relative_import_namespace_member',
84
+ };
85
+ const qualifiedName = `${objectName}.${memberName}`;
86
+ return {
87
+ calleeExpression: qualifiedName,
88
+ calleeLocalName: qualifiedName,
89
+ importSource,
90
+ relation: importSource
91
+ ? isRelativeImport(importSource) ? 'relative_import' : 'package_import'
92
+ : 'indexed_local_symbol',
93
+ };
94
+ }
95
+ function handlerReferenceTarget(
96
+ expression: ts.Expression,
97
+ source: ts.SourceFile,
98
+ imports: Map<string, string>,
99
+ namespaceImports: Set<string>,
100
+ ): HandlerReferenceTarget | undefined {
101
+ const direct = directHandlerReferenceTarget(
102
+ expression, source, imports, namespaceImports,
103
+ );
104
+ if (direct) return direct;
105
+ if (!ts.isCallExpression(expression) || expression.questionDotToken
106
+ || expression.arguments.length !== 1) return undefined;
107
+ const inner = directHandlerReferenceTarget(
108
+ expression.arguments[0], source, imports, namespaceImports,
109
+ );
110
+ return inner
111
+ ? { ...inner, wrapperFunction: expression.expression.getText(source) }
112
+ : undefined;
113
+ }
41
114
  function isObjectFunction(node: ts.Node): boolean {
42
115
  return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
43
116
  }
@@ -185,6 +258,11 @@ export async function parseExecutableSymbols(
185
258
  const calls: SymbolCallFact[] = [];
186
259
  const imports = new Map<string, string>();
187
260
  const namespaceImports = new Set<string>();
261
+ const eventSubscriptionOffsets = new Set(
262
+ classifyOutboundCallsInSource(source, sourceFile)
263
+ .filter((call) => call.fact.callType === 'async_subscribe')
264
+ .map((call) => call.node.getStart(source)),
265
+ );
188
266
  const exportNames = exportDeclarations(source);
189
267
  const objectExports = new Set<string>();
190
268
  const exportedClasses = new Set<string>();
@@ -305,6 +383,33 @@ export async function parseExecutableSymbols(
305
383
  const name = `event:${eventName}:${startLine}`;
306
384
  symbols.push({ kind: 'event_registration', localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: 'synthetic_event_registration', eventName: eventArg.text, registrationLine: startLine, receiver } });
307
385
  }
386
+ if (eventSubscriptionOffsets.has(node.getStart(source))) {
387
+ const startLine = lineOf(source, node.getStart(source));
388
+ const handlerArgument = node.arguments[1];
389
+ const target = handlerArgument
390
+ ? handlerReferenceTarget(
391
+ handlerArgument, source, imports, namespaceImports,
392
+ )
393
+ : undefined;
394
+ const anchor = nearest(symbols, startLine);
395
+ if (target && anchor) calls.push({
396
+ callerQualifiedName: anchor.qualifiedName,
397
+ calleeExpression: target.calleeExpression,
398
+ calleeLocalName: target.calleeLocalName,
399
+ importSource: target.importSource,
400
+ sourceFile,
401
+ sourceLine: startLine,
402
+ evidence: {
403
+ relation: target.relation,
404
+ caller: anchor.qualifiedName,
405
+ targetName: target.calleeLocalName,
406
+ ...(target.wrapperFunction
407
+ ? { wrapperFunction: target.wrapperFunction }
408
+ : {}),
409
+ candidateStrategy: 'event_subscribe_handler_reference',
410
+ },
411
+ });
412
+ }
308
413
  }
309
414
  ts.forEachChild(node, visitEventRegistrationSymbols);
310
415
  };