perf(ivy): support simple generic type constraints in local type ctors (#34021)

In Ivy's template type checker, type constructors are created for all
directive types to allow for accurate type inference to work. The type
checker has two strategies for dealing with such type constructors:

1. They can be emitted local to the type check block/type check file.
2. They can be emitted as static `ngTypeCtor` field into the directive
itself.

The first strategy is preferred, as it avoids having to update the
directive type which would cause a more expensive rebuild. However, this
strategy is not suitable for directives that have constrained generic
types, as those constraints would need to be present on the local type
constructor declaration. This is not trivial, as it requires that any
type references within a type parameter's constraint are imported into
the local context of the type check block.

For example, lets consider the `NgForOf` directive from '@angular/core'
looks as follows:

```typescript
import {NgIterable} from '@angular/core';

export class NgForOf<T, U extends NgIterable<T>> {}
```

The type constructor will then have the signature:
`(o: Pick<i1.NgForOf<T, U>, 'ngForOf'>) => i1.NgForOf<T, U>`

Notice how this refers to the type parameters `T` and `U`, so the type
constructor needs to be emitted into a scope where those types are
available, _and_ have the correct constraints.

Previously, the template type checker would detect the situation where a
type parameter is constrained, and would emit the type constructor
using strategy 2; within the directive type itself. This approach makes
any type references within the generic type constraints lexically
available:

```typescript
export class NgForOf<T, U extends NgIterable<T>> {
  static ngTypeCtor<T = any, U extends NgIterable<T> = any>
    (o: Pick<NgForOf<T, U>, 'ngForOf'>): NgForOf<T, U> { return null!; }
}
```

This commit introduces the ability to emit a type parameter with
constraints into a different context, under the condition that it can
be imported from an absolute module. This allows a generic type
constructor to be emitted into a type check block or type check file
according to strategy 1, as imports have been generated for all type
references within generic type constraints. For example:

```typescript
import * as i0 from '@angular/core';
import * as i1 from '@angular/common';

const _ctor1: <T = any, U extends i0.NgIterable<T> = any>
  (o: Pick<i1.NgForOf<T, U>, 'ngForOf'>) => i1.NgForOf<T, U> = null!;
```

Notice how the generic type constraint of `U` has resulted in an import
of `@angular/core`, and the `NgIterable` is transformed into a qualified
name during the emitting process.

Resolves FW-1739

PR Close #34021
This commit is contained in:
JoostK 2019-11-24 17:35:20 +01:00 committed by Alex Rickabaugh
parent 19944c2424
commit f27187c063
12 changed files with 520 additions and 42 deletions

View File

@ -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);

View File

@ -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) {

View File

@ -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<ClassDeclaration<ts.ClassDeclaration>>;
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<ClassDeclaration<ts.ClassDeclaration>>,
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);

View File

@ -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<ClassDeclaration<ts.ClassDeclaration>>): 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.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.
*

View File

@ -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));
}

View File

@ -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<ts.ClassDeclaration>, meta: TypeCtorMetadata,
nodeTypeRef: ts.Identifier | ts.QualifiedName, config: TypeCheckingConfig): ts.Statement {
if (requiresInlineTypeCtor(node)) {
node: ClassDeclaration<ts.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<ts.TypeParameterDeclaration>)
return params.map(param => ts.createTypeReferenceNode(param.name, undefined));
}
export function requiresInlineTypeCtor(node: ClassDeclaration<ts.ClassDeclaration>): boolean {
// The class requires an inline type constructor if it has constrained (bound) generics.
return !checkIfGenericTypesAreUnbound(node);
export function requiresInlineTypeCtor(
node: ClassDeclaration<ts.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<ts.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();
}
/**

View File

@ -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<T, U extends NgIterable<T>> {}
* ```
*
* 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: <T, U extends i0.NgIterable<T>>(o: Pick<i1.NgForOf<T, U>, 'ngForOf'>):
* i1.NgForOf<T, U>;
* ```
*
* 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<ts.TypeNode>|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<R> {
visitTypeReferenceNode(type: ts.TypeReferenceNode): R;
visitArrayTypeNode(type: ts.ArrayTypeNode): R;
visitKeywordType(type: ts.KeywordTypeNode): R;
visitOtherType(type: ts.TypeNode): R;
}
function visitTypeNode<R>(type: ts.TypeNode, visitor: TypeEmitterVisitor<R>): 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);
}
}

View File

@ -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<ts.TypeParameterDeclaration>|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);
}
}

View File

@ -92,6 +92,8 @@ export function angularCoreDts(): TestFile {
export declare class EventEmitter<T> {
subscribe(generatorOrNext?: any, error?: any, complete?: any): unknown;
}
export declare type NgIterable<T> = Array<T> | Iterable<T>;
`
};
}
@ -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);

View File

@ -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(

View File

@ -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<T> {}`))).toEqual('<T>');
expect(emit(createEmitter(`export class TestClass<T extends any> {}`)))
.toEqual('<T extends any>');
expect(emit(createEmitter(`export class TestClass<T extends unknown> {}`)))
.toEqual('<T extends unknown>');
expect(emit(createEmitter(`export class TestClass<T extends string> {}`)))
.toEqual('<T extends string>');
expect(emit(createEmitter(`export class TestClass<T extends number> {}`)))
.toEqual('<T extends number>');
expect(emit(createEmitter(`export class TestClass<T extends boolean> {}`)))
.toEqual('<T extends boolean>');
expect(emit(createEmitter(`export class TestClass<T extends object> {}`)))
.toEqual('<T extends object>');
expect(emit(createEmitter(`export class TestClass<T extends null> {}`)))
.toEqual('<T extends null>');
expect(emit(createEmitter(`export class TestClass<T extends undefined> {}`)))
.toEqual('<T extends undefined>');
expect(emit(createEmitter(`export class TestClass<T extends string[]> {}`)))
.toEqual('<T extends string[]>');
});
it('can emit references into external modules', () => {
const emitter = createEmitter(`
import {NgIterable} from '@angular/core';
export class TestClass<T extends NgIterable<any>> {}`);
expect(emitter.canEmit()).toBe(true);
expect(emit(emitter)).toEqual('<T extends test.NgIterable<any>>');
});
it('can emit references into external modules using qualified name', () => {
const emitter = createEmitter(`
import * as ng from '@angular/core';
export class TestClass<T extends ng.NgIterable<any>> {}`);
expect(emitter.canEmit()).toBe(true);
expect(emit(emitter)).toEqual('<T extends test.NgIterable<any>>');
});
it('can emit references to other type parameters', () => {
const emitter = createEmitter(`
import {NgIterable} from '@angular/core';
export class TestClass<T, U extends NgIterable<T>> {}`);
expect(emitter.canEmit()).toBe(true);
expect(emit(emitter)).toEqual('<T, U extends test.NgIterable<T>>');
});
it('cannot emit references to local declarations', () => {
const emitter = createEmitter(`
export class Local {};
export class TestClass<T extends Local> {}`);
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<T extends NgIterable<Local>> {}`);
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<T extends NgIterable[]> {}`);
expect(emitter.canEmit()).toBe(true);
expect(emit(emitter)).toEqual('<T extends test.NgIterable[]>');
});
it('cannot emit references to local declarations within array types', () => {
const emitter = createEmitter(`
export class Local {};
export class TestClass<T extends Local[]> {}`);
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<T extends Internal> {}`,
additionalFiles);
expect(emitter.canEmit()).toBe(false);
expect(() => emit(emitter))
.toThrowError('A type reference to emit must be imported from an absolute module');
});
});
});

View File

@ -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 {