Skip to content
File

Blob: types/test/transforms/overrides/index.spec.ts

typescript481 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 
5import assert from 'assert';
6import { test } from 'node:test';
7import path from 'path';
8import { Member_Nested, StructureGroups, Type } from '@workerd/jsg/rtti';
9import { Message } from 'capnp-es';
10import ts from 'typescript';
11import { generateDefinitions } from '../../../src/generator';
12import { printNodeList, printer } from '../../../src/print';
13import { createMemoryProgram } from '../../../src/program';
14import {
15 compileOverridesDefines,
16 createOverrideDefineTransformer,
17} from '../../../src/transforms';
18 
19function printDefinitionsWithOverrides(root: StructureGroups): string {
20 const { nodes } = generateDefinitions(root);
21 
22 const [sources, replacements] = compileOverridesDefines(root);
23 const sourcePath = path.resolve(__dirname, 'source.ts');
24 const source = printNodeList(nodes);
25 sources.set(sourcePath, source);
26 
27 const program = createMemoryProgram(sources);
28 const sourceFile = program.getSourceFile(sourcePath);
29 assert(sourceFile !== undefined);
30 
31 const result = ts.transform(sourceFile, [
32 createOverrideDefineTransformer(program, replacements),
33 ]);
34 assert.strictEqual(result.transformed.length, 1);
35 
36 return printer.printFile(result.transformed[0]);
37}
38 
39test('createOverrideDefineTransformer: applies type renames', () => {
40 const root = new Message().initRoot(StructureGroups);
41 const group = root._initGroups(1).get(0);
42 const structures = group._initStructures(2);
43 
44 const thing = structures.get(0);
45 thing.name = 'Thing';
46 thing.fullyQualifiedName = 'workerd::api::Thing';
47 thing.tsOverride = 'RenamedThing';
48 function referenceThing(type: Type | Member_Nested) {
49 const structureType = type._initStructure();
50 structureType.name = 'Thing';
51 structureType.fullyQualifiedName = 'workerd::api::Thing';
52 }
53 
54 // Create type root that references Thing in different ways to test renaming
55 const root1 = structures.get(1);
56 root1.name = 'Root1';
57 root1.fullyQualifiedName = 'workerd::api::Root1';
58 root1.tsRoot = true;
59 // Make sure references to original names in overrides get renamed too
60 root1.tsOverride = '{ newProp: Thing; }';
61 {
62 const members = root1._initMembers(3);
63 
64 const prop = members.get(0)._initProperty();
65 prop.name = 'prop';
66 referenceThing(prop._initType());
67 
68 const method = members.get(1)._initMethod();
69 method.name = 'method';
70 referenceThing(method._initArgs(1).get(0));
71 referenceThing(method._initReturnType());
72 
73 const nested = members.get(2)._initNested();
74 nested.name = 'Thing'; // Should keep original name
75 referenceThing(nested);
76 }
77 
78 assert.strictEqual(
79 printDefinitionsWithOverrides(root),
80 `declare abstract class RenamedThing {
81}
82interface Root1 {
83 prop: RenamedThing;
84 method(param0: RenamedThing): RenamedThing;
85 Thing: typeof RenamedThing;
86 newProp: RenamedThing;
87}
88`
89 );
90});
91 
92test('createOverrideDefineTransformer: applies property overrides', () => {
93 const root = new Message().initRoot(StructureGroups);
94 const group = root._initGroups(1).get(0);
95 const structures = group._initStructures(1);
96 
97 const root1 = structures.get(0);
98 root1.name = 'Root1';
99 root1.fullyQualifiedName = 'workerd::api::Root1';
100 root1.tsRoot = true;
101 {
102 const members = root1._initMembers(6);
103 
104 // Readonly instance property, overridden to be mutable and optional
105 const prop1 = members.get(0)._initProperty();
106 prop1.name = 'prop1';
107 prop1._initType()._initString().name = 'kj::String';
108 prop1.readonly = true;
109 
110 // Mutable instance property, overridden to be readonly and required
111 const prop2 = members.get(1)._initProperty();
112 prop2.name = 'prop2';
113 prop2._initType()._initMaybe()._initValue().boolt = true;
114 
115 // Readonly prototype property, overridden to be mutable
116 const prop3 = members.get(2)._initProperty();
117 prop3.name = 'prop3';
118 prop3._initType()._initArray()._initElement().boolt = true;
119 prop3.readonly = true;
120 prop3.prototype = true;
121 
122 // Mutable prototype property, overridden to be readonly
123 const prop4 = members.get(3)._initProperty();
124 prop4.name = 'prop4';
125 prop4._initType()._initNumber().name = 'int';
126 prop4.prototype = true;
127 
128 // Deleted property
129 const prop5 = members.get(4)._initProperty();
130 prop5.name = 'prop5';
131 prop5._initType().boolt = true;
132 
133 // Untouched property
134 const prop6 = members.get(5)._initProperty();
135 prop6.name = 'prop6';
136 prop6._initType()._initPromise()._initValue().voidt = true;
137 }
138 root1.tsOverride = `{
139 prop1?: "thing";
140 readonly prop2: true;
141 get prop3(): false;
142 set prop3(value: false);
143 get prop4(): 1 | 2 | 3;
144 prop5: never;
145 }`;
146 
147 assert.strictEqual(
148 printDefinitionsWithOverrides(root),
149 `interface Root1 {
150 prop1?: "thing";
151 readonly prop2: true;
152 get prop3(): false;
153 set prop3(value: false);
154 get prop4(): 1 | 2 | 3;
155 prop6: Promise<void>;
156}
157`
158 );
159});
160 
161test('createOverrideDefineTransformer: applies method overrides', () => {
162 const root = new Message().initRoot(StructureGroups);
163 const group = root._initGroups(1).get(0);
164 const structures = group._initStructures(1);
165 
166 const root1 = structures.get(0);
167 root1.name = 'Root1';
168 root1.fullyQualifiedName = 'workerd::api::Root1';
169 root1.tsRoot = true;
170 {
171 const members = root1._initMembers(7);
172 
173 // Static and instance methods with the same names
174 const method1 = members.get(0)._initMethod();
175 method1.name = 'one';
176 method1._initReturnType()._initNumber().name = 'int';
177 const staticMethod1 = members.get(1)._initMethod();
178 staticMethod1.name = 'one';
179 staticMethod1._initReturnType()._initNumber().name = 'int';
180 staticMethod1.static = true;
181 const method2 = members.get(2)._initMethod();
182 method2.name = 'two';
183 method2._initReturnType()._initNumber().name = 'int';
184 const staticMethod2 = members.get(3)._initMethod();
185 staticMethod2.name = 'two';
186 staticMethod2._initReturnType()._initNumber().name = 'int';
187 staticMethod2.static = true;
188 
189 // Method with multiple overloads
190 const methodGet = members.get(4)._initMethod();
191 methodGet.name = 'get';
192 {
193 const args = methodGet._initArgs(2);
194 args.get(0)._initString().name = 'kj::String';
195 const variants = args.get(1)._initOneOf()._initVariants(2);
196 variants.get(0)._initString().name = 'kj::String';
197 variants.get(1).unknown = true;
198 }
199 const methodGetReturn = methodGet._initReturnType()._initMaybe();
200 methodGetReturn.name = 'kj::Maybe';
201 methodGetReturn._initValue().unknown = true;
202 
203 // Deleted method
204 const methodDeleteAll = members.get(5)._initMethod();
205 methodDeleteAll.name = 'deleteAll';
206 methodDeleteAll._initReturnType().voidt = true;
207 
208 // Untouched method
209 const methodThing = members.get(6)._initMethod();
210 methodThing.name = 'thing';
211 methodThing._initArgs(1).get(0).boolt = true;
212 methodThing._initReturnType().boolt = true;
213 }
214 // These overrides test:
215 // - Overriding a static method with an instance method of the same name
216 // - Overriding an instance method with a static method of the same name
217 // - Split overloads, these should be grouped
218 // - Deleted method
219 root1.tsOverride = `{
220 static one(): 1;
221 two(): 2;
222
223 get(key: string, type: "text"): Promise<string | null>;
224 get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
225
226 deleteAll: never;
227
228 get<T>(key: string, type: "json"): Promise<T | null>;
229 }`;
230 
231 assert.strictEqual(
232 printDefinitionsWithOverrides(root),
233 `declare abstract class Root1 {
234 one(): number;
235 static one(): 1;
236 two(): 2;
237 static two(): number;
238 get(key: string, type: "text"): Promise<string | null>;
239 get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
240 get<T>(key: string, type: "json"): Promise<T | null>;
241 thing(param0: boolean): boolean;
242}
243`
244 );
245});
246 
247test('createOverrideDefineTransformer: applies type parameter overrides', () => {
248 const root = new Message().initRoot(StructureGroups);
249 const group = root._initGroups(1).get(0);
250 const structures = group._initStructures(2);
251 
252 const struct = structures.get(0);
253 struct.name = 'Struct';
254 struct.fullyQualifiedName = 'workerd::api::Struct';
255 {
256 const members = struct._initMembers(1);
257 const prop = members.get(0)._initProperty();
258 prop.name = 'type';
259 prop._initType().unknown = true;
260 }
261 struct.tsOverride = `RenamedStruct<Type extends string = string> {
262 type: Type;
263 }`;
264 
265 const root1 = structures.get(1);
266 root1.name = 'Root1';
267 root1.fullyQualifiedName = 'workerd::api::Root1';
268 root1.tsRoot = true;
269 {
270 const members = root1._initMembers(2);
271 
272 const methodGet = members.get(0)._initMethod();
273 methodGet.name = 'get';
274 const returnStruct = methodGet._initReturnType()._initStructure();
275 returnStruct.name = 'Struct';
276 returnStruct.fullyQualifiedName = 'workerd::api::Struct';
277 
278 const methodRead = members.get(1)._initMethod();
279 methodRead.name = 'read';
280 methodRead._initReturnType()._initPromise()._initValue().unknown = true;
281 }
282 root1.tsOverride = `<R> {
283 read(): Promise<R>;
284 }`;
285 
286 assert.strictEqual(
287 printDefinitionsWithOverrides(root),
288 `interface RenamedStruct<Type extends string = string> {
289 type: Type;
290}
291interface Root1<R> {
292 get(): RenamedStruct;
293 read(): Promise<R>;
294}
295`
296 );
297});
298 
299test('createOverrideDefineTransformer: applies heritage overrides', () => {
300 const root = new Message().initRoot(StructureGroups);
301 const group = root._initGroups(1).get(0);
302 const structures = group._initStructures(4);
303 
304 const superclass = structures.get(0);
305 superclass.name = `Superclass`;
306 superclass.fullyQualifiedName = `workerd::api::Superclass`;
307 superclass.tsOverride = '<T, U = unknown>';
308 
309 const root1 = structures.get(1);
310 root1.name = 'Root1';
311 root1.fullyQualifiedName = 'workerd::api::Root1';
312 const root1Extends = root1._initExtends()._initStructure();
313 root1Extends.name = 'Superclass';
314 root1Extends.fullyQualifiedName = 'workerd::api::Superclass';
315 root1.tsRoot = true;
316 root1.tsOverride = `extends Superclass<ArrayBuffer | ArrayBufferView, Uint8Array>`;
317 
318 const root2 = structures.get(2);
319 root2.name = 'Root2';
320 root2.fullyQualifiedName = 'workerd::api::Root2';
321 const root2Extends = root1._initExtends()._initStructure();
322 root2Extends.name = 'Superclass';
323 root2Extends.fullyQualifiedName = 'workerd::api::Superclass';
324 root2.tsRoot = true;
325 root2.tsOverride = 'Root2<T> implements Superclass<T>';
326 
327 const root3 = structures.get(3);
328 root3.name = 'Root3';
329 root3.fullyQualifiedName = 'workerd::api::Root3';
330 const root3Extends = root1._initExtends()._initStructure();
331 root3Extends.name = 'Superclass';
332 root3Extends.fullyQualifiedName = 'workerd::api::Superclass';
333 root3.tsRoot = true;
334 {
335 const members = root3._initMembers(1);
336 const prop = members.get(0)._initProperty();
337 prop.name = 'prop';
338 prop._initType()._initNumber().name = 'int';
339 }
340 root3.tsOverride = `extends Superclass<boolean> {
341 prop: 1 | 2 | 3;
342 }`;
343 
344 assert.strictEqual(
345 printDefinitionsWithOverrides(root),
346 `declare abstract class Superclass<T, U = unknown> {
347}
348interface Root1 extends Superclass<ArrayBuffer | ArrayBufferView, Uint8Array> {
349}
350interface Root2<T> implements Superclass<T> {
351}
352interface Root3 extends Superclass<boolean> {
353 prop: 1 | 2 | 3;
354}
355`
356 );
357});
358 
359test('createOverrideDefineTransformer: applies full type replacements', () => {
360 const root = new Message().initRoot(StructureGroups);
361 const group = root._initGroups(1).get(0);
362 const structures = group._initStructures(4);
363 
364 const root1 = structures.get(0);
365 root1.name = 'Root1';
366 root1.fullyQualifiedName = 'workerd::api::Root1';
367 root1.tsRoot = true;
368 root1.tsOverride = `const Root1 = {
369 new (): { 0: Root2; 1: Root3; };
370 }`;
371 
372 const root2 = structures.get(1);
373 root2.name = 'Root2';
374 root2.fullyQualifiedName = 'workerd::api::Root2';
375 root2.tsRoot = true;
376 root2.tsOverride = `enum Root2 { ONE, TWO, THREE; }`;
377 
378 const root3 = structures.get(2);
379 root3.name = 'Root3';
380 root3.fullyQualifiedName = 'workerd::api::Root3';
381 root3.tsRoot = true;
382 // Check renames still applied with full-type replacements
383 root3.tsOverride = `type RenamedRoot3<T = any> = { done: false; value: T; } | { done: true; value: undefined; }`;
384 
385 const root4 = structures.get(3);
386 root4.name = 'Root4';
387 root4.fullyQualifiedName = 'workerd::api::Root4';
388 root4.tsRoot = true;
389 root4.tsOverride = `type Root4 = never`;
390 
391 assert.strictEqual(
392 printDefinitionsWithOverrides(root),
393 `declare const Root1 = {
394 new(): {
395 0: Root2;
396 1: RenamedRoot3;
397 };
398};
399declare enum Root2 {
400 ONE,
401 TWO,
402 THREE
403}
404type RenamedRoot3<T = any> = {
405 done: false;
406 value: T;
407} | {
408 done: true;
409 value: undefined;
410};
411`
412 );
413});
414 
415test('createOverrideDefineTransformer: applies overrides with literals', () => {
416 const root = new Message().initRoot(StructureGroups);
417 const group = root._initGroups(1).get(0);
418 const structures = group._initStructures(1);
419 
420 const root1 = structures.get(0);
421 root1.name = 'Root1';
422 root1.fullyQualifiedName = 'workerd::api::Root1';
423 root1.tsRoot = true;
424 root1.tsOverride = `{
425 literalString: "hello";
426 literalNumber: 42;
427 literalArray: [a: "a", b: 2];
428 literalObject: { a: "a"; b: 2; };
429 literalTemplate: \`\${string}-\${number}\`;
430 }`;
431 
432 assert.strictEqual(
433 printDefinitionsWithOverrides(root),
434 `interface Root1 {
435 literalString: "hello";
436 literalNumber: 42;
437 literalArray: [
438 a: "a",
439 b: 2
440 ];
441 literalObject: {
442 a: "a";
443 b: 2;
444 };
445 literalTemplate: \`\${string}-\${number}\`;
446}
447`
448 );
449});
450 
451test('createOverrideDefineTransformer: inserts extra defines', () => {
452 const root = new Message().initRoot(StructureGroups);
453 const group = root._initGroups(1).get(0);
454 const structures = group._initStructures(2);
455 
456 const root1 = structures.get(0);
457 root1.name = 'Root1';
458 root1.fullyQualifiedName = 'workerd::api::Root1';
459 root1.tsRoot = true;
460 
461 const root2 = structures.get(1);
462 root2.name = 'Root2';
463 root2.fullyQualifiedName = 'workerd::api::Root2';
464 root2.tsRoot = true;
465 root2.tsDefine = 'interface Root2Extra<Type> { prop: Type }';
466 root2.tsOverride = 'RenamedRoot2';
467 
468 // Check defines inserted before structure
469 assert.strictEqual(
470 printDefinitionsWithOverrides(root),
471 `interface Root1 {
472}
473interface Root2Extra<Type> {
474 prop: Type;
475}
476interface RenamedRoot2 {
477}
478`
479 );
480});