@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,231 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { normalizePath } from '../utils/path-utils.js';
|
|
4
|
+
import { resolveBinding } from './query-entity-resolution.js';
|
|
5
|
+
import {
|
|
6
|
+
collectSymbolImportBindings,
|
|
7
|
+
lexicalIdentifierDeclaration,
|
|
8
|
+
type SymbolImportBinding,
|
|
9
|
+
} from './symbol-import-bindings.js';
|
|
10
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
11
|
+
import { EVENT_ENVIRONMENT_KEY_ALLOWLIST } from
|
|
12
|
+
'./environment-declarations.js';
|
|
13
|
+
|
|
14
|
+
export type EventEnvironmentTransform = 'toUpperCase' | 'toLowerCase';
|
|
15
|
+
|
|
16
|
+
export interface EventEnvironmentReference {
|
|
17
|
+
status: 'resolved' | 'refused';
|
|
18
|
+
sourceKey: string;
|
|
19
|
+
environmentKey?: string;
|
|
20
|
+
transforms: EventEnvironmentTransform[];
|
|
21
|
+
sourceFile?: string;
|
|
22
|
+
startOffset?: number;
|
|
23
|
+
endOffset?: number;
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type EventEnvironmentReferenceResolver = (
|
|
28
|
+
expression: ts.Expression,
|
|
29
|
+
) => EventEnvironmentReference | undefined;
|
|
30
|
+
|
|
31
|
+
interface ResolutionContext {
|
|
32
|
+
sources: RepositorySourceContext;
|
|
33
|
+
source: ts.SourceFile;
|
|
34
|
+
sourceFile: string;
|
|
35
|
+
depth: number;
|
|
36
|
+
seen: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const maxEnvironmentReferenceDepth = 6;
|
|
40
|
+
const allowedEnvironmentKeys = new Set<string>(
|
|
41
|
+
EVENT_ENVIRONMENT_KEY_ALLOWLIST,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
45
|
+
if (ts.isParenthesizedExpression(expression)
|
|
46
|
+
|| ts.isAsExpression(expression)
|
|
47
|
+
|| ts.isSatisfiesExpression(expression)
|
|
48
|
+
|| ts.isTypeAssertionExpression(expression)
|
|
49
|
+
|| ts.isNonNullExpression(expression))
|
|
50
|
+
return unwrapExpression(expression.expression);
|
|
51
|
+
return expression;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function processEnvironmentKey(
|
|
55
|
+
expression: ts.Expression,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
if (!ts.isPropertyAccessExpression(expression)
|
|
58
|
+
|| !ts.isPropertyAccessExpression(expression.expression)
|
|
59
|
+
|| !ts.isIdentifier(expression.expression.expression)
|
|
60
|
+
|| expression.expression.expression.text !== 'process'
|
|
61
|
+
|| expression.expression.name.text !== 'env'
|
|
62
|
+
|| lexicalIdentifierDeclaration(expression.expression.expression))
|
|
63
|
+
return undefined;
|
|
64
|
+
return expression.name.text;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function relativeCandidates(
|
|
68
|
+
callerFile: string,
|
|
69
|
+
specifier: string,
|
|
70
|
+
): string[] {
|
|
71
|
+
const base = normalizePath(posix.normalize(
|
|
72
|
+
posix.join(posix.dirname(callerFile), specifier),
|
|
73
|
+
));
|
|
74
|
+
return /\.[jt]s$/.test(base)
|
|
75
|
+
? [base]
|
|
76
|
+
: [`${base}.ts`, `${base}.js`, `${base}/index.ts`, `${base}/index.js`];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function bindingFor(
|
|
80
|
+
identifier: ts.Identifier,
|
|
81
|
+
bindings: readonly SymbolImportBinding[],
|
|
82
|
+
): SymbolImportBinding | undefined {
|
|
83
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
84
|
+
if (!declaration) return undefined;
|
|
85
|
+
const start = declaration.getStart(identifier.getSourceFile());
|
|
86
|
+
const end = declaration.getEnd();
|
|
87
|
+
const matches = bindings.filter((binding) =>
|
|
88
|
+
binding.localName === identifier.text
|
|
89
|
+
&& binding.bindingSiteStartOffset === start
|
|
90
|
+
&& binding.bindingSiteEndOffset === end);
|
|
91
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function exportedVariableInitializer(
|
|
95
|
+
source: ts.SourceFile,
|
|
96
|
+
name: string,
|
|
97
|
+
): ts.Expression | undefined {
|
|
98
|
+
const exportNames = new Set(source.statements.flatMap((statement) =>
|
|
99
|
+
ts.isExportDeclaration(statement) && !statement.moduleSpecifier
|
|
100
|
+
&& statement.exportClause && ts.isNamedExports(statement.exportClause)
|
|
101
|
+
? statement.exportClause.elements.map((element) =>
|
|
102
|
+
element.propertyName?.text ?? element.name.text)
|
|
103
|
+
: []));
|
|
104
|
+
for (const statement of source.statements) {
|
|
105
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
106
|
+
const exported = ts.canHaveModifiers(statement)
|
|
107
|
+
&& ts.getModifiers(statement)?.some((modifier) =>
|
|
108
|
+
modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
109
|
+
for (const declaration of statement.declarationList.declarations)
|
|
110
|
+
if (ts.isIdentifier(declaration.name)
|
|
111
|
+
&& declaration.name.text === name
|
|
112
|
+
&& (exported || exportNames.has(name))
|
|
113
|
+
&& (statement.declarationList.flags & ts.NodeFlags.Const) !== 0)
|
|
114
|
+
return declaration.initializer;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function importedIdentifier(
|
|
120
|
+
identifier: ts.Identifier,
|
|
121
|
+
context: ResolutionContext,
|
|
122
|
+
): { source: ts.SourceFile; sourceFile: string;
|
|
123
|
+
initializer: ts.Expression } | undefined {
|
|
124
|
+
const binding = bindingFor(
|
|
125
|
+
identifier, collectSymbolImportBindings(context.source),
|
|
126
|
+
);
|
|
127
|
+
if (!binding || binding.typeOnly || binding.moduleKind !== 'relative'
|
|
128
|
+
|| !binding.importedName) return undefined;
|
|
129
|
+
const candidates = relativeCandidates(
|
|
130
|
+
context.sourceFile, binding.rawModuleSpecifier,
|
|
131
|
+
).flatMap((filePath) => {
|
|
132
|
+
const snapshot = context.sources.get(filePath);
|
|
133
|
+
return snapshot ? [{ sourceFile: filePath, source: snapshot.sourceFile() }]
|
|
134
|
+
: [];
|
|
135
|
+
});
|
|
136
|
+
if (candidates.length !== 1 || !candidates[0]) return undefined;
|
|
137
|
+
const initializer = exportedVariableInitializer(
|
|
138
|
+
candidates[0].source, binding.importedName,
|
|
139
|
+
);
|
|
140
|
+
return initializer ? { ...candidates[0], initializer } : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function transformedReference(
|
|
144
|
+
expression: ts.CallExpression,
|
|
145
|
+
context: ResolutionContext,
|
|
146
|
+
): EventEnvironmentReference | undefined {
|
|
147
|
+
if (!ts.isPropertyAccessExpression(expression.expression)) return undefined;
|
|
148
|
+
const method = expression.expression.name.text;
|
|
149
|
+
const base = resolveEnvironmentReference(
|
|
150
|
+
expression.expression.expression, context,
|
|
151
|
+
);
|
|
152
|
+
if (!base) return undefined;
|
|
153
|
+
if (expression.arguments.length !== 0
|
|
154
|
+
|| !['toUpperCase', 'toLowerCase'].includes(method)) return {
|
|
155
|
+
...base,
|
|
156
|
+
status: 'refused',
|
|
157
|
+
reason: 'event_environment_transform_unsupported',
|
|
158
|
+
};
|
|
159
|
+
return base.status === 'resolved'
|
|
160
|
+
? {
|
|
161
|
+
...base,
|
|
162
|
+
transforms: [
|
|
163
|
+
...base.transforms, method as EventEnvironmentTransform,
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
: base;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function identifierReference(
|
|
170
|
+
identifier: ts.Identifier,
|
|
171
|
+
context: ResolutionContext,
|
|
172
|
+
): EventEnvironmentReference | undefined {
|
|
173
|
+
const local = resolveBinding(identifier, identifier);
|
|
174
|
+
if (local.declaration && local.initializer && local.immutable)
|
|
175
|
+
return resolveEnvironmentReference(local.initializer, context);
|
|
176
|
+
const imported = importedIdentifier(identifier, context);
|
|
177
|
+
if (!imported) return undefined;
|
|
178
|
+
return resolveEnvironmentReference(imported.initializer, {
|
|
179
|
+
...context,
|
|
180
|
+
source: imported.source,
|
|
181
|
+
sourceFile: imported.sourceFile,
|
|
182
|
+
depth: context.depth + 1,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveEnvironmentReference(
|
|
187
|
+
expression: ts.Expression,
|
|
188
|
+
context: ResolutionContext,
|
|
189
|
+
): EventEnvironmentReference | undefined {
|
|
190
|
+
const sourceKey = expression.getText(context.source);
|
|
191
|
+
if (context.depth >= maxEnvironmentReferenceDepth
|
|
192
|
+
|| context.seen.has(`${context.sourceFile}\0${sourceKey}`)) return {
|
|
193
|
+
status: 'refused', sourceKey, transforms: [],
|
|
194
|
+
reason: 'event_environment_reference_ambiguous',
|
|
195
|
+
};
|
|
196
|
+
const seen = new Set(context.seen).add(
|
|
197
|
+
`${context.sourceFile}\0${sourceKey}`,
|
|
198
|
+
);
|
|
199
|
+
const next = { ...context, depth: context.depth + 1, seen };
|
|
200
|
+
const value = unwrapExpression(expression);
|
|
201
|
+
const key = processEnvironmentKey(value);
|
|
202
|
+
if (key && allowedEnvironmentKeys.has(key)) return {
|
|
203
|
+
status: 'resolved', sourceKey, environmentKey: key, transforms: [],
|
|
204
|
+
sourceFile: context.sourceFile,
|
|
205
|
+
startOffset: value.getStart(context.source), endOffset: value.getEnd(),
|
|
206
|
+
};
|
|
207
|
+
if (key) return undefined;
|
|
208
|
+
if (ts.isCallExpression(value))
|
|
209
|
+
return transformedReference(value, next);
|
|
210
|
+
return ts.isIdentifier(value)
|
|
211
|
+
? identifierReference(value, next) : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function createEventEnvironmentReferenceResolver(
|
|
215
|
+
sources: RepositorySourceContext,
|
|
216
|
+
source: ts.SourceFile,
|
|
217
|
+
sourceFile: string,
|
|
218
|
+
): EventEnvironmentReferenceResolver {
|
|
219
|
+
return (expression) => resolveEnvironmentReference(expression, {
|
|
220
|
+
sources, source, sourceFile, depth: 0, seen: new Set(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function applyEventEnvironmentTransforms(
|
|
225
|
+
value: string,
|
|
226
|
+
transforms: readonly EventEnvironmentTransform[],
|
|
227
|
+
): string {
|
|
228
|
+
return transforms.reduce((current, transform) =>
|
|
229
|
+
transform === 'toUpperCase'
|
|
230
|
+
? current.toUpperCase() : current.toLowerCase(), value);
|
|
231
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import {
|
|
3
|
+
collectStringConstantLookups,
|
|
4
|
+
type StaticStringConstant,
|
|
5
|
+
} from './string-constant-lookups.js';
|
|
6
|
+
import { resolveBinding } from './query-entity-resolution.js';
|
|
7
|
+
import { lexicalIdentifierDeclaration } from './symbol-import-bindings.js';
|
|
8
|
+
|
|
9
|
+
const loopValueCap = 32;
|
|
10
|
+
const loopExpressionLimit = 256;
|
|
11
|
+
|
|
12
|
+
function unwrap(expression: ts.Expression): ts.Expression {
|
|
13
|
+
if (ts.isParenthesizedExpression(expression)
|
|
14
|
+
|| ts.isAsExpression(expression)
|
|
15
|
+
|| ts.isSatisfiesExpression(expression)
|
|
16
|
+
|| ts.isTypeAssertionExpression(expression))
|
|
17
|
+
return unwrap(expression.expression);
|
|
18
|
+
return expression;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stringArray(expression: ts.Expression): string[] | undefined {
|
|
22
|
+
const value = unwrap(expression);
|
|
23
|
+
if (!ts.isArrayLiteralExpression(value)) return undefined;
|
|
24
|
+
const strings = value.elements.flatMap((element) => {
|
|
25
|
+
const item = ts.isSpreadElement(element) ? undefined : unwrap(element);
|
|
26
|
+
return item && ts.isStringLiteralLike(item) ? [item.text] : [];
|
|
27
|
+
});
|
|
28
|
+
return strings.length === value.elements.length ? strings : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function identifierArray(
|
|
32
|
+
identifier: ts.Identifier,
|
|
33
|
+
): string[] | undefined {
|
|
34
|
+
const binding = resolveBinding(identifier, identifier);
|
|
35
|
+
return binding.immutable && binding.initializer
|
|
36
|
+
? stringArray(binding.initializer) : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function orderedValues(values: StaticStringConstant[]): string[] {
|
|
40
|
+
return [...values]
|
|
41
|
+
.sort((left, right) =>
|
|
42
|
+
left.declarationStartOffset - right.declarationStartOffset)
|
|
43
|
+
.map((item) => item.value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function objectValues(
|
|
47
|
+
expression: ts.CallExpression,
|
|
48
|
+
source: ts.SourceFile,
|
|
49
|
+
): string[] | undefined {
|
|
50
|
+
if (!ts.isPropertyAccessExpression(expression.expression))
|
|
51
|
+
return undefined;
|
|
52
|
+
const root = expression.expression.expression;
|
|
53
|
+
if (!ts.isIdentifier(root) || root.text !== 'Object'
|
|
54
|
+
|| lexicalIdentifierDeclaration(root)
|
|
55
|
+
|| expression.expression.name.text !== 'values'
|
|
56
|
+
|| expression.arguments.length !== 1) return undefined;
|
|
57
|
+
const argument = expression.arguments[0];
|
|
58
|
+
if (!argument || !ts.isIdentifier(argument)) return undefined;
|
|
59
|
+
const lookups = collectStringConstantLookups(source);
|
|
60
|
+
const prefix = `${argument.text}.`;
|
|
61
|
+
const refused = [...lookups.refusedMembers.keys()].some((key) =>
|
|
62
|
+
key.startsWith(prefix));
|
|
63
|
+
const values = [
|
|
64
|
+
...lookups.enumMembers.values(),
|
|
65
|
+
...lookups.objectProperties.values(),
|
|
66
|
+
].filter((item) => item.key.startsWith(prefix));
|
|
67
|
+
return !refused && values.length > 0 ? orderedValues(values) : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function staticCollectionValues(
|
|
71
|
+
expression: ts.Expression,
|
|
72
|
+
source: ts.SourceFile,
|
|
73
|
+
): string[] | undefined {
|
|
74
|
+
const value = unwrap(expression);
|
|
75
|
+
if (ts.isArrayLiteralExpression(value)) return stringArray(value);
|
|
76
|
+
if (ts.isIdentifier(value)) return identifierArray(value);
|
|
77
|
+
return ts.isCallExpression(value) ? objectValues(value, source) : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function enclosingForEach(
|
|
81
|
+
node: ts.Node,
|
|
82
|
+
): ts.CallExpression | undefined {
|
|
83
|
+
let current: ts.Node | undefined = node.parent;
|
|
84
|
+
while (current && !ts.isSourceFile(current)) {
|
|
85
|
+
if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current))
|
|
86
|
+
&& ts.isCallExpression(current.parent)
|
|
87
|
+
&& current.parent.arguments.includes(current)
|
|
88
|
+
&& ts.isPropertyAccessExpression(current.parent.expression)
|
|
89
|
+
&& current.parent.expression.name.text === 'forEach')
|
|
90
|
+
return current.parent;
|
|
91
|
+
if (ts.isFunctionLike(current)) return undefined;
|
|
92
|
+
current = current.parent;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function boundedExpression(
|
|
98
|
+
expression: ts.Expression,
|
|
99
|
+
source: ts.SourceFile,
|
|
100
|
+
): string {
|
|
101
|
+
return expression.getText(source).slice(0, loopExpressionLimit);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function eventLoopRegistrationEvidence(
|
|
105
|
+
node: ts.CallExpression,
|
|
106
|
+
source: ts.SourceFile,
|
|
107
|
+
): Record<string, unknown> {
|
|
108
|
+
const loop = enclosingForEach(node);
|
|
109
|
+
if (!loop || !ts.isPropertyAccessExpression(loop.expression)) return {};
|
|
110
|
+
const collection = loop.expression.expression;
|
|
111
|
+
const values = staticCollectionValues(collection, source);
|
|
112
|
+
if (!values) return {
|
|
113
|
+
subscriptionRegisteredInLoop: true,
|
|
114
|
+
subscriptionLoopRegistrationStatus: 'unresolved',
|
|
115
|
+
subscriptionLoopUnresolvedReason:
|
|
116
|
+
'subscription_loop_collection_not_statically_enumerable',
|
|
117
|
+
subscriptionLoopCollectionExpression:
|
|
118
|
+
boundedExpression(collection, source),
|
|
119
|
+
};
|
|
120
|
+
const shown = values.slice(0, loopValueCap);
|
|
121
|
+
return {
|
|
122
|
+
subscriptionRegisteredInLoop: true,
|
|
123
|
+
subscriptionLoopRegistrationStatus: 'enumerated',
|
|
124
|
+
subscriptionLoopCollectionExpression:
|
|
125
|
+
boundedExpression(collection, source),
|
|
126
|
+
subscriptionLoopRegistrationCount: values.length,
|
|
127
|
+
subscriptionLoopValues: shown,
|
|
128
|
+
shownSubscriptionLoopValueCount: shown.length,
|
|
129
|
+
omittedSubscriptionLoopValueCount:
|
|
130
|
+
Math.max(0, values.length - shown.length),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { posix } from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { normalizePath } from '../utils/path-utils.js';
|
|
4
|
+
import {
|
|
5
|
+
collectStringConstantLookups,
|
|
6
|
+
type StaticStringConstant,
|
|
7
|
+
type StaticStringLookupResult,
|
|
8
|
+
type StringConstantLookups,
|
|
9
|
+
} from './string-constant-lookups.js';
|
|
10
|
+
import {
|
|
11
|
+
collectSymbolImportBindings,
|
|
12
|
+
derivedMemberImportReference,
|
|
13
|
+
lexicalIdentifierDeclaration,
|
|
14
|
+
type SymbolImportBinding,
|
|
15
|
+
type SymbolImportReference,
|
|
16
|
+
} from './symbol-import-bindings.js';
|
|
17
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
18
|
+
|
|
19
|
+
interface ImportedMemberRequest {
|
|
20
|
+
binding: SymbolImportBinding;
|
|
21
|
+
containerName: string;
|
|
22
|
+
memberName?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ImportedEventNameResult =
|
|
26
|
+
| { status: 'resolved'; constant: StaticStringConstant }
|
|
27
|
+
| { status: 'not_found' }
|
|
28
|
+
| {
|
|
29
|
+
status: 'refused';
|
|
30
|
+
reason: string;
|
|
31
|
+
packageImportReference?: SymbolImportReference;
|
|
32
|
+
};
|
|
33
|
+
export type ImportedEventNameResolver = (
|
|
34
|
+
expression: ts.Expression,
|
|
35
|
+
) => ImportedEventNameResult;
|
|
36
|
+
|
|
37
|
+
function bindingFor(
|
|
38
|
+
identifier: ts.Identifier,
|
|
39
|
+
bindings: readonly SymbolImportBinding[],
|
|
40
|
+
): SymbolImportBinding | undefined {
|
|
41
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
42
|
+
if (!declaration) return undefined;
|
|
43
|
+
const start = declaration.getStart(identifier.getSourceFile());
|
|
44
|
+
const end = declaration.getEnd();
|
|
45
|
+
const matches = bindings.filter((binding) =>
|
|
46
|
+
binding.localName === identifier.text
|
|
47
|
+
&& binding.bindingSiteStartOffset === start
|
|
48
|
+
&& binding.bindingSiteEndOffset === end);
|
|
49
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function directMemberRequest(
|
|
53
|
+
expression: ts.PropertyAccessExpression,
|
|
54
|
+
bindings: readonly SymbolImportBinding[],
|
|
55
|
+
): ImportedMemberRequest | undefined {
|
|
56
|
+
if (!ts.isIdentifier(expression.expression)) return undefined;
|
|
57
|
+
const binding = bindingFor(expression.expression, bindings);
|
|
58
|
+
if (!binding || binding.typeOnly || binding.importedName === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
return {
|
|
61
|
+
binding,
|
|
62
|
+
containerName: binding.importedName,
|
|
63
|
+
memberName: expression.name.text,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function namespaceMemberRequest(
|
|
68
|
+
expression: ts.PropertyAccessExpression,
|
|
69
|
+
bindings: readonly SymbolImportBinding[],
|
|
70
|
+
): ImportedMemberRequest | undefined {
|
|
71
|
+
const container = expression.expression;
|
|
72
|
+
if (!ts.isPropertyAccessExpression(container)
|
|
73
|
+
|| !ts.isIdentifier(container.expression)) return undefined;
|
|
74
|
+
const binding = bindingFor(container.expression, bindings);
|
|
75
|
+
if (!binding || binding.typeOnly
|
|
76
|
+
|| !['esm_namespace', 'cjs_namespace'].includes(binding.bindingKind))
|
|
77
|
+
return undefined;
|
|
78
|
+
return {
|
|
79
|
+
binding,
|
|
80
|
+
containerName: container.name.text,
|
|
81
|
+
memberName: expression.name.text,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function importedMemberRequest(
|
|
86
|
+
expression: ts.Expression,
|
|
87
|
+
bindings: readonly SymbolImportBinding[],
|
|
88
|
+
): ImportedMemberRequest | undefined {
|
|
89
|
+
if (ts.isIdentifier(expression)) {
|
|
90
|
+
const binding = bindingFor(expression, bindings);
|
|
91
|
+
return binding && !binding.typeOnly && binding.importedName
|
|
92
|
+
? { binding, containerName: binding.importedName }
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
if (!ts.isPropertyAccessExpression(expression)
|
|
96
|
+
|| expression.questionDotToken) return undefined;
|
|
97
|
+
return directMemberRequest(expression, bindings)
|
|
98
|
+
?? namespaceMemberRequest(expression, bindings);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rootIdentifier(
|
|
102
|
+
expression: ts.Expression,
|
|
103
|
+
): ts.Identifier | undefined {
|
|
104
|
+
if (ts.isIdentifier(expression)) return expression;
|
|
105
|
+
if (ts.isPropertyAccessExpression(expression)
|
|
106
|
+
|| ts.isElementAccessExpression(expression))
|
|
107
|
+
return rootIdentifier(expression.expression);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function importedAliasBinding(
|
|
112
|
+
identifier: ts.Identifier,
|
|
113
|
+
bindings: readonly SymbolImportBinding[],
|
|
114
|
+
): SymbolImportBinding | undefined {
|
|
115
|
+
const direct = bindingFor(identifier, bindings);
|
|
116
|
+
if (direct) return direct;
|
|
117
|
+
const declaration = lexicalIdentifierDeclaration(identifier);
|
|
118
|
+
if (!declaration || !ts.isVariableDeclaration(declaration.parent))
|
|
119
|
+
return undefined;
|
|
120
|
+
const initializer = declaration.parent.initializer;
|
|
121
|
+
return initializer && ts.isIdentifier(initializer)
|
|
122
|
+
? bindingFor(initializer, bindings) : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function unsupportedImportedContainer(
|
|
126
|
+
expression: ts.Expression,
|
|
127
|
+
bindings: readonly SymbolImportBinding[],
|
|
128
|
+
): boolean {
|
|
129
|
+
const root = rootIdentifier(expression);
|
|
130
|
+
return Boolean(root && importedAliasBinding(root, bindings));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function constantImportReference(
|
|
134
|
+
request: ImportedMemberRequest,
|
|
135
|
+
): SymbolImportReference | undefined {
|
|
136
|
+
const binding = request.binding;
|
|
137
|
+
if (!request.memberName) {
|
|
138
|
+
if (!binding.importedName) return undefined;
|
|
139
|
+
return {
|
|
140
|
+
...binding,
|
|
141
|
+
referenceShape: 'identifier',
|
|
142
|
+
referencedMemberName: null,
|
|
143
|
+
requestedPublicName: binding.importedName,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (binding.bindingKind !== 'esm_namespace'
|
|
147
|
+
&& binding.bindingKind !== 'cjs_namespace')
|
|
148
|
+
return derivedMemberImportReference(binding, request.memberName);
|
|
149
|
+
const requestedPublicName =
|
|
150
|
+
`${request.containerName}.${request.memberName}`;
|
|
151
|
+
return {
|
|
152
|
+
...binding,
|
|
153
|
+
referenceShape: 'namespace_member',
|
|
154
|
+
referencedMemberName: requestedPublicName,
|
|
155
|
+
requestedPublicName,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function relativeCandidates(
|
|
160
|
+
callerFile: string,
|
|
161
|
+
specifier: string,
|
|
162
|
+
): string[] {
|
|
163
|
+
const base = normalizePath(posix.normalize(
|
|
164
|
+
posix.join(posix.dirname(callerFile), specifier),
|
|
165
|
+
));
|
|
166
|
+
if (/\.[jt]s$/.test(base)) return [base];
|
|
167
|
+
return [
|
|
168
|
+
`${base}.ts`, `${base}.js`, `${base}/index.ts`, `${base}/index.js`,
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function targetLookups(
|
|
173
|
+
context: RepositorySourceContext,
|
|
174
|
+
callerFile: string,
|
|
175
|
+
request: ImportedMemberRequest,
|
|
176
|
+
): StringConstantLookups[] {
|
|
177
|
+
if (request.binding.moduleKind !== 'relative') return [];
|
|
178
|
+
return relativeCandidates(
|
|
179
|
+
callerFile, request.binding.rawModuleSpecifier,
|
|
180
|
+
).flatMap((candidate) => {
|
|
181
|
+
const snapshot = context.get(candidate);
|
|
182
|
+
return snapshot
|
|
183
|
+
? [collectStringConstantLookups(snapshot.sourceFile())] : [];
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function targetResult(
|
|
188
|
+
lookups: readonly StringConstantLookups[],
|
|
189
|
+
request: ImportedMemberRequest,
|
|
190
|
+
): StaticStringLookupResult {
|
|
191
|
+
if (lookups.length !== 1) return {
|
|
192
|
+
status: 'refused',
|
|
193
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
194
|
+
};
|
|
195
|
+
const key = request.memberName
|
|
196
|
+
? `${request.containerName}.${request.memberName}`
|
|
197
|
+
: request.containerName;
|
|
198
|
+
const constant = request.memberName
|
|
199
|
+
? lookups[0]?.enumMembers.get(key)
|
|
200
|
+
?? lookups[0]?.objectProperties.get(key)
|
|
201
|
+
: lookups[0]?.identifiers.get(key);
|
|
202
|
+
if (constant && constant.exported && constant.stable)
|
|
203
|
+
return { status: 'resolved', constant };
|
|
204
|
+
const refusal = lookups[0]?.refusedMembers.get(key);
|
|
205
|
+
if (refusal) return { status: 'refused', reason: refusal.reason };
|
|
206
|
+
if (constant && !constant.exported) return {
|
|
207
|
+
status: 'refused',
|
|
208
|
+
reason: 'event_name_constant_container_not_exported',
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
status: 'refused',
|
|
212
|
+
reason: 'event_name_constant_member_not_string',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createImportedEventNameResolver(
|
|
217
|
+
context: RepositorySourceContext,
|
|
218
|
+
source: ts.SourceFile,
|
|
219
|
+
sourceFile: string,
|
|
220
|
+
): ImportedEventNameResolver {
|
|
221
|
+
const bindings = collectSymbolImportBindings(source);
|
|
222
|
+
return (expression): ImportedEventNameResult => {
|
|
223
|
+
const request = importedMemberRequest(expression, bindings);
|
|
224
|
+
if (!request) return unsupportedImportedContainer(expression, bindings)
|
|
225
|
+
? {
|
|
226
|
+
status: 'refused',
|
|
227
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
228
|
+
}
|
|
229
|
+
: { status: 'not_found' };
|
|
230
|
+
if (request.binding.moduleKind !== 'relative') {
|
|
231
|
+
const packageImportReference = constantImportReference(request);
|
|
232
|
+
return packageImportReference ? {
|
|
233
|
+
status: 'refused',
|
|
234
|
+
reason: 'event_name_constant_resolution_pending',
|
|
235
|
+
packageImportReference,
|
|
236
|
+
} : {
|
|
237
|
+
status: 'refused',
|
|
238
|
+
reason: 'event_name_constant_container_ambiguous',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return targetResult(targetLookups(context, sourceFile, request), request);
|
|
242
|
+
};
|
|
243
|
+
}
|