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 794cd2475d..6bc966b411 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts @@ -20,6 +20,15 @@ export type ResolvedTypeReference = Reference|ts.TypeReferenceNode|null; */ export type TypeReferenceResolver = (type: ts.TypeReferenceNode) => ResolvedTypeReference; +/** + * A marker to indicate that a type reference is ineligible for emitting. This needs to be truthy + * as it's returned from `ts.forEachChild`, which only returns truthy values. + */ +type INELIGIBLE = { + __brand: 'ineligible'; +}; +const INELIGIBLE: INELIGIBLE = {} as INELIGIBLE; + /** * Determines whether the provided type can be emitted, which means that it can be safely emitted * into a different location. @@ -32,13 +41,24 @@ export function canEmitType(type: ts.TypeNode, resolver: TypeReferenceResolver): return canEmitTypeWorker(type); function canEmitTypeWorker(type: ts.TypeNode): boolean { - return visitTypeNode(type, { - visitTypeReferenceNode: type => canEmitTypeReference(type), - visitArrayTypeNode: type => canEmitTypeWorker(type.elementType), - visitKeywordType: () => true, - visitLiteralType: () => true, - visitOtherType: () => false, - }); + return visitNode(type) !== INELIGIBLE; + } + + // 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. + function visitNode(node: ts.Node): INELIGIBLE|undefined { + // 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. + if (ts.isTypeReferenceNode(node) && !canEmitTypeReference(node)) { + return INELIGIBLE; + } else { + return ts.forEachChild(node, visitNode); + } } function canEmitTypeReference(type: ts.TypeReferenceNode): boolean { @@ -108,15 +128,17 @@ export class TypeEmitter { } emitType(type: ts.TypeNode): ts.TypeNode { - return visitTypeNode(type, { - visitTypeReferenceNode: type => this.emitTypeReference(type), - visitArrayTypeNode: type => ts.updateArrayTypeNode(type, this.emitType(type.elementType)), - visitKeywordType: type => type, - visitLiteralType: type => type, - visitOtherType: () => { - throw new Error('Unable to emit a complex type'); - }, - }); + const typeReferenceTransformer: ts.TransformerFactory = context => { + const visitNode = (node: ts.Node): ts.Node => { + if (ts.isTypeReferenceNode(node)) { + return this.emitTypeReference(node); + } else { + return ts.visitEachChild(node, visitNode, context); + } + }; + return node => ts.visitNode(node, visitNode); + }; + return ts.transform(type, [typeReferenceTransformer]).transformed[0]; } private emitTypeReference(type: ts.TypeReferenceNode): ts.TypeNode { @@ -151,40 +173,3 @@ export class TypeEmitter { return ts.updateTypeReferenceNode(type, typeName, typeArguments); } } - -/** - * Visitor interface that allows for unified recognition of the different types of `ts.TypeNode`s, - * so that `visitTypeNode` is a centralized piece of recognition logic to be used in both - * `canEmitType` and `TypeEmitter`. - */ -interface TypeEmitterVisitor { - visitTypeReferenceNode(type: ts.TypeReferenceNode): R; - visitArrayTypeNode(type: ts.ArrayTypeNode): R; - visitKeywordType(type: ts.KeywordTypeNode): R; - visitLiteralType(type: ts.LiteralTypeNode): R; - visitOtherType(type: ts.TypeNode): R; -} - -function visitTypeNode(type: ts.TypeNode, visitor: TypeEmitterVisitor): R { - if (ts.isTypeReferenceNode(type)) { - return visitor.visitTypeReferenceNode(type); - } else if (ts.isArrayTypeNode(type)) { - return visitor.visitArrayTypeNode(type); - } else if (ts.isLiteralTypeNode(type)) { - return visitor.visitLiteralType(type); - } - - switch (type.kind) { - case ts.SyntaxKind.AnyKeyword: - case ts.SyntaxKind.UnknownKeyword: - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.StringKeyword: - case ts.SyntaxKind.UndefinedKeyword: - case ts.SyntaxKind.NullKeyword: - return visitor.visitKeywordType(type as ts.KeywordTypeNode); - default: - return visitor.visitOtherType(type); - } -} 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 9098e8e9d8..03d2b15b09 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 @@ -33,16 +33,21 @@ runInEachFileSystem(() => { } function emit(emitter: TypeParameterEmitter) { + const canEmit = emitter.canEmit(); const emitted = emitter.emit(ref => { const typeName = ts.createQualifiedName(ts.createIdentifier('test'), ref.debugName!); return ts.createTypeReferenceNode(typeName, /* typeArguments */ undefined); }); if (emitted === undefined) { - return ''; + return canEmit ? '' : null; } - const printer = ts.createPrinter(); + if (!canEmit) { + fail('canEmit must be true when emitting succeeds'); + } + + const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); const sf = ts.createSourceFile('test.ts', '', ts.ScriptTarget.Latest); const generics = emitted.map(param => printer.printNode(ts.EmitHint.Unspecified, param, sf)).join(', '); @@ -71,6 +76,14 @@ runInEachFileSystem(() => { .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('can emit references into external modules', () => {