perf(ivy): template type-check the entire program in 1 file if possible (#29698)

The template type-checking engine previously would assemble a type-checking
program by inserting Type Check Blocks (TCBs) into existing user files. This
approach proved expensive, as TypeScript has to re-parse and re-type-check
those files when processing the type-checking program.

Instead, a far more performant approach is to augment the program with a
single type-checking file, into which all TCBs are generated. Additionally,
type constructors are also inlined into this file.

This is not always possible - both TCBs and type constructors can sometimes
require inlining into user code, particularly if bound generic type
parameters are present, so the approach taken is actually a hybrid. These
operations are inlined if necessary, but are otherwise generated in a single
file.

It is critically important that the original program also include an empty
version of the type-checking file, otherwise the shape of the two programs
will be different and TypeScript will throw away all the old program
information. This leads to a painfully slow type checking pass, on the same
order as the original program creation. A shim to generate this file in the
original program is therefore added.

Testing strategy: this commit is largely a refactor with no externally
observable behavioral differences, and thus no tests are needed.

PR Close #29698
This commit is contained in:
Alex Rickabaugh 2019-04-02 11:25:33 -07:00 committed by Ben Lesh
parent f4c536ae36
commit 98f86de8da
17 changed files with 753 additions and 317 deletions

View File

@ -313,7 +313,7 @@ export class ComponentDecoratorHandler implements
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta); matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
} }
const bound = new R3TargetBinder(matcher).bind({template: meta.parsedTemplate}); const bound = new R3TargetBinder(matcher).bind({template: meta.parsedTemplate});
ctx.addTemplate(node, bound); ctx.addTemplate(new Reference(node), bound);
} }
} }

View File

@ -27,11 +27,11 @@ import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader'; import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope';
import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, generatedFactoryTransform} from './shims'; import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims';
import {ivySwitchTransform} from './switch'; import {ivySwitchTransform} from './switch';
import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
import {aliasTransformFactory} from './transform/src/alias'; import {aliasTransformFactory} from './transform/src/alias';
import {TypeCheckContext, TypeCheckProgramHost, TypeCheckingConfig} from './typecheck'; import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck';
import {normalizeSeparators} from './util/src/path'; import {normalizeSeparators} from './util/src/path';
import {getRootDirs, isDtsPath} from './util/src/typescript'; import {getRootDirs, isDtsPath} from './util/src/typescript';
@ -64,6 +64,7 @@ export class NgtscProgram implements api.Program {
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER; private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
private perfTracker: PerfTracker|null = null; private perfTracker: PerfTracker|null = null;
private incrementalState: IncrementalState; private incrementalState: IncrementalState;
private typeCheckFilePath: AbsoluteFsPath;
constructor( constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions, rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
@ -105,6 +106,10 @@ export class NgtscProgram implements api.Program {
generators.push(summaryGenerator, factoryGenerator); generators.push(summaryGenerator, factoryGenerator);
} }
this.typeCheckFilePath = typeCheckFilePath(this.rootDirs);
generators.push(new TypeCheckShimGenerator(this.typeCheckFilePath));
rootFiles.push(this.typeCheckFilePath);
let entryPoint: string|null = null; let entryPoint: string|null = null;
if (options.flatModuleOutFile !== undefined) { if (options.flatModuleOutFile !== undefined) {
entryPoint = findFlatIndexEntryPoint(normalizedRootNames); entryPoint = findFlatIndexEntryPoint(normalizedRootNames);
@ -189,18 +194,7 @@ export class NgtscProgram implements api.Program {
fileName?: string|undefined, cancellationToken?: ts.CancellationToken| fileName?: string|undefined, cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> { undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> {
const compilation = this.ensureAnalyzed(); const compilation = this.ensureAnalyzed();
const diagnostics = [...compilation.diagnostics]; const diagnostics = [...compilation.diagnostics, ...this.getTemplateDiagnostics()];
if (!!this.options.fullTemplateTypeCheck) {
const config: TypeCheckingConfig = {
applyTemplateContextGuards: true,
checkTemplateBodies: true,
checkTypeOfBindings: true,
strictSafeNavigationTypes: true,
};
const ctx = new TypeCheckContext(config, this.refEmitter !);
compilation.typeCheck(ctx);
diagnostics.push(...this.compileTypeCheckProgram(ctx));
}
if (this.entryPoint !== null && this.exportReferenceGraph !== null) { if (this.entryPoint !== null && this.exportReferenceGraph !== null) {
diagnostics.push(...checkForPrivateExports( diagnostics.push(...checkForPrivateExports(
this.entryPoint, this.tsProgram.getTypeChecker(), this.exportReferenceGraph)); this.entryPoint, this.tsProgram.getTypeChecker(), this.exportReferenceGraph));
@ -344,8 +338,11 @@ export class NgtscProgram implements api.Program {
const emitSpan = this.perfRecorder.start('emit'); const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = []; const emitResults: ts.EmitResult[] = [];
const typeCheckFile = this.tsProgram.getSourceFile(this.typeCheckFilePath);
for (const targetSourceFile of this.tsProgram.getSourceFiles()) { for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile) { if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) {
continue; continue;
} }
@ -378,15 +375,47 @@ export class NgtscProgram implements api.Program {
return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults); return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults);
} }
private compileTypeCheckProgram(ctx: TypeCheckContext): ReadonlyArray<ts.Diagnostic> { private getTemplateDiagnostics(): ReadonlyArray<ts.Diagnostic> {
const host = new TypeCheckProgramHost(this.tsProgram, this.host, ctx); // Skip template type-checking unless explicitly requested.
const auxProgram = ts.createProgram({ if (this.options.fullTemplateTypeCheck !== true) {
host, return [];
rootNames: this.tsProgram.getRootFileNames(), }
oldProgram: this.tsProgram,
options: this.options, const compilation = this.ensureAnalyzed();
});
return auxProgram.getSemanticDiagnostics(); // Run template type-checking.
// First select a type-checking configuration, based on whether full template type-checking is
// requested.
let typeCheckingConfig: TypeCheckingConfig;
if (this.options.fullTemplateTypeCheck) {
typeCheckingConfig = {
applyTemplateContextGuards: true,
checkTemplateBodies: true,
checkTypeOfBindings: true,
strictSafeNavigationTypes: true,
};
} else {
typeCheckingConfig = {
applyTemplateContextGuards: false,
checkTemplateBodies: false,
checkTypeOfBindings: false,
strictSafeNavigationTypes: false,
};
}
// 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);
compilation.typeCheck(ctx);
this.perfRecorder.stop(prepSpan);
// Get the diagnostics.
const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics');
const diagnostics = ctx.calculateTemplateDiagnostics(this.tsProgram, this.host, this.options);
this.perfRecorder.stop(typeCheckSpan);
return diagnostics;
} }
private makeCompilation(): IvyCompilation { private makeCompilation(): IvyCompilation {

View File

@ -11,3 +11,4 @@
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator'; export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host'; export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host';
export {SummaryGenerator} from './src/summary_generator'; export {SummaryGenerator} from './src/summary_generator';
export {TypeCheckShimGenerator} from './src/typecheck_shim';

View File

@ -0,0 +1,33 @@
/**
* @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 {AbsoluteFsPath} from '../../path';
import {ShimGenerator} from './host';
/**
* A `ShimGenerator` which adds a type-checking file to the `ts.Program`.
*
* This is a requirement for performant template type-checking, as TypeScript will only reuse
* information in the main program when creating the type-checking program if the set of files in
* each are exactly the same. Thus, the main program also needs the synthetic type-checking file.
*/
export class TypeCheckShimGenerator implements ShimGenerator {
constructor(private typeCheckFile: AbsoluteFsPath) {}
recognize(fileName: AbsoluteFsPath): boolean { return fileName === this.typeCheckFile; }
generate(genFileName: AbsoluteFsPath, readFile: (fileName: string) => ts.SourceFile | null):
ts.SourceFile|null {
return ts.createSourceFile(
genFileName, 'export const USED_FOR_NG_TYPE_CHECKING = true;', ts.ScriptTarget.Latest, true,
ts.ScriptKind.TS);
}
}

View File

@ -10,9 +10,11 @@ ts_library(
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",
"@npm//typescript", "@npm//typescript",
], ],
) )

View File

@ -9,3 +9,4 @@
export * from './src/api'; export * from './src/api';
export {TypeCheckContext} from './src/context'; export {TypeCheckContext} from './src/context';
export {TypeCheckProgramHost} from './src/host'; export {TypeCheckProgramHost} from './src/host';
export {typeCheckFilePath} from './src/type_check_file';

View File

@ -32,11 +32,6 @@ export interface TypeCheckBlockMetadata {
* Semantic information about the template of the component. * Semantic information about the template of the component.
*/ */
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>; boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
/**
* The name of the requested type check block function.
*/
fnName: string;
} }
export interface TypeCtorMetadata { export interface TypeCtorMetadata {

View File

@ -9,13 +9,17 @@
import {BoundTarget} from '@angular/compiler'; import {BoundTarget} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NoopImportRewriter, ReferenceEmitter} from '../../imports'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {AbsoluteFsPath} from '../../path';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration} from '../../reflection';
import {ImportManager} from '../../translator'; import {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api';
import {generateTypeCheckBlock} from './type_check_block'; import {Environment} from './environment';
import {generateTypeCtor} from './type_constructor'; import {TypeCheckProgramHost} from './host';
import {generateTypeCheckBlock, requiresInlineTypeCheckBlock} from './type_check_block';
import {TypeCheckFile, typeCheckFilePath} from './type_check_file';
import {generateInlineTypeCtor, requiresInlineTypeCtor} from './type_constructor';
@ -27,7 +31,13 @@ import {generateTypeCtor} from './type_constructor';
* checking code. * checking code.
*/ */
export class TypeCheckContext { export class TypeCheckContext {
constructor(private config: TypeCheckingConfig, private refEmitter: ReferenceEmitter) {} private typeCheckFile: TypeCheckFile;
constructor(
private config: TypeCheckingConfig, private refEmitter: ReferenceEmitter,
typeCheckFilePath: AbsoluteFsPath) {
this.typeCheckFile = new TypeCheckFile(typeCheckFilePath, this.config, this.refEmitter);
}
/** /**
* A `Map` of `ts.SourceFile`s that the context has seen to the operations (additions of methods * A `Map` of `ts.SourceFile`s that the context has seen to the operations (additions of methods
@ -35,6 +45,12 @@ export class TypeCheckContext {
*/ */
private opMap = new Map<ts.SourceFile, Op[]>(); private opMap = new Map<ts.SourceFile, Op[]>();
/**
* Tracks when an a particular class has a pending type constructor patching operation already
* queued.
*/
private typeCtorPending = new Set<ts.ClassDeclaration>();
/** /**
* Record a template for the given component `node`, with a `SelectorMatcher` for directive * Record a template for the given component `node`, with a `SelectorMatcher` for directive
* matching. * matching.
@ -44,39 +60,50 @@ export class TypeCheckContext {
* @param matcher `SelectorMatcher` which tracks directives that are in scope for this template. * @param matcher `SelectorMatcher` which tracks directives that are in scope for this template.
*/ */
addTemplate( addTemplate(
node: ClassDeclaration<ts.ClassDeclaration>, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>): void { boundTarget: BoundTarget<TypeCheckableDirectiveMeta>): void {
// Get all of the directives used in the template and record type constructors for all of them. // Get all of the directives used in the template and record type constructors for all of them.
boundTarget.getUsedDirectives().forEach(dir => { for (const dir of boundTarget.getUsedDirectives()) {
const dirNode = dir.ref.node as ClassDeclaration<ts.ClassDeclaration>; const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
// Add a type constructor operation for the directive. const dirNode = dirRef.node;
this.addTypeCtor(dirNode.getSourceFile(), dirNode, { if (requiresInlineTypeCtor(dirNode)) {
fnName: 'ngTypeCtor', // Add a type constructor operation for the directive.
// The constructor should have a body if the directive comes from a .ts file, but not if it this.addInlineTypeCtor(dirNode.getSourceFile(), dirRef, {
// comes from a .d.ts file. .d.ts declarations don't have bodies. fnName: 'ngTypeCtor',
body: !dirNode.getSourceFile().fileName.endsWith('.d.ts'), // The constructor should have a body if the directive comes from a .ts file, but not if
fields: { // it comes from a .d.ts file. .d.ts declarations don't have bodies.
inputs: Object.keys(dir.inputs), body: !dirNode.getSourceFile().isDeclarationFile,
outputs: Object.keys(dir.outputs), fields: {
// TODO: support queries inputs: Object.keys(dir.inputs),
queries: dir.queries, outputs: Object.keys(dir.outputs),
}, // TODO(alxhub): support queries
}); queries: dir.queries,
}); },
});
}
}
// Record the type check block operation for the template itself. if (requiresInlineTypeCheckBlock(ref.node)) {
this.addTypeCheckBlock(node.getSourceFile(), node, { // This class didn't meet the requirements for external type checking, so generate an inline
boundTarget, // TCB for the class.
fnName: `${node.name.text}_TypeCheckBlock`, this.addInlineTypeCheckBlock(ref, {boundTarget});
}); } else {
// The class can be type-checked externally as normal.
this.typeCheckFile.addTypeCheckBlock(ref, {boundTarget});
}
} }
/** /**
* Record a type constructor for the given `node` with the given `ctorMetadata`. * Record a type constructor for the given `node` with the given `ctorMetadata`.
*/ */
addTypeCtor( addInlineTypeCtor(
sf: ts.SourceFile, node: ClassDeclaration<ts.ClassDeclaration>, sf: ts.SourceFile, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
ctorMeta: TypeCtorMetadata): void { ctorMeta: TypeCtorMetadata): void {
if (this.typeCtorPending.has(ref.node)) {
return;
}
this.typeCtorPending.add(ref.node);
// Lazily construct the operation map. // Lazily construct the operation map.
if (!this.opMap.has(sf)) { if (!this.opMap.has(sf)) {
this.opMap.set(sf, []); this.opMap.set(sf, []);
@ -84,7 +111,7 @@ export class TypeCheckContext {
const ops = this.opMap.get(sf) !; const ops = this.opMap.get(sf) !;
// Push a `TypeCtorOp` into the operation queue for the source file. // Push a `TypeCtorOp` into the operation queue for the source file.
ops.push(new TypeCtorOp(node, ctorMeta)); ops.push(new TypeCtorOp(ref, ctorMeta));
} }
/** /**
@ -134,14 +161,57 @@ export class TypeCheckContext {
return ts.createSourceFile(sf.fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); return ts.createSourceFile(sf.fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
} }
private addTypeCheckBlock( calculateTemplateDiagnostics(
sf: ts.SourceFile, node: ClassDeclaration<ts.ClassDeclaration>, originalProgram: ts.Program, originalHost: ts.CompilerHost,
originalOptions: ts.CompilerOptions): ts.Diagnostic[] {
const typeCheckSf = this.typeCheckFile.render();
// First, build the map of original source files.
const sfMap = new Map<string, ts.SourceFile>();
const interestingFiles: ts.SourceFile[] = [typeCheckSf];
for (const originalSf of originalProgram.getSourceFiles()) {
const sf = this.transform(originalSf);
sfMap.set(sf.fileName, sf);
if (!sf.isDeclarationFile && this.opMap.has(originalSf)) {
interestingFiles.push(sf);
}
}
sfMap.set(typeCheckSf.fileName, typeCheckSf);
const typeCheckProgram = ts.createProgram({
host: new TypeCheckProgramHost(sfMap, originalHost),
options: originalOptions,
oldProgram: originalProgram,
rootNames: originalProgram.getRootFileNames(),
});
const diagnostics: ts.Diagnostic[] = [];
for (const sf of interestingFiles) {
diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(sf));
}
return diagnostics.filter((diag: ts.Diagnostic): boolean => {
if (diag.code === 6133 /* $var is declared but its value is never read. */) {
return false;
} else if (diag.code === 6199 /* All variables are unused. */) {
return false;
} else if (
diag.code === 2695 /* Left side of comma operator is unused and has no side effects. */) {
return false;
}
return true;
});
}
private addInlineTypeCheckBlock(
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
tcbMeta: TypeCheckBlockMetadata): void { tcbMeta: TypeCheckBlockMetadata): void {
const sf = ref.node.getSourceFile();
if (!this.opMap.has(sf)) { if (!this.opMap.has(sf)) {
this.opMap.set(sf, []); this.opMap.set(sf, []);
} }
const ops = this.opMap.get(sf) !; const ops = this.opMap.get(sf) !;
ops.push(new TcbOp(node, tcbMeta, this.config)); ops.push(new TcbOp(ref, tcbMeta, this.config));
} }
} }
@ -152,7 +222,7 @@ interface Op {
/** /**
* The node in the file which will have code generated for it. * The node in the file which will have code generated for it.
*/ */
readonly node: ClassDeclaration<ts.ClassDeclaration>; readonly ref: Reference<ClassDeclaration<ts.ClassDeclaration>>;
/** /**
* Index into the source text where the code generated by the operation should be inserted. * Index into the source text where the code generated by the operation should be inserted.
@ -171,18 +241,20 @@ interface Op {
*/ */
class TcbOp implements Op { class TcbOp implements Op {
constructor( constructor(
readonly node: ClassDeclaration<ts.ClassDeclaration>, readonly meta: TypeCheckBlockMetadata, readonly ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
readonly config: TypeCheckingConfig) {} readonly meta: TypeCheckBlockMetadata, readonly config: TypeCheckingConfig) {}
/** /**
* Type check blocks are inserted immediately after the end of the component class. * Type check blocks are inserted immediately after the end of the component class.
*/ */
get splitPoint(): number { return this.node.end + 1; } get splitPoint(): number { return this.ref.node.end + 1; }
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
string { string {
const tcb = generateTypeCheckBlock(this.node, this.meta, this.config, im, refEmitter); const env = new Environment(this.config, im, refEmitter, sf);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); const fnName = ts.createIdentifier(`_tcb_${this.ref.node.pos}`);
const fn = generateTypeCheckBlock(env, this.ref, fnName, this.meta);
return printer.printNode(ts.EmitHint.Unspecified, fn, sf);
} }
} }
@ -191,16 +263,17 @@ class TcbOp implements Op {
*/ */
class TypeCtorOp implements Op { class TypeCtorOp implements Op {
constructor( constructor(
readonly node: ClassDeclaration<ts.ClassDeclaration>, readonly meta: TypeCtorMetadata) {} readonly ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
readonly meta: TypeCtorMetadata) {}
/** /**
* Type constructor operations are inserted immediately before the end of the directive class. * Type constructor operations are inserted immediately before the end of the directive class.
*/ */
get splitPoint(): number { return this.node.end - 1; } get splitPoint(): number { return this.ref.node.end - 1; }
execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer):
string { string {
const tcb = generateTypeCtor(this.node, this.meta); const tcb = generateInlineTypeCtor(this.ref.node, this.meta);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
} }
} }

View File

@ -0,0 +1,137 @@
/**
* @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 {DYNAMIC_TYPE, ExpressionType, ExternalExpr, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports';
import {ClassDeclaration} from '../../reflection';
import {ImportManager, translateExpression, translateType} from '../../translator';
import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api';
import {generateTypeCtorDeclarationFn, requiresInlineTypeCtor} from './type_constructor';
/**
* A context which hosts one or more Type Check Blocks (TCBs).
*
* An `Environment` supports the generation of TCBs by tracking necessary imports, declarations of
* type constructors, and other statements beyond the type-checking code within the TCB itself.
* Through method calls on `Environment`, the TCB generator can request `ts.Expression`s which
* reference declarations in the `Environment` for these artifacts`.
*
* `Environment` can be used in a standalone fashion, or can be extended to support more specialized
* usage.
*/
export class Environment {
private nextIds = {
typeCtor: 1,
};
private typeCtors = new Map<ClassDeclaration, ts.Expression>();
protected typeCtorStatements: ts.Statement[] = [];
constructor(
readonly config: TypeCheckingConfig, protected importManager: ImportManager,
private refEmitter: ReferenceEmitter, protected contextFile: ts.SourceFile) {}
/**
* Get an expression referring to a type constructor for the given directive.
*
* Depending on the shape of the directive itself, this could be either a reference to a declared
* type constructor, or to an inline type constructor.
*/
typeCtorFor(dir: TypeCheckableDirectiveMeta): ts.Expression {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
const node = dirRef.node;
if (this.typeCtors.has(node)) {
return this.typeCtors.get(node) !;
}
if (requiresInlineTypeCtor(node)) {
// The constructor has already been created inline, we just need to construct a reference to
// it.
const ref = this.reference(dirRef);
const typeCtorExpr = ts.createPropertyAccess(ref, 'ngTypeCtor');
this.typeCtors.set(node, typeCtorExpr);
return typeCtorExpr;
} else {
const fnName = `_ctor${this.nextIds.typeCtor++}`;
const nodeTypeRef = this.referenceType(dirRef);
if (!ts.isTypeReferenceNode(nodeTypeRef)) {
throw new Error(`Expected TypeReferenceNode from reference to ${dirRef.debugName}`);
}
const meta: TypeCtorMetadata = {
fnName,
body: true,
fields: {
inputs: Object.keys(dir.inputs),
outputs: Object.keys(dir.outputs),
// TODO: support queries
queries: dir.queries,
}
};
const typeCtor = generateTypeCtorDeclarationFn(node, meta, nodeTypeRef.typeName);
this.typeCtorStatements.push(typeCtor);
const fnId = ts.createIdentifier(fnName);
this.typeCtors.set(node, fnId);
return fnId;
}
}
/**
* Generate a `ts.Expression` that references the given node.
*
* This may involve importing the node into the file if it's not declared there already.
*/
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
const ngExpr = this.refEmitter.emit(ref, this.contextFile);
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER);
}
/**
* Generate a `ts.TypeNode` that references the given node as a type.
*
* This may involve importing the node into the file if it's not declared there already.
*/
referenceType(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.TypeNode {
const ngExpr = this.refEmitter.emit(ref, this.contextFile);
// Create an `ExpressionType` from the `Expression` and translate it via `translateType`.
// TODO(alxhub): support references to types with generic arguments in a clean way.
return translateType(new ExpressionType(ngExpr), this.importManager);
}
/**
* Generate a `ts.TypeNode` that references a given type from '@angular/core'.
*
* This will involve importing the type into the file, and will also add a number of generic type
* parameters (using `any`) as requested.
*/
referenceCoreType(name: string, typeParamCount: number = 0): ts.TypeNode {
const external = new ExternalExpr({
moduleName: '@angular/core',
name,
});
let typeParams: Type[]|null = null;
if (typeParamCount > 0) {
typeParams = [];
for (let i = 0; i < typeParamCount; i++) {
typeParams.push(DYNAMIC_TYPE);
}
}
return translateType(new ExpressionType(external, null, typeParams), this.importManager);
}
getPreludeStatements(): ts.Statement[] {
return [
...this.typeCtorStatements,
];
}
}

View File

