@saptools/service-flow 0.1.38 → 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-WE3A6TOJ.js → chunk-774PPGGK.js} +173 -61
- 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 +147 -37
- package/dist/chunk-WE3A6TOJ.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,12 +90,115 @@ 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
163
|
if (ts.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
96
164
|
return undefined;
|
|
97
165
|
}
|
|
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 };
|
|
171
|
+
}
|
|
172
|
+
function operationPathFromStatic(text: string | undefined): string | undefined {
|
|
173
|
+
return text ? `/${stripQuotes(text).replace(/^\//, '')}` : undefined;
|
|
174
|
+
}
|
|
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();
|
|
182
|
+
const paths: string[] = [];
|
|
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);
|
|
190
|
+
const visit = (node: ts.Node): void => {
|
|
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);
|
|
194
|
+
ts.forEachChild(node, visit);
|
|
195
|
+
};
|
|
196
|
+
visit(scope);
|
|
197
|
+
const candidatePaths = [...new Set(paths)];
|
|
198
|
+
if (candidatePaths.length === 0) return undefined;
|
|
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 };
|
|
201
|
+
}
|
|
98
202
|
function destinationExpressionShape(expr: ts.Expression | undefined): string | undefined {
|
|
99
203
|
if (!expr) return undefined;
|
|
100
204
|
if (ts.isIdentifier(expr)) return 'identifier';
|
|
@@ -120,57 +224,59 @@ function propertyInitializer(object: ts.ObjectLiteralExpression, key: string): t
|
|
|
120
224
|
}
|
|
121
225
|
return undefined;
|
|
122
226
|
}
|
|
123
|
-
function httpMethodFromObject(object: ts.ObjectLiteralExpression,
|
|
124
|
-
const text =
|
|
227
|
+
function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node): string | undefined {
|
|
228
|
+
const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
|
|
125
229
|
return text ? stripQuotes(text).toUpperCase() : undefined;
|
|
126
230
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
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 };
|
|
131
236
|
return { kind: 'unknown', dynamic: false };
|
|
132
237
|
}
|
|
133
|
-
function destinationTargetFromExpression(expr: ts.Expression | undefined,
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
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>());
|
|
137
243
|
if (candidates) return { kind: 'destination', dynamic: true, expressionShape: 'conditional', candidateLiterals: candidates };
|
|
138
244
|
const shape = destinationExpressionShape(expr);
|
|
139
245
|
if (shape) return { kind: 'destination', dynamic: true, expressionShape: shape };
|
|
140
246
|
return undefined;
|
|
141
247
|
}
|
|
142
|
-
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 {
|
|
143
249
|
const expr = node.expression;
|
|
144
250
|
const exprText = expr.getText(source);
|
|
145
251
|
if (exprText === 'useOrFetchDestination') {
|
|
146
252
|
const objectArg = node.arguments[0];
|
|
147
253
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
148
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'),
|
|
254
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, 'destinationName'), node);
|
|
149
255
|
return { externalTarget: destination ?? { kind: 'unknown', dynamic: false }, classifier: 'sap_destination_lookup', sourceCallShape: 'useOrFetchDestination' };
|
|
150
256
|
}
|
|
151
257
|
}
|
|
152
258
|
if (exprText === 'executeHttpRequest') {
|
|
153
|
-
const destination = destinationTargetFromExpression(node.arguments[0],
|
|
259
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
154
260
|
const config = node.arguments[1];
|
|
155
|
-
const method = config && ts.isObjectLiteralExpression(config) ? httpMethodFromObject(config,
|
|
156
|
-
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 };
|
|
157
263
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: 'sap_execute_http_request', sourceCallShape: 'executeHttpRequest' };
|
|
158
264
|
}
|
|
159
265
|
if (exprText === 'axios') {
|
|
160
266
|
const config = node.arguments[0];
|
|
161
267
|
if (config && ts.isObjectLiteralExpression(config)) {
|
|
162
|
-
const method = httpMethodFromObject(config,
|
|
163
|
-
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)' };
|
|
164
270
|
}
|
|
165
271
|
return { externalTarget: { kind: 'unknown', dynamic: false }, classifier: 'axios_unknown_call', sourceCallShape: 'axios(...)' };
|
|
166
272
|
}
|
|
167
273
|
if (exprText === 'fetch') {
|
|
168
274
|
const init = node.arguments[1];
|
|
169
|
-
const method = init && ts.isObjectLiteralExpression(init) ? httpMethodFromObject(init,
|
|
170
|
-
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' };
|
|
171
277
|
}
|
|
172
278
|
if (ts.isPropertyAccessExpression(expr) && ['get','post','put','patch','delete','head'].includes(expr.name.text) && expr.expression.getText(source) === 'axios') {
|
|
173
|
-
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}` };
|
|
174
280
|
}
|
|
175
281
|
return undefined;
|
|
176
282
|
}
|
|
@@ -235,7 +341,7 @@ function collectWrapperSpecs(source: ts.SourceFile): Map<string, WrapperSpec> {
|
|
|
235
341
|
const methodProp = propertyInitializer(objectArg, 'method');
|
|
236
342
|
const pathName = pathProp && ts.isIdentifier(pathProp) ? pathProp.text : undefined;
|
|
237
343
|
const methodName = methodProp && ts.isIdentifier(methodProp) ? methodProp.text : undefined;
|
|
238
|
-
const methodLiteral =
|
|
344
|
+
const methodLiteral = resolveExpression(methodProp, node, 'literal').value;
|
|
239
345
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
240
346
|
}
|
|
241
347
|
}
|
|
@@ -285,18 +391,19 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
285
391
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|
|
286
392
|
const receiver = receiverName(expr.expression);
|
|
287
393
|
const query = objectPropertyText(objectArg, 'query');
|
|
288
|
-
const method = stripQuotes(objectPropertyText(objectArg, 'method') ?? 'POST');
|
|
394
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, 'method'), node, 'literal').value ?? objectPropertyText(objectArg, 'method') ?? 'POST');
|
|
289
395
|
const pathExpr = propertyInitializer(objectArg, 'path') ?? propertyInitializer(objectArg, 'event');
|
|
290
|
-
const
|
|
396
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
397
|
+
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : undefined;
|
|
291
398
|
const shorthandPath = objectPropertyIsShorthand(objectArg, 'path');
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const operationPathExpr =
|
|
399
|
+
const candidateEvidence = pathExpr && ts.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : undefined;
|
|
400
|
+
const candidateOperationPath = candidateEvidence && !candidateEvidence.hasDynamicAssignments && candidateEvidence.normalizedCandidateOperations.length === 1 && candidateEvidence.candidatePaths.length > 0 ? candidateEvidence.candidatePaths[0] : undefined;
|
|
401
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
295
402
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
296
403
|
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' };
|
|
297
404
|
const entityCallType = entityCallTypes[intent.kind];
|
|
298
405
|
const isODataQueryRead = method.toUpperCase() === 'GET' && ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
|
|
299
|
-
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: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, literalPathSource:
|
|
406
|
+
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: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? (resolvedPath.sourceKind === 'const' ? 'same_scope_const_initializer' : resolvedPath.sourceKind) : undefined, odataPathIntent: operationPathExpr ? intent : undefined, staticPathCandidates: candidateEvidence, parserWarning: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined });
|
|
300
407
|
}
|
|
301
408
|
} else if (((ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) || (ts.isIdentifier(expr) && wrapperSpecs.has(expr.text)))) {
|
|
302
409
|
const wrapperName = ts.isIdentifier(expr) ? expr.text : ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) ? expr.expression.text : '';
|
|
@@ -306,11 +413,14 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
306
413
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : undefined;
|
|
307
414
|
const methodArg = spec?.methodIndex === undefined ? undefined : wrapperArgs[spec.methodIndex];
|
|
308
415
|
const receiver = clientArg && ts.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
309
|
-
const method = stripQuotes(
|
|
310
|
-
|
|
311
|
-
|
|
416
|
+
const method = stripQuotes(resolveExpression(methodArg, node, 'literal').value ?? spec?.methodLiteral ?? 'POST');
|
|
417
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
418
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
419
|
+
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : undefined;
|
|
420
|
+
if (spec && receiver && operationPathExpr) {
|
|
421
|
+
add(node, { callType: 'remote_action', serviceVariableName: receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.75 }, { receiver, classifier: resolvedPath.sourceKind === 'literal' ? 'higher_order_wrapper_literal_path' : 'higher_order_wrapper_static_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, normalizedOperationPath, literalPathSource: resolvedPath.sourceKind === 'const' ? 'same_scope_const_initializer' : `wrapper_call_${resolvedPath.sourceKind}`, literalCallerArgumentDetected: true });
|
|
312
422
|
} else if (spec && receiver) {
|
|
313
|
-
add(node, { callType: 'remote_action', serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: 'dynamic_operation_path_identifier' }, { receiver, classifier: 'higher_order_wrapper_dynamic_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), parserWarning: 'dynamic_operation_path_identifier' });
|
|
423
|
+
add(node, { callType: 'remote_action', serviceVariableName: receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.45, unresolvedReason: 'dynamic_operation_path_identifier' }, { receiver, classifier: 'higher_order_wrapper_dynamic_path', wrapperFunction: wrapperName, wrapperDefinitionLine: spec.definitionLine, callerLine: lineOf(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: 'dynamic_operation_path_identifier' });
|
|
314
424
|
}
|
|
315
425
|
} else if (ts.isPropertyAccessExpression(expr) && ['emit', 'publish', 'on'].includes(expr.name.text)) {
|
|
316
426
|
const receiver = receiverName(expr.expression);
|
|
@@ -320,7 +430,7 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
320
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' });
|
|
321
431
|
}
|
|
322
432
|
} else {
|
|
323
|
-
const external = externalHttpEvidence(node, source
|
|
433
|
+
const external = externalHttpEvidence(node, source);
|
|
324
434
|
if (external) {
|
|
325
435
|
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
326
436
|
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|