@saptools/service-flow 0.1.55 → 0.1.56

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-GXYVIHJ5.js";
20
+ } from "./chunk-Y7H7ZU5B.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.55",
3
+ "version": "0.1.56",
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": {
@@ -0,0 +1,287 @@
1
+ import ts from 'typescript';
2
+
3
+ const capQueryBuilderRoots = new Set([
4
+ 'SELECT.from',
5
+ 'SELECT.one.from',
6
+ 'SELECT.one',
7
+ 'INSERT.into',
8
+ 'UPSERT.into',
9
+ 'UPDATE.entity',
10
+ 'UPDATE',
11
+ 'DELETE.from',
12
+ ]);
13
+ const promiseValueShadowCache = new WeakMap<ts.SourceFile, boolean>();
14
+
15
+ export type DirectQueryExecutionContext =
16
+ | 'await'
17
+ | 'async_return'
18
+ | 'promise_return'
19
+ | 'promise_aggregate';
20
+
21
+ export interface DirectQueryBuilderStatement {
22
+ root: ts.CallExpression;
23
+ logicalCall: ts.CallExpression;
24
+ statement: ts.Expression;
25
+ executionContext: DirectQueryExecutionContext;
26
+ }
27
+
28
+ export function isCapQueryBuilderRootName(name: string): boolean {
29
+ return capQueryBuilderRoots.has(name);
30
+ }
31
+
32
+ export function queryBuilderRoot(
33
+ expression: ts.Expression,
34
+ ): ts.CallExpression | undefined {
35
+ const unwrapped = unwrapQueryExpression(expression);
36
+ if (!ts.isCallExpression(unwrapped)) return undefined;
37
+ if (isCapQueryBuilderRootName(expressionName(unwrapped.expression)))
38
+ return unwrapped;
39
+ return ts.isPropertyAccessExpression(unwrapped.expression)
40
+ ? queryBuilderRoot(unwrapped.expression.expression)
41
+ : undefined;
42
+ }
43
+
44
+ export function directQueryBuilderStatement(
45
+ node: ts.CallExpression,
46
+ ): DirectQueryBuilderStatement | undefined {
47
+ const root = queryBuilderRoot(node);
48
+ if (!root) return undefined;
49
+ const logicalCall = outerFluentQueryCall(root);
50
+ if (logicalCall !== node) return undefined;
51
+ const expression = outerTransparentExpression(logicalCall);
52
+ const awaitExpression = directAwaitExpression(expression);
53
+ if (awaitExpression)
54
+ return { root, logicalCall, statement: awaitExpression, executionContext: 'await' };
55
+ const returnContext = returnExecutionContext(expression);
56
+ if (returnContext)
57
+ return { root, logicalCall, statement: expression, executionContext: returnContext };
58
+ if (isAwaitedPromiseAllElement(expression))
59
+ return { root, logicalCall, statement: expression, executionContext: 'promise_aggregate' };
60
+ return undefined;
61
+ }
62
+
63
+ function expressionName(expression: ts.Expression): string {
64
+ if (ts.isIdentifier(expression)) return expression.text;
65
+ if (ts.isPropertyAccessExpression(expression))
66
+ return `${expressionName(expression.expression)}.${expression.name.text}`;
67
+ return expression.getText();
68
+ }
69
+
70
+ function unwrapQueryExpression(expression: ts.Expression): ts.Expression {
71
+ if (ts.isParenthesizedExpression(expression) || ts.isAwaitExpression(expression)
72
+ || ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)
73
+ || ts.isNonNullExpression(expression) || ts.isSatisfiesExpression(expression))
74
+ return unwrapQueryExpression(expression.expression);
75
+ return expression;
76
+ }
77
+
78
+ function wrapperParent(node: ts.Expression): ts.Expression | undefined {
79
+ const parent = node.parent;
80
+ if ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent)
81
+ || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent)
82
+ || ts.isSatisfiesExpression(parent)) && parent.expression === node)
83
+ return parent;
84
+ return undefined;
85
+ }
86
+
87
+ function fluentCallParent(node: ts.Expression): ts.CallExpression | undefined {
88
+ const property = node.parent;
89
+ if (!ts.isPropertyAccessExpression(property) || property.expression !== node)
90
+ return undefined;
91
+ const call = property.parent;
92
+ return ts.isCallExpression(call) && call.expression === property ? call : undefined;
93
+ }
94
+
95
+ function outerFluentQueryCall(root: ts.CallExpression): ts.CallExpression {
96
+ let current: ts.Expression = root;
97
+ let outer = root;
98
+ while (true) {
99
+ const wrapper = wrapperParent(current);
100
+ if (wrapper) {
101
+ current = wrapper;
102
+ continue;
103
+ }
104
+ const next = fluentCallParent(current);
105
+ if (!next) return outer;
106
+ outer = next;
107
+ current = next;
108
+ }
109
+ }
110
+
111
+ function outerTransparentExpression(expression: ts.Expression): ts.Expression {
112
+ let current = expression;
113
+ while (true) {
114
+ const wrapper = wrapperParent(current);
115
+ if (!wrapper) return current;
116
+ current = wrapper;
117
+ }
118
+ }
119
+
120
+ function directAwaitExpression(
121
+ expression: ts.Expression,
122
+ ): ts.AwaitExpression | undefined {
123
+ const parent = expression.parent;
124
+ return ts.isAwaitExpression(parent) && parent.expression === expression
125
+ ? parent
126
+ : undefined;
127
+ }
128
+
129
+ function returnExecutionContext(
130
+ expression: ts.Expression,
131
+ ): DirectQueryExecutionContext | undefined {
132
+ const callable = returnedExpressionCallable(expression);
133
+ if (!callable) return undefined;
134
+ if (hasAsyncModifier(callable)) return 'async_return';
135
+ return hasGuaranteedPromiseReturn(callable) ? 'promise_return' : undefined;
136
+ }
137
+
138
+ function returnedExpressionCallable(
139
+ expression: ts.Expression,
140
+ ): ts.FunctionLikeDeclaration | undefined {
141
+ const parent = expression.parent;
142
+ if (ts.isArrowFunction(parent) && parent.body === expression) return parent;
143
+ if (!ts.isReturnStatement(parent) || parent.expression !== expression)
144
+ return undefined;
145
+ return nearestCallable(parent);
146
+ }
147
+
148
+ function nearestCallable(node: ts.Node): ts.FunctionLikeDeclaration | undefined {
149
+ let current = node.parent;
150
+ while (current) {
151
+ if (isRuntimeCallable(current)) return current;
152
+ current = current.parent;
153
+ }
154
+ return undefined;
155
+ }
156
+
157
+ function isRuntimeCallable(node: ts.Node): node is ts.FunctionLikeDeclaration {
158
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
159
+ || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
160
+ || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node)
161
+ || ts.isSetAccessorDeclaration(node);
162
+ }
163
+
164
+ function hasAsyncModifier(node: ts.Node): boolean {
165
+ return !isGeneratorCallable(node) && ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some(
166
+ (modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword,
167
+ ) ?? false);
168
+ }
169
+
170
+ function isGeneratorCallable(node: ts.Node): boolean {
171
+ return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
172
+ || ts.isMethodDeclaration(node)) && Boolean(node.asteriskToken);
173
+ }
174
+
175
+ function hasGuaranteedPromiseReturn(
176
+ callable: ts.FunctionLikeDeclaration,
177
+ ): boolean {
178
+ const returnType = declaredReturnType(callable);
179
+ return Boolean(returnType && isGuaranteedPromiseType(returnType));
180
+ }
181
+
182
+ function declaredReturnType(
183
+ callable: ts.FunctionLikeDeclaration,
184
+ ): ts.TypeNode | undefined {
185
+ if (ts.isFunctionDeclaration(callable) || ts.isFunctionExpression(callable)
186
+ || ts.isArrowFunction(callable) || ts.isMethodDeclaration(callable))
187
+ return callable.type;
188
+ return undefined;
189
+ }
190
+
191
+ function isGuaranteedPromiseType(type: ts.TypeNode): boolean {
192
+ if (ts.isParenthesizedTypeNode(type))
193
+ return isGuaranteedPromiseType(type.type);
194
+ if (ts.isTypeReferenceNode(type))
195
+ return isStandardPromiseTypeName(type.typeName);
196
+ if (ts.isUnionTypeNode(type))
197
+ return type.types.length > 0 && type.types.every(isGuaranteedPromiseType);
198
+ if (ts.isIntersectionTypeNode(type))
199
+ return type.types.some(isGuaranteedPromiseType);
200
+ return false;
201
+ }
202
+
203
+ function isStandardPromiseTypeName(name: ts.EntityName): boolean {
204
+ if (ts.isIdentifier(name))
205
+ return name.text === 'Promise' || name.text === 'PromiseLike';
206
+ return ts.isIdentifier(name.left)
207
+ && (name.left.text === 'globalThis' || name.left.text === 'global')
208
+ && (name.right.text === 'Promise' || name.right.text === 'PromiseLike');
209
+ }
210
+
211
+ function isAwaitedPromiseAllElement(expression: ts.Expression): boolean {
212
+ const array = directArrayParent(expression);
213
+ if (!array) return false;
214
+ const aggregate = aggregateCallForArray(array);
215
+ return Boolean(aggregate && isBuiltInPromiseAll(aggregate)
216
+ && directAwaitExpression(outerTransparentExpression(aggregate)));
217
+ }
218
+
219
+ function directArrayParent(
220
+ expression: ts.Expression,
221
+ ): ts.ArrayLiteralExpression | undefined {
222
+ const parent = expression.parent;
223
+ return ts.isArrayLiteralExpression(parent) && parent.elements.some(
224
+ (element) => element === expression,
225
+ ) ? parent : undefined;
226
+ }
227
+
228
+ function aggregateCallForArray(
229
+ array: ts.ArrayLiteralExpression,
230
+ ): ts.CallExpression | undefined {
231
+ const argument = outerTransparentExpression(array);
232
+ const parent = argument.parent;
233
+ return ts.isCallExpression(parent) && parent.arguments.length === 1
234
+ && parent.arguments[0] === argument ? parent : undefined;
235
+ }
236
+
237
+ function isBuiltInPromiseAll(call: ts.CallExpression): boolean {
238
+ return ts.isPropertyAccessExpression(call.expression)
239
+ && ts.isIdentifier(call.expression.expression)
240
+ && call.expression.expression.text === 'Promise'
241
+ && call.expression.name.text === 'all'
242
+ && !hasPromiseValueShadow(call.getSourceFile());
243
+ }
244
+
245
+ function hasPromiseValueShadow(source: ts.SourceFile): boolean {
246
+ const cached = promiseValueShadowCache.get(source);
247
+ if (cached !== undefined) return cached;
248
+ let shadowed = false;
249
+ const visit = (node: ts.Node): void => {
250
+ if (shadowed) return;
251
+ if (declaresPromiseValue(node)) {
252
+ shadowed = true;
253
+ return;
254
+ }
255
+ ts.forEachChild(node, visit);
256
+ };
257
+ visit(source);
258
+ promiseValueShadowCache.set(source, shadowed);
259
+ return shadowed;
260
+ }
261
+
262
+ function declaresPromiseValue(node: ts.Node): boolean {
263
+ if (ts.isVariableDeclaration(node) || ts.isParameter(node))
264
+ return bindingNameIsPromise(node.name);
265
+ if (ts.isImportClause(node))
266
+ return !node.isTypeOnly && nodeIsPromise(node.name);
267
+ if (ts.isImportSpecifier(node))
268
+ return !node.isTypeOnly && nodeIsPromise(node.name);
269
+ if (ts.isNamespaceImport(node) || ts.isImportEqualsDeclaration(node))
270
+ return nodeIsPromise(node.name);
271
+ if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)
272
+ || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node))
273
+ return nodeIsPromise(node.name);
274
+ return false;
275
+ }
276
+
277
+ function bindingNameIsPromise(name: ts.BindingName): boolean {
278
+ if (ts.isIdentifier(name)) return name.text === 'Promise';
279
+ if (ts.isObjectBindingPattern(name))
280
+ return name.elements.some((element) => bindingNameIsPromise(element.name));
281
+ return name.elements.some((element) => ts.isBindingElement(element)
282
+ && bindingNameIsPromise(element.name));
283
+ }
284
+
285
+ function nodeIsPromise(name: ts.Node | undefined): boolean {
286
+ return Boolean(name && ts.isIdentifier(name) && name.text === 'Promise');
287
+ }
@@ -8,6 +8,12 @@ import { summarizeExpression } from '../utils/redaction.js';
8
8
  import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
