@saptools/service-flow 0.1.68 → 0.1.69

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +11 -5
  3. package/TECHNICAL-NOTE.md +9 -0
  4. package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
  5. package/dist/chunk-3N3B5KHV.js.map +1 -0
  6. package/dist/cli.js +317 -105
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cli/001-index-summary.ts +22 -0
  12. package/src/cli.ts +151 -87
  13. package/src/db/000-call-fact-repository.ts +45 -19
  14. package/src/db/003-current-fact-semantics.ts +1 -0
  15. package/src/db/004-package-target-invalidation.ts +10 -0
  16. package/src/db/006-relative-symbol-resolution.ts +28 -7
  17. package/src/db/008-relative-fact-semantics.ts +6 -3
  18. package/src/db/009-binding-fact-semantics.ts +5 -0
  19. package/src/db/012-binding-reference-proof.ts +4 -0
  20. package/src/db/013-index-publication-failure.ts +91 -0
  21. package/src/db/014-binding-helper-provenance.ts +17 -0
  22. package/src/db/repositories.ts +22 -5
  23. package/src/indexer/cds-extension-resolver.ts +27 -3
  24. package/src/indexer/repository-indexer.ts +66 -5
  25. package/src/indexer/workspace-indexer.ts +141 -29
  26. package/src/linker/004-event-subscription-handler-linker.ts +32 -11
  27. package/src/linker/006-event-template-link.ts +72 -0
  28. package/src/linker/007-call-edge-insertion.ts +568 -0
  29. package/src/linker/cross-repo-linker.ts +2 -165
  30. package/src/parsers/000-direct-query-execution.ts +11 -0
  31. package/src/parsers/006-binding-identity.ts +6 -1
  32. package/src/parsers/014-service-binding-helper-flow.ts +70 -4
  33. package/src/parsers/021-binding-visibility.ts +17 -1
  34. package/src/parsers/022-outbound-expression-analysis.ts +700 -0
  35. package/src/parsers/023-outbound-call-classifier.ts +692 -0
  36. package/src/parsers/outbound-call-parser.ts +146 -509
  37. package/src/parsers/symbol-parser.ts +37 -5
  38. package/src/trace/007-implementation-start-diagnostic.ts +1 -0
  39. package/src/trace/011-event-subscriber-traversal.ts +100 -8
  40. package/src/trace/013-trace-root-scopes.ts +2 -3
  41. package/src/trace/014-compact-contract.ts +6 -0
  42. package/src/trace/015-trace-edge-recorder.ts +61 -4
  43. package/src/trace/016-compact-projector.ts +2 -3
  44. package/src/trace/019-trace-edge-semantics.ts +6 -10
  45. package/src/trace/020-compact-field-projection.ts +74 -1
  46. package/src/trace/021-compact-decision-normalization.ts +75 -9
  47. package/src/trace/024-compact-observation-decision.ts +7 -2
  48. package/src/trace/026-trace-start-scope.ts +1 -0
  49. package/src/trace/027-trace-scope-execution.ts +33 -33
  50. package/src/trace/030-event-runtime-resolution.ts +151 -0
  51. package/src/trace/031-local-call-expansion.ts +37 -0
  52. package/src/trace/implementation-hints.ts +9 -6
  53. package/src/trace/selectors.ts +1 -0
  54. package/src/types.ts +2 -1
  55. package/src/version.ts +1 -1
  56. package/dist/chunk-AEM4JY22.js.map +0 -1
@@ -1,19 +1,9 @@
1
1
  import type { Db } from '../db/connection.js';
2
- import { applyVariables } from './dynamic-edge-resolver.js';
3
- import { classifyODataPathIntent, normalizeODataOperationInvocationPath } from './odata-path-normalizer.js';
4
- import { buildRemoteQueryTarget } from './remote-query-target.js';
5
- import { resolveOperation } from './service-resolver.js';
6
2
  import { linkHelperPackages } from './helper-package-linker.js';
7
- import { externalHttpTarget } from './external-http-target.js';
8
3
  import { linkImplementations as linkCanonicalImplementations } from './000-implementation-candidates.js';
