@saptools/service-flow 0.1.70 → 0.1.73
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 +21 -0
- package/README.md +23 -6
- package/TECHNICAL-NOTE.md +24 -2
- package/dist/{chunk-GSLFY6J2.js → chunk-32WOTGTS.js} +12061 -9288
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +854 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -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 +446 -0
- package/src/cli/doctor.ts +4 -6
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -29
- package/src/db/event-fact-semantics.ts +546 -0
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +15 -1
- package/src/db/package-target-invalidation.ts +80 -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 +93 -10
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +27 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +204 -0
- package/src/linker/event-subscription-handler-linker.ts +220 -25
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +302 -0
- package/src/output/repository-inspection.ts +11 -0
- package/src/output/table-output.ts +13 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +404 -0
- package/src/parsers/event-call-analysis.ts +370 -0
- package/src/parsers/event-environment-reference.ts +242 -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 +394 -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 +42 -10
- package/src/parsers/string-constant-lookups.ts +408 -0
- package/src/trace/edge-target.ts +65 -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 +90 -8
- package/src/trace/evidence.ts +7 -28
- package/src/trace/trace-scope-execution.ts +22 -30
- package/src/types.ts +19 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -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
|
+
}
|