@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.
- package/CHANGELOG.md +11 -0
- package/dist/{chunk-CRSSXWQ2.js → chunk-BGD7UYJN.js} +433 -166
- package/dist/chunk-BGD7UYJN.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/parsers/000-direct-query-execution.ts +2 -0
- package/src/parsers/001-query-entity-resolution.ts +622 -0
- package/src/parsers/outbound-call-parser.ts +15 -109
- package/dist/chunk-CRSSXWQ2.js.map +0 -1
|
@@ -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
|
|
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
|
|
489
|
-
|
|
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);
|