@saptools/service-flow 0.1.39 → 0.1.41
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 +14 -0
- package/README.md +4 -0
- package/TECHNICAL-NOTE.md +4 -0
- package/dist/{chunk-SAZ5OK7R.js → chunk-7L4QKKGN.js} +189 -86
- package/dist/chunk-7L4QKKGN.js.map +1 -0
- package/dist/cli.js +79 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/db/migrations.ts +14 -1
- package/src/db/repositories.ts +9 -2
- package/src/db/schema.ts +4 -2
- package/src/indexer/cds-extension-resolver.ts +65 -0
- package/src/indexer/workspace-indexer.ts +2 -0
- package/src/parsers/cds-parser.ts +43 -16
- package/src/parsers/outbound-call-parser.ts +142 -62
- package/src/types.ts +10 -0
- package/dist/chunk-SAZ5OK7R.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.41
|
|
4
|
+
|
|
5
|
+
- Added imported CDS extension provenance and materialized inherited operations at concrete extension paths without guessing by simple service name.
|
|
6
|
+
- Resolved TypeScript identifier expressions through lexical bindings so module constants work and inaccessible shadowed block values are excluded.
|
|
7
|
+
- Classified positional CAP `Service.send(...)` dispatch only for proven CAP service receivers while leaving generic `send` calls untouched.
|
|
8
|
+
- Bumped the SQLite schema to persist extension/base and operation provenance metadata.
|
|
9
|
+
|
|
10
|
+
## 0.1.40
|
|
11
|
+
|
|
12
|
+
- Hardened operation path expression analysis to respect lexical scope, declaration order, aliases, and bounded branch candidates.
|
|
13
|
+
- Kept external URL and destination templates with substitutions dynamic with sanitized labels.
|
|
14
|
+
- Fixed CDS path annotation parsing for prefix/suffix annotations and supported service extension syntax.
|
|
15
|
+
|
|
3
16
|
## 0.1.35
|
|
4
17
|
|
|
5
18
|
- 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 +274,4 @@
|
|
|
261
274
|
## 0.1.0
|
|
262
275
|
|
|
263
276
|
- Initial `@saptools/service-flow` CLI package for indexing and tracing SAP CAP service flows.
|
|
277
|
+
|
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,35 @@ 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) };
|
|
204
|
+
}
|
|
205
|
+
function collectUsings(masked) {
|
|
206
|
+
const imports = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const m of masked.matchAll(/\busing\s*\{([^}]*)\}\s*from\s*(['"])(.*?)\2\s*;/gs)) {
|
|
208
|
+
const moduleSpecifier = m[3] ?? "";
|
|
209
|
+
for (const part of (m[1] ?? "").split(",")) {
|
|
210
|
+
const text = part.trim();
|
|
211
|
+
if (!text) continue;
|
|
212
|
+
const alias = /^(\w+)\s+as\s+(\w+)$/.exec(text) ?? /^(\w+)\s*:\s*(\w+)$/.exec(text);
|
|
213
|
+
const importedSymbol = alias?.[1] ?? text;
|
|
214
|
+
const localAlias = alias?.[2] ?? importedSymbol;
|
|
215
|
+
imports.set(localAlias, { importedSymbol, localAlias, moduleSpecifier, importKind: moduleSpecifier.startsWith(".") ? "relative" : "package" });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return imports;
|
|
200
219
|
}
|
|
201
220
|
function operationsFromBody(text, maskedBody, bodyOffset, filePath) {
|
|
202
221
|
return [...maskedBody.matchAll(/\b(action|function|event)\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?:returns\s+([^;{]+))?/g)].map((m) => ({
|
|
@@ -216,40 +235,46 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
216
235
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
217
236
|
const services = [];
|
|
218
237
|
const pendingAnnotations = [];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
const serviceRegex = /\b(extend\s+)?service\s+([\w.]+)\b/g;
|
|
238
|
+
const usings = collectUsings(text);
|
|
239
|
+
for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
|
|
240
|
+
const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
|
|
224
241
|
let match;
|
|
225
242
|
while (match = serviceRegex.exec(masked)) {
|
|
226
|
-
const
|
|
243
|
+
const isExtend = match[1] === "extend";
|
|
244
|
+
const hasServiceKeyword = match[2] === "service";
|
|
245
|
+
if (!isExtend && !hasServiceKeyword) continue;
|
|
246
|
+
const afterName = annotationRawAt(text, masked, serviceRegex.lastIndex);
|
|
227
247
|
const open = masked.indexOf("{", afterName.end);
|
|
228
248
|
if (open === -1) continue;
|
|
249
|
+
const between = masked.slice(afterName.end, open).trim();
|
|
250
|
+
if (between.length > 0) continue;
|
|
229
251
|
const matchIndex = match.index;
|
|
230
252
|
const prefix = pendingAnnotations.filter((a) => a.end <= matchIndex && matchIndex - a.end < 8).map((a) => a.raw).join("");
|
|
231
253
|
const annotations = `${prefix}${afterName.raw}`;
|
|
232
254
|
const end = matchingBrace(masked, open);
|
|
233
255
|
const body = masked.slice(open + 1, end);
|
|
234
|
-
const name = match[
|
|
256
|
+
const name = match[3] ?? "UnknownService";
|
|
235
257
|
const serviceName = name.split(".").pop() ?? name;
|
|
258
|
+
const imported = isExtend ? usings.get(name) ?? usings.get(serviceName) : void 0;
|
|
236
259
|
const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
|
|
237
260
|
services.push({
|
|
238
261
|
namespace,
|
|
239
262
|
serviceName,
|
|
240
263
|
qualifiedName: name.includes(".") ? name : namespace ? `${namespace}.${name}` : name,
|
|
241
264
|
servicePath,
|
|
242
|
-
isExtend
|
|
265
|
+
isExtend,
|
|
243
266
|
sourceFile: normalizePath(filePath),
|
|
244
267
|
sourceLine: lineOf(text, match.index),
|
|
245
|
-
operations: operationsFromBody(text, body, open + 1, filePath)
|
|
268
|
+
operations: operationsFromBody(text, body, open + 1, filePath),
|
|
269
|
+
extension: isExtend ? { localReference: name, importedSymbol: imported?.importedSymbol, localAlias: imported?.localAlias, moduleSpecifier: imported?.moduleSpecifier, importKind: imported?.importKind ?? "none" } : void 0
|
|
246
270
|
});
|
|
247
271
|
serviceRegex.lastIndex = end + 1;
|
|
248
272
|
}
|
|
249
273
|
const baseOps = new Map(services.filter((s) => !s.isExtend).map((s) => [s.qualifiedName, s.operations]));
|
|
250
274
|
for (const service of services.filter((s) => s.isExtend && s.operations.length === 0)) {
|
|
275
|
+
if (service.extension?.moduleSpecifier) continue;
|
|
251
276
|
const inherited = baseOps.get(service.qualifiedName) ?? baseOps.get(service.serviceName);
|
|
252
|
-
if (inherited) service.operations = inherited.map((op) => ({ ...op,
|
|
277
|
+
if (inherited) service.operations = inherited.map((op) => ({ ...op, provenance: "inherited" }));
|
|
253
278
|
}
|
|
254
279
|
return services;
|
|
255
280
|
}
|
|
@@ -815,11 +840,12 @@ function expressionName(expr) {
|
|
|
815
840
|
}
|
|
816
841
|
function variableInitializers(source) {
|
|
817
842
|
const initializers = /* @__PURE__ */ new Map();
|
|
818
|
-
const
|
|
819
|
-
if (ts4.
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
843
|
+
for (const statement of source.statements) {
|
|
844
|
+
if (!ts4.isVariableStatement(statement) || (statement.declarationList.flags & ts4.NodeFlags.Const) === 0) continue;
|
|
845
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
846
|
+
if (ts4.isIdentifier(declaration.name) && declaration.initializer) initializers.set(declaration.name.text, declaration.initializer);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
823
849
|
return initializers;
|
|
824
850
|
}
|
|
825
851
|
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
@@ -876,48 +902,113 @@ function nameOfProperty(name) {
|
|
|
876
902
|
if (ts4.isIdentifier(name) || ts4.isStringLiteral(name) || ts4.isNumericLiteral(name)) return name.text;
|
|
877
903
|
return void 0;
|
|
878
904
|
}
|
|
905
|
+
var maxAliasDepth = 5;
|
|
906
|
+
function safeRaw(expr) {
|
|
907
|
+
if (ts4.isStringLiteral(expr) || ts4.isNoSubstitutionTemplateLiteral(expr) || ts4.isIdentifier(expr) || ts4.isTemplateExpression(expr)) return expr.getText(expr.getSourceFile());
|
|
908
|
+
return void 0;
|
|
909
|
+
}
|
|
910
|
+
function placeholders(expr) {
|
|
911
|
+
return expr.templateSpans.map((span) => span.expression.getText(expr.getSourceFile()));
|
|
912
|
+
}
|
|
913
|
+
function isFunctionLikeScope(node) {
|
|
914
|
+
return ts4.isFunctionLike(node) || ts4.isSourceFile(node);
|
|
915
|
+
}
|
|
916
|
+
function nodeContains(parent, child) {
|
|
917
|
+
const source = child.getSourceFile();
|
|
918
|
+
return child.getStart(source) >= parent.getStart(source) && child.getEnd() <= parent.getEnd();
|
|
919
|
+
}
|
|
920
|
+
function declarationScope(node) {
|
|
921
|
+
if (ts4.isParameter(node)) return node.parent;
|
|
922
|
+
const list = node.parent;
|
|
923
|
+
const blockScoped = (list.flags & (ts4.NodeFlags.Const | ts4.NodeFlags.Let)) !== 0;
|
|
924
|
+
let current = list.parent;
|
|
925
|
+
if (!blockScoped) {
|
|
926
|
+
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
927
|
+
return current;
|
|
928
|
+
}
|
|
929
|
+
while (current.parent && !ts4.isBlock(current) && !ts4.isSourceFile(current) && !ts4.isModuleBlock(current) && !ts4.isCaseBlock(current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
930
|
+
return current;
|
|
931
|
+
}
|
|
932
|
+
function isAccessibleDeclaration(declaration, use) {
|
|
933
|
+
const source = use.getSourceFile();
|
|
934
|
+
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
935
|
+
const scope = declarationScope(declaration);
|
|
936
|
+
return ts4.isSourceFile(scope) || nodeContains(scope, use);
|
|
937
|
+
}
|
|
938
|
+
function resolveBinding(identifier, use) {
|
|
939
|
+
const source = use.getSourceFile();
|
|
940
|
+
let best;
|
|
941
|
+
const visit = (node) => {
|
|
942
|
+
if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
943
|
+
if (ts4.isParameter(node) && ts4.isIdentifier(node.name) && node.name.text === identifier.text && isAccessibleDeclaration(node, use)) best = node;
|
|
944
|
+
ts4.forEachChild(node, visit);
|
|
945
|
+
};
|
|
946
|
+
visit(source);
|
|
947
|
+
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
948
|
+
const immutable = ts4.isVariableDeclaration(best) && (best.parent.flags & ts4.NodeFlags.Const) !== 0;
|
|
949
|
+
return { declaration: best, initializer: ts4.isVariableDeclaration(best) ? best.initializer : void 0, immutable, evidence: [immutable ? "lexical_const_binding_before_use" : "lexical_mutable_or_parameter_binding"] };
|
|
950
|
+
}
|
|
951
|
+
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
952
|
+
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
953
|
+
if (ts4.isStringLiteral(expr)) return { status: "static", sourceKind: "string_literal", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["string_literal"] };
|
|
954
|
+
if (ts4.isNoSubstitutionTemplateLiteral(expr)) return { status: "static", sourceKind: "no_substitution_template", value: expr.text, rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["no_substitution_template"] };
|
|
955
|
+
if (ts4.isTemplateExpression(expr)) {
|
|
956
|
+
const keys = placeholders(expr);
|
|
957
|
+
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"] };
|
|
958
|
+
return { status: "dynamic", sourceKind: "template_with_substitutions", placeholderKeys: keys, evidence: ["template_substitutions_not_static_external_target"] };
|
|
959
|
+
}
|
|
960
|
+
if (ts4.isIdentifier(expr)) {
|
|
961
|
+
if (depth >= maxAliasDepth) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_depth_exceeded"], constName: expr.text };
|
|
962
|
+
const binding = resolveBinding(expr, use);
|
|
963
|
+
if (!binding.declaration || !binding.initializer || !binding.immutable) return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: binding.evidence, constName: expr.text };
|
|
964
|
+
if (seen.has(binding.declaration)) return { status: "unknown", sourceKind: "const_alias", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: ["alias_cycle_detected"], constName: expr.text };
|
|
965
|
+
seen.add(binding.declaration);
|
|
966
|
+
const resolved2 = resolveExpression(binding.initializer, binding.declaration, policy, depth + 1, seen);
|
|
967
|
+
return { ...resolved2, sourceKind: "const_alias", rawExpression: safeRaw(expr), constName: expr.text, evidence: [...binding.evidence, ...resolved2.evidence] };
|
|
968
|
+
}
|
|
969
|
+
return { status: "dynamic", sourceKind: "dynamic_expression", rawExpression: safeRaw(expr), placeholderKeys: [], evidence: [`unsupported_${ts4.SyntaxKind[expr.kind] ?? "expression"}`] };
|
|
970
|
+
}
|
|
879
971
|
function staticExpressionText(expr, initializers) {
|
|
880
972
|
if (!expr) return void 0;
|
|
881
973
|
if (isStringLike(expr)) return expr.text;
|
|
882
|
-
if (ts4.isTemplateExpression(expr)) return stripQuotes(expr.getText(expr.getSourceFile()));
|
|
883
974
|
if (ts4.isIdentifier(expr) && initializers.has(expr.text)) return staticExpressionText(initializers.get(expr.text), initializers);
|
|
884
975
|
return void 0;
|
|
885
976
|
}
|
|
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 };
|
|
977
|
+
function staticPathExpression(expr, use) {
|
|
978
|
+
const resolution = resolveExpression(expr, use, "operation_path");
|
|
979
|
+
const sourceKind = resolution.sourceKind === "const_alias" ? "const" : resolution.sourceKind === "template_with_substitutions" || resolution.sourceKind === "no_substitution_template" ? "template" : resolution.status === "static" ? "literal" : "dynamic";
|
|
980
|
+
return { text: resolution.value, sourceKind, rawExpression: resolution.rawExpression, constName: resolution.constName, resolution };
|
|
896
981
|
}
|
|
897
982
|
function operationPathFromStatic(text) {
|
|
898
983
|
return text ? `/${stripQuotes(text).replace(/^\//, "")}` : void 0;
|
|
899
984
|
}
|
|
900
|
-
function staticPathCandidates(
|
|
985
|
+
function staticPathCandidates(identifier, call, method) {
|
|
986
|
+
const binding = resolveBinding(identifier, call);
|
|
987
|
+
const declaration = binding.declaration;
|
|
988
|
+
if (!declaration) return void 0;
|
|
989
|
+
const source = call.getSourceFile();
|
|
901
990
|
const paths = [];
|
|
902
991
|
let hasDynamicAssignments = false;
|
|
992
|
+
const addExpr = (expr, origin) => {
|
|
993
|
+
const resolved2 = staticPathExpression(expr, origin);
|
|
994
|
+
if (resolved2.text) paths.push(operationPathFromStatic(resolved2.text) ?? resolved2.text);
|
|
995
|
+
else hasDynamicAssignments = true;
|
|
996
|
+
};
|
|
997
|
+
if (ts4.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
903
998
|
const visit = (node) => {
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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;
|
|
999
|
+
if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
|
|
1000
|
+
if (node.getStart(source) >= call.getStart(source)) return;
|
|
1001
|
+
if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left)) {
|
|
1002
|
+
const target = resolveBinding(node.left, node);
|
|
1003
|
+
if (target.declaration === declaration) addExpr(node.right, node);
|
|
913
1004
|
}
|
|
914
1005
|
ts4.forEachChild(node, visit);
|
|
915
1006
|
};
|
|
916
1007
|
visit(source);
|
|
917
1008
|
const candidatePaths = [...new Set(paths)];
|
|
918
1009
|
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 };
|
|
1010
|
+
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
|
|
1011
|
+
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
1012
|
}
|
|
922
1013
|
function destinationExpressionShape(expr) {
|
|
923
1014
|
if (!expr) return void 0;
|
|
@@ -944,57 +1035,61 @@ function propertyInitializer(object, key) {
|
|
|
944
1035
|
}
|
|
945
1036
|
return void 0;
|
|
946
1037
|
}
|
|
947
|
-
function httpMethodFromObject(object,
|
|
948
|
-
const text =
|
|
1038
|
+
function httpMethodFromObject(object, use) {
|
|
1039
|
+
const text = resolveExpression(propertyInitializer(object, "method"), use, "literal").value;
|
|
949
1040
|
return text ? stripQuotes(text).toUpperCase() : void 0;
|
|
950
1041
|
}
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1042
|
+
function hasTemplatePlaceholder(value) {
|
|
1043
|
+
return /\$\{|%7B|%7D/i.test(value);
|
|
1044
|
+
}
|
|
1045
|
+
function urlTargetFromExpression(expr, use) {
|
|
1046
|
+
const resolved2 = resolveExpression(expr, use, "external");
|
|
1047
|
+
if (resolved2.status === "static" && resolved2.value && !hasTemplatePlaceholder(resolved2.value)) return { kind: "static_url", expression: resolved2.value, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1048
|
+
if (expr) return { kind: "url_expression", dynamic: true, expression: `${resolved2.sourceKind}:${resolved2.placeholderKeys.join("|")}`, expressionShape: resolved2.sourceKind, placeholderKeys: resolved2.placeholderKeys };
|
|
955
1049
|
return { kind: "unknown", dynamic: false };
|
|
956
1050
|
}
|
|
957
|
-
function destinationTargetFromExpression(expr,
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
1051
|
+
function destinationTargetFromExpression(expr, use) {
|
|
1052
|
+
const resolved2 = resolveExpression(expr, use, "external");
|
|
1053
|
+
const text = resolved2.value;
|
|
1054
|
+
if (resolved2.status === "static" && text && !hasTemplatePlaceholder(text)) return { kind: "destination", expression: text, dynamic: false, sourceKind: resolved2.sourceKind };
|
|
1055
|
+
const candidates = staticConditionalCandidates(expr, /* @__PURE__ */ new Map());
|
|
961
1056
|
if (candidates) return { kind: "destination", dynamic: true, expressionShape: "conditional", candidateLiterals: candidates };
|
|
962
1057
|
const shape = destinationExpressionShape(expr);
|
|
963
1058
|
if (shape) return { kind: "destination", dynamic: true, expressionShape: shape };
|
|
964
1059
|
return void 0;
|
|
965
1060
|
}
|
|
966
|
-
function externalHttpEvidence(node, source
|
|
1061
|
+
function externalHttpEvidence(node, source) {
|
|
967
1062
|
const expr = node.expression;
|
|
968
1063
|
const exprText = expr.getText(source);
|
|
969
1064
|
if (exprText === "useOrFetchDestination") {
|
|
970
1065
|
const objectArg = node.arguments[0];
|
|
971
1066
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
972
|
-
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"),
|
|
1067
|
+
const destination = destinationTargetFromExpression(propertyInitializer(objectArg, "destinationName"), node);
|
|
973
1068
|
return { externalTarget: destination ?? { kind: "unknown", dynamic: false }, classifier: "sap_destination_lookup", sourceCallShape: "useOrFetchDestination" };
|
|
974
1069
|
}
|
|
975
1070
|
}
|
|
976
1071
|
if (exprText === "executeHttpRequest") {
|
|
977
|
-
const destination = destinationTargetFromExpression(node.arguments[0],
|
|
1072
|
+
const destination = destinationTargetFromExpression(node.arguments[0], node);
|
|
978
1073
|
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"),
|
|
1074
|
+
const method = config && ts4.isObjectLiteralExpression(config) ? httpMethodFromObject(config, node) : void 0;
|
|
1075
|
+
const url = config && ts4.isObjectLiteralExpression(config) ? urlTargetFromExpression(propertyInitializer(config, "url"), node) : { kind: "unknown", dynamic: false };
|
|
981
1076
|
return { method, externalTarget: destination ? { ...url, destination } : url, classifier: "sap_execute_http_request", sourceCallShape: "executeHttpRequest" };
|
|
982
1077
|
}
|
|
983
1078
|
if (exprText === "axios") {
|
|
984
1079
|
const config = node.arguments[0];
|
|
985
1080
|
if (config && ts4.isObjectLiteralExpression(config)) {
|
|
986
|
-
const method = httpMethodFromObject(config,
|
|
987
|
-
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"),
|
|
1081
|
+
const method = httpMethodFromObject(config, node);
|
|
1082
|
+
return { method, externalTarget: urlTargetFromExpression(propertyInitializer(config, "url"), node), classifier: "axios_config_call", sourceCallShape: "axios(config)" };
|
|
988
1083
|
}
|
|
989
1084
|
return { externalTarget: { kind: "unknown", dynamic: false }, classifier: "axios_unknown_call", sourceCallShape: "axios(...)" };
|
|
990
1085
|
}
|
|
991
1086
|
if (exprText === "fetch") {
|
|
992
1087
|
const init = node.arguments[1];
|
|
993
|
-
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init,
|
|
994
|
-
return { method, externalTarget: urlTargetFromExpression(node.arguments[0],
|
|
1088
|
+
const method = init && ts4.isObjectLiteralExpression(init) ? httpMethodFromObject(init, node) : void 0;
|
|
1089
|
+
return { method, externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "fetch_call", sourceCallShape: "fetch" };
|
|
995
1090
|
}
|
|
996
1091
|
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],
|
|
1092
|
+
return { method: expr.name.text.toUpperCase(), externalTarget: urlTargetFromExpression(node.arguments[0], node), classifier: "axios_member_call", sourceCallShape: `axios.${expr.name.text}` };
|
|
998
1093
|
}
|
|
999
1094
|
return void 0;
|
|
1000
1095
|
}
|
|
@@ -1057,7 +1152,7 @@ function collectWrapperSpecs(source) {
|
|
|
1057
1152
|
const methodProp = propertyInitializer(objectArg, "method");
|
|
1058
1153
|
const pathName = pathProp && ts4.isIdentifier(pathProp) ? pathProp.text : void 0;
|
|
1059
1154
|
const methodName = methodProp && ts4.isIdentifier(methodProp) ? methodProp.text : void 0;
|
|
1060
|
-
const methodLiteral =
|
|
1155
|
+
const methodLiteral = resolveExpression(methodProp, node, "literal").value;
|
|
1061
1156
|
if (pathName) sends.push({ client: node.expression.expression.text, path: pathName, method: methodName, methodLiteral, start: node.getStart(source), end: node.getEnd() });
|
|
1062
1157
|
}
|
|
1063
1158
|
}
|
|
@@ -1107,12 +1202,12 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1107
1202
|
if (objectArg && ts4.isObjectLiteralExpression(objectArg)) {
|
|
1108
1203
|
const receiver = receiverName(expr.expression);
|
|
1109
1204
|
const query = objectPropertyText(objectArg, "query");
|
|
1110
|
-
const method = stripQuotes(objectPropertyText(objectArg, "method") ?? "POST");
|
|
1205
|
+
const method = stripQuotes(resolveExpression(propertyInitializer(objectArg, "method"), node, "literal").value ?? objectPropertyText(objectArg, "method") ?? "POST");
|
|
1111
1206
|
const pathExpr = propertyInitializer(objectArg, "path") ?? propertyInitializer(objectArg, "event");
|
|
1112
|
-
const resolvedPath = staticPathExpression(pathExpr,
|
|
1207
|
+
const resolvedPath = staticPathExpression(pathExpr, node);
|
|
1113
1208
|
const op = pathExpr ? resolvedPath.text ?? pathExpr.getText(source) : void 0;
|
|
1114
1209
|
const shorthandPath = objectPropertyIsShorthand(objectArg, "path");
|
|
1115
|
-
const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(
|
|
1210
|
+
const candidateEvidence2 = pathExpr && ts4.isIdentifier(pathExpr) && !resolvedPath.text ? staticPathCandidates(pathExpr, node, method) : void 0;
|
|
1116
1211
|
const candidateOperationPath = candidateEvidence2 && !candidateEvidence2.hasDynamicAssignments && candidateEvidence2.normalizedCandidateOperations.length === 1 && candidateEvidence2.candidatePaths.length > 0 ? candidateEvidence2.candidatePaths[0] : void 0;
|
|
1117
1212
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text) ?? candidateOperationPath;
|
|
1118
1213
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
@@ -1129,8 +1224,8 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1129
1224
|
const pathArg = spec ? wrapperArgs[spec.pathIndex] : void 0;
|
|
1130
1225
|
const methodArg = spec?.methodIndex === void 0 ? void 0 : wrapperArgs[spec.methodIndex];
|
|
1131
1226
|
const receiver = clientArg && ts4.isIdentifier(clientArg) ? clientArg.text : spec?.clientName;
|
|
1132
|
-
const method = stripQuotes(
|
|
1133
|
-
const resolvedPath = staticPathExpression(pathArg,
|
|
1227
|
+
const method = stripQuotes(resolveExpression(methodArg, node, "literal").value ?? spec?.methodLiteral ?? "POST");
|
|
1228
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
1134
1229
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
1135
1230
|
const normalizedOperationPath = operationPathExpr ? classifyODataPathIntent(operationPathExpr, method).topLevelOperationName : void 0;
|
|
1136
1231
|
if (spec && receiver && operationPathExpr) {
|
|
@@ -1146,7 +1241,7 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
1146
1241
|
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
1242
|
}
|
|
1148
1243
|
} else {
|
|
1149
|
-
const external = externalHttpEvidence(node, source
|
|
1244
|
+
const external = externalHttpEvidence(node, source);
|
|
1150
1245
|
if (external) {
|
|
1151
1246
|
const evidenceTarget = { ...external.externalTarget, method: external.method, parserClassifier: external.classifier, sourceCallShape: external.sourceCallShape };
|
|
1152
1247
|
const safeTarget = externalHttpTarget({ method: external.method, evidence_json: JSON.stringify({ externalTarget: evidenceTarget }) });
|
|
@@ -1180,7 +1275,7 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
1180
1275
|
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
1181
1276
|
}
|
|
1182
1277
|
if (ts4.isCallExpression(node)) {
|
|
1183
|
-
const parsed = serviceOperationCall(node
|
|
1278
|
+
const parsed = serviceOperationCall(node, aliases);
|
|
1184
1279
|
if (parsed && parsed.operation !== "entities") calls.push({
|
|
1185
1280
|
callType: "local_service_call",
|
|
1186
1281
|
operationPathExpr: `/${parsed.operation}`,
|
|
@@ -1193,7 +1288,8 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
1193
1288
|
confidence: 0.9,
|
|
1194
1289
|
unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
|
|
1195
1290
|
evidence: parserEvidence(source, node, {
|
|
1196
|
-
classifier:
|
|
1291
|
+
classifier: parsed.classifier,
|
|
1292
|
+
parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
|
|
1197
1293
|
localServiceLookup: parsed.lookup,
|
|
1198
1294
|
localServiceName: parsed.service,
|
|
1199
1295
|
operation: parsed.operation,
|
|
@@ -1212,12 +1308,19 @@ function serviceLookup(expr, aliases) {
|
|
|
1212
1308
|
if (ts4.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts4.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
|
|
1213
1309
|
return void 0;
|
|
1214
1310
|
}
|
|
1215
|
-
function serviceOperationCall(
|
|
1311
|
+
function serviceOperationCall(node, aliases) {
|
|
1312
|
+
const expr = node.expression;
|
|
1216
1313
|
if (!ts4.isPropertyAccessExpression(expr)) return void 0;
|
|
1217
|
-
const operation = expr.name.text;
|
|
1218
1314
|
const origin = serviceLookup(expr.expression, aliases);
|
|
1219
1315
|
if (!origin) return void 0;
|
|
1220
|
-
|
|
1316
|
+
if (expr.name.text === "send") {
|
|
1317
|
+
const first = literalText(node.arguments[0]);
|
|
1318
|
+
const second = literalText(node.arguments[1]);
|
|
1319
|
+
const method = first?.toUpperCase();
|
|
1320
|
+
if (method && ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(method) && second) return { ...origin, operation: second.replace(/^\//, ""), classifier: "cap_service_send_method_path" };
|
|
1321
|
+
if (first) return { ...origin, operation: first.replace(/^\//, ""), classifier: "cap_service_send_local_dispatch" };
|
|
1322
|
+
}
|
|
1323
|
+
return { ...origin, operation: expr.name.text, classifier: "local_cap_service_call" };
|
|
1221
1324
|
}
|
|
1222
1325
|
|
|
1223
1326
|
// src/parsers/service-binding-parser.ts
|
|
@@ -1235,7 +1338,7 @@ function stringValue(node) {
|
|
|
1235
1338
|
return node.getText().replace(/^`|`$/g, "");
|
|
1236
1339
|
return node.getText();
|
|
1237
1340
|
}
|
|
1238
|
-
function
|
|
1341
|
+
function placeholders2(value) {
|
|
1239
1342
|
return [...(value ?? "").matchAll(/\$\{([^}]*)\}/g)].map((m) => (m[1] ?? "").trim()).filter(Boolean);
|
|
1240
1343
|
}
|
|
1241
1344
|
function connectFactFromCall(call) {
|
|
@@ -1261,7 +1364,7 @@ function connectFactFromCall(call) {
|
|
|
1261
1364
|
return {
|
|
1262
1365
|
aliasExpr,
|
|
1263
1366
|
isDynamic: true,
|
|
1264
|
-
placeholders:
|
|
1367
|
+
placeholders: placeholders2(aliasExpr)
|
|
1265
1368
|
};
|
|
1266
1369
|
let destinationExpr;
|
|
1267
1370
|
let servicePathExpr;
|
|
@@ -1279,9 +1382,9 @@ function connectFactFromCall(call) {
|
|
|
1279
1382
|
}
|
|
1280
1383
|
if (objectArg) visitObject(objectArg);
|
|
1281
1384
|
const ph = [
|
|
1282
|
-
...
|
|
1283
|
-
...
|
|
1284
|
-
...
|
|
1385
|
+
...placeholders2(aliasExpr ?? alias),
|
|
1386
|
+
...placeholders2(destinationExpr),
|
|
1387
|
+
...placeholders2(servicePathExpr)
|
|
1285
1388
|
];
|
|
1286
1389
|
return {
|
|
1287
1390
|
alias,
|
|
@@ -1931,9 +2034,9 @@ function extractPlaceholders(template) {
|
|
|
1931
2034
|
}
|
|
1932
2035
|
function substituteVariables(template, vars) {
|
|
1933
2036
|
if (!template) return { placeholders: [], missing: [], supplied: [], changed: false };
|
|
1934
|
-
const
|
|
1935
|
-
const supplied =
|
|
1936
|
-
const missing =
|
|
2037
|
+
const placeholders3 = [...new Set(extractPlaceholders(template))];
|
|
2038
|
+
const supplied = placeholders3.filter((key) => Object.hasOwn(vars, key));
|
|
2039
|
+
const missing = placeholders3.filter((key) => !Object.hasOwn(vars, key));
|
|
1937
2040
|
const effective = template.replace(PLACEHOLDER, (_m, key) => {
|
|
1938
2041
|
const trimmed = key.trim();
|
|
1939
2042
|
return Object.hasOwn(vars, trimmed) ? vars[trimmed] ?? "" : `\${${trimmed}}`;
|
|
@@ -1941,7 +2044,7 @@ function substituteVariables(template, vars) {
|
|
|
1941
2044
|
return {
|
|
1942
2045
|
original: template,
|
|
1943
2046
|
effective,
|
|
1944
|
-
placeholders:
|
|
2047
|
+
placeholders: placeholders3,
|
|
1945
2048
|
missing,
|
|
1946
2049
|
supplied,
|
|
1947
2050
|
changed: effective !== template
|
|
@@ -3120,4 +3223,4 @@ export {
|
|
|
3120
3223
|
linkWorkspace,
|
|
3121
3224
|
trace
|
|
3122
3225
|
};
|
|
3123
|
-
//# sourceMappingURL=chunk-
|
|
3226
|
+
//# sourceMappingURL=chunk-7L4QKKGN.js.map
|