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:
parent
f4c536ae36
commit
98f86de8da
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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); }
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue