From 4c482bf3f1768a18b35e0648c91029504a8d8649 Mon Sep 17 00:00:00 2001 From: JoostK Date: Sun, 4 Jul 2021 00:59:46 +0200 Subject: [PATCH] fix(compiler-cli): properly emit literal types when recreating type parameters in a different file (#42761) In #42492 the template type checker became capable of replicating a wider range of generic type parameters for use in template type-check files. Any literal types within a type parameter would however emit invalid code, as TypeScript was emitting the literals using the text as extracted from the template type-check file instead of the original source file where the type node was taken from. This commit works around the issue by cloning any literal types and marking them as synthetic, signalling to TypeScript that the literal text has to be extracted from the node itself instead from the source file. This commit also excludes `import()` type nodes from being supported, as their module specifier may potentially need to be rewritten. Fixes #42667 PR Close #42761 --- .../src/ngtsc/typecheck/src/type_emitter.ts | 30 +++++++++++++++---- .../test/type_parameter_emitter_spec.ts | 22 ++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts index 4c7ed9151f..b198be9f9f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts @@ -46,12 +46,18 @@ export function canEmitType(type: ts.TypeNode, resolver: TypeReferenceResolver): } // To determine whether a type can be emitted, we have to recursively look through all type nodes. - // If a type reference node is found at any position within the type and that type reference - // cannot be emitted, then the `INELIGIBLE` constant is returned to stop the recursive walk as - // the type as a whole cannot be emitted in that case. Otherwise, the result of visiting all child - // nodes determines the result. If no ineligible type reference node is found then the walk - // returns `undefined`, indicating that no type node was visited that could not be emitted. + // If an unsupported type node is found at any position within the type, then the `INELIGIBLE` + // constant is returned to stop the recursive walk as the type as a whole cannot be emitted in + // that case. Otherwise, the result of visiting all child nodes determines the result. If no + // ineligible type reference node is found then the walk returns `undefined`, indicating that + // no type node was visited that could not be emitted. function visitNode(node: ts.Node): INELIGIBLE|undefined { + // `import('module')` type nodes are not supported, as it may require rewriting the module + // specifier which is currently not done. + if (ts.isImportTypeNode(node)) { + return INELIGIBLE; + } + // Emitting a type reference node in a different context requires that an import for the type // can be created. If a type reference node cannot be emitted, `INELIGIBLE` is returned to stop // the walk. @@ -130,8 +136,22 @@ export class TypeEmitter { emitType(type: ts.TypeNode): ts.TypeNode { const typeReferenceTransformer: ts.TransformerFactory = context => { const visitNode = (node: ts.Node): ts.Node => { + if (ts.isImportTypeNode(node)) { + throw new Error('Unable to emit import type'); + } + if (ts.isTypeReferenceNode(node)) { return this.emitTypeReference(node); + } else if (ts.isLiteralExpression(node)) { + // TypeScript would typically take the emit text for a literal expression from the source + // file itself. As the type node is being emitted into a different file, however, + // TypeScript would extract the literal text from the wrong source file. To mitigate this + // issue the literal is cloned and explicitly marked as synthesized by setting its text + // range to a negative range, forcing TypeScript to determine the node's literal text from + // the synthesized node's text instead of the incorrect source file. + const clone = ts.getMutableClone(node); + ts.setTextRange(clone, {pos: -1, end: -1}); + return clone; } else { return ts.visitEachChild(node, visitNode, context); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts index ed56a33a50..27172b32e8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts @@ -86,6 +86,28 @@ runInEachFileSystem(() => { .toEqual(''); }); + it('can emit literal types', () => { + expect(emit(createEmitter(`export class TestClass {}`))) + .toEqual(``); + expect(emit(createEmitter(`export class TestClass {}`))) + .toEqual(``); + expect(emit(createEmitter(`export class TestClass {}`))) + .toEqual(``); + expect(emit(createEmitter(`export class TestClass {}`))) + .toEqual(``); + expect(emit(createEmitter(`export class TestClass {}`))) + .toEqual(``); + expect(emit(createEmitter(`export class TestClass {}`))) + .toEqual(``); + }); + + it('cannot emit import types', () => { + const emitter = createEmitter(`export class TestClass {}`); + + expect(emitter.canEmit()).toBe(false); + expect(() => emit(emitter)).toThrowError('Unable to emit import type'); + }); + it('can emit references into external modules', () => { const emitter = createEmitter(` import {NgIterable} from '@angular/core';