@spine-event-engine/proto-tools 2.0.0-snapshot.2 → 2.0.0-snapshot.20
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/README.md +54 -46
- package/REFERENCE.md +26 -4
- package/dist/src/generation/build-time-handler-analyzer.d.ts +217 -0
- package/dist/src/generation/build-time-handler-analyzer.d.ts.map +1 -0
- package/dist/src/generation/build-time-handler-analyzer.js +2806 -0
- package/dist/src/generation/build-time-handler-analyzer.js.map +1 -0
- package/dist/src/generation/entity-query-generator.d.ts +19 -0
- package/dist/src/generation/entity-query-generator.d.ts.map +1 -0
- package/dist/src/generation/entity-query-generator.js +298 -0
- package/dist/src/generation/entity-query-generator.js.map +1 -0
- package/dist/src/generation/generated-registry-writer.d.ts +58 -0
- package/dist/src/generation/generated-registry-writer.d.ts.map +1 -0
- package/dist/src/generation/generated-registry-writer.js +496 -0
- package/dist/src/generation/generated-registry-writer.js.map +1 -0
- package/dist/src/generation/generation-reuse.mjs +24 -2
- package/dist/src/generation/generator.d.ts.map +1 -1
- package/dist/src/generation/generator.js +195 -24
- package/dist/src/generation/generator.js.map +1 -1
- package/dist/src/generation/handler-codegen.d.ts +48 -0
- package/dist/src/generation/handler-codegen.d.ts.map +1 -0
- package/dist/src/generation/handler-codegen.js +214 -0
- package/dist/src/generation/handler-codegen.js.map +1 -0
- package/dist/src/generation/handler-generator.js +1 -1
- package/dist/src/generation/handler-generator.js.map +1 -1
- package/dist/src/generation/rejection-generator.d.ts +1 -0
- package/dist/src/generation/rejection-generator.d.ts.map +1 -1
- package/dist/src/generation/rejection-generator.js +14 -11
- package/dist/src/generation/rejection-generator.js.map +1 -1
- package/dist/src/testing/index.d.ts +8 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +20 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -6
|
@@ -0,0 +1,2806 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { Buffer } from "node:buffer";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
18
|
+
import ts from "typescript";
|
|
19
|
+
/**
|
|
20
|
+
* Provides build-time analysis for bare Spine handler decorators.
|
|
21
|
+
*/
|
|
22
|
+
export const BuildHandlerAnalyzer = Object.freeze({
|
|
23
|
+
// prettier-ignore
|
|
24
|
+
/**
|
|
25
|
+
* Inspects source files and returns receiver handler records with deterministic diagnostics.
|
|
26
|
+
*
|
|
27
|
+
* @param program TypeScript program that owns the source files and diagnostics.
|
|
28
|
+
* @param sourceFiles Application source files to inspect; defaults to program files.
|
|
29
|
+
* @returns Receiver handler records and deterministic diagnostics.
|
|
30
|
+
*/
|
|
31
|
+
analyze(program, sourceFiles = HandlerSources.appSourceFiles(program)) {
|
|
32
|
+
PackageDependencies.load(program);
|
|
33
|
+
const receivers = [];
|
|
34
|
+
const diagnostics = [];
|
|
35
|
+
for (const source of sourceFiles) {
|
|
36
|
+
const syntaxDiagnostics = program.getSyntacticDiagnostics(source);
|
|
37
|
+
if (syntaxDiagnostics.length > 0) {
|
|
38
|
+
diagnostics.push(...syntaxDiagnostics.map((diagnostic) => HandlerTypes.syntaxDiagnostic(source, diagnostic)));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const scope = {
|
|
42
|
+
program,
|
|
43
|
+
source,
|
|
44
|
+
imports: HandlerSources.buildImportState(source, program),
|
|
45
|
+
diagnostics,
|
|
46
|
+
};
|
|
47
|
+
const sourceReceivers = HandlerSources.analyzeSource(scope);
|
|
48
|
+
receivers.push(...sourceReceivers);
|
|
49
|
+
}
|
|
50
|
+
return { receivers, diagnostics };
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const handlerDecorators = new Set(["Assign", "Command", "React", "Subscribe"]);
|
|
54
|
+
const entityBaseNames = new Set(["Aggregate", "Projection", "ProcessManager"]);
|
|
55
|
+
const standaloneBaseNames = new Set([
|
|
56
|
+
"AbstractAssignee",
|
|
57
|
+
"AbstractCommander",
|
|
58
|
+
"AbstractEventReactor",
|
|
59
|
+
"AbstractEventSubscriber",
|
|
60
|
+
]);
|
|
61
|
+
const maxAliasDepth = 50;
|
|
62
|
+
// `spine.options.entity` in the frozen `spine/options.proto` contract.
|
|
63
|
+
const entityOptionFieldNumber = 73903;
|
|
64
|
+
let packageDependencies;
|
|
65
|
+
const HandlerSources = Object.freeze({
|
|
66
|
+
/**
|
|
67
|
+
* Gets non-declaration source files from the program.
|
|
68
|
+
*
|
|
69
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
70
|
+
* @returns The program's source files, excluding declaration files.
|
|
71
|
+
*/
|
|
72
|
+
appSourceFiles(program) {
|
|
73
|
+
return program.getSourceFiles().filter((source) => !source.isDeclarationFile);
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Collects supported receiver classes declared in one source file.
|
|
77
|
+
*
|
|
78
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
79
|
+
* @returns The receiver records found in the source file.
|
|
80
|
+
*/
|
|
81
|
+
analyzeSource(scope) {
|
|
82
|
+
const receivers = [];
|
|
83
|
+
for (const statement of scope.source.statements) {
|
|
84
|
+
if (!ts.isClassDeclaration(statement) ||
|
|
85
|
+
(statement.name === undefined &&
|
|
86
|
+
!HandlerTypes.hasModifier(statement, ts.SyntaxKind.DefaultKeyword))) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const receiver = HandlerSources.analyzeClass(statement, scope);
|
|
90
|
+
if (receiver !== undefined) {
|
|
91
|
+
receivers.push(receiver);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return receivers;
|
|
95
|
+
},
|
|
96
|
+
/**
|
|
97
|
+
* Validates a decorated class and assembles its receiver record.
|
|
98
|
+
*
|
|
99
|
+
* @param node The syntax node being inspected.
|
|
100
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
101
|
+
* @returns The receiver record, or undefined when the class is ineligible.
|
|
102
|
+
*/
|
|
103
|
+
analyzeClass(node, scope) {
|
|
104
|
+
const className = node.name?.text ?? "DefaultReceiver";
|
|
105
|
+
const exportIssue = HandlerSources.receiverExportIssue(node, scope.source);
|
|
106
|
+
if (HandlerSources.hasDecoratedMethod(node, scope.imports) && exportIssue !== undefined) {
|
|
107
|
+
HandlerTypes.pushDiagnostic(scope, exportIssue.code, node.name ?? node, exportIssue.message, className);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const lineage = HandlerSources.receiverLineage(node, scope);
|
|
111
|
+
const handlers = HandlerSources.analyzeMethods(node, className, lineage, scope);
|
|
112
|
+
if (lineage === undefined || handlers.length === 0) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return HandlerSources.receiverRecord(node, className, lineage, handlers, scope.source.fileName);
|
|
116
|
+
},
|
|
117
|
+
/**
|
|
118
|
+
* Collects valid decorated handler methods from a class.
|
|
119
|
+
*
|
|
120
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
121
|
+
* @param lineage The resolved receiver base and state schema, when available.
|
|
122
|
+
* @param node The syntax node being inspected.
|
|
123
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
124
|
+
* @returns The valid handler records found in the class.
|
|
125
|
+
*/
|
|
126
|
+
analyzeMethods(node, className, lineage, scope) {
|
|
127
|
+
const handlers = [];
|
|
128
|
+
for (const member of node.members) {
|
|
129
|
+
if (!ts.isMethodDeclaration(member))
|
|
130
|
+
continue;
|
|
131
|
+
const handler = HandlerSources.analyzeMethod(member, className, lineage?.base, lineage?.stateSchema, lineage?.receiverKind, scope);
|
|
132
|
+
if (handler !== undefined)
|
|
133
|
+
handlers.push(handler);
|
|
134
|
+
}
|
|
135
|
+
return handlers;
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* Builds the entity or standalone receiver record from its lineage and handlers.
|
|
139
|
+
*
|
|
140
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
141
|
+
* @param handlers The validated handlers found on the receiver.
|
|
142
|
+
* @param lineage The resolved receiver base and state schema, when available.
|
|
143
|
+
* @param node The syntax node being inspected.
|
|
144
|
+
* @param sourceFile The receiver source path.
|
|
145
|
+
* @returns The receiver record, or undefined when entity state is unresolved.
|
|
146
|
+
*/
|
|
147
|
+
receiverRecord(node, className, lineage, handlers, sourceFile) {
|
|
148
|
+
return lineage.receiverKind === "entity"
|
|
149
|
+
? lineage.stateSchema === undefined
|
|
150
|
+
? undefined
|
|
151
|
+
: {
|
|
152
|
+
receiverKind: "entity",
|
|
153
|
+
className,
|
|
154
|
+
...(HandlerTypes.hasModifier(node, ts.SyntaxKind.DefaultKeyword)
|
|
155
|
+
? { defaultExport: true }
|
|
156
|
+
: {}),
|
|
157
|
+
sourceFile,
|
|
158
|
+
stateSchema: lineage.stateSchema,
|
|
159
|
+
handlers,
|
|
160
|
+
}
|
|
161
|
+
: {
|
|
162
|
+
receiverKind: "standalone",
|
|
163
|
+
className,
|
|
164
|
+
...(HandlerTypes.hasModifier(node, ts.SyntaxKind.DefaultKeyword)
|
|
165
|
+
? { defaultExport: true }
|
|
166
|
+
: {}),
|
|
167
|
+
sourceFile,
|
|
168
|
+
handlers,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Finds a handler decorator and validates one method.
|
|
173
|
+
*
|
|
174
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
175
|
+
* @param entityBase The resolved entity base class, when present.
|
|
176
|
+
* @param node The syntax node being inspected.
|
|
177
|
+
* @param receiverKind The resolved entity or standalone receiver kind, when present.
|
|
178
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
179
|
+
* @param stateSchema The resolved entity state schema, when present.
|
|
180
|
+
* @returns The handler record, or undefined when validation fails.
|
|
181
|
+
*/
|
|
182
|
+
analyzeMethod(node, className, entityBase, stateSchema, receiverKind, scope) {
|
|
183
|
+
const decorators = HandlerSources.methodDecorators(node, scope.imports);
|
|
184
|
+
const handler = decorators.find(HandlerSources.isHandlerUse);
|
|
185
|
+
const whereUses = decorators.filter((decorator) => decorator.name === "Where");
|
|
186
|
+
const throwsUses = decorators.filter((decorator) => decorator.name === "Throws");
|
|
187
|
+
const method = HandlerTypes.methodName(node);
|
|
188
|
+
const use = {
|
|
189
|
+
node,
|
|
190
|
+
handler,
|
|
191
|
+
whereUses,
|
|
192
|
+
throwsUses,
|
|
193
|
+
className,
|
|
194
|
+
entityBase,
|
|
195
|
+
stateSchema,
|
|
196
|
+
receiverKind,
|
|
197
|
+
scope,
|
|
198
|
+
method,
|
|
199
|
+
};
|
|
200
|
+
if (!HandlerSources.validMethodUse(use))
|
|
201
|
+
return undefined;
|
|
202
|
+
return HandlerSources.analyzeValidMethod(use);
|
|
203
|
+
},
|
|
204
|
+
/**
|
|
205
|
+
* Resolves a validated method's input, context, outcomes, and declarations.
|
|
206
|
+
*
|
|
207
|
+
* @param input The collected method or validation input.
|
|
208
|
+
* @returns The handler record, or undefined when a declaration is invalid.
|
|
209
|
+
*/
|
|
210
|
+
analyzeValidMethod(input) {
|
|
211
|
+
const { className, handler, method, node, scope } = input;
|
|
212
|
+
const parameters = node.parameters;
|
|
213
|
+
const origin = HandlerSources.externalOrigin(parameters, scope, className, method);
|
|
214
|
+
if (origin === undefined)
|
|
215
|
+
return undefined;
|
|
216
|
+
const schemas = HandlerSources.methodSchemaUses(node, handler, origin.type, scope);
|
|
217
|
+
if (schemas === undefined || method === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
const { signal, returnedSchemas } = schemas;
|
|
220
|
+
if (!HandlerSources.validContextParameter(parameters, signal.kind, scope, className, method))
|
|
221
|
+
return undefined;
|
|
222
|
+
const declarations = HandlerSources.methodDeclarations(input, signal.kind, method);
|
|
223
|
+
if (declarations === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
return HandlerSources.buildHandlerRecord(node, handler, signal, method, returnedSchemas, declarations.thrown, origin.value, declarations.where);
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* Validates the Where and Throws declarations attached to a handler.
|
|
229
|
+
*
|
|
230
|
+
* @param input The collected method or validation input.
|
|
231
|
+
* @param methodName The handler method name used in diagnostics.
|
|
232
|
+
* @param signalKind The resolved input signal kind.
|
|
233
|
+
* @returns The validated filter and rejection schemas, or undefined on failure.
|
|
234
|
+
*/
|
|
235
|
+
methodDeclarations(input, signalKind, methodName) {
|
|
236
|
+
const { className, handler, scope, throwsUses, whereUses } = input;
|
|
237
|
+
const where = HandlerSources.whereDeclaration(whereUses, handler, signalKind, scope, className, methodName);
|
|
238
|
+
if (whereUses.length > 0 && where === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
const thrown = HandlerSources.throwsDeclaration(throwsUses, handler, signalKind, scope, className, methodName);
|
|
241
|
+
return throwsUses.length > 0 && thrown === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: { where, thrown: thrown ?? [] };
|
|
244
|
+
},
|
|
245
|
+
/**
|
|
246
|
+
* Resolves the input signal and emitted message schemas of a handler.
|
|
247
|
+
*
|
|
248
|
+
* @param handler The recognized handler decorator.
|
|
249
|
+
* @param node The syntax node being inspected.
|
|
250
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
251
|
+
* @param signalType The handler's declared input type.
|
|
252
|
+
* @returns The input and output schemas, or undefined when resolution fails.
|
|
253
|
+
*/
|
|
254
|
+
methodSchemaUses(node, handler, signalType, scope) {
|
|
255
|
+
const signal = HandlerSources.schemaUseFromType(signalType, scope.imports);
|
|
256
|
+
const returned = HandlerSources.emittedSchemaUses(node.type === undefined ? undefined : HandlerSources.unwrapOuterPromise(node.type, scope), HandlerSources.handlerKind(handler.name, signal?.kind), scope);
|
|
257
|
+
return signal === undefined || returned === undefined
|
|
258
|
+
? undefined
|
|
259
|
+
: { signal, returnedSchemas: returned.map((schema) => schema.reference) };
|
|
260
|
+
},
|
|
261
|
+
/**
|
|
262
|
+
* Builds the generated handler record from resolved method metadata.
|
|
263
|
+
*
|
|
264
|
+
* @param handler The recognized handler decorator.
|
|
265
|
+
* @param method The handler method name.
|
|
266
|
+
* @param node The syntax node being inspected.
|
|
267
|
+
* @param origin Whether the input is domestic or external.
|
|
268
|
+
* @param returnedSchemas The generated schemas emitted by the handler.
|
|
269
|
+
* @param signal The resolved input schema and signal kind.
|
|
270
|
+
* @param thrownSchemas The rejection schemas declared by the handler.
|
|
271
|
+
* @param where The validated event filter, when declared.
|
|
272
|
+
* @returns The generated handler record.
|
|
273
|
+
*/
|
|
274
|
+
buildHandlerRecord(node, handler, signal, method, returnedSchemas, thrownSchemas, origin, where) {
|
|
275
|
+
return {
|
|
276
|
+
kind: HandlerSources.handlerKind(handler.name, signal.kind),
|
|
277
|
+
methodName: method,
|
|
278
|
+
input: { schema: signal.reference, origin, ...(where === undefined ? {} : { where }) },
|
|
279
|
+
outcomes: { returned: returnedSchemas, thrown: thrownSchemas },
|
|
280
|
+
parameterCount: node.parameters.length,
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
/**
|
|
284
|
+
* Rejects unsupported decorator combinations and receiver placements.
|
|
285
|
+
*
|
|
286
|
+
* @param input The collected method or validation input.
|
|
287
|
+
* @returns Whether the decorator combination and receiver placement are valid.
|
|
288
|
+
*/
|
|
289
|
+
validMethodUse(input) {
|
|
290
|
+
if (input.handler === undefined)
|
|
291
|
+
return HandlerSources.reportMissingHandler(input);
|
|
292
|
+
const { className, handler, method, node, scope } = input;
|
|
293
|
+
if (!HandlerSources.validHandlerKind({ ...input, handler }))
|
|
294
|
+
return false;
|
|
295
|
+
if (!HandlerSources.validBareHandlerDecorator(handler, scope, className, method))
|
|
296
|
+
return false;
|
|
297
|
+
return !HandlerSources.validateHandlerNode(node, handler.name, input.receiverKind === "entity" ? input.stateSchema : undefined, input.receiverKind !== "standalone", scope, className, method);
|
|
298
|
+
},
|
|
299
|
+
/**
|
|
300
|
+
* Resolves declared rejection schemas for a handler.
|
|
301
|
+
*
|
|
302
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
303
|
+
* @param handler The recognized handler decorator.
|
|
304
|
+
* @param methodName The handler method name used in diagnostics.
|
|
305
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
306
|
+
* @param signalKind The resolved input signal kind.
|
|
307
|
+
* @param uses The decorator uses under inspection.
|
|
308
|
+
* @returns The declared rejection schemas, or undefined on failure.
|
|
309
|
+
*/
|
|
310
|
+
throwsDeclaration(uses, handler, signalKind, scope, className, methodName) {
|
|
311
|
+
if (uses.length === 0)
|
|
312
|
+
return [];
|
|
313
|
+
const expression = HandlerSources.throwsCall(uses, handler, signalKind, scope, className, methodName);
|
|
314
|
+
if (expression === undefined)
|
|
315
|
+
return undefined;
|
|
316
|
+
return HandlerSources.thrownSchemas(expression, scope, className, methodName);
|
|
317
|
+
},
|
|
318
|
+
/**
|
|
319
|
+
* Reads rejection schema references from a Throws decorator call.
|
|
320
|
+
*
|
|
321
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
322
|
+
* @param handler The recognized handler decorator.
|
|
323
|
+
* @param methodName The handler method name used in diagnostics.
|
|
324
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
325
|
+
* @param signalKind The resolved input signal kind.
|
|
326
|
+
* @param uses The decorator uses under inspection.
|
|
327
|
+
* @returns The schema expressions in the decorator call, or undefined when invalid.
|
|
328
|
+
*/
|
|
329
|
+
throwsCall(uses, handler, signalKind, scope, className, methodName) {
|
|
330
|
+
const use = uses[0];
|
|
331
|
+
const commandReceptor = handler.name === "Assign" || (handler.name === "Command" && signalKind === "command");
|
|
332
|
+
if (uses.length !== 1 || !commandReceptor || use === undefined) {
|
|
333
|
+
HandlerSources.invalidThrows(scope, use?.node ?? handler.node, "@Throws is allowed once on a command-accepting @Assign or @Command handler.", className, methodName);
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const expression = use.node.expression;
|
|
337
|
+
if (ts.isCallExpression(expression) && expression.arguments.length > 0)
|
|
338
|
+
return expression;
|
|
339
|
+
HandlerSources.invalidThrows(scope, use.node, "@Throws requires at least one generated rejection companion.", className, methodName);
|
|
340
|
+
return undefined;
|
|
341
|
+
},
|
|
342
|
+
/**
|
|
343
|
+
* Resolves rejection schemas named by a Throws declaration.
|
|
344
|
+
*
|
|
345
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
346
|
+
* @param expression The expression being inspected.
|
|
347
|
+
* @param methodName The handler method name used in diagnostics.
|
|
348
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
349
|
+
* @returns The resolved rejection schemas, or undefined when invalid.
|
|
350
|
+
*/
|
|
351
|
+
thrownSchemas(expression, scope, className, methodName) {
|
|
352
|
+
const schemas = expression.arguments.map((argument) => HandlerSources.rejectionSchemaFromExpression(argument, scope.imports));
|
|
353
|
+
if (schemas.some((schema) => schema === undefined)) {
|
|
354
|
+
HandlerSources.invalidThrows(scope, expression, "@Throws accepts only generated rejection companions.", className, methodName);
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
const resolved = schemas;
|
|
358
|
+
const keys = resolved.map((schema) => `${schema.moduleSpecifier}\u0000${schema.exportName}`);
|
|
359
|
+
if (new Set(keys).size !== keys.length) {
|
|
360
|
+
HandlerSources.invalidThrows(scope, expression, "@Throws cannot declare the same rejection more than once.", className, methodName);
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
return resolved;
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* Records an invalid Throws declaration at its decorator.
|
|
367
|
+
*
|
|
368
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
369
|
+
* @param message The descriptor message or diagnostic text being inspected.
|
|
370
|
+
* @param methodName The handler method name used in diagnostics.
|
|
371
|
+
* @param node The syntax node being inspected.
|
|
372
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
373
|
+
*/
|
|
374
|
+
invalidThrows(scope, node, message, className, methodName) {
|
|
375
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_THROWS", node, message, className, methodName);
|
|
376
|
+
return undefined;
|
|
377
|
+
},
|
|
378
|
+
/**
|
|
379
|
+
* Resolves a rejection schema referenced by a decorator expression.
|
|
380
|
+
*
|
|
381
|
+
* @param expression The expression being inspected.
|
|
382
|
+
* @param imports The imports indexed for the current source.
|
|
383
|
+
* @returns The imported rejection schema, or undefined when unresolved.
|
|
384
|
+
*/
|
|
385
|
+
rejectionSchemaFromExpression(expression, imports) {
|
|
386
|
+
const unwrapped = HandlerSources.unwrapExpression(expression);
|
|
387
|
+
if (ts.isIdentifier(unwrapped))
|
|
388
|
+
return imports.rejectionSymbols.get(unwrapped.text);
|
|
389
|
+
if (!ts.isPropertyAccessExpression(unwrapped) || !ts.isIdentifier(unwrapped.expression)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const namespace = imports.rejectionNamespaces.get(unwrapped.expression.text);
|
|
393
|
+
const schemaName = `${unwrapped.name.text}Schema`;
|
|
394
|
+
return namespace?.exports.schemaRoles.get(schemaName) !== "rejection"
|
|
395
|
+
? undefined
|
|
396
|
+
: { moduleSpecifier: namespace.moduleSpecifier, exportName: schemaName };
|
|
397
|
+
},
|
|
398
|
+
/**
|
|
399
|
+
* Records Where or Throws decorators without a handler decorator.
|
|
400
|
+
*
|
|
401
|
+
* @param input The collected method or validation input.
|
|
402
|
+
* @returns False to stop analysis when no handler decorator is present.
|
|
403
|
+
*/
|
|
404
|
+
reportMissingHandler(input) {
|
|
405
|
+
if (input.whereUses.length > 0)
|
|
406
|
+
HandlerTypes.pushDiagnostic(input.scope, "INVALID_WHERE", input.whereUses[0]?.node ?? input.node, "@Where requires an Event-consuming @Subscribe, @React, or @Command handler.", input.className, input.method);
|
|
407
|
+
if (input.throwsUses.length > 0)
|
|
408
|
+
HandlerTypes.pushDiagnostic(input.scope, "INVALID_THROWS", input.throwsUses[0]?.node ?? input.node, "@Throws requires an @Assign handler.", input.className, input.method);
|
|
409
|
+
return false;
|
|
410
|
+
},
|
|
411
|
+
/**
|
|
412
|
+
* Checks that a handler decorator has no arguments.
|
|
413
|
+
*
|
|
414
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
415
|
+
* @param handler The recognized handler decorator.
|
|
416
|
+
* @param method The handler method name.
|
|
417
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
418
|
+
* @returns Whether the handler decorator is bare.
|
|
419
|
+
*/
|
|
420
|
+
validBareHandlerDecorator(handler, scope, className, method) {
|
|
421
|
+
if (!handler.hasArguments)
|
|
422
|
+
return true;
|
|
423
|
+
HandlerTypes.pushDiagnostic(scope, "SCHEMA_BEARING_DECORATOR", handler.node, `@${handler.name}(...) is not supported in analyzed app source.`, className, method);
|
|
424
|
+
return false;
|
|
425
|
+
},
|
|
426
|
+
/**
|
|
427
|
+
* Checks that a handler decorator fits the receiver kind.
|
|
428
|
+
*
|
|
429
|
+
* @param input The collected method or validation input.
|
|
430
|
+
* @returns Whether the decorator fits the receiver kind.
|
|
431
|
+
*/
|
|
432
|
+
validHandlerKind(input) {
|
|
433
|
+
return (HandlerSources.validEntityHandlerKind(input) &&
|
|
434
|
+
HandlerSources.validStandaloneHandlerKind(input));
|
|
435
|
+
},
|
|
436
|
+
/**
|
|
437
|
+
* Checks an entity handler against its aggregate, projection, or process manager base.
|
|
438
|
+
*
|
|
439
|
+
* @param input The collected method or validation input.
|
|
440
|
+
* @returns Whether the entity base supports this handler kind.
|
|
441
|
+
*/
|
|
442
|
+
validEntityHandlerKind(input) {
|
|
443
|
+
const { className, entityBase, handler, method, scope } = input;
|
|
444
|
+
if (entityBase === "Projection" && handler.name === "Assign")
|
|
445
|
+
return HandlerSources.unsupported(scope, "UNSUPPORTED_ASSIGN_HANDLER", handler.node, "Projection handlers cannot use @Assign.", className, method);
|
|
446
|
+
return ((entityBase !== "Aggregate" && entityBase !== "Projection") ||
|
|
447
|
+
handler.name !== "Command" ||
|
|
448
|
+
HandlerSources.unsupported(scope, "UNSUPPORTED_COMMAND_HANDLER", handler.node, "Only Process Managers support @Command handlers.", className, method));
|
|
449
|
+
},
|
|
450
|
+
/**
|
|
451
|
+
* Checks a standalone handler against its receiver base.
|
|
452
|
+
*
|
|
453
|
+
* @param input The collected method or validation input.
|
|
454
|
+
* @returns Whether the standalone base supports this handler kind.
|
|
455
|
+
*/
|
|
456
|
+
validStandaloneHandlerKind(input) {
|
|
457
|
+
const { className, entityBase, handler, method, receiverKind, scope } = input;
|
|
458
|
+
return (receiverKind !== "standalone" ||
|
|
459
|
+
HandlerSources.allowsStandaloneDecorator(entityBase, handler.name) ||
|
|
460
|
+
HandlerSources.unsupported(scope, "UNSUPPORTED_COMMAND_HANDLER", handler.node, `@${handler.name} is not legal for standalone ${entityBase ?? "receiver"}.`, className, method));
|
|
461
|
+
},
|
|
462
|
+
/**
|
|
463
|
+
* Records an unsupported handler configuration.
|
|
464
|
+
*
|
|
465
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
466
|
+
* @param code The diagnostic code to report.
|
|
467
|
+
* @param message The descriptor message or diagnostic text being inspected.
|
|
468
|
+
* @param method The handler method name.
|
|
469
|
+
* @param node The syntax node being inspected.
|
|
470
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
471
|
+
* @returns False after recording the unsupported configuration.
|
|
472
|
+
*/
|
|
473
|
+
unsupported(scope, code, node, message, className, method) {
|
|
474
|
+
HandlerTypes.pushDiagnostic(scope, code, node, message, className, method);
|
|
475
|
+
return false;
|
|
476
|
+
},
|
|
477
|
+
/**
|
|
478
|
+
* Validates and reads the event filter declared by Where.
|
|
479
|
+
*
|
|
480
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
481
|
+
* @param handler The recognized handler decorator.
|
|
482
|
+
* @param methodName The handler method name used in diagnostics.
|
|
483
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
484
|
+
* @param signalKind The resolved input signal kind.
|
|
485
|
+
* @param uses The decorator uses under inspection.
|
|
486
|
+
* @returns The validated event filter, or undefined when absent or invalid.
|
|
487
|
+
*/
|
|
488
|
+
whereDeclaration(uses, handler, signalKind, scope, className, methodName) {
|
|
489
|
+
if (uses.length === 0)
|
|
490
|
+
return undefined;
|
|
491
|
+
const use = uses[0];
|
|
492
|
+
if (uses.length !== 1 ||
|
|
493
|
+
(handler.name !== "Subscribe" && handler.name !== "React" && handler.name !== "Command") ||
|
|
494
|
+
(signalKind !== "event" && signalKind !== "rejection") ||
|
|
495
|
+
use === undefined) {
|
|
496
|
+
HandlerSources.invalidWhere(scope, use?.node ?? handler.node, "@Where is allowed once on an Event-consuming @Subscribe, @React, or @Command handler.", className, methodName);
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
return HandlerSources.whereOptions(use, scope, className, methodName);
|
|
500
|
+
},
|
|
501
|
+
/**
|
|
502
|
+
* Reads the object literal from one Where decorator.
|
|
503
|
+
*
|
|
504
|
+
* @param use Where decorator to inspect.
|
|
505
|
+
* @param scope Current analysis context.
|
|
506
|
+
* @param className Receiver class name.
|
|
507
|
+
* @param methodName Handler method name.
|
|
508
|
+
* @returns Parsed filter, or undefined when invalid.
|
|
509
|
+
*/
|
|
510
|
+
whereOptions(use, scope, className, methodName) {
|
|
511
|
+
const expression = use.node.expression;
|
|
512
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1) {
|
|
513
|
+
HandlerSources.invalidWhere(scope, use.node, "@Where requires one object literal.", className, methodName);
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
const options = expression.arguments[0];
|
|
517
|
+
if (options === undefined || !ts.isObjectLiteralExpression(options)) {
|
|
518
|
+
HandlerSources.invalidWhere(scope, expression, "@Where options must be an object literal.", className, methodName);
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
return HandlerSources.whereProperties(options, scope, className, methodName);
|
|
522
|
+
},
|
|
523
|
+
/**
|
|
524
|
+
* Validates string-valued Where filter properties.
|
|
525
|
+
*
|
|
526
|
+
* @param options Object literal supplied to Where.
|
|
527
|
+
* @param scope Current analysis context.
|
|
528
|
+
* @param className Receiver class name.
|
|
529
|
+
* @param methodName Handler method name.
|
|
530
|
+
* @returns Parsed filter, or undefined when invalid.
|
|
531
|
+
*/
|
|
532
|
+
whereProperties(options, scope, className, methodName) {
|
|
533
|
+
const values = new Map();
|
|
534
|
+
for (const property of options.properties) {
|
|
535
|
+
if (!HandlerSources.readWhereProperty(property, values, scope, className, methodName))
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
const eventField = values.get("eventField"), equals = values.get("equals");
|
|
539
|
+
if (eventField === undefined || eventField.trim().length === 0 || equals === undefined) {
|
|
540
|
+
HandlerSources.invalidWhere(scope, options, "@Where requires non-empty eventField and string equals.", className, methodName);
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze({ eventField, equals });
|
|
544
|
+
},
|
|
545
|
+
/**
|
|
546
|
+
* Reads one literal Where filter property.
|
|
547
|
+
*
|
|
548
|
+
* @param property Object-literal member to inspect.
|
|
549
|
+
* @param values Parsed filter entries.
|
|
550
|
+
* @param scope Current analysis context.
|
|
551
|
+
* @param className Receiver class name.
|
|
552
|
+
* @param methodName Handler method name.
|
|
553
|
+
* @returns Whether the property was accepted.
|
|
554
|
+
*/
|
|
555
|
+
readWhereProperty(property, values, scope, className, methodName) {
|
|
556
|
+
if (!ts.isPropertyAssignment(property) ||
|
|
557
|
+
(property.name.kind !== ts.SyntaxKind.Identifier && !ts.isStringLiteralLike(property.name)) ||
|
|
558
|
+
!ts.isStringLiteralLike(property.initializer)) {
|
|
559
|
+
HandlerSources.invalidWhere(scope, property, "@Where accepts only eventField and equals string literals.", className, methodName);
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
const name = property.name.text;
|
|
563
|
+
if ((name !== "eventField" && name !== "equals") || values.has(name)) {
|
|
564
|
+
HandlerSources.invalidWhere(scope, property, "@Where accepts each of eventField and equals exactly once.", className, methodName);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
values.set(name, property.initializer.text);
|
|
568
|
+
return true;
|
|
569
|
+
},
|
|
570
|
+
/**
|
|
571
|
+
* Checks an invalid Where declaration and records its diagnostic.
|
|
572
|
+
*
|
|
573
|
+
* @param scope Current analysis context.
|
|
574
|
+
* @param node Invalid syntax node.
|
|
575
|
+
* @param message Diagnostic message.
|
|
576
|
+
* @param className Receiver class name.
|
|
577
|
+
* @param methodName Handler method name.
|
|
578
|
+
*/
|
|
579
|
+
invalidWhere(scope, node, message, className, methodName) {
|
|
580
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_WHERE", node, message, className, methodName);
|
|
581
|
+
},
|
|
582
|
+
/**
|
|
583
|
+
* Checks whether a class has a recognized method decorator.
|
|
584
|
+
*
|
|
585
|
+
* @param imports The imports indexed for the current source.
|
|
586
|
+
* @param node The syntax node being inspected.
|
|
587
|
+
* @returns Whether the class has a recognized decorated method.
|
|
588
|
+
*/
|
|
589
|
+
hasDecoratedMethod(node, imports) {
|
|
590
|
+
return node.members.some((member) => ts.isMethodDeclaration(member) &&
|
|
591
|
+
HandlerSources.methodDecorators(member, imports).length > 0);
|
|
592
|
+
},
|
|
593
|
+
/**
|
|
594
|
+
* Finds the export problem that prevents generated receiver imports.
|
|
595
|
+
*
|
|
596
|
+
* @param node The syntax node being inspected.
|
|
597
|
+
* @param source The source file being inspected.
|
|
598
|
+
* @returns Whether the source exports the class by name.
|
|
599
|
+
*/
|
|
600
|
+
receiverExportIssue(node, source) {
|
|
601
|
+
if (HandlerTypes.hasModifier(node, ts.SyntaxKind.DefaultKeyword))
|
|
602
|
+
return undefined;
|
|
603
|
+
if (HandlerTypes.hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
|
|
604
|
+
return undefined;
|
|
605
|
+
}
|
|
606
|
+
if (node.name !== undefined && HandlerSources.hasNamedClassExport(node.name.text, source)) {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
code: "NON_EXPORTED_RECEIVER_CLASS",
|
|
611
|
+
message: "Decorated receiver classes must be exported for generated registry imports.",
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
/**
|
|
615
|
+
* Checks whether a class is exported by name from its source.
|
|
616
|
+
*
|
|
617
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
618
|
+
* @param source The source file being inspected.
|
|
619
|
+
* @returns Whether the export clause includes the class name.
|
|
620
|
+
*/
|
|
621
|
+
hasNamedClassExport(className, source) {
|
|
622
|
+
return source.statements.some((statement) => ts.isExportDeclaration(statement) &&
|
|
623
|
+
statement.moduleSpecifier === undefined &&
|
|
624
|
+
statement.exportClause !== undefined &&
|
|
625
|
+
HandlerSources.namedExportIncludes(statement.exportClause, className));
|
|
626
|
+
},
|
|
627
|
+
/**
|
|
628
|
+
* Checks whether an export clause names a class.
|
|
629
|
+
*
|
|
630
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
631
|
+
* @param clause The named export clause to inspect.
|
|
632
|
+
* @returns Whether the named export clause includes the class.
|
|
633
|
+
*/
|
|
634
|
+
namedExportIncludes(clause, className) {
|
|
635
|
+
return (ts.isNamedExports(clause) &&
|
|
636
|
+
clause.elements.some((element) => element.name.text === className &&
|
|
637
|
+
(element.propertyName === undefined || element.propertyName.text === className)));
|
|
638
|
+
},
|
|
639
|
+
/**
|
|
640
|
+
* Checks the method declaration and its decorator before schema analysis.
|
|
641
|
+
*
|
|
642
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
643
|
+
* @param decorator The decorator under inspection.
|
|
644
|
+
* @param method The handler method name.
|
|
645
|
+
* @param node The syntax node being inspected.
|
|
646
|
+
* @param requiresEntityState Whether the receiver requires a generated entity state schema.
|
|
647
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
648
|
+
* @param stateSchema The resolved entity state schema, when present.
|
|
649
|
+
* @returns Whether the method can be analyzed.
|
|
650
|
+
*/
|
|
651
|
+
validateHandlerNode(node, decorator, stateSchema, requiresEntityState, scope, className, method) {
|
|
652
|
+
return [
|
|
653
|
+
HandlerSources.validateEntityState(node, stateSchema, requiresEntityState, scope, className, method),
|
|
654
|
+
HandlerSources.validateName(node, scope, className),
|
|
655
|
+
HandlerSources.validateVisibility(node, scope, className, method),
|
|
656
|
+
HandlerSources.validateParameters(node, decorator, scope, className, method),
|
|
657
|
+
HandlerSources.validateReturn(node, decorator, scope, className, method),
|
|
658
|
+
].some(Boolean);
|
|
659
|
+
},
|
|
660
|
+
/**
|
|
661
|
+
* Checks that an entity receiver declares a generated state schema.
|
|
662
|
+
*
|
|
663
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
664
|
+
* @param method The handler method name.
|
|
665
|
+
* @param node The syntax node being inspected.
|
|
666
|
+
* @param requiresEntityState Whether the receiver requires a generated entity state schema.
|
|
667
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
668
|
+
* @param stateSchema The resolved entity state schema, when present.
|
|
669
|
+
* @returns Whether the entity state schema is valid.
|
|
670
|
+
*/
|
|
671
|
+
validateEntityState(node, stateSchema, requiresEntityState, scope, className, method) {
|
|
672
|
+
if (!requiresEntityState || stateSchema !== undefined) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_ENTITY_STATE_SCHEMA", node, "Decorated handlers must be declared on an entity class with an inferred state schema.", className, method);
|
|
676
|
+
return true;
|
|
677
|
+
},
|
|
678
|
+
/**
|
|
679
|
+
* Checks that a handler method has an importable name.
|
|
680
|
+
*
|
|
681
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
682
|
+
* @param node The syntax node being inspected.
|
|
683
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
684
|
+
* @returns Whether the method name is importable.
|
|
685
|
+
*/
|
|
686
|
+
validateName(node, scope, className) {
|
|
687
|
+
if (HandlerTypes.methodName(node) !== undefined) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_HANDLER_NAME", node.name, "Decorated handlers must use a string method name.", className);
|
|
691
|
+
return true;
|
|
692
|
+
},
|
|
693
|
+
/**
|
|
694
|
+
* Checks that a handler method is publicly callable.
|
|
695
|
+
*
|
|
696
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
697
|
+
* @param method The handler method name.
|
|
698
|
+
* @param node The syntax node being inspected.
|
|
699
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
700
|
+
* @returns Whether the handler method is public.
|
|
701
|
+
*/
|
|
702
|
+
validateVisibility(node, scope, className, method) {
|
|
703
|
+
if (!HandlerTypes.hasModifier(node, ts.SyntaxKind.StaticKeyword) &&
|
|
704
|
+
!HandlerTypes.hasModifier(node, ts.SyntaxKind.PrivateKeyword) &&
|
|
705
|
+
!HandlerTypes.hasModifier(node, ts.SyntaxKind.ProtectedKeyword)) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_HANDLER_VISIBILITY", node.name, "Decorated handlers must be public instance methods.", className, method);
|
|
709
|
+
return true;
|
|
710
|
+
},
|
|
711
|
+
/**
|
|
712
|
+
* Checks the parameter count and required input declaration.
|
|
713
|
+
*
|
|
714
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
715
|
+
* @param decorator The decorator under inspection.
|
|
716
|
+
* @param method The handler method name.
|
|
717
|
+
* @param node The syntax node being inspected.
|
|
718
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
719
|
+
* @returns Whether the input and context parameters are valid.
|
|
720
|
+
*/
|
|
721
|
+
validateParameters(node, decorator, scope, className, method) {
|
|
722
|
+
if (HandlerSources.parameterShapeIssue(node, decorator, scope, className, method))
|
|
723
|
+
return true;
|
|
724
|
+
const origin = HandlerSources.externalOrigin(node.parameters, scope, className, method);
|
|
725
|
+
if (origin === undefined)
|
|
726
|
+
return true;
|
|
727
|
+
const signal = HandlerSources.schemaUseFromType(origin.type, scope.imports);
|
|
728
|
+
if (signal !== undefined && HandlerSources.acceptsSignalKind(decorator, signal.kind)) {
|
|
729
|
+
if (origin.value === "external" && signal.kind === "command") {
|
|
730
|
+
HandlerTypes.pushDiagnostic(scope, "EXTERNAL_COMMAND_RECEIVER", node.parameters[0]?.type ?? node, "Command receivers cannot declare External<Command>.", className, method);
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_SIGNAL_TYPE", node.parameters[0]?.type ?? node, `@${decorator} first parameter must be ${HandlerSources.signalMessage(decorator)}.`, className, method);
|
|
736
|
+
return true;
|
|
737
|
+
},
|
|
738
|
+
/**
|
|
739
|
+
* Checks handler parameter count and explicit input annotation.
|
|
740
|
+
*
|
|
741
|
+
* @param node Handler method declaration.
|
|
742
|
+
* @param decorator Handler decorator.
|
|
743
|
+
* @param scope Current analysis context.
|
|
744
|
+
* @param className Receiver class name.
|
|
745
|
+
* @param method Handler method name.
|
|
746
|
+
* @returns Whether a parameter-shape issue was reported.
|
|
747
|
+
*/
|
|
748
|
+
parameterShapeIssue(node, decorator, scope, className, method) {
|
|
749
|
+
if (node.parameters.length !== 1 && node.parameters.length !== 2) {
|
|
750
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_PARAMETER_COUNT", node, "Decorated handlers must declare one or two public parameters.", className, method);
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
if (node.parameters[0]?.type === undefined) {
|
|
754
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_SIGNAL_TYPE", node, `@${decorator} handlers require an explicit first parameter type.`, className, method);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
return false;
|
|
758
|
+
},
|
|
759
|
+
/**
|
|
760
|
+
* Checks the explicit return shape required by a handler decorator.
|
|
761
|
+
*
|
|
762
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
763
|
+
* @param decorator The decorator under inspection.
|
|
764
|
+
* @param method The handler method name.
|
|
765
|
+
* @param node The syntax node being inspected.
|
|
766
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
767
|
+
* @returns Whether the return declaration meets the handler contract.
|
|
768
|
+
*/
|
|
769
|
+
validateReturn(node, decorator, scope, className, method) {
|
|
770
|
+
if (decorator === "Subscribe")
|
|
771
|
+
return HandlerSources.validateSubscribeReturn(node, scope, className, method);
|
|
772
|
+
if (node.type === undefined) {
|
|
773
|
+
HandlerTypes.pushDiagnostic(scope, "MISSING_RETURN_TYPE", node, `@${decorator} handlers require an explicit return type.`, className, method);
|
|
774
|
+
return true;
|
|
775
|
+
}
|
|
776
|
+
const returnType = HandlerSources.unwrapOuterPromise(node.type, scope);
|
|
777
|
+
if (HandlerSources.rejectFrameworkEnvelope(node, returnType, scope, className, method))
|
|
778
|
+
return true;
|
|
779
|
+
return HandlerSources.validateEmittedReturn(node, decorator, scope, className, method, returnType, HandlerSources.resolvedHandlerKind(node, decorator, scope));
|
|
780
|
+
},
|
|
781
|
+
/**
|
|
782
|
+
* Checks for a framework envelope used as an application handler return.
|
|
783
|
+
*
|
|
784
|
+
* @param node Decorated handler method.
|
|
785
|
+
* @param returnType Unwrapped declared return type.
|
|
786
|
+
* @param scope Current source and import context.
|
|
787
|
+
* @param className Receiver class name for diagnostics.
|
|
788
|
+
* @param method Handler method name for diagnostics.
|
|
789
|
+
* @returns Whether an invalid framework envelope was reported.
|
|
790
|
+
*/
|
|
791
|
+
rejectFrameworkEnvelope(node, returnType, scope, className, method) {
|
|
792
|
+
const envelope = HandlerSources.frameworkEnvelope(returnType, scope.imports);
|
|
793
|
+
if (envelope === undefined)
|
|
794
|
+
return false;
|
|
795
|
+
HandlerTypes.pushDiagnostic(scope, "FRAMEWORK_ENVELOPE_RETURN", node.type ?? node, `Handler return type must not be framework ${envelope}.`, className, method);
|
|
796
|
+
return true;
|
|
797
|
+
},
|
|
798
|
+
/**
|
|
799
|
+
* Resolves a handler role from its input signal.
|
|
800
|
+
*
|
|
801
|
+
* @param node Decorated handler method.
|
|
802
|
+
* @param decorator Handler decorator under inspection.
|
|
803
|
+
* @param scope Current source and import context.
|
|
804
|
+
* @returns The concrete generated handler role.
|
|
805
|
+
*/
|
|
806
|
+
resolvedHandlerKind(node, decorator, scope) {
|
|
807
|
+
const first = node.parameters[0]?.type;
|
|
808
|
+
const input = first === undefined
|
|
809
|
+
? undefined
|
|
810
|
+
: (HandlerSources.externalMarker(first, scope)?.type ?? first);
|
|
811
|
+
const signal = HandlerSources.schemaUseFromType(input, scope.imports);
|
|
812
|
+
return HandlerSources.handlerKind(decorator, signal?.kind);
|
|
813
|
+
},
|
|
814
|
+
/**
|
|
815
|
+
* Checks that a subscription handler explicitly returns void.
|
|
816
|
+
*
|
|
817
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
818
|
+
* @param method The handler method name.
|
|
819
|
+
* @param node The syntax node being inspected.
|
|
820
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
821
|
+
* @returns Whether the return type is explicitly void.
|
|
822
|
+
*/
|
|
823
|
+
validateSubscribeReturn(node, scope, className, method) {
|
|
824
|
+
if (node.type !== undefined &&
|
|
825
|
+
HandlerSources.isSubscriberVoidReturn(node.type, scope.program)) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_SUBSCRIBE_RETURN", node, "@Subscribe handlers must return explicit void.", className, method);
|
|
829
|
+
return true;
|
|
830
|
+
},
|
|
831
|
+
/**
|
|
832
|
+
* Checks the generated messages emitted by a handler.
|
|
833
|
+
*
|
|
834
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
835
|
+
* @param decorator The decorator under inspection.
|
|
836
|
+
* @param kind The resolved handler role.
|
|
837
|
+
* @param method The handler method name.
|
|
838
|
+
* @param node The syntax node being inspected.
|
|
839
|
+
* @param returnType The declared handler return type.
|
|
840
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
841
|
+
* @returns Whether emitted schemas meet the handler contract.
|
|
842
|
+
*/
|
|
843
|
+
validateEmittedReturn(node, decorator, scope, className, method, returnType = node.type, kind = HandlerSources.handlerKind(decorator, undefined)) {
|
|
844
|
+
const schemas = HandlerSources.emittedSchemaUses(returnType, kind, scope);
|
|
845
|
+
const issue = HandlerSources.emittedIssue(schemas, decorator, kind, returnType, scope);
|
|
846
|
+
return issue === undefined
|
|
847
|
+
? false
|
|
848
|
+
: HandlerSources.returnDiagnostic(scope, issue.code, returnType ?? node, issue.message, className, method);
|
|
849
|
+
},
|
|
850
|
+
/**
|
|
851
|
+
* Finds the first invalid emitted return shape.
|
|
852
|
+
*
|
|
853
|
+
* @param decorator The decorator under inspection.
|
|
854
|
+
* @param kind The resolved handler role.
|
|
855
|
+
* @param returnType The declared handler return type.
|
|
856
|
+
* @param schemas The generated schemas under inspection.
|
|
857
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
858
|
+
* @returns The first invalid return condition, or undefined when valid.
|
|
859
|
+
*/
|
|
860
|
+
emittedIssue(schemas, decorator, kind, returnType, scope) {
|
|
861
|
+
if (schemas === undefined)
|
|
862
|
+
return {
|
|
863
|
+
code: "UNSUPPORTED_RETURN_TYPE",
|
|
864
|
+
message: `@${decorator} return type must resolve to generated schema references.`,
|
|
865
|
+
};
|
|
866
|
+
const expected = HandlerSources.emittedSignalKind(decorator);
|
|
867
|
+
if (expected !== undefined && schemas.some((schema) => schema.kind !== expected))
|
|
868
|
+
return {
|
|
869
|
+
code: "INVALID_EMITTED_SCHEMA",
|
|
870
|
+
message: `@${decorator} return type must emit generated ${expected} schemas.`,
|
|
871
|
+
};
|
|
872
|
+
if (HandlerSources.missingRequiredOutput(kind, schemas, returnType, scope))
|
|
873
|
+
return {
|
|
874
|
+
code: "MISSING_EMITTED_SCHEMAS",
|
|
875
|
+
message: `@${decorator} handlers must emit at least one schema.`,
|
|
876
|
+
};
|
|
877
|
+
if (HandlerSources.missingReactionOutput(kind, schemas, returnType, scope))
|
|
878
|
+
return {
|
|
879
|
+
code: HandlerSources.isExplicitVoidType(returnType)
|
|
880
|
+
? "UNSUPPORTED_RETURN_TYPE"
|
|
881
|
+
: "MISSING_EMITTED_SCHEMAS",
|
|
882
|
+
message: `@${decorator} no-output reactions must declare undefined.`,
|
|
883
|
+
};
|
|
884
|
+
return undefined;
|
|
885
|
+
},
|
|
886
|
+
/**
|
|
887
|
+
* Checks whether a required-output handler lacks a guaranteed Event.
|
|
888
|
+
*
|
|
889
|
+
* @param kind Resolved handler role.
|
|
890
|
+
* @param schemas Resolved output schemas.
|
|
891
|
+
* @param returnType Declared return type.
|
|
892
|
+
* @param scope Current TypeScript analysis context.
|
|
893
|
+
* @returns Whether the handler lacks a guaranteed output.
|
|
894
|
+
*/
|
|
895
|
+
missingRequiredOutput(kind, schemas, returnType, scope) {
|
|
896
|
+
return ((kind === "command-assignment" || kind === "command-substitution") &&
|
|
897
|
+
(schemas.length === 0 || HandlerSources.optionalOnlyTuple(returnType, scope)));
|
|
898
|
+
},
|
|
899
|
+
/**
|
|
900
|
+
* Checks whether a reaction omits its explicit undefined result.
|
|
901
|
+
*
|
|
902
|
+
* @param kind Resolved handler role.
|
|
903
|
+
* @param schemas Resolved output schemas.
|
|
904
|
+
* @param returnType Declared return type.
|
|
905
|
+
* @param scope Current TypeScript analysis context.
|
|
906
|
+
* @returns Whether the reaction return is invalid.
|
|
907
|
+
*/
|
|
908
|
+
missingReactionOutput(kind, schemas, returnType, scope) {
|
|
909
|
+
if (kind !== "event-reaction" && kind !== "command-reaction")
|
|
910
|
+
return false;
|
|
911
|
+
if (schemas.length !== 0)
|
|
912
|
+
return false;
|
|
913
|
+
if (returnType === undefined)
|
|
914
|
+
return true;
|
|
915
|
+
const type = scope.program.getTypeChecker().getTypeFromTypeNode(returnType);
|
|
916
|
+
return (type.flags & ts.TypeFlags.Undefined) === 0;
|
|
917
|
+
},
|
|
918
|
+
/**
|
|
919
|
+
* Checks a result tuple whose members can all be absent.
|
|
920
|
+
*
|
|
921
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
922
|
+
* @param typeNode The declared type node under inspection.
|
|
923
|
+
* @returns Whether every tuple member may be absent.
|
|
924
|
+
*/
|
|
925
|
+
optionalOnlyTuple(typeNode, scope) {
|
|
926
|
+
if (typeNode === undefined)
|
|
927
|
+
return false;
|
|
928
|
+
const checker = scope.program.getTypeChecker();
|
|
929
|
+
const type = checker.getTypeFromTypeNode(typeNode);
|
|
930
|
+
return (type.isUnion() ? type.types : [type]).some((branch) => {
|
|
931
|
+
if (!checker.isTupleType(branch))
|
|
932
|
+
return false;
|
|
933
|
+
const tuple = branch;
|
|
934
|
+
const members = checker.getTypeArguments(tuple);
|
|
935
|
+
return !members.some((member, index) => ((tuple.target.elementFlags[index] ?? 0) & ts.ElementFlags.Required) !== 0 &&
|
|
936
|
+
!HandlerSources.includesUndefined(member));
|
|
937
|
+
});
|
|
938
|
+
},
|
|
939
|
+
/**
|
|
940
|
+
* Checks for an undefined alternative in a resolved tuple member.
|
|
941
|
+
*
|
|
942
|
+
* @param type Resolved TypeScript member type.
|
|
943
|
+
* @returns Whether the member can be undefined.
|
|
944
|
+
*/
|
|
945
|
+
includesUndefined(type) {
|
|
946
|
+
return type.isUnion()
|
|
947
|
+
? type.types.some(HandlerSources.includesUndefined)
|
|
948
|
+
: (type.flags & ts.TypeFlags.Undefined) !== 0;
|
|
949
|
+
},
|
|
950
|
+
/**
|
|
951
|
+
* Records an invalid handler return shape.
|
|
952
|
+
*
|
|
953
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
954
|
+
* @param code The diagnostic code to report.
|
|
955
|
+
* @param message The descriptor message or diagnostic text being inspected.
|
|
956
|
+
* @param method The handler method name.
|
|
957
|
+
* @param node The syntax node being inspected.
|
|
958
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
959
|
+
* @returns True after adding the return diagnostic.
|
|
960
|
+
*/
|
|
961
|
+
returnDiagnostic(scope, code, node, message, className, method) {
|
|
962
|
+
HandlerTypes.pushDiagnostic(scope, code, node, message, className, method);
|
|
963
|
+
return true;
|
|
964
|
+
},
|
|
965
|
+
/**
|
|
966
|
+
* Collects recognized Spine decorators attached to a method.
|
|
967
|
+
*
|
|
968
|
+
* @param imports The imports indexed for the current source.
|
|
969
|
+
* @param node The syntax node being inspected.
|
|
970
|
+
* @returns The recognized decorators attached to the method.
|
|
971
|
+
*/
|
|
972
|
+
methodDecorators(node, imports) {
|
|
973
|
+
return (ts.getDecorators(node) ?? []).flatMap((decorator) => {
|
|
974
|
+
const expression = ts.isCallExpression(decorator.expression)
|
|
975
|
+
? decorator.expression.expression
|
|
976
|
+
: decorator.expression;
|
|
977
|
+
const name = HandlerSources.serverDecoratorName(expression, imports);
|
|
978
|
+
return name === undefined
|
|
979
|
+
? []
|
|
980
|
+
: [{ hasArguments: ts.isCallExpression(decorator.expression), name, node: decorator }];
|
|
981
|
+
});
|
|
982
|
+
},
|
|
983
|
+
/**
|
|
984
|
+
* Resolves a decorator expression to its imported Spine name.
|
|
985
|
+
*
|
|
986
|
+
* @param expression The expression being inspected.
|
|
987
|
+
* @param imports The imports indexed for the current source.
|
|
988
|
+
* @returns The imported Spine decorator name, or undefined when unrecognized.
|
|
989
|
+
*/
|
|
990
|
+
serverDecoratorName(expression, imports) {
|
|
991
|
+
if (ts.isIdentifier(expression)) {
|
|
992
|
+
return HandlerTypes.serverDecorator(imports.serverSymbols.get(expression.text));
|
|
993
|
+
}
|
|
994
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
995
|
+
const namespace = HandlerTypes.expressionName(expression.expression);
|
|
996
|
+
if (namespace !== undefined && imports.serverNamespaces.has(namespace)) {
|
|
997
|
+
return HandlerTypes.serverDecorator(expression.name.text);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return undefined;
|
|
1001
|
+
},
|
|
1002
|
+
/**
|
|
1003
|
+
* Checks a recognized decorator to a handler decorator.
|
|
1004
|
+
*
|
|
1005
|
+
* @param decorator The decorator under inspection.
|
|
1006
|
+
* @returns Whether the decorator is a handler decorator.
|
|
1007
|
+
*/
|
|
1008
|
+
isHandlerUse(decorator) {
|
|
1009
|
+
return handlerDecorators.has(decorator.name);
|
|
1010
|
+
},
|
|
1011
|
+
/**
|
|
1012
|
+
* Checks whether a standalone receiver base permits a handler decorator.
|
|
1013
|
+
*
|
|
1014
|
+
* @param base The resolved framework receiver base, when present.
|
|
1015
|
+
* @param decorator The decorator under inspection.
|
|
1016
|
+
* @returns Whether the base permits this handler decorator.
|
|
1017
|
+
*/
|
|
1018
|
+
allowsStandaloneDecorator(base, decorator) {
|
|
1019
|
+
return ((base === "AbstractAssignee" && decorator === "Assign") ||
|
|
1020
|
+
(base === "AbstractCommander" && decorator === "Command") ||
|
|
1021
|
+
(base === "AbstractEventReactor" && decorator === "React") ||
|
|
1022
|
+
(base === "AbstractEventSubscriber" && decorator === "Subscribe"));
|
|
1023
|
+
},
|
|
1024
|
+
/**
|
|
1025
|
+
* Resolves a class to its entity or standalone receiver ancestry.
|
|
1026
|
+
*
|
|
1027
|
+
* @param node The syntax node being inspected.
|
|
1028
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1029
|
+
* @param seen The declarations already visited during traversal.
|
|
1030
|
+
* @returns The resolved receiver ancestry, or undefined when unsupported.
|
|
1031
|
+
*/
|
|
1032
|
+
receiverLineage(node, scope, seen = new Set()) {
|
|
1033
|
+
if (seen.has(node))
|
|
1034
|
+
return undefined;
|
|
1035
|
+
const nextSeen = new Set(seen);
|
|
1036
|
+
nextSeen.add(node);
|
|
1037
|
+
for (const clause of node.heritageClauses ?? []) {
|
|
1038
|
+
for (const type of clause.types) {
|
|
1039
|
+
const direct = HandlerSources.directReceiverLineage(type, scope);
|
|
1040
|
+
if (direct !== undefined)
|
|
1041
|
+
return direct;
|
|
1042
|
+
const inherited = HandlerSources.inheritedReceiverLineage(type.expression, scope, nextSeen);
|
|
1043
|
+
if (inherited !== undefined)
|
|
1044
|
+
return inherited;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return undefined;
|
|
1048
|
+
},
|
|
1049
|
+
/**
|
|
1050
|
+
* Resolves a class's direct framework receiver base.
|
|
1051
|
+
*
|
|
1052
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1053
|
+
* @param type The checked or declared type under inspection.
|
|
1054
|
+
* @returns The direct receiver ancestry, or undefined when absent.
|
|
1055
|
+
*/
|
|
1056
|
+
directReceiverLineage(type, scope) {
|
|
1057
|
+
const base = HandlerSources.directEntityBaseName(type.expression, scope.imports);
|
|
1058
|
+
if (base !== undefined) {
|
|
1059
|
+
const stateType = type.typeArguments?.[1];
|
|
1060
|
+
const stateSchema = stateType === undefined
|
|
1061
|
+
? undefined
|
|
1062
|
+
: HandlerSources.schemaFromTypeQuery(stateType, scope.imports, HandlerSources.newTypeWalk());
|
|
1063
|
+
return { receiverKind: "entity", base, stateSchema };
|
|
1064
|
+
}
|
|
1065
|
+
const standalone = HandlerSources.directStandaloneBaseName(type.expression, scope.imports);
|
|
1066
|
+
if (standalone !== undefined)
|
|
1067
|
+
return { receiverKind: "standalone", base: standalone, stateSchema: undefined };
|
|
1068
|
+
},
|
|
1069
|
+
/**
|
|
1070
|
+
* Resolves receiver ancestry through a local base class.
|
|
1071
|
+
*
|
|
1072
|
+
* @param expression The expression being inspected.
|
|
1073
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1074
|
+
* @param seen The declarations already visited during traversal.
|
|
1075
|
+
* @returns The inherited receiver ancestry, or undefined when unresolved.
|
|
1076
|
+
*/
|
|
1077
|
+
inheritedReceiverLineage(expression, scope, seen) {
|
|
1078
|
+
const parent = HandlerSources.classDeclarationFor(expression, scope);
|
|
1079
|
+
if (parent === undefined)
|
|
1080
|
+
return undefined;
|
|
1081
|
+
const source = parent.getSourceFile();
|
|
1082
|
+
return HandlerSources.receiverLineage(parent, { ...scope, source, imports: HandlerSources.buildImportState(source, scope.program) }, seen);
|
|
1083
|
+
},
|
|
1084
|
+
/**
|
|
1085
|
+
* Finds the framework entity base named by an extends expression.
|
|
1086
|
+
*
|
|
1087
|
+
* @param expression The expression being inspected.
|
|
1088
|
+
* @param imports The imports indexed for the current source.
|
|
1089
|
+
* @returns The entity base name, or undefined when absent.
|
|
1090
|
+
*/
|
|
1091
|
+
directEntityBaseName(expression, imports) {
|
|
1092
|
+
if (ts.isIdentifier(expression)) {
|
|
1093
|
+
const base = imports.serverSymbols.get(expression.text);
|
|
1094
|
+
return base !== undefined && entityBaseNames.has(base) ? base : undefined;
|
|
1095
|
+
}
|
|
1096
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1097
|
+
const namespace = HandlerTypes.expressionName(expression.expression);
|
|
1098
|
+
return namespace !== undefined &&
|
|
1099
|
+
imports.serverNamespaces.has(namespace) &&
|
|
1100
|
+
entityBaseNames.has(expression.name.text)
|
|
1101
|
+
? expression.name.text
|
|
1102
|
+
: undefined;
|
|
1103
|
+
}
|
|
1104
|
+
return undefined;
|
|
1105
|
+
},
|
|
1106
|
+
/**
|
|
1107
|
+
* Finds the framework standalone base named by an extends expression.
|
|
1108
|
+
*
|
|
1109
|
+
* @param expression The expression being inspected.
|
|
1110
|
+
* @param imports The imports indexed for the current source.
|
|
1111
|
+
* @returns The standalone base name, or undefined when absent.
|
|
1112
|
+
*/
|
|
1113
|
+
directStandaloneBaseName(expression, imports) {
|
|
1114
|
+
if (ts.isIdentifier(expression)) {
|
|
1115
|
+
const base = imports.serverSymbols.get(expression.text);
|
|
1116
|
+
return base !== undefined && standaloneBaseNames.has(base) ? base : undefined;
|
|
1117
|
+
}
|
|
1118
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
1119
|
+
const namespace = HandlerTypes.expressionName(expression.expression);
|
|
1120
|
+
return namespace !== undefined &&
|
|
1121
|
+
imports.serverNamespaces.has(namespace) &&
|
|
1122
|
+
standaloneBaseNames.has(expression.name.text)
|
|
1123
|
+
? expression.name.text
|
|
1124
|
+
: undefined;
|
|
1125
|
+
}
|
|
1126
|
+
return undefined;
|
|
1127
|
+
},
|
|
1128
|
+
/**
|
|
1129
|
+
* Finds the local class declaration referenced by an extends expression.
|
|
1130
|
+
*
|
|
1131
|
+
* @param expression The expression being inspected.
|
|
1132
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1133
|
+
* @returns The local base class declaration, or undefined when absent.
|
|
1134
|
+
*/
|
|
1135
|
+
classDeclarationFor(expression, scope) {
|
|
1136
|
+
const checker = scope.program.getTypeChecker();
|
|
1137
|
+
const location = ts.isPropertyAccessExpression(expression) ? expression.name : expression;
|
|
1138
|
+
let symbol = checker.getSymbolAtLocation(location);
|
|
1139
|
+
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
1140
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1141
|
+
}
|
|
1142
|
+
const declaration = symbol?.declarations?.find(ts.isClassDeclaration);
|
|
1143
|
+
if (declaration !== undefined)
|
|
1144
|
+
return declaration;
|
|
1145
|
+
const declarations = checker.getTypeAtLocation(expression).getSymbol()?.declarations;
|
|
1146
|
+
return declarations === undefined ? undefined : declarations.find(ts.isClassDeclaration);
|
|
1147
|
+
},
|
|
1148
|
+
/**
|
|
1149
|
+
* Checks whether an extends expression names an entity base.
|
|
1150
|
+
*
|
|
1151
|
+
* @param expression The expression being inspected.
|
|
1152
|
+
* @param imports The imports indexed for the current source.
|
|
1153
|
+
* @returns Whether the expression names a framework entity base.
|
|
1154
|
+
*/
|
|
1155
|
+
isEntityBase(expression, imports) {
|
|
1156
|
+
return HandlerSources.directEntityBaseName(expression, imports) !== undefined;
|
|
1157
|
+
},
|
|
1158
|
+
/**
|
|
1159
|
+
* Resolves an entity state schema referenced by a type query.
|
|
1160
|
+
*
|
|
1161
|
+
* @param imports The imports indexed for the current source.
|
|
1162
|
+
* @param typeNode The declared type node under inspection.
|
|
1163
|
+
* @param walk The bounded alias traversal state.
|
|
1164
|
+
* @returns The referenced generated schema, or undefined when unresolved.
|
|
1165
|
+
*/
|
|
1166
|
+
schemaFromTypeQuery(typeNode, imports, walk) {
|
|
1167
|
+
if (!HandlerSources.consumeTypeWalk(walk))
|
|
1168
|
+
return undefined;
|
|
1169
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
1170
|
+
return HandlerSources.schemaFromTypeQuery(typeNode.type, imports, walk);
|
|
1171
|
+
}
|
|
1172
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
1173
|
+
const alias = imports.localTypeAliases.get(typeNode.typeName.text);
|
|
1174
|
+
return alias === undefined
|
|
1175
|
+
? undefined
|
|
1176
|
+
: HandlerSources.resolveAlias(typeNode.typeName.text, alias, walk, (resolved) => HandlerSources.schemaFromTypeQuery(resolved, imports, walk));
|
|
1177
|
+
}
|
|
1178
|
+
if (!ts.isTypeQueryNode(typeNode)) {
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
return HandlerSources.schemaFromEntityName(typeNode.exprName, imports);
|
|
1182
|
+
},
|
|
1183
|
+
/**
|
|
1184
|
+
* Collects generated schemas allowed by a handler's return type.
|
|
1185
|
+
*
|
|
1186
|
+
* @param kind The resolved handler role.
|
|
1187
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1188
|
+
* @param typeNode The declared type node under inspection.
|
|
1189
|
+
* @returns The emitted schema uses, or undefined when the return type is invalid.
|
|
1190
|
+
*/
|
|
1191
|
+
emittedSchemaUses(typeNode, kind, scope) {
|
|
1192
|
+
if (kind === "event-subscription" || kind === "state-subscription")
|
|
1193
|
+
return [];
|
|
1194
|
+
if (typeNode?.kind === ts.SyntaxKind.VoidKeyword) {
|
|
1195
|
+
return [];
|
|
1196
|
+
}
|
|
1197
|
+
if (typeNode === undefined) {
|
|
1198
|
+
return undefined;
|
|
1199
|
+
}
|
|
1200
|
+
const optional = kind === "event-reaction" || kind === "command-reaction";
|
|
1201
|
+
const direct = HandlerSources.schemaListFromType(typeNode, scope, HandlerSources.newTypeWalk(), optional);
|
|
1202
|
+
if (direct !== undefined)
|
|
1203
|
+
return direct;
|
|
1204
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
1205
|
+
if (ts.isTupleTypeNode(unwrapped) &&
|
|
1206
|
+
unwrapped.elements.some((member) => ts.isRestTypeNode(member) ||
|
|
1207
|
+
(ts.isNamedTupleMember(member) && member.dotDotDotToken !== undefined)))
|
|
1208
|
+
return undefined;
|
|
1209
|
+
return (HandlerSources.checkedPromiseSchemas(typeNode, scope, optional) ??
|
|
1210
|
+
HandlerSources.checkedSchemas(typeNode, scope, optional));
|
|
1211
|
+
},
|
|
1212
|
+
/**
|
|
1213
|
+
* Resolves one built-in Promise layer after TypeScript expands a whole-return alias.
|
|
1214
|
+
*
|
|
1215
|
+
* @param typeNode Declared handler return type.
|
|
1216
|
+
* @param scope Source and checker context.
|
|
1217
|
+
* @param optional Whether the reaction may return no result.
|
|
1218
|
+
* @returns Inner generated schemas, or undefined for a non-Promise or invalid result.
|
|
1219
|
+
*/
|
|
1220
|
+
checkedPromiseSchemas(typeNode, scope, optional) {
|
|
1221
|
+
if (!ts.isMethodDeclaration(typeNode.parent) && !ts.isTypeAliasDeclaration(typeNode.parent))
|
|
1222
|
+
return undefined;
|
|
1223
|
+
const checker = scope.program.getTypeChecker();
|
|
1224
|
+
const type = checker.getTypeFromTypeNode(typeNode);
|
|
1225
|
+
const symbol = type.getSymbol();
|
|
1226
|
+
if (symbol?.getName() !== "Promise" ||
|
|
1227
|
+
!symbol.declarations?.some((declaration) => scope.program.isSourceFileDefaultLibrary(declaration.getSourceFile())))
|
|
1228
|
+
return undefined;
|
|
1229
|
+
const [inner] = checker.getTypeArguments(type);
|
|
1230
|
+
return inner === undefined
|
|
1231
|
+
? undefined
|
|
1232
|
+
: HandlerSources.fromCheckedType(inner, checker, scope, HandlerSources.newTypeWalk(), optional);
|
|
1233
|
+
},
|
|
1234
|
+
/**
|
|
1235
|
+
* Resolves concrete generated schemas from a checked TypeScript type.
|
|
1236
|
+
*
|
|
1237
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1238
|
+
* @param typeNode The declared type node under inspection.
|
|
1239
|
+
* @param optional Whether a whole reaction result may be absent.
|
|
1240
|
+
* @returns The generated schema uses, or undefined when expansion fails.
|
|
1241
|
+
*/
|
|
1242
|
+
checkedSchemas(typeNode, scope, optional = false) {
|
|
1243
|
+
const checker = scope.program.getTypeChecker();
|
|
1244
|
+
return HandlerSources.fromCheckedType(checker.getTypeFromTypeNode(typeNode), checker, scope, HandlerSources.newTypeWalk(), optional);
|
|
1245
|
+
},
|
|
1246
|
+
/**
|
|
1247
|
+
* Collects generated schemas from checked union, array, or tuple types.
|
|
1248
|
+
*
|
|
1249
|
+
* @param checker The TypeScript type checker for the program.
|
|
1250
|
+
* @param optional Whether this checked branch may be absent.
|
|
1251
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1252
|
+
* @param type The checked or declared type under inspection.
|
|
1253
|
+
* @param walk The bounded alias traversal state.
|
|
1254
|
+
* @param collectionDepth Number of array or tuple containers already entered.
|
|
1255
|
+
* @returns The schema uses, or undefined when a checked type is unsupported.
|
|
1256
|
+
*/
|
|
1257
|
+
fromCheckedType(type, checker, scope, walk, optional = false, collectionDepth = 0) {
|
|
1258
|
+
if (!HandlerSources.consumeTypeWalk(walk))
|
|
1259
|
+
return undefined;
|
|
1260
|
+
if (optional && (type.flags & ts.TypeFlags.Undefined) !== 0)
|
|
1261
|
+
return [];
|
|
1262
|
+
if (type.isUnion()) {
|
|
1263
|
+
return HandlerSources.checkedBranches(type.types, checker, scope, walk, optional, collectionDepth);
|
|
1264
|
+
}
|
|
1265
|
+
if (checker.isTupleType(type)) {
|
|
1266
|
+
return HandlerSources.fromCheckedTuple(type, checker, scope, walk, collectionDepth);
|
|
1267
|
+
}
|
|
1268
|
+
if (checker.isArrayType(type)) {
|
|
1269
|
+
return HandlerSources.fromCheckedArray(type, checker, scope, walk, optional, collectionDepth);
|
|
1270
|
+
}
|
|
1271
|
+
const schema = HandlerSources.checkedMessage(type, scope);
|
|
1272
|
+
return schema === undefined ? undefined : [schema];
|
|
1273
|
+
},
|
|
1274
|
+
/**
|
|
1275
|
+
* Resolves one checked array without permitting nested collections.
|
|
1276
|
+
*
|
|
1277
|
+
* @param type Checked array type.
|
|
1278
|
+
* @param checker TypeScript type checker.
|
|
1279
|
+
* @param scope Current source and imports.
|
|
1280
|
+
* @param walk Bounded alias traversal state.
|
|
1281
|
+
* @param optional Whether undefined may occur within the array.
|
|
1282
|
+
* @param collectionDepth Number of enclosing collections.
|
|
1283
|
+
* @returns Array member schemas, or undefined for an invalid shape.
|
|
1284
|
+
*/
|
|
1285
|
+
fromCheckedArray(type, checker, scope, walk, optional, collectionDepth) {
|
|
1286
|
+
if (collectionDepth > 0)
|
|
1287
|
+
return undefined;
|
|
1288
|
+
const member = checker.getTypeArguments(type)[0];
|
|
1289
|
+
return member === undefined
|
|
1290
|
+
? undefined
|
|
1291
|
+
: HandlerSources.fromCheckedType(member, checker, scope, walk, optional, collectionDepth + 1);
|
|
1292
|
+
},
|
|
1293
|
+
/**
|
|
1294
|
+
* Resolves one checked tuple without permitting nested collection shapes.
|
|
1295
|
+
*
|
|
1296
|
+
* @param type Checked tuple type.
|
|
1297
|
+
* @param checker TypeScript checker for its member types.
|
|
1298
|
+
* @param scope Current source and diagnostic context.
|
|
1299
|
+
* @param walk Bounded alias traversal state.
|
|
1300
|
+
* @param collectionDepth Number of prior tuple or array containers.
|
|
1301
|
+
* @returns Member schemas, or undefined for nested or variadic tuples.
|
|
1302
|
+
*/
|
|
1303
|
+
fromCheckedTuple(type, checker, scope, walk, collectionDepth) {
|
|
1304
|
+
if (collectionDepth > 0)
|
|
1305
|
+
return undefined;
|
|
1306
|
+
const tuple = type;
|
|
1307
|
+
if (tuple.target.combinedFlags & ts.ElementFlags.Variable)
|
|
1308
|
+
return undefined;
|
|
1309
|
+
return HandlerSources.checkedBranches(checker.getTypeArguments(tuple), checker, scope, walk, true, collectionDepth + 1);
|
|
1310
|
+
},
|
|
1311
|
+
/**
|
|
1312
|
+
* Collects schemas across checked alternatives and optional branches.
|
|
1313
|
+
*
|
|
1314
|
+
* @param checker The TypeScript type checker for the program.
|
|
1315
|
+
* @param optional Whether this checked branch may be absent.
|
|
1316
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1317
|
+
* @param types The checked type alternatives to inspect.
|
|
1318
|
+
* @param walk The bounded alias traversal state.
|
|
1319
|
+
* @param collectionDepth Number of array or tuple containers already entered.
|
|
1320
|
+
* @returns The schema uses across all branches, or undefined when one cannot resolve.
|
|
1321
|
+
*/
|
|
1322
|
+
checkedBranches(types, checker, scope, walk, optional, collectionDepth) {
|
|
1323
|
+
const branches = types.map((type) => HandlerSources.fromCheckedType(type, checker, scope, walk, optional, collectionDepth));
|
|
1324
|
+
return branches.some((branch) => branch === undefined)
|
|
1325
|
+
? undefined
|
|
1326
|
+
: branches.flatMap((branch) => branch ?? []);
|
|
1327
|
+
},
|
|
1328
|
+
/**
|
|
1329
|
+
* Maps a checked generated message type to its schema reference.
|
|
1330
|
+
*
|
|
1331
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1332
|
+
* @param type The checked or declared type under inspection.
|
|
1333
|
+
* @returns The generated message schema, or undefined when unresolved.
|
|
1334
|
+
*/
|
|
1335
|
+
checkedMessage(type, scope) {
|
|
1336
|
+
const declaration = type.aliasSymbol?.declarations?.find(ts.isTypeAliasDeclaration) ??
|
|
1337
|
+
type.getSymbol()?.declarations?.find(ts.isInterfaceDeclaration);
|
|
1338
|
+
if (declaration === undefined)
|
|
1339
|
+
return HandlerSources.checkedEmptyMessage(type, scope);
|
|
1340
|
+
const source = declaration.getSourceFile();
|
|
1341
|
+
if (!/(^|\/)generated\/.+_pb\.ts$/u.test(source.fileName))
|
|
1342
|
+
return HandlerSources.checkedEmptyMessage(type, scope);
|
|
1343
|
+
const schemaName = `${declaration.name.text}Schema`;
|
|
1344
|
+
const exports = HandlerSources.exportedNames(source);
|
|
1345
|
+
if (!exports.types.has(declaration.name.text) || !exports.values.has(schemaName))
|
|
1346
|
+
return undefined;
|
|
1347
|
+
const path = relative(dirname(scope.source.fileName), source.fileName).replace(/\.ts$/u, ".js");
|
|
1348
|
+
return {
|
|
1349
|
+
kind: exports.schemaRoles.get(schemaName),
|
|
1350
|
+
reference: {
|
|
1351
|
+
moduleSpecifier: path.startsWith(".") ? path : `./${path}`,
|
|
1352
|
+
exportName: schemaName,
|
|
1353
|
+
},
|
|
1354
|
+
};
|
|
1355
|
+
},
|
|
1356
|
+
/**
|
|
1357
|
+
* Finds an empty generated message alias after TypeScript collapses its intersection.
|
|
1358
|
+
*
|
|
1359
|
+
* @param type The checked message type without a generated alias symbol.
|
|
1360
|
+
* @param scope The current source, imports, and compiler program.
|
|
1361
|
+
* @returns A generated schema only for an identical checked alias body.
|
|
1362
|
+
*/
|
|
1363
|
+
checkedEmptyMessage(type, scope) {
|
|
1364
|
+
if (type.aliasSymbol?.getName() !== "Message")
|
|
1365
|
+
return undefined;
|
|
1366
|
+
const checker = scope.program.getTypeChecker();
|
|
1367
|
+
for (const source of scope.program.getSourceFiles()) {
|
|
1368
|
+
if (!/(^|\/)generated\/.+_pb\.ts$/u.test(source.fileName))
|
|
1369
|
+
continue;
|
|
1370
|
+
const exports = HandlerSources.exportedNames(source);
|
|
1371
|
+
for (const statement of source.statements) {
|
|
1372
|
+
if (!ts.isTypeAliasDeclaration(statement))
|
|
1373
|
+
continue;
|
|
1374
|
+
const schemaName = `${statement.name.text}Schema`;
|
|
1375
|
+
if (!exports.types.has(statement.name.text) || !exports.values.has(schemaName))
|
|
1376
|
+
continue;
|
|
1377
|
+
if (checker.getTypeFromTypeNode(statement.type) !== type)
|
|
1378
|
+
continue;
|
|
1379
|
+
const path = relative(dirname(scope.source.fileName), source.fileName).replace(/\.ts$/u, ".js");
|
|
1380
|
+
return {
|
|
1381
|
+
kind: exports.schemaRoles.get(schemaName),
|
|
1382
|
+
reference: {
|
|
1383
|
+
moduleSpecifier: path.startsWith(".") ? path : `./${path}`,
|
|
1384
|
+
exportName: schemaName,
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return undefined;
|
|
1390
|
+
},
|
|
1391
|
+
/**
|
|
1392
|
+
* Collects generated schemas from a declared return type.
|
|
1393
|
+
*
|
|
1394
|
+
* @param scope The current source, imports, and compiler program.
|
|
1395
|
+
* @param typeNode The declared type node under inspection.
|
|
1396
|
+
* @param walk The bounded alias traversal state.
|
|
1397
|
+
* @param optional Whether a whole reaction result may be absent.
|
|
1398
|
+
* @param collectionDepth Number of enclosing array or tuple collections.
|
|
1399
|
+
* @returns The emitted schemas, or undefined when the declared type is unsupported.
|
|
1400
|
+
*/
|
|
1401
|
+
schemaListFromType(typeNode, scope, walk, optional = false, collectionDepth = 0) {
|
|
1402
|
+
if (!HandlerSources.consumeTypeWalk(walk))
|
|
1403
|
+
return undefined;
|
|
1404
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
1405
|
+
if (optional && unwrapped.kind === ts.SyntaxKind.UndefinedKeyword)
|
|
1406
|
+
return [];
|
|
1407
|
+
if (ts.isUnionTypeNode(unwrapped)) {
|
|
1408
|
+
const branches = unwrapped.types.map((branch) => HandlerSources.schemaListFromType(branch, scope, walk, optional, collectionDepth));
|
|
1409
|
+
return branches.some((branch) => branch === undefined)
|
|
1410
|
+
? undefined
|
|
1411
|
+
: branches.flatMap((branch) => branch ?? []);
|
|
1412
|
+
}
|
|
1413
|
+
if (ts.isArrayTypeNode(unwrapped) ||
|
|
1414
|
+
ts.isTupleTypeNode(unwrapped) ||
|
|
1415
|
+
(ts.isTypeReferenceNode(unwrapped) &&
|
|
1416
|
+
HandlerSources.isArrayReferenceType(unwrapped, scope.program))) {
|
|
1417
|
+
return HandlerSources.schemaListFromCollection(unwrapped, scope, walk, optional, collectionDepth);
|
|
1418
|
+
}
|
|
1419
|
+
const schema = HandlerSources.schemaUseFromType(unwrapped, scope.imports, walk);
|
|
1420
|
+
return schema === undefined ? undefined : [schema];
|
|
1421
|
+
},
|
|
1422
|
+
/**
|
|
1423
|
+
* Resolves a declared array or tuple collection return.
|
|
1424
|
+
*
|
|
1425
|
+
* @param typeNode Declared collection type.
|
|
1426
|
+
* @param scope The current source, imports, and compiler program.
|
|
1427
|
+
* @param walk Bounded alias traversal state.
|
|
1428
|
+
* @param optional Whether undefined is permitted in a reaction result.
|
|
1429
|
+
* @param collectionDepth Number of enclosing collections.
|
|
1430
|
+
* @returns Collection member schemas, or undefined for an invalid shape.
|
|
1431
|
+
*/
|
|
1432
|
+
schemaListFromCollection(typeNode, scope, walk, optional, collectionDepth) {
|
|
1433
|
+
if (collectionDepth > 0)
|
|
1434
|
+
return undefined;
|
|
1435
|
+
if (ts.isTupleTypeNode(typeNode))
|
|
1436
|
+
return HandlerSources.schemaListFromTuple(typeNode, scope, walk);
|
|
1437
|
+
const member = ts.isArrayTypeNode(typeNode)
|
|
1438
|
+
? typeNode.elementType
|
|
1439
|
+
: ts.isTypeReferenceNode(typeNode)
|
|
1440
|
+
? typeNode.typeArguments?.[0]
|
|
1441
|
+
: undefined;
|
|
1442
|
+
return member === undefined
|
|
1443
|
+
? undefined
|
|
1444
|
+
: HandlerSources.schemaListFromType(member, scope, walk, optional, collectionDepth + 1);
|
|
1445
|
+
},
|
|
1446
|
+
/**
|
|
1447
|
+
* Collects generated schemas from a tuple return type.
|
|
1448
|
+
*
|
|
1449
|
+
* @param scope The current source, imports, and compiler program.
|
|
1450
|
+
* @param typeNode The declared type node under inspection.
|
|
1451
|
+
* @param walk The bounded alias traversal state.
|
|
1452
|
+
* @returns The tuple's emitted schemas, or undefined when a member is invalid.
|
|
1453
|
+
*/
|
|
1454
|
+
schemaListFromTuple(typeNode, scope, walk) {
|
|
1455
|
+
const schemas = [];
|
|
1456
|
+
for (const element of typeNode.elements) {
|
|
1457
|
+
const member = HandlerSources.schemaFromTupleElement(element, scope, walk, true);
|
|
1458
|
+
if (member === undefined) {
|
|
1459
|
+
return undefined;
|
|
1460
|
+
}
|
|
1461
|
+
schemas.push(...member);
|
|
1462
|
+
}
|
|
1463
|
+
return schemas;
|
|
1464
|
+
},
|
|
1465
|
+
/**
|
|
1466
|
+
* Resolves a generated schema from one tuple member.
|
|
1467
|
+
*
|
|
1468
|
+
* @param scope The current source, imports, and compiler program.
|
|
1469
|
+
* @param typeNode The declared type node under inspection.
|
|
1470
|
+
* @param walk The bounded alias traversal state.
|
|
1471
|
+
* @param optional Whether this tuple member may be absent.
|
|
1472
|
+
* @returns The member's schema use, or undefined when it cannot resolve.
|
|
1473
|
+
*/
|
|
1474
|
+
schemaFromTupleElement(typeNode, scope, walk, optional) {
|
|
1475
|
+
const member = ts.isNamedTupleMember(typeNode) ? typeNode.type : typeNode;
|
|
1476
|
+
if (ts.isNamedTupleMember(typeNode) && typeNode.dotDotDotToken !== undefined)
|
|
1477
|
+
return undefined;
|
|
1478
|
+
if (ts.isRestTypeNode(member))
|
|
1479
|
+
return undefined;
|
|
1480
|
+
const candidate = HandlerSources.unwrapReadonly(ts.isOptionalTypeNode(member) ? member.type : member);
|
|
1481
|
+
const optionalMember = ts.isNamedTupleMember(typeNode)
|
|
1482
|
+
? typeNode.questionToken !== undefined
|
|
1483
|
+
: ts.isOptionalTypeNode(member);
|
|
1484
|
+
return HandlerSources.schemaListFromType(candidate, scope, walk, optional || optionalMember, 1);
|
|
1485
|
+
},
|
|
1486
|
+
/**
|
|
1487
|
+
* Wraps one resolved tuple schema as a schema list.
|
|
1488
|
+
*
|
|
1489
|
+
* @param imports The imports indexed for the current source.
|
|
1490
|
+
* @param typeNode The declared type node under inspection.
|
|
1491
|
+
* @param walk The bounded alias traversal state.
|
|
1492
|
+
* @returns The single schema as a list, or undefined when it is unresolved.
|
|
1493
|
+
*/
|
|
1494
|
+
singleSchema(typeNode, imports, walk) {
|
|
1495
|
+
const schema = HandlerSources.schemaUseFromType(typeNode, imports, walk);
|
|
1496
|
+
return schema === undefined ? undefined : [schema];
|
|
1497
|
+
},
|
|
1498
|
+
/**
|
|
1499
|
+
* Resolves a generated signal schema from a type node.
|
|
1500
|
+
*
|
|
1501
|
+
* @param imports The imports indexed for the current source.
|
|
1502
|
+
* @param typeNode The declared type node under inspection.
|
|
1503
|
+
* @param walk The bounded alias traversal state.
|
|
1504
|
+
* @returns The generated schema use, or undefined when unresolved.
|
|
1505
|
+
*/
|
|
1506
|
+
schemaUseFromType(typeNode, imports, walk = HandlerSources.newTypeWalk()) {
|
|
1507
|
+
if (typeNode === undefined) {
|
|
1508
|
+
return undefined;
|
|
1509
|
+
}
|
|
1510
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
1511
|
+
return undefined;
|
|
1512
|
+
}
|
|
1513
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
1514
|
+
if (!ts.isTypeReferenceNode(unwrapped)) {
|
|
1515
|
+
return undefined;
|
|
1516
|
+
}
|
|
1517
|
+
if (ts.isIdentifier(unwrapped.typeName)) {
|
|
1518
|
+
const alias = imports.localTypeAliases.get(unwrapped.typeName.text);
|
|
1519
|
+
if (alias !== undefined) {
|
|
1520
|
+
return HandlerSources.resolveAlias(unwrapped.typeName.text, alias, walk, (resolved) => HandlerSources.schemaUseFromType(resolved, imports, walk));
|
|
1521
|
+
}
|
|
1522
|
+
return HandlerSources.schemaUseFromSymbol(unwrapped.typeName.text, imports);
|
|
1523
|
+
}
|
|
1524
|
+
if (ts.isQualifiedName(unwrapped.typeName)) {
|
|
1525
|
+
return HandlerSources.schemaUseFromName(unwrapped.typeName, imports);
|
|
1526
|
+
}
|
|
1527
|
+
return undefined;
|
|
1528
|
+
},
|
|
1529
|
+
/**
|
|
1530
|
+
* Resolves an imported generated symbol to its schema.
|
|
1531
|
+
*
|
|
1532
|
+
* @param imports The imports indexed for the current source.
|
|
1533
|
+
* @param name The symbol, class, or member name to resolve.
|
|
1534
|
+
* @returns The generated schema use, or undefined when unresolved.
|
|
1535
|
+
*/
|
|
1536
|
+
schemaUseFromSymbol(name, imports) {
|
|
1537
|
+
const symbol = imports.generatedSymbols.get(name);
|
|
1538
|
+
if (symbol?.schemaExportName === undefined) {
|
|
1539
|
+
return undefined;
|
|
1540
|
+
}
|
|
1541
|
+
return {
|
|
1542
|
+
kind: symbol.kind,
|
|
1543
|
+
reference: { moduleSpecifier: symbol.moduleSpecifier, exportName: symbol.schemaExportName },
|
|
1544
|
+
};
|
|
1545
|
+
},
|
|
1546
|
+
/**
|
|
1547
|
+
* Resolves a qualified generated type name to its schema.
|
|
1548
|
+
*
|
|
1549
|
+
* @param imports The imports indexed for the current source.
|
|
1550
|
+
* @param name The symbol, class, or member name to resolve.
|
|
1551
|
+
* @returns The generated schema use, or undefined when unresolved.
|
|
1552
|
+
*/
|
|
1553
|
+
schemaUseFromName(name, imports) {
|
|
1554
|
+
if (!ts.isIdentifier(name.left)) {
|
|
1555
|
+
return undefined;
|
|
1556
|
+
}
|
|
1557
|
+
const namespace = imports.generatedNamespaces.get(name.left.text);
|
|
1558
|
+
const schemaName = `${name.right.text}Schema`;
|
|
1559
|
+
return namespace === undefined ||
|
|
1560
|
+
!HandlerTypes.hasGeneratedType(namespace.exports, name.right.text) ||
|
|
1561
|
+
!namespace.exports.values.has(schemaName)
|
|
1562
|
+
? undefined
|
|
1563
|
+
: {
|
|
1564
|
+
kind: namespace.exports.schemaRoles.get(schemaName),
|
|
1565
|
+
reference: { moduleSpecifier: namespace.moduleSpecifier, exportName: schemaName },
|
|
1566
|
+
};
|
|
1567
|
+
},
|
|
1568
|
+
/**
|
|
1569
|
+
* Resolves a generated entity name to its schema reference.
|
|
1570
|
+
*
|
|
1571
|
+
* @param imports The imports indexed for the current source.
|
|
1572
|
+
* @param name The symbol, class, or member name to resolve.
|
|
1573
|
+
* @returns The generated schema reference, or undefined when unresolved.
|
|
1574
|
+
*/
|
|
1575
|
+
schemaFromEntityName(name, imports) {
|
|
1576
|
+
if (ts.isIdentifier(name)) {
|
|
1577
|
+
const symbol = imports.generatedSymbols.get(name.text);
|
|
1578
|
+
return symbol?.schemaValue !== true
|
|
1579
|
+
? undefined
|
|
1580
|
+
: { moduleSpecifier: symbol.moduleSpecifier, exportName: symbol.exportName };
|
|
1581
|
+
}
|
|
1582
|
+
if (ts.isQualifiedName(name) && ts.isIdentifier(name.left)) {
|
|
1583
|
+
const namespace = imports.generatedNamespaces.get(name.left.text);
|
|
1584
|
+
return namespace?.exports.values.has(name.right.text) !== true
|
|
1585
|
+
? undefined
|
|
1586
|
+
: { moduleSpecifier: namespace.moduleSpecifier, exportName: name.right.text };
|
|
1587
|
+
}
|
|
1588
|
+
return undefined;
|
|
1589
|
+
},
|
|
1590
|
+
/**
|
|
1591
|
+
* Checks a framework Command or Event envelope return type.
|
|
1592
|
+
*
|
|
1593
|
+
* @param imports The imports indexed for the current source.
|
|
1594
|
+
* @param typeNode The declared type node under inspection.
|
|
1595
|
+
* @param walk The bounded alias traversal state.
|
|
1596
|
+
* @returns Whether the type is a framework Command or Event envelope.
|
|
1597
|
+
*/
|
|
1598
|
+
frameworkEnvelope(typeNode, imports, walk = HandlerSources.newTypeWalk()) {
|
|
1599
|
+
if (!HandlerSources.consumeTypeWalk(walk)) {
|
|
1600
|
+
return undefined;
|
|
1601
|
+
}
|
|
1602
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
1603
|
+
if (!ts.isTypeReferenceNode(unwrapped)) {
|
|
1604
|
+
return undefined;
|
|
1605
|
+
}
|
|
1606
|
+
if (ts.isIdentifier(unwrapped.typeName)) {
|
|
1607
|
+
const alias = imports.localTypeAliases.get(unwrapped.typeName.text);
|
|
1608
|
+
if (alias !== undefined) {
|
|
1609
|
+
return HandlerSources.resolveAlias(unwrapped.typeName.text, alias, walk, (resolved) => HandlerSources.frameworkEnvelope(resolved, imports, walk));
|
|
1610
|
+
}
|
|
1611
|
+
return imports.protoSymbols.has(unwrapped.typeName.text)
|
|
1612
|
+
? unwrapped.typeName.text
|
|
1613
|
+
: undefined;
|
|
1614
|
+
}
|
|
1615
|
+
if (ts.isQualifiedName(unwrapped.typeName) && ts.isIdentifier(unwrapped.typeName.left)) {
|
|
1616
|
+
const namespace = unwrapped.typeName.left.text;
|
|
1617
|
+
const name = unwrapped.typeName.right.text;
|
|
1618
|
+
return imports.protoNamespaces.has(namespace) && (name === "Event" || name === "Command")
|
|
1619
|
+
? `${namespace}.${name}`
|
|
1620
|
+
: undefined;
|
|
1621
|
+
}
|
|
1622
|
+
return undefined;
|
|
1623
|
+
},
|
|
1624
|
+
/**
|
|
1625
|
+
* Removes a Readonly wrapper from a declared type.
|
|
1626
|
+
*
|
|
1627
|
+
* @param typeNode The declared type node under inspection.
|
|
1628
|
+
* @returns The inner type when wrapped in Readonly, otherwise the original type.
|
|
1629
|
+
*/
|
|
1630
|
+
unwrapReadonly(typeNode) {
|
|
1631
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
1632
|
+
return HandlerSources.unwrapReadonly(typeNode.type);
|
|
1633
|
+
}
|
|
1634
|
+
if (ts.isTypeOperatorNode(typeNode) && typeNode.operator === ts.SyntaxKind.ReadonlyKeyword) {
|
|
1635
|
+
return HandlerSources.unwrapReadonly(typeNode.type);
|
|
1636
|
+
}
|
|
1637
|
+
return typeNode;
|
|
1638
|
+
},
|
|
1639
|
+
/**
|
|
1640
|
+
* Removes a top-level Promise wrapper from a declared return type.
|
|
1641
|
+
*
|
|
1642
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1643
|
+
* @param typeNode The declared type node under inspection.
|
|
1644
|
+
* @param walk The bounded alias traversal state.
|
|
1645
|
+
* @returns The inner type when wrapped in Promise, otherwise the original type.
|
|
1646
|
+
*/
|
|
1647
|
+
unwrapOuterPromise(typeNode, scope, walk = HandlerSources.newTypeWalk()) {
|
|
1648
|
+
if (!HandlerSources.consumeTypeWalk(walk))
|
|
1649
|
+
return typeNode;
|
|
1650
|
+
const unwrapped = HandlerSources.unwrapReadonly(typeNode);
|
|
1651
|
+
if (!ts.isTypeReferenceNode(unwrapped) || !ts.isIdentifier(unwrapped.typeName)) {
|
|
1652
|
+
return typeNode;
|
|
1653
|
+
}
|
|
1654
|
+
const alias = scope.imports.localTypeAliases.get(unwrapped.typeName.text);
|
|
1655
|
+
if (alias !== undefined && unwrapped.typeArguments?.length === undefined) {
|
|
1656
|
+
return (HandlerSources.resolveAlias(unwrapped.typeName.text, alias, walk, (resolved) => HandlerSources.unwrapOuterPromise(resolved, scope, walk)) ?? typeNode);
|
|
1657
|
+
}
|
|
1658
|
+
const imported = HandlerSources.importedPromiseAlias(unwrapped, scope);
|
|
1659
|
+
if (imported !== undefined) {
|
|
1660
|
+
return (HandlerSources.resolveAlias(unwrapped.typeName.text, imported, walk, (resolved) => HandlerSources.unwrapOuterPromise(resolved, scope, walk)) ?? typeNode);
|
|
1661
|
+
}
|
|
1662
|
+
if (unwrapped.typeName.text !== "Promise" || unwrapped.typeArguments?.length !== 1)
|
|
1663
|
+
return typeNode;
|
|
1664
|
+
if (!HandlerSources.isBuiltInPromise(unwrapped, scope.program))
|
|
1665
|
+
return typeNode;
|
|
1666
|
+
return unwrapped.typeArguments[0] ?? typeNode;
|
|
1667
|
+
},
|
|
1668
|
+
/**
|
|
1669
|
+
* Finds a concrete imported alias to the built-in Promise type.
|
|
1670
|
+
*
|
|
1671
|
+
* @param reference Imported type reference in a handler declaration.
|
|
1672
|
+
* @param scope Current compiler program and source context.
|
|
1673
|
+
* @returns The Promise alias body, or undefined for other references.
|
|
1674
|
+
*/
|
|
1675
|
+
importedPromiseAlias(reference, scope) {
|
|
1676
|
+
if (reference.typeArguments?.length)
|
|
1677
|
+
return undefined;
|
|
1678
|
+
const checker = scope.program.getTypeChecker();
|
|
1679
|
+
const symbol = checker.getSymbolAtLocation(reference.typeName);
|
|
1680
|
+
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Alias) === 0)
|
|
1681
|
+
return undefined;
|
|
1682
|
+
const alias = checker.getAliasedSymbol(symbol);
|
|
1683
|
+
const declaration = alias.declarations?.find(ts.isTypeAliasDeclaration);
|
|
1684
|
+
if (declaration === undefined || declaration.typeParameters?.length)
|
|
1685
|
+
return undefined;
|
|
1686
|
+
const body = HandlerSources.unwrapReadonly(declaration.type);
|
|
1687
|
+
return ts.isTypeReferenceNode(body) &&
|
|
1688
|
+
ts.isIdentifier(body.typeName) &&
|
|
1689
|
+
body.typeName.text === "Promise" &&
|
|
1690
|
+
HandlerSources.isBuiltInPromise(body, scope.program)
|
|
1691
|
+
? declaration.type
|
|
1692
|
+
: undefined;
|
|
1693
|
+
},
|
|
1694
|
+
/**
|
|
1695
|
+
* Checks whether a type reference resolves to the built-in Promise.
|
|
1696
|
+
*
|
|
1697
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
1698
|
+
* @param typeNode The declared type node under inspection.
|
|
1699
|
+
* @returns Whether the type is the built-in Promise.
|
|
1700
|
+
*/
|
|
1701
|
+
isBuiltInPromise(typeNode, program) {
|
|
1702
|
+
const symbol = program.getTypeChecker().getTypeFromTypeNode(typeNode).getSymbol();
|
|
1703
|
+
return (symbol?.getName() === "Promise" &&
|
|
1704
|
+
symbol.declarations?.some((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) === true);
|
|
1705
|
+
},
|
|
1706
|
+
/**
|
|
1707
|
+
* Accepts void or one built-in Promise<void> after concrete alias instantiation.
|
|
1708
|
+
*
|
|
1709
|
+
* @param typeNode Subscriber's declared return type.
|
|
1710
|
+
* @param program Compiler program resolving generic and imported aliases.
|
|
1711
|
+
* @returns Whether the concrete result contains exactly the permitted void shape.
|
|
1712
|
+
*/
|
|
1713
|
+
isSubscriberVoidReturn(typeNode, program) {
|
|
1714
|
+
const checker = program.getTypeChecker();
|
|
1715
|
+
const resolved = checker.getTypeFromTypeNode(typeNode);
|
|
1716
|
+
if ((resolved.flags & ts.TypeFlags.Void) !== 0)
|
|
1717
|
+
return true;
|
|
1718
|
+
if ((resolved.flags & ts.TypeFlags.Object) === 0)
|
|
1719
|
+
return false;
|
|
1720
|
+
const reference = resolved;
|
|
1721
|
+
const symbol = reference.getSymbol();
|
|
1722
|
+
if (symbol?.getName() !== "Promise" ||
|
|
1723
|
+
symbol.declarations?.some((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) !== true)
|
|
1724
|
+
return false;
|
|
1725
|
+
const arguments_ = checker.getTypeArguments(reference);
|
|
1726
|
+
return arguments_.length === 1 && ((arguments_[0]?.flags ?? 0) & ts.TypeFlags.Void) !== 0;
|
|
1727
|
+
},
|
|
1728
|
+
/**
|
|
1729
|
+
* Checks whether a declared type is explicitly void.
|
|
1730
|
+
*
|
|
1731
|
+
* @param typeNode The declared type node under inspection.
|
|
1732
|
+
* @returns Whether the declared type is explicitly void.
|
|
1733
|
+
*/
|
|
1734
|
+
isExplicitVoidType(typeNode) {
|
|
1735
|
+
if (typeNode === undefined) {
|
|
1736
|
+
return false;
|
|
1737
|
+
}
|
|
1738
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
1739
|
+
return HandlerSources.isExplicitVoidType(typeNode.type);
|
|
1740
|
+
}
|
|
1741
|
+
return typeNode.kind === ts.SyntaxKind.VoidKeyword;
|
|
1742
|
+
},
|
|
1743
|
+
/**
|
|
1744
|
+
* Checks whether a type reference resolves to a built-in array type.
|
|
1745
|
+
*
|
|
1746
|
+
* @param typeNode The declared type node under inspection.
|
|
1747
|
+
* @param program The program resolving the reference.
|
|
1748
|
+
* @returns Whether the reference is a built-in array type.
|
|
1749
|
+
*/
|
|
1750
|
+
isArrayReferenceType(typeNode, program) {
|
|
1751
|
+
if (!ts.isIdentifier(typeNode.typeName))
|
|
1752
|
+
return false;
|
|
1753
|
+
const name = typeNode.typeName.text;
|
|
1754
|
+
if (name !== "Array" && name !== "ReadonlyArray")
|
|
1755
|
+
return false;
|
|
1756
|
+
const symbol = program.getTypeChecker().getSymbolAtLocation(typeNode.typeName);
|
|
1757
|
+
return (symbol?.getName() === name &&
|
|
1758
|
+
symbol.declarations?.some((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile())) === true);
|
|
1759
|
+
},
|
|
1760
|
+
/**
|
|
1761
|
+
* Maps a handler decorator and input signal to the generated handler kind.
|
|
1762
|
+
*
|
|
1763
|
+
* @param decorator The decorator under inspection.
|
|
1764
|
+
* @param signalKind The resolved input signal kind.
|
|
1765
|
+
* @returns The generated handler kind for the decorator and input signal.
|
|
1766
|
+
*/
|
|
1767
|
+
handlerKind(decorator, signalKind) {
|
|
1768
|
+
switch (decorator) {
|
|
1769
|
+
case "Assign":
|
|
1770
|
+
return "command-assignment";
|
|
1771
|
+
case "Command":
|
|
1772
|
+
return signalKind === "command" ? "command-substitution" : "command-reaction";
|
|
1773
|
+
case "React":
|
|
1774
|
+
return "event-reaction";
|
|
1775
|
+
case "Subscribe":
|
|
1776
|
+
return signalKind === "state" ? "state-subscription" : "event-subscription";
|
|
1777
|
+
}
|
|
1778
|
+
},
|
|
1779
|
+
/**
|
|
1780
|
+
* Checks whether a decorator accepts the resolved input signal kind.
|
|
1781
|
+
*
|
|
1782
|
+
* @param decorator The decorator under inspection.
|
|
1783
|
+
* @param kind The signal, modifier, or syntax kind being checked.
|
|
1784
|
+
* @returns Whether the decorator accepts the input signal kind.
|
|
1785
|
+
*/
|
|
1786
|
+
acceptsSignalKind(decorator, kind) {
|
|
1787
|
+
if (decorator === "Command") {
|
|
1788
|
+
return kind === "command" || kind === "event" || kind === "rejection";
|
|
1789
|
+
}
|
|
1790
|
+
if (decorator === "Assign") {
|
|
1791
|
+
return kind === "command";
|
|
1792
|
+
}
|
|
1793
|
+
return (kind === "event" || kind === "rejection" || (decorator === "Subscribe" && kind === "state"));
|
|
1794
|
+
},
|
|
1795
|
+
/**
|
|
1796
|
+
* Describes the input signal expected by a handler decorator.
|
|
1797
|
+
*
|
|
1798
|
+
* @param decorator The decorator under inspection.
|
|
1799
|
+
* @returns The diagnostic description of the expected input signal.
|
|
1800
|
+
*/
|
|
1801
|
+
signalMessage(decorator) {
|
|
1802
|
+
if (decorator === "Command") {
|
|
1803
|
+
return "a generated command, event, or rejection type";
|
|
1804
|
+
}
|
|
1805
|
+
if (decorator === "Assign") {
|
|
1806
|
+
return "a generated command type";
|
|
1807
|
+
}
|
|
1808
|
+
return decorator === "Subscribe"
|
|
1809
|
+
? "a generated event, rejection, or Entity state type"
|
|
1810
|
+
: "a generated event or rejection type";
|
|
1811
|
+
},
|
|
1812
|
+
/**
|
|
1813
|
+
* Determines the generated signal kind emitted by a handler decorator.
|
|
1814
|
+
*
|
|
1815
|
+
* @param decorator The decorator under inspection.
|
|
1816
|
+
* @returns The emitted signal kind, or undefined for no emission.
|
|
1817
|
+
*/
|
|
1818
|
+
emittedSignalKind(decorator) {
|
|
1819
|
+
if (decorator === "Command") {
|
|
1820
|
+
return "command";
|
|
1821
|
+
}
|
|
1822
|
+
if (decorator === "Assign" || decorator === "React") {
|
|
1823
|
+
return "event";
|
|
1824
|
+
}
|
|
1825
|
+
return undefined;
|
|
1826
|
+
},
|
|
1827
|
+
/**
|
|
1828
|
+
* Finds the handler input type and domestic or external origin.
|
|
1829
|
+
*
|
|
1830
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
1831
|
+
* @param method The handler method name.
|
|
1832
|
+
* @param parameters The handler parameter declarations.
|
|
1833
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1834
|
+
* @returns The input type and domestic or external origin, or undefined when invalid.
|
|
1835
|
+
*/
|
|
1836
|
+
externalOrigin(parameters, scope, className, method) {
|
|
1837
|
+
const first = parameters[0]?.type;
|
|
1838
|
+
if (first === undefined)
|
|
1839
|
+
return undefined;
|
|
1840
|
+
const marker = HandlerSources.externalMarker(first, scope);
|
|
1841
|
+
const laterMarker = parameters
|
|
1842
|
+
.slice(1)
|
|
1843
|
+
.some((parameter) => parameter.type === undefined
|
|
1844
|
+
? false
|
|
1845
|
+
: HandlerSources.containsExternalMarker(parameter.type, scope));
|
|
1846
|
+
const nestedMarker = marker === undefined && HandlerSources.containsExternalMarker(first, scope);
|
|
1847
|
+
if (laterMarker || nestedMarker || (marker !== undefined && !marker.direct)) {
|
|
1848
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_EXTERNAL_ORIGIN", laterMarker ? (parameters[1]?.type ?? first) : first, "External<T> is valid only as the direct first receptor parameter type.", className, method);
|
|
1849
|
+
return undefined;
|
|
1850
|
+
}
|
|
1851
|
+
if (marker?.direct === true && marker.type !== undefined) {
|
|
1852
|
+
return { value: "external", type: marker.type };
|
|
1853
|
+
}
|
|
1854
|
+
return { value: "domestic", type: first };
|
|
1855
|
+
},
|
|
1856
|
+
/**
|
|
1857
|
+
* Checks the optional context parameter for the input signal kind.
|
|
1858
|
+
*
|
|
1859
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
1860
|
+
* @param method The handler method name.
|
|
1861
|
+
* @param parameters The handler parameter declarations.
|
|
1862
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1863
|
+
* @param signalKind The resolved input signal kind.
|
|
1864
|
+
* @returns Whether the context parameter matches the signal kind.
|
|
1865
|
+
*/
|
|
1866
|
+
validContextParameter(parameters, signalKind, scope, className, method) {
|
|
1867
|
+
if (parameters.length !== 2)
|
|
1868
|
+
return true;
|
|
1869
|
+
const context = parameters[1]?.type;
|
|
1870
|
+
const expected = signalKind === "command" ? "CommandContext" : "EventContext";
|
|
1871
|
+
if (context !== undefined && HandlerSources.isCanonicalContext(context, expected, scope)) {
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
HandlerTypes.pushDiagnostic(scope, "INVALID_HANDLER_CONTEXT", context ?? parameters[1] ?? scope.source, `Two-argument handlers receiving ${signalKind === "command" ? "Commands" : "Events, rejections, or Entity states"} must declare ${expected} as their second parameter.`, className, method);
|
|
1875
|
+
return false;
|
|
1876
|
+
},
|
|
1877
|
+
/**
|
|
1878
|
+
* Checks that a context type references the framework declaration.
|
|
1879
|
+
*
|
|
1880
|
+
* @param expected The canonical framework context type expected for this signal.
|
|
1881
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1882
|
+
* @param type The checked or declared type under inspection.
|
|
1883
|
+
* @returns Whether the type is the framework context declaration.
|
|
1884
|
+
*/
|
|
1885
|
+
isCanonicalContext(type, expected, scope) {
|
|
1886
|
+
if (!ts.isTypeReferenceNode(type))
|
|
1887
|
+
return false;
|
|
1888
|
+
if (ts.isIdentifier(type.typeName)) {
|
|
1889
|
+
return scope.imports.protoContextSymbols.get(type.typeName.text) === expected;
|
|
1890
|
+
}
|
|
1891
|
+
return (ts.isIdentifier(type.typeName.left) &&
|
|
1892
|
+
scope.imports.protoNamespaces.has(type.typeName.left.text) &&
|
|
1893
|
+
type.typeName.right.text === expected);
|
|
1894
|
+
},
|
|
1895
|
+
/**
|
|
1896
|
+
* Finds an External marker in a handler parameter type.
|
|
1897
|
+
*
|
|
1898
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1899
|
+
* @param type The checked or declared type under inspection.
|
|
1900
|
+
* @returns The type wrapped by External, or undefined when absent or invalid.
|
|
1901
|
+
*/
|
|
1902
|
+
externalMarker(type, scope) {
|
|
1903
|
+
if (!ts.isTypeReferenceNode(type))
|
|
1904
|
+
return undefined;
|
|
1905
|
+
const canonical = (ts.isIdentifier(type.typeName) &&
|
|
1906
|
+
scope.imports.serverSymbols.get(type.typeName.text) === "External") ||
|
|
1907
|
+
(ts.isQualifiedName(type.typeName) &&
|
|
1908
|
+
ts.isIdentifier(type.typeName.left) &&
|
|
1909
|
+
type.typeName.right.text === "External" &&
|
|
1910
|
+
scope.imports.serverNamespaces.has(type.typeName.left.text));
|
|
1911
|
+
if (!canonical || !HandlerSources.isCanonicalExternal(type.typeName, scope.program))
|
|
1912
|
+
return undefined;
|
|
1913
|
+
const argument = type.typeArguments?.[0];
|
|
1914
|
+
return argument === undefined
|
|
1915
|
+
? { direct: false }
|
|
1916
|
+
: { direct: type.typeArguments?.length === 1, type: argument };
|
|
1917
|
+
},
|
|
1918
|
+
/**
|
|
1919
|
+
* Finds nested type nodes for an External marker.
|
|
1920
|
+
*
|
|
1921
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
1922
|
+
* @param seen The declarations already visited during traversal.
|
|
1923
|
+
* @param type The checked or declared type under inspection.
|
|
1924
|
+
* @returns Whether a nested type contains the External marker.
|
|
1925
|
+
*/
|
|
1926
|
+
containsExternalMarker(type, scope, seen = new Set()) {
|
|
1927
|
+
if (HandlerSources.externalMarker(type, scope) !== undefined)
|
|
1928
|
+
return true;
|
|
1929
|
+
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName)) {
|
|
1930
|
+
const alias = scope.imports.localTypeAliases.get(type.typeName.text);
|
|
1931
|
+
if (alias !== undefined && !seen.has(type.typeName.text)) {
|
|
1932
|
+
const next = new Set(seen);
|
|
1933
|
+
next.add(type.typeName.text);
|
|
1934
|
+
if (HandlerSources.containsExternalMarker(alias, scope, next))
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
let found = false;
|
|
1939
|
+
const visit = (node) => {
|
|
1940
|
+
if (found)
|
|
1941
|
+
return;
|
|
1942
|
+
if (ts.isTypeNode(node) && HandlerSources.externalMarker(node, scope) !== undefined) {
|
|
1943
|
+
found = true;
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
ts.forEachChild(node, visit);
|
|
1947
|
+
};
|
|
1948
|
+
ts.forEachChild(type, visit);
|
|
1949
|
+
return found;
|
|
1950
|
+
},
|
|
1951
|
+
/**
|
|
1952
|
+
* Checks that an External type names the framework declaration.
|
|
1953
|
+
*
|
|
1954
|
+
* @param name The symbol, class, or member name to resolve.
|
|
1955
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
1956
|
+
* @returns Whether the type is the framework External declaration.
|
|
1957
|
+
*/
|
|
1958
|
+
isCanonicalExternal(name, program) {
|
|
1959
|
+
const checker = program.getTypeChecker();
|
|
1960
|
+
const location = ts.isQualifiedName(name) ? name.right : name;
|
|
1961
|
+
let symbol = checker.getSymbolAtLocation(location);
|
|
1962
|
+
if (symbol === undefined)
|
|
1963
|
+
return false;
|
|
1964
|
+
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
1965
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
1966
|
+
}
|
|
1967
|
+
return (symbol.declarations ?? []).some((declaration) => ts.isTypeAliasDeclaration(declaration) &&
|
|
1968
|
+
declaration.name.text === "External" &&
|
|
1969
|
+
PackageIdentity.nameFor(declaration.getSourceFile().fileName) ===
|
|
1970
|
+
"@spine-event-engine/server" &&
|
|
1971
|
+
/^external(?:\.d)?\.ts$/u.test(declaration.getSourceFile().fileName.split(/[\\/]/u).at(-1) ?? ""));
|
|
1972
|
+
},
|
|
1973
|
+
/**
|
|
1974
|
+
* Creates a bounded traversal state for recursive type aliases.
|
|
1975
|
+
*
|
|
1976
|
+
* @returns A fresh bounded alias traversal state.
|
|
1977
|
+
*/
|
|
1978
|
+
newTypeWalk() {
|
|
1979
|
+
return { remaining: maxAliasDepth, seen: new Set() };
|
|
1980
|
+
},
|
|
1981
|
+
/**
|
|
1982
|
+
* Checks one alias traversal step and rejects repeated or excessive recursion.
|
|
1983
|
+
*
|
|
1984
|
+
* @param walk The bounded alias traversal state.
|
|
1985
|
+
* @returns Whether alias traversal can continue.
|
|
1986
|
+
*/
|
|
1987
|
+
consumeTypeWalk(walk) {
|
|
1988
|
+
walk.remaining -= 1;
|
|
1989
|
+
return walk.remaining >= 0;
|
|
1990
|
+
},
|
|
1991
|
+
/**
|
|
1992
|
+
* Resolves a local type alias while guarding against recursive expansion.
|
|
1993
|
+
*
|
|
1994
|
+
* @param name The symbol, class, or member name to resolve.
|
|
1995
|
+
* @param resolveType The callback that inspects the expanded alias type.
|
|
1996
|
+
* @param typeNode The declared type node under inspection.
|
|
1997
|
+
* @param walk The bounded alias traversal state.
|
|
1998
|
+
* @typeParam T The result type produced by the alias callback.
|
|
1999
|
+
* @returns The callback result, or undefined when alias expansion cannot continue.
|
|
2000
|
+
*/
|
|
2001
|
+
resolveAlias(name, typeNode, walk, resolveType) {
|
|
2002
|
+
if (walk.seen.has(name)) {
|
|
2003
|
+
return undefined;
|
|
2004
|
+
}
|
|
2005
|
+
walk.seen.add(name);
|
|
2006
|
+
const resolved = resolveType(typeNode);
|
|
2007
|
+
walk.seen.delete(name);
|
|
2008
|
+
return resolved;
|
|
2009
|
+
},
|
|
2010
|
+
/**
|
|
2011
|
+
* Records framework and generated imports used by a source file.
|
|
2012
|
+
*
|
|
2013
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2014
|
+
* @param source The source file being inspected.
|
|
2015
|
+
* @returns The indexed imports and local aliases.
|
|
2016
|
+
*/
|
|
2017
|
+
buildImportState(source, program) {
|
|
2018
|
+
const state = {
|
|
2019
|
+
generatedNamespaces: new Map(),
|
|
2020
|
+
generatedSymbols: new Map(),
|
|
2021
|
+
rejectionNamespaces: new Map(),
|
|
2022
|
+
rejectionSymbols: new Map(),
|
|
2023
|
+
localTypeAliases: new Map(),
|
|
2024
|
+
serverNamespaces: new Set(),
|
|
2025
|
+
serverSymbols: new Map(),
|
|
2026
|
+
protoNamespaces: new Set(),
|
|
2027
|
+
protoSymbols: new Set(),
|
|
2028
|
+
protoContextSymbols: new Map(),
|
|
2029
|
+
};
|
|
2030
|
+
for (const statement of source.statements) {
|
|
2031
|
+
HandlerSources.recordImportStatement(statement, source, program, state);
|
|
2032
|
+
}
|
|
2033
|
+
return state;
|
|
2034
|
+
},
|
|
2035
|
+
/**
|
|
2036
|
+
* Records one source import or local type alias.
|
|
2037
|
+
*
|
|
2038
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2039
|
+
* @param source The source file being inspected.
|
|
2040
|
+
* @param state The mutable import index receiving declarations.
|
|
2041
|
+
* @param statement The declaration or import statement being indexed.
|
|
2042
|
+
*/
|
|
2043
|
+
recordImportStatement(statement, source, program, state) {
|
|
2044
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
2045
|
+
HandlerSources.recordImportDeclaration(statement, statement.moduleSpecifier.text, source, program, state);
|
|
2046
|
+
}
|
|
2047
|
+
if (ts.isTypeAliasDeclaration(statement)) {
|
|
2048
|
+
state.localTypeAliases.set(statement.name.text, statement.type);
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
/**
|
|
2052
|
+
* Records the named bindings of an import declaration.
|
|
2053
|
+
*
|
|
2054
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2055
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2056
|
+
* @param source The source file being inspected.
|
|
2057
|
+
* @param state The mutable import index receiving declarations.
|
|
2058
|
+
* @param statement The declaration or import statement being indexed.
|
|
2059
|
+
*/
|
|
2060
|
+
recordImportDeclaration(statement, moduleSpecifier, source, program, state) {
|
|
2061
|
+
const bindings = statement.importClause?.namedBindings;
|
|
2062
|
+
if (bindings === undefined)
|
|
2063
|
+
return;
|
|
2064
|
+
HandlerSources.recordFrameworkImport(bindings, moduleSpecifier, state);
|
|
2065
|
+
if (HandlerSources.isGeneratedModule(moduleSpecifier)) {
|
|
2066
|
+
HandlerSources.recordGeneratedImport(bindings, moduleSpecifier, statement.importClause?.phaseModifier !== ts.SyntaxKind.TypeKeyword, source, program, state);
|
|
2067
|
+
}
|
|
2068
|
+
if (!HandlerSources.isRejectionCompanion(moduleSpecifier))
|
|
2069
|
+
return;
|
|
2070
|
+
HandlerSources.recordRejectionCompanionImport(bindings, moduleSpecifier, source, program, state);
|
|
2071
|
+
},
|
|
2072
|
+
/**
|
|
2073
|
+
* Records declarations imported from a framework module.
|
|
2074
|
+
*
|
|
2075
|
+
* @param bindings The named import bindings being indexed.
|
|
2076
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2077
|
+
* @param state The mutable import index receiving declarations.
|
|
2078
|
+
*/
|
|
2079
|
+
recordFrameworkImport(bindings, moduleSpecifier, state) {
|
|
2080
|
+
if (moduleSpecifier === "@spine-event-engine/server") {
|
|
2081
|
+
HandlerSources.recordServerImport(bindings, state);
|
|
2082
|
+
}
|
|
2083
|
+
if (moduleSpecifier === "@spine-event-engine/proto") {
|
|
2084
|
+
HandlerSources.recordProtoImport(bindings, state);
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
/**
|
|
2088
|
+
* Records rejection schemas from a generated companion module.
|
|
2089
|
+
*
|
|
2090
|
+
* @param bindings The named import bindings being indexed.
|
|
2091
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2092
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2093
|
+
* @param source The source file being inspected.
|
|
2094
|
+
* @param state The mutable import index receiving declarations.
|
|
2095
|
+
*/
|
|
2096
|
+
recordRejectionCompanionImport(bindings, moduleSpecifier, source, program, state) {
|
|
2097
|
+
const schemaModule = HandlerSources.rejectionSchemaModule(moduleSpecifier);
|
|
2098
|
+
const companion = HandlerSources.generatedModuleSource(source, moduleSpecifier, program);
|
|
2099
|
+
const companionSchemaModule = companion === undefined ? undefined : HandlerSources.rejectionSchemaImport(companion);
|
|
2100
|
+
const exported = HandlerSources.generatedModuleExports(companion ?? source, companionSchemaModule ?? schemaModule, program);
|
|
2101
|
+
if (exported === undefined)
|
|
2102
|
+
return;
|
|
2103
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
2104
|
+
state.rejectionNamespaces.set(bindings.name.text, {
|
|
2105
|
+
exports: exported,
|
|
2106
|
+
moduleSpecifier: schemaModule,
|
|
2107
|
+
});
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
for (const element of bindings.elements) {
|
|
2111
|
+
if (element.isTypeOnly)
|
|
2112
|
+
continue;
|
|
2113
|
+
const exportName = element.propertyName?.text ?? element.name.text;
|
|
2114
|
+
const schemaName = `${exportName}Schema`;
|
|
2115
|
+
if (exported.schemaRoles.get(schemaName) !== "rejection")
|
|
2116
|
+
continue;
|
|
2117
|
+
state.rejectionSymbols.set(element.name.text, {
|
|
2118
|
+
moduleSpecifier: schemaModule,
|
|
2119
|
+
exportName: schemaName,
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
},
|
|
2123
|
+
/**
|
|
2124
|
+
* Finds the generated message module paired with a rejection companion.
|
|
2125
|
+
*
|
|
2126
|
+
* @param source The source file being inspected.
|
|
2127
|
+
* @returns The paired generated message module, or undefined when absent.
|
|
2128
|
+
*/
|
|
2129
|
+
rejectionSchemaImport(source) {
|
|
2130
|
+
for (const statement of source.statements) {
|
|
2131
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
const bindings = statement.importClause?.namedBindings;
|
|
2135
|
+
if (bindings !== undefined &&
|
|
2136
|
+
!ts.isNamespaceImport(bindings) &&
|
|
2137
|
+
bindings.elements.some((element) => (element.propertyName?.text ?? element.name.text).endsWith("Schema"))) {
|
|
2138
|
+
return statement.moduleSpecifier.text;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return undefined;
|
|
2142
|
+
},
|
|
2143
|
+
/**
|
|
2144
|
+
* Records server decorators and receiver bases from an import.
|
|
2145
|
+
*
|
|
2146
|
+
* @param bindings The named import bindings being indexed.
|
|
2147
|
+
* @param state The mutable import index receiving declarations.
|
|
2148
|
+
*/
|
|
2149
|
+
recordServerImport(bindings, state) {
|
|
2150
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
2151
|
+
state.serverNamespaces.add(bindings.name.text);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
for (const element of bindings.elements) {
|
|
2155
|
+
state.serverSymbols.set(element.name.text, element.propertyName?.text ?? element.name.text);
|
|
2156
|
+
}
|
|
2157
|
+
},
|
|
2158
|
+
/**
|
|
2159
|
+
* Records framework context and External types from an import.
|
|
2160
|
+
*
|
|
2161
|
+
* @param bindings The named import bindings being indexed.
|
|
2162
|
+
* @param state The mutable import index receiving declarations.
|
|
2163
|
+
*/
|
|
2164
|
+
recordProtoImport(bindings, state) {
|
|
2165
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
2166
|
+
state.protoNamespaces.add(bindings.name.text);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
for (const element of bindings.elements) {
|
|
2170
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
2171
|
+
if (imported === "Event" || imported === "Command") {
|
|
2172
|
+
state.protoSymbols.add(element.name.text);
|
|
2173
|
+
}
|
|
2174
|
+
if (imported === "CommandContext" || imported === "EventContext") {
|
|
2175
|
+
state.protoContextSymbols.set(element.name.text, imported);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
/**
|
|
2180
|
+
* Records generated message types and schema values from an import.
|
|
2181
|
+
*
|
|
2182
|
+
* @param bindings The named import bindings being indexed.
|
|
2183
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2184
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2185
|
+
* @param source The source file being inspected.
|
|
2186
|
+
* @param state The mutable import index receiving declarations.
|
|
2187
|
+
* @param valueImport Whether the import includes a runtime value.
|
|
2188
|
+
*/
|
|
2189
|
+
recordGeneratedImport(bindings, moduleSpecifier, valueImport, source, program, state) {
|
|
2190
|
+
const exported = HandlerSources.generatedModuleExports(source, moduleSpecifier, program);
|
|
2191
|
+
if (exported === undefined) {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
2195
|
+
state.generatedNamespaces.set(bindings.name.text, { exports: exported, moduleSpecifier });
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
for (const element of bindings.elements) {
|
|
2199
|
+
const exportName = element.propertyName?.text ?? element.name.text;
|
|
2200
|
+
if (!HandlerTypes.hasGeneratedType(exported, exportName)) {
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
const schemaExportName = `${exportName}Schema`;
|
|
2204
|
+
state.generatedSymbols.set(element.name.text, {
|
|
2205
|
+
exports: exported,
|
|
2206
|
+
moduleSpecifier,
|
|
2207
|
+
exportName,
|
|
2208
|
+
kind: exported.schemaRoles.get(schemaExportName),
|
|
2209
|
+
schemaExportName: exported.values.has(schemaExportName) ? schemaExportName : undefined,
|
|
2210
|
+
schemaValue: valueImport && !element.isTypeOnly && exported.values.has(exportName),
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
/**
|
|
2215
|
+
* Checks whether a module path denotes generated code.
|
|
2216
|
+
*
|
|
2217
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2218
|
+
* @returns Whether the module path denotes generated code.
|
|
2219
|
+
*/
|
|
2220
|
+
isGeneratedModule(moduleSpecifier) {
|
|
2221
|
+
return /(^|\/)generated\/.+_pb(\.js)?$/.test(moduleSpecifier);
|
|
2222
|
+
},
|
|
2223
|
+
/**
|
|
2224
|
+
* Checks whether a module path denotes a rejection companion.
|
|
2225
|
+
*
|
|
2226
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2227
|
+
* @returns Whether the module path denotes a rejection companion.
|
|
2228
|
+
*/
|
|
2229
|
+
isRejectionCompanion(moduleSpecifier) {
|
|
2230
|
+
return /(^|\/)generated\/(?:.+\/)?(?:[^/]+_)?rejections(\.js)?$/u.test(moduleSpecifier);
|
|
2231
|
+
},
|
|
2232
|
+
/**
|
|
2233
|
+
* Finds the message module paired with a rejection companion.
|
|
2234
|
+
*
|
|
2235
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2236
|
+
* @returns The paired generated message module path.
|
|
2237
|
+
*/
|
|
2238
|
+
rejectionSchemaModule(moduleSpecifier) {
|
|
2239
|
+
return moduleSpecifier.endsWith(".js")
|
|
2240
|
+
? moduleSpecifier.replace(/\.js$/u, "_pb.js")
|
|
2241
|
+
: `${moduleSpecifier}_pb`;
|
|
2242
|
+
},
|
|
2243
|
+
/**
|
|
2244
|
+
* Reads generated type, value, and schema exports from a module.
|
|
2245
|
+
*
|
|
2246
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2247
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2248
|
+
* @param source The source file being inspected.
|
|
2249
|
+
* @returns The generated module's indexed exports, or undefined when unresolved.
|
|
2250
|
+
*/
|
|
2251
|
+
generatedModuleExports(source, moduleSpecifier, program) {
|
|
2252
|
+
const module = HandlerSources.generatedModuleSource(source, moduleSpecifier, program);
|
|
2253
|
+
if (module === undefined)
|
|
2254
|
+
return undefined;
|
|
2255
|
+
const declarations = HandlerSources.exportedNames(module);
|
|
2256
|
+
const runtime = HandlerSources.pairedRuntimeExports(module);
|
|
2257
|
+
if (runtime === undefined)
|
|
2258
|
+
return declarations;
|
|
2259
|
+
return {
|
|
2260
|
+
types: declarations.types,
|
|
2261
|
+
values: runtime.values,
|
|
2262
|
+
schemaRoles: runtime.schemaRoles,
|
|
2263
|
+
};
|
|
2264
|
+
},
|
|
2265
|
+
/**
|
|
2266
|
+
* Finds runtime exports paired with generated type declarations.
|
|
2267
|
+
*
|
|
2268
|
+
* @param declarations The generated declaration source file.
|
|
2269
|
+
* @returns The paired runtime export index, or undefined when absent.
|
|
2270
|
+
*/
|
|
2271
|
+
pairedRuntimeExports(declarations) {
|
|
2272
|
+
if (!declarations.isDeclarationFile)
|
|
2273
|
+
return undefined;
|
|
2274
|
+
const runtimePath = declarations.fileName.replace(/\.d\.ts$/u, ".js");
|
|
2275
|
+
try {
|
|
2276
|
+
return HandlerSources.exportedNames(ts.createSourceFile(runtimePath, readFileSync(runtimePath, "utf8"), ts.ScriptTarget.Latest));
|
|
2277
|
+
}
|
|
2278
|
+
catch {
|
|
2279
|
+
return undefined;
|
|
2280
|
+
}
|
|
2281
|
+
},
|
|
2282
|
+
/**
|
|
2283
|
+
* Resolves the source file for a generated module.
|
|
2284
|
+
*
|
|
2285
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2286
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2287
|
+
* @param source The source file being inspected.
|
|
2288
|
+
* @returns The generated module source file, or undefined when unresolved.
|
|
2289
|
+
*/
|
|
2290
|
+
generatedModuleSource(source, moduleSpecifier, program) {
|
|
2291
|
+
const imported = HandlerSources.importedModuleSource(source, moduleSpecifier, program);
|
|
2292
|
+
if (imported !== undefined)
|
|
2293
|
+
return imported;
|
|
2294
|
+
const resolved = ts.resolveModuleName(moduleSpecifier, source.fileName, program.getCompilerOptions(), ts.sys).resolvedModule?.resolvedFileName;
|
|
2295
|
+
const base = resolve(dirname(source.fileName), moduleSpecifier);
|
|
2296
|
+
const candidates = HandlerSources.uniqueStrings([
|
|
2297
|
+
...(resolved === undefined ? [] : [resolved]),
|
|
2298
|
+
base.replace(/\.js$/, ".ts"),
|
|
2299
|
+
`${base}.ts`,
|
|
2300
|
+
base,
|
|
2301
|
+
base.replace(/\.js$/, ".d.ts"),
|
|
2302
|
+
`${base}.d.ts`,
|
|
2303
|
+
]);
|
|
2304
|
+
const sourceFiles = new Map(program.getSourceFiles().map((candidate) => [resolve(candidate.fileName), candidate]));
|
|
2305
|
+
for (const candidate of candidates) {
|
|
2306
|
+
const sourceFile = sourceFiles.get(candidate);
|
|
2307
|
+
if (sourceFile !== undefined)
|
|
2308
|
+
return sourceFile;
|
|
2309
|
+
}
|
|
2310
|
+
return undefined;
|
|
2311
|
+
},
|
|
2312
|
+
/**
|
|
2313
|
+
* Resolves a module import to a source file in the program.
|
|
2314
|
+
*
|
|
2315
|
+
* @param moduleSpecifier The imported module path to resolve.
|
|
2316
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2317
|
+
* @param source The source file being inspected.
|
|
2318
|
+
* @returns The imported source file, or undefined when unresolved.
|
|
2319
|
+
*/
|
|
2320
|
+
importedModuleSource(source, moduleSpecifier, program) {
|
|
2321
|
+
const declaration = source.statements.find((statement) => ts.isImportDeclaration(statement) &&
|
|
2322
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
2323
|
+
statement.moduleSpecifier.text === moduleSpecifier);
|
|
2324
|
+
if (declaration === undefined)
|
|
2325
|
+
return undefined;
|
|
2326
|
+
const symbol = program.getTypeChecker().getSymbolAtLocation(declaration.moduleSpecifier);
|
|
2327
|
+
const imported = symbol?.declarations
|
|
2328
|
+
?.map((candidate) => candidate.getSourceFile())
|
|
2329
|
+
.find((candidate) => candidate !== source);
|
|
2330
|
+
return imported;
|
|
2331
|
+
},
|
|
2332
|
+
/**
|
|
2333
|
+
* Returns strings while preserving their first occurrence.
|
|
2334
|
+
*
|
|
2335
|
+
* @param values The strings to deduplicate.
|
|
2336
|
+
* @returns The distinct strings in first-seen order.
|
|
2337
|
+
*/
|
|
2338
|
+
uniqueStrings(values) {
|
|
2339
|
+
return [...new Set(values)];
|
|
2340
|
+
},
|
|
2341
|
+
/**
|
|
2342
|
+
* Records generated type, value, and schema exports in a source file.
|
|
2343
|
+
*
|
|
2344
|
+
* @param source The source file being inspected.
|
|
2345
|
+
* @returns The indexed type, value, and schema exports.
|
|
2346
|
+
*/
|
|
2347
|
+
exportedNames(source) {
|
|
2348
|
+
const files = HandlerSources.generatedFiles(source);
|
|
2349
|
+
const schemaRoles = new Map();
|
|
2350
|
+
const exports = { types: new Set(), values: new Set(), schemaRoles };
|
|
2351
|
+
for (const statement of source.statements) {
|
|
2352
|
+
HandlerSources.recordExportedNames(statement, exports, files);
|
|
2353
|
+
}
|
|
2354
|
+
return exports;
|
|
2355
|
+
},
|
|
2356
|
+
/**
|
|
2357
|
+
* Records file descriptors declared by generated code.
|
|
2358
|
+
*
|
|
2359
|
+
* @param source The source file being inspected.
|
|
2360
|
+
* @returns The generated file descriptors indexed by declaration.
|
|
2361
|
+
*/
|
|
2362
|
+
generatedFiles(source) {
|
|
2363
|
+
const files = new Map();
|
|
2364
|
+
for (const statement of source.statements) {
|
|
2365
|
+
if (!ts.isVariableStatement(statement)) {
|
|
2366
|
+
continue;
|
|
2367
|
+
}
|
|
2368
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2369
|
+
if (!ts.isIdentifier(declaration.name)) {
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
const descriptor = HandlerSources.fileDescriptor(declaration.initializer);
|
|
2373
|
+
if (descriptor !== undefined) {
|
|
2374
|
+
files.set(declaration.name.text, descriptor);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
return files;
|
|
2379
|
+
},
|
|
2380
|
+
/**
|
|
2381
|
+
* Records an exported declaration and its schema role.
|
|
2382
|
+
*
|
|
2383
|
+
* @param exports The generated export index to update or inspect.
|
|
2384
|
+
* @param files The generated file descriptors indexed for this source.
|
|
2385
|
+
* @param statement The declaration or import statement being indexed.
|
|
2386
|
+
*/
|
|
2387
|
+
recordExportedNames(statement, exports, files) {
|
|
2388
|
+
if (ts.isVariableStatement(statement) &&
|
|
2389
|
+
HandlerTypes.hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
2390
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
2391
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
2392
|
+
exports.values.add(declaration.name.text);
|
|
2393
|
+
const schemaRole = HandlerSources.schemaRoleFromInitializer(declaration.name.text, declaration.initializer, files);
|
|
2394
|
+
if (schemaRole.found) {
|
|
2395
|
+
exports.schemaRoles.set(declaration.name.text, schemaRole.kind);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (HandlerTypes.hasModifier(statement, ts.SyntaxKind.ExportKeyword)) {
|
|
2401
|
+
HandlerTypes.recordNamedExport(statement, exports);
|
|
2402
|
+
}
|
|
2403
|
+
},
|
|
2404
|
+
/**
|
|
2405
|
+
* Determines a generated schema's signal role from its initializer.
|
|
2406
|
+
*
|
|
2407
|
+
* @param files The generated file descriptors indexed for this source.
|
|
2408
|
+
* @param initializer The generated declaration initializer to inspect.
|
|
2409
|
+
* @param schemaExportName The generated schema export name being classified.
|
|
2410
|
+
* @returns The schema's signal kind, or undefined when unknown.
|
|
2411
|
+
*/
|
|
2412
|
+
schemaRoleFromInitializer(schemaExportName, initializer, files) {
|
|
2413
|
+
const call = HandlerSources.callExpression(initializer, "messageDesc");
|
|
2414
|
+
if (call === undefined) {
|
|
2415
|
+
return { found: false, kind: undefined };
|
|
2416
|
+
}
|
|
2417
|
+
const fileName = call.arguments[0];
|
|
2418
|
+
if (fileName === undefined || !ts.isIdentifier(fileName)) {
|
|
2419
|
+
return { found: false, kind: undefined };
|
|
2420
|
+
}
|
|
2421
|
+
const file = files.get(fileName.text);
|
|
2422
|
+
if (file === undefined) {
|
|
2423
|
+
return { found: true, kind: undefined };
|
|
2424
|
+
}
|
|
2425
|
+
const indexes = HandlerSources.messageDescIndexes(call);
|
|
2426
|
+
const message = HandlerSources.descriptorMessageAt(file, indexes);
|
|
2427
|
+
const messageName = message?.exportName;
|
|
2428
|
+
const expectedName = schemaExportName.replace(/Schema$/, "");
|
|
2429
|
+
if (messageName === undefined || messageName !== expectedName) {
|
|
2430
|
+
return { found: true, kind: undefined };
|
|
2431
|
+
}
|
|
2432
|
+
return {
|
|
2433
|
+
found: true,
|
|
2434
|
+
kind: message?.descriptor.isEntityState
|
|
2435
|
+
? "state"
|
|
2436
|
+
: HandlerSources.signalKindFromProto(file.sourceFile, indexes),
|
|
2437
|
+
};
|
|
2438
|
+
},
|
|
2439
|
+
/**
|
|
2440
|
+
* Finds descriptor message indexes in a generated declaration call.
|
|
2441
|
+
*
|
|
2442
|
+
* @param call The call expression being inspected.
|
|
2443
|
+
* @returns The descriptor index path, or undefined when absent.
|
|
2444
|
+
*/
|
|
2445
|
+
messageDescIndexes(call) {
|
|
2446
|
+
const indexes = [];
|
|
2447
|
+
for (const argument of call.arguments.slice(1)) {
|
|
2448
|
+
if (!ts.isNumericLiteral(argument)) {
|
|
2449
|
+
return undefined;
|
|
2450
|
+
}
|
|
2451
|
+
const index = Number(argument.text);
|
|
2452
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
2453
|
+
return undefined;
|
|
2454
|
+
}
|
|
2455
|
+
indexes.push(index);
|
|
2456
|
+
}
|
|
2457
|
+
return indexes.length === 0 ? undefined : indexes;
|
|
2458
|
+
},
|
|
2459
|
+
/**
|
|
2460
|
+
* Gets a nested message descriptor by index path.
|
|
2461
|
+
*
|
|
2462
|
+
* @param file The generated file descriptor or source path under inspection.
|
|
2463
|
+
* @param indexes The nested descriptor index path.
|
|
2464
|
+
* @returns The selected nested descriptor, or undefined for invalid indexes.
|
|
2465
|
+
*/
|
|
2466
|
+
descriptorMessageAt(file, indexes) {
|
|
2467
|
+
if (indexes === undefined) {
|
|
2468
|
+
return undefined;
|
|
2469
|
+
}
|
|
2470
|
+
let messages = file.messages;
|
|
2471
|
+
let message;
|
|
2472
|
+
const path = [];
|
|
2473
|
+
for (const index of indexes) {
|
|
2474
|
+
message = messages[index];
|
|
2475
|
+
if (message === undefined) {
|
|
2476
|
+
return undefined;
|
|
2477
|
+
}
|
|
2478
|
+
path.push(message.name);
|
|
2479
|
+
messages = message.nested;
|
|
2480
|
+
}
|
|
2481
|
+
return message === undefined ? undefined : { descriptor: message, exportName: path.join("_") };
|
|
2482
|
+
},
|
|
2483
|
+
/**
|
|
2484
|
+
* Reads a file descriptor from a generated initializer.
|
|
2485
|
+
*
|
|
2486
|
+
* @param initializer The generated declaration initializer to inspect.
|
|
2487
|
+
* @returns The decoded file descriptor, or undefined when invalid.
|
|
2488
|
+
*/
|
|
2489
|
+
fileDescriptor(initializer) {
|
|
2490
|
+
const call = HandlerSources.callExpression(initializer, "fileDesc");
|
|
2491
|
+
const descriptor = call?.arguments[0];
|
|
2492
|
+
if (descriptor === undefined || !ts.isStringLiteralLike(descriptor)) {
|
|
2493
|
+
return undefined;
|
|
2494
|
+
}
|
|
2495
|
+
try {
|
|
2496
|
+
const { protobuf, protobufWkt } = PackageDependencies.current();
|
|
2497
|
+
const file = protobuf.fromBinary(protobufWkt.FileDescriptorProtoSchema, Buffer.from(descriptor.text, "base64"));
|
|
2498
|
+
return file.name === ""
|
|
2499
|
+
? undefined
|
|
2500
|
+
: {
|
|
2501
|
+
sourceFile: file.name,
|
|
2502
|
+
messages: file.messageType.map(HandlerSources.descriptorMessage),
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
catch {
|
|
2506
|
+
return undefined;
|
|
2507
|
+
}
|
|
2508
|
+
},
|
|
2509
|
+
/**
|
|
2510
|
+
* Removes an expression when it calls a named function.
|
|
2511
|
+
*
|
|
2512
|
+
* @param expression The expression being inspected.
|
|
2513
|
+
* @param functionName The expected called function name.
|
|
2514
|
+
* @returns The matching call expression, or undefined when the function differs.
|
|
2515
|
+
*/
|
|
2516
|
+
callExpression(expression, functionName) {
|
|
2517
|
+
if (expression === undefined) {
|
|
2518
|
+
return undefined;
|
|
2519
|
+
}
|
|
2520
|
+
const unwrapped = HandlerSources.unwrapExpression(expression);
|
|
2521
|
+
if (!ts.isCallExpression(unwrapped) || !ts.isIdentifier(unwrapped.expression)) {
|
|
2522
|
+
return undefined;
|
|
2523
|
+
}
|
|
2524
|
+
return unwrapped.expression.text === functionName ? unwrapped : undefined;
|
|
2525
|
+
},
|
|
2526
|
+
/**
|
|
2527
|
+
* Removes syntactic wrappers from an expression.
|
|
2528
|
+
*
|
|
2529
|
+
* @param expression The expression being inspected.
|
|
2530
|
+
* @returns The expression after removing syntactic wrappers.
|
|
2531
|
+
*/
|
|
2532
|
+
unwrapExpression(expression) {
|
|
2533
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
2534
|
+
return HandlerSources.unwrapExpression(expression.expression);
|
|
2535
|
+
}
|
|
2536
|
+
if (ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) {
|
|
2537
|
+
return HandlerSources.unwrapExpression(expression.expression);
|
|
2538
|
+
}
|
|
2539
|
+
return expression;
|
|
2540
|
+
},
|
|
2541
|
+
/**
|
|
2542
|
+
* Determines a generated message's signal kind from its descriptor.
|
|
2543
|
+
*
|
|
2544
|
+
* @param messageIndexes The nested message indexes within the file descriptor.
|
|
2545
|
+
* @param sourceFile The receiver source path.
|
|
2546
|
+
* @returns The message's signal kind, or undefined when unresolved.
|
|
2547
|
+
*/
|
|
2548
|
+
signalKindFromProto(sourceFile, messageIndexes) {
|
|
2549
|
+
const sourceName = sourceFile.split(/[\\/]/u).at(-1);
|
|
2550
|
+
if (sourceName === "rejections.proto" || sourceName?.endsWith("_rejections.proto") === true) {
|
|
2551
|
+
return messageIndexes?.length === 1 ? "rejection" : undefined;
|
|
2552
|
+
}
|
|
2553
|
+
if (sourceName === "commands.proto" || sourceName?.endsWith("_commands.proto") === true) {
|
|
2554
|
+
return "command";
|
|
2555
|
+
}
|
|
2556
|
+
if (sourceName === "events.proto" || sourceName?.endsWith("_events.proto") === true) {
|
|
2557
|
+
return "event";
|
|
2558
|
+
}
|
|
2559
|
+
return undefined;
|
|
2560
|
+
},
|
|
2561
|
+
/**
|
|
2562
|
+
* Reads a message name, entity option, and nested descriptors.
|
|
2563
|
+
*
|
|
2564
|
+
* @param message The descriptor message or diagnostic text being inspected.
|
|
2565
|
+
* @returns The message descriptor summary including nested messages.
|
|
2566
|
+
*/
|
|
2567
|
+
descriptorMessage(message) {
|
|
2568
|
+
return {
|
|
2569
|
+
name: message.name,
|
|
2570
|
+
isEntityState: message.options?.$unknown?.some((field) => field.no === entityOptionFieldNumber) === true,
|
|
2571
|
+
nested: message.nestedType.map(HandlerSources.descriptorMessage),
|
|
2572
|
+
};
|
|
2573
|
+
},
|
|
2574
|
+
});
|
|
2575
|
+
/**
|
|
2576
|
+
* Finds the package identity that owns an analyzed source file.
|
|
2577
|
+
*/
|
|
2578
|
+
export const PackageIdentity = Object.freeze({
|
|
2579
|
+
nameFor(sourceFile) {
|
|
2580
|
+
let directory = resolve(dirname(sourceFile));
|
|
2581
|
+
let manifest = join(directory, "package.json");
|
|
2582
|
+
while (!existsSync(manifest)) {
|
|
2583
|
+
const parent = dirname(directory);
|
|
2584
|
+
if (parent === directory)
|
|
2585
|
+
return undefined;
|
|
2586
|
+
directory = parent;
|
|
2587
|
+
manifest = join(directory, "package.json");
|
|
2588
|
+
}
|
|
2589
|
+
try {
|
|
2590
|
+
const { name } = JSON.parse(readFileSync(manifest, "utf8"));
|
|
2591
|
+
return typeof name === "string" ? name : undefined;
|
|
2592
|
+
}
|
|
2593
|
+
catch {
|
|
2594
|
+
return undefined;
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
});
|
|
2598
|
+
const PackageDependencies = Object.freeze({
|
|
2599
|
+
/**
|
|
2600
|
+
* Loads Protobuf dependencies for the analyzed program.
|
|
2601
|
+
*
|
|
2602
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2603
|
+
*/
|
|
2604
|
+
load(program) {
|
|
2605
|
+
packageDependencies ??= {
|
|
2606
|
+
protobuf: PackageDependencies.require("@bufbuild/protobuf", program),
|
|
2607
|
+
protobufWkt: PackageDependencies.require("@bufbuild/protobuf/wkt", program),
|
|
2608
|
+
};
|
|
2609
|
+
},
|
|
2610
|
+
/**
|
|
2611
|
+
* Returns the Protobuf dependencies loaded for analysis.
|
|
2612
|
+
*
|
|
2613
|
+
* @returns The loaded Protobuf runtime modules.
|
|
2614
|
+
*/
|
|
2615
|
+
current() {
|
|
2616
|
+
if (packageDependencies === undefined)
|
|
2617
|
+
throw new Error("Build handler analyzer dependencies are not loaded.");
|
|
2618
|
+
return packageDependencies;
|
|
2619
|
+
},
|
|
2620
|
+
/**
|
|
2621
|
+
* Resolves a dependency from the analyzer or application package boundary.
|
|
2622
|
+
*
|
|
2623
|
+
* @param program The TypeScript program supplying source files and type information.
|
|
2624
|
+
* @param specifier The dependency module specifier.
|
|
2625
|
+
* @returns The resolved module exports.
|
|
2626
|
+
*/
|
|
2627
|
+
require(specifier, program) {
|
|
2628
|
+
try {
|
|
2629
|
+
return createRequire(import.meta.url)(specifier);
|
|
2630
|
+
}
|
|
2631
|
+
catch (directError) {
|
|
2632
|
+
const appRoot = program.getRootFileNames()[0];
|
|
2633
|
+
if (appRoot === undefined)
|
|
2634
|
+
throw directError;
|
|
2635
|
+
const applicationRequire = createRequire(appRoot);
|
|
2636
|
+
try {
|
|
2637
|
+
return applicationRequire(specifier);
|
|
2638
|
+
}
|
|
2639
|
+
catch {
|
|
2640
|
+
// The in-repository generator transpiles this module into a temporary
|
|
2641
|
+
// cache directory. A fresh workspace install has the descriptor
|
|
2642
|
+
// package at the application's resolution boundary, while the
|
|
2643
|
+
// temporary module has no package boundary of its own.
|
|
2644
|
+
}
|
|
2645
|
+
try {
|
|
2646
|
+
const toolingEntry = applicationRequire.resolve("@spine-event-engine/proto-tools");
|
|
2647
|
+
return createRequire(join(PackageDependencies.rootFor(toolingEntry), "package.json"))(specifier);
|
|
2648
|
+
}
|
|
2649
|
+
catch {
|
|
2650
|
+
throw directError;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
},
|
|
2654
|
+
/**
|
|
2655
|
+
* Finds the package directory containing an entry point.
|
|
2656
|
+
*
|
|
2657
|
+
* @param entry The resolved package entry path.
|
|
2658
|
+
* @returns The containing package directory.
|
|
2659
|
+
*/
|
|
2660
|
+
rootFor(entry) {
|
|
2661
|
+
let directory = dirname(entry);
|
|
2662
|
+
while (!existsSync(join(directory, "package.json"))) {
|
|
2663
|
+
const parent = dirname(directory);
|
|
2664
|
+
if (parent === directory)
|
|
2665
|
+
throw new Error("Cannot locate proto-tools package manifest.");
|
|
2666
|
+
directory = parent;
|
|
2667
|
+
}
|
|
2668
|
+
return directory;
|
|
2669
|
+
},
|
|
2670
|
+
});
|
|
2671
|
+
const HandlerTypes = Object.freeze({
|
|
2672
|
+
/**
|
|
2673
|
+
* Records a generated declaration's type and value exports.
|
|
2674
|
+
*
|
|
2675
|
+
* @param exports The generated export index to update or inspect.
|
|
2676
|
+
* @param statement The declaration or import statement being indexed.
|
|
2677
|
+
*/
|
|
2678
|
+
recordNamedExport(statement, exports) {
|
|
2679
|
+
if (ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement)) {
|
|
2680
|
+
const name = statement.name;
|
|
2681
|
+
if (name !== undefined) {
|
|
2682
|
+
exports.types.add(name.text);
|
|
2683
|
+
exports.values.add(name.text);
|
|
2684
|
+
}
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (ts.isFunctionDeclaration(statement)) {
|
|
2688
|
+
const name = statement.name;
|
|
2689
|
+
if (name !== undefined) {
|
|
2690
|
+
exports.values.add(name.text);
|
|
2691
|
+
}
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) {
|
|
2695
|
+
exports.types.add(statement.name.text);
|
|
2696
|
+
}
|
|
2697
|
+
},
|
|
2698
|
+
/**
|
|
2699
|
+
* Checks whether generated exports contain a named type or value.
|
|
2700
|
+
*
|
|
2701
|
+
* @param exports The generated export index to update or inspect.
|
|
2702
|
+
* @param name The symbol, class, or member name to resolve.
|
|
2703
|
+
* @returns Whether the exports include the named generated type or value.
|
|
2704
|
+
*/
|
|
2705
|
+
hasGeneratedType(exports, name) {
|
|
2706
|
+
return exports.types.has(name) || exports.values.has(name);
|
|
2707
|
+
},
|
|
2708
|
+
/**
|
|
2709
|
+
* Checks a Spine server decorator name.
|
|
2710
|
+
*
|
|
2711
|
+
* @param name The symbol, class, or member name to resolve.
|
|
2712
|
+
* @returns The recognized server decorator name, or undefined otherwise.
|
|
2713
|
+
*/
|
|
2714
|
+
serverDecorator(name) {
|
|
2715
|
+
return name === "Assign" ||
|
|
2716
|
+
name === "Command" ||
|
|
2717
|
+
name === "React" ||
|
|
2718
|
+
name === "Subscribe" ||
|
|
2719
|
+
name === "Where" ||
|
|
2720
|
+
name === "Throws"
|
|
2721
|
+
? name
|
|
2722
|
+
: undefined;
|
|
2723
|
+
},
|
|
2724
|
+
/**
|
|
2725
|
+
* Reads a handler method's identifier or string literal name.
|
|
2726
|
+
*
|
|
2727
|
+
* @param node The syntax node being inspected.
|
|
2728
|
+
* @returns The identifier or string literal method name, or undefined otherwise.
|
|
2729
|
+
*/
|
|
2730
|
+
methodName(node) {
|
|
2731
|
+
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) {
|
|
2732
|
+
return node.name.text;
|
|
2733
|
+
}
|
|
2734
|
+
return undefined;
|
|
2735
|
+
},
|
|
2736
|
+
/**
|
|
2737
|
+
* Checks whether a syntax node carries a modifier.
|
|
2738
|
+
*
|
|
2739
|
+
* @param kind The signal, modifier, or syntax kind being checked.
|
|
2740
|
+
* @param node The syntax node being inspected.
|
|
2741
|
+
* @returns Whether the node has the requested modifier.
|
|
2742
|
+
*/
|
|
2743
|
+
hasModifier(node, kind) {
|
|
2744
|
+
return (ts.canHaveModifiers(node) &&
|
|
2745
|
+
(ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === kind));
|
|
2746
|
+
},
|
|
2747
|
+
/**
|
|
2748
|
+
* Reads a dotted name from an identifier or property access.
|
|
2749
|
+
*
|
|
2750
|
+
* @param expression The expression being inspected.
|
|
2751
|
+
* @returns The dotted expression name, or undefined for unsupported expressions.
|
|
2752
|
+
*/
|
|
2753
|
+
expressionName(expression) {
|
|
2754
|
+
if (ts.isIdentifier(expression)) {
|
|
2755
|
+
return expression.text;
|
|
2756
|
+
}
|
|
2757
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
2758
|
+
const prefix = HandlerTypes.expressionName(expression.expression);
|
|
2759
|
+
return prefix === undefined ? undefined : `${prefix}.${expression.name.text}`;
|
|
2760
|
+
}
|
|
2761
|
+
return undefined;
|
|
2762
|
+
},
|
|
2763
|
+
/**
|
|
2764
|
+
* Adds a located handler diagnostic to the analysis scope.
|
|
2765
|
+
*
|
|
2766
|
+
* @param className The receiver class name used in records and diagnostics.
|
|
2767
|
+
* @param code The diagnostic code to report.
|
|
2768
|
+
* @param message The descriptor message or diagnostic text being inspected.
|
|
2769
|
+
* @param methodNameValue The optional method name included in the diagnostic.
|
|
2770
|
+
* @param node The syntax node being inspected.
|
|
2771
|
+
* @param scope The current source, imports, program, and diagnostic collection.
|
|
2772
|
+
*/
|
|
2773
|
+
pushDiagnostic(scope, code, node, message, className, methodNameValue) {
|
|
2774
|
+
const location = scope.source.getLineAndCharacterOfPosition(node.getStart(scope.source));
|
|
2775
|
+
const base = {
|
|
2776
|
+
code,
|
|
2777
|
+
sourceFile: scope.source.fileName,
|
|
2778
|
+
line: location.line + 1,
|
|
2779
|
+
column: location.character + 1,
|
|
2780
|
+
message,
|
|
2781
|
+
};
|
|
2782
|
+
scope.diagnostics.push({
|
|
2783
|
+
...base,
|
|
2784
|
+
...(className === undefined ? {} : { className }),
|
|
2785
|
+
...(methodNameValue === undefined ? {} : { methodName: methodNameValue }),
|
|
2786
|
+
});
|
|
2787
|
+
},
|
|
2788
|
+
/**
|
|
2789
|
+
* Converts a TypeScript syntax error to a handler diagnostic.
|
|
2790
|
+
*
|
|
2791
|
+
* @param diagnostic The TypeScript diagnostic to convert.
|
|
2792
|
+
* @param source The source file being inspected.
|
|
2793
|
+
* @returns A located handler diagnostic for the syntax error.
|
|
2794
|
+
*/
|
|
2795
|
+
syntaxDiagnostic(source, diagnostic) {
|
|
2796
|
+
const location = source.getLineAndCharacterOfPosition(diagnostic.start);
|
|
2797
|
+
return {
|
|
2798
|
+
code: "TYPESCRIPT_SYNTAX_ERROR",
|
|
2799
|
+
sourceFile: source.fileName,
|
|
2800
|
+
line: location.line + 1,
|
|
2801
|
+
column: location.character + 1,
|
|
2802
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
|
|
2803
|
+
};
|
|
2804
|
+
},
|
|
2805
|
+
});
|
|
2806
|
+
//# sourceMappingURL=build-time-handler-analyzer.js.map
|