@saptools/service-flow 0.1.58 → 0.1.60

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.
@@ -10,10 +10,16 @@ import { parseServiceBindings } from './service-binding-parser.js';
10
10
  import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
11
11
  import {
12
12
  directQueryBuilderStatement,
13
- isCapQueryBuilderRootName,
14
13
  queryBuilderRoot,
15
14
  type DirectQueryBuilderStatement,
16
15
  } from './000-direct-query-execution.js';
16
+ import {
17
+ expressionName,
18
+ maxAliasDepth,
19
+ queryEntityFromAst,
20
+ resolveBinding,
21
+ variableInitializers,
22
+ } from './001-query-entity-resolution.js';
17
23
  import type { RepositorySourceContext } from './ts-project.js';
18
24
  import {
19
25
  analyzeOperationPath,
@@ -24,47 +30,6 @@ import {
24
30
  function lineOf(text: string, idx: number): number {
25
31
  return text.slice(0, idx).split('\n').length;
26
32
  }
27
- function entityFromExpression(expr: ts.Expression | undefined): string | undefined {
28
- if (!expr) return undefined;
29
- if (ts.isIdentifier(expr) || ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
30
- if (ts.isPropertyAccessExpression(expr) && expr.expression.kind === ts.SyntaxKind.ThisKeyword) return expr.name.text;
31
- if (ts.isElementAccessExpression(expr) && expr.argumentExpression && (ts.isStringLiteral(expr.argumentExpression) || ts.isNoSubstitutionTemplateLiteral(expr.argumentExpression))) return expr.argumentExpression.text;
32
- return undefined;
33
- }
34
- function expressionName(expr: ts.Expression): string {
35
- if (ts.isIdentifier(expr)) return expr.text;
36
- if (ts.isPropertyAccessExpression(expr)) return `${expressionName(expr.expression)}.${expr.name.text}`;
37
- return expr.getText();
38
- }
39
- function variableInitializers(source: ts.SourceFile): Map<string, ts.Expression> {
40
- const initializers = new Map<string, ts.Expression>();
41
- for (const statement of source.statements) {
42
- if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
43
- for (const declaration of statement.declarationList.declarations) {
44
- if (ts.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
45
- }
46
- }
47
- return initializers;
48
- }
49
- function unwrapQueryExpression(expr: ts.Expression): ts.Expression {
50
- if (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)
51
- || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)
52
- || ts.isNonNullExpression(expr) || ts.isSatisfiesExpression(expr))
53
- return unwrapQueryExpression(expr.expression);
54
- return expr;
55
- }
56
- function queryEntityFromAst(expr: ts.Expression, initializers = new Map<string, ts.Expression>()): string | undefined {
57
- const unwrapped = unwrapQueryExpression(expr);
58
- if (ts.isIdentifier(unwrapped) && initializers.has(unwrapped.text)) return queryEntityFromAst(initializers.get(unwrapped.text) as ts.Expression, initializers);
59
- if (ts.isCallExpression(unwrapped)) {
60
- const name = expressionName(unwrapped.expression);
61
- if (name === 'cds.run') return queryEntityFromAst(unwrapped.arguments[0], initializers);
62
- if (isCapQueryBuilderRootName(name)) return entityFromExpression(unwrapped.arguments[0]);
63
- const receiver = ts.isPropertyAccessExpression(unwrapped.expression) ? unwrapped.expression.expression : undefined;
64
- if (receiver) return queryEntityFromAst(receiver, initializers);
65
- }
66
- return undefined;
67
- }
68
33
  function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuilderStatement): Record<string, unknown> {
69
34
  return {
70
35
  classifier: 'cap_query_builder_direct',
@@ -92,18 +57,6 @@ function queryRunEvidence(
92
57
  } : {}),
93
58
  };
94
59
  }
