@saptools/service-flow 0.1.20 → 0.1.21

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.21
4
+
5
+ - Propagate helper-returned connected clients from function declarations, arrow-function variables, function-expression variables, named export lists, and aliased exports into caller destructuring and transaction aliases.
6
+ - Preserve conservative object-return analysis by binding only returned properties backed by local `cds.connect.to(...)` client variables, while ignoring unrelated metadata fields.
7
+ - Document contextual implementation selection evidence and strict doctor checks for operation-path-only remote actions.
8
+
3
9
  ## 0.1.20
4
10
 
5
11
  - Preserve service-binding evidence when helpers return connected clients inside object properties and callers destructure or create simple transaction aliases.
package/README.md CHANGED
@@ -227,7 +227,15 @@ service-flow list calls --workspace /path/to/workspace --repo facade-service --o
227
227
  - Service bindings are matched to outbound calls by repository, source file, and
228
228
  variable name to avoid false cross-file matches. If a helper-returned client is
229
229
  not linked, export the helper from a relative import target and ensure it returns
230
- `cds.connect.to(...)` directly or through a simple wrapper. Trace evidence includes the caller variable, imported helper, source file, and exported symbol.
230
+ `cds.connect.to(...)` directly or returns an object property backed by a local
231
+ connected-client variable. Supported helper shapes include function declarations,
232
+ arrow-function variables, function-expression variables, named export lists, and
233
+ aliased exports such as `export { connectCatalog as createCatalogClient }`.
234
+ Shorthand returns like `return { client }` and explicit returns like
235
+ `return { serviceClient: client }` are followed only when the returned value is
236
+ a concrete client. Trace evidence includes the caller variable, returned
237
+ property, imported helper, source file, exported symbol, placeholders, and
238
+ transaction alias steps.
231
239
  - `SELECT.one.from(Entity)`, `SELECT.from(Entity)`, `INSERT.into(Entity)`,
232
240
  `UPDATE(Entity)`, and `DELETE.from(Entity)` are indexed as local database
233
241
  query entities when statically knowable.
@@ -410,7 +418,7 @@ Run `service-flow index` after source, CDS, package metadata, or helper-package
410
418
  <details>
411
419
  <summary><b>Why is an expected call unresolved?</b></summary>
412
420
 
