Skip to content
File

Blob: types/src/transforms/overrides/index.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 "node:assert";
6import ts from "typescript";
7import { isUnsatisfiable } from "../../generator/type";
8import { printNode } from "../../print";
9import { ensureStatementModifiers, hasModifier } from "../helpers";
10import { maybeGetDefines, maybeGetOverride } from "./compiler";
11 
12export { compileOverridesDefines } from "./compiler";
13 
14// Applies handwritten partial TypeScript overrides to generate types to improve
15// output fidelity. Also applies type renames and inserts additional handwritten
16// definitions for non-generated types.
17//
18// See the `JSG_TS_OVERRIDE` macro's documentation in `src/workerd/jsg/jsg.h`
19// for a full explanation of override rules and examples.
20//
21// `compileOverridesDefines()` must be used to compile overrides and defines
22// into valid TypeScript source files. These should be included in the *same*
23// TypeScript `Program` as the source file being transformed. This `Program`,
24// along with the set of types that should be fully-replaced (also returned from
25// `compileOverridesDefines()`), should be passed to this transformer factory.
26//
27// ```ts
28// export declare class A {
29// thing: string;
30// }
31// export declare class B {
32// get(key: string, type: string): Promise<any>;
33// put(key: string, value: string): Promise<void>;
34// }
35// ```
36//
37// ...with the following overrides and defines...
38//
39// - `A`'s override: `RenamedA<Type extends string> { thing: Type; }`
40// - `B`'s override: `{
41// get(key: string, type: "text"): Promise<string | null>;
42// get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
43// }`
44// - `B`'s define: `interface C { foo: A; }`
45//
46// --- transforms to --->
47//
48// ```ts
49// export declare class RenamedA<Type extends string> {
50// thing: Type;
51// }
52// export interface C {
53// foo: RenamedA;
54// }
55// export declare class B {
56// get(key: string, type: "text"): Promise<string | null>;
57// get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
58// put(key: string, value: string): Promise<void>;
59// }
60// ```
61export function createOverrideDefineTransformer(
62 program: ts.Program,
63 replacements: Set<string>
64): ts.TransformerFactory<ts.SourceFile> {
65 return (ctx) => {
66 return (node) => {
67 const overrideCtx: OverrideTransformContext = {
68 program,
69 replacements,
70 renames: new Map<string, string>(),
71 };
72 const v1 = createOverrideDefineVisitor(ctx, overrideCtx);
73 const v2 = createRenameVisitor(ctx, overrideCtx.renames);
74 node = ts.visitEachChild(node, v1, ctx);
75 return ts.visitEachChild(node, v2, ctx);
76 };
77 };
78}
79 
80interface OverrideTransformContext {
81 program: ts.Program;
82 replacements: Set<string>;
83 renames: Map</* from */ string, /* to */ string>;
84}
85 
86// Gets an identifying label for this member, shared between method overloads
87function getMemberKey(member: ts.ClassElement | ts.TypeElement): string {
88 if (ts.isConstructorDeclaration(member)) return "constructor$";
89 
90 const name = member.name;
91 assert(
92 name !== undefined,
93 `Expected named member, got "${printNode(member)}"`
94 );
95 
96 // Put static and instance members in different namespaces. For example, this
97 // allows instance methods to be overridden without affecting static methods
98 // of the same name.
99 const isStatic =
100 ts.canHaveModifiers(member) &&
101 hasModifier(ts.getModifiers(member), ts.SyntaxKind.StaticKeyword);
102 const keyNamespace = isStatic ? "static$" : "instance$";
103 
104 if (
105 ts.isIdentifier(name) ||
106 ts.isStringLiteral(name) ||
107 ts.isNumericLiteral(name)
108 ) {
109 return keyNamespace + name.text;
110 }
111 if (ts.isComputedPropertyName(name)) {
112 const expression = name.expression;
113 if (ts.isStringLiteral(expression) || ts.isNumericLiteral(expression)) {
114 return keyNamespace + expression.text;
115 }
116 }
117 return keyNamespace + printNode(name);
118}
119 
120// Groups override members by their identifying labels from `getMemberKey()`
121function groupMembersByKey<Member extends ts.ClassElement | ts.TypeElement>(
122 members: ts.NodeArray<Member>
123): Map<string, Member[]> {
124 const result = new Map<string, Member[]>();
125 members.forEach((member) => {
126 const key = getMemberKey(member);
127 let array = result.get(key);
128 if (array === undefined) result.set(key, (array = []));
129 array.push(member);
130 });
131 return result;
132}
133 
134// Returns the index of a member in `members` with the specified `key`, or -1 if
135// none exists
136// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
137function findMemberIndex<Member extends ts.ClassElement | ts.TypeElement>(
138 members: Member[],
139 key: string,
140 fromIndex = 0
141): number {
142 return members.findIndex(
143 (member, index) => fromIndex <= index && getMemberKey(member) === key
144 );
145}
146 
147// Merges generated members with overrides according to the following rules:
148// 1. Members in the override but not in the generated type are inserted
149// 2. If an override has the same key as a member in the generated type, the
150// generated member is removed, and the override is inserted instead
151// 3. If an override member property is declared type `never`, it is not
152// inserted, but its presence may remove the generated member (as per 2)
153function mergeMembers<Member extends ts.ClassElement | ts.TypeElement>(
154 generated: ts.NodeArray<Member>,
155 overrides: ts.NodeArray<ts.ClassElement>,
156 transformer: (member: ts.ClassElement) => Member
157): Member[] {
158 const result = [...generated];
159 const grouped = groupMembersByKey(overrides);
160 for (const [key, overrideMembers] of grouped) {
161 const filteredOverrideMembers = overrideMembers.filter((member) => {
162 // Filter out `never` typed properties
163 if (ts.isPropertyDeclaration(member) && member.type !== undefined) {
164 return !isUnsatisfiable(member.type);
165 }
166 // Include all other members
167 return true;
168 });
169 // Transform all class elements into the correct member type. If `Member` is
170 // `ts.ClassElement` already, `transformer` will be the identify function.
171 const transformedOverrideMembers = filteredOverrideMembers.map(transformer);
172 
173 // Try to find index of existing generated member with same key
174 const index = findMemberIndex(result, key);
175 if (index === -1) {
176 // If the member couldn't be found, insert overrides at the end
177 result.push(...transformedOverrideMembers);
178 } else {
179 const member = result[index];
180 const nextIndex = findMemberIndex(result, key, index + 1);
181 if (
182 ts.isGetAccessorDeclaration(member) ||
183 ts.isSetAccessorDeclaration(member)
184 ) {
185 // If this is a getter/setter, it's possible there's one other
186 // getter/setter with the same key.
187 if (nextIndex !== -1) {
188 // Make sure this other member was a getter/setter
189 const nextMember = result[nextIndex];
190 assert(
191 ts.isGetAccessorDeclaration(nextMember) ||
192 ts.isSetAccessorDeclaration(nextMember),
193 `Expected getter/setter, got "${printNode(nextMember)}"`
194 );
195 
196 // Remove the other getter/setter. Because `nextIndex > index`, we'll
197 // still be able to `splice(index)` later on.
198 assert(nextIndex > index);
199 result.splice(nextIndex, /* deleteCount */ 1);
200 
201 // Make sure this was the only other member with this key
202 const nextNextIndex = findMemberIndex(result, key, nextIndex + 1);
203 assert(nextNextIndex === -1);
204 }
205 } else {
206 // Otherwise, make sure this was the only generated member with this key
207 assert(nextIndex === -1);
208 }
209 
210 // Remove the member at that index and replace it with overrides
211 result.splice(index, /* deleteCount */ 1, ...transformedOverrideMembers);
212 }
213 }
214 return result;
215}
216 
217// Converts class members to interface members where possible. Used as a
218// transformer when merging override members (which will always be class
219// members) into an interface.
220function classToTypeElement(
221 ctx: ts.TransformationContext,
222 member: ts.ClassElement
223): ts.TypeElement {
224 if (ts.isMethodDeclaration(member)) {
225 return ctx.factory.createMethodSignature(
226 ts.getModifiers(member),
227 member.name,
228 member.questionToken,
229 member.typeParameters,
230 member.parameters,
231 member.type
232 );
233 }
234 if (ts.isPropertyDeclaration(member)) {
235 return ctx.factory.createPropertySignature(
236 ts.getModifiers(member),
237 member.name,
238 member.questionToken,
239 member.type
240 );
241 }
242 if (
243 ts.isGetAccessorDeclaration(member) ||
244 ts.isSetAccessorDeclaration(member) ||
245 ts.isIndexSignatureDeclaration(member)
246 ) {
247 return member;
248 }
249 assert.fail(
250 `Expected interface-compatible member, got "${printNode(member)}".
251You'll need to define a full-replacement override to a "class" if you wish to insert this member (i.e. "JSG_TS_OVERRIDE(class MyClass { <all_members> })").`
252 );
253}
254 
255// Finds and applies the override (if any) for a node, returning the new
256// potentially overridden node
257function applyOverride<
258 Node extends ts.ClassDeclaration | ts.InterfaceDeclaration,
259>(
260 ctx: ts.TransformationContext,
261 overrideCtx: OverrideTransformContext,
262 node: Node,
263 updateDeclaration: (node: Node, override: ts.ClassDeclaration) => Node
264): ts.Node {
265 assert(node.name !== undefined);
266 const name = node.name.text;
267 const override = maybeGetOverride(overrideCtx.program, name);
268 const isReplacement = overrideCtx.replacements.has(name);
269 
270 // Full-type replacement may rename type too, so record renames now
271 if (override !== undefined) {
272 // If override's name is different to the node's name, rename it later
273 const overrideIdentifier = maybeGetStatementName(override);
274 if (overrideIdentifier !== undefined) {
275 const overrideName = overrideIdentifier.text;
276 if (name !== overrideName) overrideCtx.renames.set(name, overrideName);
277 }
278 }
279 
280 if (isReplacement) {
281 assert(override !== undefined);
282 return ensureStatementModifiers(ctx, override, {
283 declare: true,
284 export: false,
285 });
286 } else if (override !== undefined) {
287 // Merge override into declaration. Whilst we convert all non-replacement
288 // overrides to classes, this type classification is ignored when merging.
289 // Classes just support all possible forms of override. See `./compiler.ts`
290 // `compileOverride()` for details.
291 assert(ts.isClassDeclaration(override));
292 return updateDeclaration(node, override);
293 } else {
294 // No override, so return the node as is
295 return node;
296 }
297}
298 
299// Apply all overrides, insert defines, and record type renames
300function createOverrideDefineVisitor(
301 ctx: ts.TransformationContext,
302 overrideCtx: OverrideTransformContext
303): ts.Visitor {
304 // Copies all string and numeric literals. Without this, garbage would be
305 // inserted in locations of literals instead.
306 // TODO(soon): work out why this happens, something to do with source ranges
307 // and invalid source files/programs maybe?
308 const copyLiteralsVisitor: ts.Visitor<ts.Node, ts.Node> = (node) => {
309 node = ts.visitEachChild(node, copyLiteralsVisitor, ctx);
310 if (ts.isStringLiteral(node)) {
311 return ctx.factory.createStringLiteral(node.text);
312 }
313 if (ts.isNumericLiteral(node)) {
314 return ctx.factory.createNumericLiteral(node.text);
315 }
316 return node;
317 };
318 
319 const visitor: ts.Visitor = (node) => {
320 // Visit classes and interfaces inside module declarations too
321 if (ts.isModuleDeclaration(node) || ts.isModuleBody(node)) {
322 return ts.visitEachChild(node, visitor, ctx);
323 }
324 
325 let defines: ts.NodeArray<ts.Statement> | undefined;
326 
327 if (ts.isClassDeclaration(node) && node.name !== undefined) {
328 defines = maybeGetDefines(overrideCtx.program, node.name.text);
329 node = applyOverride(ctx, overrideCtx, node, (node, override) => {
330 return ctx.factory.updateClassDeclaration(
331 node,
332 node.modifiers,
333 override.name,
334 override.typeParameters ?? node.typeParameters,
335 override.heritageClauses ?? node.heritageClauses,
336 mergeMembers(node.members, override.members, (member) => member)
337 );
338 });
339 } else if (ts.isInterfaceDeclaration(node)) {
340 defines = maybeGetDefines(overrideCtx.program, node.name.text);
341 node = applyOverride(ctx, overrideCtx, node, (node, override) => {
342 assert(override.name !== undefined);
343 return ctx.factory.updateInterfaceDeclaration(
344 node,
345 node.modifiers,
346 override.name,
347 override.typeParameters ?? node.typeParameters,
348 override.heritageClauses ?? node.heritageClauses,
349 mergeMembers(node.members, override.members, (member) =>
350 classToTypeElement(ctx, member)
351 )
352 );
353 });
354 }
355 
356 // Process node and defines if defined
357 node = ts.visitNode(node, copyLiteralsVisitor);
358 defines = ts.visitNodes(defines, copyLiteralsVisitor, ts.isStatement);
359 defines = ts.visitNodes(
360 defines,
361 (node) =>
362 ensureStatementModifiers(ctx, node, { declare: true, export: false }),
363 ts.isStatement
364 );
365 
366 if (ts.isTypeAliasDeclaration(node) && isUnsatisfiable(node.type)) {
367 // If node was overridden to `type T = never`, delete it, and just insert
368 // defines if any
369 return defines === undefined ? undefined : [...defines];
370 } else {
371 // Otherwise, return potentially overridden node, inserting defines if any
372 // before node
373 return defines == undefined ? node : [...defines, node];
374 }
375 };
376 return visitor;
377}
378 
379// Apply previously-recorded type renames to all type references
380export function createRenameVisitor(
381 ctx: ts.TransformationContext,
382 renames: Map</* from */ string, /* to */ string>,
383 renameClassesInterfaces = false
384): ts.Visitor {
385 const visitor: ts.Visitor = (node) => {
386 // Recursively visit all nodes
387 node = ts.visitEachChild(node, visitor, ctx);
388 
389 // Rename all type references
390 if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
391 const rename = renames.get(node.typeName.text);
392 if (rename !== undefined) {
393 return ctx.factory.updateTypeReferenceNode(
394 node,
395 ctx.factory.createIdentifier(rename),
396 node.typeArguments
397 );
398 }
399 }
400 
401 // Rename all type queries (e.g. nested types)
402 if (ts.isTypeQueryNode(node) && ts.isIdentifier(node.exprName)) {
403 const rename = renames.get(node.exprName.text);
404 if (rename !== undefined) {
405 return ctx.factory.updateTypeQueryNode(
406 node,
407 ctx.factory.createIdentifier(rename),
408 node.typeArguments
409 );
410 }
411 }
412 
413 // Rename all expressions with type arguments (e.g. heritage clauses)
414 if (
415 ts.isExpressionWithTypeArguments(node) &&
416 ts.isIdentifier(node.expression)
417 ) {
418 const rename = renames.get(node.expression.text);
419 if (rename !== undefined) {
420 return ctx.factory.updateExpressionWithTypeArguments(
421 node,
422 ctx.factory.createIdentifier(rename),
423 node.typeArguments
424 );
425 }
426 }
427 
428 // Rename all class and interface names
429 if (renameClassesInterfaces) {
430 if (
431 ts.isClassDeclaration(node) &&
432 node.name !== undefined &&
433 ts.isIdentifier(node.name)
434 ) {
435 const rename = renames.get(node.name.text);
436 if (rename !== undefined) {
437 return ctx.factory.updateClassDeclaration(
438 node,
439 node.modifiers,
440 ctx.factory.createIdentifier(rename),
441 node.typeParameters,
442 node.heritageClauses,
443 node.members
444 );
445 }
446 }
447 if (ts.isInterfaceDeclaration(node) && ts.isIdentifier(node.name)) {
448 const rename = renames.get(node.name.text);
449 if (rename !== undefined) {
450 return ctx.factory.updateInterfaceDeclaration(
451 node,
452 node.modifiers,
453 ctx.factory.createIdentifier(rename),
454 node.typeParameters,
455 node.heritageClauses,
456 node.members
457 );
458 }
459 }
460 }
461 
462 return node;
463 };
464 return visitor;
465}
466 
467// Returns a statement's identifier if it has one. This is used to get the
468// name to rename too (if any), hence we ignore variables and functions, since
469// replacing a reference to a type with a referencing to a variable/function is
470// an error.
471function maybeGetStatementName(node: ts.Statement): ts.Identifier | undefined {
472 if (
473 ts.isClassDeclaration(node) ||
474 ts.isInterfaceDeclaration(node) ||
475 ts.isEnumDeclaration(node) ||
476 ts.isTypeAliasDeclaration(node)
477 ) {
478 return node.name;
479 }
480}