@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/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,12 +883,109 @@ 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
952
|
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
883
953
|
return void 0;
|
|
884
954
|
}
|
|
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 };
|
|
959
|
+
}
|
|
960
|
+
function operationPathFromStatic(text) {
|
|
961
|
+
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
962
|
+
}
|
|
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();
|
|
969
|
+
const paths = [];
|
|
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);
|
|
977
|
+
const visit = (node) => {
|
|
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);
|
|
981
|
+
ts4.forEachChild(node, visit);
|
|
982
|
+
};
|
|
983
|
+
visit(scope);
|
|
984
|
+
const candidatePaths = [...new Set(paths)];
|
|
985
|
+
if (candidatePaths.length === 0) return void 0;
|
|
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 };
|
|
988
|
+
}
|
|
885
989
|
function destinationExpressionShape(expr) {
|
|
886
990
|
if (!expr) return void 0;
|
|
887
991
|
if (ts4.isIdentifier(expr)) return "identifier";
|
|
@@ -907,57 +1011,61 @@ function propertyInitializer(object, key) {
|
|
|
907
1011
|
}
|
|
908
1012
|
return void 0;
|
|
909
1013
|
}
|
|
910
|
-
function httpMethodFromObject(object,
|
|
911
|
-
const text =
|
|
1014
|
+
function httpMethodFromObject(object, use) {
|
|
1015
|
+
const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
|
|
912
1016
|
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
913
1017
|
}
|
|
914
|
-
function
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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 };
|
|
918
1025
|
return { kind: "unknown", dynamic: false };
|
|
919
1026
|
}
|
|
920
|
-
function destinationTargetFromExpression(expr,
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
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());
|
|
924
1032
|
if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
|
|
925
1033
|
const shape = destinationExpressionShape(expr);
|
|
926
1034
|
if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
|
|
927
1035
|
return void 0;
|
|
928
1036
|
}
|
|
929
|
-
function externalHttpEvidence(node, source
|
|
1037
|
+
function externalHttpEvidence(node, source) {
|
|
930
1038
|
const expr = node.expression;
|
|
931
1039
|
const exprText = expr.getText(source);
|
|
932
1040
|
if (exprText === "useOrFetchDestination") {
|
|
933
1041
|
const objectArg = node.arguments[0];
|
|
934
1042
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
935
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"),
|
|
1043
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
936
1044
|
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
937
1045
|
}
|
|
938
1046
|
}
|
|
939
1047
|
if (exprText === "executeHttpRequest") {
|
|
940
|
-
const destination = destinationTargetFromExpression(node.arguments[0],
|
|
1048
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
941
1049
|
const config = node.arguments[1];
|
|
942
|
-
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config,
|
|
943
|
-
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 };
|
|
944
1052
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
945
1053
|
}
|
|
946
1054
|
if (exprText === "axios") {
|
|
947
1055
|
const config = node.arguments[0];
|
|
948
1056
|
if (config && ts4.isObjectLiteralExpression(config)) {
|
|
949
|
-
const method = httpMethodFromObject(config,
|
|
950
|
-
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)" };
|
|
951
1059
|
}
|
|
952
1060
|
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
953
1061
|
}
|
|
954
1062
|
if (exprText === "fetch") {
|
|
955
1063
|
const init = node.arguments[1];
|
|
956
|
-
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init,
|
|
957
|
-
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" };
|
|
958
1066
|
}
|
|
959
1067
|
if (ts4.isPropertyAccessExpression(expr) && ["get", "post", "put", "patch", "delete", "head"].includes(expr.name.text) && expr.expression.getText(source) === "axios") {
|
|
960
|
-
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}` };
|
|
961
1069
|
}
|
|
962
1070
|
return void 0;
|
|
963
1071
|
}
|
|
@@ -1020,7 +1128,7 @@ function collectWrapperSpecs(source) {
|
|
|
1020
1128
|
const methodProp = propertyInitializer(objectArg, "method");
|
|
1021
1129
|
const pathName = pathProp && ts4.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
1022
1130
|
const methodName = methodProp && ts4.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
1023
|
-
const methodLiteral =
|
|
1131
|
+
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
1024
1132
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
1025
1133
|
}
|
|
1026
1134
|
}
|
|
@@ -1070,18 +1178,19 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1070
1178
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
1071
1179
|
const receiver = receiverName(expr.expression);
|
|
1072
1180
|
const query = objectPropertyText(objectArg, "query");
|
|
1073
|
-
const method = stripQuotes(objectPropertyText(objectArg, "method") ?? "POST");
|
|
1181
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
1074
1182
|
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
1075
|
-
const
|
|
1183
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
1184
|
+
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
|
|
1076
1185
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
1079
|
-
const operationPathExpr =
|
|
1186
|
+
const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
|
|
1187
|
+
const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
|
|
1188
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
1080
1189
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
1081
1190
|
const entityCallTypes = { entity_mutation: "remote_entity_mutation", entity_delete: "remote_entity_delete", entity_media: "remote_entity_media", entity_candidate: "remote_entity_candidate" };
|
|
1082
1191
|
const entityCallType = entityCallTypes[intent.kind];
|
|
1083
1192
|
const isODataQueryRead = method.toUpperCase() === "GET" && ["entity_query", "entity_key_read", "entity_navigation_query"].includes(intent.kind);
|
|
1084
|
-
add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, literalPathSource:
|
|
1193
|
+
add(node, { callType: query ? "remote_query" : entityCallType ?? (isODataQueryRead ? "remote_query" : "remote_action"), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : void 0, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 }, { receiver, classifier: "service_client_send_object", operationPathExpression: shorthandPath ? op : void 0, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? resolvedPath.sourceKind === "const" ? "same_scope_const_initializer" : resolvedPath.sourceKind : void 0, odataPathIntent: operationPathExpr ? intent : void 0, staticPathCandidates: candidateEvidence2, parserWarning: !query && pathExpr && !operationPathExpr ? "dynamic_operation_path_identifier" : void 0 });
|
|
1085
1194
|
}
|
|
1086
1195
|
} else if (ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text) || ts4.isIdentifier(expr) && wrapperSpecs.has(expr.text)) {
|
|
1087
1196
|
const wrapperName = ts4.isIdentifier(expr) ? expr.text : ts4.isCallExpression(expr) && ts4.isIdentifier(expr.expression) ? expr.expression.text : "";
|
|
@@ -1091,11 +1200,14 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1091
1200
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
1092
1201
|
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
1093
1202
|
const receiver = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
1094
|
-
const method = stripQuotes(
|
|
1095
|
-
|
|
1096
|
-
|
|
1203
|
+
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
1204
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1205
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1206
|
+
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
1207
|
+
if (spec && receiver && operationPathExpr) {
|
|
1208
|
+
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: lineOf3(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 });
|
|
1097
1209
|
} else if (spec && receiver) {
|
|
1098
|
-
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: lineOf3(source.text, node.getStart(source)), parserWarning: "dynamic_operation_path_identifier" });
|
|
1210
|
+
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: lineOf3(source.text, node.getStart(source)), wrapperPathSourceKind: resolvedPath.sourceKind, rawPathExpression: resolvedPath.rawExpression, parserWarning: "dynamic_operation_path_identifier" });
|
|
1099
1211
|
}
|
|
1100
1212
|
} else if (ts4.isPropertyAccessExpression(expr) && ["emit", "publish", "on"].includes(expr.name.text)) {
|
|
1101
1213
|
const receiver = receiverName(expr.expression);
|
|
@@ -1105,7 +1217,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1105
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" });
|
|
1106
1218
|
}
|
|
1107
1219
|
} else {
|
|
1108
|
-
const external = externalHttpEvidence(node, source
|
|
1220
|
+
const external = externalHttpEvidence(node, source);
|
|
1109
1221
|
if (external) {
|
|
1110
1222
|
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
1111
1223
|
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
@@ -1194,7 +1306,7 @@ function stringValue(node) {
|
|
|
1194
1306
|
return node.getText().replace(/^`|`$/g, "");
|
|
1195
1307
|
return node.getText();
|
|
1196
1308
|
}
|
|
1197
|
-
function
|
|
1309
|
+
function placeholders2(value) {
|
|
1198
1310
|
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
1199
1311
|
}
|
|
1200
1312
|
function connectFactFromCall(call) {
|
|
@@ -1220,7 +1332,7 @@ function connectFactFromCall(call) {
|
|
|
1220
1332
|
return {
|
|
1221
1333
|
aliasExpr,
|
|
1222
1334
|
isDynamic: true,
|
|
1223
|
-
placeholders:
|
|
1335
|
+
placeholders: placeholders2(aliasExpr)
|
|
1224
1336
|
};
|
|
1225
1337
|
let destinationExpr;
|
|
1226
1338
|
let servicePathExpr;
|
|
@@ -1238,9 +1350,9 @@ function connectFactFromCall(call) {
|
|
|
1238
1350
|
}
|
|
1239
1351
|
if (objectArg) visitObject(objectArg);
|
|
1240
1352
|
const ph = [
|
|
1241
|
-
...
|
|
1242
|
-
...
|
|
1243
|
-
...
|
|
1353
|
+
...placeholders2(aliasExpr ?? alias),
|
|
1354
|
+
...placeholders2(destinationExpr),
|
|
1355
|
+
...placeholders2(servicePathExpr)
|
|
1244
1356
|
];
|
|
1245
1357
|
return {
|
|
1246
1358
|
alias,
|
|
@@ -1890,9 +2002,9 @@ function extractPlaceholders(template) {
|
|
|
1890
2002
|
}
|
|
1891
2003
|
function substituteVariables(template, vars) {
|
|
1892
2004
|
if (!template) return { placeholders: [], missing: [], supplied: [], changed: false };
|
|
1893
|
-
const
|
|
1894
|
-
const supplied =
|
|
1895
|
-
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));
|
|
1896
2008
|
const effective = template.replace(PLACEHOLDER, (_m, key) => {
|
|
1897
2009
|
const trimmed = key.trim();
|
|
1898
2010
|
return Object.hasOwn(vars, trimmed) ? vars[trimmed] ?? "" : `\${${trimmed}}`;
|
|
@@ -1900,7 +2012,7 @@ function substituteVariables(template, vars) {
|
|
|
1900
2012
|
return {
|
|
1901
2013
|
original: template,
|
|
1902
2014
|
effective,
|
|
1903
|
-
placeholders:
|
|
2015
|
+
placeholders: placeholders3,
|
|
1904
2016
|
missing,
|
|
1905
2017
|
supplied,
|
|
1906
2018
|
changed: effective !== template
|
|
@@ -3079,4 +3191,4 @@ export {
|
|
|
3079
3191
|
linkWorkspace,
|
|
3080
3192
|
trace
|
|
3081
3193
|
};
|
|
3082
|
-
//# sourceMappingURL=chunk-
|
|
3194
|
+
//# sourceMappingURL=chunk-774PPGGK.js.map
|