diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 5ce5d601fd..b2bd027fc0 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -542,7 +542,8 @@ export class NgtscProgram implements api.Program { // Execute the typeCheck phase of each decorator in the program. const prepSpan = this.perfRecorder.start('typeCheckPrep'); - const ctx = new TypeCheckContext(typeCheckingConfig, this.refEmitter !, this.typeCheckFilePath); + const ctx = new TypeCheckContext( + typeCheckingConfig, this.refEmitter !, this.reflector, this.typeCheckFilePath); compilation.typeCheck(ctx); this.perfRecorder.stop(prepSpan); diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 4af9b4b1e2..304b56570c 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -434,7 +434,7 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { } visitExpressionType(type: ExpressionType, context: Context): ts.TypeReferenceType { - const expr: ts.Identifier|ts.QualifiedName = type.value.visitExpression(this, context); + const expr: ts.EntityName = type.value.visitExpression(this, context); const typeArgs = type.typeParams !== null ? type.typeParams.map(param => param.visitType(this, context)) : undefined; @@ -494,7 +494,7 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { throw new Error('Method not implemented.'); } - visitExternalExpr(ast: ExternalExpr, context: Context): ts.TypeNode { + visitExternalExpr(ast: ExternalExpr, context: Context): ts.Node { if (ast.value.moduleName === null || ast.value.name === null) { throw new Error(`Import unknown module or symbol`); } @@ -503,13 +503,15 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor { const symbolIdentifier = ts.createIdentifier(symbol); const typeName = moduleImport ? - ts.createPropertyAccess(ts.createIdentifier(moduleImport), symbolIdentifier) : + ts.createQualifiedName(ts.createIdentifier(moduleImport), symbolIdentifier) : symbolIdentifier; - const typeArguments = - ast.typeParams ? ast.typeParams.map(type => type.visitType(this, context)) : undefined; + if (ast.typeParams === null) { + return typeName; + } - return ts.createExpressionWithTypeArguments(typeArguments, typeName); + const typeArguments = ast.typeParams.map(type => type.visitType(this, context)); + return ts.createTypeReferenceNode(typeName, typeArguments); } visitConditionalExpr(ast: ConditionalExpr, context: Context) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 57d8e63933..1538b964a2 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; -import {ClassDeclaration} from '../../reflection'; +import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; import {TemplateSourceMapping, TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api'; @@ -39,8 +39,8 @@ export class TypeCheckContext { constructor( private config: TypeCheckingConfig, private refEmitter: ReferenceEmitter, - typeCheckFilePath: AbsoluteFsPath) { - this.typeCheckFile = new TypeCheckFile(typeCheckFilePath, this.config, this.refEmitter); + private reflector: ReflectionHost, typeCheckFilePath: AbsoluteFsPath) { + this.typeCheckFile = new TypeCheckFile(typeCheckFilePath, config, refEmitter, reflector); } /** @@ -80,7 +80,7 @@ export class TypeCheckContext { for (const dir of boundTarget.getUsedDirectives()) { const dirRef = dir.ref as Reference>; const dirNode = dirRef.node; - if (requiresInlineTypeCtor(dirNode)) { + if (requiresInlineTypeCtor(dirNode, this.reflector)) { // Add a type constructor operation for the directive. this.addInlineTypeCtor(dirNode.getSourceFile(), dirRef, { fnName: 'ngTypeCtor', @@ -239,7 +239,8 @@ export class TypeCheckContext { this.opMap.set(sf, []); } const ops = this.opMap.get(sf) !; - ops.push(new TcbOp(ref, tcbMeta, this.config, this.domSchemaChecker, this.oobRecorder)); + ops.push(new TcbOp( + ref, tcbMeta, this.config, this.reflector, this.domSchemaChecker, this.oobRecorder)); } } @@ -271,7 +272,7 @@ class TcbOp implements Op { constructor( readonly ref: Reference>, readonly meta: TypeCheckBlockMetadata, readonly config: TypeCheckingConfig, - readonly domSchemaChecker: DomSchemaChecker, + readonly reflector: ReflectionHost, readonly domSchemaChecker: DomSchemaChecker, readonly oobRecorder: OutOfBandDiagnosticRecorder) {} /** @@ -281,7 +282,7 @@ class TcbOp implements Op { execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): string { - const env = new Environment(this.config, im, refEmitter, sf); + const env = new Environment(this.config, im, refEmitter, this.reflector, sf); const fnName = ts.createIdentifier(`_tcb_${this.ref.node.pos}`); const fn = generateTypeCheckBlock( env, this.ref, fnName, this.meta, this.domSchemaChecker, this.oobRecorder); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts index c96cfb8005..00a8a959ad 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -10,12 +10,13 @@ import {ExpressionType, ExternalExpr, ReadVarExpr, Type} from '@angular/compiler import * as ts from 'typescript'; import {NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports'; -import {ClassDeclaration} from '../../reflection'; +import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager, translateExpression, translateType} from '../../translator'; import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api'; import {tsDeclareVariable} from './ts_util'; import {generateTypeCtorDeclarationFn, requiresInlineTypeCtor} from './type_constructor'; +import {TypeParameterEmitter} from './type_parameter_emitter'; /** * A context which hosts one or more Type Check Blocks (TCBs). @@ -45,7 +46,8 @@ export class Environment { constructor( readonly config: TypeCheckingConfig, protected importManager: ImportManager, - private refEmitter: ReferenceEmitter, protected contextFile: ts.SourceFile) {} + private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, + protected contextFile: ts.SourceFile) {} /** * Get an expression referring to a type constructor for the given directive. @@ -60,7 +62,7 @@ export class Environment { return this.typeCtors.get(node) !; } - if (requiresInlineTypeCtor(node)) { + if (requiresInlineTypeCtor(node, this.reflector)) { // The constructor has already been created inline, we just need to construct a reference to // it. const ref = this.reference(dirRef); @@ -84,7 +86,9 @@ export class Environment { }, coercedInputFields: dir.coercedInputFields, }; - const typeCtor = generateTypeCtorDeclarationFn(node, meta, nodeTypeRef.typeName, this.config); + const typeParams = this.emitTypeParameters(node); + const typeCtor = generateTypeCtorDeclarationFn( + node, meta, nodeTypeRef.typeName, typeParams, this.reflector); this.typeCtorStatements.push(typeCtor); const fnId = ts.createIdentifier(fnName); this.typeCtors.set(node, fnId); @@ -213,7 +217,7 @@ export class Environment { * * This may involve importing the node into the file if it's not declared there already. */ - referenceType(ref: Reference>): ts.TypeNode { + referenceType(ref: Reference): ts.TypeNode { const ngExpr = this.refEmitter.emit(ref, this.contextFile); // Create an `ExpressionType` from the `Expression` and translate it via `translateType`. @@ -221,6 +225,12 @@ export class Environment { return translateType(new ExpressionType(ngExpr), this.importManager); } + private emitTypeParameters(declaration: ClassDeclaration): + ts.TypeParameterDeclaration[]|undefined { + const emitter = new TypeParameterEmitter(declaration.typeParameters, this.reflector); + return emitter.emit(ref => this.referenceType(ref)); + } + /** * Generate a `ts.TypeNode` that references a given type from the provided module. * diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts index 3a08bde37f..5df3ab2fb8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {AbsoluteFsPath, join} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; -import {ClassDeclaration} from '../../reflection'; +import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; import {TypeCheckBlockMetadata, TypeCheckingConfig} from './api'; @@ -32,9 +32,11 @@ export class TypeCheckFile extends Environment { private nextTcbId = 1; private tcbStatements: ts.Statement[] = []; - constructor(private fileName: string, config: TypeCheckingConfig, refEmitter: ReferenceEmitter) { + constructor( + private fileName: string, config: TypeCheckingConfig, refEmitter: ReferenceEmitter, + reflector: ReflectionHost) { super( - config, new ImportManager(new NoopImportRewriter(), 'i'), refEmitter, + config, new ImportManager(new NoopImportRewriter(), 'i'), refEmitter, reflector, ts.createSourceFile(fileName, '', ts.ScriptTarget.Latest, true)); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts index 96160a9690..55c559f655 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts @@ -8,25 +8,25 @@ import * as ts from 'typescript'; -import {ClassDeclaration} from '../../reflection'; +import {ClassDeclaration, ReflectionHost} from '../../reflection'; -import {TypeCheckingConfig, TypeCtorMetadata} from './api'; -import {checkIfGenericTypesAreUnbound} from './ts_util'; +import {TypeCtorMetadata} from './api'; +import {TypeParameterEmitter} from './type_parameter_emitter'; export function generateTypeCtorDeclarationFn( - node: ClassDeclaration, meta: TypeCtorMetadata, - nodeTypeRef: ts.Identifier | ts.QualifiedName, config: TypeCheckingConfig): ts.Statement { - if (requiresInlineTypeCtor(node)) { + node: ClassDeclaration, meta: TypeCtorMetadata, nodeTypeRef: ts.EntityName, + typeParams: ts.TypeParameterDeclaration[] | undefined, + reflector: ReflectionHost): ts.Statement { + if (requiresInlineTypeCtor(node, reflector)) { throw new Error(`${node.name.text} requires an inline type constructor`); } - const rawTypeArgs = - node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined; + const rawTypeArgs = typeParams !== undefined ? generateGenericArgs(typeParams) : undefined; const rawType = ts.createTypeReferenceNode(nodeTypeRef, rawTypeArgs); const initParam = constructTypeCtorParameter(node, meta, rawType); - const typeParameters = typeParametersWithDefaultTypes(node.typeParameters); + const typeParameters = typeParametersWithDefaultTypes(typeParams); if (meta.body) { const fnType = ts.createFunctionTypeNode( @@ -188,9 +188,17 @@ function generateGenericArgs(params: ReadonlyArray) return params.map(param => ts.createTypeReferenceNode(param.name, undefined)); } -export function requiresInlineTypeCtor(node: ClassDeclaration): boolean { - // The class requires an inline type constructor if it has constrained (bound) generics. - return !checkIfGenericTypesAreUnbound(node); +export function requiresInlineTypeCtor( + node: ClassDeclaration, host: ReflectionHost): boolean { + // The class requires an inline type constructor if it has generic type bounds that can not be + // emitted into a different context. + return !checkIfGenericTypeBoundsAreContextFree(node, host); +} + +function checkIfGenericTypeBoundsAreContextFree( + node: ClassDeclaration, reflector: ReflectionHost): boolean { + // Generic type parameters are considered context free if they can be emitted into any context. + return new TypeParameterEmitter(node.typeParameters, reflector).canEmit(); } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts new file mode 100644 index 0000000000..877235ef4b --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_emitter.ts @@ -0,0 +1,183 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; +import {Reference} from '../../imports'; + +/** + * A resolved type reference can either be a `Reference`, the original `ts.TypeReferenceNode` itself + * or null to indicate the no reference could be resolved. + */ +export type ResolvedTypeReference = Reference | ts.TypeReferenceNode | null; + +/** + * A type reference resolver function is responsible for finding the declaration of the type + * reference and verifying whether it can be emitted. + */ +export type TypeReferenceResolver = (type: ts.TypeReferenceNode) => ResolvedTypeReference; + +/** + * Determines whether the provided type can be emitted, which means that it can be safely emitted + * into a different location. + * + * If this function returns true, a `TypeEmitter` should be able to succeed. Vice versa, if this + * function returns false, then using the `TypeEmitter` should not be attempted as it is known to + * fail. + */ +export function canEmitType(type: ts.TypeNode, resolver: TypeReferenceResolver): boolean { + return canEmitTypeWorker(type); + + function canEmitTypeWorker(type: ts.TypeNode): boolean { + return visitTypeNode(type, { + visitTypeReferenceNode: type => canEmitTypeReference(type), + visitArrayTypeNode: type => canEmitTypeWorker(type.elementType), + visitKeywordType: () => true, + visitOtherType: () => false, + }); + } + + function canEmitTypeReference(type: ts.TypeReferenceNode): boolean { + const reference = resolver(type); + + // If the type could not be resolved, it can not be emitted. + if (reference === null) { + return false; + } + + // If the type is a reference without a owning module, consider the type not to be eligible for + // emitting. + if (reference instanceof Reference && !reference.hasOwningModuleGuess) { + return false; + } + + // The type can be emitted if either it does not have any type arguments, or all of them can be + // emitted. + return type.typeArguments === undefined || type.typeArguments.every(canEmitTypeWorker); + } +} + +/** + * Given a `ts.TypeNode`, this class derives an equivalent `ts.TypeNode` that has been emitted into + * a different context. + * + * For example, consider the following code: + * + * ``` + * import {NgIterable} from '@angular/core'; + * + * class NgForOf> {} + * ``` + * + * Here, the generic type parameters `T` and `U` can be emitted into a different context, as the + * type reference to `NgIterable` originates from an absolute module import so that it can be + * emitted anywhere, using that same module import. The process of emitting translates the + * `NgIterable` type reference to a type reference that is valid in the context in which it is + * emitted, for example: + * + * ``` + * import * as i0 from '@angular/core'; + * import * as i1 from '@angular/common'; + * + * const _ctor1: >(o: Pick, 'ngForOf'>): + * i1.NgForOf; + * ``` + * + * Notice how the type reference for `NgIterable` has been translated into a qualified name, + * referring to the namespace import that was created. + */ +export class TypeEmitter { + /** + * Resolver function that computes a `Reference` corresponding with a `ts.TypeReferenceNode`. + */ + private resolver: TypeReferenceResolver; + + /** + * Given a `Reference`, this function is responsible for the actual emitting work. It should + * produce a `ts.TypeNode` that is valid within the desired context. + */ + private emitReference: (ref: Reference) => ts.TypeNode; + + constructor(resolver: TypeReferenceResolver, emitReference: (ref: Reference) => ts.TypeNode) { + this.resolver = resolver; + this.emitReference = emitReference; + } + + 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, + visitOtherType: () => { throw new Error('Unable to emit a complex type'); }, + }); + } + + private emitTypeReference(type: ts.TypeReferenceNode): ts.TypeNode { + // Determine the reference that the type corresponds with. + const reference = this.resolver(type); + if (reference === null) { + throw new Error('Unable to emit an unresolved reference'); + } + + // Emit the type arguments, if any. + let typeArguments: ts.NodeArray|undefined = undefined; + if (type.typeArguments !== undefined) { + typeArguments = ts.createNodeArray(type.typeArguments.map(typeArg => this.emitType(typeArg))); + } + + // Emit the type name. + let typeName = type.typeName; + if (reference instanceof Reference) { + if (!reference.hasOwningModuleGuess) { + throw new Error('A type reference to emit must be imported from an absolute module'); + } + + const emittedType = this.emitReference(reference); + if (!ts.isTypeReferenceNode(emittedType)) { + throw new Error( + `Expected TypeReferenceNode for emitted reference, got ${ts.SyntaxKind[emittedType.kind]}`); + } + + typeName = emittedType.typeName; + } + + 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; + 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); + } + + 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/src/type_parameter_emitter.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_parameter_emitter.ts new file mode 100644 index 0000000000..20d6bf1b18 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_parameter_emitter.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; + +import {OwningModule, Reference} from '../../imports'; +import {ReflectionHost} from '../../reflection'; + +import {ResolvedTypeReference, TypeEmitter, canEmitType} from './type_emitter'; + + +/** + * See `TypeEmitter` for more information on the emitting process. + */ +export class TypeParameterEmitter { + constructor( + private typeParameters: ts.NodeArray|undefined, + private reflector: ReflectionHost) {} + + /** + * Determines whether the type parameters can be emitted. If this returns true, then a call to + * `emit` is known to succeed. Vice versa, if false is returned then `emit` should not be + * called, as it would fail. + */ + canEmit(): boolean { + if (this.typeParameters === undefined) { + return true; + } + + return this.typeParameters.every(typeParam => { + if (typeParam.constraint === undefined) { + return true; + } + + return canEmitType(typeParam.constraint, type => this.resolveTypeReference(type)); + }); + } + + /** + * Emits the type parameters using the provided emitter function for `Reference`s. + */ + emit(emitReference: (ref: Reference) => ts.TypeNode): ts.TypeParameterDeclaration[]|undefined { + if (this.typeParameters === undefined) { + return undefined; + } + + const emitter = new TypeEmitter(type => this.resolveTypeReference(type), emitReference); + + return this.typeParameters.map(typeParam => { + const constraint = + typeParam.constraint !== undefined ? emitter.emitType(typeParam.constraint) : undefined; + + return ts.updateTypeParameterDeclaration( + /* node */ typeParam, + /* name */ typeParam.name, + /* constraint */ constraint, + /* defaultType */ typeParam.default); + }); + } + + private resolveTypeReference(type: ts.TypeReferenceNode): ResolvedTypeReference { + const target = ts.isIdentifier(type.typeName) ? type.typeName : type.typeName.right; + const declaration = this.reflector.getDeclarationOfIdentifier(target); + + // If no declaration could be resolved or does not have a `ts.Declaration`, the type cannot be + // resolved. + if (declaration === null || declaration.node === null) { + return null; + } + + // If the declaration corresponds with a local type parameter, the type reference can be used + // as is. + if (this.isLocalTypeParameter(declaration.node)) { + return type; + } + + let owningModule: OwningModule|null = null; + if (declaration.viaModule !== null) { + owningModule = { + specifier: declaration.viaModule, + resolutionContext: type.getSourceFile().fileName, + }; + } + + return new Reference(declaration.node, owningModule); + } + + private isLocalTypeParameter(decl: ts.Declaration): boolean { + // Checking for local type parameters only occurs during resolution of type parameters, so it is + // guaranteed that type parameters are present. + return this.typeParameters !.some(param => param === decl); + } +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index f14c2c91f8..6c127651d6 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -92,6 +92,8 @@ export function angularCoreDts(): TestFile { export declare class EventEmitter { subscribe(generatorOrNext?: any, error?: any, complete?: any): unknown; } + + export declare type NgIterable = Array | Iterable; ` }; } @@ -258,7 +260,8 @@ export function typecheck( program, checker, moduleResolver, new TypeScriptReflectionHost(checker)), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); - const ctx = new TypeCheckContext({...ALL_ENABLED_CONFIG, ...config}, emitter, typeCheckFilePath); + const ctx = new TypeCheckContext( + {...ALL_ENABLED_CONFIG, ...config}, emitter, reflectionHost, typeCheckFilePath); const templateUrl = 'synthetic.html'; const templateFile = new ParseSourceFile(template, templateUrl); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index e164342bd5..0dbcd2f9bd 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -40,8 +40,9 @@ runInEachFileSystem(() => { }); it('should not produce an empty SourceFile when there is nothing to typecheck', () => { - const file = - new TypeCheckFile(_('/_typecheck_.ts'), ALL_ENABLED_CONFIG, new ReferenceEmitter([])); + const file = new TypeCheckFile( + _('/_typecheck_.ts'), ALL_ENABLED_CONFIG, new ReferenceEmitter([]), + /* reflector */ null !); const sf = file.render(); expect(sf.statements.length).toBe(1); }); @@ -71,7 +72,8 @@ TestClass.ngTypeCtor({value: 'test'}); new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); - const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts')); + const ctx = + new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, reflectionHost, _('/_typecheck_.ts')); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( @@ -106,7 +108,8 @@ TestClass.ngTypeCtor({value: 'test'}); new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); - const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts')); + const ctx = + new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, reflectionHost, _('/_typecheck_.ts')); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( @@ -147,7 +150,8 @@ TestClass.ngTypeCtor({value: 'test'}); new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); - const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, _('/_typecheck_.ts')); + const ctx = + new TypeCheckContext(ALL_ENABLED_CONFIG, emitter, reflectionHost, _('/_typecheck_.ts')); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( 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 new file mode 100644 index 0000000000..e8ef2f0193 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts @@ -0,0 +1,166 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as ts from 'typescript'; + +import {absoluteFrom} from '../../file_system'; +import {TestFile, runInEachFileSystem} from '../../file_system/testing'; +import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; +import {getDeclaration, makeProgram} from '../../testing'; +import {TypeParameterEmitter} from '../src/type_parameter_emitter'; +import {angularCoreDts} from './test_utils'; + + +runInEachFileSystem(() => { + describe('type parameter emitter', () => { + + function createEmitter(source: string, additionalFiles: TestFile[] = []) { + const files: TestFile[] = [ + angularCoreDts(), {name: absoluteFrom('/main.ts'), contents: source}, ...additionalFiles + ]; + const {program} = makeProgram(files, undefined, undefined, false); + const checker = program.getTypeChecker(); + const reflector = new TypeScriptReflectionHost(checker); + + const TestClass = + getDeclaration(program, absoluteFrom('/main.ts'), 'TestClass', isNamedClassDeclaration); + + return new TypeParameterEmitter(TestClass.typeParameters, reflector); + } + + function emit(emitter: TypeParameterEmitter) { + const emitted = emitter.emit(ref => { + const typeName = ts.createQualifiedName(ts.createIdentifier('test'), ref.debugName !); + return ts.createTypeReferenceNode(typeName, /* typeArguments */ undefined); + }); + + if (emitted === undefined) { + return ''; + } + + const printer = ts.createPrinter(); + const sf = ts.createSourceFile('test.ts', '', ts.ScriptTarget.Latest); + const generics = + emitted.map(param => printer.printNode(ts.EmitHint.Unspecified, param, sf)).join(', '); + + return `<${generics}>`; + } + + it('can emit for simple generic 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(''); + 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', () => { + const emitter = createEmitter(` + import {NgIterable} from '@angular/core'; + + export class TestClass> {}`); + + expect(emitter.canEmit()).toBe(true); + expect(emit(emitter)).toEqual('>'); + }); + + it('can emit references into external modules using qualified name', () => { + const emitter = createEmitter(` + import * as ng from '@angular/core'; + + export class TestClass> {}`); + + expect(emitter.canEmit()).toBe(true); + expect(emit(emitter)).toEqual('>'); + }); + + it('can emit references to other type parameters', () => { + const emitter = createEmitter(` + import {NgIterable} from '@angular/core'; + + export class TestClass> {}`); + + expect(emitter.canEmit()).toBe(true); + expect(emit(emitter)).toEqual('>'); + }); + + it('cannot emit references to local declarations', () => { + const emitter = createEmitter(` + export class Local {}; + export class TestClass {}`); + + expect(emitter.canEmit()).toBe(false); + expect(() => emit(emitter)) + .toThrowError('A type reference to emit must be imported from an absolute module'); + }); + + it('cannot emit references to local declarations as nested type arguments', () => { + const emitter = createEmitter(` + import {NgIterable} from '@angular/core'; + + export class Local {}; + export class TestClass> {}`); + + expect(emitter.canEmit()).toBe(false); + expect(() => emit(emitter)) + .toThrowError('A type reference to emit must be imported from an absolute module'); + }); + + it('can emit references into external modules within array types', () => { + const emitter = createEmitter(` + import {NgIterable} from '@angular/core'; + + export class TestClass {}`); + + expect(emitter.canEmit()).toBe(true); + expect(emit(emitter)).toEqual(''); + }); + + it('cannot emit references to local declarations within array types', () => { + const emitter = createEmitter(` + export class Local {}; + export class TestClass {}`); + + expect(emitter.canEmit()).toBe(false); + expect(() => emit(emitter)) + .toThrowError('A type reference to emit must be imported from an absolute module'); + }); + + it('cannot emit references into relative files', () => { + const additionalFiles: TestFile[] = [{ + name: absoluteFrom('/internal.ts'), + contents: `export class Internal {}`, + }]; + const emitter = createEmitter( + ` + import {Internal} from './internal'; + + export class TestClass {}`, + additionalFiles); + + expect(emitter.canEmit()).toBe(false); + expect(() => emit(emitter)) + .toThrowError('A type reference to emit must be imported from an absolute module'); + }); + + }); +}); diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index 0eedf3cfbc..09df8fcfc4 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -67,8 +67,9 @@ export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifie } export function isDeclaration(node: ts.Node): node is ts.Declaration { - return false || ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) || - ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node); + return ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) || + ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node) || + ts.isTypeAliasDeclaration(node); } export function isExported(node: ts.Declaration): boolean {