413
- Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`, and custom wrappers may need new parser support.
421
+ Check `service-flow doctor`, then inspect the facts with `service-flow list services`, `service-flow list operations`, and `service-flow list calls`. Dynamic destinations may need `--var key=value`; the key is the full trimmed expression inside `${...}` and is matched literally without JavaScript evaluation. Operation-path-only ambiguous remote actions usually mean the call had no service binding id; inspect `list calls`, `inspect operation`, and `doctor --strict` to determine whether helper-return propagation or wrapper support is missing. Contextual implementation selection only continues into a handler when static evidence such as caller repository, resolved service path, destination/alias expression, dependency edges, registration package, and local service ownership makes exactly one candidate stronger; ties remain ambiguous with reasons.
414
422
  Default doctor output is intended to focus on actionable indexing or trace-impacting issues; use `--strict` when you need exhaustive model-shape diagnostics for entity-only or extension-heavy CDS models. Strict mode also reports normalized OData invocation ambiguity and remote-action target quality, including whether unresolved unknown/dynamic paths are semantic instead of numeric call ids.
415
423
 
416
424
  </details>
package/TECHNICAL-NOTE.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Service Flow Resolution Notes
2
2
 
3
+ ## 0.1.21 helper-return propagation and contextual trace notes
4
+
5
+ - Helper-return binding analysis uses the same returned-object scanner for `function` declarations, `async function` declarations, arrow-function variables, async arrow-function variables, and function-expression variables. Named export lists and aliases are resolved, so `export { connectCatalog as createCatalogClient }` can be destructured by callers without losing evidence.
6
+ - Returned object properties are bound only when their value is a local variable initialized from `cds.connect.to(...)`. Shorthand properties and explicit property assignments are supported; unrelated strings, codes, and metadata fields are ignored. Destructuring renames and `.tx()` aliases append helper-chain evidence instead of replacing the original binding evidence.
7
+ - Trace-time contextual implementation selection may use caller repository, runtime-resolved service path, destination or alias expression, package dependency evidence, handler and registration packages, and local service ownership to continue from an ambiguous operation edge into one handler. If candidates tie or no static signal is strong enough, trace keeps the ambiguous edge and reports the tie or unresolved reason.
8
+ - `doctor --strict` is the intended place for broad regression aggregates, including helper-return coverage, contextual implementation ambiguity, and remote actions that have an operation path but no service binding id.
9
+
3
10
  ## 0.1.20 helper object clients, expression placeholders, and remote-action target notes
4
11
 
5
12
  - Helper-return binding analysis now follows concrete `cds.connect.to(...)` clients returned through object shorthand or explicit properties. Callers that destructure those properties, rename them, or assign a simple `.tx()` transaction alias keep helper-chain evidence including caller variable, returned property, helper source, destination expression, service-path expression, and placeholders. Arbitrary object returns are ignored.
@@ -900,6 +900,8 @@ function collectReturnedObjectBindings(fn) {
900
900
  const bindings = /* @__PURE__ */ new Map();
901
901
  const returns = /* @__PURE__ */ new Map();
902
902
  function visit(node) {
903
+ if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
904
+ return;
903
905
  if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
904
906
  const fact = findConnectInExpression(node.initializer);
905
907
  if (fact) bindings.set(node.name.text, fact);
@@ -923,6 +925,35 @@ function collectReturnedObjectBindings(fn) {
923
925
  }
924
926
  return out;
925
927
  }
928
+ function functionLikeInitializer(expr) {
929
+ if (!expr) return void 0;
930
+ if (ts5.isArrowFunction(expr) || ts5.isFunctionExpression(expr)) return expr;
931
+ return void 0;
932
+ }
933
+ function directReturnConnectFact(fn) {
934
+ const localBindings = /* @__PURE__ */ new Map();
935
+ let returned;
936
+ function visit(node) {
937
+ if (node !== fn && (ts5.isFunctionDeclaration(node) || ts5.isArrowFunction(node) || ts5.isFunctionExpression(node)))
938
+ return;
939
+ if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
940
+ const fact = findConnectInExpression(node.initializer);
941
+ if (fact) localBindings.set(node.name.text, fact);
942
+ }
943
+ if (!returned && ts5.isReturnStatement(node) && node.expression)
944
+ returned = node.expression;
945
+ if (!returned) ts5.forEachChild(node, visit);
946
+ }
947
+ visit(fn);
948
+ if (!returned) return void 0;
949
+ if (ts5.isIdentifier(returned)) return localBindings.get(returned.text);
950
+ return findConnectInExpression(returned);
951
+ }
952
+ function directConnectFactFromFunctionLike(fn) {
953
+ if (ts5.isArrowFunction(fn) && fn.body && !ts5.isBlock(fn.body))
954
+ return findConnectInExpression(fn.body);
955
+ return directReturnConnectFact(fn);
956
+ }
926
957
  function exportedLocalNames(sf) {
927
958
  const exports = /* @__PURE__ */ new Map();
928
959
  for (const stmt of sf.statements) {
@@ -951,12 +982,7 @@ async function helperBindings(repoPath, filePath) {
951
982
  const factsByLocal = /* @__PURE__ */ new Map();
952
983
  for (const stmt of sf.statements) {
953
984
  if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
954
- let fact;
955
- stmt.forEachChild(function visit(node) {
956
- if (!fact && ts5.isReturnStatement(node) && node.expression)
957
- fact = findConnectInExpression(node.expression);
958
- if (!fact) ts5.forEachChild(node, visit);
959
- });
985
+ const fact = directConnectFactFromFunctionLike(stmt);
960
986
  if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
961
987
  for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(stmt))
962
988
  factsByLocal.set(`${stmt.name.text}#${returnedProperty}`, { ...objectFact, returnedProperty, sourceLine: lineOf4(sf, stmt) });
