diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index e68a072982..323df82312 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -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); } } diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 67783807b5..7759af0a1c 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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, 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 { 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 { - 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 { + // 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 { diff --git a/packages/compiler-cli/src/ngtsc/shims/index.ts b/packages/compiler-cli/src/ngtsc/shims/index.ts index 25e731d29e..96d6e22bd1 100644 --- a/packages/compiler-cli/src/ngtsc/shims/index.ts +++ b/packages/compiler-cli/src/ngtsc/shims/index.ts @@ -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'; diff --git a/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts b/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts new file mode 100644 index 0000000000..5c0a8b0a0d --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/shims/src/typecheck_shim.ts @@ -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); + } +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel index d300ac3594..42d5ff643d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel @@ -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", ], ) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/index.ts index 444f0c279f..1b9d47c769 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/index.ts @@ -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'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts index 475f8e110e..92ef8883cb 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts @@ -32,11 +32,6 @@ export interface TypeCheckBlockMetadata { * Semantic information about the template of the component. */ boundTarget: BoundTarget; - - /** - * The name of the requested type check block function. - */ - fnName: string; } export interface TypeCtorMetadata { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 71035f4809..f7c1c2b66c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -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(); + /** + * Tracks when an a particular class has a pending type constructor patching operation already + * queued. + */ + private typeCtorPending = new Set(); + /** * 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, + ref: Reference>, boundTarget: BoundTarget): 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; - // 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>; + 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, + addInlineTypeCtor( + sf: ts.SourceFile, ref: Reference>, 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, + 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(); + 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>, 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; + readonly ref: Reference>; /** * 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, readonly meta: TypeCheckBlockMetadata, - readonly config: TypeCheckingConfig) {} + readonly ref: Reference>, + 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, readonly meta: TypeCtorMetadata) {} + readonly ref: Reference>, + 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); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts new file mode 100644 index 0000000000..cdd8c3c306 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -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(); + 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>; + 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>): 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>): 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, + ]; + } +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/host.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/host.ts index dad3c0156e..70e339b7ca 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/host.ts @@ -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(); + private sfMap: Map; - /** - * Tracks those files in `sfCache` which have been augmented with type checking information - * already. - */ - private augmentedSourceFiles = new Set(); - - 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, 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|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); } } \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts new file mode 100644 index 0000000000..8169a79bdc --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts @@ -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): + boolean { + if (node.typeParameters === undefined) { + return true; + } + return node.typeParameters.every(param => param.constraint === undefined); +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 3b0dd0a2bf..dd9d7c5646 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -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, 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>, 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>); // 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, - private sourceFile: ts.SourceFile, private importManager: ImportManager, - private refEmitter: ReferenceEmitter) {} + readonly env: Environment, readonly boundTarget: BoundTarget) {} /** * 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.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.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, 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)` 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): 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; + } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts new file mode 100644 index 0000000000..d5d03e13b4 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts @@ -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 + */ + +/// +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>, 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')); +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts index 95bb2a2df0..c308ebfd7a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.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, 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(init: Partial, 'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): * NgForOf; * - * A typical usage would be: + * A typical constructor would be: * * NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']}); // Infers a type of NgForOf. * + * 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` 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, 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`, its rawType would be `FooDirective`. - 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, 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.TypeNode[] { + return params.map(param => ts.createTypeReferenceNode(param.name, undefined)); +} + +export function requiresInlineTypeCtor(node: ClassDeclaration): boolean { + // The class requires an inline type constructor if it has constrained (bound) generics. + return !checkIfGenericTypesAreUnbound(node); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 927f0135c3..8d4c824e96 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -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 = `
`; - 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 { @@ -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} {}`).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, 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>): ts.Expression { + return ref.node.name; + } + + referenceType(ref: Reference>): 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; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index 1670447621..a56d7477f6 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -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); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 935f4143ed..9b1d35b7cd 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -124,7 +124,7 @@ describe('ngtsc type checking', () => { selector: 'test', template: '
{{user.does_not_exist}}
', }) - 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();