@saptools/service-flow 0.1.51 → 0.1.52
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 +7 -0
- package/README.md +10 -5
- package/TECHNICAL-NOTE.md +8 -4
- package/dist/{chunk-YZJKE5UX.js → chunk-PTLDSHRC.js} +2412 -553
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +280 -506
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +75 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +13 -1
- package/src/output/table-output.ts +29 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-targets.ts +459 -229
- package/src/trace/evidence.ts +305 -46
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +90 -83
- package/src/types.ts +27 -1
- package/dist/chunk-YZJKE5UX.js.map +0 -1
|
@@ -1,17 +1,65 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import ts from 'typescript';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
HandlerClassFact,
|
|
6
|
+
HandlerLifecycleEvent,
|
|
7
|
+
HandlerLifecyclePhase,
|
|
8
|
+
HandlerMethodFact,
|
|
9
|
+
HandlerMethodKind,
|
|
10
|
+
} from '../types.js';
|
|
5
11
|
import { generatedOperationNameFromConstant } from '../linker/operation-decorator-normalizer.js';
|
|
6
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createSourceFile,
|
|
14
|
+
type RepositorySourceContext,
|
|
15
|
+
} from './ts-project.js';
|
|
7
16
|
import { normalizePath } from '../utils/path-utils.js';
|
|
8
17
|
|
|
9
18
|
type DecoratorResolution = HandlerMethodFact['decoratorResolution'];
|
|
19
|
+
type ResolvedArgumentKind = Exclude<
|
|
20
|
+
DecoratorResolution['resolutionKind'],
|
|
21
|
+
'lifecycle_implicit' | 'unresolved'
|
|
22
|
+
>;
|
|
10
23
|
interface StringLookups {
|
|
11
24
|
identifiers: Map<string, string>;
|
|
12
25
|
enumMembers: Map<string, string>;
|
|
13
26
|
objectProperties: Map<string, string>;
|
|
27
|
+
capDecoratorNames: Map<string, string>;
|
|
28
|
+
capDecoratorNamespaces: Set<string>;
|
|
14
29
|
}
|
|
30
|
+
interface LifecycleMetadata {
|
|
31
|
+
phase: HandlerLifecyclePhase;
|
|
32
|
+
event: HandlerLifecycleEvent;
|
|
33
|
+
}
|
|
34
|
+
interface MethodClassification {
|
|
35
|
+
handlerKind: HandlerMethodKind;
|
|
36
|
+
executable: boolean;
|
|
37
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
38
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
39
|
+
}
|
|
40
|
+
interface ParsedMethodDecorator {
|
|
41
|
+
classification: MethodClassification;
|
|
42
|
+
resolution: DecoratorResolution;
|
|
43
|
+
decoratorKind: string;
|
|
44
|
+
importedKind?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const OPERATION_DECORATORS = new Set(['Action', 'Func', 'On']);
|
|
48
|
+
const EVENT_DECORATORS = new Set(['Event']);
|
|
49
|
+
const LIFECYCLE_DECORATORS = new Map<string, LifecycleMetadata>([
|
|
50
|
+
['BeforeCreate', { phase: 'before', event: 'CREATE' }],
|
|
51
|
+
['OnCreate', { phase: 'on', event: 'CREATE' }],
|
|
52
|
+
['AfterCreate', { phase: 'after', event: 'CREATE' }],
|
|
53
|
+
['BeforeRead', { phase: 'before', event: 'READ' }],
|
|
54
|
+
['OnRead', { phase: 'on', event: 'READ' }],
|
|
55
|
+
['AfterRead', { phase: 'after', event: 'READ' }],
|
|
56
|
+
['BeforeUpdate', { phase: 'before', event: 'UPDATE' }],
|
|
57
|
+
['OnUpdate', { phase: 'on', event: 'UPDATE' }],
|
|
58
|
+
['AfterUpdate', { phase: 'after', event: 'UPDATE' }],
|
|
59
|
+
['BeforeDelete', { phase: 'before', event: 'DELETE' }],
|
|
60
|
+
['OnDelete', { phase: 'on', event: 'DELETE' }],
|
|
61
|
+
['AfterDelete', { phase: 'after', event: 'DELETE' }],
|
|
62
|
+
]);
|
|
15
63
|
|
|
16
64
|
function line(sf: ts.SourceFile, pos: number): number {
|
|
17
65
|
return sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
@@ -27,6 +75,15 @@ function firstArg(d: ts.Decorator): ts.Expression | undefined {
|
|
|
27
75
|
const e = d.expression;
|
|
28
76
|
return ts.isCallExpression(e) ? e.arguments[0] : undefined;
|
|
29
77
|
}
|
|
78
|
+
function decoratorArguments(d: ts.Decorator): readonly ts.Expression[] | undefined {
|
|
79
|
+
return ts.isCallExpression(d.expression) ? d.expression.arguments : undefined;
|
|
80
|
+
}
|
|
81
|
+
function methodName(name: ts.PropertyName): string {
|
|
82
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
83
|
+
|| ts.isNumericLiteral(name)
|
|
84
|
+
? name.text
|
|
85
|
+
: name.getText();
|
|
86
|
+
}
|
|
30
87
|
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
31
88
|
let current = expression;
|
|
32
89
|
while (
|
|
@@ -77,8 +134,11 @@ function collectStringLookups(source: ts.SourceFile): StringLookups {
|
|
|
77
134
|
identifiers: new Map(),
|
|
78
135
|
enumMembers: new Map(),
|
|
79
136
|
objectProperties: new Map(),
|
|
137
|
+
capDecoratorNames: new Map(),
|
|
138
|
+
capDecoratorNamespaces: new Set(),
|
|
80
139
|
};
|
|
81
140
|
for (const statement of source.statements) {
|
|
141
|
+
collectCapDecoratorImports(statement, lookups);
|
|
82
142
|
if (ts.isEnumDeclaration(statement)) collectEnumMembers(statement, lookups);
|
|
83
143
|
if (!ts.isVariableStatement(statement)
|
|
84
144
|
|| !(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
@@ -91,8 +151,62 @@ function collectStringLookups(source: ts.SourceFile): StringLookups {
|
|
|
91
151
|
}
|
|
92
152
|
return lookups;
|
|
93
153
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
154
|
+
function collectCapDecoratorImports(
|
|
155
|
+
statement: ts.Statement,
|
|
156
|
+
lookups: StringLookups,
|
|
157
|
+
): void {
|
|
158
|
+
if (!ts.isImportDeclaration(statement)
|
|
159
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier)
|
|
160
|
+
|| statement.moduleSpecifier.text !== 'cds-routing-handlers') return;
|
|
161
|
+
const clause = statement.importClause;
|
|
162
|
+
if (!clause || clause.isTypeOnly) return;
|
|
163
|
+
const bindings = clause.namedBindings;
|
|
164
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
165
|
+
for (const element of bindings.elements) {
|
|
166
|
+
if (element.isTypeOnly) continue;
|
|
167
|
+
lookups.capDecoratorNames.set(
|
|
168
|
+
element.name.text,
|
|
169
|
+
element.propertyName?.text ?? element.name.text,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (bindings && ts.isNamespaceImport(bindings))
|
|
174
|
+
lookups.capDecoratorNamespaces.add(bindings.name.text);
|
|
175
|
+
}
|
|
176
|
+
function capDecoratorName(
|
|
177
|
+
decorator: ts.Decorator,
|
|
178
|
+
lookups: StringLookups,
|
|
179
|
+
): string | undefined {
|
|
180
|
+
const expression = ts.isCallExpression(decorator.expression)
|
|
181
|
+
? decorator.expression.expression
|
|
182
|
+
: decorator.expression;
|
|
183
|
+
if (ts.isIdentifier(expression))
|
|
184
|
+
return lookups.capDecoratorNames.get(expression.text);
|
|
185
|
+
if (ts.isPropertyAccessExpression(expression)
|
|
186
|
+
&& ts.isIdentifier(expression.expression)
|
|
187
|
+
&& lookups.capDecoratorNamespaces.has(expression.expression.text))
|
|
188
|
+
return expression.name.text;
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function unresolved(
|
|
192
|
+
rawExpression: string,
|
|
193
|
+
reason: string,
|
|
194
|
+
argumentExpression?: string,
|
|
195
|
+
): DecoratorResolution {
|
|
196
|
+
return {
|
|
197
|
+
rawExpression,
|
|
198
|
+
argumentExpression,
|
|
199
|
+
resolutionKind: 'unresolved',
|
|
200
|
+
unresolvedReason: reason,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function resolved(
|
|
204
|
+
rawExpression: string,
|
|
205
|
+
argumentExpression: string,
|
|
206
|
+
resolvedValue: string,
|
|
207
|
+
resolutionKind: ResolvedArgumentKind,
|
|
208
|
+
): DecoratorResolution {
|
|
209
|
+
return { rawExpression, argumentExpression, resolvedValue, resolutionKind };
|
|
96
210
|
}
|
|
97
211
|
function generatedConstant(rawExpression: string): string | undefined {
|
|
98
212
|
const match = /(?:^|\.)(Action[A-Z][\w$]*|Func[A-Z][\w$]*)\.name$/
|
|
@@ -104,76 +218,296 @@ function generatedConstant(rawExpression: string): string | undefined {
|
|
|
104
218
|
function resolveDecoratorArgument(
|
|
105
219
|
argument: ts.Expression | undefined,
|
|
106
220
|
lookups: StringLookups,
|
|
221
|
+
rawExpression: string,
|
|
107
222
|
): DecoratorResolution {
|
|
108
|
-
if (!argument) return unresolved(
|
|
109
|
-
const
|
|
223
|
+
if (!argument) return unresolved(rawExpression, 'decorator_argument_missing');
|
|
224
|
+
const argumentExpression = argument.getText();
|
|
110
225
|
const expression = unwrapExpression(argument);
|
|
111
226
|
if (ts.isStringLiteralLike(expression))
|
|
112
|
-
return
|
|
227
|
+
return resolved(rawExpression, argumentExpression, expression.text, 'literal');
|
|
113
228
|
if (ts.isIdentifier(expression)) {
|
|
114
229
|
const value = lookups.identifiers.get(expression.text);
|
|
115
230
|
return value === undefined
|
|
116
|
-
? unresolved(rawExpression, 'identifier_not_resolved_to_local_const_string')
|
|
117
|
-
:
|
|
231
|
+
? unresolved(rawExpression, 'identifier_not_resolved_to_local_const_string', argumentExpression)
|
|
232
|
+
: resolved(rawExpression, argumentExpression, value, 'const_identifier');
|
|
118
233
|
}
|
|
119
234
|
if (ts.isPropertyAccessExpression(expression)
|
|
120
235
|
&& ts.isIdentifier(expression.expression)) {
|
|
121
236
|
const key = `${expression.expression.text}.${expression.name.text}`;
|
|
122
237
|
const enumValue = lookups.enumMembers.get(key);
|
|
123
238
|
if (enumValue !== undefined)
|
|
124
|
-
return
|
|
239
|
+
return resolved(rawExpression, argumentExpression, enumValue, 'enum_member');
|
|
125
240
|
const objectValue = lookups.objectProperties.get(key);
|
|
126
241
|
if (objectValue !== undefined)
|
|
127
|
-
return
|
|
242
|
+
return resolved(rawExpression, argumentExpression, objectValue, 'const_object_property');
|
|
128
243
|
}
|
|
129
|
-
const generatedValue = generatedConstant(
|
|
244
|
+
const generatedValue = generatedConstant(argumentExpression);
|
|
130
245
|
if (generatedValue !== undefined)
|
|
131
|
-
return
|
|
246
|
+
return resolved(
|
|
132
247
|
rawExpression,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
248
|
+
argumentExpression,
|
|
249
|
+
generatedValue,
|
|
250
|
+
'generated_constant_name',
|
|
251
|
+
);
|
|
136
252
|
if (ts.isPropertyAccessExpression(expression))
|
|
137
|
-
return unresolved(
|
|
138
|
-
|
|
253
|
+
return unresolved(
|
|
254
|
+
rawExpression,
|
|
255
|
+
'property_access_not_resolved_to_local_string',
|
|
256
|
+
argumentExpression,
|
|
257
|
+
);
|
|
258
|
+
return unresolved(rawExpression, 'unsupported_decorator_expression', argumentExpression);
|
|
259
|
+
}
|
|
260
|
+
function classificationFor(name: string): MethodClassification | undefined {
|
|
261
|
+
if (OPERATION_DECORATORS.has(name))
|
|
262
|
+
return { handlerKind: 'operation', executable: true };
|
|
263
|
+
if (EVENT_DECORATORS.has(name))
|
|
264
|
+
return { handlerKind: 'event', executable: true };
|
|
265
|
+
const lifecycle = LIFECYCLE_DECORATORS.get(name);
|
|
266
|
+
if (!lifecycle) return undefined;
|
|
267
|
+
return {
|
|
268
|
+
handlerKind: 'entity_lifecycle',
|
|
269
|
+
executable: true,
|
|
270
|
+
lifecyclePhase: lifecycle.phase,
|
|
271
|
+
lifecycleEvent: lifecycle.event,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function lifecycleLikePhase(name: string): HandlerLifecyclePhase | undefined {
|
|
275
|
+
if (/^On[A-Z]/.test(name)) return 'on';
|
|
276
|
+
if (/^Before[A-Z]/.test(name)) return 'before';
|
|
277
|
+
if (/^After[A-Z]/.test(name)) return 'after';
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
function withClassification(
|
|
281
|
+
resolution: DecoratorResolution,
|
|
282
|
+
classification: MethodClassification,
|
|
283
|
+
): DecoratorResolution {
|
|
284
|
+
return {
|
|
285
|
+
...resolution,
|
|
286
|
+
handlerKind: classification.handlerKind,
|
|
287
|
+
executable: classification.executable,
|
|
288
|
+
lifecyclePhase: classification.lifecyclePhase,
|
|
289
|
+
lifecycleEvent: classification.lifecycleEvent,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function withDecoratorEvidence(
|
|
293
|
+
resolution: DecoratorResolution,
|
|
294
|
+
decorator: ts.Decorator,
|
|
295
|
+
resolvedDecoratorKind: string | undefined,
|
|
296
|
+
): DecoratorResolution {
|
|
297
|
+
return {
|
|
298
|
+
...resolution,
|
|
299
|
+
decoratorExpression: decorator.expression.getText(),
|
|
300
|
+
resolvedDecoratorKind,
|
|
301
|
+
decoratorImportSource: resolvedDecoratorKind
|
|
302
|
+
? 'cds-routing-handlers'
|
|
303
|
+
: undefined,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function lifecycleDecoratorResolution(
|
|
307
|
+
decorator: ts.Decorator,
|
|
308
|
+
lookups: StringLookups,
|
|
309
|
+
classification: MethodClassification,
|
|
310
|
+
): { classification: MethodClassification; resolution: DecoratorResolution } {
|
|
311
|
+
const rawExpression = decorator.expression.getText();
|
|
312
|
+
const args = decoratorArguments(decorator);
|
|
313
|
+
if (args?.length === 0)
|
|
314
|
+
return {
|
|
315
|
+
classification,
|
|
316
|
+
resolution: withClassification({
|
|
317
|
+
rawExpression,
|
|
318
|
+
resolutionKind: 'lifecycle_implicit',
|
|
319
|
+
}, classification),
|
|
320
|
+
};
|
|
321
|
+
const resolution = args?.length === 1
|
|
322
|
+
? resolveDecoratorArgument(args[0], lookups, rawExpression)
|
|
323
|
+
: unresolved(rawExpression, args ? 'unsupported_lifecycle_argument_count' : 'lifecycle_decorator_call_required');
|
|
324
|
+
const unsupported = { ...classification, handlerKind: 'unsupported_lifecycle', executable: false } as const;
|
|
325
|
+
const unsupportedResolution = args?.length === 1
|
|
326
|
+
? { ...resolution, unresolvedReason: 'lifecycle_decorator_arguments_not_supported' }
|
|
327
|
+
: resolution;
|
|
328
|
+
return {
|
|
329
|
+
classification: unsupported,
|
|
330
|
+
resolution: withClassification(unsupportedResolution, unsupported),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function unsupportedLifecycleResolution(
|
|
334
|
+
decorator: ts.Decorator,
|
|
335
|
+
phase: HandlerLifecyclePhase,
|
|
336
|
+
): { classification: MethodClassification; resolution: DecoratorResolution } {
|
|
337
|
+
const classification: MethodClassification = {
|
|
338
|
+
handlerKind: 'unsupported_lifecycle',
|
|
339
|
+
executable: false,
|
|
340
|
+
lifecyclePhase: phase,
|
|
341
|
+
};
|
|
342
|
+
const resolution = unresolved(
|
|
343
|
+
decorator.expression.getText(),
|
|
344
|
+
'lifecycle_decorator_not_allowlisted',
|
|
345
|
+
);
|
|
346
|
+
return { classification, resolution: withClassification(resolution, classification) };
|
|
347
|
+
}
|
|
348
|
+
function unsupportedDecoratorResolution(
|
|
349
|
+
decorator: ts.Decorator,
|
|
350
|
+
reason: string,
|
|
351
|
+
phase?: HandlerLifecyclePhase,
|
|
352
|
+
): { classification: MethodClassification; resolution: DecoratorResolution } {
|
|
353
|
+
const classification: MethodClassification = {
|
|
354
|
+
handlerKind: phase ? 'unsupported_lifecycle' : 'unsupported_decorator',
|
|
355
|
+
executable: false,
|
|
356
|
+
lifecyclePhase: phase,
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
classification,
|
|
360
|
+
resolution: withClassification(
|
|
361
|
+
unresolved(decorator.expression.getText(), reason),
|
|
362
|
+
classification,
|
|
363
|
+
),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function parseMethodDecorator(
|
|
367
|
+
decorator: ts.Decorator,
|
|
368
|
+
lookups: StringLookups,
|
|
369
|
+
handlerClass: boolean,
|
|
370
|
+
allowLifecycle: boolean,
|
|
371
|
+
): ParsedMethodDecorator | undefined {
|
|
372
|
+
const decoratorKind = callName(decorator);
|
|
373
|
+
const importedKind = capDecoratorName(decorator, lookups);
|
|
374
|
+
const resolvedKind = importedKind ?? decoratorKind;
|
|
375
|
+
const base = classificationFor(resolvedKind);
|
|
376
|
+
const phase = lifecycleLikePhase(resolvedKind);
|
|
377
|
+
const isLifecycle = base?.handlerKind === 'entity_lifecycle' || Boolean(phase && !base);
|
|
378
|
+
if (!handlerClass && isLifecycle) return undefined;
|
|
379
|
+
const parsed = isLifecycle && (!allowLifecycle || !importedKind)
|
|
380
|
+
? unsupportedDecoratorResolution(
|
|
381
|
+
decorator, 'lifecycle_decorator_import_not_supported', phase,
|
|
382
|
+
)
|
|
383
|
+
: base?.handlerKind === 'entity_lifecycle'
|
|
384
|
+
? lifecycleDecoratorResolution(decorator, lookups, base)
|
|
385
|
+
: phase && !base
|
|
386
|
+
? unsupportedLifecycleResolution(decorator, phase)
|
|
387
|
+
: !base && handlerClass
|
|
388
|
+
? unsupportedDecoratorResolution(decorator, 'decorator_not_allowlisted')
|
|
389
|
+
: {
|
|
390
|
+
classification: base,
|
|
391
|
+
resolution: resolveDecoratorArgument(
|
|
392
|
+
firstArg(decorator),
|
|
393
|
+
lookups,
|
|
394
|
+
firstArg(decorator)?.getText() ?? decorator.expression.getText(),
|
|
395
|
+
),
|
|
396
|
+
};
|
|
397
|
+
if (!parsed.classification) return undefined;
|
|
398
|
+
return {
|
|
399
|
+
classification: parsed.classification,
|
|
400
|
+
resolution: parsed.resolution,
|
|
401
|
+
decoratorKind,
|
|
402
|
+
importedKind,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function methodDecoratorFact(
|
|
406
|
+
method: ts.MethodDeclaration,
|
|
407
|
+
decorator: ts.Decorator,
|
|
408
|
+
lookups: StringLookups,
|
|
409
|
+
source: ts.SourceFile,
|
|
410
|
+
filePath: string,
|
|
411
|
+
handlerClass: boolean,
|
|
412
|
+
allowLifecycle: boolean,
|
|
413
|
+
): HandlerMethodFact | undefined {
|
|
414
|
+
const parsed = parseMethodDecorator(decorator, lookups, handlerClass, allowLifecycle);
|
|
415
|
+
if (!parsed) return undefined;
|
|
416
|
+
const classification = method.body
|
|
417
|
+
? parsed.classification
|
|
418
|
+
: { ...parsed.classification, executable: false };
|
|
419
|
+
const resolution = method.body
|
|
420
|
+
? parsed.resolution
|
|
421
|
+
: { ...parsed.resolution, unresolvedReason: 'handler_method_body_missing' };
|
|
422
|
+
const argumentExpression = firstArg(decorator)?.getText();
|
|
423
|
+
return {
|
|
424
|
+
methodName: methodName(method.name),
|
|
425
|
+
decoratorKind: parsed.decoratorKind,
|
|
426
|
+
decoratorValue: parsed.resolution.resolvedValue,
|
|
427
|
+
decoratorRawExpression: argumentExpression ?? decorator.expression.getText(),
|
|
428
|
+
handlerKind: classification.handlerKind,
|
|
429
|
+
executable: classification.executable,
|
|
430
|
+
lifecyclePhase: classification.lifecyclePhase,
|
|
431
|
+
lifecycleEvent: classification.lifecycleEvent,
|
|
432
|
+
decoratorResolution: withDecoratorEvidence(
|
|
433
|
+
withClassification(resolution, classification),
|
|
434
|
+
decorator,
|
|
435
|
+
parsed.importedKind,
|
|
436
|
+
),
|
|
437
|
+
sourceFile: normalizePath(filePath),
|
|
438
|
+
sourceLine: line(source, method.getStart()),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function unique(values: string[]): string[] {
|
|
442
|
+
return [...new Set(values)];
|
|
443
|
+
}
|
|
444
|
+
function parseClassMethods(
|
|
445
|
+
node: ts.ClassDeclaration,
|
|
446
|
+
lookups: StringLookups,
|
|
447
|
+
source: ts.SourceFile,
|
|
448
|
+
filePath: string,
|
|
449
|
+
handlerClass: boolean,
|
|
450
|
+
allowLifecycle: boolean,
|
|
451
|
+
): Pick<HandlerClassFact, 'methods' | 'observedDecoratorNames' | 'unsupportedDecoratorNames'> {
|
|
452
|
+
const methods: HandlerMethodFact[] = [];
|
|
453
|
+
const observed: string[] = [];
|
|
454
|
+
const unsupported: string[] = [];
|
|
455
|
+
for (const method of node.members.filter(ts.isMethodDeclaration)) {
|
|
456
|
+
for (const decorator of decs(method)) {
|
|
457
|
+
observed.push(callName(decorator));
|
|
458
|
+
const fact = methodDecoratorFact(
|
|
459
|
+
method, decorator, lookups, source, filePath, handlerClass, allowLifecycle,
|
|
460
|
+
);
|
|
461
|
+
if (fact) methods.push(fact);
|
|
462
|
+
if (!fact?.executable) unsupported.push(callName(decorator));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
methods,
|
|
467
|
+
observedDecoratorNames: unique(observed),
|
|
468
|
+
unsupportedDecoratorNames: unique(unsupported),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function parseHandlerClass(
|
|
472
|
+
node: ts.ClassDeclaration,
|
|
473
|
+
lookups: StringLookups,
|
|
474
|
+
source: ts.SourceFile,
|
|
475
|
+
filePath: string,
|
|
476
|
+
): HandlerClassFact | undefined {
|
|
477
|
+
const classDecoratorNames = unique(decs(node).map(callName));
|
|
478
|
+
const classDecorators = decs(node);
|
|
479
|
+
const hasImportedHandler = classDecorators.some((decorator) =>
|
|
480
|
+
capDecoratorName(decorator, lookups) === 'Handler');
|
|
481
|
+
const hasHandlerDecorator = hasImportedHandler
|
|
482
|
+
|| classDecoratorNames.includes('Handler');
|
|
483
|
+
const parsed = parseClassMethods(
|
|
484
|
+
node, lookups, source, filePath, hasHandlerDecorator, hasImportedHandler,
|
|
485
|
+
);
|
|
486
|
+
if (!hasHandlerDecorator && parsed.methods.length === 0) return undefined;
|
|
487
|
+
return {
|
|
488
|
+
className: node.name?.text ?? 'AnonymousHandler',
|
|
489
|
+
sourceFile: normalizePath(filePath),
|
|
490
|
+
sourceLine: line(source, node.getStart()),
|
|
491
|
+
...parsed,
|
|
492
|
+
hasHandlerDecorator,
|
|
493
|
+
classDecoratorNames,
|
|
494
|
+
};
|
|
139
495
|
}
|
|
140
496
|
export async function parseDecorators(
|
|
141
497
|
repoPath: string,
|
|
142
|
-
filePath: string
|
|
498
|
+
filePath: string,
|
|
499
|
+
context?: RepositorySourceContext,
|
|
143
500
|
): Promise<HandlerClassFact[]> {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
501
|
+
const snapshot = context?.get(filePath);
|
|
502
|
+
const text = snapshot?.text
|
|
503
|
+
?? await fs.readFile(path.join(repoPath, filePath), 'utf8');
|
|
504
|
+
const sf = snapshot?.sourceFile() ?? createSourceFile(filePath, text);
|
|
146
505
|
const lookups = collectStringLookups(sf);
|
|
147
506
|
const handlers: HandlerClassFact[] = [];
|
|
148
507
|
function visit(node: ts.Node): void {
|
|
149
508
|
if (ts.isClassDeclaration(node)) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const methods = node.members.filter(ts.isMethodDeclaration).flatMap((m) =>
|
|
153
|
-
decs(m)
|
|
154
|
-
.filter((d) =>
|
|
155
|
-
['Func', 'Action', 'On', 'Event'].includes(callName(d))
|
|
156
|
-
)
|
|
157
|
-
.map((d) => {
|
|
158
|
-
const decoratorResolution = resolveDecoratorArgument(firstArg(d), lookups);
|
|
159
|
-
return {
|
|
160
|
-
methodName: m.name.getText(),
|
|
161
|
-
decoratorKind: callName(d),
|
|
162
|
-
decoratorValue: decoratorResolution.resolvedValue,
|
|
163
|
-
decoratorRawExpression: decoratorResolution.rawExpression,
|
|
164
|
-
decoratorResolution,
|
|
165
|
-
sourceFile: normalizePath(filePath),
|
|
166
|
-
sourceLine: line(sf, m.getStart())
|
|
167
|
-
};
|
|
168
|
-
})
|
|
169
|
-
);
|
|
170
|
-
if (hasHandler || methods.length > 0)
|
|
171
|
-
handlers.push({
|
|
172
|
-
className,
|
|
173
|
-
sourceFile: normalizePath(filePath),
|
|
174
|
-
sourceLine: line(sf, node.getStart()),
|
|
175
|
-
methods
|
|
176
|
-
});
|
|
509
|
+
const handler = parseHandlerClass(node, lookups, sf, filePath);
|
|
510
|
+
if (handler) handlers.push(handler);
|
|
177
511
|
}
|
|
178
512
|
ts.forEachChild(node, visit);
|
|
179
513
|
}
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import ts from 'typescript';
|
|
5
5
|
import type { HandlerRegistrationFact } from '../types.js';
|
|
6
6
|
import { normalizePath } from '../utils/path-utils.js';
|
|
7
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
7
8
|
|
|
8
9
|
interface ImportEvidence { importedName: string; source: string }
|
|
9
10
|
interface ClassEvidence { className: string; importSource?: string }
|
|
@@ -26,16 +27,26 @@ function importSourceFor(identifier: string, imports: Map<string, ImportEvidence
|
|
|
26
27
|
return evidence ? `${evidence.source}#${evidence.importedName}` : undefined;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
export async function parseHandlerRegistrations(
|
|
30
|
+
export async function parseHandlerRegistrations(
|
|
31
|
+
repoPath: string,
|
|
32
|
+
filePath: string,
|
|
33
|
+
context?: RepositorySourceContext,
|
|
34
|
+
): Promise<HandlerRegistrationFact[]> {
|
|
30
35
|
const absolutePath = path.join(repoPath, filePath);
|
|
31
|
-
const
|
|
32
|
-
const
|
|
36
|
+
const snapshot = context?.get(filePath);
|
|
37
|
+
const text = snapshot?.text ?? await fs.readFile(absolutePath, 'utf8');
|
|
38
|
+
const sourceFile = snapshot?.sourceFile() ?? ts.createSourceFile(
|
|
39
|
+
filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS,
|
|
40
|
+
);
|
|
33
41
|
const imports = collectImports(sourceFile);
|
|
34
|
-
const localArrays = collectLocalArrays(
|
|
42
|
+
const localArrays = collectLocalArrays(
|
|
43
|
+
sourceFile, imports, new Map(), repoPath, filePath, context,
|
|
44
|
+
);
|
|
35
45
|
const out: HandlerRegistrationFact[] = [];
|
|
36
|
-
|
|
37
46
|
function emitFromExpression(expression: ts.Expression, call: ts.CallExpression): void {
|
|
38
|
-
const classes = resolveArrayExpression(
|
|
47
|
+
const classes = resolveArrayExpression(
|
|
48
|
+
expression, localArrays, imports, repoPath, filePath, new Set(), context,
|
|
49
|
+
);
|
|
39
50
|
for (const cls of classes) {
|
|
40
51
|
out.push({
|
|
41
52
|
className: cls.className,
|
|
@@ -55,7 +66,6 @@ export async function parseHandlerRegistrations(repoPath: string, filePath: stri
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
|
-
|
|
59
69
|
function visit(node: ts.Node): void {
|
|
60
70
|
if (ts.isCallExpression(node) && isRegistrationCall(node)) {
|
|
61
71
|
const handlerExpr = handlerExpression(node, sourceFile);
|
|
@@ -98,45 +108,47 @@ function collectImports(sourceFile: ts.SourceFile): Map<string, ImportEvidence>
|
|
|
98
108
|
}
|
|
99
109
|
return imports;
|
|
100
110
|
}
|
|
101
|
-
function collectLocalArrays(sourceFile: ts.SourceFile, imports: Map<string, ImportEvidence>, seed: Map<string, ClassEvidence[]>, repoPath = '', fromFile = ''): Map<string, ClassEvidence[]> {
|
|
111
|
+
function collectLocalArrays(sourceFile: ts.SourceFile, imports: Map<string, ImportEvidence>, seed: Map<string, ClassEvidence[]>, repoPath = '', fromFile = '', context?: RepositorySourceContext): Map<string, ClassEvidence[]> {
|
|
102
112
|
const arrays = new Map(seed);
|
|
103
113
|
for (const statement of sourceFile.statements) {
|
|
104
114
|
if (ts.isVariableStatement(statement)) {
|
|
105
115
|
for (const decl of statement.declarationList.declarations) {
|
|
106
116
|
if (ts.isIdentifier(decl.name) && decl.initializer && ts.isArrayLiteralExpression(decl.initializer)) {
|
|
107
|
-
arrays.set(decl.name.text, resolveArrayLiteral(
|
|
117
|
+
arrays.set(decl.name.text, resolveArrayLiteral(
|
|
118
|
+
decl.initializer, arrays, imports, repoPath, fromFile, new Set(), context,
|
|
119
|
+
));
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
124
|
return arrays;
|
|
113
125
|
}
|
|
114
|
-
function resolveArrayExpression(expr: ts.Expression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string
|
|
115
|
-
if (ts.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen);
|
|
126
|
+
function resolveArrayExpression(expr: ts.Expression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string>, context?: RepositorySourceContext): ClassEvidence[] {
|
|
127
|
+
if (ts.isArrayLiteralExpression(expr)) return resolveArrayLiteral(expr, arrays, imports, repoPath, fromFile, seen, context);
|
|
116
128
|
if (ts.isIdentifier(expr)) {
|
|
117
129
|
const local = arrays.get(expr.text);
|
|
118
130
|
if (local) return local;
|
|
119
131
|
const evidence = imports.get(expr.text);
|
|
120
|
-
if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen);
|
|
132
|
+
if (evidence && isRelative(evidence.source)) return resolveImportedArray(repoPath, fromFile, evidence, seen, context);
|
|
121
133
|
if (evidence) return [{ className: evidence.importedName === 'default' ? expr.text : evidence.importedName, importSource: `${evidence.source}#${evidence.importedName}` }];
|
|
122
134
|
}
|
|
123
135
|
return [];
|
|
124
136
|
}
|
|
125
|
-
function resolveArrayLiteral(array: ts.ArrayLiteralExpression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string
|
|
137
|
+
function resolveArrayLiteral(array: ts.ArrayLiteralExpression, arrays: Map<string, ClassEvidence[]>, imports: Map<string, ImportEvidence>, repoPath: string, fromFile: string, seen: Set<string>, context?: RepositorySourceContext): ClassEvidence[] {
|
|
126
138
|
const out: ClassEvidence[] = [];
|
|
127
139
|
for (const element of array.elements) {
|
|
128
|
-
if (ts.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen));
|
|
140
|
+
if (ts.isSpreadElement(element)) out.push(...resolveArrayExpression(element.expression, arrays, imports, repoPath, fromFile, seen, context));
|
|
129
141
|
else if (ts.isIdentifier(element)) out.push({ className: element.text, importSource: importSourceFor(element.text, imports) });
|
|
130
142
|
}
|
|
131
143
|
return out;
|
|
132
144
|
}
|
|
133
|
-
function resolveImportedArray(repoPath: string, fromFile: string, evidence: ImportEvidence, seen: Set<string
|
|
145
|
+
function resolveImportedArray(repoPath: string, fromFile: string, evidence: ImportEvidence, seen: Set<string>, context?: RepositorySourceContext): ClassEvidence[] {
|
|
134
146
|
const moduleFile = resolveRelativeModule(repoPath, fromFile, evidence.source);
|
|
135
147
|
if (!moduleFile) return [];
|
|
136
148
|
const key = `${moduleFile}:${evidence.importedName}`;
|
|
137
149
|
if (seen.has(key) || seen.size > MAX_EXPORT_DEPTH) return [];
|
|
138
150
|
seen.add(key);
|
|
139
|
-
const exports = readExports(repoPath, moduleFile, seen);
|
|
151
|
+
const exports = readExports(repoPath, moduleFile, seen, context);
|
|
140
152
|
if (evidence.importedName === 'default') return exports.defaultArray ?? [];
|
|
141
153
|
return exports.arrays.get(evidence.importedName) ?? exports.arrays.get(exports.aliases.get(evidence.importedName) ?? evidence.importedName) ?? [];
|
|
142
154
|
}
|
|
@@ -150,13 +162,16 @@ function resolveRelativeModule(repoPath: string, fromFile: string, specifier: st
|
|
|
150
162
|
}
|
|
151
163
|
return undefined;
|
|
152
164
|
}
|
|
153
|
-
function readExports(repoPath: string, filePath: string, seen: Set<string
|
|
165
|
+
function readExports(repoPath: string, filePath: string, seen: Set<string>, context?: RepositorySourceContext): FileExports {
|
|
154
166
|
const absolute = path.join(repoPath, filePath);
|
|
155
167
|
let text: string;
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
const snapshot = context?.get(filePath);
|
|
169
|
+
try { text = snapshot?.text ?? fsSync.readFileSync(absolute, 'utf8'); } catch { return { arrays: new Map(), aliases: new Map() }; }
|
|
170
|
+
const sourceFile = snapshot?.sourceFile() ?? ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
158
171
|
const imports = collectImports(sourceFile);
|
|
159
|
-
const arrays = collectLocalArrays(
|
|
172
|
+
const arrays = collectLocalArrays(
|
|
173
|
+
sourceFile, imports, new Map(), repoPath, filePath, context,
|
|
174
|
+
);
|
|
160
175
|
const aliases = new Map<string, string>();
|
|
161
176
|
let defaultArray: ClassEvidence[] | undefined;
|
|
162
177
|
for (const statement of sourceFile.statements) {
|
|
@@ -167,7 +182,9 @@ function readExports(repoPath: string, filePath: string, seen: Set<string>): Fil
|
|
|
167
182
|
const local = element.propertyName?.text ?? element.name.text;
|
|
168
183
|
aliases.set(element.name.text, local);
|
|
169
184
|
if (module && isRelative(module)) {
|
|
170
|
-
const imported = resolveImportedArray(
|
|
185
|
+
const imported = resolveImportedArray(
|
|
186
|
+
repoPath, filePath, { source: module, importedName: local }, seen, context,
|
|
187
|
+
);
|
|
171
188
|
if (imported.length > 0) arrays.set(element.name.text, imported);
|
|
172
189
|
}
|
|
173
190
|
}
|