@saptools/service-flow 0.1.67 → 0.1.68
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 +20 -8
- package/TECHNICAL-NOTE.md +27 -0
- package/dist/{chunk-ZQABU7MR.js → chunk-AEM4JY22.js} +12676 -6714
- package/dist/chunk-AEM4JY22.js.map +1 -0
- package/dist/cli.js +2325 -413
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +61 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/003-doctor-package-resolution.ts +68 -0
- package/src/cli/doctor.ts +25 -14
- package/src/db/000-call-fact-repository.ts +475 -342
- package/src/db/001-fact-lifecycle.ts +60 -30
- package/src/db/002-fact-json-inventory.ts +169 -0
- package/src/db/003-current-fact-semantics.ts +698 -0
- package/src/db/004-package-target-invalidation.ts +173 -0
- package/src/db/005-schema-structure.ts +201 -0
- package/src/db/006-relative-symbol-resolution.ts +443 -0
- package/src/db/007-package-fact-semantics.ts +573 -0
- package/src/db/008-relative-fact-semantics.ts +207 -0
- package/src/db/009-binding-fact-semantics.ts +347 -0
- package/src/db/010-package-symbol-surface-semantics.ts +320 -0
- package/src/db/011-symbol-call-semantics.ts +144 -0
- package/src/db/012-binding-reference-proof.ts +264 -0
- package/src/db/migrations.ts +16 -3
- package/src/db/repositories.ts +113 -6
- package/src/db/schema.ts +4 -2
- package/src/index.ts +12 -0
- package/src/indexer/repository-indexer.ts +72 -11
- package/src/indexer/workspace-indexer.ts +123 -32
- package/src/linker/003-package-import-symbol-resolver.ts +363 -131
- package/src/linker/004-event-subscription-handler-linker.ts +2 -0
- package/src/linker/005-odata-path-structure.ts +371 -0
- package/src/linker/cross-repo-linker.ts +2 -1
- package/src/linker/odata-path-normalizer.ts +273 -180
- package/src/linker/service-resolver.ts +197 -77
- package/src/parsers/002-symbol-import-bindings.ts +516 -0
- package/src/parsers/003-package-public-surface.ts +661 -0
- package/src/parsers/004-fact-identity.ts +108 -0
- package/src/parsers/005-event-subscription-facts.ts +281 -0
- package/src/parsers/006-binding-identity.ts +343 -0
- package/src/parsers/007-source-fact-reconciliation.ts +105 -0
- package/src/parsers/008-package-surface-publication.ts +82 -0
- package/src/parsers/009-symbol-call-facts.ts +528 -0
- package/src/parsers/010-package-public-surface-analysis.ts +352 -0
- package/src/parsers/011-binding-lexical-scope.ts +583 -0
- package/src/parsers/012-package-fact-contract.ts +306 -0
- package/src/parsers/013-executable-body-eligibility.ts +35 -0
- package/src/parsers/014-service-binding-helper-flow.ts +240 -0
- package/src/parsers/015-service-binding-collector.ts +693 -0
- package/src/parsers/016-local-symbol-reference.ts +261 -0
- package/src/parsers/017-symbol-derived-contexts.ts +268 -0
- package/src/parsers/018-package-commonjs-syntax.ts +142 -0
- package/src/parsers/019-binding-assignment-targets.ts +76 -0
- package/src/parsers/020-stable-local-value.ts +217 -0
- package/src/parsers/021-binding-visibility.ts +152 -0
- package/src/parsers/operation-path-analysis.ts +6 -1
- package/src/parsers/outbound-call-parser.ts +19 -6
- package/src/parsers/package-json-parser.ts +45 -3
- package/src/parsers/service-binding-parser-helpers.ts +86 -15
- package/src/parsers/service-binding-parser.ts +147 -597
- package/src/parsers/symbol-parser.ts +482 -353
- package/src/trace/002-trace-diagnostics.ts +36 -1
- package/src/trace/016-compact-projector.ts +16 -17
- package/src/trace/020-compact-field-projection.ts +48 -37
- package/src/trace/021-compact-decision-normalization.ts +105 -0
- package/src/trace/022-trace-fact-preflight.ts +21 -0
- package/src/trace/023-nested-event-scopes.ts +23 -0
- package/src/trace/024-compact-observation-decision.ts +76 -0
- package/src/trace/025-trace-implementation-scope.ts +123 -0
- package/src/trace/026-trace-start-scope.ts +335 -0
- package/src/trace/027-trace-scope-execution.ts +566 -0
- package/src/trace/028-trace-operation-execution.ts +336 -0
- package/src/trace/029-trace-start-implementation.ts +172 -0
- package/src/trace/trace-engine.ts +122 -624
- package/src/types.ts +56 -0
- package/src/utils/001-placeholders.ts +188 -10
- package/src/version.ts +1 -1
- package/dist/chunk-ZQABU7MR.js.map +0 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type {
|
|
3
|
+
ExecutableSymbolFact,
|
|
4
|
+
LexicalScopeFact,
|
|
5
|
+
OutboundCallFact,
|
|
6
|
+
ServiceBindingFact,
|
|
7
|
+
ServiceBindingReference,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
import {
|
|
10
|
+
executableSymbolCandidates,
|
|
11
|
+
selectCallOwner,
|
|
12
|
+
} from './004-fact-identity.js';
|
|
13
|
+
import {
|
|
14
|
+
bindingSite,
|
|
15
|
+
createBindingLexicalIndex,
|
|
16
|
+
lexicalScopeChain,
|
|
17
|
+
type BindingLexicalIndex,
|
|
18
|
+
type BindingLexicalSite,
|
|
19
|
+
type VisibleBinding,
|
|
20
|
+
} from './011-binding-lexical-scope.js';
|
|
21
|
+
import { selectVisibleBinding } from './021-binding-visibility.js';
|
|
22
|
+
|
|
23
|
+
const scopeChainCap = 16;
|
|
24
|
+
|
|
25
|
+
interface PreparedBinding {
|
|
26
|
+
fact: ServiceBindingFact;
|
|
27
|
+
site: BindingLexicalSite;
|
|
28
|
+
aliasProven: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchFactsToSites(
|
|
32
|
+
index: BindingLexicalIndex,
|
|
33
|
+
facts: readonly ServiceBindingFact[],
|
|
34
|
+
): Array<{ fact: ServiceBindingFact; site: BindingLexicalSite }> {
|
|
35
|
+
return facts.map((fact) => {
|
|
36
|
+
if (fact.bindingSiteStartOffset === undefined
|
|
37
|
+
|| fact.bindingSiteEndOffset === undefined)
|
|
38
|
+
throw new Error('invalid_prepared_repository_snapshot:binding_site_missing');
|
|
39
|
+
const site = bindingSite(
|
|
40
|
+
index,
|
|
41
|
+
fact.variableName,
|
|
42
|
+
fact.bindingSiteStartOffset,
|
|
43
|
+
fact.bindingSiteEndOffset,
|
|
44
|
+
);
|
|
45
|
+
if (!site)
|
|
46
|
+
throw new Error('invalid_prepared_repository_snapshot:binding_site_invalid');
|
|
47
|
+
return { fact, site };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function exactOwner(
|
|
52
|
+
fact: ServiceBindingFact,
|
|
53
|
+
site: BindingLexicalSite,
|
|
54
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
55
|
+
): ServiceBindingFact {
|
|
56
|
+
const candidates = executableSymbolCandidates(symbols, fact.sourceFile);
|
|
57
|
+
const owner = selectCallOwner(
|
|
58
|
+
candidates, site.startOffset, site.endOffset,
|
|
59
|
+
);
|
|
60
|
+
if (owner.status === 'ambiguous')
|
|
61
|
+
throw new Error('invalid_prepared_repository_snapshot:binding_owner_ambiguous');
|
|
62
|
+
return {
|
|
63
|
+
...fact,
|
|
64
|
+
bindingSiteStartOffset: site.startOffset,
|
|
65
|
+
bindingSiteEndOffset: site.endOffset,
|
|
66
|
+
sourceSymbolQualifiedName: owner.owner?.qualifiedName,
|
|
67
|
+
ownerResolution: owner.status === 'resolved'
|
|
68
|
+
? 'owned_exact'
|
|
69
|
+
: 'ownerless_file_scope',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function prepareBindings(
|
|
74
|
+
index: BindingLexicalIndex,
|
|
75
|
+
facts: readonly ServiceBindingFact[],
|
|
76
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
77
|
+
): PreparedBinding[] {
|
|
78
|
+
return matchFactsToSites(index, facts).map(({ fact, site }) => ({
|
|
79
|
+
fact: exactOwner(fact, site, symbols),
|
|
80
|
+
site,
|
|
81
|
+
aliasProven: site.aliasSource === undefined,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function aliasStep(binding: PreparedBinding, source: PreparedBinding): Record<string, unknown> {
|
|
86
|
+
const aliasKind = binding.site.aliasKind
|
|
87
|
+
?? (binding.site.flow === 'assignment'
|
|
88
|
+
? 'identity-assignment'
|
|
89
|
+
: 'identity');
|
|
90
|
+
return {
|
|
91
|
+
callerVariable: binding.fact.variableName,
|
|
92
|
+
aliasOf: source.fact.variableName,
|
|
93
|
+
aliasKind,
|
|
94
|
+
scopeRule: 'exact_lexical_scope',
|
|
95
|
+
...(aliasKind === 'transaction'
|
|
96
|
+
? { transactionAliasSource: source.fact.variableName }
|
|
97
|
+
: {}),
|
|
98
|
+
...(binding.site.aliasArrayIndex === undefined ? {} : {
|
|
99
|
+
sourceVariable: source.fact.variableName,
|
|
100
|
+
arrayIndex: binding.site.aliasArrayIndex,
|
|
101
|
+
promiseAll: binding.site.aliasPromiseAll ?? false,
|
|
102
|
+
arrayContainer: binding.site.aliasPromiseAll
|
|
103
|
+
? 'Promise.all'
|
|
104
|
+
: 'array_literal',
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function copyAliasProvenance(
|
|
110
|
+
binding: PreparedBinding,
|
|
111
|
+
source: PreparedBinding,
|
|
112
|
+
): void {
|
|
113
|
+
binding.fact = {
|
|
114
|
+
...binding.fact,
|
|
115
|
+
alias: source.fact.alias,
|
|
116
|
+
aliasExpr: source.fact.aliasExpr,
|
|
117
|
+
destinationExpr: source.fact.destinationExpr,
|
|
118
|
+
servicePathExpr: source.fact.servicePathExpr,
|
|
119
|
+
isDynamic: source.fact.isDynamic,
|
|
120
|
+
placeholders: source.fact.placeholders,
|
|
121
|
+
helperChain: [
|
|
122
|
+
...(source.fact.helperChain ?? []),
|
|
123
|
+
aliasStep(binding, source),
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
binding.aliasProven = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function bindingCandidates(
|
|
130
|
+
bindings: readonly PreparedBinding[],
|
|
131
|
+
): Array<{
|
|
132
|
+
variableName: string;
|
|
133
|
+
bindingSiteStartOffset: number;
|
|
134
|
+
bindingSiteEndOffset: number;
|
|
135
|
+
value: PreparedBinding;
|
|
136
|
+
}> {
|
|
137
|
+
return bindings.map((binding) => ({
|
|
138
|
+
variableName: binding.fact.variableName,
|
|
139
|
+
bindingSiteStartOffset: binding.site.startOffset,
|
|
140
|
+
bindingSiteEndOffset: binding.site.endOffset,
|
|
141
|
+
value: binding,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolvedAliasSource(
|
|
146
|
+
selected: VisibleBinding<PreparedBinding>,
|
|
147
|
+
): PreparedBinding | undefined {
|
|
148
|
+
if (selected.status !== 'resolved') return undefined;
|
|
149
|
+
if (!selected.site?.deterministic) return undefined;
|
|
150
|
+
if (selected.declarationSite?.declarationKind === 'var') return undefined;
|
|
151
|
+
const source = selected.candidate?.value;
|
|
152
|
+
return source?.aliasProven ? source : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function reconcileAliases(
|
|
156
|
+
index: BindingLexicalIndex,
|
|
157
|
+
bindings: PreparedBinding[],
|
|
158
|
+
): void {
|
|
159
|
+
const candidates = bindingCandidates(bindings);
|
|
160
|
+
for (const binding of bindings) {
|
|
161
|
+
const sourceName = binding.site.aliasSource;
|
|
162
|
+
if (!sourceName) continue;
|
|
163
|
+
const selected = selectVisibleBinding(
|
|
164
|
+
index, candidates, sourceName, binding.site.node,
|
|
165
|
+
);
|
|
166
|
+
const source = resolvedAliasSource(selected);
|
|
167
|
+
if (source) copyAliasProvenance(binding, source);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertUniqueSites(bindings: readonly PreparedBinding[]): void {
|
|
172
|
+
const seen = new Set<string>();
|
|
173
|
+
for (const binding of bindings) {
|
|
174
|
+
const key = [
|
|
175
|
+
binding.fact.sourceFile,
|
|
176
|
+
binding.fact.variableName,
|
|
177
|
+
binding.site.startOffset,
|
|
178
|
+
binding.site.endOffset,
|
|
179
|
+
].join('\u0000');
|
|
180
|
+
if (seen.has(key))
|
|
181
|
+
throw new Error('invalid_prepared_repository_snapshot:duplicate_service_binding_site');
|
|
182
|
+
seen.add(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function callNodeMap(source: ts.SourceFile): Map<string, ts.CallExpression> {
|
|
187
|
+
const calls = new Map<string, ts.CallExpression>();
|
|
188
|
+
const visit = (node: ts.Node): void => {
|
|
189
|
+
if (ts.isCallExpression(node))
|
|
190
|
+
calls.set(`${node.getStart(source)}:${node.getEnd()}`, node);
|
|
191
|
+
ts.forEachChild(node, visit);
|
|
192
|
+
};
|
|
193
|
+
visit(source);
|
|
194
|
+
return calls;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function emptyReference(
|
|
198
|
+
call: OutboundCallFact,
|
|
199
|
+
reason?: ServiceBindingReference['reason'],
|
|
200
|
+
): ServiceBindingReference {
|
|
201
|
+
return {
|
|
202
|
+
status: call.serviceVariableName ? 'unresolved' : 'not_applicable',
|
|
203
|
+
variableName: call.serviceVariableName,
|
|
204
|
+
scopeChainTotal: 0,
|
|
205
|
+
scopeChainShown: 0,
|
|
206
|
+
scopeChainOmitted: 0,
|
|
207
|
+
reason,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function rejectedReference(
|
|
212
|
+
call: OutboundCallFact,
|
|
213
|
+
chain: LexicalScopeFact[],
|
|
214
|
+
reason: ServiceBindingReference['reason'],
|
|
215
|
+
status: 'ambiguous' | 'unresolved' = 'unresolved',
|
|
216
|
+
): ServiceBindingReference {
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
variableName: call.serviceVariableName,
|
|
220
|
+
scopeChainTotal: chain.length,
|
|
221
|
+
scopeChainShown: Math.min(chain.length, scopeChainCap),
|
|
222
|
+
scopeChainOmitted: Math.max(0, chain.length - scopeChainCap),
|
|
223
|
+
reason,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resolvedReference(
|
|
228
|
+
call: OutboundCallFact,
|
|
229
|
+
selected: PreparedBinding,
|
|
230
|
+
chain: LexicalScopeFact[],
|
|
231
|
+
scopeIndex: number,
|
|
232
|
+
): ServiceBindingReference {
|
|
233
|
+
return {
|
|
234
|
+
status: 'resolved_exact',
|
|
235
|
+
variableName: call.serviceVariableName,
|
|
236
|
+
bindingSourceFile: selected.fact.sourceFile,
|
|
237
|
+
bindingSiteStartOffset: selected.site.startOffset,
|
|
238
|
+
bindingSiteEndOffset: selected.site.endOffset,
|
|
239
|
+
resolutionStrategy: selected.site.flow === 'assignment'
|
|
240
|
+
? 'deterministic_reaching_assignment'
|
|
241
|
+
: selected.site.aliasSource
|
|
242
|
+
? 'lexical_alias_declaration'
|
|
243
|
+
: 'lexical_declaration',
|
|
244
|
+
lexicalScopeChain: chain,
|
|
245
|
+
bindingScopeIndex: scopeIndex,
|
|
246
|
+
scopeChainTotal: chain.length,
|
|
247
|
+
scopeChainShown: chain.length,
|
|
248
|
+
scopeChainOmitted: 0,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function selectedBindingReference(
|
|
253
|
+
call: OutboundCallFact,
|
|
254
|
+
chain: LexicalScopeFact[],
|
|
255
|
+
chosen: VisibleBinding<PreparedBinding>,
|
|
256
|
+
): ServiceBindingReference {
|
|
257
|
+
const selected = chosen.candidate?.value;
|
|
258
|
+
if (!selected) {
|
|
259
|
+
return rejectedReference(
|
|
260
|
+
call, chain, chosen.reason ?? 'binding_not_found',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (chosen.declarationSite?.declarationKind === 'var')
|
|
264
|
+
return rejectedReference(call, chain, 'unsupported_var_binding');
|
|
265
|
+
if (!selected.site.deterministic || !selected.aliasProven)
|
|
266
|
+
return rejectedReference(call, chain, 'unsupported_reaching_assignment');
|
|
267
|
+
return resolvedReference(call, selected, chain, chosen.scopeIndex ?? 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function bindingReference(
|
|
271
|
+
call: OutboundCallFact,
|
|
272
|
+
node: ts.CallExpression | undefined,
|
|
273
|
+
index: BindingLexicalIndex,
|
|
274
|
+
bindings: readonly PreparedBinding[],
|
|
275
|
+
): ServiceBindingReference {
|
|
276
|
+
if (!call.serviceVariableName) return emptyReference(call);
|
|
277
|
+
if (!node) return emptyReference(call, 'binding_flow_unsupported');
|
|
278
|
+
const chain = lexicalScopeChain(node, index.source);
|
|
279
|
+
if (chain.length > scopeChainCap)
|
|
280
|
+
return rejectedReference(call, chain, 'scope_chain_limit_exceeded');
|
|
281
|
+
const chosen = selectVisibleBinding(
|
|
282
|
+
index, bindingCandidates(bindings), call.serviceVariableName, node,
|
|
283
|
+
);
|
|
284
|
+
if (chosen.status === 'ambiguous')
|
|
285
|
+
return rejectedReference(
|
|
286
|
+
call, chain, 'binding_scope_ambiguous', 'ambiguous',
|
|
287
|
+
);
|
|
288
|
+
return selectedBindingReference(call, chain, chosen);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function exactCallOwner(
|
|
292
|
+
call: OutboundCallFact,
|
|
293
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
294
|
+
): { qualifiedName?: string; resolution: 'owned_exact' | 'ownerless_file_scope' } {
|
|
295
|
+
const start = call.callSiteStartOffset;
|
|
296
|
+
const end = call.callSiteEndOffset;
|
|
297
|
+
if (start === undefined || end === undefined)
|
|
298
|
+
return { resolution: 'ownerless_file_scope' };
|
|
299
|
+
const selected = selectCallOwner(
|
|
300
|
+
executableSymbolCandidates(symbols, call.sourceFile),
|
|
301
|
+
start,
|
|
302
|
+
end,
|
|
303
|
+
call.callType === 'async_subscribe',
|
|
304
|
+
);
|
|
305
|
+
if (selected.status === 'ambiguous')
|
|
306
|
+
throw new Error('invalid_prepared_repository_snapshot:outbound_owner_ambiguous');
|
|
307
|
+
if (call.callType === 'async_subscribe' && selected.status !== 'resolved')
|
|
308
|
+
throw new Error('invalid_prepared_repository_snapshot:subscription_owner_missing');
|
|
309
|
+
return selected.owner
|
|
310
|
+
? { qualifiedName: selected.owner.qualifiedName, resolution: 'owned_exact' }
|
|
311
|
+
: { resolution: 'ownerless_file_scope' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function reconcileBindingAndCallIdentity(
|
|
315
|
+
source: ts.SourceFile,
|
|
316
|
+
bindingFacts: readonly ServiceBindingFact[],
|
|
317
|
+
callFacts: readonly OutboundCallFact[],
|
|
318
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
319
|
+
): { bindings: ServiceBindingFact[]; calls: OutboundCallFact[] } {
|
|
320
|
+
const lexicalIndex = createBindingLexicalIndex(source);
|
|
321
|
+
const bindings = prepareBindings(lexicalIndex, bindingFacts, symbols);
|
|
322
|
+
reconcileAliases(lexicalIndex, bindings);
|
|
323
|
+
assertUniqueSites(bindings);
|
|
324
|
+
const nodes = callNodeMap(source);
|
|
325
|
+
const calls = callFacts.map((call) => {
|
|
326
|
+
const key = `${call.callSiteStartOffset}:${call.callSiteEndOffset}`;
|
|
327
|
+
const reference = bindingReference(
|
|
328
|
+
call, nodes.get(key), lexicalIndex, bindings,
|
|
329
|
+
);
|
|
330
|
+
const owner = exactCallOwner(call, symbols);
|
|
331
|
+
return {
|
|
332
|
+
...call,
|
|
333
|
+
sourceSymbolQualifiedName: owner.qualifiedName,
|
|
334
|
+
serviceBindingReference: reference,
|
|
335
|
+
evidence: {
|
|
336
|
+
...(call.evidence ?? {}),
|
|
337
|
+
sourceOwnerResolution: owner.resolution,
|
|
338
|
+
serviceBindingReference: reference,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
return { bindings: bindings.map((binding) => binding.fact), calls };
|
|
343
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type {
|
|
3
|
+
ExecutableSymbolFact,
|
|
4
|
+
OutboundCallFact,
|
|
5
|
+
ServiceBindingFact,
|
|
6
|
+
SymbolCallFact,
|
|
7
|
+
} from '../types.js';
|
|
8
|
+
import {
|
|
9
|
+
executableSymbolCandidates,
|
|
10
|
+
selectCallOwner,
|
|
11
|
+
} from './004-fact-identity.js';
|
|
12
|
+
import { reconcileEventSubscriptions } from './005-event-subscription-facts.js';
|
|
13
|
+
import { reconcileBindingAndCallIdentity } from './006-binding-identity.js';
|
|
14
|
+
import type { ClassifiedOutboundCall } from './outbound-call-parser.js';
|
|
15
|
+
|
|
16
|
+
export interface ReconciledSourceFacts {
|
|
17
|
+
bindings: ServiceBindingFact[];
|
|
18
|
+
outboundCalls: OutboundCallFact[];
|
|
19
|
+
symbols: ExecutableSymbolFact[];
|
|
20
|
+
symbolCalls: SymbolCallFact[];
|
|
21
|
+
classifications: ClassifiedOutboundCall[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function callKey(call: Pick<
|
|
25
|
+
OutboundCallFact,
|
|
26
|
+
'callType' | 'callSiteStartOffset' | 'callSiteEndOffset'
|
|
27
|
+
>): string {
|
|
28
|
+
return `${call.callType}:${call.callSiteStartOffset}:${call.callSiteEndOffset}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mergedOutboundCalls(
|
|
32
|
+
existing: readonly OutboundCallFact[],
|
|
33
|
+
classifications: readonly ClassifiedOutboundCall[],
|
|
34
|
+
): OutboundCallFact[] {
|
|
35
|
+
const classifiedKeys = new Set(
|
|
36
|
+
classifications.map((item) => callKey(item.fact)),
|
|
37
|
+
);
|
|
38
|
+
return [
|
|
39
|
+
...classifications.map((item) => item.fact),
|
|
40
|
+
...existing.filter((call) => !classifiedKeys.has(callKey(call))),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function exactSymbolCallOwner(
|
|
45
|
+
call: SymbolCallFact,
|
|
46
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
47
|
+
): ExecutableSymbolFact | undefined {
|
|
48
|
+
const start = call.callSiteStartOffset;
|
|
49
|
+
const end = call.callSiteEndOffset;
|
|
50
|
+
if (start === undefined || end === undefined) return undefined;
|
|
51
|
+
const selected = selectCallOwner(
|
|
52
|
+
executableSymbolCandidates(symbols, call.sourceFile),
|
|
53
|
+
start,
|
|
54
|
+
end,
|
|
55
|
+
call.callRole === 'event_subscribe_handler',
|
|
56
|
+
);
|
|
57
|
+
if (selected.status === 'ambiguous')
|
|
58
|
+
throw new Error('invalid_prepared_repository_snapshot:symbol_call_owner_ambiguous');
|
|
59
|
+
if (call.callRole === 'event_subscribe_handler' && !selected.owner)
|
|
60
|
+
throw new Error('invalid_prepared_repository_snapshot:handler_owner_missing');
|
|
61
|
+
return selected.owner;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function reconcileSymbolCallOwners(
|
|
65
|
+
calls: readonly SymbolCallFact[],
|
|
66
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
67
|
+
): SymbolCallFact[] {
|
|
68
|
+
return calls.flatMap((call) => {
|
|
69
|
+
const owner = exactSymbolCallOwner(call, symbols);
|
|
70
|
+
if (!owner) return [];
|
|
71
|
+
return [{
|
|
72
|
+
...call,
|
|
73
|
+
callerQualifiedName: owner.qualifiedName,
|
|
74
|
+
evidence: {
|
|
75
|
+
...call.evidence,
|
|
76
|
+
caller: owner.qualifiedName,
|
|
77
|
+
callerResolution: 'full_span_containment',
|
|
78
|
+
},
|
|
79
|
+
}];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function reconcileSourceFacts(
|
|
84
|
+
source: ts.SourceFile,
|
|
85
|
+
classifications: readonly ClassifiedOutboundCall[],
|
|
86
|
+
bindings: readonly ServiceBindingFact[],
|
|
87
|
+
outboundCalls: readonly OutboundCallFact[],
|
|
88
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
89
|
+
symbolCalls: readonly SymbolCallFact[],
|
|
90
|
+
): ReconciledSourceFacts {
|
|
91
|
+
const events = reconcileEventSubscriptions(
|
|
92
|
+
source, classifications, symbols, symbolCalls,
|
|
93
|
+
);
|
|
94
|
+
const allCalls = mergedOutboundCalls(outboundCalls, events.classifications);
|
|
95
|
+
const identities = reconcileBindingAndCallIdentity(
|
|
96
|
+
source, bindings, allCalls, events.symbols,
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
bindings: identities.bindings,
|
|
100
|
+
outboundCalls: identities.calls,
|
|
101
|
+
symbols: events.symbols,
|
|
102
|
+
symbolCalls: reconcileSymbolCallOwners(events.calls, events.symbols),
|
|
103
|
+
classifications: events.classifications,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExecutableSymbolFact,
|
|
3
|
+
PackageFacts,
|
|
4
|
+
} from '../types.js';
|
|
5
|
+
import { normalizePath } from '../utils/path-utils.js';
|
|
6
|
+
import type { PackageEntrypointManifest } from './package-json-parser.js';
|
|
7
|
+
import {
|
|
8
|
+
analyzePackagePublicSurface,
|
|
9
|
+
type PackagePublicSurfaceAnalysis,
|
|
10
|
+
type SymbolPublicSurfaceEvidence,
|
|
11
|
+
} from './003-package-public-surface.js';
|
|
12
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
13
|
+
|
|
14
|
+
function symbolIdentity(
|
|
15
|
+
symbol: Pick<
|
|
16
|
+
ExecutableSymbolFact,
|
|
17
|
+
'sourceFile' | 'kind' | 'qualifiedName' | 'startOffset' | 'endOffset'
|
|
18
|
+
>,
|
|
19
|
+
): string {
|
|
20
|
+
return [
|
|
21
|
+
symbol.sourceFile,
|
|
22
|
+
symbol.kind,
|
|
23
|
+
symbol.qualifiedName,
|
|
24
|
+
symbol.startOffset,
|
|
25
|
+
symbol.endOffset,
|
|
26
|
+
].join('\0');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function exposureBySymbol(
|
|
30
|
+
analysis: PackagePublicSurfaceAnalysis,
|
|
31
|
+
): Map<string, SymbolPublicSurfaceEvidence> {
|
|
32
|
+
return new Map(analysis.symbols.map((item) => [
|
|
33
|
+
symbolIdentity({
|
|
34
|
+
sourceFile: item.target.sourceFile,
|
|
35
|
+
kind: item.target.kind,
|
|
36
|
+
qualifiedName: item.target.qualifiedName,
|
|
37
|
+
startOffset: item.target.startOffset,
|
|
38
|
+
endOffset: item.target.endOffset,
|
|
39
|
+
}),
|
|
40
|
+
item,
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function analyzeRepositoryPackageSurface(
|
|
45
|
+
facts: PackageFacts,
|
|
46
|
+
manifest: PackageEntrypointManifest,
|
|
47
|
+
sources: RepositorySourceContext,
|
|
48
|
+
): PackagePublicSurfaceAnalysis {
|
|
49
|
+
const modules = sources.entries()
|
|
50
|
+
.filter((snapshot) => /\.[jt]s$/.test(snapshot.filePath))
|
|
51
|
+
.map((snapshot) => ({
|
|
52
|
+
sourceFile: normalizePath(snapshot.filePath),
|
|
53
|
+
source: snapshot.sourceFile(),
|
|
54
|
+
}));
|
|
55
|
+
return analyzePackagePublicSurface(facts.packageName, manifest, modules);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mergePackageSymbolEvidence(
|
|
59
|
+
symbols: readonly ExecutableSymbolFact[],
|
|
60
|
+
analysis: PackagePublicSurfaceAnalysis,
|
|
61
|
+
): ExecutableSymbolFact[] {
|
|
62
|
+
const exposures = exposureBySymbol(analysis);
|
|
63
|
+
return symbols.map((symbol) => {
|
|
64
|
+
const exposure = exposures.get(symbolIdentity(symbol));
|
|
65
|
+
if (!exposure) return symbol;
|
|
66
|
+
return {
|
|
67
|
+
...symbol,
|
|
68
|
+
importExportEvidence: {
|
|
69
|
+
...(symbol.importExportEvidence ?? {}),
|
|
70
|
+
packagePublicSurface: {
|
|
71
|
+
schema: analysis.surface.schema,
|
|
72
|
+
recordCap: analysis.surface.recordCap,
|
|
73
|
+
bodyEligibility: exposure.target.bodyEligibility,
|
|
74
|
+
exposures: exposure.exposures,
|
|
75
|
+
exposureTotal: exposure.exposureTotal,
|
|
76
|
+
shownExposureCount: exposure.shownExposureCount,
|
|
77
|
+
omittedExposureCount: exposure.omittedExposureCount,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
}
|