@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.40
|
|
4
|
+
|
|
5
|
+
- Hardened operation path expression analysis to respect lexical scope, declaration order, aliases, and bounded branch candidates.
|
|
6
|
+
- Kept external URL and destination templates with substitutions dynamic with sanitized labels.
|
|
7
|
+
- Fixed CDS path annotation parsing for prefix/suffix annotations and supported service extension syntax.
|
|
8
|
+
|
|
3
9
|
## 0.1.35
|
|
4
10
|
|
|
5
11
|
- Hardened OData path precedence so entity key, navigation, and media/property paths with placeholders remain terminal entity evidence instead of dynamic operation candidates.
|
|
@@ -261,3 +267,4 @@
|
|
|
261
267
|
## 0.1.0
|
|
262
268
|
|
|
263
269
|
- Initial `@saptools/service-flow` CLI package for indexing and tracing SAP CAP service flows.
|
|
270
|
+
|
package/README.md
CHANGED
|
@@ -505,3 +505,7 @@ Decorator signals are interpreted as resolved, absent, unsupported, or malformed
|
|
|
505
505
|
External HTTP facts persist queryable target kind, stable id, safe label, and dynamic flag on `outbound_calls`; graph evidence uses the same sanitized model. URL user info, fragments, query values, headers, cookies, credentials, tokens, API keys, and request/response bodies are not persisted. Legacy workspaces may relink from old evidence when possible, but full semantic enrichment requires re-index plus relink when old parser facts did not contain safe target metadata.
|
|
506
506
|
|
|
507
507
|
Graph taxonomy now keeps CAP operation candidates separate from external HTTP targets. Dynamic, ambiguous, and unresolved CAP calls use operation-candidate semantics, external destinations/endpoints are produced only by `external_http` calls, and terminal local transport-client methods use transport-method semantics rather than external HTTP edges. `doctor --strict` reports external target quality and taxonomy consistency.
|
|
508
|
+
### Runtime and parser hardening
|
|
509
|
+
|
|
510
|
+
`service-flow` treats URL and destination templates with substitutions as dynamic external targets, preserves operation-path template evidence without promoting unrelated variables, and parses CDS `@(path: ...)` annotations from original source text. Service extension declarations using both `extend Name` and `extend service Name` are recognized for routing provenance.
|
|
511
|
+
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -142,3 +142,7 @@ Outbound call extraction is AST-based and ignores comments, block comments, and
|
|
|
142
142
|
## 0.1.35 OData placeholder semantics
|
|
143
143
|
|
|
144
144
|
Service-flow now records OData placeholders by semantic layer. Service-routing placeholders belong to service bindings and can make an operation edge dynamic until runtime variables are supplied. Operation invocation argument placeholders, such as action/function call arguments, remain operation evidence but are not service-routing variables. Entity key placeholders belong to entity addressing, so key, navigation, and media/property paths remain terminal remote entity/query edges unless indexed CDS operation evidence provides a credible operation match.
|
|
145
|
+
## 0.1.40 analyzer hardening
|
|
146
|
+
|
|
147
|
+
Expression resolution is consumer-specific: operation paths may retain template placeholders for OData normalization, while external URL and destination classifiers require literal or no-substitution-template evidence before a static target is persisted. Identifier resolution is bounded to the call-site lexical scope and ignores sibling scopes, later declarations, nested function bodies, and mutable or computed writes unless they are recorded as conservative candidate evidence.
|
|
148
|
+
|
|
@@ -187,16 +187,20 @@ function collectAnnotations(text, index) {
|
|
|
187
187
|
return { end: i, raw };
|
|
188
188
|
}
|
|
189
189
|
function pathAnnotation(raw) {
|
|
190
|
-
return /path\s*:\s*
|
|
190
|
+
return /path\s*:\s*(['"])(.*?)\1/s.exec(raw)?.[2];
|
|
191
191
|
}
|
|
192
|
-
function matchingBrace(
|
|
192
|
+
function matchingBrace(maskedText, open) {
|
|
193
193
|
let depth = 0;
|
|
194
|
-
for (let i = open; i <
|
|
195
|
-
if (
|
|
196
|
-
if (
|
|
194
|
+
for (let i = open; i < maskedText.length; i += 1) {
|
|
195
|
+
if (maskedText[i] === "{") depth += 1;
|
|
196
|
+
if (maskedText[i] === "}") depth -= 1;
|
|
197
197
|
if (depth === 0) return i;
|
|
198
198
|
}
|
|
199
|
-
return
|
|
199
|
+
return maskedText.length - 1;
|
|
200
|
+
}
|
|
201
|
+
function annotationRawAt(original, masked, index) {
|
|
202
|
+
const collected = collectAnnotations(masked, index);
|
|
203
|
+
return { end: collected.end, raw: original.slice(index, collected.end) };
|
|
200
204
|
}
|
|
201
205
|
function operationsFromBody(text, maskedBody, bodyOffset, filePath) {
|
|
202
206
|
return [...maskedBody.matchAll(/\b(action|function|event)\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?:returns\s+([^;{]+))?/g)].map((m) => ({
|
|
@@ -216,22 +220,24 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
216
220
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
217
221
|
const services = [];
|
|
218
222
|
const pendingAnnotations = [];
|
|
219
|
-
for (const a of masked.matchAll(/@\s*\(/g))
|
|
220
|
-
|
|
221
|
-
pendingAnnotations.push(annotation);
|
|
222
|
-
}
|
|
223
|
-
const serviceRegex = /\b(extend\s+)?service\s+([\w.]+)\b/g;
|
|
223
|
+
for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
|
|
224
|
+
const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
|
|
224
225
|
let match;
|
|
225
226
|
while (match = serviceRegex.exec(masked)) {
|
|
226
|
-
const
|
|
227
|
+
const isExtend = match[1] === "extend";
|
|
228
|
+
const hasServiceKeyword = match[2] === "service";
|
|
229
|
+
if (!isExtend && !hasServiceKeyword) continue;
|
|
230
|
+
const afterName = annotationRawAt(text, masked, serviceRegex.lastIndex);
|
|
227
231
|
const open = masked.indexOf("{", afterName.end);
|
|
228
232
|
if (open === -1) continue;
|
|
233
|
+
const between = masked.slice(afterName.end, open).trim();
|
|
234
|
+
if (between.length > 0) continue;
|
|
229
235
|
const matchIndex = match.index;
|
|
230
236
|
const prefix = pendingAnnotations.filter((a) => a.end <= matchIndex && matchIndex - a.end < 8).map((a) => a.raw).join("");
|
|
231
237
|
const annotations = `${prefix}${afterName.raw}`;
|
|
232
238
|
const end = matchingBrace(masked, open);
|
|
233
239
|
const body = masked.slice(open + 1, end);
|
|
234
|
-
const name = match[
|
|
240
|
+
const name = match[3] ?? "UnknownService";
|
|
235
241
|
const serviceName = name.split(".").pop() ?? name;
|
|
236
242
|
const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
|
|
237
243
|
services.push({
|
|
@@ -239,7 +245,7 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
239
245
|
serviceName,
|
|
240
246
|
qualifiedName: name.includes(".") ? name : namespace ? `${namespace}.${name}` : name,
|
|
241
247
|
servicePath,
|
|
242
|
-
isExtend
|
|
248
|
+
isExtend,
|
|
243
249
|
sourceFile: normalizePath(filePath),
|
|
244
250
|
sourceLine: lineOf(text, match.index),
|
|
245
251
|
operations: operationsFromBody(text, body, open + 1, filePath)
|
|
@@ -815,11 +821,12 @@ function expressionName(expr) {
|
|
|
815
821
|
}
|
|
816
822
|
function variableInitializers(source) {
|
|
817
823
|
const initializers = /* @__PURE__ */ new Map();
|
|
818
|
-
const
|
|
819
|
-
if (ts4.
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
824
|
+
for (const statement of source.statements) {
|
|
825
|
+
if (!ts4.isVariableStatement(statement) || (statement.declarationList.flags & ts4.NodeFlags.Const) === 0) continue;
|
|
826
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
827
|
+
if (ts4.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
823
830
|
return initializers;
|
|
824
831
|
}
|
|
825
832
|
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
@@ -876,48 +883,108 @@ function nameOfProperty(name) {
|
|
|
876
883
|
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
877
884
|
return void 0;
|
|
878
885
|
}
|
|
886
|
+
var maxAliasDepth = 5;
|
|
887
|
+
function safeRaw(expr) {
|
|
888
|
+
if (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr) || ts4.isIdentifier(expr) || ts4.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
889
|
+
return void 0;
|
|
890
|
+
}
|
|
891
|
+
function placeholders(expr) {
|
|
892
|
+
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
893
|
+
}
|
|
894
|
+
function isFunctionLikeScope(node) {
|
|
895
|
+
return ts4.isFunctionLike(node) || ts4.isSourceFile(node);
|
|
896
|
+
}
|
|
897
|
+
function containingScope(node) {
|
|
898
|
+
let current = node.parent;
|
|
899
|
+
while (current && !isFunctionLikeScope(current)) current = current.parent;
|
|
900
|
+
return current ?? node.getSourceFile();
|
|
901
|
+
}
|
|
902
|
+
function nodeContains(parent, child) {
|
|
903
|
+
return child.getStart(child.getSourceFile()) >= parent.getStart(parent.getSourceFile()) && child.getEnd() <= parent.getEnd();
|
|
904
|
+
}
|
|
905
|
+
function nestedFunctionContains(scope, candidate, use) {
|
|
906
|
+
let current = candidate.parent;
|
|
907
|
+
while (current && current !== scope) {
|
|
908
|
+
if (isFunctionLikeScope(current) && !nodeContains(current, use)) return true;
|
|
909
|
+
current = current.parent;
|
|
910
|
+
}
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
function resolveBinding(identifier, use) {
|
|
914
|
+
const scope = containingScope(use);
|
|
915
|
+
const source = use.getSourceFile();
|
|
916
|
+
let best;
|
|
917
|
+
const visit = (node) => {
|
|
918
|
+
if (node !== scope && isFunctionLikeScope(node)) return;
|
|
919
|
+
if (node.getStart(source) >= identifier.getStart(source)) return;
|
|
920
|
+
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
|
|
921
|
+
if (ts4.isParameter(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && !nestedFunctionContains(scope, node, use)) best = node;
|
|
922
|
+
ts4.forEachChild(node, visit);
|
|
923
|
+
};
|
|
924
|
+
visit(scope);
|
|
925
|
+
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
926
|
+
const immutable = ts4.isVariableDeclaration(best) && (best.parent.flags & ts4.NodeFlags.Const) !== 0;
|
|
927
|
+
return { declaration: best, initializer: ts4.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "const_binding_before_use" : "mutable_or_parameter_binding"] };
|
|
928
|
+
}
|
|
929
|
+
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
930
|
+
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
931
|
+
if (ts4.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
|
|
932
|
+
if (ts4.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
|
|
933
|
+
if (ts4.isTemplateExpression(expr)) {
|
|
934
|
+
const keys = placeholders(expr);
|
|
935
|
+
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"] };
|
|
936
|
+
return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
|
|
937
|
+
}
|
|
938
|
+
if (ts4.isIdentifier(expr)) {
|
|
939
|
+
if (depth >= maxAliasDepth) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
|
|
940
|
+
const binding = resolveBinding(expr, use);
|
|
941
|
+
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
942
|
+
if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
|
|
943
|
+
seen.add(binding.declaration);
|
|
944
|
+
const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
945
|
+
return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
|
|
946
|
+
}
|
|
947
|
+
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts4.SyntaxKind[expr.kind] ?? "expression"}`] };
|
|
948
|
+
}
|
|
879
949
|
function staticExpressionText(expr, initializers) {
|
|
880
950
|
if (!expr) return void 0;
|
|
881
951
|
if (isStringLike(expr)) return expr.text;
|
|
882
|
-
if (ts4.isTemplateExpression(expr)) return stripQuotes(expr.getText(expr.getSourceFile()));
|
|
883
952
|
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
884
953
|
return void 0;
|
|
885
954
|
}
|
|
886
|
-
function staticPathExpression(expr,
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
if (ts4.isTemplateExpression(expr)) return { text: stripQuotes(rawExpression), sourceKind: "template", rawExpression };
|
|
891
|
-
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) {
|
|
892
|
-
const resolved2 = staticPathExpression(initializers.get(expr.text), initializers);
|
|
893
|
-
return resolved2.text ? { ...resolved2, sourceKind: "const", rawExpression, constName: expr.text } : { sourceKind: "dynamic", rawExpression };
|
|
894
|
-
}
|
|
895
|
-
return { sourceKind: "dynamic", rawExpression };
|
|
955
|
+
function staticPathExpression(expr, use) {
|
|
956
|
+
const resolution = resolveExpression(expr, use, "operation_path");
|
|
957
|
+
const sourceKind = resolution.sourceKind === "const_alias" ? "const" : resolution.sourceKind === "template_with_substitutions" || resolution.sourceKind === "no_substitution_template" ? "template" : resolution.status === "static" ? "literal" : "dynamic";
|
|
958
|
+
return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
|
|
896
959
|
}
|
|
897
960
|
function operationPathFromStatic(text) {
|
|
898
961
|
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
899
962
|
}
|
|
900
|
-
function staticPathCandidates(
|
|
963
|
+
function staticPathCandidates(identifier, call, method) {
|
|
964
|
+
const binding = resolveBinding(identifier, call);
|
|
965
|
+
const declaration = binding.declaration;
|
|
966
|
+
if (!declaration) return void 0;
|
|
967
|
+
const scope = containingScope(call);
|
|
968
|
+
const source = call.getSourceFile();
|
|
901
969
|
const paths = [];
|
|
902
970
|
let hasDynamicAssignments = false;
|
|
971
|
+
const addExpr = (expr, origin) => {
|
|
972
|
+
const resolved2 = staticPathExpression(expr, origin);
|
|
973
|
+
if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
|
|
974
|
+
else hasDynamicAssignments = true;
|
|
975
|
+
};
|
|
976
|
+
if (ts4.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
903
977
|
const visit = (node) => {
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
else hasDynamicAssignments = true;
|
|
908
|
-
}
|
|
909
|
-
if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left) && node.left.text === identifier) {
|
|
910
|
-
const resolved2 = staticPathExpression(node.right, initializers);
|
|
911
|
-
if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
|
|
912
|
-
else hasDynamicAssignments = true;
|
|
913
|
-
}
|
|
978
|
+
if (node !== scope && isFunctionLikeScope(node)) return;
|
|
979
|
+
if (node.getStart(source) >= call.getStart(source)) return;
|
|
980
|
+
if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left) && node.left.text === identifier.text) addExpr(node.right, node);
|
|
914
981
|
ts4.forEachChild(node, visit);
|
|
915
982
|
};
|
|
916
|
-
visit(
|
|
983
|
+
visit(scope);
|
|
917
984
|
const candidatePaths = [...new Set(paths)];
|
|
918
985
|
if (candidatePaths.length === 0) return void 0;
|
|
919
|
-
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate,
|
|
920
|
-
return { candidatePaths, normalizedCandidateOperations, candidateSourceKind: "static candidates observed", candidateIdentifier: identifier, hasDynamicAssignments };
|
|
986
|
+
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
|
|
987
|
+
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" : void 0 };
|
|
921
988
|
}
|
|
922
989
|
function destinationExpressionShape(expr) {
|
|
923
990
|
if (!expr) return void 0;
|
|
@@ -944,57 +1011,61 @@ function propertyInitializer(object, key) {
|
|
|
944
1011
|
}
|
|
945
1012
|
return void 0;
|
|
946
1013
|
}
|
|
947
|
-
function httpMethodFromObject(object,
|
|
948
|
-
const text =
|
|
1014
|
+
function httpMethodFromObject(object, use) {
|
|
1015
|
+
const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
|
|
949
1016
|
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
950
1017
|
}
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1018
|
+
function hasTemplatePlaceholder(value) {
|
|
1019
|
+
return /\$\{|%7B|%7D/i.test(value);
|
|
1020
|
+
}
|
|
1021
|
+
function urlTargetFromExpression(expr, use) {
|
|
1022
|
+
const resolved2 = resolveExpression(expr, use, "external");
|
|
1023
|
+
if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1024
|
+
if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
|
|
955
1025
|
return { kind: "unknown", dynamic: false };
|
|
956
1026
|
}
|
|
957
|
-
function destinationTargetFromExpression(expr,
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
1027
|
+
function destinationTargetFromExpression(expr, use) {
|
|
1028
|
+
const resolved2 = resolveExpression(expr, use, "external");
|
|
1029
|
+
const text = resolved2.value;
|
|
1030
|
+
if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1031
|
+
const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
|
|
961
1032
|
if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
|
|
962
1033
|
const shape = destinationExpressionShape(expr);
|
|
963
1034
|
if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
|
|
964
1035
|
return void 0;
|
|
965
1036
|
}
|
|
966
|
-
function externalHttpEvidence(node, source
|
|
1037
|
+
function externalHttpEvidence(node, source) {
|
|
967
1038
|
const expr = node.expression;
|
|
968
1039
|
const exprText = expr.getText(source);
|
|
969
1040
|
if (exprText === "useOrFetchDestination") {
|
|
970
1041
|
const objectArg = node.arguments[0];
|
|
971
1042
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
972
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"),
|
|
1043
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
973
1044
|
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
974
1045
|
}
|
|
975
1046
|
}
|
|
976
1047
|
if (exprText === "executeHttpRequest") {
|
|
977
|
-
const destination = destinationTargetFromExpression(node.arguments[0],
|
|
1048
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
978
1049
|
const config = node.arguments[1];
|
|
979
|
-
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config,
|
|
980
|
-
const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"),
|
|
1050
|
+
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
|
|
1051
|
+
const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
|
|
981
1052
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
982
1053
|
}
|
|
983
1054
|
if (exprText === "axios") {
|
|
984
1055
|
const config = node.arguments[0];
|
|
985
1056
|
if (config && ts4.isObjectLiteralExpression(config)) {
|
|
986
|
-
const method = httpMethodFromObject(config,
|
|
987
|
-
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"),
|
|
1057
|
+
const method = httpMethodFromObject(config, node);
|
|
1058
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
988
1059
|
}
|
|
989
1060
|
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
990
1061
|
}
|
|
991
1062
|
if (exprText === "fetch") {
|
|
992
1063
|
const init = node.arguments[1];
|
|
993
|
-
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init,
|
|
994
|
-
return { method, externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
1064
|
+
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
|
|
1065
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
995
1066
|
}
|
|
996
1067
|
if (ts4.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
997
|
-
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
1068
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
998
1069
|
}
|
|
999
1070
|
return void 0;
|
|
1000
1071
|
}
|
|
@@ -1057,7 +1128,7 @@ function collectWrapperSpecs(source) {
|
|
|
1057
1128
|
const methodProp = propertyInitializer(objectArg, "method");
|
|
1058
1129
|
const pathName = pathProp && ts4.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
1059
1130
|
const methodName = methodProp && ts4.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
1060
|
-
const methodLiteral =
|
|
1131
|
+
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
1061
1132
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
1062
1133
|
}
|
|
1063
1134
|
}
|
|
@@ -1107,12 +1178,12 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1107
1178
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
1108
1179
|
const receiver = receiverName(expr.expression);
|
|
1109
1180
|
const query = objectPropertyText(objectArg, "query");
|
|
1110
|
-
const method = stripQuotes(objectPropertyText(objectArg, "method") ?? "POST");
|
|
1181
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
1111
1182
|
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
1112
|
-
const resolvedPath = staticPathExpression(pathExpr,
|
|
1183
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
1113
1184
|
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
|
|
1114
1185
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
1115
|
-
const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(
|
|
1186
|
+
const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
|
|
1116
1187
|
const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
|
|
1117
1188
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
1118
1189
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
@@ -1129,8 +1200,8 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1129
1200
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
1130
1201
|
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
1131
1202
|
const receiver = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
1132
|
-
const method = stripQuotes(
|
|
1133
|
-
const resolvedPath = staticPathExpression(pathArg,
|
|
1203
|
+
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
1204
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1134
1205
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1135
1206
|
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
1136
1207
|
if (spec && receiver && operationPathExpr) {
|
|
@@ -1146,7 +1217,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1146
1217
|
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" });
|
|
1147
1218
|
}
|
|
1148
1219
|
} else {
|
|
1149
|
-
const external = externalHttpEvidence(node, source
|
|
1220
|
+
const external = externalHttpEvidence(node, source);
|
|
1150
1221
|
if (external) {
|
|
1151
1222
|
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
1152
1223
|
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
@@ -1235,7 +1306,7 @@ function stringValue(node) {
|
|
|
1235
1306
|
return node.getText().replace(/^`|`$/g, "");
|
|
1236
1307
|
return node.getText();
|
|
1237
1308
|
}
|
|
1238
|
-
function
|
|
1309
|
+
function placeholders2(value) {
|
|
1239
1310
|
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
1240
1311
|
}
|
|
1241
1312
|
function connectFactFromCall(call) {
|
|
@@ -1261,7 +1332,7 @@ function connectFactFromCall(call) {
|
|
|
1261
1332
|
return {
|
|
1262
1333
|
aliasExpr,
|
|
1263
1334
|
isDynamic: true,
|
|
1264
|
-
placeholders:
|
|
1335
|
+
placeholders: placeholders2(aliasExpr)
|
|
1265
1336
|
};
|
|
1266
1337
|
let destinationExpr;
|
|
1267
1338
|
let servicePathExpr;
|
|
@@ -1279,9 +1350,9 @@ function connectFactFromCall(call) {
|
|
|
1279
1350
|
}
|
|
1280
1351
|
if (objectArg) visitObject(objectArg);
|
|
1281
1352
|
const ph = [
|
|
1282
|
-
...
|
|
1283
|
-
...
|
|
1284
|
-
...
|
|
1353
|
+
...placeholders2(aliasExpr ?? alias),
|
|
1354
|
+
...placeholders2(destinationExpr),
|
|
1355
|
+
...placeholders2(servicePathExpr)
|
|
1285
1356
|
];
|
|
1286
1357
|
return {
|
|
1287
1358
|
alias,
|
|
@@ -1931,9 +2002,9 @@ function extractPlaceholders(template) {
|
|
|
1931
2002
|
}
|
|
1932
2003
|
function substituteVariables(template, vars) {
|
|
1933
2004
|
if (!template) return { placeholders: [], missing: [], supplied: [], changed: false };
|
|
1934
|
-
const
|
|
1935
|
-
const supplied =
|
|
1936
|
-
const missing =
|
|
2005
|
+
const placeholders3 = [...new Set(extractPlaceholders(template))];
|
|
2006
|
+
const supplied = placeholders3.filter((key) => Object.hasOwn(vars, key));
|
|
2007
|
+
const missing = placeholders3.filter((key) => !Object.hasOwn(vars, key));
|
|
1937
2008
|
const effective = template.replace(PLACEHOLDER, (_m, key) => {
|
|
1938
2009
|
const trimmed = key.trim();
|
|
1939
2010
|
return Object.hasOwn(vars, trimmed) ? vars[trimmed] ?? "" : `\${${trimmed}}`;
|
|
@@ -1941,7 +2012,7 @@ function substituteVariables(template, vars) {
|
|
|
1941
2012
|
return {
|
|
1942
2013
|
original: template,
|
|
1943
2014
|
effective,
|
|
1944
|
-
placeholders:
|
|
2015
|
+
placeholders: placeholders3,
|
|
1945
2016
|
missing,
|
|
1946
2017
|
supplied,
|
|
1947
2018
|
changed: effective !== template
|
|
@@ -3120,4 +3191,4 @@ export {
|
|
|
3120
3191
|
linkWorkspace,
|
|
3121
3192
|
trace
|
|
3122
3193
|
};
|
|
3123
|
-
//# sourceMappingURL=chunk-
|
|
3194
|
+
//# sourceMappingURL=chunk-774PPGGK.js.map
|