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'; | ||||
|  | ||||
							
								
								
									
										33
									
								
								packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
| @ -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); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										137
									
								
								packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts
									
									
									
									
									
										Normal 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, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
| @ -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); } | ||||
| } | ||||
							
								
								
									
										119
									
								
								packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user