@saptools/service-flow 0.1.70 → 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 +13 -0
- package/README.md +9 -5
- package/TECHNICAL-NOTE.md +13 -0
- package/dist/{chunk-GSLFY6J2.js → chunk-Z6D433R5.js} +8696 -6476
- package/dist/chunk-Z6D433R5.js.map +1 -0
- package/dist/cli.js +685 -142
- 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/doctor.ts +4 -6
- package/src/cli.ts +1 -1
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -5
- package/src/db/event-fact-semantics.ts +347 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/migrations.ts +12 -1
- package/src/db/package-target-invalidation.ts +79 -0
- package/src/db/repositories.ts +28 -2
- package/src/db/schema-structure.ts +41 -1
- package/src/db/schema.ts +6 -2
- package/src/indexer/repository-indexer.ts +45 -6
- package/src/linker/cross-repo-linker.ts +25 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +161 -0
- package/src/linker/event-subscription-handler-linker.ts +123 -19
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +298 -0
- package/src/output/table-output.ts +13 -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/event-subscription-facts.ts +4 -0
- package/src/parsers/generated-constants-parser.ts +80 -14
- package/src/parsers/outbound-call-classifier.ts +27 -124
- package/src/parsers/outbound-call-parser.ts +13 -1
- package/src/parsers/outbound-expression-analysis.ts +2 -2
- package/src/parsers/stable-local-value.ts +30 -9
- package/src/parsers/string-constant-lookups.ts +358 -0
- package/src/trace/event-runtime-resolution.ts +24 -3
- package/src/trace/event-shape-candidate-trace.ts +172 -0
- package/src/trace/event-subscriber-traversal.ts +19 -7
- package/src/trace/evidence.ts +7 -0
- package/src/trace/trace-scope-execution.ts +21 -29
- package/src/types.ts +17 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -11,11 +11,9 @@ import {
|
|
|
11
11
|
variableInitializers,
|
|
12
12
|
} from './query-entity-resolution.js';
|
|
13
13
|
import {
|
|
14
|
-
CDS_LIFECYCLE_EVENTS,
|
|
15
14
|
calledWrapperNames,
|
|
16
15
|
collectServiceVariables,
|
|
17
16
|
externalHttpEvidence,
|
|
18
|
-
isSupportedEventReceiver,
|
|
19
17
|
legacyPathCandidates,
|
|
20
18
|
lineOf,
|
|
21
19
|
literalPathSource,
|
|
@@ -35,6 +33,17 @@ import {
|
|
|
35
33
|
type ExpressionResolution,
|
|
36
34
|
type WrapperSpec,
|
|
37
35
|
} from './outbound-expression-analysis.js';
|
|
36
|
+
import {
|
|
37
|
+
analyzeEventCall,
|
|
38
|
+
createEventCallAnalysisContext,
|
|
39
|
+
type EventCallAnalysisContext,
|
|
40
|
+
} from './event-call-analysis.js';
|
|
41
|
+
import type {
|
|
42
|
+
ImportedEventNameResolver,
|
|
43
|
+
} from './event-name-import-resolution.js';
|
|
44
|
+
import type {
|
|
45
|
+
EventEnvironmentReferenceResolver,
|
|
46
|
+
} from './event-environment-reference.js';
|
|
38
47
|
import {
|
|
39
48
|
analyzeOperationPath,
|
|
40
49
|
operationPathExpression,
|
|
@@ -47,6 +56,11 @@ export interface ClassifiedOutboundCall {
|
|
|
47
56
|
node: ts.CallExpression;
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
export interface OutboundClassificationOptions {
|
|
60
|
+
importedEventNameResolver?: ImportedEventNameResolver;
|
|
61
|
+
eventEnvironmentReferenceResolver?: EventEnvironmentReferenceResolver;
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
function namedFunctionLike(
|
|
51
65
|
node: ts.Node,
|
|
52
66
|
): { name: string; fn: ts.FunctionLikeDeclaration } | undefined {
|
|
@@ -79,122 +93,6 @@ function collectWrapperSpecs(
|
|
|
79
93
|
return specs;
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
interface EventCallClassification {
|
|
83
|
-
fact: Pick<OutboundCallFact,
|
|
84
|
-
'callType' | 'serviceVariableName' | 'eventNameExpr'
|
|
85
|
-
| 'confidence' | 'unresolvedReason'>;
|
|
86
|
-
evidence: Record<string, unknown>;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface EventReceiver {
|
|
90
|
-
effectiveReceiver: string;
|
|
91
|
-
receiver?: string;
|
|
92
|
-
rootReceiver?: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function eventReceiver(
|
|
96
|
-
expression: ts.PropertyAccessExpression,
|
|
97
|
-
serviceVariables: Set<string>,
|
|
98
|
-
): EventReceiver | undefined {
|
|
99
|
-
const receiver = receiverName(expression.expression);
|
|
100
|
-
const rootReceiver = rootReceiverName(expression.expression);
|
|
101
|
-
if (!isSupportedEventReceiver(receiver, rootReceiver, serviceVariables))
|
|
102
|
-
return undefined;
|
|
103
|
-
const effectiveReceiver = rootReceiver ?? receiver;
|
|
104
|
-
return effectiveReceiver
|
|
105
|
-
? { receiver, rootReceiver, effectiveReceiver } : undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function eventUnresolvedReason(
|
|
109
|
-
resolved: ExpressionResolution,
|
|
110
|
-
): string | undefined {
|
|
111
|
-
if (resolved.status === 'static') return undefined;
|
|
112
|
-
return resolved.value !== undefined && resolved.placeholderKeys.length > 0
|
|
113
|
-
? 'dynamic_event_name_identifier'
|
|
114
|
-
: 'dynamic_event_name_unsupported_expression';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function eventConfidence(reason: string | undefined): number {
|
|
118
|
-
if (!reason) return 0.8;
|
|
119
|
-
return reason === 'dynamic_event_name_identifier' ? 0.6 : 0.3;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function eventClassificationEvidence(
|
|
123
|
-
expression: ts.PropertyAccessExpression,
|
|
124
|
-
receiver: EventReceiver,
|
|
125
|
-
resolved: ExpressionResolution,
|
|
126
|
-
): Record<string, unknown> {
|
|
127
|
-
return {
|
|
128
|
-
receiver: receiver.receiver,
|
|
129
|
-
rootReceiver: receiver.rootReceiver,
|
|
130
|
-
classifier: expression.name.text === 'on'
|
|
131
|
-
? 'cap_service_event_subscription' : 'cap_service_event_emit',
|
|
132
|
-
receiverClassification: 'cap_evidence',
|
|
133
|
-
...(resolved.status === 'static' ? {} : {
|
|
134
|
-
eventNameStatus: resolved.status,
|
|
135
|
-
eventNameSourceKind: resolved.sourceKind,
|
|
136
|
-
eventNamePlaceholderKeys: resolved.placeholderKeys,
|
|
137
|
-
}),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
interface EventNameState {
|
|
142
|
-
eventName: string;
|
|
143
|
-
resolved: ExpressionResolution;
|
|
144
|
-
unresolvedReason?: string;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function eventNameState(node: ts.CallExpression): EventNameState | undefined {
|
|
148
|
-
const expression = node.arguments[0];
|
|
149
|
-
const resolved = resolveExpression(expression, node, 'operation_path');
|
|
150
|
-
let eventName = resolved.value;
|
|
151
|
-
if (eventName === undefined) eventName = resolved.rawExpression;
|
|
152
|
-
if (eventName === undefined && expression)
|
|
153
|
-
eventName = expression.getText(node.getSourceFile());
|
|
154
|
-
if (!eventName) return undefined;
|
|
155
|
-
return {
|
|
156
|
-
eventName,
|
|
157
|
-
resolved,
|
|
158
|
-
unresolvedReason: eventUnresolvedReason(resolved),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function excludedEventName(
|
|
163
|
-
expression: ts.PropertyAccessExpression,
|
|
164
|
-
receiver: EventReceiver,
|
|
165
|
-
state: EventNameState,
|
|
166
|
-
): boolean {
|
|
167
|
-
if (state.resolved.status !== 'static') return false;
|
|
168
|
-
if (receiver.effectiveReceiver === 'cds'
|
|
169
|
-
&& CDS_LIFECYCLE_EVENTS.has(state.eventName)) return true;
|
|
170
|
-
return expression.name.text === 'on' && state.eventName === 'error';
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function eventCallClassification(
|
|
174
|
-
node: ts.CallExpression,
|
|
175
|
-
expression: ts.PropertyAccessExpression,
|
|
176
|
-
serviceVariables: Set<string>,
|
|
177
|
-
): EventCallClassification | undefined {
|
|
178
|
-
const receiver = eventReceiver(expression, serviceVariables);
|
|
179
|
-
if (!receiver) return undefined;
|
|
180
|
-
const state = eventNameState(node);
|
|
181
|
-
if (!state || excludedEventName(expression, receiver, state))
|
|
182
|
-
return undefined;
|
|
183
|
-
return {
|
|
184
|
-
fact: {
|
|
185
|
-
callType: expression.name.text === 'on'
|
|
186
|
-
? 'async_subscribe' : 'async_emit',
|
|
187
|
-
serviceVariableName: receiver.effectiveReceiver,
|
|
188
|
-
eventNameExpr: state.eventName,
|
|
189
|
-
confidence: eventConfidence(state.unresolvedReason),
|
|
190
|
-
unresolvedReason: state.unresolvedReason,
|
|
191
|
-
},
|
|
192
|
-
evidence: eventClassificationEvidence(
|
|
193
|
-
expression, receiver, state.resolved,
|
|
194
|
-
),
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
96
|
type OutboundFactInput = Omit<
|
|
199
97
|
OutboundCallFact, 'sourceFile' | 'sourceLine' | 'confidence'
|
|
200
98
|
> & { confidence?: number };
|
|
@@ -208,6 +106,7 @@ interface OutboundCallContext {
|
|
|
208
106
|
source: ts.SourceFile;
|
|
209
107
|
initializers: Map<string, ts.Expression>;
|
|
210
108
|
serviceVariables: Set<string>;
|
|
109
|
+
eventAnalysis: EventCallAnalysisContext;
|
|
211
110
|
wrapperSpecs: Map<string, WrapperSpec>;
|
|
212
111
|
add: AddOutboundCall;
|
|
213
112
|
}
|
|
@@ -589,11 +488,10 @@ function classifyEventCall(
|
|
|
589
488
|
const expression = node.expression;
|
|
590
489
|
if (!ts.isPropertyAccessExpression(expression)
|
|
591
490
|
|| !['emit', 'publish', 'on'].includes(expression.name.text)) return false;
|
|
592
|
-
const event =
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
return true;
|
|
491
|
+
const event = analyzeEventCall(node, expression, context.eventAnalysis);
|
|
492
|
+
if (event.status === 'classified')
|
|
493
|
+
context.add(node, event.fact, event.evidence);
|
|
494
|
+
return event.status !== 'unclassified';
|
|
597
495
|
}
|
|
598
496
|
|
|
599
497
|
function classifyExternalCall(
|
|
@@ -661,11 +559,16 @@ function visitOutboundCalls(
|
|
|
661
559
|
export function classifyOutboundCallsInSource(
|
|
662
560
|
source: ts.SourceFile,
|
|
663
561
|
filePath: string,
|
|
562
|
+
options: OutboundClassificationOptions = {},
|
|
664
563
|
): ClassifiedOutboundCall[] {
|
|
665
564
|
const calls: ClassifiedOutboundCall[] = [];
|
|
666
565
|
const sourceFile = normalizePath(filePath);
|
|
667
566
|
const initializers = variableInitializers(source);
|
|
668
567
|
const serviceVariables = collectServiceVariables(source);
|
|
568
|
+
const eventAnalysis = createEventCallAnalysisContext(
|
|
569
|
+
source, serviceVariables, options.importedEventNameResolver,
|
|
570
|
+
options.eventEnvironmentReferenceResolver,
|
|
571
|
+
);
|
|
669
572
|
const wrapperSpecs = collectWrapperSpecs(source);
|
|
670
573
|
const internalRanges = [...wrapperSpecs.values()].map((spec) => ({
|
|
671
574
|
start: spec.internalStart, end: spec.internalEnd,
|
|
@@ -682,7 +585,7 @@ export function classifyOutboundCallsInSource(
|
|
|
682
585
|
} });
|
|
683
586
|
};
|
|
684
587
|
const context = {
|
|
685
|
-
source, initializers, serviceVariables, wrapperSpecs, add,
|
|
588
|
+
source, initializers, serviceVariables, eventAnalysis, wrapperSpecs, add,
|
|
686
589
|
};
|
|
687
590
|
visitOutboundCalls(
|
|
688
591
|
source, internalRanges,
|
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
|
|
16
16
|
import { parseServiceBindings } from './service-binding-parser.js';
|
|
17
17
|
import type { RepositorySourceContext } from './ts-project.js';
|
|
18
|
+
import {
|
|
19
|
+
createImportedEventNameResolver,
|
|
20
|
+
} from './event-name-import-resolution.js';
|
|
21
|
+
import {
|
|
22
|
+
createEventEnvironmentReferenceResolver,
|
|
23
|
+
} from './event-environment-reference.js';
|
|
18
24
|
|
|
19
25
|
export {
|
|
20
26
|
classifyOutboundCallsInSource,
|
|
@@ -77,7 +83,13 @@ export async function parseOutboundCalls(
|
|
|
77
83
|
repoPath, filePath, source, bindingNames, context,
|
|
78
84
|
);
|
|
79
85
|
const nativeCalls = classified
|
|
80
|
-
?? classifyOutboundCallsInSource(source, filePath
|
|
86
|
+
?? classifyOutboundCallsInSource(source, filePath, context ? {
|
|
87
|
+
importedEventNameResolver: createImportedEventNameResolver(
|
|
88
|
+
context, source, filePath,
|
|
89
|
+
),
|
|
90
|
+
eventEnvironmentReferenceResolver:
|
|
91
|
+
createEventEnvironmentReferenceResolver(context, source, filePath),
|
|
92
|
+
} : undefined);
|
|
81
93
|
return [
|
|
82
94
|
...nativeCalls.map((call) => call.fact),
|
|
83
95
|
...importedWrappers,
|
|
@@ -55,7 +55,6 @@ export function queryWarning(expression: string): string {
|
|
|
55
55
|
return 'query_variable_without_static_initializer';
|
|
56
56
|
return 'dynamic_entity_expression';
|
|
57
57
|
}
|
|
58
|
-
|
|
59
58
|
export function parserEvidence(
|
|
60
59
|
source: ts.SourceFile,
|
|
61
60
|
node: ts.CallExpression,
|
|
@@ -109,7 +108,8 @@ type ExpressionSourceKind =
|
|
|
109
108
|
| 'string_literal'
|
|
110
109
|
| 'no_substitution_template'
|
|
111
110
|
| 'template_with_substitutions'
|
|
112
|
-
| 'const_alias'
|
|
111
|
+
| 'const_alias' | 'const_identifier'
|
|
112
|
+
| 'enum_member' | 'const_object_property'
|
|
113
113
|
| 'conditional_candidates'
|
|
114
114
|
| 'dynamic_expression';
|
|
115
115
|
|
|
@@ -37,19 +37,38 @@ function directWriteTarget(node: ts.Node): boolean {
|
|
|
37
37
|
&& parent.initializer === use;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
type MemberContainer =
|
|
41
|
-
|
|
40
|
+
type MemberContainer =
|
|
41
|
+
| ts.ObjectLiteralExpression
|
|
42
|
+
| ts.ClassLikeDeclaration
|
|
43
|
+
| ts.EnumDeclaration;
|
|
44
|
+
type StableMember =
|
|
45
|
+
| ts.ObjectLiteralElementLike
|
|
46
|
+
| ts.ClassElement
|
|
47
|
+
| ts.EnumMember;
|
|
48
|
+
|
|
49
|
+
function memberInitializer(
|
|
50
|
+
expression: ts.Expression,
|
|
51
|
+
): ts.Expression {
|
|
52
|
+
if (ts.isParenthesizedExpression(expression)
|
|
53
|
+
|| ts.isAsExpression(expression)
|
|
54
|
+
|| ts.isSatisfiesExpression(expression)
|
|
55
|
+
|| ts.isTypeAssertionExpression(expression))
|
|
56
|
+
return memberInitializer(expression.expression);
|
|
57
|
+
return expression;
|
|
58
|
+
}
|
|
42
59
|
|
|
43
60
|
function memberContainer(
|
|
44
61
|
declaration: ts.Identifier,
|
|
45
62
|
): MemberContainer | undefined {
|
|
46
63
|
const parent = declaration.parent;
|
|
47
64
|
if (ts.isClassDeclaration(parent)) return parent;
|
|
65
|
+
if (ts.isEnumDeclaration(parent)) return parent;
|
|
48
66
|
if (!ts.isVariableDeclaration(parent) || parent.name !== declaration
|
|
49
67
|
|| !parent.initializer) return undefined;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
const initializer = memberInitializer(parent.initializer);
|
|
69
|
+
return ts.isObjectLiteralExpression(initializer)
|
|
70
|
+
|| ts.isClassExpression(initializer)
|
|
71
|
+
? initializer
|
|
53
72
|
: undefined;
|
|
54
73
|
}
|
|
55
74
|
|
|
@@ -81,10 +100,12 @@ function matchingMember(
|
|
|
81
100
|
): StableMember | undefined {
|
|
82
101
|
const members = ts.isObjectLiteralExpression(value)
|
|
83
102
|
? value.properties
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
: ts.isEnumDeclaration(value)
|
|
104
|
+
? value.members
|
|
105
|
+
: value.members.filter((member) =>
|
|
106
|
+
ts.canHaveModifiers(member)
|
|
107
|
+
&& ts.getModifiers(member)?.some((modifier) =>
|
|
108
|
+
modifier.kind === ts.SyntaxKind.StaticKeyword));
|
|
88
109
|
const matches = members.filter((member) =>
|
|
89
110
|
'name' in member && member.name && memberName(member.name) === name);
|
|
90
111
|
if (matches.length === 1) return matches[0];
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { stableLocalValueReference } from './stable-local-value.js';
|
|
3
|
+
import { lexicalIdentifierDeclaration } from './symbol-import-bindings.js';
|
|
4
|
+
|
|
5
|
+
export type StaticStringConstantKind =
|
|
6
|
+
| 'const_identifier'
|
|
7
|
+
| 'enum_member'
|
|
8
|
+
| 'const_object_property';
|
|
9
|
+
|
|
10
|
+
export interface StaticStringConstant {
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
kind: StaticStringConstantKind;
|
|
14
|
+
containerName?: string;
|
|
15
|
+
memberName?: string;
|
|
16
|
+
sourceFile: string;
|
|
17
|
+
declarationStartOffset: number;
|
|
18
|
+
declarationEndOffset: number;
|
|
19
|
+
valueStartOffset: number;
|
|
20
|
+
valueEndOffset: number;
|
|
21
|
+
exported: boolean;
|
|
22
|
+
stable: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StaticStringRefusal {
|
|
26
|
+
key: string;
|
|
27
|
+
kind: StaticStringConstantKind;
|
|
28
|
+
containerName?: string;
|
|
29
|
+
memberName?: string;
|
|
30
|
+
sourceFile: string;
|
|
31
|
+
declarationStartOffset: number;
|
|
32
|
+
declarationEndOffset: number;
|
|
33
|
+
exported: boolean;
|
|
34
|
+
stable: boolean;
|
|
35
|
+
reason: 'event_name_constant_member_not_string'
|
|
36
|
+
| 'event_name_constant_container_mutable';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StringConstantLookups {
|
|
40
|
+
identifiers: Map<string, StaticStringConstant>;
|
|
41
|
+
enumMembers: Map<string, StaticStringConstant>;
|
|
42
|
+
objectProperties: Map<string, StaticStringConstant>;
|
|
43
|
+
refusedMembers: Map<string, StaticStringRefusal>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type StaticStringLookupResult =
|
|
47
|
+
| { status: 'resolved'; constant: StaticStringConstant }
|
|
48
|
+
| { status: 'refused'; reason: string }
|
|
49
|
+
| { status: 'not_found' };
|
|
50
|
+
|
|
51
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
52
|
+
if (ts.isParenthesizedExpression(expression)
|
|
53
|
+
|| ts.isAsExpression(expression)
|
|
54
|
+
|| ts.isSatisfiesExpression(expression)
|
|
55
|
+
|| ts.isTypeAssertionExpression(expression))
|
|
56
|
+
return unwrapExpression(expression.expression);
|
|
57
|
+
return expression;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stringValue(
|
|
61
|
+
expression: ts.Expression | undefined,
|
|
62
|
+
): { value: string; node: ts.Expression } | undefined {
|
|
63
|
+
if (!expression) return undefined;
|
|
64
|
+
const value = unwrapExpression(expression);
|
|
65
|
+
return ts.isStringLiteralLike(value)
|
|
66
|
+
? { value: value.text, node: value } : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function propertyName(name: ts.PropertyName): string | undefined {
|
|
70
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
71
|
+
|| ts.isNumericLiteral(name) ? name.text : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function exportedNames(source: ts.SourceFile): Set<string> {
|
|
75
|
+
const names = new Set<string>();
|
|
76
|
+
for (const statement of source.statements) {
|
|
77
|
+
if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier
|
|
78
|
+
|| !statement.exportClause
|
|
79
|
+
|| !ts.isNamedExports(statement.exportClause)) continue;
|
|
80
|
+
for (const element of statement.exportClause.elements)
|
|
81
|
+
names.add(element.propertyName?.text ?? element.name.text);
|
|
82
|
+
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasExportModifier(node: ts.Node): boolean {
|
|
87
|
+
return ts.canHaveModifiers(node)
|
|
88
|
+
&& Boolean(ts.getModifiers(node)?.some((modifier) =>
|
|
89
|
+
modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function staticFact(
|
|
93
|
+
source: ts.SourceFile,
|
|
94
|
+
key: string,
|
|
95
|
+
value: { value: string; node: ts.Expression },
|
|
96
|
+
declaration: ts.Node,
|
|
97
|
+
fields: Pick<StaticStringConstant, 'kind' | 'exported' | 'stable'>
|
|
98
|
+
& Partial<Pick<StaticStringConstant, 'containerName' | 'memberName'>>,
|
|
99
|
+
): StaticStringConstant {
|
|
100
|
+
return {
|
|
101
|
+
key,
|
|
102
|
+
value: value.value,
|
|
103
|
+
sourceFile: source.fileName,
|
|
104
|
+
declarationStartOffset: declaration.getStart(source),
|
|
105
|
+
declarationEndOffset: declaration.getEnd(),
|
|
106
|
+
valueStartOffset: value.node.getStart(source),
|
|
107
|
+
valueEndOffset: value.node.getEnd(),
|
|
108
|
+
...fields,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function refusalFact(
|
|
113
|
+
source: ts.SourceFile,
|
|
114
|
+
key: string,
|
|
115
|
+
declaration: ts.Node,
|
|
116
|
+
fields: Pick<StaticStringRefusal,
|
|
117
|
+
'kind' | 'exported' | 'stable' | 'reason'>
|
|
118
|
+
& Partial<Pick<StaticStringRefusal, 'containerName' | 'memberName'>>,
|
|
119
|
+
): StaticStringRefusal {
|
|
120
|
+
return {
|
|
121
|
+
key,
|
|
122
|
+
sourceFile: source.fileName,
|
|
123
|
+
declarationStartOffset: declaration.getStart(source),
|
|
124
|
+
declarationEndOffset: declaration.getEnd(),
|
|
125
|
+
...fields,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function collectEnum(
|
|
130
|
+
source: ts.SourceFile,
|
|
131
|
+
statement: ts.EnumDeclaration,
|
|
132
|
+
exports: Set<string>,
|
|
133
|
+
lookups: StringConstantLookups,
|
|
134
|
+
): void {
|
|
135
|
+
const stable = stableLocalValueReference(source, statement.name);
|
|
136
|
+
const exported = hasExportModifier(statement)
|
|
137
|
+
|| exports.has(statement.name.text);
|
|
138
|
+
for (const member of statement.members) {
|
|
139
|
+
const memberName = propertyName(member.name);
|
|
140
|
+
if (!memberName) continue;
|
|
141
|
+
const key = `${statement.name.text}.${memberName}`;
|
|
142
|
+
const value = stringValue(member.initializer);
|
|
143
|
+
if (!stable) lookups.refusedMembers.set(key, refusalFact(
|
|
144
|
+
source, key, member, {
|
|
145
|
+
kind: 'enum_member', containerName: statement.name.text,
|
|
146
|
+
memberName, exported, stable,
|
|
147
|
+
reason: 'event_name_constant_container_mutable',
|
|
148
|
+
},
|
|
149
|
+
));
|
|
150
|
+
else if (!value) lookups.refusedMembers.set(key, refusalFact(
|
|
151
|
+
source, key, member, {
|
|
152
|
+
kind: 'enum_member', containerName: statement.name.text,
|
|
153
|
+
memberName, exported, stable,
|
|
154
|
+
reason: 'event_name_constant_member_not_string',
|
|
155
|
+
},
|
|
156
|
+
));
|
|
157
|
+
else lookups.enumMembers.set(key, staticFact(
|
|
158
|
+
source, key, value, member, {
|
|
159
|
+
kind: 'enum_member',
|
|
160
|
+
containerName: statement.name.text,
|
|
161
|
+
memberName,
|
|
162
|
+
exported,
|
|
163
|
+
stable,
|
|
164
|
+
},
|
|
165
|
+
));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function objectLiteral(
|
|
170
|
+
expression: ts.Expression,
|
|
171
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
172
|
+
const value = unwrapExpression(expression);
|
|
173
|
+
return ts.isObjectLiteralExpression(value) ? value : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectObject(
|
|
177
|
+
source: ts.SourceFile,
|
|
178
|
+
statement: ts.VariableStatement,
|
|
179
|
+
declaration: ts.VariableDeclaration,
|
|
180
|
+
exports: Set<string>,
|
|
181
|
+
lookups: StringConstantLookups,
|
|
182
|
+
): void {
|
|
183
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
|
|
184
|
+
const object = objectLiteral(declaration.initializer);
|
|
185
|
+
if (!object) return;
|
|
186
|
+
const stable = stableLocalValueReference(source, declaration.name);
|
|
187
|
+
const exported = hasExportModifier(statement)
|
|
188
|
+
|| exports.has(declaration.name.text);
|
|
189
|
+
for (const property of object.properties) {
|
|
190
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
191
|
+
const memberName = propertyName(property.name);
|
|
192
|
+
if (!memberName) continue;
|
|
193
|
+
const key = `${declaration.name.text}.${memberName}`;
|
|
194
|
+
const value = stringValue(property.initializer);
|
|
195
|
+
if (!stable) lookups.refusedMembers.set(key, refusalFact(
|
|
196
|
+
source, key, property, {
|
|
197
|
+
kind: 'const_object_property',
|
|
198
|
+
containerName: declaration.name.text, memberName,
|
|
199
|
+
exported, stable,
|
|
200
|
+
reason: 'event_name_constant_container_mutable',
|
|
201
|
+
},
|
|
202
|
+
));
|
|
203
|
+
else if (!value) lookups.refusedMembers.set(key, refusalFact(
|
|
204
|
+
source, key, property, {
|
|
205
|
+
kind: 'const_object_property',
|
|
206
|
+
containerName: declaration.name.text, memberName,
|
|
207
|
+
exported, stable,
|
|
208
|
+
reason: 'event_name_constant_member_not_string',
|
|
209
|
+
},
|
|
210
|
+
));
|
|
211
|
+
else lookups.objectProperties.set(key, staticFact(
|
|
212
|
+
source, key, value, property, {
|
|
213
|
+
kind: 'const_object_property',
|
|
214
|
+
containerName: declaration.name.text,
|
|
215
|
+
memberName,
|
|
216
|
+
exported,
|
|
217
|
+
stable,
|
|
218
|
+
},
|
|
219
|
+
));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function collectIdentifier(
|
|
224
|
+
source: ts.SourceFile,
|
|
225
|
+
statement: ts.VariableStatement,
|
|
226
|
+
declaration: ts.VariableDeclaration,
|
|
227
|
+
exports: Set<string>,
|
|
228
|
+
lookups: StringConstantLookups,
|
|
229
|
+
): void {
|
|
230
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) return;
|
|
231
|
+
const value = stringValue(declaration.initializer);
|
|
232
|
+
if (!value) return;
|
|
233
|
+
const key = declaration.name.text;
|
|
234
|
+
lookups.identifiers.set(key, staticFact(
|
|
235
|
+
source, key, value, declaration, {
|
|
236
|
+
kind: 'const_identifier',
|
|
237
|
+
exported: hasExportModifier(statement) || exports.has(key),
|
|
238
|
+
stable: true,
|
|
239
|
+
},
|
|
240
|
+
));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function collectStringConstantLookups(
|
|
244
|
+
source: ts.SourceFile,
|
|
245
|
+
): StringConstantLookups {
|
|
246
|
+
const lookups: StringConstantLookups = {
|
|
247
|
+
identifiers: new Map(),
|
|
248
|
+
enumMembers: new Map(),
|
|
249
|
+
objectProperties: new Map(),
|
|
250
|
+
refusedMembers: new Map(),
|
|
251
|
+
};
|
|
252
|
+
const exports = exportedNames(source);
|
|
253
|
+
for (const statement of source.statements) {
|
|
254
|
+
if (ts.isEnumDeclaration(statement))
|
|
255
|
+
collectEnum(source, statement, exports, lookups);
|
|
256
|
+
if (!ts.isVariableStatement(statement)
|
|
257
|
+
|| (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
258
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
259
|
+
collectIdentifier(source, statement, declaration, exports, lookups);
|
|
260
|
+
collectObject(source, statement, declaration, exports, lookups);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return lookups;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function resolveStringConstant(
|
|
267
|
+
expression: ts.Expression,
|
|
268
|
+
lookups: StringConstantLookups,
|
|
269
|
+
): StaticStringLookupResult {
|
|
270
|
+
const value = unwrapExpression(expression);
|
|
271
|
+
if (ts.isIdentifier(value)) {
|
|
272
|
+
const constant = lookups.identifiers.get(value.text);
|
|
273
|
+
return constant && constantReferenceMatches(value, constant)
|
|
274
|
+
? { status: 'resolved', constant } : { status: 'not_found' };
|
|
275
|
+
}
|
|
276
|
+
if (ts.isElementAccessExpression(value)
|
|
277
|
+
&& ts.isIdentifier(value.expression)) {
|
|
278
|
+
const prefix = `${value.expression.text}.`;
|
|
279
|
+
const known = [
|
|
280
|
+
...lookups.enumMembers.keys(),
|
|
281
|
+
...lookups.objectProperties.keys(),
|
|
282
|
+
...lookups.refusedMembers.keys(),
|
|
283
|
+
].some((key) => key.startsWith(prefix));
|
|
284
|
+
return known && constantContainerMatches(
|
|
285
|
+
value.expression, lookups, prefix,
|
|
286
|
+
) ? {
|
|
287
|
+
status: 'refused',
|
|
288
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
289
|
+
} : { status: 'not_found' };
|
|
290
|
+
}
|
|
291
|
+
if (!ts.isPropertyAccessExpression(value)
|
|
292
|
+
|| value.questionDotToken || !ts.isIdentifier(value.expression))
|
|
293
|
+
return { status: 'not_found' };
|
|
294
|
+
const key = `${value.expression.text}.${value.name.text}`;
|
|
295
|
+
const constant = lookups.enumMembers.get(key)
|
|
296
|
+
?? lookups.objectProperties.get(key);
|
|
297
|
+
if (constant && constantReferenceMatches(value.expression, constant))
|
|
298
|
+
return { status: 'resolved', constant };
|
|
299
|
+
const reason = lookups.refusedMembers.get(key);
|
|
300
|
+
return reason && refusalReferenceMatches(value.expression, reason)
|
|
301
|
+
? { status: 'refused', reason: reason.reason }
|
|
302
|
+
: constant || reason ? {
|
|
303
|
+
status: 'refused',
|
|
304
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
305
|
+
} : { status: 'not_found' };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function declarationContains(
|
|
309
|
+
identifier: ts.Identifier,
|
|
310
|
+
startOffset: number,
|
|
311
|
+
endOffset: number,
|
|
312
|
+
): boolean {
|
|
313
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
314
|
+
if (!declaration) return false;
|
|
315
|
+
const owner = declaration.parent;
|
|
316
|
+
return owner.getSourceFile() === identifier.getSourceFile()
|
|
317
|
+
&& owner.getStart(identifier.getSourceFile()) <= startOffset
|
|
318
|
+
&& owner.getEnd() >= endOffset;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function constantReferenceMatches(
|
|
322
|
+
identifier: ts.Identifier,
|
|
323
|
+
constant: StaticStringConstant,
|
|
324
|
+
): boolean {
|
|
325
|
+
return declarationContains(
|
|
326
|
+
identifier,
|
|
327
|
+
constant.declarationStartOffset,
|
|
328
|
+
constant.declarationEndOffset,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function refusalReferenceMatches(
|
|
333
|
+
identifier: ts.Identifier,
|
|
334
|
+
refusal: StaticStringRefusal,
|
|
335
|
+
): boolean {
|
|
336
|
+
return declarationContains(
|
|
337
|
+
identifier,
|
|
338
|
+
refusal.declarationStartOffset,
|
|
339
|
+
refusal.declarationEndOffset,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function constantContainerMatches(
|
|
344
|
+
identifier: ts.Identifier,
|
|
345
|
+
lookups: StringConstantLookups,
|
|
346
|
+
prefix: string,
|
|
347
|
+
): boolean {
|
|
348
|
+
const constants = [
|
|
349
|
+
...lookups.enumMembers.values(),
|
|
350
|
+
...lookups.objectProperties.values(),
|
|
351
|
+
].filter((item) => item.key.startsWith(prefix));
|
|
352
|
+
const refusals = [...lookups.refusedMembers.values()]
|
|
353
|
+
.filter((item) => item.key.startsWith(prefix));
|
|
354
|
+
return [...constants, ...refusals].some((item) =>
|
|
355
|
+
declarationContains(
|
|
356
|
+
identifier, item.declarationStartOffset, item.declarationEndOffset,
|
|
357
|
+
));
|
|
358
|
+
}
|