@ -16,24 +16,11 @@ import {TypeCheckContext} from './context';
export class TypeCheckProgramHost implements ts.CompilerHost { export class TypeCheckProgramHost implements ts.CompilerHost {
/** /**
* Map of source file names to `ts.SourceFile` instances. * Map of source file names to `ts.SourceFile` instances.
*
* This is prepopulated with all the old source files, and updated as files are augmented.
*/ */
private sfCache = new Map<string, ts.SourceFile>(); private sfMap: Map<string, ts.SourceFile>;
/** constructor(sfMap: Map<string, ts.SourceFile>, private delegate: ts.CompilerHost) {
* Tracks those files in `sfCache` which have been augmented with type checking information this.sfMap = sfMap;
* already.
*/
private augmentedSourceFiles = new Set<ts.SourceFile>();
constructor(
program: ts.Program, private delegate: ts.CompilerHost, private context: TypeCheckContext) {
// The `TypeCheckContext` uses object identity for `ts.SourceFile`s to track which files need
// type checking code inserted. Additionally, the operation of getting a source file should be
// as efficient as possible. To support both of these requirements, all of the program's
// source files are loaded into the cache up front.
program.getSourceFiles().forEach(file => { this.sfCache.set(file.fileName, file); });
if (delegate.getDirectories !== undefined) { if (delegate.getDirectories !== undefined) {
this.getDirectories = (path: string) => delegate.getDirectories !(path); this.getDirectories = (path: string) => delegate.getDirectories !(path);
@ -45,25 +32,15 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
onError?: ((message: string) => void)|undefined, onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
// Look in the cache for the source file. // Look in the cache for the source file.
let sf: ts.SourceFile|undefined = this.sfCache.get(fileName); let sf: ts.SourceFile|undefined = this.sfMap.get(fileName);
if (sf === undefined) { if (sf === undefined) {
// There should be no cache misses, but just in case, delegate getSourceFile in the event of // There should be no cache misses, but just in case, delegate getSourceFile in the event of
// a cache miss. // a cache miss.
sf = this.delegate.getSourceFile( sf = this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile); fileName, languageVersion, onError, shouldCreateNewSourceFile);
sf && this.sfCache.set(fileName, sf); sf && this.sfMap.set(fileName, sf);
}
if (sf !== undefined) {
// Maybe augment the file with type checking code via the `TypeCheckContext`.
if (!this.augmentedSourceFiles.has(sf)) {
sf = this.context.transform(sf);
this.sfCache.set(fileName, sf);
this.augmentedSourceFiles.add(sf);
}
return sf;
} else {
return undefined;
} }
return sf;
} }
// The rest of the methods simply delegate to the underlying `ts.CompilerHost`. // The rest of the methods simply delegate to the underlying `ts.CompilerHost`.
@ -76,7 +53,7 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
fileName: string, data: string, writeByteOrderMark: boolean, fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined, onError: ((message: string) => void)|undefined,
sourceFiles: ReadonlyArray<ts.SourceFile>|undefined): void { sourceFiles: ReadonlyArray<ts.SourceFile>|undefined): void {
return this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); throw new Error(`TypeCheckProgramHost should never write files`);
} }
getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); } getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); }
@ -91,7 +68,9 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
getNewLine(): string { return this.delegate.getNewLine(); } getNewLine(): string { return this.delegate.getNewLine(); }
fileExists(fileName: string): boolean { return this.delegate.fileExists(fileName); } fileExists(fileName: string): boolean {
return this.sfMap.has(fileName) || this.delegate.fileExists(fileName);
}
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); } readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
} }

View File

@ -0,0 +1,119 @@
/**
* @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 {ClassDeclaration} from '../../reflection';
export function tsCastToAny(expr: ts.Expression): ts.Expression {
return ts.createParen(
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
}
/**
* Create an expression which instantiates an element by its HTML tagName.
*
* Thanks to narrowing of `document.createElement()`, this expression will have its type inferred
* based on the tag name, including for custom elements that have appropriate .d.ts definitions.
*/
export function tsCreateElement(tagName: string): ts.Expression {
const createElement = ts.createPropertyAccess(
/* expression */ ts.createIdentifier('document'), 'createElement');
return ts.createCall(
/* expression */ createElement,
/* typeArguments */ undefined,
/* argumentsArray */[ts.createLiteral(tagName)]);
}
/**
* Create a `ts.VariableStatement` which declares a variable without explicit initialization.
*
* The initializer `null!` is used to bypass strict variable initialization checks.
*
* Unlike with `tsCreateVariable`, the type of the variable is explicitly specified.
*/
export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.VariableStatement {
const decl = ts.createVariableDeclaration(
/* name */ id,
/* type */ type,
/* initializer */ ts.createNonNullExpression(ts.createNull()));
return ts.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */[decl]);
}
/**
* Create a `ts.VariableStatement` that initializes a variable with a given expression.
*
* Unlike with `tsDeclareVariable`, the type of the variable is inferred from the initializer
* expression.
*/
export function tsCreateVariable(
id: ts.Identifier, initializer: ts.Expression): ts.VariableStatement {
const decl = ts.createVariableDeclaration(
/* name */ id,
/* type */ undefined,
/* initializer */ initializer);
return ts.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */[decl]);
}
/**
* Construct a `ts.CallExpression` that calls a method on a receiver.
*/
export function tsCallMethod(
receiver: ts.Expression, methodName: string, args: ts.Expression[] = []): ts.CallExpression {
const methodAccess = ts.createPropertyAccess(receiver, methodName);
return ts.createCall(
/* expression */ methodAccess,
/* typeArguments */ undefined,
/* argumentsArray */ args);
}
export function checkIfClassIsExported(node: ClassDeclaration): boolean {
// A class is exported if one of two conditions is met:
// 1) it has the 'export' modifier.
// 2) it's declared at the top level, and there is an export statement for the class.
if (node.modifiers !== undefined &&
node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) {
// Condition 1 is true, the class has an 'export' keyword attached.
return true;
} else if (
node.parent !== undefined && ts.isSourceFile(node.parent) &&
checkIfFileHasExport(node.parent, node.name.text)) {
// Condition 2 is true, the class is exported via an 'export {}' statement.
return true;
}
return false;
}
function checkIfFileHasExport(sf: ts.SourceFile, name: string): boolean {
for (const stmt of sf.statements) {
if (ts.isExportDeclaration(stmt) && stmt.exportClause !== undefined) {
for (const element of stmt.exportClause.elements) {
if (element.propertyName === undefined && element.name.text === name) {
// The named declaration is directly exported.
return true;
} else if (element.propertyName !== undefined && element.propertyName.text == name) {
// The named declaration is exported via an alias.
return true;
}
}
}
}
return false;
}
export function checkIfGenericTypesAreUnbound(node: ClassDeclaration<ts.ClassDeclaration>):
boolean {
if (node.typeParameters === undefined) {
return true;
}
return node.typeParameters.every(param => param.constraint === undefined);
}

View File

