Skip to content
File

Blob: types/src/generator/structure.ts

typescript472 lines
1// Copyright (c) 2022-2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// TODO(soon): Fallthrough have false positives here. Investigate this.
6/* eslint-disable no-fallthrough */
7 
8import assert from 'node:assert';
9import {
10 Constant,
11 Member,
12 Member_Nested,
13 Member_Which,
14 Method,
15 Property,
16 Structure,
17} from '@workerd/jsg/rtti';
18import ts, { factory as f } from 'typescript';
19import { printNode } from '../print';
20import {
21 createParamDeclarationNodes,
22 createTypeNode,
23 getTypeName,
24 isUnsatisfiable,
25 maybeUnwrapOptional,
26} from './type';
27 
28export const FULLY_QUALIFIED_NAME_PREFIX = 'fqn$';
29 
30export function createMethodPartial(
31 fullyQualifiedParentName: string,
32 method: Method
33): [ts.Modifier[], string, ts.ParameterDeclaration[], ts.TypeNode] {
34 const modifiers: ts.Modifier[] = [];
35 if (method.static) {
36 modifiers.push(f.createToken(ts.SyntaxKind.StaticKeyword));
37 }
38 const name = method.name;
39 const params = createParamDeclarationNodes(
40 fullyQualifiedParentName,
41 name,
42 method.args.toArray(),
43 /* forMethod */ true
44 );
45 const result = createTypeNode(method.returnType);
46 return [modifiers, name, params, result];
47}
48 
49function createIteratorMethodPartial(
50 fullyQualifiedParentName: string,
51 method: Method,
52 isAsync: boolean
53): [ts.Modifier[], ts.PropertyName, ts.ParameterDeclaration[], ts.TypeNode] {
54 const [modifiers, , params, result] = createMethodPartial(
55 fullyQualifiedParentName,
56 method
57 );
58 const symbolIteratorExpression = f.createPropertyAccessExpression(
59 f.createIdentifier('Symbol'),
60 isAsync ? 'asyncIterator' : 'iterator'
61 );
62 const name = f.createComputedPropertyName(symbolIteratorExpression);
63 return [modifiers, name, params, result];
64}
65 
66function createInstancePropertyPartial(
67 prop: Property
68): [ts.Modifier[], string, ts.QuestionToken | undefined, ts.TypeNode] {
69 assert(!prop.prototype);
70 const modifiers: ts.Modifier[] = [];
71 if (prop.readonly) {
72 modifiers.push(f.createToken(ts.SyntaxKind.ReadonlyKeyword));
73 }
74 const name = prop.name;
75 let value = createTypeNode(prop.type);
76 
77 // If this is an optional type, use an optional property with a `?`
78 let questionToken: ts.QuestionToken | undefined;
79 const unwrappedValue = maybeUnwrapOptional(value);
80 if (unwrappedValue !== undefined) {
81 value = unwrappedValue;
82 questionToken = f.createToken(ts.SyntaxKind.QuestionToken);
83 }
84 
85 return [modifiers, name, questionToken, value];
86}
87 
88function createPrototypeProperty(
89 prop: Property
90):
91 | ts.GetAccessorDeclaration
92 | [ts.GetAccessorDeclaration, ts.SetAccessorDeclaration] {
93 assert(prop.prototype);
94 const name = prop.name;
95 const value = createTypeNode(prop.type);
96 
97 const getter = f.createGetAccessorDeclaration(
98 /* modifiers */ undefined,
99 name,
100 /* params */ [],
101 value,
102 /* body */ undefined
103 );
104 
105 if (prop.readonly) {
106 return getter;
107 } else {
108 const param = f.createParameterDeclaration(
109 /* modifiers */ undefined,
110 /* dotDotToken */ undefined,
111 'value',
112 /* questionToken */ undefined,
113 value
114 );
115 const setter = f.createSetAccessorDeclaration(
116 /* modifiers */ undefined,
117 name,
118 [param],
119 /* body */ undefined
120 );
121 return [getter, setter];
122 }
123}
124 
125function createNestedPartial(nested: Member_Nested): [string, ts.TypeNode] {
126 const name = nested.name;
127 const targetName = getTypeName(nested.structure);
128 const value = f.createTypeQueryNode(f.createIdentifier(targetName));
129 // Custom `name` will be empty string if omitted, so `??` wouldn't work here
130 return [name || targetName, value];
131}
132 
133function createConstantPartial(
134 constant: Constant
135): [ts.Modifier[], string, ts.TypeNode] {
136 const modifiers: ts.Modifier[] = [
137 f.createToken(ts.SyntaxKind.StaticKeyword),
138 f.createToken(ts.SyntaxKind.ReadonlyKeyword),
139 ];
140 const name = constant.name;
141 // Rather than using the constant value as a literal type here, just type them
142 // as `number` to encourage people to use them as constants. This is also what
143 // TypeScript does in its official lib types.
144 const valueNode = f.createTypeReferenceNode('number');
145 return [modifiers, name, valueNode];
146}
147 
148function createInterfaceMemberNode(
149 fullyQualifiedInterfaceName: string,
150 member: Member
151): ts.TypeElement | ts.TypeElement[] {
152 let modifiers: ts.Modifier[];
153 let name: string;
154 let params: ts.ParameterDeclaration[];
155 let result: ts.TypeNode;
156 let questionToken: ts.QuestionToken | undefined;
157 
158 const which = member.which();
159 // noinspection FallThroughInSwitchStatementJS
160 switch (which) {
161 case Member_Which.METHOD: {
162 const method = member.method;
163 [modifiers, name, params, result] = createMethodPartial(
164 fullyQualifiedInterfaceName,
165 method
166 );
167 return f.createMethodSignature(
168 modifiers,
169 name,
170 /* questionToken */ undefined,
171 /* typeParams */ undefined,
172 params,
173 result
174 );
175 }
176 case Member_Which.PROPERTY: {
177 const prop = member.property;
178 if (prop.prototype) {
179 return createPrototypeProperty(prop);
180 } else {
181 [modifiers, name, questionToken, result] =
182 createInstancePropertyPartial(prop);
183 return f.createPropertySignature(
184 modifiers,
185 name,
186 questionToken,
187 result
188 );
189 }
190 }
191 case Member_Which.NESTED: {
192 const nested = member.nested;
193 [name, result] = createNestedPartial(nested);
194 return f.createPropertySignature(
195 /* modifiers */ undefined,
196 name,
197 /* questionToken */ undefined,
198 result
199 );
200 }
201 case Member_Which.CONSTANT: {
202 const constant = member.constant;
203 [modifiers, name, result] = createConstantPartial(constant);
204 return f.createPropertySignature(
205 [f.createToken(ts.SyntaxKind.ReadonlyKeyword)],
206 name,
207 /* questionToken */ undefined,
208 result
209 );
210 }
211 case Member_Which.CONSTRUCTOR: {
212 assert.fail('Unexpected constructor member inside interface');
213 }
214 default: {
215 assert.fail(`Unknown member: ${which satisfies never}`);
216 }
217 }
218}
219 
220function createIteratorInterfaceMemberNode(
221 fullyQualifiedInterfaceName: string,
222 method: Method,
223 isAsync: boolean
224): ts.TypeElement {
225 const [modifiers, name, params, result] = createIteratorMethodPartial(
226 fullyQualifiedInterfaceName,
227 method,
228 isAsync
229 );
230 return f.createMethodSignature(
231 modifiers,
232 name,
233 /* questionToken */ undefined,
234 /* typeParams */ undefined,
235 params,
236 result
237 );
238}
239 
240function createClassMemberNode(
241 fullyQualifiedClassName: string,
242 member: Member
243): ts.ClassElement | ts.ClassElement[] {
244 let modifiers: ts.Modifier[];
245 let name: string;
246 let params: ts.ParameterDeclaration[];
247 let result: ts.TypeNode;
248 let questionToken: ts.QuestionToken | undefined;
249 
250 const which = member.which();
251 switch (which) {
252 case Member_Which.METHOD: {
253 const method = member.method;
254 [modifiers, name, params, result] = createMethodPartial(
255 fullyQualifiedClassName,
256 method
257 );
258 return f.createMethodDeclaration(
259 modifiers,
260 /* asteriskToken */ undefined,
261 name,
262 /* questionToken */ undefined,
263 /* typeParameters */ undefined,
264 params,
265 result,
266 /* body */ undefined
267 );
268 }
269 case Member_Which.PROPERTY: {
270 const prop = member.property;
271 if (prop.prototype) {
272 return createPrototypeProperty(prop);
273 } else {
274 [modifiers, name, questionToken, result] =
275 createInstancePropertyPartial(prop);
276 return f.createPropertyDeclaration(
277 modifiers,
278 name,
279 questionToken,
280 result,
281 /* initializer */ undefined
282 );
283 }
284 }
285 case Member_Which.NESTED: {
286 const nested = member.nested;
287 [name, result] = createNestedPartial(nested);
288 return f.createPropertyDeclaration(
289 /* modifiers */ undefined,
290 name,
291 /* questionToken */ undefined,
292 result,
293 /* initializer */ undefined
294 );
295 }
296 case Member_Which.CONSTANT: {
297 const constant = member.constant;
298 [modifiers, name, result] = createConstantPartial(constant);
299 return f.createPropertyDeclaration(
300 modifiers,
301 name,
302 /* questionToken */ undefined,
303 result,
304 /* initializer */ undefined
305 );
306 }
307 case Member_Which.CONSTRUCTOR: {
308 const constructor = member.$constructor;
309 params = createParamDeclarationNodes(
310 fullyQualifiedClassName,
311 'constructor',
312 constructor.args.toArray(),
313 /* forMethod */ true
314 );
315 return f.createConstructorDeclaration(
316 /* modifiers */ undefined,
317 params,
318 /* body */ undefined
319 );
320 }
321 default:
322 assert.fail(`Unknown member: ${which satisfies never}`);
323 }
324}
325 
326function createIteratorClassMemberNode(
327 fullyQualifiedClassName: string,
328 method: Method,
329 isAsync: boolean
330): ts.ClassElement {
331 const [modifiers, name, params, result] = createIteratorMethodPartial(
332 fullyQualifiedClassName,
333 method,
334 isAsync
335 );
336 return f.createMethodDeclaration(
337 modifiers,
338 /* asteriskToken */ undefined,
339 name,
340 /* questionToken */ undefined,
341 /* typeParams */ undefined,
342 params,
343 result,
344 /* body */ undefined
345 );
346}
347 
348// Remove all properties with type `never` and methods with return type `never`
349function filterUnimplementedProperties<
350 T extends ts.TypeElement | ts.ClassElement,
351>(members: T[]): T[] {
352 return members.filter((member) => {
353 // Could collapse these `if` statements, but this is much clearer
354 if (
355 ts.isPropertySignature(member) ||
356 ts.isPropertyDeclaration(member) ||
357 ts.isGetAccessorDeclaration(member) ||
358 ts.isSetAccessorDeclaration(member) ||
359 ts.isMethodSignature(member) ||
360 ts.isMethodDeclaration(member)
361 ) {
362 if (member.type !== undefined && isUnsatisfiable(member.type)) {
363 return false;
364 }
365 }
366 return true;
367 });
368}
369 
370export interface CreateStructureNodeOptions {
371 asClass: boolean;
372 ambientContext?: boolean;
373}
374export function createStructureNode(
375 structure: Structure,
376 opts: CreateStructureNodeOptions
377): ts.InterfaceDeclaration | ts.ClassDeclaration {
378 const { asClass, ambientContext } = opts;
379 const modifiers: ts.Modifier[] = [];
380 const name = getTypeName(structure);
381 const fullyQualifiedName = structure.fullyQualifiedName;
382 
383 const heritage: ts.HeritageClause[] = [];
384 if (structure._hasExtends()) {
385 const typeNode = createTypeNode(structure.extends);
386 assert(
387 ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName),
388 `Expected type reference, got "${printNode(typeNode)}"`
389 );
390 const expr = f.createExpressionWithTypeArguments(
391 typeNode.typeName,
392 typeNode.typeArguments
393 );
394 heritage.push(f.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [expr]));
395 }
396 
397 const members = structure.members;
398 if (asClass) {
399 // Should only add `declare` if we're not already in an ambient context
400 if (!ambientContext) {
401 modifiers.push(f.createToken(ts.SyntaxKind.DeclareKeyword));
402 }
403 
404 // Can't use `flatMap()` here as `members` is a `capnp.List`
405 const classMembers = members
406 .map((member) => createClassMemberNode(fullyQualifiedName, member))
407 .flat();
408 
409 const constructorIndex = classMembers.findIndex((member) =>
410 ts.isConstructorDeclaration(member)
411 );
412 if (constructorIndex === -1) {
413 // If this class doesn't have a constructor, it must be `abstract`, as we
414 // never rely on the implicit default constructor. If a class can be
415 // constructed using the empty constructor, it always defines it.
416 modifiers.push(f.createToken(ts.SyntaxKind.AbstractKeyword));
417 } else {
418 // Otherwise, ensure that the constructor always comes first
419 classMembers.unshift(...classMembers.splice(constructorIndex, 1));
420 }
421 
422 // Add iterator members
423 if (structure._hasIterator()) {
424 const iterator = structure.iterator;
425 classMembers.push(
426 createIteratorClassMemberNode(fullyQualifiedName, iterator, false)
427 );
428 }
429 if (structure._hasAsyncIterator()) {
430 const iterator = structure.asyncIterator;
431 classMembers.push(
432 createIteratorClassMemberNode(fullyQualifiedName, iterator, true)
433 );
434 }
435 
436 return f.createClassDeclaration(
437 modifiers,
438 name,
439 /* typeParams */ undefined,
440 heritage,
441 filterUnimplementedProperties(classMembers)
442 );
443 } else {
444 // Can't use `flatMap()` here as `members` is a `capnp.List`
445 const interfaceMembers = members
446 .map((member) => createInterfaceMemberNode(fullyQualifiedName, member))
447 .flat();
448 
449 // Add iterator members
450 if (structure._hasIterator()) {
451 const iterator = structure.iterator;
452 interfaceMembers.push(
453 createIteratorInterfaceMemberNode(fullyQualifiedName, iterator, false)
454 );
455 }
456 if (structure._hasAsyncIterator()) {
457 const iterator = structure.asyncIterator;
458 interfaceMembers.push(
459 createIteratorInterfaceMemberNode(fullyQualifiedName, iterator, true)
460 );
461 }
462 
463 return f.createInterfaceDeclaration(
464 modifiers,
465 name,
466 /* typeParams */ undefined,
467 heritage,
468 filterUnimplementedProperties(interfaceMembers)
469 );
470 }
471}