9
9
  import { parseServiceBindings } from './service-binding-parser.js';
10
10
  import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
11
+ import {
12
+ directQueryBuilderStatement,
13
+ isCapQueryBuilderRootName,
14
+ queryBuilderRoot,
15
+ type DirectQueryBuilderStatement,
16
+ } from './000-direct-query-execution.js';
11
17
  import type { RepositorySourceContext } from './ts-project.js';
12
18
  import {
13
19
  analyzeOperationPath,
@@ -53,82 +59,12 @@ function queryEntityFromAst(expr: ts.Expression, initializers = new Map<string,
53
59
  if (ts.isCallExpression(unwrapped)) {
54
60
  const name = expressionName(unwrapped.expression);
55
61
  if (name === 'cds.run') return queryEntityFromAst(unwrapped.arguments[0], initializers);
56
- if (capQueryBuilderRoots.has(name)) return entityFromExpression(unwrapped.arguments[0]);
62
+ if (isCapQueryBuilderRootName(name)) return entityFromExpression(unwrapped.arguments[0]);
57
63
  const receiver = ts.isPropertyAccessExpression(unwrapped.expression) ? unwrapped.expression.expression : undefined;
58
64
  if (receiver) return queryEntityFromAst(receiver, initializers);
59
65
  }
60
66
  return undefined;
61
67
  }
62
- const capQueryBuilderRoots = new Set([
63
- 'SELECT.from',
64
- 'SELECT.one.from',
65
- 'SELECT.one',
66
- 'INSERT.into',
67
- 'UPSERT.into',
68
- 'UPDATE.entity',
69
- 'UPDATE',
70
- 'DELETE.from',
71
- ]);
72
- interface DirectQueryBuilderStatement {
73
- root: ts.CallExpression;
74
- logicalCall: ts.CallExpression;
75
- awaitExpression: ts.AwaitExpression;
76
- }
77
- function wrapperParent(node: ts.Expression): ts.Expression | undefined {
78
- const parent = node.parent;
79
- if ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent)
80
- || ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent)
81
- || ts.isSatisfiesExpression(parent)) && parent.expression === node)
82
- return parent;
83
- return undefined;
84
- }
85
- function fluentCallParent(node: ts.Expression): ts.CallExpression | undefined {
86
- const property = node.parent;
87
- if (!ts.isPropertyAccessExpression(property) || property.expression !== node) return undefined;
88
- const call = property.parent;
89
- return ts.isCallExpression(call) && call.expression === property ? call : undefined;
90
- }
91
- function queryBuilderRoot(expr: ts.Expression): ts.CallExpression | undefined {
92
- const unwrapped = unwrapQueryExpression(expr);
93
- if (!ts.isCallExpression(unwrapped)) return undefined;
94
- if (capQueryBuilderRoots.has(expressionName(unwrapped.expression))) return unwrapped;
95
- return ts.isPropertyAccessExpression(unwrapped.expression)
96
- ? queryBuilderRoot(unwrapped.expression.expression)
97
- : undefined;
98
- }
99
- function outerFluentQueryCall(root: ts.CallExpression): ts.CallExpression {
100
- let current: ts.Expression = root;
101
- let outer = root;
102
- while (true) {
103
- const wrapper = wrapperParent(current);
104
- if (wrapper) {
105
- current = wrapper;
106
- continue;
107
- }
108
- const next = fluentCallParent(current);
109
- if (!next) return outer;
110
- outer = next;
111
- current = next;
112
- }
113
- }
114
- function directQueryBuilderStatement(node: ts.CallExpression): DirectQueryBuilderStatement | undefined {
115
- const root = queryBuilderRoot(node);
116
- if (!root) return undefined;
117
- const logicalCall = outerFluentQueryCall(root);
118
- if (logicalCall !== node) return undefined;
119
- let current: ts.Expression = logicalCall;
120
- while (true) {
121
- const wrapper = wrapperParent(current);
122
- if (wrapper) {
123
- current = wrapper;
124
- continue;
125
- }
126
- const parent = current.parent;
127
- return ts.isAwaitExpression(parent) && parent.expression === current
128
- ? { root, logicalCall, awaitExpression: parent }
129
- : undefined;
130
- }
131
- }
132
68
  function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuilderStatement): Record<string, unknown> {
133
69
  return {
134
70
  classifier: 'cap_query_builder_direct',
@@ -136,8 +72,9 @@ function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuild
136
72
  queryRoot: expressionName(statement.root.expression),
137
73
  queryRootStartOffset: statement.root.getStart(source),
138
74
  queryRootEndOffset: statement.root.getEnd(),
139
- queryStatementStartOffset: statement.awaitExpression.getStart(source),
140
- queryStatementEndOffset: statement.awaitExpression.getEnd(),
75
+ queryStatementStartOffset: statement.statement.getStart(source),
76
+ queryStatementEndOffset: statement.statement.getEnd(),
77
+ queryExecutionContext: statement.executionContext,
141
78
  };
142
79
  }
143
80
  function queryRunEvidence(