Skip to content
File

Blob: types/src/generator/type.ts

typescript420 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 ArrayType,
11 BuiltinType_Type,
12 JsgImplType_Type,
13 MaybeType,
14 NumberType,
15 Structure,
16 StructureType,
17 Type,
18 Type_Which,
19} from '@workerd/jsg/rtti';
20import ts, { factory as f } from 'typescript';
21import { printNode } from '../print';
22import { getParameterName } from './parameter-names';
23 
24// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex
25export function findLastIndex<T>(
26 array: T[],
27 predicate: (value: T, index: number, array: T[]) => unknown
28): number {
29 for (let i = array.length - 1; i >= 0; i--) {
30 if (predicate(array[i], i, array)) return i;
31 }
32 return -1;
33}
34 
35// If `typeNode` has the shape `T | undefined`, returns `T`, otherwise returns
36// `undefined`.
37export function maybeUnwrapOptional(
38 typeNode: ts.TypeNode
39): ts.TypeNode | undefined {
40 if (
41 ts.isUnionTypeNode(typeNode) &&
42 typeNode.types.length === 2 &&
43 ts.isTypeReferenceNode(typeNode.types[1]) &&
44 ts.isIdentifier(typeNode.types[1].typeName) &&
45 typeNode.types[1].typeName.escapedText === 'undefined' // eslint-disable-line @typescript-eslint/no-unsafe-enum-comparison
46 ) {
47 return typeNode.types[0];
48 }
49 return undefined;
50}
51 
52// Returns `true` iff this maybe type represents `T | null`, not `T | undefined`
53function isNullMaybe(maybe: MaybeType): boolean {
54 // https://github.com/cloudflare/workerd/blob/33e692f2216704b7226c8c59b1455eefedf79068/src/workerd/jsg/jsg.h#L220-L221
55 return maybe.name === 'kj::Maybe';
56}
57 
58// Returns `true` iff this number type represents a character
59function isCharNumber(number: NumberType): boolean {
60 // https://github.com/cloudflare/workerd/blob/33e692f2216704b7226c8c59b1455eefedf79068/src/workerd/jsg/rtti.h#L158
61 const name = number.name;
62 return name === 'char';
63}
64 
65// Returns `true` iff this number type represents a byte
66function isByteNumber(number: NumberType): boolean {
67 // https://github.com/cloudflare/workerd/blob/33e692f2216704b7226c8c59b1455eefedf79068/src/workerd/jsg/rtti.h#L160
68 const name = number.name;
69 return name === 'unsigned char';
70}
71 
72// Returns `true` iff this number type represents `number | bigint`
73function isBigNumber(number: NumberType): boolean {
74 // https://github.com/cloudflare/workerd/blob/33e692f2216704b7226c8c59b1455eefedf79068/src/workerd/jsg/README.md?plain=1#L56-L82
75 // https://github.com/cloudflare/workerd/blob/33e692f2216704b7226c8c59b1455eefedf79068/src/workerd/jsg/rtti.h#L157-L167
76 const name = number.name;
77 return (
78 name === 'long' ||
79 name === 'unsigned long' ||
80 name === 'long long' ||
81 name === 'unsigned long long' ||
82 name === 'jsg::JsBigInt'
83 );
84}
85 
86// Returns `true` iff this array type represents a pointer to an array
87function isArrayPointer(array: ArrayType): boolean {
88 return array.name === 'kj::ArrayPtr';
89}
90 
91// Returns `true` iff this array type represents an iterable
92function isIterable(array: ArrayType): boolean {
93 // https://github.com/cloudflare/workerd/blob/33e692f2216704b7226c8c59b1455eefedf79068/src/workerd/jsg/README.md?plain=1#L185-L186
94 return array.name === 'jsg::Sequence';
95}
96 
97// Returns `true` iff `typeNode` is `never`
98export function isUnsatisfiable(typeNode: ts.TypeNode): boolean {
99 const isNeverTypeReference =
100 ts.isTypeReferenceNode(typeNode) &&
101 ts.isIdentifier(typeNode.typeName) &&
102 typeNode.typeName.text === 'never';
103 const isNeverKeyword =
104 ts.isToken(typeNode) && typeNode.kind == ts.SyntaxKind.NeverKeyword;
105 return isNeverTypeReference || isNeverKeyword;
106}
107 
108// Strings to replace in fully-qualified structure names with nothing
109// `workerd` references APIs by fully qualified names such as `workerd::api::Whatever`
110// This wouldn't be all that user-friendly as a type, and so this regex captures all the
111// parts of an API name that should be removed when turning an API into a TS type
112// For instance, this turns `workerd::api::Whatever` into `Whatever`
113// If any new namespaced APIs are added, they should be added to this regex.
114// If they're _not_ added to this regex, a sane-ish fallback will be used.
115// For instance, a new hypothetical API called `workerd::api::magic::MakeASpell` would be
116// `magicMakeASpell`.
117const replaceEmpty =
118 /^workerd::api::public_beta::|^workerd::api::urlpattern::|^workerd::api::node::|^workerd::api::user_tracing::|^workerd::api::|^workerd::jsg::|::|[ >]/g;
119// Strings to replace in fully-qualified structure names with an underscore
120const replaceUnderscore = /[<,]/g;
121export function getTypeName(
122 structure: Structure | StructureType | /* fullyQualifiedName */ string
123): string {
124 let name: string;
125 if (typeof structure === 'string') {
126 assert(
127 structure.includes('::'),
128 `Expected fully-qualified structure name, got "${structure}"`
129 );
130 name = structure;
131 } else {
132 name = structure.fullyQualifiedName;
133 }
134 name = name.replace(replaceEmpty, '');
135 name = name.replace(replaceUnderscore, '_');
136 return name;
137}
138 
139export function createParamDeclarationNodes(
140 fullyQualifiedParentName: string,
141 name: string,
142 args: Type[],
143 forMethod = false
144): ts.ParameterDeclaration[] {
145 // Find the index of the last required parameter, all optional before this
146 // will use the `| undefined` syntax, as opposed to a `?` token.
147 const lastRequiredParameter = findLastIndex(args, (type) => {
148 // Could simplify this to a single return, but this reads clearer
149 if (type._isMaybe && !isNullMaybe(type.maybe)) {
150 // `type` is `T | undefined` so optional
151 return false;
152 }
153 // noinspection RedundantIfStatementJS
154 if (type._isJsgImpl) {
155 // `type` is varargs or internal implementation type so optional
156 return false;
157 }
158 return true;
159 });
160 
161 // `args` may include internal implementation types that shouldn't appear
162 // in parameters. Therefore, we may end up with fewer params than args.
163 const params: ts.ParameterDeclaration[] = [];
164 
165 for (let i = 0; i < args.length; i++) {
166 const arg = args[i];
167 let typeNode = createTypeNode(
168 arg,
169 true, // Always allow coercion in function params
170 forMethod // Allow additional coercion in method params
171 );
172 
173 let dotDotDotToken: ts.DotDotDotToken | undefined;
174 let questionToken: ts.QuestionToken | undefined;
175 
176 const which = arg.which();
177 if (which === Type_Which.MAYBE) {
178 // If this is an optional type, and we don't have any required args
179 // left, use an optional parameter with a `?`
180 const unwrappedTypeNode = maybeUnwrapOptional(typeNode);
181 if (unwrappedTypeNode !== undefined && i > lastRequiredParameter) {
182 typeNode = unwrappedTypeNode;
183 questionToken = f.createToken(ts.SyntaxKind.QuestionToken);
184 }
185 } else if (which === Type_Which.JSG_IMPL) {
186 if (arg.jsgImpl.type === JsgImplType_Type.JSG_VARARGS) {
187 // If this is a varargs type, make sure we include `...`
188 assert(
189 ts.isArrayTypeNode(typeNode),
190 `Expected "T[]", got "${printNode(typeNode)}"`
191 );
192 dotDotDotToken = f.createToken(ts.SyntaxKind.DotDotDotToken);
193 } else if (isUnsatisfiable(typeNode)) {
194 // If this is an internal implementation type, omit it, and skip to
195 // the next arg
196 continue;
197 }
198 }
199 
200 const param = f.createParameterDeclaration(
201 /* modifiers */ undefined,
202 dotDotDotToken,
203 getParameterName(fullyQualifiedParentName, name, i),
204 questionToken,
205 typeNode
206 );
207 params.push(param);
208 }
209 
210 return params;
211}
212 
213export function createTypeNode(
214 type: Type,
215 allowCoercion = false,
216 allowMethodParameterCoercion = false
217): ts.TypeNode {
218 // If `allowMethodParameterCoercion` is set, `allowCoercion` must be set too.
219 // `allowMethodParameterCoercion` enables additional coercions for C++ method
220 // parameters.
221 assert(
222 !allowMethodParameterCoercion || allowCoercion,
223 `"allowMethodParameterCoercion" requires "allowCoercion"`
224 );
225 
226 const which = type.which();
227 // noinspection FallThroughInSwitchStatementJS
228 switch (which) {
229 case Type_Which.UNKNOWN:
230 return f.createTypeReferenceNode('any');
231 case Type_Which.VOIDT:
232 return f.createTypeReferenceNode('void');
233 case Type_Which.BOOLT:
234 return f.createTypeReferenceNode('boolean');
235 case Type_Which.NUMBER: {
236 const number = type.number;
237 if (isBigNumber(number)) {
238 return f.createUnionTypeNode([
239 f.createTypeReferenceNode('number'),
240 f.createTypeReferenceNode('bigint'),
241 ]);
242 } else {
243 return f.createTypeReferenceNode('number');
244 }
245 }
246 case Type_Which.PROMISE: {
247 const value = type.promise.value;
248 
249 if (allowMethodParameterCoercion && value.which() === Type_Which.VOIDT) {
250 // For C++ method parameters, treat `Promise<void>` as `Promise<any>`.
251 // We don't use `allowCoercion` here, as we want stream callback return
252 // types to be `Promise<void>` so they match official TypeScript types:
253 // https://github.com/microsoft/TypeScript/blob/f1288c33a1594046dcb4bad2ecdda80a1b035bb7/lib/lib.webworker.d.ts#L5987-L6025
254 return f.createTypeReferenceNode('Promise', [
255 f.createTypeReferenceNode('any'),
256 ]);
257 }
258 
259 const valueType = createTypeNode(value, allowCoercion);
260 const promiseType = f.createTypeReferenceNode('Promise', [valueType]);
261 if (allowCoercion) {
262 return f.createUnionTypeNode([valueType, promiseType]);
263 } else {
264 return promiseType;
265 }
266 }
267 case Type_Which.STRUCTURE:
268 return f.createTypeReferenceNode(getTypeName(type.structure));
269 case Type_Which.STRING:
270 return f.createTypeReferenceNode('string');
271 case Type_Which.OBJECT:
272 return f.createTypeReferenceNode('any');
273 case Type_Which.ARRAY: {
274 const array = type.array;
275 const element = array.element;
276 if (element._isNumber && isCharNumber(element.number)) {
277 return f.createTypeReferenceNode('string');
278 } else if (element._isNumber && isByteNumber(element.number)) {
279 // If the array element is a `byte`...
280 if (allowCoercion) {
281 // When coercion is enabled (e.g. method param), `kj::Array<byte>` and
282 // `kj::ArrayPtr<byte>` both mean `ArrayBuffer | ArrayBufferView`
283 return f.createUnionTypeNode([
284 f.createTypeReferenceNode('ArrayBuffer'),
285 f.createTypeReferenceNode('ArrayBufferView'),
286 ]);
287 } else {
288 // When coercion is disabled, `kj::ArrayPtr<byte>` corresponds to
289 // `ArrayBufferView`, whereas `kj::Array<byte>` is `ArrayBuffer`
290 return f.createTypeReferenceNode(
291 isArrayPointer(array) ? 'ArrayBufferView' : 'ArrayBuffer'
292 );
293 }
294 } else if (isIterable(array) && allowCoercion) {
295 // If this is a `jsg::Sequence` parameter, it should accept any iterable
296 return f.createTypeReferenceNode('Iterable', [
297 createTypeNode(element, allowCoercion),
298 ]);
299 } else {
300 // Otherwise, return a regular array
301 return f.createArrayTypeNode(createTypeNode(element, allowCoercion));
302 }
303 }
304 case Type_Which.MAYBE: {
305 const maybe = type.maybe;
306 const alternative = isNullMaybe(maybe) ? 'null' : 'undefined';
307 return f.createUnionTypeNode([
308 createTypeNode(maybe.value, allowCoercion),
309 f.createTypeReferenceNode(alternative),
310 ]);
311 }
312 case Type_Which.DICT: {
313 const dict = type.dict;
314 return f.createTypeReferenceNode('Record', [
315 createTypeNode(dict.key, allowCoercion),
316 createTypeNode(dict.value, allowCoercion),
317 ]);
318 }
319 case Type_Which.ONE_OF: {
320 const variants = type.oneOf.variants.map((variant) =>
321 createTypeNode(variant, allowCoercion)
322 );
323 return f.createUnionTypeNode(variants);
324 }
325 case Type_Which.BUILTIN: {
326 const builtin = type.builtin.type;
327 switch (builtin) {
328 case BuiltinType_Type.V8UINT8ARRAY:
329 return f.createTypeReferenceNode('Uint8Array');
330 case BuiltinType_Type.V8ARRAY_BUFFER_VIEW:
331 return f.createTypeReferenceNode('ArrayBufferView');
332 case BuiltinType_Type.V8ARRAY_BUFFER:
333 return f.createTypeReferenceNode('ArrayBuffer');
334 case BuiltinType_Type.JSG_BUFFER_SOURCE:
335 return f.createUnionTypeNode([
336 f.createTypeReferenceNode('ArrayBuffer'),
337 f.createTypeReferenceNode('ArrayBufferView'),
338 ]);
339 case BuiltinType_Type.KJ_DATE:
340 if (allowCoercion) {
341 return f.createUnionTypeNode([
342 f.createTypeReferenceNode('number'),
343 f.createTypeReferenceNode('Date'),
344 ]);
345 } else {
346 return f.createTypeReferenceNode('Date');
347 }
348 case BuiltinType_Type.V8FUNCTION:
349 return f.createTypeReferenceNode('Function');
350 default:
351 assert.fail(`Unknown builtin type: ${builtin satisfies never}`);
352 }
353 }
354 case Type_Which.INTRINSIC: {
355 const intrinsic = type.intrinsic.name;
356 switch (intrinsic) {
357 case 'v8::kErrorPrototype':
358 return f.createTypeReferenceNode('Error');
359 case 'v8::kIteratorPrototype':
360 return f.createTypeReferenceNode('Iterator', [
361 f.createTypeReferenceNode('unknown'),
362 ]);
363 case 'v8::kAsyncIteratorPrototype':
364 return f.createTypeReferenceNode('AsyncIterator', [
365 f.createTypeReferenceNode('unknown'),
366 ]);
367 default:
368 assert.fail(`Unknown intrinsic type: ${intrinsic}`);
369 }
370 }
371 case Type_Which.FUNCTION: {
372 const func = type.function;
373 const params = createParamDeclarationNodes(
374 'FUNCTION_TODO',
375 'FUNCTION_TODO',
376 func.args.toArray()
377 );
378 const result = createTypeNode(
379 func.returnType,
380 true // Always allow coercion in callback functions
381 );
382 return f.createFunctionTypeNode(
383 /* typeParams */ undefined,
384 params,
385 result
386 );
387 }
388 case Type_Which.JSG_IMPL: {
389 const impl = type.jsgImpl.type;
390 switch (impl) {
391 case JsgImplType_Type.CONFIGURATION:
392 case JsgImplType_Type.V8ISOLATE:
393 case JsgImplType_Type.JSG_LOCK:
394 case JsgImplType_Type.JSG_TYPE_HANDLER:
395 case JsgImplType_Type.JSG_UNIMPLEMENTED:
396 case JsgImplType_Type.JSG_SELF_REF:
397 case JsgImplType_Type.V8FUNCTION_CALLBACK_INFO:
398 case JsgImplType_Type.V8PROPERTY_CALLBACK_INFO:
399 // All these types should be omitted from function parameters
400 return f.createTypeReferenceNode('never');
401 case JsgImplType_Type.JSG_VARARGS:
402 return f.createArrayTypeNode(f.createTypeReferenceNode('any'));
403 case JsgImplType_Type.JSG_NAME:
404 return f.createTypeReferenceNode('PropertyKey');
405 default:
406 assert.fail(
407 `Unknown JSG implementation type: ${impl satisfies never}`
408 );
409 }
410 }
411 case Type_Which.JS_BUILTIN: {
412 // TODO(soon): implement
413 assert.fail('`JS_BUILTIN`s are not yet supported');
414 }
415 default: {
416 assert.fail(`Unknown type: ${which satisfies never}`);
417 }
418 }
419}