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);
}
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 {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
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 {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform';
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 {getRootDirs, isDtsPath} from './util/src/typescript';
@ -64,6 +64,7 @@ export class NgtscProgram implements api.Program {
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
private perfTracker: PerfTracker|null = null;
private incrementalState: IncrementalState;
private typeCheckFilePath: AbsoluteFsPath;
constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
@ -105,6 +106,10 @@ export class NgtscProgram implements api.Program {
generators.push(summaryGenerator, factoryGenerator);
}
this.typeCheckFilePath = typeCheckFilePath(this.rootDirs);
generators.push(new TypeCheckShimGenerator(this.typeCheckFilePath));
rootFiles.push(this.typeCheckFilePath);
let entryPoint: string|null = null;
if (options.flatModuleOutFile !== undefined) {
entryPoint = findFlatIndexEntryPoint(normalizedRootNames);
@ -189,18 +194,7 @@ export class NgtscProgram implements api.Program {
fileName?: string|undefined, cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> {
const compilation = this.ensureAnalyzed();
const diagnostics = [...compilation.diagnostics];
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));
}
const diagnostics = [...compilation.diagnostics, ...this.getTemplateDiagnostics()];
if (this.entryPoint !== null && this.exportReferenceGraph !== null) {
diagnostics.push(...checkForPrivateExports(
this.entryPoint, this.tsProgram.getTypeChecker(), this.exportReferenceGraph));
@ -344,8 +338,11 @@ export class NgtscProgram implements api.Program {
const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = [];
const typeCheckFile = this.tsProgram.getSourceFile(this.typeCheckFilePath);
for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile) {
if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) {
continue;
}
@ -378,15 +375,47 @@ export class NgtscProgram implements api.Program {
return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults);
}
private compileTypeCheckProgram(ctx: TypeCheckContext): ReadonlyArray<ts.Diagnostic> {
const host = new TypeCheckProgramHost(this.tsProgram, this.host, ctx);
const auxProgram = ts.createProgram({
host,
rootNames: this.tsProgram.getRootFileNames(),
oldProgram: this.tsProgram,
options: this.options,
});
return auxProgram.getSemanticDiagnostics();
private getTemplateDiagnostics(): ReadonlyArray<ts.Diagnostic> {
// Skip template type-checking unless explicitly requested.
if (this.options.fullTemplateTypeCheck !== true) {
return [];
}
const compilation = this.ensureAnalyzed();
// 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 {

View File

@ -11,3 +11,4 @@
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host';
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-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/node",
"@npm//typescript",
],
)

View File

@ -9,3 +9,4 @@
export * from './src/api';
export {TypeCheckContext} from './src/context';
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.
*/
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>;
/**
* The name of the requested type check block function.
*/
fnName: string;
}
export interface TypeCtorMetadata {

View File

@ -9,13 +9,17 @@
import {BoundTarget} from '@angular/compiler';
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 {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api';
import {generateTypeCheckBlock} from './type_check_block';
import {generateTypeCtor} from './type_constructor';
import {Environment} from './environment';
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.
*/
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
@ -35,6 +45,12 @@ export class TypeCheckContext {
*/
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
* matching.
@ -44,39 +60,50 @@ export class TypeCheckContext {
* @param matcher `SelectorMatcher` which tracks directives that are in scope for this template.
*/
addTemplate(
node: ClassDeclaration<ts.ClassDeclaration>,
ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>): void {
// Get all of the directives used in the template and record type constructors for all of them.
boundTarget.getUsedDirectives().forEach(dir => {
const dirNode = dir.ref.node as ClassDeclaration<ts.ClassDeclaration>;
// Add a type constructor operation for the directive.
this.addTypeCtor(dirNode.getSourceFile(), dirNode, {
fnName: 'ngTypeCtor',
// The constructor should have a body if the directive comes from a .ts file, but not if it
// comes from a .d.ts file. .d.ts declarations don't have bodies.
body: !dirNode.getSourceFile().fileName.endsWith('.d.ts'),
fields: {
inputs: Object.keys(dir.inputs),
outputs: Object.keys(dir.outputs),
// TODO: support queries
queries: dir.queries,
},
});
});
for (const dir of boundTarget.getUsedDirectives()) {
const dirRef = dir.ref as Reference<ClassDeclaration<ts.ClassDeclaration>>;
const dirNode = dirRef.node;
if (requiresInlineTypeCtor(dirNode)) {
// Add a type constructor operation for the directive.
this.addInlineTypeCtor(dirNode.getSourceFile(), dirRef, {
fnName: 'ngTypeCtor',
// The constructor should have a body if the directive comes from a .ts file, but not if
// it comes from a .d.ts file. .d.ts declarations don't have bodies.
body: !dirNode.getSourceFile().isDeclarationFile,
fields: {
inputs: Object.keys(dir.inputs),
outputs: Object.keys(dir.outputs),
// TODO(alxhub): support queries
queries: dir.queries,
},
});
}
}
// Record the type check block operation for the template itself.
this.addTypeCheckBlock(node.getSourceFile(), node, {
boundTarget,
fnName: `${node.name.text}_TypeCheckBlock`,
});
if (requiresInlineTypeCheckBlock(ref.node)) {
// This class didn't meet the requirements for external type checking, so generate an inline
// TCB for the class.
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`.
*/
addTypeCtor(
sf: ts.SourceFile, node: ClassDeclaration<ts.ClassDeclaration>,
addInlineTypeCtor(
sf: ts.SourceFile, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
ctorMeta: TypeCtorMetadata): void {
if (this.typeCtorPending.has(ref.node)) {
return;
}
this.typeCtorPending.add(ref.node);
// Lazily construct the operation map.
if (!this.opMap.has(sf)) {
this.opMap.set(sf, []);
@ -84,7 +111,7 @@ export class TypeCheckContext {
const ops = this.opMap.get(sf) !;
// 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);
}
private addTypeCheckBlock(
sf: ts.SourceFile, node: ClassDeclaration<ts.ClassDeclaration>,
calculateTemplateDiagnostics(
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 {
const sf = ref.node.getSourceFile();
if (!this.opMap.has(sf)) {
this.opMap.set(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.
*/
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.
@ -171,18 +241,20 @@ interface Op {
*/
class TcbOp implements Op {
constructor(
readonly node: ClassDeclaration<ts.ClassDeclaration>, readonly meta: TypeCheckBlockMetadata,
readonly config: TypeCheckingConfig) {}
readonly ref: Reference<ClassDeclaration<ts.ClassDeclaration>>,
readonly meta: TypeCheckBlockMetadata, readonly config: TypeCheckingConfig) {}
/**
* 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):
string {
const tcb = generateTypeCheckBlock(this.node, this.meta, this.config, im, refEmitter);
return printer.printNode(ts.EmitHint.Unspecified, tcb, sf);
const env = new Environment(this.config, im, refEmitter, 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 {
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.
*/
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):
string {
const tcb = generateTypeCtor(this.node, this.meta);
const tcb = generateInlineTypeCtor(this.ref.node, this.meta);
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 {
/**
* 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>;
/**
* Tracks those files in `sfCache` which have been augmented with type checking information
* 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); });
constructor(sfMap: Map<string, ts.SourceFile>, private delegate: ts.CompilerHost) {
this.sfMap = sfMap;
if (delegate.getDirectories !== undefined) {
this.getDirectories = (path: string) => delegate.getDirectories !(path);
@ -45,25 +32,15 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
// 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) {
// There should be no cache misses, but just in case, delegate getSourceFile in the event of
// a cache miss.
sf = this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile);
sf && this.sfCache.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;
sf && this.sfMap.set(fileName, sf);
}
return sf;
}
// 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,
onError: ((message: string) => void)|undefined,
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(); }
@ -91,7 +68,9 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
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); }
}

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
*/
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 {NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports';
import {Reference} from '../../imports';
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 {checkIfClassIsExported, checkIfGenericTypesAreUnbound, tsCallMethod, tsCastToAny, tsCreateElement, tsCreateVariable, tsDeclareVariable} from './ts_util';
/**
* 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.
*/
export function generateTypeCheckBlock(
node: ClassDeclaration<ts.ClassDeclaration>, meta: TypeCheckBlockMetadata,
config: TypeCheckingConfig, importManager: ImportManager,
refEmitter: ReferenceEmitter): ts.FunctionDeclaration {
const tcb =
new Context(config, meta.boundTarget, node.getSourceFile(), importManager, refEmitter);
env: Environment, ref: Reference<ClassDeclaration<ts.ClassDeclaration>>, name: ts.Identifier,
meta: TypeCheckBlockMetadata): ts.FunctionDeclaration {
const tcb = new Context(env, meta.boundTarget);
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(
/* decorators */ undefined,
/* modifiers */ undefined,
/* asteriskToken */ undefined,
/* name */ meta.fnName,
/* typeParameters */ node.typeParameters,
/* parameters */[tcbCtxParam(node)],
/* name */ name,
/* typeParameters */ ref.node.typeParameters,
/* parameters */ paramList,
/* type */ undefined,
/* body */ ts.createBlock([ts.createIf(ts.createTrue(), scope.renderToBlock())]));
/* body */ body);
}
/**
@ -163,7 +178,8 @@ class TcbTemplateBodyOp extends TcbOp {
if (directives !== null) {
for (const dir of directives) {
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
// 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
// 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 guardInvoke = tsCallMethod(dirId, 'ngTemplateContextGuard', [dirInstId, ctx]);
directiveGuards.push(guardInvoke);
@ -214,7 +230,7 @@ class TcbTemplateBodyOp extends TcbOp {
// the `if` block is created by rendering the template's `Scope.
const tmplIf = ts.createIf(
/* expression */ guard,
/* thenStatement */ tmplScope.renderToBlock());
/* thenStatement */ ts.createBlock(tmplScope.render()));
this.scope.addStatement(tmplIf);
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
// 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));
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
// the assignment.
if (!this.tcb.config.checkTypeOfBindings) {
if (!this.tcb.env.config.checkTypeOfBindings) {
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
* also contains the template metadata itself.
*/
class Context {
export class Context {
private nextId = 1;
constructor(
readonly config: TypeCheckingConfig,
readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
private sourceFile: ts.SourceFile, private importManager: ImportManager,
private refEmitter: ReferenceEmitter) {}
readonly env: Environment, readonly boundTarget: BoundTarget<TypeCheckableDirectiveMeta>) {}
/**
* 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.
*/
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); }
/**
* 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++) {
this.executeOp(i);
}
return ts.createBlock(this.statements);
return this.statements;
}
private resolveLocal(
@ -614,7 +582,7 @@ class Scope {
} else if (node instanceof TmplAstTemplate) {
// Template children are rendered in a child scope.
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;
this.templateCtxOpMap.set(node, ctxIndex);
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
* 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;
// Check if the component is generic, and pass generic type parameters if so.
if (node.typeParameters !== undefined) {
typeArguments =
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(
/* decorators */ 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
// interprets specific expression nodes that interact with the `ImplicitReceiver`. These nodes
// 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.
*/
function tcbCallTypeCtor(
el: TmplAstElement | TmplAstTemplate, dir: TypeCheckableDirectiveMeta, tcb: Context,
scope: Scope, bindings: TcbBinding[]): ts.Expression {
const dirClass = tcb.reference(dir.ref);
dir: TypeCheckableDirectiveMeta, tcb: Context, bindings: TcbBinding[]): ts.Expression {
const typeCtor = tcb.env.typeCtorFor(dir);
// Construct an array of `ts.PropertyAssignment`s for each input of the directive that has a
// matching binding.
const members = bindings.map(({field, expression}) => {
if (!tcb.config.checkTypeOfBindings) {
if (!tcb.env.config.checkTypeOfBindings) {
expression = tsCastToAny(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
// from the matched inputs.
return tsCallMethod(
/* receiver */ dirClass,
/* methodName */ 'ngTypeCtor',
/* args */[ts.createObjectLiteral(members)]);
return ts.createCall(
/* expression */ typeCtor,
/* typeArguments */ undefined,
/* argumentsArray */[ts.createObjectLiteral(members)]);
}
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.
*
@ -858,7 +766,7 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
// `(null as any as TemplateRef<any>)` is constructed.
let value: ts.Expression = ts.createNull();
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);
return value;
} else {
@ -891,7 +799,17 @@ function tcbResolve(ast: AST, tcb: Context, scope: Scope): ts.Expression|null {
}
}
function tsCastToAny(expr: ts.Expression): ts.Expression {
return ts.createParen(
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
export function requiresInlineTypeCheckBlock(node: ClassDeclaration<ts.ClassDeclaration>): boolean {
// In order to qualify for a declared TCB (not inline) two conditions must be met:
// 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 {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
* a directive class itself, that permits type inference of any generic type parameters of the class
* from the types of expressions bound to inputs or outputs, and the types of elements that match
* queries performed by the directive. It also catches any errors in the types of these expressions.
* This method is never called at runtime, but is used in type-check blocks to construct directive
* types.
* An inline type constructor is a specially shaped TypeScript static method, intended to be placed
* within a directive class itself, that permits type inference of any generic type parameters of
* the class from the types of expressions bound to inputs or outputs, and the types of elements
* that match queries performed by the directive. It also catches any errors in the types of these
* expressions. This method is never called at runtime, but is used in type-check blocks to
* 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'>>):
* NgForOf<T>;
*
* A typical usage would be:
* A typical constructor would be:
*
* 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
* generated.
* @param meta additional metadata required to generate 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 {
// 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
// `FooDirective<T extends Bar>`, its rawType would be `FooDirective<T>`.
const rawTypeArgs = node.typeParameters !== undefined ?
node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)) :
undefined;
const rawTypeArgs =
node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined;
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.
// 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]);
}
// 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.
const initParam = ts.createParameter(
return ts.createParameter(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'init',
/* questionToken */ undefined,
/* type */ initType,
/* initializer */ undefined, );
// 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, );
/* initializer */ undefined);
}
function generateGenericArgs(params: ReadonlyArray<ts.TypeParameterDeclaration>): ts.TypeNode[] {
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);
}

View File

@ -6,13 +6,13 @@
* 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 {ImportMode, Reference, ReferenceEmitStrategy, ReferenceEmitter} from '../../imports';
import {Reference} from '../../imports';
import {ClassDeclaration, isNamedClassDeclaration} from '../../reflection';
import {ImportManager} from '../../translator';
import {TypeCheckBlockMetadata, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../src/api';
import {Environment} from '../src/environment';
import {generateTypeCheckBlock} from '../src/type_check_block';
@ -60,7 +60,7 @@ describe('type check blocks', () => {
}];
expect(tcb(TEMPLATE, DIRECTIVES))
.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', () => {
@ -92,7 +92,7 @@ describe('type check blocks', () => {
describe('config.applyTemplateContextGuards', () => {
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', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
@ -124,13 +124,13 @@ describe('type check blocks', () => {
it('should check types of bindings when enabled', () => {
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;');
});
it('should not check types of bindings when disabled', () => {
const DISABLED_CONFIG = {...BASE_CONFIG, checkTypeOfBindings: false};
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);');
});
});
@ -163,7 +163,7 @@ it('should generate a circular directive reference correctly', () => {
exportAs: ['dir'],
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', () => {
@ -187,8 +187,8 @@ it('should generate circular references between two directives correctly', () =>
];
expect(tcb(TEMPLATE, DIRECTIVES))
.toContain(
'var _t3 = i0.DirB.ngTypeCtor({ inputA: (null!) });' +
' var _t2 = i1.DirA.ngTypeCtor({ inputA: _t3 });');
'var _t3 = DirB.ngTypeCtor({ inputA: (null!) }); ' +
'var _t2 = DirA.ngTypeCtor({ inputA: _t3 });');
});
function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.ClassDeclaration> {
@ -208,7 +208,7 @@ type TestDirective =
function tcb(
template: string, directives: TestDirective[] = [], config?: TypeCheckingConfig): string {
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 clazz = getClass(sf, 'Test');
@ -234,10 +234,7 @@ function tcb(
const binder = new R3TargetBinder(matcher);
const boundTarget = binder.bind({template: nodes});
const meta: TypeCheckBlockMetadata = {
boundTarget,
fnName: 'Test_TCB',
};
const meta: TypeCheckBlockMetadata = {boundTarget};
config = config || {
applyTemplateContextGuards: true,
@ -246,19 +243,40 @@ function tcb(
strictSafeNavigationTypes: true,
};
const im = new ImportManager(undefined, 'i');
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);
return res.replace(/\s+/g, ' ');
}
class FakeReferenceStrategy implements ReferenceEmitStrategy {
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importMode?: ImportMode): Expression {
return new ExternalExpr({
moduleName: `types/${ref.debugName}`,
name: ref.debugName,
});
class FakeEnvironment /* implements Environment */ {
constructor(readonly config: TypeCheckingConfig) {}
typeCtorFor(dir: TypeCheckableDirectiveMeta): ts.Expression {
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 {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitter} from '../../imports';
import {LogicalFileSystem} from '../../path';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports';
import {AbsoluteFsPath, LogicalFileSystem} from '../../path';
import {isNamedClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {getRootDirs} from '../../util/src/typescript';
@ -55,9 +55,10 @@ TestClass.ngTypeCtor({value: 'test'});
new AbsoluteModuleStrategy(program, checker, options, host),
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);
ctx.addTypeCtor(program.getSourceFile('main.ts') !, TestClass, {
ctx.addInlineTypeCtor(program.getSourceFile('main.ts') !, new Reference(TestClass), {
fnName: 'ngTypeCtor',
body: true,
fields: {
@ -66,8 +67,7 @@ TestClass.ngTypeCtor({value: 'test'});
queries: [],
},
});
const augHost = new TypeCheckProgramHost(program, host, ctx);
makeProgram(files, undefined, augHost, true);
ctx.calculateTemplateDiagnostics(program, host, options);
});
});
});

View File

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