95
- function extractQueryEntity(expr: string): string | undefined {
96
- const source = ts.createSourceFile('query.ts', `const __query = (${expr});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
97
- const initializers = variableInitializers(source);
98
- let found: string | undefined;
99
- const visit = (node: ts.Node): void => {
100
- if (found) return;
101
- if (ts.isParenthesizedExpression(node)) found = queryEntityFromAst(node.expression, initializers);
102
- ts.forEachChild(node, visit);
103
- };
104
- visit(source);
105
- return found;
106
- }
107
60
  function queryWarning(expr: string): string {
108
61
  if (/^\s*[`'"]/.test(expr)) return 'raw_sql_or_cql_expression';
109
62
  if (/^\s*\w+\s*$/.test(expr)) return 'query_variable_without_static_initializer';
@@ -140,8 +93,6 @@ function nameOfProperty(name: ts.PropertyName): string | undefined {
140
93
  type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
141
94
  type ExpressionSourceKind = 'string_literal' | 'no_substitution_template' | 'template_with_substitutions' | 'const_alias' | 'conditional_candidates' | 'dynamic_expression';
142
95
  interface ExpressionResolution { status: ExpressionStatus; sourceKind: ExpressionSourceKind; value?: string; rawExpression?: string; placeholderKeys: string[]; evidence: string[]; constName?: string }
143
- interface BindingResolution { declaration?: ts.VariableDeclaration | ts.ParameterDeclaration; initializer?: ts.Expression; immutable: boolean; evidence: string[] }
144
- const maxAliasDepth = 5;
145
96
  function safeRaw(expr: ts.Expression): string | undefined {
146
97
  if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr) || ts.isIdentifier(expr) || ts.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
147
98
  return undefined;
@@ -149,56 +100,6 @@ function safeRaw(expr: ts.Expression): string | undefined {
149
100
  function placeholders(expr: ts.TemplateExpression): string[] {
150
101
  return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
151
102
  }
152
- function isFunctionLikeScope(node: ts.Node): boolean {
153
- return ts.isFunctionLike(node) || ts.isSourceFile(node);
154
- }
155
- function nodeContains(parent: ts.Node, child: ts.Node): boolean {
156
- const source = child.getSourceFile();
157
- return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
158
- }
159
- function declarationScope(node: ts.VariableDeclaration | ts.ParameterDeclaration): ts.Node {
160
- if (ts.isParameter(node)) return node.parent;
161
- if (ts.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
162
- const list = node.parent;
163
- const blockScoped = (list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0;
164
- let current: ts.Node = list.parent;
165
- if (!blockScoped) {
166
- while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
167
- return current;
168
- }
169
- while (current.parent && !ts.isBlock(current) && !ts.isSourceFile(current) && !ts.isModuleBlock(current) && !ts.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
170
- return current;
171
- }
172
- function isLoopInitializerScope(declaration: ts.VariableDeclaration, scope: ts.Node): boolean {
173
- const list = declaration.parent;
174
- return (ts.isForStatement(scope) && scope.initializer === list) || ((ts.isForInStatement(scope) || ts.isForOfStatement(scope)) && scope.initializer === list);
175
- }
176
- function catchBindingScope(declaration: ts.VariableDeclaration | ts.ParameterDeclaration): ts.CatchClause | undefined {
177
- if (ts.isParameter(declaration)) return undefined;
178
- return ts.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : undefined;
179
- }
180
- function isAccessibleDeclaration(declaration: ts.VariableDeclaration | ts.ParameterDeclaration, use: ts.Node): boolean {
181
- const source = use.getSourceFile();
182
- if (declaration.name.getStart(source) >= use.getStart(source)) return false;
183
- const catchScope = catchBindingScope(declaration);
184
- if (catchScope) return nodeContains(catchScope.block, use);
185
- const scope = declarationScope(declaration);
186
- if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) return nodeContains(scope.statement, use);
187
- return ts.isSourceFile(scope) || nodeContains(scope, use);
188
- }
189
- function resolveBinding(identifier: ts.Identifier, use: ts.Node): BindingResolution {
190
- const source = use.getSourceFile();
191
- let best: ts.VariableDeclaration | ts.ParameterDeclaration | undefined;
192
- const visit = (node: ts.Node): void => {
193
- if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
194
- if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
195
- ts.forEachChild(node, visit);
196
- };
197
- visit(source);
198
- if (!best) return { immutable: false, evidence: ['binding_not_found'] };
199
- const immutable = ts.isVariableDeclaration(best) && (best.parent.flags & ts.NodeFlags.Const) !== 0;
200
- return { declaration: best, initializer: ts.isVariableDeclaration(best) ? best.initializer : undefined, immutable, evidence: [immutable ? 'lexical_const_binding_before_use' : 'lexical_mutable_or_parameter_binding'] };
201
- }
202
103
  function resolveExpression(expr: ts.Expression | undefined, use: ts.Node, policy: 'operation_path' | 'external' | 'literal', depth = 0, seen = new Set<ts.Node>()): ExpressionResolution {
203
104
  if (!expr) return { status: 'unknown', sourceKind: 'dynamic_expression', placeholderKeys: [], evidence: ['expression_missing'] };
204
105
  if (ts.isStringLiteral(expr)) return { status: 'static', sourceKind: 'string_literal', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['string_literal'] };
@@ -474,7 +375,7 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
474
375
  const objectArg = node.arguments[0];
475
376
  if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
476
377
  const receiver = receiverName(expr.expression);
477
- const query = objectPropertyText(objectArg, 'query');
378
+ const queryExpression = propertyInitializer(objectArg, 'query');
478
379
  const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, 'method'), node, 'literal').value ?? objectPropertyText(objectArg, 'method') ?? 'POST');
479
380
  const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
480
381
  const pathAnalysis = analyzeOperationPath(pathExpr, node, method);
@@ -485,8 +386,13 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
485
386
  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' };
486
387
  const entityCallType = entityCallTypes[intent.kind];
487
388
  const isODataQueryRead = method.toUpperCase() === 'GET' && ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
488
- const unresolvedReason = !query && pathExpr ? pathUnresolvedReason(pathAnalysis) : undefined;
489
- 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 }, { 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 });
389
+ const queryEntity = queryExpression
390
+ ? queryEntityFromAst(queryExpression, initializers)
391
+ : isODataQueryRead ? intent.entitySegment : undefined;
392
+ const unresolvedReason = queryExpression
393
+ ? queryEntity ? undefined : queryWarning(queryExpression.getText(source))
394
+ : pathExpr ? pathUnresolvedReason(pathAnalysis) : undefined;
395
+ 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 });
490
396
  } else {
491
397
  const receiver = receiverName(expr.expression);
492
398
  const rootReceiver = rootReceiverName(expr.expression);