@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 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, sourceFile: service.sourceFile, sourceLine: service.sourceLine }));
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
- 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;
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 !== 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;
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(scope);
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 ? "const_binding_before_use" : "mutable_or_parameter_binding"] };
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 !== scope && isFunctionLikeScope(node)) return;
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) && node.left.text === identifier.text) addExpr(node.right, node);
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(scope);
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.expression, aliases);
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: "local_cap_service_call",
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(expr, aliases) {
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
- return { ...origin, operation };
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-774PPGGK.js.map
3226
+ //# sourceMappingURL=chunk-7L4QKKGN.js.map