@saptools/service-flow 0.1.40 → 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 +7 -0
- package/dist/{chunk-774PPGGK.js → chunk-7L4QKKGN.js} +65 -33
- 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 +23 -2
- package/src/parsers/outbound-call-parser.ts +42 -29
- package/src/types.ts +10 -0
- package/dist/chunk-774PPGGK.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.1.40
|
|
4
11
|
|
|
5
12
|
- Hardened operation path expression analysis to respect lexical scope, declaration order, aliases, and bounded branch candidates.
|
|
@@ -202,6 +202,21 @@ function annotationRawAt(original, masked, index) {
|
|
|
202
202
|
const collected = collectAnnotations(masked, index);
|
|
203
203
|
return { end: collected.end, raw: original.slice(index, collected.end) };
|
|
204
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;
|
|
219
|
+
}
|
|
205
220
|
function operationsFromBody(text, maskedBody, bodyOffset, filePath) {
|
|
206
221
|
return [...maskedBody.matchAll(/\b(action|function|event)\s+(\w+)\s*(?:\(([^)]*)\))?\s*(?:returns\s+([^;{]+))?/g)].map((m) => ({
|
|
207
222
|
operationType: m[1] ?? "action",
|
|
@@ -220,6 +235,7 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
220
235
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
221
236
|
const services = [];
|
|
222
237
|
const pendingAnnotations = [];
|
|
238
|
+
const usings = collectUsings(text);
|
|
223
239
|
for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
|
|
224
240
|
const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
|
|
225
241
|
let match;
|
|
@@ -239,6 +255,7 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
239
255
|
const body = masked.slice(open + 1, end);
|
|
240
256
|
const name = match[3] ?? "UnknownService";
|
|
241
257
|
const serviceName = name.split(".").pop() ?? name;
|
|
258
|
+
const imported = isExtend ? usings.get(name) ?? usings.get(serviceName) : void 0;
|
|
242
259
|
const servicePath = ensureLeadingSlash(pathAnnotation(annotations) ?? serviceName);
|
|
243
260
|
services.push({
|
|
244
261
|
namespace,
|
|
@@ -248,14 +265,16 @@ async function parseCdsFile(repoPath, filePath) {
|
|
|
248
265
|
isExtend,
|
|
249
266
|
sourceFile: normalizePath(filePath),
|
|
250
267
|
sourceLine: lineOf(text, match.index),
|
|
251
|
-
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
|
|
252
270
|
});
|
|
253
271
|
serviceRegex.lastIndex = end + 1;
|
|
254
272
|
}
|
|
255
273
|
const baseOps = new Map(services.filter((s) => !s.isExtend).map((s) => [s.qualifiedName, s.operations]));
|
|
256
274
|
for (const service of services.filter((s) => s.isExtend && s.operations.length === 0)) {
|
|
275
|
+
if (service.extension?.moduleSpecifier) continue;
|
|
257
276
|
const inherited = baseOps.get(service.qualifiedName) ?? baseOps.get(service.serviceName);
|
|
258
|
-
if (inherited) service.operations = inherited.map((op) => ({ ...op,
|
|
277
|
+
if (inherited) service.operations = inherited.map((op) => ({ ...op, provenance: "inherited" }));
|
|
259
278
|
}
|
|
260
279
|
return services;
|
|
261
280
|
}
|
|
@@ -894,37 +913,40 @@ function placeholders(expr) {
|
|
|
894
913
|
function isFunctionLikeScope(node) {
|
|
895
914
|
return ts4.isFunctionLike(node) || ts4.isSourceFile(node);
|
|
896
915
|
}
|
|
897
|
-
function containingScope(node) {
|
|
898
|
-
let current = node.parent;
|
|
899
|
-
while (current && !isFunctionLikeScope(current)) current = current.parent;
|
|
900
|
-
return current ?? node.getSourceFile();
|
|
901
|
-
}
|
|
902
916
|
function nodeContains(parent, child) {
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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);
|
|
912
937
|
}
|
|
913
938
|
function resolveBinding(identifier, use) {
|
|
914
|
-
const scope = containingScope(use);
|
|
915
939
|
const source = use.getSourceFile();
|
|
916
940
|
let best;
|
|
917
941
|
const visit = (node) => {
|
|
918
|
-
if (node
|
|
919
|
-
if (node.
|
|
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;
|
|
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;
|
|
922
944
|
ts4.forEachChild(node, visit);
|
|
923
945
|
};
|
|
924
|
-
visit(
|
|
946
|
+
visit(source);
|
|
925
947
|
if (!best) return { immutable: false, evidence: ["binding_not_found"] };
|
|
926
948
|
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 ? "
|
|
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"] };
|
|
928
950
|
}
|
|
929
951
|
function resolveExpression(expr, use, policy, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
930
952
|
if (!expr) return { status: "unknown", sourceKind: "dynamic_expression", placeholderKeys: [], evidence: ["expression_missing"] };
|
|
@@ -964,7 +986,6 @@ function staticPathCandidates(identifier, call, method) {
|
|
|
964
986
|
const binding = resolveBinding(identifier, call);
|
|
965
987
|
const declaration = binding.declaration;
|
|
966
988
|
if (!declaration) return void 0;
|
|
967
|
-
const scope = containingScope(call);
|
|
968
989
|
const source = call.getSourceFile();
|
|
969
990
|
const paths = [];
|
|
970
991
|
let hasDynamicAssignments = false;
|
|
@@ -975,12 +996,15 @@ function staticPathCandidates(identifier, call, method) {
|
|
|
975
996
|
};
|
|
976
997
|
if (ts4.isVariableDeclaration(declaration)) addExpr(declaration.initializer, declaration);
|
|
977
998
|
const visit = (node) => {
|
|
978
|
-
if (node !==
|
|
999
|
+
if (node !== source && isFunctionLikeScope(node) && !nodeContains(node, call)) return;
|
|
979
1000
|
if (node.getStart(source) >= call.getStart(source)) return;
|
|
980
|
-
if (ts4.isBinaryExpression(node) && node.operatorToken.kind === ts4.SyntaxKind.EqualsToken && ts4.isIdentifier(node.left)
|
|
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);
|
|
1004
|
+
}
|
|
981
1005
|
ts4.forEachChild(node, visit);
|
|
982
1006
|
};
|
|
983
|
-
visit(
|
|
1007
|
+
visit(source);
|
|
984
1008
|
const candidatePaths = [...new Set(paths)];
|
|
985
1009
|
if (candidatePaths.length === 0) return void 0;
|
|
986
1010
|
const normalizedCandidateOperations = [...new Set(candidatePaths.map((candidate) => classifyODataPathIntent(candidate, method).topLevelOperationName).filter((value) => Boolean(value)))];
|
|
@@ -1251,7 +1275,7 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
1251
1275
|
if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
|
|
1252
1276
|
}
|
|
1253
1277
|
if (ts4.isCallExpression(node)) {
|
|
1254
|
-
const parsed = serviceOperationCall(node
|
|
1278
|
+
const parsed = serviceOperationCall(node, aliases);
|
|
1255
1279
|
if (parsed && parsed.operation !== "entities") calls.push({
|
|
1256
1280
|
callType: "local_service_call",
|
|
1257
1281
|
operationPathExpr: `/${parsed.operation}`,
|
|
@@ -1264,7 +1288,8 @@ function parseLocalServiceCalls(text, filePath) {
|
|
|
1264
1288
|
confidence: 0.9,
|
|
1265
1289
|
unresolvedReason: ["send", "emit", "publish", "on"].includes(parsed.operation) ? "transport_client_method" : void 0,
|
|
1266
1290
|
evidence: parserEvidence(source, node, {
|
|
1267
|
-
classifier:
|
|
1291
|
+
classifier: parsed.classifier,
|
|
1292
|
+
parserCallType: parsed.operation === "send" ? "transport_client_method" : parsed.classifier,
|
|
1268
1293
|
localServiceLookup: parsed.lookup,
|
|
1269
1294
|
localServiceName: parsed.service,
|
|
1270
1295
|
operation: parsed.operation,
|
|
@@ -1283,12 +1308,19 @@ function serviceLookup(expr, aliases) {
|
|
|
1283
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()] };
|
|
1284
1309
|
return void 0;
|
|
1285
1310
|
}
|
|
1286
|
-
function serviceOperationCall(
|
|
1311
|
+
function serviceOperationCall(node, aliases) {
|
|
1312
|
+
const expr = node.expression;
|
|
1287
1313
|
if (!ts4.isPropertyAccessExpression(expr)) return void 0;
|
|
1288
|
-
const operation = expr.name.text;
|
|
1289
1314
|
const origin = serviceLookup(expr.expression, aliases);
|
|
1290
1315
|
if (!origin) return void 0;
|
|
1291
|
-
|
|
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" };
|
|
1292
1324
|
}
|
|
1293
1325
|
|
|
1294
1326
|
// src/parsers/service-binding-parser.ts
|
|
@@ -3191,4 +3223,4 @@ export {
|
|
|
3191
3223
|
linkWorkspace,
|
|
3192
3224
|
trace
|
|
3193
3225
|
};
|
|
3194
|
-
//# sourceMappingURL=chunk-
|
|
3226
|
+
//# sourceMappingURL=chunk-7L4QKKGN.js.map
|