@@ -964,6 +990,22 @@ async function helperBindings(repoPath, filePath) {
964
990
  if (ts5.isVariableStatement(stmt))
965
991
  for (const decl of stmt.declarationList.declarations) {
966
992
  if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
993
+ const helper = functionLikeInitializer(decl.initializer);
994
+ if (helper) {
995
+ const directReturn = directConnectFactFromFunctionLike(helper);
996
+ if (directReturn)
997
+ factsByLocal.set(decl.name.text, {
998
+ ...directReturn,
999
+ sourceLine: lineOf4(sourceFileAst, decl)
1000
+ });
1001
+ for (const [returnedProperty, objectFact] of collectReturnedObjectBindings(helper))
1002
+ factsByLocal.set(`${decl.name.text}#${returnedProperty}`, {
1003
+ ...objectFact,
1004
+ returnedProperty,
1005
+ sourceLine: lineOf4(sourceFileAst, decl)
1006
+ });
1007
+ continue;
1008
+ }
967
1009
  const fact = findConnectInExpression(decl.initializer);
968
1010
  if (fact)
969
1011
  factsByLocal.set(decl.name.text, {
@@ -1002,22 +1044,36 @@ async function parseServiceBindings(repoPath, filePath) {
1002
1044
  const classHelpers = collectClassHelpers(sourceFileAst);
1003
1045
  const localObjectHelpers = /* @__PURE__ */ new Map();
1004
1046
  for (const stmt of sourceFileAst.statements) {
1005
- if (!ts5.isFunctionDeclaration(stmt) || !stmt.name) continue;
1006
- const rows2 = [];
1007
- for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
1008
- rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
1009
- if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
1047
+ if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
1048
+ const rows2 = [];
1049
+ for (const [returnedProperty, fact] of collectReturnedObjectBindings(stmt))
1050
+ rows2.push({ ...fact, exportedName: stmt.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, stmt) });
1051
+ if (rows2.length > 0) localObjectHelpers.set(stmt.name.text, rows2);
1052
+ }
1053
+ if (ts5.isVariableStatement(stmt)) {
1054
+ for (const decl of stmt.declarationList.declarations) {
1055
+ if (!ts5.isIdentifier(decl.name)) continue;
1056
+ const helper = functionLikeInitializer(decl.initializer);
1057
+ if (!helper) continue;
1058
+ const rows2 = [];
1059
+ for (const [returnedProperty, fact] of collectReturnedObjectBindings(helper))
1060
+ rows2.push({ ...fact, exportedName: decl.name.text, returnedProperty, sourceFile: normalizePath(filePath), sourceLine: lineOf4(sourceFileAst, decl) });
1061
+ if (rows2.length > 0) localObjectHelpers.set(decl.name.text, rows2);
1062
+ }
1063
+ }
1010
1064
  }
1011
- async function importedHelper(localName) {
1065
+ async function importedHelpers(localName) {
1012
1066
  const imp = imports.find((i) => i.localName === localName && i.sourceFile);
1013
- if (!imp?.sourceFile) return void 0;
1067
+ if (!imp?.sourceFile) return [];
1014
1068
  if (!helperCache.has(imp.sourceFile))
1015
1069
  helperCache.set(
1016
1070
  imp.sourceFile,
1017
1071
  await helperBindings(repoPath, imp.sourceFile)
1018
1072
  );
1019
- const helper = helperCache.get(imp.sourceFile)?.find((h) => h.exportedName === imp.exportedName);
1020
- return helper ? { imp, helper } : void 0;
1073
+ return (helperCache.get(imp.sourceFile) ?? []).filter((h) => h.exportedName === imp.exportedName).map((helper) => ({ imp, helper }));
1074
+ }
1075
+ async function importedHelper(localName) {
1076
+ return (await importedHelpers(localName)).find((row) => !row.helper.returnedProperty);
1021
1077
  }
1022
1078
  async function recordVariable(decl) {
1023
1079
  if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
@@ -1057,23 +1113,24 @@ async function parseServiceBindings(repoPath, filePath) {
1057
1113
  });
1058
1114
  }
1059
1115
  }
1060
- async function helperForCall(call) {
1061
- if (!ts5.isIdentifier(call.expression)) return void 0;
1062
- const local = localObjectHelpers.get(call.expression.text)?.[0];
1063
- if (local) return { helper: local };
1064
- const resolved = await importedHelper(call.expression.text);
1065
- return resolved ? { helper: resolved.helper, imp: resolved.imp } : void 0;
1116
+ async function helpersForCall(call) {
1117
+ if (!ts5.isIdentifier(call.expression)) return [];
1118
+ const local = localObjectHelpers.get(call.expression.text) ?? [];
1119
+ const imported = await importedHelpers(call.expression.text);
1120
+ return [...local.map((helper) => ({ helper })), ...imported];
1066
1121
  }
1067
1122
  async function recordDestructuredHelper(decl) {
1068
1123
  if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
1069
1124
  const call = unwrapCall(decl.initializer);
1070
1125
  if (!call) return;
1071
- const resolved = await helperForCall(call);
1072
- if (!resolved) return;
1126
+ const helpers = await helpersForCall(call);
1127
+ if (helpers.length === 0) return;
1073
1128
  for (const el of decl.name.elements) {
1074
1129
  if (!ts5.isIdentifier(el.name)) continue;
1075
1130
  const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
1076
- if (resolved.helper.returnedProperty !== propertyName) continue;
1131
+ const matches = helpers.filter((row) => row.helper.returnedProperty === propertyName);
1132
+ if (matches.length !== 1) continue;
1133
+ const resolved = matches[0];
1077
1134
  out.push({
1078
1135
  variableName: el.name.text,
1079
1136
  alias: resolved.helper.alias,
@@ -1084,7 +1141,7 @@ async function parseServiceBindings(repoPath, filePath) {
1084
1141
  placeholders: resolved.helper.placeholders,
1085
1142
  sourceFile: normalizePath(filePath),
1086
1143
  sourceLine: lineOf4(sourceFileAst, decl),
1087
- helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
1144
+ helperChain: [{ callerVariable: el.name.text, helperFunction: call.expression.getText(sourceFileAst), returnedProperty: propertyName, importSource: resolved.imp?.sourceFile, exportedSymbol: resolved.imp?.exportedName, helperSourceFile: resolved.helper.sourceFile, helperSourceLine: resolved.helper.sourceLine }]
1088
1145
  });
1089
1146
  }
1090
1147
  }
@@ -2221,4 +2278,4 @@ export {
2221
2278
  linkWorkspace,
2222
2279
  trace
2223
2280
  };
2224
- //# sourceMappingURL=chunk-EKLZJJW7.js.map
2281
+ //# sourceMappingURL=chunk-TSMDIKML.js.map