@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.
- package/CHANGELOG.md +10 -0
- package/README.md +11 -5
- package/TECHNICAL-NOTE.md +9 -0
- package/dist/{chunk-AEM4JY22.js → chunk-3N3B5KHV.js} +6983 -5500
- package/dist/chunk-3N3B5KHV.js.map +1 -0
- package/dist/cli.js +317 -105
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/001-index-summary.ts +22 -0
- package/src/cli.ts +151 -87
- package/src/db/000-call-fact-repository.ts +45 -19
- package/src/db/003-current-fact-semantics.ts +1 -0
- package/src/db/004-package-target-invalidation.ts +10 -0
- package/src/db/006-relative-symbol-resolution.ts +28 -7
- package/src/db/008-relative-fact-semantics.ts +6 -3
- package/src/db/009-binding-fact-semantics.ts +5 -0
- package/src/db/012-binding-reference-proof.ts +4 -0
- package/src/db/013-index-publication-failure.ts +91 -0
- package/src/db/014-binding-helper-provenance.ts +17 -0
- package/src/db/repositories.ts +22 -5
- package/src/indexer/cds-extension-resolver.ts +27 -3
- package/src/indexer/repository-indexer.ts +66 -5
- package/src/indexer/workspace-indexer.ts +141 -29
- package/src/linker/004-event-subscription-handler-linker.ts +32 -11
- package/src/linker/006-event-template-link.ts +72 -0
- package/src/linker/007-call-edge-insertion.ts +568 -0
- package/src/linker/cross-repo-linker.ts +2 -165
- package/src/parsers/000-direct-query-execution.ts +11 -0
- package/src/parsers/006-binding-identity.ts +6 -1
- package/src/parsers/014-service-binding-helper-flow.ts +70 -4
- package/src/parsers/021-binding-visibility.ts +17 -1
- package/src/parsers/022-outbound-expression-analysis.ts +700 -0
- package/src/parsers/023-outbound-call-classifier.ts +692 -0
- package/src/parsers/outbound-call-parser.ts +146 -509
- package/src/parsers/symbol-parser.ts +37 -5
- package/src/trace/007-implementation-start-diagnostic.ts +1 -0
- package/src/trace/011-event-subscriber-traversal.ts +100 -8
- package/src/trace/013-trace-root-scopes.ts +2 -3
- package/src/trace/014-compact-contract.ts +6 -0
- package/src/trace/015-trace-edge-recorder.ts +61 -4
- package/src/trace/016-compact-projector.ts +2 -3
- package/src/trace/019-trace-edge-semantics.ts +6 -10
- package/src/trace/020-compact-field-projection.ts +74 -1
- package/src/trace/021-compact-decision-normalization.ts +75 -9
- package/src/trace/024-compact-observation-decision.ts +7 -2
- package/src/trace/026-trace-start-scope.ts +1 -0
- package/src/trace/027-trace-scope-execution.ts +33 -33
- package/src/trace/030-event-runtime-resolution.ts +151 -0
- package/src/trace/031-local-call-expansion.ts +37 -0
- package/src/trace/implementation-hints.ts +9 -6
- package/src/trace/selectors.ts +1 -0
- package/src/types.ts +2 -1
- package/src/version.ts +1 -1
- 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
|
-
:
|
|
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
|
|
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)
|
|
165
|
-
|| ts.
|
|
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
|
-
|
|
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
|
-
:
|
|
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;
|