9
- import {
10
- ambiguousPathCandidates,
11
- linkedCallEvidence,
12
- objectJson,
13
- objectValue,
14
- } from './002-call-evidence.js';
15
4
  import { linkPackageImportSymbolCalls } from './003-package-import-symbol-resolver.js';
16
5
  import { linkEventSubscriptionHandlers } from './004-event-subscription-handler-linker.js';
6
+ import { insertCallEdge } from './007-call-edge-insertion.js';
17
7
  import { assertWorkspaceLinkable } from '../db/001-fact-lifecycle.js';
18
8
  export interface LinkWorkspaceResult {
19
9
  edgeCount: number;
@@ -43,7 +33,7 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
43
33
  db.prepare('DELETE FROM graph_edges WHERE workspace_id=?').run(workspaceId);
44
34
  const deps = linkHelperPackages(db, workspaceId, generation);
45
35
  const subscriptions = linkEventSubscriptionHandlers(
46
- db, workspaceId, generation,
36
+ db, workspaceId, generation, vars,
47
37
  );
48
38
  const impl = linkCanonicalImplementations(db, workspaceId, generation);
49
39
  const callSummary = linkCalls(db, workspaceId, vars, generation);
@@ -89,156 +79,3 @@ function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, ge
89
79
  }
90
80
  return { edgeCount, unresolvedCount, resolvedCount, remoteResolvedCount, localResolvedCount, ambiguousCount, dynamicCount, terminalCount };
91
81
  }
