@saptools/service-flow 0.1.39 → 0.1.40
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 +7 -0
- package/README.md +4 -0
- package/TECHNICAL-NOTE.md +4 -0
- package/dist/{chunk-SAZ5OK7R.js → chunk-774PPGGK.js} +152 -81
- package/dist/chunk-774PPGGK.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/cds-parser.ts +20 -14
- package/src/parsers/outbound-call-parser.ts +126 -59
- package/dist/chunk-SAZ5OK7R.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -73,17 +73,21 @@ function collectAnnotations(text: string, index: number): { end: number; raw: st
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function pathAnnotation(raw: string): string | undefined {
|
|
76
|
-
return /path\s*:\s*
|
|
76
|
+
return /path\s*:\s*(['"])(.*?)\1/s.exec(raw)?.[2];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function matchingBrace(
|
|
79
|
+
function matchingBrace(maskedText: string, open: number): number {
|
|
80
80
|
let depth = 0;
|
|
81
|
-
for (let i = open; i <
|
|
82
|
-
if (
|
|
83
|
-
if (
|
|
81
|
+
for (let i = open; i < maskedText.length; i += 1) {
|
|
82
|
+
if (maskedText[i] === '{') depth += 1;
|
|
83
|
+
if (maskedText[i] === '}') depth -= 1;
|
|
84
84
|
if (depth === 0) return i;
|
|
85
85
|
}
|
|
86
|
-
return
|
|
86
|
+
return maskedText.length - 1;
|
|
87
|
+
}
|
|
88
|
+
function annotationRawAt(original: string, masked: string, index: number): { end: number; raw: string } {
|
|
89
|
+
const collected = collectAnnotations(masked, index);
|
|
90
|
+
return { end: collected.end, raw: original.slice(index, collected.end) };
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
function operationsFromBody(text: string, maskedBody: string, bodyOffset: number, filePath: string): CdsOperationFact[] {
|
|
@@ -105,16 +109,18 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
|
|
|
105
109
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
106
110
|
const services: CdsServiceFact[] = [];
|
|
107
111
|
const pendingAnnotations: Array<{ end: number; raw: string }> = [];
|
|
108
|
-
for (const a of masked.matchAll(/@\s*\(/g))
|
|
109
|
-
|
|
110
|
-
pendingAnnotations.push(annotation);
|
|
111
|
-
}
|
|
112
|
-
const serviceRegex = /\b(extend\s+)?service\s+([\w.]+)\b/g;
|
|
112
|
+
for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
|
|
113
|
+
const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
|
|
113
114
|
let match: RegExpExecArray | null;
|
|
114
115
|
while ((match = serviceRegex.exec(masked))) {
|
|
115
|
-
const
|
|
116
|
+
const isExtend = match[1] === 'extend';
|
|
117
|
+
const hasServiceKeyword = match[2] === 'service';
|
|
118
|
+
if (!isExtend && !hasServiceKeyword) continue;
|
|
119
|
+
const afterName = annotationRawAt(text, masked, serviceRegex.lastIndex);
|
|
116
120
|
const open = masked.indexOf('{', afterName.end);
|
|
117
121
|
if (open === -1) continue;
|
|
122
|
+
const between = masked.slice(afterName.end, open).trim();
|
|
123
|
+
if (between.length > 0) continue;
|
|
118
124
|
const matchIndex = match.index;
|
|
119
125
|
const prefix = pendingAnnotations
|
|
120
126
|
.filter((a) => a.end <= matchIndex && matchIndex - a.end < 8)
|
|
@@ -123,7 +129,7 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
|
|
|
123
129
|
const annotations = `${prefix}${afterName.raw}`;
|
|
124
130
|
const end = matchingBrace(masked, open);
|
|
125
131
|
const body = masked.slice(open + 1, end);
|
|
126
|
-
const name = match[
|
|
132
|
+
const name = match[3] ?? 'UnknownService';
|
|
127
133
|
const serviceName = name.split('.').pop() ?? name;
|
|
128
134
|
const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
|
|
129
135
|
services.push({
|
|
@@ -131,7 +137,7 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
|
|
|
131
137
|
serviceName,
|
|
132
138
|
qualifiedName: name.includes('.') ? name : namespace ? `${namespace}.${name}` : name,
|
|
133
139
|
servicePath,
|
|
134
|
-
isExtend
|
|
140
|
+
isExtend,
|
|
135
141
|
sourceFile: normalizePath(filePath),
|
|
136
142
|
sourceLine: lineOf(text, match.index),
|
|
137
143
|
operations: operationsFromBody(text, body, open + 1, filePath)
|
|
@@ -23,11 +23,12 @@ function expressionName(expr: ts.Expression): string {
|
|
|
23
23
|
}
|
|
24
24
|
function variableInitializers(source: ts.SourceFile): Map<string, ts.Expression> {
|
|
25
25
|
const initializers = new Map<string, ts.Expression>();
|
|
26
|
-
const
|
|
27
|
-
if (ts.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
for (const statement of source.statements) {
|
|
27
|
+
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
28
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
29
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
31
32
|
return initializers;
|
|
32
33
|
}
|
|
33
34
|
function queryEntityFromAst(expr: ts.Expression, initializers = new Map<string, ts.Expression>()): string | undefined {
|
|
@@ -89,50 +90,114 @@ function nameOfProperty(name: ts.PropertyName): string | undefined {
|
|
|
89
90
|
return undefined;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
|
|
94
|
+
type ExpressionSourceKind = 'string_literal' | 'no_substitution_template' | 'template_with_substitutions' | 'const_alias' | 'conditional_candidates' | 'dynamic_expression';
|
|
95
|
+
interface ExpressionResolution { status: ExpressionStatus; sourceKind: ExpressionSourceKind; value?: string; rawExpression?: string; placeholderKeys: string[]; evidence: string[]; constName?: string }
|
|
96
|
+
interface BindingResolution { declaration?: ts.VariableDeclaration | ts.ParameterDeclaration; initializer?: ts.Expression; immutable: boolean; evidence: string[] }
|
|
97
|
+
const maxAliasDepth = 5;
|
|
98
|
+
function safeRaw(expr: ts.Expression): string | undefined {
|
|
99
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr) || ts.isIdentifier(expr) || ts.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function placeholders(expr: ts.TemplateExpression): string[] {
|
|
103
|
+
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
104
|
+
}
|
|
105
|
+
function isFunctionLikeScope(node: ts.Node): boolean {
|
|
106
|
+
return ts.isFunctionLike(node) || ts.isSourceFile(node);
|
|
107
|
+
}
|
|
108
|
+
function containingScope(node: ts.Node): ts.Node {
|
|
109
|
+
let current: ts.Node | undefined = node.parent;
|
|
110
|
+
while (current && !isFunctionLikeScope(current)) current = current.parent;
|
|
111
|
+
return current ?? node.getSourceFile();
|
|
112
|
+
}
|
|
113
|
+
function nodeContains(parent: ts.Node, child: ts.Node): boolean {
|
|
114
|
+
return child.getStart(child.getSourceFile()) >= parent.getStart(parent.getSourceFile()) && child.getEnd() <= parent.getEnd();
|
|
115
|
+
}
|
|
116
|
+
function nestedFunctionContains(scope: ts.Node, candidate: ts.Node, use: ts.Node): boolean {
|
|
117
|
+
let current = candidate.parent;
|
|
118
|
+
while (current && current !== scope) {
|
|
119
|
+
if (isFunctionLikeScope(current) && !nodeContains(current, use)) return true;
|
|
120
|
+
current = current.parent;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function resolveBinding(identifier: ts.Identifier, use: ts.Node): BindingResolution {
|
|
125
|
+
const scope = containingScope(use);
|
|
126
|
+
const source = use.getSourceFile();
|
|
127
|
+
let best: ts.VariableDeclaration | ts.ParameterDeclaration | undefined;
|
|
128
|
+
const visit = (node: ts.Node): void => {
|
|
129
|
+
if (node !== scope && isFunctionLikeScope(node)) return;
|
|
130
|
+
if (node.getStart(source) >= identifier.getStart(source)) return;
|
|
131
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
|
|
132
|
+
if (ts.isParameter(node) && ts.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
|
|
133
|
+
ts.forEachChild(node, visit);
|
|
134
|
+
};
|
|
135
|
+
visit(scope);
|
|
136
|
+
if (!best) return { immutable: false, evidence: ['binding_not_found'] };
|
|
137
|
+
const immutable = ts.isVariableDeclaration(best) && (best.parent.flags & ts.NodeFlags.Const) !== 0;
|
|
138
|
+
return { declaration: best, initializer: ts.isVariableDeclaration(best) ? best.initializer : undefined, immutable, evidence: [immutable ? 'const_binding_before_use' : 'mutable_or_parameter_binding'] };
|
|
139
|
+
}
|
|
140
|
+
function resolveExpression(expr: ts.Expression | undefined, use: ts.Node, policy: 'operation_path' | 'external' | 'literal', depth = 0, seen = new Set<ts.Node>()): ExpressionResolution {
|
|
141
|
+
if (!expr) return { status: 'unknown', sourceKind: 'dynamic_expression', placeholderKeys: [], evidence: ['expression_missing'] };
|
|
142
|
+
if (ts.isStringLiteral(expr)) return { status: 'static', sourceKind: 'string_literal', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['string_literal'] };
|
|
143
|
+
if (ts.isNoSubstitutionTemplateLiteral(expr)) return { status: 'static', sourceKind: 'no_substitution_template', value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['no_substitution_template'] };
|
|
144
|
+
if (ts.isTemplateExpression(expr)) {
|
|
145
|
+
const keys = placeholders(expr);
|
|
146
|
+
if (policy === 'operation_path') return { status: 'dynamic', sourceKind: 'template_with_substitutions', value: stripQuotes(expr.getText(expr.getSourceFile())), rawExpression: safeRaw(expr), placeholderKeys: keys, evidence: ['operation_path_template_placeholders_retained'] };
|
|
147
|
+
return { status: 'dynamic', sourceKind: 'template_with_substitutions', placeholderKeys: keys, evidence: ['template_substitutions_not_static_external_target'] };
|
|
148
|
+
}
|
|
149
|
+
if (ts.isIdentifier(expr)) {
|
|
150
|
+
if (depth >= maxAliasDepth) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_depth_exceeded'], constName: expr.text };
|
|
151
|
+
const binding = resolveBinding(expr, use);
|
|
152
|
+
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
153
|
+
if (seen.has(binding.declaration)) return { status: 'unknown', sourceKind: 'const_alias', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ['alias_cycle_detected'], constName: expr.text };
|
|
154
|
+
seen.add(binding.declaration);
|
|
155
|
+
const resolved = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
156
|
+
return { ...resolved, sourceKind: 'const_alias', rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved.evidence] };
|
|
157
|
+
}
|
|
158
|
+
return { status: 'dynamic', sourceKind: 'dynamic_expression', rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts.SyntaxKind[expr.kind] ?? 'expression'}`] };
|
|
159
|
+
}
|
|
92
160
|
function staticExpressionText(expr: ts.Expression | undefined, initializers: Map<string, ts.Expression>): string | undefined {
|
|
93
161
|
if (!expr) return undefined;
|
|
94
162
|
if (isStringLike(expr)) return expr.text;
|
|
95
|
-
if (ts.isTemplateExpression(expr)) return stripQuotes(expr.getText(expr.getSourceFile()));
|
|
96
163
|
if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
97
164
|
return undefined;
|
|
98
165
|
}
|
|
99
|
-
interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string }
|
|
100
|
-
function staticPathExpression(expr: ts.Expression | undefined,
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
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 };
|
|
166
|
+
interface StaticPathResolution { text?: string; sourceKind: 'literal' | 'template' | 'const' | 'dynamic'; rawExpression?: string; constName?: string; resolution: ExpressionResolution }
|
|
167
|
+
function staticPathExpression(expr: ts.Expression | undefined, use: ts.Node): StaticPathResolution {
|
|
168
|
+
const resolution = resolveExpression(expr, use, 'operation_path');
|
|
169
|
+
const sourceKind = resolution.sourceKind === 'const_alias' ? 'const' : resolution.sourceKind === 'template_with_substitutions' || resolution.sourceKind === 'no_substitution_template' ? 'template' : resolution.status === 'static' ? 'literal' : 'dynamic';
|
|
170
|
+
return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
|
|
110
171
|
}
|
|
111
172
|
function operationPathFromStatic(text: string | undefined): string | undefined {
|
|
112
173
|
return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
|
|
113
174
|
}
|
|
114
|
-
interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean }
|
|
115
|
-
function staticPathCandidates(
|
|
175
|
+
interface PathCandidateEvidence { candidatePaths: string[]; normalizedCandidateOperations: string[]; candidateSourceKind: string; candidateIdentifier: string; hasDynamicAssignments: boolean; conservativeReason?: string }
|
|
176
|
+
function staticPathCandidates(identifier: ts.Identifier, call: ts.Node, method: string): PathCandidateEvidence | undefined {
|
|
177
|
+
const binding = resolveBinding(identifier, call);
|
|
178
|
+
const declaration = binding.declaration;
|
|
179
|
+
if (!declaration) return undefined;
|
|
180
|
+
const scope = containingScope(call);
|
|
181
|
+
const source = call.getSourceFile();
|
|
116
182
|
const paths: string[] = [];
|
|
117
183
|
let hasDynamicAssignments = false;
|
|
184
|
+
const addExpr = (expr: ts.Expression | undefined, origin: ts.Node): void => {
|
|
185
|
+
const resolved = staticPathExpression(expr, origin);
|
|
186
|
+
if (resolved.text) paths.push(operationPathFromStatic(resolved.text) ?? resolved.text);
|
|
187
|
+
else hasDynamicAssignments = true;
|
|
188
|
+
};
|
|
189
|
+
if (ts.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
118
190
|
const visit = (node: ts.Node): void => {
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
}
|
|
191
|
+
if (node !== scope && isFunctionLikeScope(node)) return;
|
|
192
|
+
if (node.getStart(source) >= call.getStart(source)) return;
|
|
193
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left) && node.left.text === identifier.text) addExpr(node.right, node);
|
|
129
194
|
ts.forEachChild(node, visit);
|
|
130
195
|
};
|
|
131
|
-
visit(
|
|
196
|
+
visit(scope);
|
|
132
197
|
const candidatePaths = [...new Set(paths)];
|
|
133
198
|
if (candidatePaths.length === 0) return undefined;
|
|
134
|
-
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate,
|
|
135
|
-
return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: 'static candidates observed', candidateIdentifier: identifier, hasDynamicAssignments };
|
|
199
|
+
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value): value is string => Boolean(value)))];
|
|
200
|
+
return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: 'static candidates observed before call in lexical scope', candidateIdentifier: identifier.text, hasDynamicAssignments, conservativeReason: hasDynamicAssignments ? 'dynamic_assignment_observed' : normalizedCandidateOperations.length > 1 ? 'candidate_tie' : undefined };
|
|
136
201
|
}
|
|
137
202
|
function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
|
|
138
203
|
if (!expr) return undefined;
|
|
@@ -159,57 +224,59 @@ function propertyInitializer(object: ts.ObjectLiteralExpression, key: string): t
|
|
|
159
224
|
}
|
|
160
225
|
return undefined;
|
|
161
226
|
}
|
|
162
|
-
function httpMethodFromObject(object: ts.ObjectLiteralExpression,
|
|
163
|
-
const text =
|
|
227
|
+
function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node): string | undefined {
|
|
228
|
+
const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
|
|
164
229
|
return text ? stripQuotes(text).toUpperCase() : undefined;
|
|
165
230
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
231
|
+
function hasTemplatePlaceholder(value: string): boolean { return /\$\{|%7B|%7D/i.test(value); }
|
|
232
|
+
function urlTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> {
|
|
233
|
+
const resolved = resolveExpression(expr, use, 'external');
|
|
234
|
+
if (resolved.status === 'static' && resolved.value && !hasTemplatePlaceholder(resolved.value)) return { kind: 'static_url', expression: resolved.value, dynamic: false, sourceKind: resolved.sourceKind };
|
|
235
|
+
if (expr) return { kind: 'url_expression', dynamic: true, expression: `${resolved.sourceKind}:${resolved.placeholderKeys.join('|')}`, expressionShape: resolved.sourceKind, placeholderKeys: resolved.placeholderKeys };
|
|
170
236
|
return { kind: 'unknown', dynamic: false };
|
|
171
237
|
}
|
|
172
|
-
function destinationTargetFromExpression(expr: ts.Expression | undefined,
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
238
|
+
function destinationTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> | undefined {
|
|
239
|
+
const resolved = resolveExpression(expr, use, 'external');
|
|
240
|
+
const text = resolved.value;
|
|
241
|
+
if (resolved.status === 'static' && text && !hasTemplatePlaceholder(text)) return { kind: 'destination', expression: text, dynamic: false, sourceKind: resolved.sourceKind };
|
|
242
|
+
const candidates = staticConditionalCandidates(expr, new Map<string, ts.Expression>());
|
|
176
243
|
if (candidates) return { kind: 'destination', dynamic: true, expressionShape: 'conditional', candidateLiterals: candidates };
|
|
177
244
|
const shape = destinationExpressionShape(expr);
|
|
178
245
|
if (shape) return { kind: 'destination', dynamic: true, expressionShape: shape };
|
|
179
246
|
return undefined;
|
|
180
247
|
}
|
|
181
|
-
function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile
|
|
248
|
+
function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile): { method?: string; externalTarget: Record<string, unknown>; classifier: string; sourceCallShape: string } | undefined {
|
|
182
249
|
const expr = node.expression;
|
|
183
250
|
const exprText = expr.getText(source);
|
|
184
251
|
if (exprText === 'useOrFetchDestination') {
|
|
185
252
|
const objectArg = node.arguments[0];
|
|
186
253
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
187
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'),
|
|
254
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), node);
|
|
188
255
|
return { externalTarget: destination ?? { kind: 'unknown', dynamic: false }, classifier: 'sap_destination_lookup', sourceCallShape: 'useOrFetchDestination' };
|
|
189
256
|
}
|
|
190
257
|
}
|
|
191
258
|
if (exprText === 'executeHttpRequest') {
|
|
192
|
-
const destination = destinationTargetFromExpression(node.arguments[0],
|
|
259
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
193
260
|
const config = node.arguments[1];
|
|
194
|
-
const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config,
|
|
195
|
-
const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'),
|
|
261
|
+
const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : undefined;
|
|
262
|
+
const url = config && ts.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, 'url'), node) : { kind: 'unknown', dynamic: false };
|
|
196
263
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: 'sap_execute_http_request', sourceCallShape: 'executeHttpRequest' };
|
|
197
264
|
}
|
|
198
265
|
if (exprText === 'axios') {
|
|
199
266
|
const config = node.arguments[0];
|
|
200
267
|
if (config && ts.isObjectLiteralExpression(config)) {
|
|
201
|
-
const method = httpMethodFromObject(config,
|
|
202
|
-
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'),
|
|
268
|
+
const method = httpMethodFromObject(config, node);
|
|
269
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, 'url'), node), classifier: 'axios_config_call', sourceCallShape: 'axios(config)' };
|
|
203
270
|
}
|
|
204
271
|
return { externalTarget: { kind: 'unknown', dynamic: false }, classifier: 'axios_unknown_call', sourceCallShape: 'axios(...)' };
|
|
205
272
|
}
|
|
206
273
|
if (exprText === 'fetch') {
|
|
207
274
|
const init = node.arguments[1];
|
|
208
|
-
const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init,
|
|
209
|
-
return { method, externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
275
|
+
const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : undefined;
|
|
276
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'fetch_call', sourceCallShape: 'fetch' };
|
|
210
277
|
}
|
|
211
278
|
if (ts.isPropertyAccessExpression(expr) && ['get','post','put','patch','delete','head'].includes(expr.name.text) && expr.expression.getText(source) === 'axios') {
|
|
212
|
-
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
279
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: 'axios_member_call', sourceCallShape: `axios.${expr.name.text}` };
|
|
213
280
|
}
|
|
214
281
|
return undefined;
|
|
215
282
|
}
|
|
@@ -274,7 +341,7 @@ function collectWrapperSpecs(source: ts.SourceFile): Map<string, WrapperSpec> {
|
|
|
274
341
|
const methodProp = propertyInitializer(objectArg, 'method');
|
|
275
342
|
const pathName = pathProp && ts.isIdentifier(pathProp) ? pathProp.text : undefined;
|
|
276
343
|
const methodName = methodProp && ts.isIdentifier(methodProp) ? methodProp.text : undefined;
|
|
277
|
-
const methodLiteral =
|
|
344
|
+
const methodLiteral = resolveExpression(methodProp, node, 'literal').value;
|
|
278
345
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
279
346
|
}
|
|
280
347
|
}
|
|
@@ -324,12 +391,12 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
324
391
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
325
392
|
const receiver = receiverName(expr.expression);
|
|
326
393
|
const query = objectPropertyText(objectArg, 'query');
|
|
327
|
-
const method = stripQuotes(objectPropertyText(objectArg, 'method') ?? 'POST');
|
|
394
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, 'method'), node, 'literal').value ?? objectPropertyText(objectArg, 'method') ?? 'POST');
|
|
328
395
|
const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
|
|
329
|
-
const resolvedPath = staticPathExpression(pathExpr,
|
|
396
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
330
397
|
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : undefined;
|
|
331
398
|
const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
|
|
332
|
-
const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(
|
|
399
|
+
const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : undefined;
|
|
333
400
|
const candidateOperationPath = candidateEvidence && !candidateEvidence.hasDynamicAssignments && candidateEvidence.normalizedCandidateOperations.length === 1 && candidateEvidence.candidatePaths.length > 0 ? candidateEvidence.candidatePaths[0] : undefined;
|
|
334
401
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
335
402
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
@@ -346,8 +413,8 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
346
413
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : undefined;
|
|
347
414
|
const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
|
|
348
415
|
const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
349
|
-
const method = stripQuotes(
|
|
350
|
-
const resolvedPath = staticPathExpression(pathArg,
|
|
416
|
+
const method = stripQuotes(resolveExpression(methodArg, node, 'literal').value ?? spec?.methodLiteral ?? 'POST');
|
|
417
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
351
418
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
352
419
|
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
|
|
353
420
|
if (spec && receiver && operationPathExpr) {
|
|
@@ -363,7 +430,7 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
363
430
|
if (eventName) add(node, { callType: expr.name.text === 'on' ? 'async_subscribe' : 'async_emit', serviceVariableName: rootReceiver ?? receiver, eventNameExpr: eventName }, { receiver, rootReceiver, classifier: expr.name.text === 'on' ? 'cap_service_event_subscription' : 'cap_service_event_emit', receiverClassification: 'cap_evidence' });
|
|
364
431
|
}
|
|
365
432
|
} else {
|
|
366
|
-
const external = externalHttpEvidence(node, source
|
|
433
|
+
const external = externalHttpEvidence(node, source);
|
|
367
434
|
if (external) {
|
|
368
435
|
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
369
436
|
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|