@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +9 -5
  3. package/TECHNICAL-NOTE.md +13 -0
  4. package/dist/{chunk-GSLFY6J2.js → chunk-Z6D433R5.js} +8696 -6476
  5. package/dist/chunk-Z6D433R5.js.map +1 -0
  6. package/dist/cli.js +685 -142
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.d.ts +53 -14
  9. package/dist/index.js +2 -23
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/doctor-event-quality.ts +371 -0
  13. package/src/cli/doctor.ts +4 -6
  14. package/src/cli.ts +1 -1
  15. package/src/db/call-fact-repository.ts +6 -3
  16. package/src/db/current-fact-semantics.ts +5 -5
  17. package/src/db/event-fact-semantics.ts +347 -0
  18. package/src/db/event-surface-invalidation.ts +38 -0
  19. package/src/db/fact-json-inventory.ts +16 -0
  20. package/src/db/migrations.ts +12 -1
  21. package/src/db/package-target-invalidation.ts +79 -0
  22. package/src/db/repositories.ts +28 -2
  23. package/src/db/schema-structure.ts +41 -1
  24. package/src/db/schema.ts +6 -2
  25. package/src/indexer/repository-indexer.ts +45 -6
  26. package/src/linker/cross-repo-linker.ts +25 -3
  27. package/src/linker/event-environment-link.ts +211 -0
  28. package/src/linker/event-shape-candidate-linker.ts +161 -0
  29. package/src/linker/event-subscription-handler-linker.ts +123 -19
  30. package/src/linker/event-template-link.ts +40 -6
  31. package/src/linker/package-event-constant-resolver.ts +298 -0
  32. package/src/output/table-output.ts +13 -1
  33. package/src/parsers/decorator-parser.ts +9 -53
  34. package/src/parsers/environment-declarations.ts +327 -0
  35. package/src/parsers/event-call-analysis.ts +242 -0
  36. package/src/parsers/event-environment-reference.ts +231 -0
  37. package/src/parsers/event-loop-registration.ts +132 -0
  38. package/src/parsers/event-name-import-resolution.ts +243 -0
  39. package/src/parsers/event-receiver-analysis.ts +404 -0
  40. package/src/parsers/event-subscription-facts.ts +4 -0
  41. package/src/parsers/generated-constants-parser.ts +80 -14
  42. package/src/parsers/outbound-call-classifier.ts +27 -124
  43. package/src/parsers/outbound-call-parser.ts +13 -1
  44. package/src/parsers/outbound-expression-analysis.ts +2 -2
  45. package/src/parsers/stable-local-value.ts +30 -9
  46. package/src/parsers/string-constant-lookups.ts +358 -0
  47. package/src/trace/event-runtime-resolution.ts +24 -3
  48. package/src/trace/event-shape-candidate-trace.ts +172 -0
  49. package/src/trace/event-subscriber-traversal.ts +19 -7
  50. package/src/trace/evidence.ts +7 -0
  51. package/src/trace/trace-scope-execution.ts +21 -29
  52. package/src/types.ts +17 -1
  53. package/src/utils/event-skeleton.ts +207 -0
  54. package/src/version.ts +1 -1
  55. 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 = eventCallClassification(
593
- node, expression, context.serviceVariables,
594
- );
595
- if (event) context.add(node, event.fact, event.evidence);
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 = ts.ObjectLiteralExpression | ts.ClassLikeDeclaration;
41
- type StableMember = ts.ObjectLiteralElementLike | ts.ClassElement;
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
- return ts.isObjectLiteralExpression(parent.initializer)
51
- || ts.isClassExpression(parent.initializer)
52
- ? parent.initializer
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
- : value.members.filter((member) =>
85
- ts.canHaveModifiers(member)
86
- && ts.getModifiers(member)?.some((modifier) =>
87
- modifier.kind === ts.SyntaxKind.StaticKeyword));
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
+ }