92
- function insertCallEdge(db: Db, workspaceId: number, call: Record<string, unknown>, vars: Record<string, string>, generation: number): { status: string; callType: string } {
93
- const callType = String(call.call_type);
94
- const rawOp = applyVariables(String(call.operation_path_expr ?? ''), vars);
95
- const intent = classifyODataPathIntent(rawOp, call.method as string | undefined);
96
- const isEntityQueryIntent = ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
97
- const resolutionRawOp = callType === 'remote_query' && isEntityQueryIntent ? intent.pathWithoutQuery : rawOp;
98
- const normalized = normalizeODataOperationInvocationPath(resolutionRawOp);
99
- const op = normalized?.normalizedOperationPath ?? resolutionRawOp;
100
- const servicePath = applyVariables((call.servicePathExpr as string | undefined) ?? (call.requireServicePath as string | undefined), vars);
101
- const destination = (call.destinationExpr as string | undefined) ?? (call.requireDestination as string | undefined);
102
- const isDynamic = Boolean(Number(call.isDynamic ?? 0));
103
- const isRemoteEntityCall = callType.startsWith('remote_entity_');
104
- const indexedOperationCandidateCount = operationCandidateCount(db, workspaceId, op, intent.topLevelOperationName);
105
- const credibleOperationSignal = Boolean(normalized?.wasInvocation) || (Boolean(intent.topLevelOperationNameCandidate) && indexedOperationCandidateCount > 0);
106
- const strongEntitySignal = ['entity_media', 'entity_delete', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind) || (intent.kind === 'entity_mutation' && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix));
107
- const operationLikeRemoteEntity = isRemoteEntityCall && Boolean(op) && credibleOperationSignal && (!strongEntitySignal || indexedOperationCandidateCount > 0);
108
- const isOperationCall = operationLikeRemoteEntity || ((callType === 'remote_action' || callType === 'local_service_call') || (callType === 'remote_query' && Boolean(op)));
109
- const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name as string | undefined, repoId: callType === 'local_service_call' ? Number(call.repo_id) : undefined, alias: applyVariables((call.aliasExpr as string | undefined) ?? (call.alias as string | undefined), vars), destination: destination ? applyVariables(destination, vars) : undefined, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === 'local_service_call' }, workspaceId) : { status: 'unresolved' as const, candidates: [], reasons: [] };
110
- const evidence: Record<string, unknown> = {
111
- ...linkedCallEvidence(
112
- call,
113
- resolution,
114
- servicePath,
115
- op,
116
- destination ? applyVariables(destination, vars) : undefined,
117
- normalized,
118
- intent,
119
- ),
120
- indexedOperationCandidateCount,
121
- parserCallType: callType,
122
- entityOperationPrecedence: operationPrecedence(
123
- callType,
124
- intent,
125
- indexedOperationCandidateCount,
126
- Boolean(resolution.target),
127
- ),
128
- };
129
- const pathAnalysis = objectValue(objectJson(call.evidence_json)?.pathAnalysis);
130
- if (callType === 'remote_action' && pathAnalysis?.status === 'ambiguous') {
131
- const candidatePaths = ambiguousPathCandidates(pathAnalysis);
132
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(
133
- workspaceId,
134
- 'UNRESOLVED_EDGE',
135
- 'ambiguous',
136
- 'call',
137
- String(call.id),
138
- 'operation_candidates',
139
- candidatePaths.items.join(','),
140
- Number(call.confidence ?? 0.5),
141
- JSON.stringify({
142
- ...evidence,
143
- ambiguousOperationPathCandidateCount: candidatePaths.totalCount,
144
- shownAmbiguousOperationPathCandidateCount: candidatePaths.shownCount,
145
- omittedAmbiguousOperationPathCandidateCount: candidatePaths.omittedCount,
146
- }),
147
- 0,
148
- 'Ambiguous operation path candidates require explicit disambiguation',
149
- generation,
150
- );
151
- return { status: 'ambiguous', callType };
152
- }
153
- if (isRemoteEntityCall && (resolution.target || resolution.candidates.length > 0 || resolution.status === 'dynamic')) {
154
- if (resolution.target) {
155
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'REMOTE_CALL_RESOLVES_TO_OPERATION', 'resolved', 'call', String(call.id), 'operation', String(resolution.target.operationId), resolution.target.score, JSON.stringify({ ...evidence, operationEntityPrecedence: 'indexed_operation_over_parser_entity' }), 0, generation);
156
- return { status: 'resolved', callType };
157
- }
158
- const status = resolution.status === 'dynamic' ? 'dynamic' : resolution.status === 'ambiguous' ? 'ambiguous' : 'unresolved';
159
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, status === 'dynamic' ? 'DYNAMIC_EDGE_CANDIDATE' : 'UNRESOLVED_EDGE', status, 'call', String(call.id), 'operation_candidate', op ? `Remote action: ${op}` : 'Remote action: unknown path', Number(call.confidence ?? 0.2), JSON.stringify({ ...evidence, operationEntityPrecedence: resolution.candidates.length > 0 ? 'parser_entity_with_indexed_operation_candidates' : 'parser_entity_operation_candidate_without_indexed_match' }), status === 'dynamic' ? 1 : 0, unresolvedOperationReason(resolution), generation);
160
- return { status, callType };
161
- }
162
- if (isRemoteEntityCall) {
163
- const target = buildRemoteQueryTarget({ queryEntity: intent.entitySegment ?? call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : undefined, isDynamic, parserWarning: evidence.parserWarning });
164
- const entityKind = callType;
165
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'HANDLER_ACCESSES_REMOTE_ENTITY', 'terminal', 'call', String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence, remoteEntityAccess: entityKind }), 0, generation);
166
- return { status: 'terminal', callType };
167
- }
168
- if (callType === 'remote_query' && (isEntityQueryIntent || !op) && !resolution.target) {
169
- const target = buildRemoteQueryTarget({ queryEntity: call.query_entity, servicePath, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination: destination ? applyVariables(destination, vars) : undefined, isDynamic, parserWarning: evidence.parserWarning });
170
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'HANDLER_RUNS_REMOTE_QUERY', 'terminal', 'call', String(call.id), target.toKind, target.toId, Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, ...target.evidence }), 0, generation);
171
- return { status: 'terminal', callType };
172
- }
173
- if (callType === 'local_service_call' && call.unresolved_reason === 'transport_client_method' && !resolution.target && resolution.candidates.length === 0) {
174
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, 'HANDLER_CALLS_TRANSPORT_METHOD', 'terminal', 'call', String(call.id), 'transport_method', String(op || 'transport_client_method'), Number(call.confidence ?? 0.5), JSON.stringify({ ...evidence, classification: 'transport_client_method' }), 0, generation);
175
- return { status: 'terminal', callType };
176
- }
177
- if (resolution.target) {
178
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, callType === 'local_service_call' ? 'LOCAL_CALL_RESOLVES_TO_OPERATION' : 'REMOTE_CALL_RESOLVES_TO_OPERATION', 'resolved', 'call', String(call.id), 'operation', String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), 0, generation);
179
- return { status: 'resolved', callType };
180
- }
181
- const edgeType = callType === 'local_db_query' ? 'HANDLER_RUNS_DB_QUERY' : callType === 'external_http' ? 'HANDLER_CALLS_EXTERNAL_HTTP' : callType === 'async_emit' ? 'HANDLER_EMITS_EVENT' : callType === 'async_subscribe' ? 'EVENT_CONSUMED_BY_HANDLER' : resolution.status === 'dynamic' ? 'DYNAMIC_EDGE_CANDIDATE' : 'UNRESOLVED_EDGE';
182
- const status = edgeType === 'DYNAMIC_EDGE_CANDIDATE' ? 'dynamic' : resolution.status === 'ambiguous' ? 'ambiguous' : edgeType === 'UNRESOLVED_EDGE' ? 'unresolved' : 'terminal';
183
- const unresolvedReason = status === 'terminal' ? null : String(call.unresolved_reason ?? unresolvedOperationReason(resolution));
184
- const externalTarget = callType === 'external_http' ? externalHttpTarget(call) : undefined;
185
- const targetKind = callType === 'local_db_query' ? 'db_entity' : callType.startsWith('async_') ? 'event' : callType === 'external_http' ? (externalTarget?.toKind ?? 'external_endpoint') : 'operation_candidate';
186
- const targetId = callType === 'local_db_query' ? String(call.query_entity ?? 'unknown') : callType === 'remote_action' ? (op ? `Remote action: ${op}` : (call.unresolved_reason === 'dynamic_operation_path_identifier' ? 'Remote action: dynamic path' : 'Remote action: unknown path')) : callType === 'external_http' ? String(externalTarget?.toId ?? 'unknown') : String(call.event_name_expr ?? op ?? 'unknown');
187
- const graphLevelDynamic = edgeType === 'DYNAMIC_EDGE_CANDIDATE' && resolution.status === 'dynamic';
188
- const finalEvidence = externalTarget ? { ...evidence, externalTarget } : evidence;
189
- db.prepare('INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)').run(workspaceId, edgeType, status, 'call', String(call.id), targetKind, targetId, Number(call.confidence ?? 0.2), JSON.stringify(finalEvidence), graphLevelDynamic ? 1 : 0, unresolvedReason, generation);
190
- return { status, callType };
191
- }
192
- function operationCandidateCount(db: Db, workspaceId: number, operationPath: string | undefined, operationName: string | undefined): number {
193
- if (!operationPath && !operationName) return 0;
194
- const normalizedName = operationName ?? operationPath?.replace(/^\//, '').split('.').at(-1);
195
- const row = db.prepare(`SELECT COUNT(*) count FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_path=? OR o.operation_path=? OR o.operation_name=?)`).get(workspaceId, operationPath, normalizedName ? `/${normalizedName}` : operationPath, normalizedName) as { count?: number } | undefined;
196
- return Number(row?.count ?? 0);
197
- }
198
-
199
- function operationPrecedence(
200
- callType: string,
201
- intent: ReturnType<typeof classifyODataPathIntent>,
202
- indexedOperationCandidateCount: number,
203
- resolvedOperation: boolean,
204
- ): Record<string, unknown> {
205
- if (resolvedOperation) {
206
- return {
207
- decision: 'operation',
208
- reason: 'indexed_operation_with_strong_service_context',
209
- indexedOperationCandidateCount,
210
- };
211
- }
212
- if (callType === 'remote_action' && intent.kind === 'operation_invocation') {
213
- return {
214
- decision: 'operation_candidate',
215
- rejectionReason: indexedOperationCandidateCount > 0
216
- ? 'indexed_candidates_lack_unique_strong_service_context'
217
- : 'no_indexed_operation_candidate',
218
- indexedOperationCandidateCount,
219
- };
220
- }
221
- if (intent.kind.startsWith('entity_')) {
222
- return {
223
- decision: 'entity',
224
- rejectionReason: indexedOperationCandidateCount > 0
225
- ? 'entity_shape_has_precedence_without_resolved_operation_context'
226
- : 'entity_shape_has_no_indexed_operation_evidence',
227
- indexedOperationCandidateCount,
228
- };
229
- }
230
- return {
231
- decision: 'unresolved',
232
- rejectionReason: 'path_has_no_safe_entity_or_operation_precedence',
233
- indexedOperationCandidateCount,
234
- };
235
- }
236
-
237
- function unresolvedOperationReason(resolution: { candidates: unknown[]; status: string; reasons: string[] }): string {
238
- if (resolution.status === 'dynamic') return `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ['missing runtime variables']).join(', ')}`;
239
- if (resolution.candidates.length === 0) return 'No indexed target operation matched';
240
- if (resolution.reasons.includes('operation_path_only_has_no_strong_target_signal')) return 'Operation candidates found but no strong service signal is available';
241
- if (resolution.reasons.includes('candidate_score_below_resolution_threshold')) return 'Operation candidates found but resolution score is below threshold';
242
- if (resolution.status === 'ambiguous') return 'Ambiguous operation candidates require a strong service signal';
243
- return 'Operation candidates found but resolution could not select a target';
244
- }
@@ -43,6 +43,17 @@ export function queryBuilderRoot(
43
43
  : undefined;
44
44
  }
45
45
 
46
+ export function chainIncludesForUpdate(node: ts.Expression): boolean {
47
+ let current = unwrapQueryExpression(node);
48
+ while (ts.isCallExpression(current)
49
+ && ts.isPropertyAccessExpression(current.expression)) {
50
+ if (isCapQueryBuilderRootName(expressionName(current.expression))) break;
51
+ if (current.expression.name.text === 'forUpdate') return true;
52
+ current = unwrapQueryExpression(current.expression.expression);
53
+ }
54
+ return false;
55
+ }
56
+
46
57
  export function directQueryBuilderStatement(
47
58
  node: ts.CallExpression,
48
59
  ): DirectQueryBuilderStatement | undefined {
@@ -230,6 +230,9 @@ function resolvedReference(
230
230
  chain: LexicalScopeFact[],
231
231
  scopeIndex: number,
232
232
  ): ServiceBindingReference {
233
+ const helperReturn = selected.fact.helperChain?.some(
234
+ (step) => step.bindingOrigin === 'single_hop_helper_return',
235
+ ) ?? false;
233
236
  return {
234
237
  status: 'resolved_exact',
235
238
  variableName: call.serviceVariableName,
@@ -240,7 +243,9 @@ function resolvedReference(
240
243
  ? 'deterministic_reaching_assignment'
241
244
  : selected.site.aliasSource
242
245
  ? 'lexical_alias_declaration'
243
- : 'lexical_declaration',
246
+ : helperReturn
247
+ ? 'single_hop_helper_return'
248
+ : 'lexical_declaration',
244
249
  lexicalScopeChain: chain,
245
250
  bindingScopeIndex: scopeIndex,
246
251
  scopeChainTotal: chain.length,
@@ -157,12 +157,58 @@ function returnedProperty(
157
157
  return { propertyName, variableName: property.initializer.text };
158
158
  }
159
159
 
160
- function branchDependent(node: ts.Node, fn: ts.FunctionLikeDeclaration): boolean {
160
+ function hazardousTryClause(
161
+ node: ts.Node | undefined,
162
+ fn: ts.FunctionLikeDeclaration,
163
+ anyReturn: boolean,
164
+ ): boolean {
165
+ if (!node) return false;
166
+ let found = false;
167
+ const visit = (current: ts.Node): void => {
168
+ if (found || current !== fn && ts.isFunctionLike(current)) return;
169
+ if (ts.isReturnStatement(current)
170
+ && (anyReturn || current.expression !== undefined)
171
+ || ts.isCallExpression(current) && connectFactFromCall(current)) {
172
+ found = true;
173
+ return;
174
+ }
175
+ ts.forEachChild(current, visit);
176
+ };
177
+ visit(node);
178
+ return found;
179
+ }
180
+
181
+ function hasAncestor(node: ts.Node, ancestor: ts.Node): boolean {
182
+ let current: ts.Node | undefined = node.parent;
183
+ while (current) {
184
+ if (current === ancestor) return true;
185
+ current = current.parent;
186
+ }
187
+ return false;
188
+ }
189
+
190
+ function unsupportedTryAncestor(
191
+ statement: ts.TryStatement,
192
+ returned: ts.Node,
193
+ fn: ts.FunctionLikeDeclaration,
194
+ ): boolean {
195
+ if (!hasAncestor(returned, statement.tryBlock)) return true;
196
+ if (hazardousTryClause(statement.catchClause?.block, fn, false))
197
+ return true;
198
+ return hazardousTryClause(statement.finallyBlock, fn, true);
199
+ }
200
+
201
+ function branchDependent(
202
+ node: ts.Node,
203
+ fn: ts.FunctionLikeDeclaration,
204
+ ): boolean {
161
205
  let current: ts.Node | undefined = node.parent;
162
206
  while (current && current !== fn) {
163
207
  if (ts.isIfStatement(current) || ts.isConditionalExpression(current)
164
- || ts.isSwitchStatement(current) || ts.isIterationStatement(current, false)
165
- || ts.isTryStatement(current)) return true;
208
+ || ts.isSwitchStatement(current)
209
+ || ts.isIterationStatement(current, false)) return true;
210
+ if (ts.isTryStatement(current)
211
+ && unsupportedTryAncestor(current, node, fn)) return true;
166
212
  current = current.parent;
167
213
  }
168
214
  return false;
@@ -231,10 +277,30 @@ function directReturnConnectFact(
231
277
  return directConnectFact(returned);
232
278
  }
233
279
 
280
+ function hasTryAncestor(
281
+ returned: ts.ReturnStatement,
282
+ fn: ts.FunctionLikeDeclaration,
283
+ ): boolean {
284
+ let current: ts.Node | undefined = returned.parent;
285
+ while (current && current !== fn) {
286
+ if (ts.isTryStatement(current)) return true;
287
+ current = current.parent;
288
+ }
289
+ return false;
290
+ }
291
+
234
292
  export function directConnectFactFromFunctionLike(
235
293
  fn: ts.FunctionLikeDeclaration,
236
294
  ): LocalBindingFact | undefined {
237
295
  if (ts.isArrowFunction(fn) && fn.body && !ts.isBlock(fn.body))
238
296
  return directConnectFact(fn.body);
239
- return directReturnConnectFact(fn);
297
+ const returned = singleReturn(fn);
298
+ const fact = directReturnConnectFact(fn);
299
+ if (!fact || !returned || !hasTryAncestor(returned, fn)) return fact;
300
+ return {
301
+ ...fact,
302
+ helperChain: [...(fact.helperChain ?? []), {
303
+ bindingOrigin: 'single_hop_helper_return',
304
+ }],
305
+ };
240
306
  }
@@ -72,6 +72,20 @@ function candidatesAtSite<T>(
72
72
  && candidate.bindingSiteEndOffset === site.endOffset);
73
73
  }
74
74
 
75
+ function helperCallDeclaration(site: BindingLexicalSite): boolean {
76
+ if (!ts.isVariableDeclaration(site.node) || !site.node.initializer)
77
+ return false;
78
+ let expression = site.node.initializer;
79
+ while (ts.isAwaitExpression(expression)
80
+ || ts.isParenthesizedExpression(expression)
81
+ || ts.isAsExpression(expression)
82
+ || ts.isSatisfiesExpression(expression)
83
+ || ts.isTypeAssertionExpression(expression))
84
+ expression = expression.expression;
85
+ return ts.isCallExpression(expression)
86
+ && ts.isIdentifier(expression.expression);
87
+ }
88
+
75
89
  function compatibleCaseClause(
76
90
  site: BindingLexicalSite,
77
91
  useNode: ts.Node,
@@ -108,7 +122,9 @@ function matchedBinding<T>(
108
122
  status: 'unresolved',
109
123
  reason: site.flow === 'assignment'
110
124
  ? 'unsupported_reaching_assignment'
111
- : 'binding_not_found',
125
+ : helperCallDeclaration(site)
126
+ ? 'binding_flow_unsupported'
127
+ : 'binding_not_found',
112
128
  };
113
129
  if (matches.length > 1) return { status: 'ambiguous' };
114
130
  return matches[0] ? { status: 'resolved', candidate: matches[0] } : undefined;