Skip to content
File

Blob: types/src/generator/index.ts

typescript225 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 'node:assert';
6import {
7 FunctionType,
8 Member,
9 Member_Which,
10 Method,
11 Structure,
12 StructureGroups,
13 Type,
14 Type_Which,
15} from '@workerd/jsg/rtti';
16import ts from 'typescript';
17import { createStructureNode } from './structure';
18 
19export { getTypeName } from './type';
20 
21export type StructureMap = Map<string, Structure>;
22// Builds a lookup table mapping type names to structures
23function collectStructureMap(root: StructureGroups): StructureMap {
24 const map = new Map<string, Structure>();
25 root.groups.forEach((group) => {
26 group.structures.forEach((structure) => {
27 map.set(structure.fullyQualifiedName, structure);
28 });
29 });
30 return map;
31}
32 
33// Builds a set containing the names of structures that should be included
34// in the definitions, because they are referenced by root types or any of their
35// children. A struct/resource type is marked as a root type using a
36// `JSG_(STRUCT_)TS_ROOT` macro.
37//
38// We need to do this as some types should only be included in the definitions
39// when certain compatibility flags are enabled (e.g. `Navigator`,
40// standards-compliant `URL`). However, these types are always included in
41// the `*_TYPES` macros.
42function collectIncluded(map: StructureMap, root?: string): Set<string> {
43 const included = new Set<string>();
44 
45 function visitType(type: Type): void {
46 switch (type.which()) {
47 case Type_Which.PROMISE: {
48 visitType(type.promise.value);
49 return;
50 }
51 case Type_Which.STRUCTURE: {
52 const name = type.structure.fullyQualifiedName;
53 const structure = map.get(name);
54 assert(structure !== undefined, `Unknown structure type: ${name}`);
55 {
56 visitStructure(structure);
57 return;
58 }
59 }
60 case Type_Which.ARRAY: {
61 visitType(type.array.element);
62 return;
63 }
64 case Type_Which.MAYBE: {
65 visitType(type.maybe.value);
66 return;
67 }
68 case Type_Which.DICT: {
69 const dict = type.dict;
70 visitType(dict.key);
71 visitType(dict.value);
72 return;
73 }
74 case Type_Which.ONE_OF: {
75 type.oneOf.variants.forEach(visitType);
76 return;
77 }
78 case Type_Which.FUNCTION: {
79 visitFunction(type.function);
80 return;
81 }
82 }
83 }
84 
85 function visitFunction(func: FunctionType | Method): void {
86 func.args.forEach(visitType);
87 visitType(func.returnType);
88 }
89 
90 function visitMember(member: Member): void {
91 switch (member.which()) {
92 case Member_Which.METHOD: {
93 visitFunction(member.method);
94 return;
95 }
96 case Member_Which.PROPERTY: {
97 visitType(member.property.type);
98 return;
99 }
100 case Member_Which.NESTED: {
101 visitStructure(member.nested.structure);
102 return;
103 }
104 case Member_Which.CONSTRUCTOR: {
105 member.$constructor.args.forEach(visitType);
106 return;
107 }
108 }
109 }
110 
111 function visitStructure(structure: Structure): void {
112 const name = structure.fullyQualifiedName;
113 if (included.has(name)) return;
114 included.add(name);
115 structure.members.forEach(visitMember);
116 if (structure._hasExtends()) {
117 visitType(structure.extends);
118 }
119 if (structure._hasIterator()) {
120 visitFunction(structure.iterator);
121 }
122 if (structure._hasAsyncIterator()) {
123 visitFunction(structure.asyncIterator);
124 }
125 }
126 
127 if (root === undefined) {
128 // If no root was specified, visit all structures with
129 // `JSG_(STRUCT_)TS_ROOT` macros
130 for (const structure of map.values()) {
131 if (structure.tsRoot) visitStructure(structure);
132 }
133 } else {
134 // Otherwise, visit just that root
135 const structure = map.get(root);
136 assert(structure !== undefined, `Unknown root: ${root}`);
137 visitStructure(structure);
138 }
139 
140 return included;
141}
142 
143// Builds a set containing the names of structures that must be declared as
144// `class`es rather than `interface`s because they either:
145// 1) Get inherited by another class (`class` `extends` requires another `class`)
146// 2) Are constructible (`constructor(...)`s can only appear in `class`es)
147// 3) Have `static` methods (`static`s can only appear in `class`es)
148// 4) Are a nested type (users could call `instanceof` with the type)
149function collectClasses(map: StructureMap): Set<string> {
150 const classes = new Set<string>();
151 for (const structure of map.values()) {
152 // 1) Add all classes inherited by this class
153 if (structure._hasExtends()) {
154 const extendsType = structure.extends;
155 if (extendsType._isStructure) {
156 classes.add(extendsType.structure.fullyQualifiedName);
157 }
158 }
159 
160 structure.members.forEach((member) => {
161 // 2) Add this class if it's constructible
162 if (member._isConstructor) {
163 classes.add(structure.fullyQualifiedName);
164 }
165 // 3) Add this class if it contains static methods
166 if (member._isMethod && member.method.static) {
167 classes.add(structure.fullyQualifiedName);
168 }
169 // 4) Add all nested types defined by this class
170 if (member._isNested) {
171 classes.add(member.nested.structure.fullyQualifiedName);
172 }
173 });
174 }
175 return classes;
176}
177 
178export function generateDefinitions(root: StructureGroups): {
179 nodes: ts.Statement[];
180 structureMap: StructureMap;
181} {
182 const structureMap = collectStructureMap(root);
183 const globalIncluded = collectIncluded(structureMap);
184 const classes = collectClasses(structureMap);
185 
186 // Can't use `flatMap()` here as `getGroups()` returns a `capnp.List`
187 const nodes = root.groups.map((group) => {
188 const structureNodes: ts.Statement[] = [];
189 group.structures.forEach((structure) => {
190 const name = structure.fullyQualifiedName;
191 if (globalIncluded.has(name)) {
192 const asClass = classes.has(name);
193 structureNodes.push(createStructureNode(structure, { asClass }));
194 }
195 });
196 return structureNodes;
197 });
198 const flatNodes = nodes.flat();
199 
200 return { nodes: flatNodes, structureMap };
201}
202 
203export function collectTypeScriptModules(root: StructureGroups): string {
204 let result = '';
205 
206 root.modules.forEach((module) => {
207 if (!module._isTsDeclarations) return;
208 const declarations = module.tsDeclarations
209 // Looks for any lines starting with `///`, which indicates a TypeScript
210 // Triple-Slash Directive (https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html)
211 .replaceAll(/^\/\/\/.+$/gm, (match) => {
212 assert.strictEqual(
213 match,
214 '/// <reference types="@workerd/types-internal" />',
215 `Unexpected triple-slash directive, got ${match}`
216 );
217 return '';
218 });
219 
220 result += `declare module "${module.specifier}" {\n${declarations}\n}\n`;
221 });
222 
223 return result;
224}