@saptools/service-flow 0.1.69 → 0.1.72
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 +17 -0
- package/README.md +9 -5
- package/TECHNICAL-NOTE.md +17 -0
- package/dist/{chunk-3N3B5KHV.js → chunk-Z6D433R5.js} +8766 -6546
- package/dist/chunk-Z6D433R5.js.map +1 -0
- package/dist/cli.js +703 -160
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +53 -14
- package/dist/index.js +2 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +371 -0
- package/src/cli/{002-doctor-lifecycle.ts → doctor-lifecycle.ts} +1 -1
- package/src/cli/{001-doctor-projection.ts → doctor-projection.ts} +1 -1
- package/src/cli/doctor.ts +9 -11
- package/src/cli.ts +6 -6
- package/src/db/{009-binding-fact-semantics.ts → binding-fact-semantics.ts} +2 -2
- package/src/db/{000-call-fact-repository.ts → call-fact-repository.ts} +12 -9
- package/src/db/{003-current-fact-semantics.ts → current-fact-semantics.ts} +8 -8
- package/src/db/event-fact-semantics.ts +347 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/{002-fact-json-inventory.ts → fact-json-inventory.ts} +16 -0
- package/src/db/{001-fact-lifecycle.ts → fact-lifecycle.ts} +3 -3
- package/src/db/migrations.ts +12 -1
- package/src/db/{007-package-fact-semantics.ts → package-fact-semantics.ts} +6 -6
- package/src/db/{010-package-symbol-surface-semantics.ts → package-symbol-surface-semantics.ts} +2 -2
- package/src/db/{004-package-target-invalidation.ts → package-target-invalidation.ts} +80 -1
- package/src/db/{008-relative-fact-semantics.ts → relative-fact-semantics.ts} +3 -3
- package/src/db/{006-relative-symbol-resolution.ts → relative-symbol-resolution.ts} +1 -1
- package/src/db/repositories.ts +31 -5
- package/src/db/{005-schema-structure.ts → schema-structure.ts} +41 -1
- package/src/db/schema.ts +6 -2
- package/src/db/{011-symbol-call-semantics.ts → symbol-call-semantics.ts} +1 -1
- package/src/index.ts +3 -3
- package/src/indexer/repository-indexer.ts +50 -11
- package/src/indexer/workspace-indexer.ts +3 -3
- package/src/linker/{007-call-edge-insertion.ts → call-edge-insertion.ts} +2 -2
- package/src/linker/{002-call-evidence.ts → call-evidence.ts} +2 -2
- package/src/linker/cross-repo-linker.ts +30 -8
- package/src/linker/dynamic-edge-resolver.ts +1 -1
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +161 -0
- package/src/linker/{004-event-subscription-handler-linker.ts → event-subscription-handler-linker.ts} +124 -20
- package/src/linker/{006-event-template-link.ts → event-template-link.ts} +40 -6
- package/src/linker/external-http-target.ts +1 -1
- package/src/linker/helper-package-linker.ts +1 -1
- package/src/linker/{000-implementation-candidates.ts → implementation-candidates.ts} +1 -1
- package/src/linker/{001-implementation-evidence-projection.ts → implementation-evidence-projection.ts} +1 -1
- package/src/linker/odata-path-normalizer.ts +1 -1
- package/src/linker/{005-odata-path-structure.ts → odata-path-structure.ts} +1 -1
- package/src/linker/package-event-constant-resolver.ts +298 -0
- package/src/linker/{003-package-import-symbol-resolver.ts → package-import-symbol-resolver.ts} +3 -3
- package/src/linker/service-resolver.ts +2 -2
- package/src/output/{001-compact-json-output.ts → compact-json-output.ts} +1 -1
- package/src/output/table-output.ts +13 -1
- package/src/parsers/{006-binding-identity.ts → binding-identity.ts} +3 -3
- package/src/parsers/{011-binding-lexical-scope.ts → binding-lexical-scope.ts} +1 -1
- package/src/parsers/{021-binding-visibility.ts → binding-visibility.ts} +1 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +327 -0
- package/src/parsers/event-call-analysis.ts +242 -0
- package/src/parsers/event-environment-reference.ts +231 -0
- package/src/parsers/event-loop-registration.ts +132 -0
- package/src/parsers/event-name-import-resolution.ts +243 -0
- package/src/parsers/event-receiver-analysis.ts +404 -0
- package/src/parsers/{005-event-subscription-facts.ts → event-subscription-facts.ts} +6 -2
- package/src/parsers/generated-constants-parser.ts +80 -14
- package/src/parsers/{016-local-symbol-reference.ts → local-symbol-reference.ts} +2 -2
- package/src/parsers/operation-path-analysis.ts +1 -1
- package/src/parsers/{023-outbound-call-classifier.ts → outbound-call-classifier.ts} +30 -127
- package/src/parsers/outbound-call-parser.ts +15 -3
- package/src/parsers/{022-outbound-expression-analysis.ts → outbound-expression-analysis.ts} +4 -4
- package/src/parsers/{018-package-commonjs-syntax.ts → package-commonjs-syntax.ts} +1 -1
- package/src/parsers/{012-package-fact-contract.ts → package-fact-contract.ts} +3 -3
- package/src/parsers/{010-package-public-surface-analysis.ts → package-public-surface-analysis.ts} +2 -2
- package/src/parsers/{003-package-public-surface.ts → package-public-surface.ts} +3 -3
- package/src/parsers/{008-package-surface-publication.ts → package-surface-publication.ts} +1 -1
- package/src/parsers/{001-query-entity-resolution.ts → query-entity-resolution.ts} +1 -1
- package/src/parsers/{015-service-binding-collector.ts → service-binding-collector.ts} +3 -3
- package/src/parsers/{014-service-binding-helper-flow.ts → service-binding-helper-flow.ts} +2 -2
- package/src/parsers/service-binding-parser-helpers.ts +1 -1
- package/src/parsers/service-binding-parser.ts +2 -2
- package/src/parsers/{007-source-fact-reconciliation.ts → source-fact-reconciliation.ts} +3 -3
- package/src/parsers/{020-stable-local-value.ts → stable-local-value.ts} +32 -11
- package/src/parsers/string-constant-lookups.ts +358 -0
- package/src/parsers/{009-symbol-call-facts.ts → symbol-call-facts.ts} +3 -3
- package/src/parsers/{017-symbol-derived-contexts.ts → symbol-derived-contexts.ts} +2 -2
- package/src/parsers/symbol-parser.ts +6 -6
- package/src/trace/{021-compact-decision-normalization.ts → compact-decision-normalization.ts} +2 -2
- package/src/trace/{020-compact-field-projection.ts → compact-field-projection.ts} +3 -3
- package/src/trace/{024-compact-observation-decision.ts → compact-observation-decision.ts} +3 -3
- package/src/trace/{016-compact-projector.ts → compact-projector.ts} +4 -4
- package/src/trace/{018-compact-trace.ts → compact-trace.ts} +3 -3
- package/src/trace/{006-contextual-projection.ts → contextual-projection.ts} +1 -1
- package/src/trace/{008-contextual-runtime-state.ts → contextual-runtime-state.ts} +1 -1
- package/src/trace/{001-dynamic-identity.ts → dynamic-identity.ts} +2 -2
- package/src/trace/{003-dynamic-references.ts → dynamic-references.ts} +2 -2
- package/src/trace/dynamic-targets.ts +6 -6
- package/src/trace/{030-event-runtime-resolution.ts → event-runtime-resolution.ts} +26 -5
- package/src/trace/event-shape-candidate-trace.ts +172 -0
- package/src/trace/{011-event-subscriber-traversal.ts → event-subscriber-traversal.ts} +20 -8
- package/src/trace/evidence.ts +9 -2
- package/src/trace/implementation-hints.ts +2 -2
- package/src/trace/{005-implementation-selection.ts → implementation-selection.ts} +2 -2
- package/src/trace/{031-local-call-expansion.ts → local-call-expansion.ts} +3 -3
- package/src/trace/{009-selected-handler-provenance.ts → selected-handler-provenance.ts} +1 -1
- package/src/trace/selectors.ts +1 -1
- package/src/trace/{017-trace-context.ts → trace-context.ts} +2 -2
- package/src/trace/{015-trace-edge-recorder.ts → trace-edge-recorder.ts} +2 -2
- package/src/trace/{019-trace-edge-semantics.ts → trace-edge-semantics.ts} +3 -3
- package/src/trace/trace-engine.ts +10 -10
- package/src/trace/{022-trace-fact-preflight.ts → trace-fact-preflight.ts} +2 -2
- package/src/trace/{025-trace-implementation-scope.ts → trace-implementation-scope.ts} +1 -1
- package/src/trace/{028-trace-operation-execution.ts → trace-operation-execution.ts} +10 -10
- package/src/trace/{013-trace-root-scopes.ts → trace-root-scopes.ts} +3 -3
- package/src/trace/{027-trace-scope-execution.ts → trace-scope-execution.ts} +35 -43
- package/src/trace/{029-trace-start-implementation.ts → trace-start-implementation.ts} +7 -7
- package/src/trace/{026-trace-start-scope.ts → trace-start-scope.ts} +5 -5
- package/src/trace/{010-traversal-scope.ts → traversal-scope.ts} +1 -1
- package/src/types.ts +17 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-3N3B5KHV.js.map +0 -1
- /package/src/cli/{000-clean.ts → clean.ts} +0 -0
- /package/src/cli/{003-doctor-package-resolution.ts → doctor-package-resolution.ts} +0 -0
- /package/src/cli/{001-index-summary.ts → index-summary.ts} +0 -0
- /package/src/db/{014-binding-helper-provenance.ts → binding-helper-provenance.ts} +0 -0
- /package/src/db/{012-binding-reference-proof.ts → binding-reference-proof.ts} +0 -0
- /package/src/db/{013-index-publication-failure.ts → index-publication-failure.ts} +0 -0
- /package/src/output/{000-stdout-policy.ts → stdout-policy.ts} +0 -0
- /package/src/parsers/{019-binding-assignment-targets.ts → binding-assignment-targets.ts} +0 -0
- /package/src/parsers/{000-direct-query-execution.ts → direct-query-execution.ts} +0 -0
- /package/src/parsers/{013-executable-body-eligibility.ts → executable-body-eligibility.ts} +0 -0
- /package/src/parsers/{004-fact-identity.ts → fact-identity.ts} +0 -0
- /package/src/parsers/{002-symbol-import-bindings.ts → symbol-import-bindings.ts} +0 -0
- /package/src/trace/{014-compact-contract.ts → compact-contract.ts} +0 -0
- /package/src/trace/{004-dynamic-candidate-sources.ts → dynamic-candidate-sources.ts} +0 -0
- /package/src/trace/{000-dynamic-target-types.ts → dynamic-target-types.ts} +0 -0
- /package/src/trace/{007-implementation-start-diagnostic.ts → implementation-start-diagnostic.ts} +0 -0
- /package/src/trace/{023-nested-event-scopes.ts → nested-event-scopes.ts} +0 -0
- /package/src/trace/{002-trace-diagnostics.ts → trace-diagnostics.ts} +0 -0
- /package/src/trace/{012-trace-graph-lookups.ts → trace-graph-lookups.ts} +0 -0
- /package/src/utils/{000-bounded-projection.ts → bounded-projection.ts} +0 -0
- /package/src/utils/{001-placeholders.ts → placeholders.ts} +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { LexicalScopeFact } from '../types.js';
|
|
3
|
+
import {
|
|
4
|
+
createBindingLexicalIndex,
|
|
5
|
+
declarationAt,
|
|
6
|
+
lexicalScopeChain,
|
|
7
|
+
sameScope,
|
|
8
|
+
type BindingLexicalIndex,
|
|
9
|
+
type BindingLexicalSite,
|
|
10
|
+
} from './binding-lexical-scope.js';
|
|
11
|
+
import {
|
|
12
|
+
collectSymbolImportBindings,
|
|
13
|
+
lexicalIdentifierDeclaration,
|
|
14
|
+
type SymbolImportBinding,
|
|
15
|
+
} from './symbol-import-bindings.js';
|
|
16
|
+
import { stableLocalValueReference } from './stable-local-value.js';
|
|
17
|
+
|
|
18
|
+
export type EventReceiverUnresolvedReason =
|
|
19
|
+
| 'event_receiver_unproven_binding'
|
|
20
|
+
| 'event_receiver_unproven_propagation'
|
|
21
|
+
| 'event_receiver_not_cap_client';
|
|
22
|
+
|
|
23
|
+
export interface EventReceiverEvidenceSite {
|
|
24
|
+
startOffset: number;
|
|
25
|
+
endOffset: number;
|
|
26
|
+
flow: BindingLexicalSite['flow'];
|
|
27
|
+
connect: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EventReceiverProof {
|
|
31
|
+
effectiveReceiver: string;
|
|
32
|
+
receiver?: string;
|
|
33
|
+
rootReceiver?: string;
|
|
34
|
+
receiverClassification: 'cap_evidence' | 'name_fallback' | 'unproven';
|
|
35
|
+
receiverProof: string;
|
|
36
|
+
unresolvedReason?: EventReceiverUnresolvedReason;
|
|
37
|
+
consideredBindingSites: EventReceiverEvidenceSite[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EventReceiverIndex {
|
|
41
|
+
source: ts.SourceFile;
|
|
42
|
+
lexical: BindingLexicalIndex;
|
|
43
|
+
imports: SymbolImportBinding[];
|
|
44
|
+
compatibilityNames: Set<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const eventReceiverNames = new Set([
|
|
48
|
+
'cds', 'srv', 'service', 'serviceClient', 'messaging', 'messageClient',
|
|
49
|
+
'eventClient',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
53
|
+
if (ts.isAwaitExpression(expression)
|
|
54
|
+
|| ts.isParenthesizedExpression(expression)
|
|
55
|
+
|| ts.isAsExpression(expression)
|
|
56
|
+
|| ts.isSatisfiesExpression(expression)
|
|
57
|
+
|| ts.isTypeAssertionExpression(expression)
|
|
58
|
+
|| ts.isNonNullExpression(expression))
|
|
59
|
+
return unwrapExpression(expression.expression);
|
|
60
|
+
return expression;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function capConnectCall(
|
|
64
|
+
expression: ts.Expression,
|
|
65
|
+
): ts.CallExpression | undefined {
|
|
66
|
+
const value = unwrapExpression(expression);
|
|
67
|
+
if (!ts.isCallExpression(value)
|
|
68
|
+
|| !ts.isPropertyAccessExpression(value.expression)) return undefined;
|
|
69
|
+
const callee = value.expression;
|
|
70
|
+
if (callee.name.text === 'catch'
|
|
71
|
+
&& ts.isCallExpression(callee.expression))
|
|
72
|
+
return capConnectCall(callee.expression);
|
|
73
|
+
if (!['to', 'messaging'].includes(callee.name.text)
|
|
74
|
+
|| !ts.isPropertyAccessExpression(callee.expression)
|
|
75
|
+
|| callee.expression.name.text !== 'connect') return undefined;
|
|
76
|
+
return ts.isIdentifier(callee.expression.expression) ? value : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function importedCdsRoot(
|
|
80
|
+
call: ts.CallExpression,
|
|
81
|
+
bindings: readonly SymbolImportBinding[],
|
|
82
|
+
): boolean {
|
|
83
|
+
const callee = call.expression;
|
|
84
|
+
if (!ts.isPropertyAccessExpression(callee)
|
|
85
|
+
|| !ts.isPropertyAccessExpression(callee.expression)
|
|
86
|
+
|| !ts.isIdentifier(callee.expression.expression)) return false;
|
|
87
|
+
const root = callee.expression.expression;
|
|
88
|
+
const declaration = lexicalIdentifierDeclaration(root);
|
|
89
|
+
if (!declaration) return root.text === 'cds';
|
|
90
|
+
const start = declaration.getStart(root.getSourceFile());
|
|
91
|
+
const end = declaration.getEnd();
|
|
92
|
+
const matches = bindings.filter((binding) =>
|
|
93
|
+
binding.localName === root.text
|
|
94
|
+
&& binding.bindingSiteStartOffset === start
|
|
95
|
+
&& binding.bindingSiteEndOffset === end);
|
|
96
|
+
return matches.length === 1
|
|
97
|
+
&& matches[0]?.rawModuleSpecifier === '@sap/cds';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function importedCdsIdentifier(
|
|
101
|
+
identifier: ts.Identifier,
|
|
102
|
+
index: EventReceiverIndex,
|
|
103
|
+
): boolean {
|
|
104
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
105
|
+
if (!declaration) return false;
|
|
106
|
+
const start = declaration.getStart(index.source);
|
|
107
|
+
const end = declaration.getEnd();
|
|
108
|
+
const matches = index.imports.filter((binding) =>
|
|
109
|
+
binding.localName === identifier.text
|
|
110
|
+
&& binding.bindingSiteStartOffset === start
|
|
111
|
+
&& binding.bindingSiteEndOffset === end);
|
|
112
|
+
return matches.length === 1
|
|
113
|
+
&& matches[0]?.rawModuleSpecifier === '@sap/cds';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isCapConnect(
|
|
117
|
+
expression: ts.Expression | undefined,
|
|
118
|
+
bindings: readonly SymbolImportBinding[],
|
|
119
|
+
): boolean {
|
|
120
|
+
if (!expression) return false;
|
|
121
|
+
const call = capConnectCall(expression);
|
|
122
|
+
return Boolean(call && importedCdsRoot(call, bindings));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function propertyName(name: ts.PropertyName): string | undefined {
|
|
126
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
127
|
+
|| ts.isNumericLiteral(name) ? name.text : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function objectSource(
|
|
131
|
+
expression: ts.Expression | undefined,
|
|
132
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
133
|
+
if (!expression) return undefined;
|
|
134
|
+
const value = unwrapExpression(expression);
|
|
135
|
+
if (ts.isObjectLiteralExpression(value)) return value;
|
|
136
|
+
if (!ts.isIdentifier(value)) return undefined;
|
|
137
|
+
const binding = resolveLocalInitializer(value);
|
|
138
|
+
return binding ? objectSource(binding) : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveLocalInitializer(
|
|
142
|
+
identifier: ts.Identifier,
|
|
143
|
+
): ts.Expression | undefined {
|
|
144
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
145
|
+
if (!declaration || !ts.isVariableDeclaration(declaration.parent)
|
|
146
|
+
|| !stableLocalValueReference(identifier.getSourceFile(), declaration))
|
|
147
|
+
return undefined;
|
|
148
|
+
return declaration.parent.initializer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function objectMember(
|
|
152
|
+
object: ts.ObjectLiteralExpression,
|
|
153
|
+
name: string,
|
|
154
|
+
): ts.Expression | undefined {
|
|
155
|
+
for (const property of object.properties) {
|
|
156
|
+
if (ts.isPropertyAssignment(property)
|
|
157
|
+
&& propertyName(property.name) === name) return property.initializer;
|
|
158
|
+
if (ts.isShorthandPropertyAssignment(property)
|
|
159
|
+
&& property.name.text === name) return property.name;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function declarationBindingKey(
|
|
165
|
+
declaration: ts.VariableDeclaration,
|
|
166
|
+
variableName: string,
|
|
167
|
+
): string | undefined {
|
|
168
|
+
if (!ts.isObjectBindingPattern(declaration.name)) return undefined;
|
|
169
|
+
const match = declaration.name.elements.find((element) =>
|
|
170
|
+
ts.isIdentifier(element.name) && element.name.text === variableName);
|
|
171
|
+
if (!match) return undefined;
|
|
172
|
+
return match.propertyName
|
|
173
|
+
? propertyName(match.propertyName) : variableName;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assignmentBindingKey(
|
|
177
|
+
node: ts.BinaryExpression,
|
|
178
|
+
variableName: string,
|
|
179
|
+
): string | undefined {
|
|
180
|
+
const left = unwrapExpression(node.left);
|
|
181
|
+
if (!ts.isObjectLiteralExpression(left)) return undefined;
|
|
182
|
+
for (const property of left.properties) {
|
|
183
|
+
if (ts.isShorthandPropertyAssignment(property)
|
|
184
|
+
&& property.name.text === variableName) return variableName;
|
|
185
|
+
if (ts.isPropertyAssignment(property)
|
|
186
|
+
&& ts.isIdentifier(property.initializer)
|
|
187
|
+
&& property.initializer.text === variableName)
|
|
188
|
+
return propertyName(property.name);
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function siteExpression(
|
|
194
|
+
site: BindingLexicalSite,
|
|
195
|
+
variableName: string,
|
|
196
|
+
): ts.Expression | undefined {
|
|
197
|
+
if (ts.isVariableDeclaration(site.node)) {
|
|
198
|
+
if (!ts.isIdentifier(site.node.name)) {
|
|
199
|
+
const key = declarationBindingKey(site.node, variableName);
|
|
200
|
+
const object = objectSource(site.node.initializer);
|
|
201
|
+
return key && object ? objectMember(object, key) : undefined;
|
|
202
|
+
}
|
|
203
|
+
if (site.node.name.text !== variableName) return undefined;
|
|
204
|
+
return site.node.initializer;
|
|
205
|
+
}
|
|
206
|
+
if (!ts.isBinaryExpression(site.node)
|
|
207
|
+
|| site.node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
208
|
+
return undefined;
|
|
209
|
+
const key = assignmentBindingKey(site.node, variableName);
|
|
210
|
+
const object = objectSource(site.node.right);
|
|
211
|
+
return key && object ? objectMember(object, key) : site.node.right;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sameExecutionScope(
|
|
215
|
+
left: LexicalScopeFact,
|
|
216
|
+
right: LexicalScopeFact,
|
|
217
|
+
): boolean {
|
|
218
|
+
return sameScope(left, right);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function unsupportedBranch(node: ts.Node): boolean {
|
|
222
|
+
let current: ts.Node | undefined = node.parent;
|
|
223
|
+
while (current && !ts.isSourceFile(current) && !ts.isFunctionLike(current)) {
|
|
224
|
+
if (ts.isIfStatement(current) || ts.isConditionalExpression(current)
|
|
225
|
+
|| ts.isSwitchStatement(current)
|
|
226
|
+
|| ts.isIterationStatement(current, false)) return true;
|
|
227
|
+
current = current.parent;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function reachingSites(
|
|
233
|
+
index: EventReceiverIndex,
|
|
234
|
+
declaration: BindingLexicalSite,
|
|
235
|
+
useStart: number,
|
|
236
|
+
): BindingLexicalSite[] {
|
|
237
|
+
return index.lexical.sites.filter((site) =>
|
|
238
|
+
site.declarationKey === declaration.declarationKey
|
|
239
|
+
&& site.startOffset < useStart
|
|
240
|
+
&& site.flow !== 'shadow'
|
|
241
|
+
&& sameExecutionScope(site.executionScope, declaration.executionScope));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function evidenceSites(
|
|
245
|
+
sites: readonly BindingLexicalSite[],
|
|
246
|
+
variableName: string,
|
|
247
|
+
bindings: readonly SymbolImportBinding[],
|
|
248
|
+
): EventReceiverEvidenceSite[] {
|
|
249
|
+
return sites.slice(0, 8).map((site) => ({
|
|
250
|
+
startOffset: site.startOffset,
|
|
251
|
+
endOffset: site.endOffset,
|
|
252
|
+
flow: site.flow,
|
|
253
|
+
connect: isCapConnect(siteExpression(site, variableName), bindings),
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function lexicalProof(
|
|
258
|
+
identifier: ts.Identifier,
|
|
259
|
+
use: ts.Node,
|
|
260
|
+
index: EventReceiverIndex,
|
|
261
|
+
): Omit<EventReceiverProof, 'effectiveReceiver' | 'receiver'
|
|
262
|
+
| 'rootReceiver'> | undefined {
|
|
263
|
+
const useStart = use.getStart(index.source);
|
|
264
|
+
const selected = declarationAt(
|
|
265
|
+
index.lexical.sites, identifier.text, useStart,
|
|
266
|
+
lexicalScopeChain(use, index.source),
|
|
267
|
+
);
|
|
268
|
+
const declaration = selected.site;
|
|
269
|
+
if (!declaration || selected.after || selected.ambiguous) return undefined;
|
|
270
|
+
if (declaration.declarationKind === 'parameter')
|
|
271
|
+
return unproven('event_receiver_unproven_propagation', 'parameter_flow');
|
|
272
|
+
const sites = reachingSites(index, declaration, useStart);
|
|
273
|
+
const expressions = sites.flatMap((site) => {
|
|
274
|
+
const value = siteExpression(site, identifier.text);
|
|
275
|
+
return value ? [value] : [];
|
|
276
|
+
});
|
|
277
|
+
const considered = evidenceSites(sites, identifier.text, index.imports);
|
|
278
|
+
if (sites.some((site) => unsupportedBranch(site.node)))
|
|
279
|
+
return unproven(
|
|
280
|
+
'event_receiver_unproven_binding', 'branch_dependent_assignment',
|
|
281
|
+
considered,
|
|
282
|
+
);
|
|
283
|
+
if (expressions.length > 0
|
|
284
|
+
&& expressions.every((value) => isCapConnect(value, index.imports)))
|
|
285
|
+
return {
|
|
286
|
+
receiverClassification: 'cap_evidence',
|
|
287
|
+
receiverProof: 'lexical_connect_assignment',
|
|
288
|
+
consideredBindingSites: considered,
|
|
289
|
+
};
|
|
290
|
+
if (expressions.some(Boolean)
|
|
291
|
+
&& expressions.every((value) => !isCapConnect(value, index.imports)))
|
|
292
|
+
return unproven(
|
|
293
|
+
'event_receiver_not_cap_client', 'non_connect_binding', considered,
|
|
294
|
+
);
|
|
295
|
+
return unproven(
|
|
296
|
+
'event_receiver_unproven_binding', 'mixed_or_missing_assignment',
|
|
297
|
+
considered,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function unproven(
|
|
302
|
+
reason: EventReceiverUnresolvedReason,
|
|
303
|
+
proof: string,
|
|
304
|
+
consideredBindingSites: EventReceiverEvidenceSite[] = [],
|
|
305
|
+
): Omit<EventReceiverProof, 'effectiveReceiver' | 'receiver'
|
|
306
|
+
| 'rootReceiver'> {
|
|
307
|
+
return {
|
|
308
|
+
receiverClassification: 'unproven',
|
|
309
|
+
receiverProof: proof,
|
|
310
|
+
unresolvedReason: reason,
|
|
311
|
+
consideredBindingSites,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function compatibilityFallback(
|
|
316
|
+
name: string,
|
|
317
|
+
index: EventReceiverIndex,
|
|
318
|
+
): Omit<EventReceiverProof, 'effectiveReceiver' | 'receiver'
|
|
319
|
+
| 'rootReceiver'> | undefined {
|
|
320
|
+
return eventReceiverNames.has(name) || index.compatibilityNames.has(name)
|
|
321
|
+
? {
|
|
322
|
+
receiverClassification: 'name_fallback',
|
|
323
|
+
receiverProof: 'compatibility_name_fallback',
|
|
324
|
+
consideredBindingSites: [],
|
|
325
|
+
}
|
|
326
|
+
: undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function identifierProof(
|
|
330
|
+
identifier: ts.Identifier,
|
|
331
|
+
use: ts.Node,
|
|
332
|
+
index: EventReceiverIndex,
|
|
333
|
+
): Omit<EventReceiverProof, 'effectiveReceiver' | 'receiver'
|
|
334
|
+
| 'rootReceiver'> {
|
|
335
|
+
if (importedCdsIdentifier(identifier, index)) return {
|
|
336
|
+
receiverClassification: 'cap_evidence',
|
|
337
|
+
receiverProof: 'imported_cds_receiver',
|
|
338
|
+
consideredBindingSites: [],
|
|
339
|
+
};
|
|
340
|
+
if (identifier.text === 'cds'
|
|
341
|
+
&& !lexicalIdentifierDeclaration(identifier)) return {
|
|
342
|
+
receiverClassification: 'cap_evidence',
|
|
343
|
+
receiverProof: 'global_cds_receiver',
|
|
344
|
+
consideredBindingSites: [],
|
|
345
|
+
};
|
|
346
|
+
const proven = lexicalProof(identifier, use, index);
|
|
347
|
+
if (proven?.receiverClassification === 'cap_evidence') return proven;
|
|
348
|
+
return compatibilityFallback(identifier.text, index)
|
|
349
|
+
?? proven
|
|
350
|
+
?? unproven('event_receiver_unproven_binding', 'binding_not_found');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function rootIdentifier(
|
|
354
|
+
expression: ts.Expression,
|
|
355
|
+
): ts.Identifier | undefined {
|
|
356
|
+
if (ts.isIdentifier(expression)) return expression;
|
|
357
|
+
if (ts.isPropertyAccessExpression(expression)
|
|
358
|
+
|| ts.isElementAccessExpression(expression))
|
|
359
|
+
return rootIdentifier(expression.expression);
|
|
360
|
+
if (ts.isCallExpression(expression))
|
|
361
|
+
return rootIdentifier(expression.expression);
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function createEventReceiverIndex(
|
|
366
|
+
source: ts.SourceFile,
|
|
367
|
+
compatibilityNames: Set<string>,
|
|
368
|
+
): EventReceiverIndex {
|
|
369
|
+
return {
|
|
370
|
+
source,
|
|
371
|
+
lexical: createBindingLexicalIndex(source),
|
|
372
|
+
imports: collectSymbolImportBindings(source),
|
|
373
|
+
compatibilityNames,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function proveEventReceiver(
|
|
378
|
+
expression: ts.Expression,
|
|
379
|
+
use: ts.Node,
|
|
380
|
+
index: EventReceiverIndex,
|
|
381
|
+
): EventReceiverProof {
|
|
382
|
+
const receiver = expression.getText(index.source);
|
|
383
|
+
if (ts.isIdentifier(expression)) {
|
|
384
|
+
const proof = identifierProof(expression, use, index);
|
|
385
|
+
return {
|
|
386
|
+
...proof, receiver, rootReceiver: expression.text,
|
|
387
|
+
effectiveReceiver: expression.text,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const root = rootIdentifier(expression);
|
|
391
|
+
const rootProof = root ? identifierProof(root, use, index) : undefined;
|
|
392
|
+
const nonCap = rootProof?.unresolvedReason === 'event_receiver_not_cap_client';
|
|
393
|
+
const proof = nonCap
|
|
394
|
+
? unproven('event_receiver_not_cap_client', 'property_non_cap_root')
|
|
395
|
+
: unproven(
|
|
396
|
+
'event_receiver_unproven_propagation', 'property_receiver_propagation',
|
|
397
|
+
);
|
|
398
|
+
return {
|
|
399
|
+
...proof,
|
|
400
|
+
receiver,
|
|
401
|
+
rootReceiver: root?.text,
|
|
402
|
+
effectiveReceiver: receiver,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
@@ -9,12 +9,15 @@ import {
|
|
|
9
9
|
symbolImportReference,
|
|
10
10
|
type SymbolImportBinding,
|
|
11
11
|
type SymbolImportReference,
|
|
12
|
-
} from './
|
|
12
|
+
} from './symbol-import-bindings.js';
|
|
13
13
|
import type { ClassifiedOutboundCall } from './outbound-call-parser.js';
|
|
14
14
|
import {
|
|
15
15
|
localSymbolTarget,
|
|
16
16
|
type LocalSymbolTargetIdentity,
|
|
17
|
-
} from './
|
|
17
|
+
} from './local-symbol-reference.js';
|
|
18
|
+
import {
|
|
19
|
+
eventLoopRegistrationEvidence,
|
|
20
|
+
} from './event-loop-registration.js';
|
|
18
21
|
|
|
19
22
|
interface HandlerTarget {
|
|
20
23
|
calleeExpression: string;
|
|
@@ -235,6 +238,7 @@ function enrichSubscription(
|
|
|
235
238
|
);
|
|
236
239
|
const evidence = {
|
|
237
240
|
...(classified.fact.evidence ?? {}),
|
|
241
|
+
...eventLoopRegistrationEvidence(classified.node, source),
|
|
238
242
|
handlerReferenceStatus: classification.status,
|
|
239
243
|
...(classification.reason
|
|
240
244
|
? { handlerReferenceReason: classification.reason }
|
|
@@ -1,23 +1,89 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { GeneratedConstantFact } from '../types.js';
|
|
4
|
-
import { normalizePath
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { normalizePath } from '../utils/path-utils.js';
|
|
5
|
+
import {
|
|
6
|
+
collectStringConstantLookups,
|
|
7
|
+
type StaticStringConstant,
|
|
8
|
+
type StaticStringRefusal,
|
|
9
|
+
} from './string-constant-lookups.js';
|
|
10
|
+
import { createSourceFile } from './ts-project.js';
|
|
11
|
+
|
|
12
|
+
function sourceLine(
|
|
13
|
+
source: ReturnType<typeof createSourceFile>,
|
|
14
|
+
offset: number,
|
|
15
|
+
): number {
|
|
16
|
+
return source.getLineAndCharacterOfPosition(offset).line + 1;
|
|
7
17
|
}
|
|
18
|
+
|
|
19
|
+
function generatedFact(
|
|
20
|
+
source: ReturnType<typeof createSourceFile>,
|
|
21
|
+
filePath: string,
|
|
22
|
+
constant: StaticStringConstant,
|
|
23
|
+
): GeneratedConstantFact {
|
|
24
|
+
return {
|
|
25
|
+
name: constant.key,
|
|
26
|
+
value: constant.value,
|
|
27
|
+
sourceFile: normalizePath(filePath),
|
|
28
|
+
sourceLine: sourceLine(source, constant.declarationStartOffset),
|
|
29
|
+
containerName: constant.containerName,
|
|
30
|
+
memberName: constant.memberName,
|
|
31
|
+
constantKind: constant.kind,
|
|
32
|
+
exported: constant.exported,
|
|
33
|
+
stable: constant.stable,
|
|
34
|
+
resolutionStatus: 'resolved',
|
|
35
|
+
declarationStartOffset: constant.declarationStartOffset,
|
|
36
|
+
declarationEndOffset: constant.declarationEndOffset,
|
|
37
|
+
valueStartOffset: constant.valueStartOffset,
|
|
38
|
+
valueEndOffset: constant.valueEndOffset,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function refusedFact(
|
|
43
|
+
source: ReturnType<typeof createSourceFile>,
|
|
44
|
+
filePath: string,
|
|
45
|
+
refusal: StaticStringRefusal,
|
|
46
|
+
): GeneratedConstantFact {
|
|
47
|
+
return {
|
|
48
|
+
name: refusal.key,
|
|
49
|
+
sourceFile: normalizePath(filePath),
|
|
50
|
+
sourceLine: sourceLine(source, refusal.declarationStartOffset),
|
|
51
|
+
containerName: refusal.containerName,
|
|
52
|
+
memberName: refusal.memberName,
|
|
53
|
+
constantKind: refusal.kind,
|
|
54
|
+
exported: refusal.exported,
|
|
55
|
+
stable: refusal.stable,
|
|
56
|
+
resolutionStatus: 'refused',
|
|
57
|
+
unresolvedReason: refusal.reason,
|
|
58
|
+
declarationStartOffset: refusal.declarationStartOffset,
|
|
59
|
+
declarationEndOffset: refusal.declarationEndOffset,
|
|
60
|
+
valueStartOffset: refusal.declarationStartOffset,
|
|
61
|
+
valueEndOffset: refusal.declarationEndOffset,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generatedConstantFacts(
|
|
66
|
+
source: ReturnType<typeof createSourceFile>,
|
|
67
|
+
filePath: string,
|
|
68
|
+
): GeneratedConstantFact[] {
|
|
69
|
+
const lookups = collectStringConstantLookups(source);
|
|
70
|
+
const constants = [
|
|
71
|
+
...lookups.identifiers.values(),
|
|
72
|
+
...lookups.enumMembers.values(),
|
|
73
|
+
...lookups.objectProperties.values(),
|
|
74
|
+
];
|
|
75
|
+
return [
|
|
76
|
+
...constants.map((constant) =>
|
|
77
|
+
generatedFact(source, filePath, constant)),
|
|
78
|
+
...[...lookups.refusedMembers.values()].map((refusal) =>
|
|
79
|
+
refusedFact(source, filePath, refusal)),
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
8
83
|
export async function parseGeneratedConstants(
|
|
9
84
|
repoPath: string,
|
|
10
|
-
filePath: string
|
|
85
|
+
filePath: string,
|
|
11
86
|
): Promise<GeneratedConstantFact[]> {
|
|
12
87
|
const text = await fs.readFile(path.join(repoPath, filePath), 'utf8');
|
|
13
|
-
return
|
|
14
|
-
...text.matchAll(
|
|
15
|
-
/(?:export\s+)?(?:const|static\s+readonly)\s+(\w+)\s*=\s*(['"])([^'"]+)\2/g
|
|
16
|
-
)
|
|
17
|
-
].map((m) => ({
|
|
18
|
-
name: m[1] ?? 'constant',
|
|
19
|
-
value: stripQuotes(m[3] ?? ''),
|
|
20
|
-
sourceFile: normalizePath(filePath),
|
|
21
|
-
sourceLine: lineOf(text, m.index ?? 0)
|
|
22
|
-
}));
|
|
88
|
+
return generatedConstantFacts(createSourceFile(filePath, text), filePath);
|
|
23
89
|
}
|
|
@@ -3,8 +3,8 @@ import type { ExecutableSymbolFact } from '../types.js';
|
|
|
3
3
|
import {
|
|
4
4
|
lexicalIdentifierDeclaration,
|
|
5
5
|
lexicalIdentifierDeclarations,
|
|
6
|
-
} from './
|
|
7
|
-
import { stableLocalValueReference } from './
|
|
6
|
+
} from './symbol-import-bindings.js';
|
|
7
|
+
import { stableLocalValueReference } from './stable-local-value.js';
|
|
8
8
|
|
|
9
9
|
export interface LocalSymbolTargetIdentity {
|
|
10
10
|
sourceFile: string;
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
classifyODataPathIntent,
|
|
4
4
|
normalizeODataOperationInvocationPath,
|
|
5
5
|
} from '../linker/odata-path-normalizer.js';
|
|
6
|
-
import { analyzeODataPathStructure } from '../linker/
|
|
6
|
+
import { analyzeODataPathStructure } from '../linker/odata-path-structure.js';
|
|
7
7
|
|
|
8
8
|
export type OperationPathStatus = 'static' | 'ambiguous' | 'dynamic' | 'unknown';
|
|
9
9
|
|