@saptools/service-flow 0.1.50 → 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 +14 -0
- package/README.md +16 -3
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-52OUS3MO.js → chunk-PTLDSHRC.js} +2663 -363
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +318 -510
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +59 -17
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +97 -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/index.ts +1 -1
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +22 -2
- package/src/linker/service-resolver.ts +15 -4
- package/src/output/table-output.ts +56 -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-branches.ts +45 -0
- package/src/trace/dynamic-targets.ts +654 -0
- package/src/trace/evidence.ts +391 -22
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +101 -94
- package/src/types.ts +39 -1
- package/dist/chunk-52OUS3MO.js.map +0 -1
|
@@ -24,12 +24,36 @@ export function renderTraceTable(result: TraceResult): string {
|
|
|
24
24
|
|
|
25
25
|
function diagnosticLines(diagnostic: Record<string, unknown>): string[] {
|
|
26
26
|
const first = `${String(diagnostic.severity ?? 'info')} ${String(diagnostic.code ?? 'diagnostic')} ${String(diagnostic.message ?? '')}`;
|
|
27
|
-
|
|
27
|
+
const details = diagnosticDetailLines(diagnostic);
|
|
28
|
+
return [first, ...[...details, ...hintLines(diagnostic)]
|
|
29
|
+
.map((hint) => ` ${hint}`)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function diagnosticDetailLines(diagnostic: Record<string, unknown>): string[] {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
if (diagnostic.sourceFile || diagnostic.sourceLine)
|
|
35
|
+
lines.push(`at ${String(diagnostic.sourceFile ?? '')}:${String(diagnostic.sourceLine ?? '')}`);
|
|
36
|
+
const unsupported = stringList(diagnostic.unsupportedDecoratorNames);
|
|
37
|
+
const observed = stringList(diagnostic.observedDecoratorNames);
|
|
38
|
+
if (unsupported.length > 0)
|
|
39
|
+
lines.push(`unsupported decorators: ${unsupported.join(', ')}`);
|
|
40
|
+
else if (observed.length > 0)
|
|
41
|
+
lines.push(`observed decorators: ${observed.join(', ')}`);
|
|
42
|
+
if (typeof diagnostic.remediation === 'string')
|
|
43
|
+
lines.push(`hint: ${diagnostic.remediation}`);
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stringList(value: unknown): string[] {
|
|
48
|
+
return Array.isArray(value)
|
|
49
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
50
|
+
: [];
|
|
28
51
|
}
|
|
29
52
|
|
|
30
53
|
function hintLines(evidence: Record<string, unknown>): string[] {
|
|
54
|
+
const dynamicLines = dynamicHintLines(evidence);
|
|
31
55
|
const suggestions = evidence.implementationHintSuggestions;
|
|
32
|
-
if (!Array.isArray(suggestions)) return
|
|
56
|
+
if (!Array.isArray(suggestions)) return dynamicLines;
|
|
33
57
|
const hints = suggestions.flatMap((item) =>
|
|
34
58
|
isRecord(item) && typeof item.cli === 'string'
|
|
35
59
|
? [item.cli]
|
|
@@ -38,9 +62,38 @@ function hintLines(evidence: Record<string, unknown>): string[] {
|
|
|
38
62
|
const shown = unique.slice(0, 3).map((hint) => `try ${hint}`);
|
|
39
63
|
if (unique.length > shown.length)
|
|
40
64
|
shown.push(`... ${unique.length - shown.length} more hint(s) available in JSON`);
|
|
41
|
-
return shown;
|
|
65
|
+
return [...dynamicLines, ...shown];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dynamicHintLines(evidence: Record<string, unknown>): string[] {
|
|
69
|
+
const exploration = isRecord(evidence.dynamicTargetExploration)
|
|
70
|
+
? evidence.dynamicTargetExploration
|
|
71
|
+
: evidence;
|
|
72
|
+
const count = numberValue(exploration.candidateCount);
|
|
73
|
+
if (count === 0) return [];
|
|
74
|
+
const shown = numberValue(exploration.shownCandidateCount);
|
|
75
|
+
const omitted = numberValue(exploration.omittedCandidateCount);
|
|
76
|
+
const rejected = numberValue(exploration.rejectedCandidateCount);
|
|
77
|
+
const lines = [
|
|
78
|
+
`viable candidates: ${shown} shown, ${omitted} omitted; rejected: ${rejected}`,
|
|
79
|
+
];
|
|
80
|
+
lines.push(...varSetHints(exploration.suggestedVarSets));
|
|
81
|
+
if (omitted > 0 || rejected > 0 || shown < count)
|
|
82
|
+
lines.push('use --dynamic-mode candidates --max-dynamic-candidates 20 to explore candidate branches');
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function varSetHints(value: unknown): string[] {
|
|
87
|
+
if (!Array.isArray(value)) return [];
|
|
88
|
+
const hints = value.flatMap((item) =>
|
|
89
|
+
isRecord(item) && typeof item.cli === 'string' ? [`try ${item.cli}`] : []);
|
|
90
|
+
return [...new Set(hints)].slice(0, 3);
|
|
42
91
|
}
|
|
43
92
|
|
|
44
93
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
94
|
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
46
95
|
}
|
|
96
|
+
|
|
97
|
+
function numberValue(value: unknown): number {
|
|
98
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
99
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import type { CdsOperationFact, CdsServiceFact } from '../types.js';
|
|
4
4
|
import { ensureLeadingSlash, normalizePath } from '../utils/path-utils.js';
|
|
5
|
+
import type { RepositorySourceContext } from './ts-project.js';
|
|
5
6
|
|
|
6
7
|
function lineOf(text: string, index: number): number {
|
|
7
8
|
return text.slice(0, index).split('\n').length;
|
|
@@ -136,9 +137,14 @@ function operationsFromBody(text: string, maskedBody: string, bodyOffset: number
|
|
|
136
137
|
}));
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
export async function parseCdsFile(
|
|
140
|
+
export async function parseCdsFile(
|
|
141
|
+
repoPath: string,
|
|
142
|
+
filePath: string,
|
|
143
|
+
context?: RepositorySourceContext,
|
|
144
|
+
): Promise<CdsServiceFact[]> {
|
|
140
145
|
const absolute = path.join(repoPath, filePath);
|
|
141
|
-
const text =
|
|
146
|
+
const text = context?.get(filePath)?.text
|
|
147
|
+
?? await fs.readFile(absolute, 'utf8');
|
|
142
148
|
const masked = maskCommentsAndStrings(text);
|
|
143
149
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
144
150
|
const services: CdsServiceFact[] = [];
|
|
@@ -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
|
}
|