@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 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*['"]([^'"]+)['"]/s.exec(raw)?.[1];
190
+ return /path\s*:\s*(['"])(.*?)\1/s.exec(raw)?.[2];
191
191
  }
192
- function matchingBrace(text, open) {
192
+ function matchingBrace(maskedText, open) {
193
193
  let depth = 0;
194
- for (let i = open; i < text.length; i += 1) {
195
- if (text[i] === "{") depth += 1;
196
- if (text[i] === "}") depth -= 1;
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 text.length - 1;
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
- const annotation = collectAnnotations(masked, a.index ?? 0);
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 afterName = collectAnnotations(masked, serviceRegex.lastIndex);
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[2] ?? "UnknownService";
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: Boolean(match[1]),
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 visit = (node) => {
819
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer && (node.parent.flags & ts4.NodeFlags.Const) !== 0) initializers.set(node.name.text, node.initializer);
820
- ts4.forEachChild(node, visit);
821
- };
822
- visit(source);
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, initializers) {
887
- if (!expr) return { sourceKind: "dynamic" };
888
- const rawExpression = expr.getText(expr.getSourceFile());
889
- if (isStringLike(expr)) return { text: expr.text, sourceKind: "literal", rawExpression };
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(source, identifier, initializers) {
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 (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier && node.initializer) {
905
- const resolved2 = staticPathExpression(node.initializer, initializers);
906
- if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
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(source);
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, "POST").topLevelOperationName).filter((value) => Boolean(value)))];
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, initializers) {
948
- const text = staticExpressionText(propertyInitializer(object, "method"), initializers);
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 urlTargetFromExpression(expr, initializers) {
952
- const text = staticExpressionText(expr, initializers);
953
- if (text) return { kind: "static_url", expression: text, dynamic: false };
954
- if (expr && (ts4.isTemplateExpression(expr) || ts4.isIdentifier(expr) || ts4.isPropertyAccessExpression(expr) || ts4.isCallExpression(expr))) return { kind: "url_expression", expression: expr.getText(expr.getSourceFile()), dynamic: true };
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, initializers) {
958
- const text = staticExpressionText(expr, initializers);
959
- if (text) return { kind: "destination", expression: text, dynamic: false };
960
- const candidates = staticConditionalCandidates(expr, initializers);
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, initializers) {
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"), initializers);
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], initializers);
1048
+ const destination = destinationTargetFromExpression(node.arguments[0], node);
978
1049
  const config = node.arguments[1];
979
- const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, initializers) : void 0;
980
- const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), initializers) : { kind: "unknown", dynamic: false };
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, initializers);
987
- return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), initializers), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
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, initializers) : void 0;
994
- return { method, externalTarget: urlTargetFromExpression(node.arguments[0], initializers), classifier: "fetch_call", sourceCallShape: "fetch" };
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], initializers), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
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 = staticExpressionText(methodProp, variableInitializers(source));
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, initializers);
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(source, pathExpr.text, initializers) : void 0;
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(staticExpressionText(methodArg, initializers) ?? spec?.methodLiteral ?? "POST");
1133
- const resolvedPath = staticPathExpression(pathArg, initializers);
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, initializers);
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 placeholders(value) {
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: placeholders(aliasExpr)
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
- ...placeholders(aliasExpr ?? alias),
1283
- ...placeholders(destinationExpr),
1284
- ...placeholders(servicePathExpr)
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 placeholders2 = [...new Set(extractPlaceholders(template))];
1935
- const supplied = placeholders2.filter((key) => Object.hasOwn(vars, key));
1936
- const missing = placeholders2.filter((key) => !Object.hasOwn(vars, key));
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: placeholders2,
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-SAZ5OK7R.js.map
3194
+ //# sourceMappingURL=chunk-774PPGGK.js.map