@ -6,15 +6,16 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AST, BindingType, BoundTarget, DYNAMIC_TYPE, ExpressionType, ExternalExpr, ImplicitReceiver, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, Type} from '@angular/compiler'; import {AST, BindingType, BoundTarget, ImplicitReceiver, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports'; import {Reference} from '../../imports';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration} from '../../reflection';
import {ImportManager, translateExpression, translateType} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig} from './api'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta} from './api';
import {Environment} from './environment';
import {astToTypescript} from './expression'; import {astToTypescript} from './expression';
import {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateVariable, tsDeclareVariable} from './ts_util';
/** /**
* Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a * Given a `ts.ClassDeclaration` for a component, and metadata regarding that component, compose a
@ -28,22 +29,36 @@ import {astToTypescript} from './expression';
* @param importManager an `ImportManager` for the file into which the TCB will be written. * @param importManager an `ImportManager` for the file into which the TCB will be written.
*/ */
export function generateTypeCheckBlock( export function generateTypeCheckBlock(
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCheckBlockMetadata, env: Environment, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>, name: ts.Identifier,
config: TypeCheckingConfig, importManager: ImportManager, meta: TypeCheckBlockMetadata): ts.FunctionDeclaration {
refEmitter: ReferenceEmitter): ts.FunctionDeclaration { const tcb = new Context(env, meta.boundTarget);
const tcb =
new Context(config, meta.boundTarget, node.getSourceFile(), importManager, refEmitter);
const scope = Scope.forNodes(tcb, null, tcb.boundTarget.target.template !); const scope = Scope.forNodes(tcb, null, tcb.boundTarget.target.template !);
const ctxRawType = env.referenceType(ref);
if (!ts.isTypeReferenceNode(ctxRawType)) {
throw new Error(
`Expected TypeReferenceNode when referencing the ctx param for ${ref.debugName}`);
}
const paramList = [tcbCtxParam(ref.node, ctxRawType.typeName)];
const scopeStatements = scope.render();
const innerBody = ts.createBlock([
...env.getPreludeStatements(),
...scopeStatements,
]);
// Wrap the body in an "if (true)" expression. This is unnecessary but has the effect of causing
// the `ts.Printer` to format the type-check block nicely.
const body = ts.createBlock([ts.createIf(ts.createTrue(), innerBody, undefined)]);
return ts.createFunctionDeclaration( return ts.createFunctionDeclaration(
/* decorators */ undefined, /* decorators */ undefined,
/* modifiers */ undefined, /* modifiers */ undefined,
/* asteriskToken */ undefined, /* asteriskToken */ undefined,
/* name */ meta.fnName, /* name */ name,
/* typeParameters */ node.typeParameters, /* typeParameters */ ref.node.typeParameters,
/* parameters */[tcbCtxParam(node)], /* parameters */ paramList,
/* type */ undefined, /* type */ undefined,
/* body */ ts.createBlock([ts.createIf(ts.createTrue(), scope.renderToBlock())])); /* body */ body);
} }
/** /**
@ -163,7 +178,8 @@ class TcbTemplateBodyOp extends TcbOp {
if (directives !== null) { if (directives !== null) {
for (const dir of directives) { for (const dir of directives) {
const dirInstId = this.scope.resolve(this.template, dir); const dirInstId = this.scope.resolve(this.template, dir);
const dirId = this.tcb.reference(dir.ref); const dirId =
this.tcb.env.reference(dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
// There are two kinds of guards. Template guards (ngTemplateGuards) allow type narrowing of // There are two kinds of guards. Template guards (ngTemplateGuards) allow type narrowing of
// the expression passed to an @Input of the directive. Scan the directive to see if it has // the expression passed to an @Input of the directive. Scan the directive to see if it has
@ -189,7 +205,7 @@ class TcbTemplateBodyOp extends TcbOp {
// The second kind of guard is a template context guard. This guard narrows the template // The second kind of guard is a template context guard. This guard narrows the template
// rendering context variable `ctx`. // rendering context variable `ctx`.
if (dir.hasNgTemplateContextGuard && this.tcb.config.applyTemplateContextGuards) { if (dir.hasNgTemplateContextGuard && this.tcb.env.config.applyTemplateContextGuards) {
const ctx = this.scope.resolve(this.template); const ctx = this.scope.resolve(this.template);
const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]); const guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
directiveGuards.push(guardInvoke); directiveGuards.push(guardInvoke);
@ -214,7 +230,7 @@ class TcbTemplateBodyOp extends TcbOp {
// the `if` block is created by rendering the template's `Scope. // the `if` block is created by rendering the template's `Scope.
const tmplIf = ts.createIf( const tmplIf = ts.createIf(
/* expression */ guard, /* expression */ guard,
/* thenStatement */ tmplScope.renderToBlock()); /* thenStatement */ ts.createBlock(tmplScope.render()));
this.scope.addStatement(tmplIf); this.scope.addStatement(tmplIf);
return null; return null;
} }
@ -258,7 +274,7 @@ class TcbDirectiveOp extends TcbOp {
// Call the type constructor of the directive to infer a type, and assign the directive // Call the type constructor of the directive to infer a type, and assign the directive
// instance. // instance.
const typeCtor = tcbCallTypeCtor(this.node, this.dir, this.tcb, this.scope, bindings); const typeCtor = tcbCallTypeCtor(this.dir, this.tcb, bindings);
this.scope.addStatement(tsCreateVariable(id, typeCtor)); this.scope.addStatement(tsCreateVariable(id, typeCtor));
return id; return id;
} }
@ -294,7 +310,7 @@ class TcbUnclaimedInputsOp extends TcbOp {
// If checking the type of bindings is disabled, cast the resulting expression to 'any' before // If checking the type of bindings is disabled, cast the resulting expression to 'any' before
// the assignment. // the assignment.
if (!this.tcb.config.checkTypeOfBindings) { if (!this.tcb.env.config.checkTypeOfBindings) {
expr = tsCastToAny(expr); expr = tsCastToAny(expr);
} }
@ -335,14 +351,11 @@ const INFER_TYPE_FOR_CIRCULAR_OP_EXPR = ts.createNonNullExpression(ts.createNull
* block. It's responsible for variable name allocation and management of any imports needed. It * block. It's responsible for variable name allocation and management of any imports needed. It
* also contains the template metadata itself. * also contains the template metadata itself.
*/ */
class Context { export class Context {
private nextId = 1; private nextId = 1;
constructor( constructor(
readonly config: TypeCheckingConfig, readonly env: Environment, readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>) {}
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
private sourceFile: ts.SourceFile, private importManager: ImportManager,
private refEmitter: ReferenceEmitter) {}
/** /**
* Allocate a new variable name for use within the `Context`. * Allocate a new variable name for use within the `Context`.
@ -351,51 +364,6 @@ class Context {
* might change depending on the type of data being stored. * might change depending on the type of data being stored.
*/ */
allocateId(): ts.Identifier { return ts.createIdentifier(`_t${this.nextId++}`); } allocateId(): ts.Identifier { return ts.createIdentifier(`_t${this.nextId++}`); }
/**
* Generate a `ts.Expression` that references the given node.
*
* This may involve importing the node into the file if it's not declared there already.
*/
reference(ref: Reference<ts.Node>): ts.Expression {
const ngExpr = this.refEmitter.emit(ref, this.sourceFile);
// Use `translateExpression` to convert the `Expression` into a `ts.Expression`.
return translateExpression(ngExpr, this.importManager, NOOP_DEFAULT_IMPORT_RECORDER);
}
/**
* Generate a `ts.TypeNode` that references the given node as a type.
*
* This may involve importing the node into the file if it's not declared there already.
*/
referenceType(ref: Reference<ts.Node>): ts.TypeNode {
const ngExpr = this.refEmitter.emit(ref, this.sourceFile);
// Create an `ExpressionType` from the `Expression` and translate it via `translateType`.
return translateType(new ExpressionType(ngExpr), this.importManager);
}
/**
* Generate a `ts.TypeNode` that references a given type from '@angular/core'.
*
* This will involve importing the type into the file, and will also add a number of generic type
* parameters (using `any`) as requested.
*/
referenceCoreType(name: string, typeParamCount: number = 0): ts.TypeNode {
const external = new ExternalExpr({
moduleName: '@angular/core',
name,
});
let typeParams: Type[]|null = null;
if (typeParamCount > 0) {
typeParams = [];
for (let i = 0; i < typeParamCount; i++) {
typeParams.push(DYNAMIC_TYPE);
}
}
return translateType(new ExpressionType(external, null, typeParams), this.importManager);
}
} }
/** /**
@ -529,13 +497,13 @@ class Scope {
addStatement(stmt: ts.Statement): void { this.statements.push(stmt); } addStatement(stmt: ts.Statement): void { this.statements.push(stmt); }
/** /**
* Get a `ts.Block` containing the statements in this scope. * Get the statements.
*/ */
renderToBlock(): ts.Block { render(): ts.Statement[] {
for (let i = 0; i < this.opQueue.length; i++) { for (let i = 0; i < this.opQueue.length; i++) {
this.executeOp(i); this.executeOp(i);
} }
return ts.createBlock(this.statements); return this.statements;
} }
private resolveLocal( private resolveLocal(
@ -614,7 +582,7 @@ class Scope {
} else if (node instanceof TmplAstTemplate) { } else if (node instanceof TmplAstTemplate) {
// Template children are rendered in a child scope. // Template children are rendered in a child scope.
this.appendDirectivesAndInputsOfNode(node); this.appendDirectivesAndInputsOfNode(node);
if (this.tcb.config.checkTemplateBodies) { if (this.tcb.env.config.checkTemplateBodies) {
const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1; const ctxIndex = this.opQueue.push(new TcbTemplateContextOp(this.tcb, this)) - 1;
this.templateCtxOpMap.set(node, ctxIndex); this.templateCtxOpMap.set(node, ctxIndex);
this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node)); this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node));
@ -666,14 +634,15 @@ class Scope {
* This is a parameter with a type equivalent to the component type, with all generic type * This is a parameter with a type equivalent to the component type, with all generic type
* parameters listed (without their generic bounds). * parameters listed (without their generic bounds).
*/ */
function tcbCtxParam(node: ts.ClassDeclaration): ts.ParameterDeclaration { function tcbCtxParam(
node: ClassDeclaration<ts.ClassDeclaration>, name: ts.EntityName): ts.ParameterDeclaration {
let typeArguments: ts.TypeNode[]|undefined = undefined; let typeArguments: ts.TypeNode[]|undefined = undefined;
// Check if the component is generic, and pass generic type parameters if so. // Check if the component is generic, and pass generic type parameters if so.
if (node.typeParameters !== undefined) { if (node.typeParameters !== undefined) {
typeArguments = typeArguments =
node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)); node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined));
} }
const type = ts.createTypeReferenceNode(node.name !, typeArguments); const type = ts.createTypeReferenceNode(name, typeArguments);
return ts.createParameter( return ts.createParameter(
/* decorators */ undefined, /* decorators */ undefined,
/* modifiers */ undefined, /* modifiers */ undefined,
@ -692,7 +661,7 @@ function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
// `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed which // `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed which
// interprets specific expression nodes that interact with the `ImplicitReceiver`. These nodes // interprets specific expression nodes that interact with the `ImplicitReceiver`. These nodes
// actually refer to identifiers within the current scope. // actually refer to identifiers within the current scope.
return astToTypescript(ast, (ast) => tcbResolve(ast, tcb, scope), tcb.config); return astToTypescript(ast, (ast) => tcbResolve(ast, tcb, scope), tcb.env.config);
} }
/** /**
@ -700,14 +669,13 @@ function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression {
* the directive instance from any bound inputs. * the directive instance from any bound inputs.
*/ */
function tcbCallTypeCtor( function tcbCallTypeCtor(
el: TmplAstElement | TmplAstTemplate, dir: TypeCheckableDirectiveMeta, tcb: Context, dir: TypeCheckableDirectiveMeta, tcb: Context, bindings: TcbBinding[]): ts.Expression {
scope: Scope, bindings: TcbBinding[]): ts.Expression { const typeCtor = tcb.env.typeCtorFor(dir);
const dirClass = tcb.reference(dir.ref);
// Construct an array of `ts.PropertyAssignment`s for each input of the directive that has a // Construct an array of `ts.PropertyAssignment`s for each input of the directive that has a
// matching binding. // matching binding.
const members = bindings.map(({field, expression}) => { const members = bindings.map(({field, expression}) => {
if (!tcb.config.checkTypeOfBindings) { if (!tcb.env.config.checkTypeOfBindings) {
expression = tsCastToAny(expression); expression = tsCastToAny(expression);
} }
return ts.createPropertyAssignment(field, expression); return ts.createPropertyAssignment(field, expression);
@ -715,10 +683,10 @@ function tcbCallTypeCtor(
// Call the `ngTypeCtor` method on the directive class, with an object literal argument created // Call the `ngTypeCtor` method on the directive class, with an object literal argument created
// from the matched inputs. // from the matched inputs.
return tsCallMethod( return ts.createCall(
/* receiver */ dirClass, /* expression */ typeCtor,
/* methodName */ 'ngTypeCtor', /* typeArguments */ undefined,
/* args */[ts.createObjectLiteral(members)]); /* argumentsArray */[ts.createObjectLiteral(members)]);
} }
interface TcbBinding { interface TcbBinding {
@ -765,66 +733,6 @@ function tcbGetInputBindingExpressions(
} }
} }
/**
* Create an expression which instantiates an element by its HTML tagName.
*
* Thanks to narrowing of `document.createElement()`, this expression will have its type inferred
* based on the tag name, including for custom elements that have appropriate .d.ts definitions.
*/
function tsCreateElement(tagName: string): ts.Expression {
const createElement = ts.createPropertyAccess(
/* expression */ ts.createIdentifier('document'), 'createElement');
return ts.createCall(
/* expression */ createElement,
/* typeArguments */ undefined,
/* argumentsArray */[ts.createLiteral(tagName)]);
}
/**
* Create a `ts.VariableStatement` which declares a variable without explicit initialization.
*
* The initializer `null!` is used to bypass strict variable initialization checks.
*
* Unlike with `tsCreateVariable`, the type of the variable is explicitly specified.
*/
function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.VariableStatement {
const decl = ts.createVariableDeclaration(
/* name */ id,
/* type */ type,
/* initializer */ ts.createNonNullExpression(ts.createNull()));
return ts.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */[decl]);
}
/**
* Create a `ts.VariableStatement` that initializes a variable with a given expression.
*
* Unlike with `tsDeclareVariable`, the type of the variable is inferred from the initializer
* expression.
*/
function tsCreateVariable(id: ts.Identifier, initializer: ts.Expression): ts.VariableStatement {
const decl = ts.createVariableDeclaration(
/* name */ id,
/* type */ undefined,
/* initializer */ initializer);
return ts.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */[decl]);
}
/**
* Construct a `ts.CallExpression` that calls a method on a receiver.
*/
function tsCallMethod(
receiver: ts.Expression, methodName: string, args: ts.Expression[] = []): ts.CallExpression {
const methodAccess = ts.createPropertyAccess(receiver, methodName);
return ts.createCall(
/* expression */ methodAccess,
/* typeArguments */ undefined,
/* argumentsArray */ args);
}
/** /**
* Resolve an `AST` expression within the given scope. * Resolve an `AST` expression within the given scope.
* *
@ -858,7 +766,7 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
// `(null as any as TemplateRef<any>)` is constructed. // `(null as any as TemplateRef<any>)` is constructed.
let value: ts.Expression = ts.createNull(); let value: ts.Expression = ts.createNull();
value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); value = ts.createAsExpression(value, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
value = ts.createAsExpression(value, tcb.referenceCoreType('TemplateRef', 1)); value = ts.createAsExpression(value, tcb.env.referenceCoreType('TemplateRef', 1));
value = ts.createParen(value); value = ts.createParen(value);
return value; return value;
} else { } else {
@ -891,7 +799,17 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
} }
} }
function tsCastToAny(expr: ts.Expression): ts.Expression { export function requiresInlineTypeCheckBlock(node: ClassDeclaration<ts.ClassDeclaration>): boolean {
return ts.createParen( // In order to qualify for a declared TCB (not inline) two conditions must be met:
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); // 1) the class must be exported
// 2) it must not have constrained generic types
if (!checkIfClassIsExported(node)) {
// Condition 1 is false, the class is not exported.
return true;
} else if (!checkIfGenericTypesAreUnbound(node)) {
// Condition 2 is false, the class has constrained generic types
return true;
} else {
return false;
}
} }

View File

@ -0,0 +1,72 @@
/**
* @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
*/
/// <reference types="node" />
import * as path from 'path';
import * as ts from 'typescript';
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
import {AbsoluteFsPath} from '../../path';
import {ClassDeclaration} from '../../reflection';
import {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckingConfig} from './api';
import {Environment} from './environment';
import {generateTypeCheckBlock} from './type_check_block';
/**
* An `Environment` representing the single type-checking file into which most (if not all) Type
* Check Blocks (TCBs) will be generated.
*
* The `TypeCheckFile` hosts multiple TCBs and allows the sharing of declarations (e.g. type
* constructors) between them. Rather than return such declarations via `getPreludeStatements()`, it
* hoists them to the top of the generated `ts.SourceFile`.
*/
export class TypeCheckFile extends Environment {
private nextTcbId = 1;
private tcbStatements: ts.Statement[] = [];
constructor(private fileName: string, config: TypeCheckingConfig, refEmitter: ReferenceEmitter) {
super(
config, new ImportManager(new NoopImportRewriter(), 'i'), refEmitter,
ts.createSourceFile(fileName, '', ts.ScriptTarget.Latest, true));
}
addTypeCheckBlock(
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>, meta: TypeCheckBlockMetadata): void {
const fnId = ts.createIdentifier(`_tcb${this.nextTcbId++}`);
const fn = generateTypeCheckBlock(this, ref, fnId, meta);
this.tcbStatements.push(fn);
}
render(): ts.SourceFile {
let source: string = this.importManager.getAllImports(this.fileName)
.map(i => `import * as ${i.qualifier} from '${i.specifier}';`)
.join('\n') +
'\n\n';
const printer = ts.createPrinter();
source += '\n';
for (const stmt of this.typeCtorStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
source += '\n';
for (const stmt of this.tcbStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
return ts.createSourceFile(
this.fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
}
getPreludeStatements(): ts.Statement[] { return []; }
}
export function typeCheckFilePath(rootDirs: AbsoluteFsPath[]): AbsoluteFsPath {
const shortest = rootDirs.concat([]).sort((a, b) => a.length - b.length)[0];
return AbsoluteFsPath.fromUnchecked(path.posix.join(shortest, '__ng_typecheck__.ts'));
}

View File

@ -10,41 +10,113 @@ import * as ts from 'typescript';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration} from '../../reflection';
import {TypeCtorMetadata} from './api'; import {TypeCtorMetadata} from './api';
import {checkIfGenericTypesAreUnbound} from './ts_util';
export function generateTypeCtorDeclarationFn(
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCtorMetadata,
nodeTypeRef: ts.Identifier | ts.QualifiedName): ts.Statement {
if (requiresInlineTypeCtor(node)) {
throw new Error(`${node.name.text} requires an inline type constructor`);
}
const rawTypeArgs =
node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined;
const rawType: ts.TypeNode = ts.createTypeReferenceNode(nodeTypeRef, rawTypeArgs);
const initParam = constructTypeCtorParameter(node, meta, rawType);
if (meta.body) {
const fnType = ts.createFunctionTypeNode(
/* typeParameters */ node.typeParameters,
/* parameters */[initParam],
/* type */ rawType, );
const decl = ts.createVariableDeclaration(
/* name */ meta.fnName,
/* type */ fnType,
/* body */ ts.createNonNullExpression(ts.createNull()));
const declList = ts.createVariableDeclarationList([decl], ts.NodeFlags.Const);
return ts.createVariableStatement(
/* modifiers */ undefined,
/* declarationList */ declList);
} else {
return ts.createFunctionDeclaration(
/* decorators */ undefined,
/* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)],
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* typeParameters */ node.typeParameters,
/* parameters */[initParam],
/* type */ rawType,
/* body */ undefined);
}
}
/** /**
* Generate a type constructor for the given class and metadata. * Generate an inline type constructor for the given class and metadata.
* *
* A type constructor is a specially shaped TypeScript static method, intended to be placed within * An inline type constructor is a specially shaped TypeScript static method, intended to be placed
* a directive class itself, that permits type inference of any generic type parameters of the class * within a directive class itself, that permits type inference of any generic type parameters of
* from the types of expressions bound to inputs or outputs, and the types of elements that match * the class from the types of expressions bound to inputs or outputs, and the types of elements
* queries performed by the directive. It also catches any errors in the types of these expressions. * that match queries performed by the directive. It also catches any errors in the types of these
* This method is never called at runtime, but is used in type-check blocks to construct directive * expressions. This method is never called at runtime, but is used in type-check blocks to
* types. * construct directive types.
* *
* A type constructor for NgFor looks like: * An inline type constructor for NgFor looks like:
* *
* static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>, 'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): * static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>, 'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>):
* NgForOf<T>; * NgForOf<T>;
* *
* A typical usage would be: * A typical constructor would be:
* *
* NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']}); // Infers a type of NgForOf<string>. * NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']}); // Infers a type of NgForOf<string>.
* *
* Inline type constructors are used when the type being created has bounded generic types which
* make writing a declared type constructor (via `generateTypeCtorDeclarationFn`) difficult or
* impossible.
*
* @param node the `ClassDeclaration<ts.ClassDeclaration>` for which a type constructor will be * @param node the `ClassDeclaration<ts.ClassDeclaration>` for which a type constructor will be
* generated. * generated.
* @param meta additional metadata required to generate the type constructor. * @param meta additional metadata required to generate the type constructor.
* @returns a `ts.MethodDeclaration` for the type constructor. * @returns a `ts.MethodDeclaration` for the type constructor.
*/ */
export function generateTypeCtor( export function generateInlineTypeCtor(
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCtorMetadata): ts.MethodDeclaration { node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCtorMetadata): ts.MethodDeclaration {
// Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from // Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from
// the definition without any type bounds. For example, if the class is // the definition without any type bounds. For example, if the class is
// `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`. // `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`.
const rawTypeArgs = node.typeParameters !== undefined ? const rawTypeArgs =
node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)) : node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined;
undefined;
const rawType: ts.TypeNode = ts.createTypeReferenceNode(node.name, rawTypeArgs); const rawType: ts.TypeNode = ts.createTypeReferenceNode(node.name, rawTypeArgs);
const initParam = constructTypeCtorParameter(node, meta, rawType);
// If this constructor is being generated into a .ts file, then it needs a fake body. The body
// is set to a return of `null!`. If the type constructor is being generated into a .d.ts file,
// it needs no body.
let body: ts.Block|undefined = undefined;
if (meta.body) {
body = ts.createBlock([
ts.createReturn(ts.createNonNullExpression(ts.createNull())),
]);
}
// Create the type constructor method declaration.
return ts.createMethod(
/* decorators */ undefined,
/* modifiers */[ts.createModifier(ts.SyntaxKind.StaticKeyword)],
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* questionToken */ undefined,
/* typeParameters */ node.typeParameters,
/* parameters */[initParam],
/* type */ rawType,
/* body */ body, );
}
function constructTypeCtorParameter(
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCtorMetadata,
rawType: ts.TypeNode): ts.ParameterDeclaration {
// initType is the type of 'init', the single argument to the type constructor method. // initType is the type of 'init', the single argument to the type constructor method.
// If the Directive has any inputs, outputs, or queries, its initType will be: // If the Directive has any inputs, outputs, or queries, its initType will be:
// //
@ -77,35 +149,22 @@ export function generateTypeCtor(
initType = ts.createTypeReferenceNode('Partial', [pickType]); initType = ts.createTypeReferenceNode('Partial', [pickType]);
} }
// If this constructor is being generated into a .ts file, then it needs a fake body. The body
// is set to a return of `null!`. If the type constructor is being generated into a .d.ts file,
// it needs no body.
let body: ts.Block|undefined = undefined;
if (meta.body) {
body = ts.createBlock([
ts.createReturn(ts.createNonNullExpression(ts.createNull())),
]);
}
// Create the 'init' parameter itself. // Create the 'init' parameter itself.
const initParam = ts.createParameter( return ts.createParameter(
/* decorators */ undefined, /* decorators */ undefined,
/* modifiers */ undefined, /* modifiers */ undefined,
/* dotDotDotToken */ undefined, /* dotDotDotToken */ undefined,
/* name */ 'init', /* name */ 'init',
/* questionToken */ undefined, /* questionToken */ undefined,
/* type */ initType, /* type */ initType,
/* initializer */ undefined, ); /* initializer */ undefined);
}
// Create the type constructor method declaration.
return ts.createMethod( function generateGenericArgs(params: ReadonlyArray<ts.TypeParameterDeclaration>): ts.TypeNode[] {
/* decorators */ undefined, return params.map(param => ts.createTypeReferenceNode(param.name, undefined));
/* modifiers */[ts.createModifier(ts.SyntaxKind.StaticKeyword)], }
/* asteriskToken */ undefined,
/* name */ meta.fnName, export function requiresInlineTypeCtor(node: ClassDeclaration<ts.ClassDeclaration>): boolean {
/* questionToken */ undefined, // The class requires an inline type constructor if it has constrained (bound) generics.
/* typeParameters */ node.typeParameters, return !checkIfGenericTypesAreUnbound(node);
/* parameters */[initParam],
/* type */ rawType,
/* body */ body, );
} }

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CssSelector, Expression, ExternalExpr, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler'; import {CssSelector, R3TargetBinder, SelectorMatcher, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ImportMode, Reference, ReferenceEmitStrategy, ReferenceEmitter} from '../../imports'; import {Reference} from '../../imports';
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection'; import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
import {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../src/api'; import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../src/api';
import {Environment} from '../src/environment';
import {generateTypeCheckBlock} from '../src/type_check_block'; import {generateTypeCheckBlock} from '../src/type_check_block';
@ -60,7 +60,7 @@ describe('type check blocks', () => {
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .toContain(
'var _t1 = i0.Dir.ngTypeCtor({}); _t1.value; var _t2 = document.createElement("div");'); 'var _t1 = Dir.ngTypeCtor({}); _t1.value; var _t2 = document.createElement("div");');
}); });
it('should handle style and class bindings specially', () => { it('should handle style and class bindings specially', () => {
@ -92,7 +92,7 @@ describe('type check blocks', () => {
describe('config.applyTemplateContextGuards', () => { describe('config.applyTemplateContextGuards', () => {
const TEMPLATE = `<div *dir></div>`; const TEMPLATE = `<div *dir></div>`;
const GUARD_APPLIED = 'if (i0.Dir.ngTemplateContextGuard('; const GUARD_APPLIED = 'if (Dir.ngTemplateContextGuard(';
it('should apply template context guards when enabled', () => { it('should apply template context guards when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
@ -124,13 +124,13 @@ describe('type check blocks', () => {
it('should check types of bindings when enabled', () => { it('should check types of bindings when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('i0.Dir.ngTypeCtor({ dirInput: ctx.a })'); expect(block).toContain('Dir.ngTypeCtor({ dirInput: ctx.a })');
expect(block).toContain('.nonDirInput = ctx.a;'); expect(block).toContain('.nonDirInput = ctx.a;');
}); });
it('should not check types of bindings when disabled', () => { it('should not check types of bindings when disabled', () => {
const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfBindings: false}; const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('i0.Dir.ngTypeCtor({ dirInput: (ctx.a as any) })'); expect(block).toContain('Dir.ngTypeCtor({ dirInput: (ctx.a as any) })');
expect(block).toContain('.nonDirInput = (ctx.a as any);'); expect(block).toContain('.nonDirInput = (ctx.a as any);');
}); });
}); });
@ -163,7 +163,7 @@ it('should generate a circular directive reference correctly', () => {
exportAs: ['dir'], exportAs: ['dir'],
inputs: {input: 'input'}, inputs: {input: 'input'},
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t2 = i0.Dir.ngTypeCtor({ input: (null!) });'); expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t2 = Dir.ngTypeCtor({ input: (null!) });');
}); });
it('should generate circular references between two directives correctly', () => { it('should generate circular references between two directives correctly', () => {
@ -187,8 +187,8 @@ it('should generate circular references between two directives correctly', () =>
]; ];
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .toContain(
'var _t3 = i0.DirB.ngTypeCtor({ inputA: (null!) });' + 'var _t3 = DirB.ngTypeCtor({ inputA: (null!) }); ' +
' var _t2 = i1.DirA.ngTypeCtor({ inputA: _t3 });'); 'var _t2 = DirA.ngTypeCtor({ inputA: _t3 });');
}); });
function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDeclaration> { function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDeclaration> {
@ -208,7 +208,7 @@ type TestDirective =
function tcb( function tcb(
template: string, directives: TestDirective[] = [], config?: TypeCheckingConfig): string { template: string, directives: TestDirective[] = [], config?: TypeCheckingConfig): string {
const classes = ['Test', ...directives.map(dir => dir.name)]; const classes = ['Test', ...directives.map(dir => dir.name)];
const code = classes.map(name => `class ${name} {}`).join('\n'); const code = classes.map(name => `class ${name}<T extends string> {}`).join('\n');
const sf = ts.createSourceFile('synthetic.ts', code, ts.ScriptTarget.Latest, true); const sf = ts.createSourceFile('synthetic.ts', code, ts.ScriptTarget.Latest, true);
const clazz = getClass(sf, 'Test'); const clazz = getClass(sf, 'Test');
@ -234,10 +234,7 @@ function tcb(
const binder = new R3TargetBinder(matcher); const binder = new R3TargetBinder(matcher);
const boundTarget = binder.bind({template: nodes}); const boundTarget = binder.bind({template: nodes});
const meta: TypeCheckBlockMetadata = { const meta: TypeCheckBlockMetadata = {boundTarget};
boundTarget,
fnName: 'Test_TCB',
};
config = config || { config = config || {
applyTemplateContextGuards: true, applyTemplateContextGuards: true,
@ -246,19 +243,40 @@ function tcb(
strictSafeNavigationTypes: true, strictSafeNavigationTypes: true,
}; };
const im = new ImportManager(undefined, 'i');
const tcb = generateTypeCheckBlock( const tcb = generateTypeCheckBlock(
clazz, meta, config, im, new ReferenceEmitter([new FakeReferenceStrategy()])); FakeEnvironment.newFake(config), new Reference(clazz), ts.createIdentifier('Test_TCB'), meta);
const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tcb, sf); const res = ts.createPrinter().printNode(ts.EmitHint.Unspecified, tcb, sf);
return res.replace(/\s+/g, ' '); return res.replace(/\s+/g, ' ');
} }
class FakeReferenceStrategy implements ReferenceEmitStrategy { class FakeEnvironment /* implements Environment */ {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode?: ImportMode): Expression { constructor(readonly config: TypeCheckingConfig) {}
return new ExternalExpr({
moduleName: `types/${ref.debugName}`, typeCtorFor(dir: TypeCheckableDirectiveMeta): ts.Expression {
name: ref.debugName, return ts.createPropertyAccess(ts.createIdentifier(dir.name), 'ngTypeCtor');
}); }
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
return ref.node.name;
}
referenceType(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.TypeNode {
return ts.createTypeReferenceNode(ref.node.name, /* typeArguments */ undefined);
}
referenceCoreType(name: string, typeParamCount: number = 0): ts.TypeNode {
const typeArgs: ts.TypeNode[] = [];
for (let i = 0; i < typeParamCount; i++) {
typeArgs.push(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
}
const qName = ts.createQualifiedName(ts.createIdentifier('ng'), name);
return ts.createTypeReferenceNode(qName, typeParamCount > 0 ? typeArgs : undefined);
}
getPreludeStatements(): ts.Statement[] { return []; }
static newFake(config: TypeCheckingConfig): Environment {
return new FakeEnvironment(config) as Environment;
} }
} }

View File

@ -8,8 +8,8 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitter} from '../../imports'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports';
import {LogicalFileSystem} from '../../path'; import {AbsoluteFsPath, LogicalFileSystem} from '../../path';
import {isNamedClassDeclaration} from '../../reflection'; import {isNamedClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getRootDirs} from '../../util/src/typescript'; import {getRootDirs} from '../../util/src/typescript';
@ -55,9 +55,10 @@ TestClass.ngTypeCtor({value: 'test'});
new AbsoluteModuleStrategy(program, checker, options, host), new AbsoluteModuleStrategy(program, checker, options, host),
new LogicalProjectStrategy(checker, logicalFs), new LogicalProjectStrategy(checker, logicalFs),
]); ]);
const ctx = new TypeCheckContext(ALL_ENABLED_CONFIG, emitter); const ctx = new TypeCheckContext(
ALL_ENABLED_CONFIG, emitter, AbsoluteFsPath.fromUnchecked('/_typecheck_.ts'));
const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration); const TestClass = getDeclaration(program, 'main.ts', 'TestClass', isNamedClassDeclaration);
ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, { ctx.addInlineTypeCtor(program.getSourceFile('main.ts') !, new Reference(TestClass), {
fnName: 'ngTypeCtor', fnName: 'ngTypeCtor',
body: true, body: true,
fields: { fields: {
@ -66,8 +67,7 @@ TestClass.ngTypeCtor({value: 'test'});
queries: [], queries: [],
}, },
}); });
const augHost = new TypeCheckProgramHost(program, host, ctx); ctx.calculateTemplateDiagnostics(program, host, options);
makeProgram(files, undefined, augHost, true);
}); });
}); });
}); });

View File

@ -124,7 +124,7 @@ describe('ngtsc type checking', () => {
selector: 'test', selector: 'test',
template: '<div *ngFor="let user of users">{{user.does_not_exist}}</div>', template: '<div *ngFor="let user of users">{{user.does_not_exist}}</div>',
}) })
class TestCmp { export class TestCmp {
users: {name: string}[]; users: {name: string}[];
} }
@ -132,7 +132,7 @@ describe('ngtsc type checking', () => {
declarations: [TestCmp], declarations: [TestCmp],
imports: [CommonModule], imports: [CommonModule],
}) })
class Module {} export class Module {}
`); `);
const diags = env.driveDiagnostics(); const diags = env.driveDiagnostics();