diff --git a/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts b/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts index 405e479cd7..7d951acba3 100644 --- a/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts +++ b/packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts @@ -90,4 +90,6 @@ class NoIncrementalBuild implements IncrementalBuild { priorTypeCheckingResultsFor(): null { return null; } + + recordSuccessfulTypeCheck(): void {} } diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index e63dc1852f..b2687e727d 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -22,7 +22,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/shims:api", "//packages/compiler-cli/src/ngtsc/transform", - "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/util", "@npm//@types/node", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index abaf8d0826..c2b5559f6a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -21,7 +21,7 @@ import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform'; -import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck'; +import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api'; import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300'; import {SubsetOfKeys} from '../../util/src/typescript'; @@ -426,10 +426,10 @@ export class ComponentDecoratorHandler implements schemas = scope.schemas; } - const bound = new R3TargetBinder(matcher).bind({template: meta.template.diagNodes}); + const binder = new R3TargetBinder(matcher); ctx.addTemplate( - new Reference(node), bound, pipes, schemas, meta.template.sourceMapping, - meta.template.file); + new Reference(node), binder, meta.template.diagNodes, pipes, schemas, + meta.template.sourceMapping, meta.template.file); } resolve(node: ClassDeclaration, analysis: Readonly): diff --git a/packages/compiler-cli/src/ngtsc/core/BUILD.bazel b/packages/compiler-cli/src/ngtsc/core/BUILD.bazel index f0c86d51f9..e6b2d8fe54 100644 --- a/packages/compiler-cli/src/ngtsc/core/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/core/BUILD.bazel @@ -33,6 +33,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/switch", "//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index c5077cb3fa..6c63f02846 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -13,7 +13,7 @@ import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecorato import {CycleAnalyzer, ImportGraph} from '../../cycles'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; -import {getSourceFileOrError, LogicalFileSystem} from '../../file_system'; +import {LogicalFileSystem} from '../../file_system'; import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; import {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; @@ -28,7 +28,8 @@ import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeRe import {generatedFactoryTransform} from '../../shims'; import {ivySwitchTransform} from '../../switch'; import {aliasTransformFactory, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform'; -import {isTemplateDiagnostic, TemplateTypeChecker, TypeCheckContext, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck'; +import {isTemplateDiagnostic, TemplateTypeCheckerImpl} from '../../typecheck'; +import {TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api'; import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript'; import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api'; @@ -209,6 +210,10 @@ export class NgCompiler { return this.nextProgram; } + getTemplateTypeChecker(): TemplateTypeChecker { + return this.ensureAnalyzed().templateTypeChecker; + } + /** * Perform Angular's analysis step (as a precursor to `getDiagnostics` or `prepareEmit`) * asynchronously. @@ -494,12 +499,6 @@ export class NgCompiler { const compilation = this.ensureAnalyzed(); - // Execute the typeCheck phase of each decorator in the program. - const prepSpan = this.perfRecorder.start('typeCheckPrep'); - const results = compilation.templateTypeChecker.refresh(); - this.incrementalDriver.recordSuccessfulTypeCheck(results.perFileData); - this.perfRecorder.stop(prepSpan); - // Get the diagnostics. const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics'); const diagnostics: ts.Diagnostic[] = []; @@ -734,7 +733,7 @@ export class NgCompiler { handlers, reflector, this.perfRecorder, this.incrementalDriver, this.options.compileNonExportedClasses !== false, dtsTransforms); - const templateTypeChecker = new TemplateTypeChecker( + const templateTypeChecker = new TemplateTypeCheckerImpl( this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler, this.getTypeCheckingConfig(), refEmitter, reflector, this.adapter, this.incrementalDriver); diff --git a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts index cd2234e4fa..cb4589494b 100644 --- a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts +++ b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {absoluteFrom as _, FileSystem, getFileSystem, getSourceFileOrError, NgtscCompilerHost, setFileSystem} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {NoopIncrementalBuildStrategy} from '../../incremental'; -import {ReusedProgramStrategy} from '../../typecheck/src/augmented_program'; +import {ReusedProgramStrategy} from '../../typecheck'; import {NgCompilerOptions} from '../api'; import {NgCompiler} from '../src/compiler'; import {NgCompilerHost} from '../src/host'; diff --git a/packages/compiler-cli/src/ngtsc/incremental/api.ts b/packages/compiler-cli/src/ngtsc/incremental/api.ts index 14d43c8a9f..10a222e2bd 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/api.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/api.ts @@ -27,6 +27,12 @@ export interface IncrementalBuild { * Retrieve the prior type-checking work, if any, that's been done for the given source file. */ priorTypeCheckingResultsFor(fileSf: ts.SourceFile): FileTypeCheckDataT|null; + + /** + * Reports that template type-checking has completed successfully, with a map of type-checking + * data for each user file which can be reused in a future incremental iteration. + */ + recordSuccessfulTypeCheck(results: Map): void; } /** diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts b/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts index b0869309b9..4f4803232e 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/noop.ts @@ -11,4 +11,5 @@ import {IncrementalBuild} from '../api'; export const NOOP_INCREMENTAL_BUILD: IncrementalBuild = { priorWorkFor: () => null, priorTypeCheckingResultsFor: () => null, + recordSuccessfulTypeCheck: () => {}, }; diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts index 9bf237d791..dbcb93ef34 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/state.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/state.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {ClassRecord, TraitCompiler} from '../../transform'; -import {FileTypeCheckingData} from '../../typecheck/src/context'; +import {FileTypeCheckingData} from '../../typecheck/src/checker'; import {IncrementalBuild} from '../api'; import {FileDependencyGraph} from './dependency_tracking'; diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index bf81c77638..2f11883953 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -28,7 +28,7 @@ import {ReusedProgramStrategy} from './typecheck'; * command-line main() function or the Angular CLI. */ export class NgtscProgram implements api.Program { - private compiler: NgCompiler; + readonly compiler: NgCompiler; /** * The primary TypeScript program, which is used for analysis and emit. diff --git a/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel b/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel index 2c5854c872..2662e5b47f 100644 --- a/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel @@ -13,7 +13,6 @@ ts_library( "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/reflection", - "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index 4a61e72a18..ea7ca9453a 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -18,7 +18,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/translator", - "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 8f54bf9eb3..d6651f5c44 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -13,7 +13,7 @@ import {Reexport} from '../../imports'; import {IndexingContext} from '../../indexer'; import {ClassDeclaration, Decorator} from '../../reflection'; import {ImportManager} from '../../translator'; -import {TypeCheckContext} from '../../typecheck'; +import {TypeCheckContext} from '../../typecheck/api'; export enum HandlerPrecedence { /** diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index 0d57a1e622..812fdfabb7 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -14,7 +14,7 @@ import {IncrementalBuild} from '../../incremental/api'; import {IndexingContext} from '../../indexer'; import {PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, ReflectionHost} from '../../reflection'; -import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck'; +import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api'; import {getSourceFile, isExported} from '../../util/src/typescript'; import {AnalysisOutput, CompileResult, DecoratorHandler, HandlerFlags, HandlerPrecedence, ResolveResult} from './api'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel index 349905994c..9676cb540f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/BUILD.bazel @@ -4,7 +4,9 @@ package(default_visibility = ["//visibility:public"]) ts_library( name = "typecheck", - srcs = glob(["**/*.ts"]), + srcs = glob( + ["**/*.ts"], + ), deps = [ "//packages:types", "//packages/compiler", @@ -17,6 +19,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/shims:api", "//packages/compiler-cli/src/ngtsc/translator", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/util", "@npm//@types/node", "@npm//typescript", diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/api/BUILD.bazel new file mode 100644 index 0000000000..aa34cc2d95 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/BUILD.bazel @@ -0,0 +1,18 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "api", + srcs = glob(["**/*.ts"]), + module_name = "@angular/compiler-cli/src/ngtsc/typecheck/api", + deps = [ + "//packages:types", + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/metadata", + "//packages/compiler-cli/src/ngtsc/reflection", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts similarity index 92% rename from packages/compiler-cli/src/ngtsc/typecheck/src/api.ts rename to packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index fc9babdd87..cc674c33c5 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -13,7 +13,6 @@ import {AbsoluteFsPath} from '../../file_system'; import {Reference} from '../../imports'; import {TemplateGuardMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; -import {ComponentToShimMappingStrategy} from './context'; /** @@ -278,6 +277,25 @@ export interface ExternalTemplateSourceMapping { templateUrl: string; } +/** + * Abstracts the operation of determining which shim file will host a particular component's + * template type-checking code. + * + * Different consumers of the type checking infrastructure may choose different approaches to + * optimize for their specific use case (for example, the command-line compiler optimizes for + * efficient `ts.Program` reuse in watch mode). + */ +export interface ComponentToShimMappingStrategy { + /** + * Given a component, determine a path to the shim file into which that component's type checking + * code will be generated. + * + * A major constraint is that components in different input files must not share the same shim + * file. The behavior of the template type-checking system is undefined if this is violated. + */ + shimPathForComponent(node: ts.ClassDeclaration): AbsoluteFsPath; +} + /** * Strategy used to manage a `ts.Program` which contains template type-checking code and update it * over time. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts new file mode 100644 index 0000000000..47a3646b86 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google LLC 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 {TmplAstNode} from '@angular/compiler'; + +import * as ts from 'typescript'; + +/** + * Interface to the Angular Template Type Checker to extract diagnostics and intelligence from the + * compiler's understanding of component templates. + * + * This interface is analogous to TypeScript's own `ts.TypeChecker` API. + * + * In general, this interface supports two kinds of operations: + * - updating Type Check Blocks (TCB)s that capture the template in the form of TypeScript code + * - querying information about available TCBs, including diagnostics + * + * Once a TCB is available, information about it can be queried. If no TCB is available to answer a + * query, depending on the method either `null` will be returned or an error will be thrown. + */ +export interface TemplateTypeChecker { + /** + * Get all `ts.Diagnostic`s currently available for the given `ts.SourceFile`. + * + * This method will fail (throw) if there are components within the `ts.SourceFile` that do not + * have TCBs available. + */ + getDiagnosticsForFile(sf: ts.SourceFile): ts.Diagnostic[]; + + /** + * Retrieve the top-level node representing the TCB for the given component. + * + * This can return `null` if there is no TCB available for the component. + * + * This method always runs in `OptimizeFor.SingleFile` mode. + */ + getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts new file mode 100644 index 0000000000..9895075cd4 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google LLC 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 {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; +import * as ts from 'typescript'; + +import {Reference} from '../../imports'; +import {ClassDeclaration} from '../../reflection'; + +import {TemplateSourceMapping, TypeCheckableDirectiveMeta} from './api'; + +/** + * A currently pending type checking operation, into which templates for type-checking can be + * registered. + */ +export interface TypeCheckContext { + /** + * Register a template to potentially be type-checked. + * + * Templates registered via `addTemplate` are available for checking, but might be skipped if + * checking of that component is not required. This can happen for a few reasons, including if + * the component was previously checked and the prior results are still valid. + * + * @param ref a `Reference` to the component class which yielded this template. + * @param binder an `R3TargetBinder` which encapsulates the scope of this template, including all + * available directives. + * @param template the original template AST of this component. + * @param pipes a `Map` of pipes available within the scope of this template. + * @param schemas any schemas which apply to this template. + * @param sourceMapping a `TemplateSourceMapping` instance which describes the origin of the + * template text described by the AST. + * @param file the `ParseSourceFile` associated with the template. + */ + addTemplate( + ref: Reference>, + binder: R3TargetBinder, template: TmplAstNode[], + pipes: Map>>, + schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile): void; +} + +/** + * Interface to trigger generation of type-checking code for a program given a new + * `TypeCheckContext`. + */ +export interface ProgramTypeCheckAdapter { + typeCheck(sf: ts.SourceFile, ctx: TypeCheckContext): void; +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/index.ts new file mode 100644 index 0000000000..5a59d80402 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/index.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google LLC 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 + */ + +export * from './api'; +export * from './checker'; +export * from './context'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/index.ts index 9727f3a2cc..6c81aae866 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/index.ts @@ -6,11 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './src/api'; export {ReusedProgramStrategy} from './src/augmented_program'; -export {TemplateTypeChecker, ProgramTypeCheckAdapter} from './src/checker'; -export {TypeCheckContext} from './src/context'; -export {TemplateDiagnostic, isTemplateDiagnostic} from './src/diagnostics'; -export {TypeCheckShimGenerator} from './src/shim'; +export {FileTypeCheckingData, TemplateTypeCheckerImpl} from './src/checker'; +export {TypeCheckContextImpl} from './src/context'; +export {isTemplateDiagnostic, TemplateDiagnostic} from './src/diagnostics'; export {TypeCheckProgramHost} from './src/host'; +export {TypeCheckShimGenerator} from './src/shim'; export {typeCheckFilePath} from './src/type_check_file'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/augmented_program.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/augmented_program.ts index 902f0c4130..b2c6f535ca 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/augmented_program.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/augmented_program.ts @@ -10,8 +10,8 @@ import * as ts from 'typescript'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {retagAllTsFiles, untagAllTsFiles} from '../../shims'; +import {TypeCheckingProgramStrategy, UpdateMode} from '../api'; -import {TypeCheckingProgramStrategy, UpdateMode} from './api'; import {TypeCheckProgramHost} from './host'; import {TypeCheckShimGenerator} from './shim'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 9acdf13b46..e4cdac6f26 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -13,99 +13,160 @@ import {ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; import {ReflectionHost} from '../../reflection'; import {isShim} from '../../shims'; +import {getSourceFileOrNull} from '../../util/src/typescript'; +import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api'; -import {TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from './api'; -import {FileTypeCheckingData, TypeCheckContext, TypeCheckRequest} from './context'; -import {shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics'; - -/** - * Interface to trigger generation of type-checking code for a program given a new - * `TypeCheckContext`. - */ -export interface ProgramTypeCheckAdapter { - typeCheck(sf: ts.SourceFile, ctx: TypeCheckContext): void; -} +import {ShimTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from './context'; +import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics'; +import {TemplateSourceManager} from './source'; /** * Primary template type-checking engine, which performs type-checking using a * `TypeCheckingProgramStrategy` for type-checking program maintenance, and the * `ProgramTypeCheckAdapter` for generation of template type-checking code. */ -export class TemplateTypeChecker { - private files = new Map(); +export class TemplateTypeCheckerImpl implements TemplateTypeChecker { + private state = new Map(); + private isComplete = false; constructor( private originalProgram: ts.Program, - private typeCheckingStrategy: TypeCheckingProgramStrategy, + readonly typeCheckingStrategy: TypeCheckingProgramStrategy, private typeCheckAdapter: ProgramTypeCheckAdapter, private config: TypeCheckingConfig, private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, private compilerHost: Pick, private priorBuild: IncrementalBuild) {} - /** - * Reset the internal type-checking program by generating type-checking code from the user's - * program. - */ - refresh(): TypeCheckRequest { - this.files.clear(); - - const ctx = new TypeCheckContext( - this.config, this.compilerHost, this.typeCheckingStrategy, this.refEmitter, this.reflector); - - // Typecheck all the files. - for (const sf of this.originalProgram.getSourceFiles()) { - if (sf.isDeclarationFile || isShim(sf)) { - continue; - } - - const previousResults = this.priorBuild.priorTypeCheckingResultsFor(sf); - if (previousResults === null) { - // Previous results were not available, so generate new type-checking code for this file. - this.typeCheckAdapter.typeCheck(sf, ctx); - } else { - // Previous results were available, and can be adopted into the current build. - ctx.adoptPriorResults(sf, previousResults); - } - } - - const results = ctx.finalize(); - this.typeCheckingStrategy.updateFiles(results.updates, UpdateMode.Complete); - for (const [file, fileData] of results.perFileData) { - this.files.set(file, fileData); - } - - return results; - } - /** * Retrieve type-checking diagnostics from the given `ts.SourceFile` using the most recent * type-checking program. */ getDiagnosticsForFile(sf: ts.SourceFile): ts.Diagnostic[] { - const path = absoluteFromSourceFile(sf); - if (!this.files.has(path)) { - return []; - } - const fileRecord = this.files.get(path)!; + this.ensureAllShimsForAllFiles(); + + const sfPath = absoluteFromSourceFile(sf); + const fileRecord = this.state.get(sfPath)!; const typeCheckProgram = this.typeCheckingStrategy.getProgram(); const diagnostics: (ts.Diagnostic|null)[] = []; if (fileRecord.hasInlines) { - const inlineSf = getSourceFileOrError(typeCheckProgram, path); + const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceResolver))); + diag => convertDiagnostic(diag, fileRecord.sourceManager))); } + for (const [shimPath, shimRecord] of fileRecord.shimData) { const shimSf = getSourceFileOrError(typeCheckProgram, shimPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceResolver))); + diag => convertDiagnostic(diag, fileRecord.sourceManager))); diagnostics.push(...shimRecord.genesisDiagnostics); } + return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null); } + + getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null { + this.ensureAllShimsForAllFiles(); + + const program = this.typeCheckingStrategy.getProgram(); + const filePath = absoluteFromSourceFile(component.getSourceFile()); + const shimPath = this.typeCheckingStrategy.shimPathForComponent(component); + + if (!this.state.has(filePath)) { + throw new Error(`Error: no data for source file: ${filePath}`); + } + const fileRecord = this.state.get(filePath)!; + const id = fileRecord.sourceManager.getTemplateId(component); + + const shimSf = getSourceFileOrNull(program, shimPath); + if (shimSf === null) { + throw new Error(`Error: no shim file in program: ${shimPath}`); + } + + let node: ts.Node|null = findTypeCheckBlock(shimSf, id); + if (node === null) { + // Try for an inline block. + const inlineSf = getSourceFileOrError(program, filePath); + node = findTypeCheckBlock(inlineSf, id); + } + + return node; + } + + private maybeAdoptPriorResultsForFile(sf: ts.SourceFile): void { + const sfPath = absoluteFromSourceFile(sf); + if (this.state.has(sfPath)) { + const existingResults = this.state.get(sfPath)!; + + if (existingResults.isComplete) { + // All data for this file has already been generated, so no need to adopt anything. + return; + } + } + + const previousResults = this.priorBuild.priorTypeCheckingResultsFor(sf); + if (previousResults === null || !previousResults.isComplete) { + return; + } + + this.state.set(sfPath, previousResults); + } + + private ensureAllShimsForAllFiles(): void { + if (this.isComplete) { + return; + } + + const host = new WholeProgramTypeCheckingHost(this); + const ctx = this.newContext(host); + + for (const sf of this.originalProgram.getSourceFiles()) { + if (sf.isDeclarationFile || isShim(sf)) { + continue; + } + + this.maybeAdoptPriorResultsForFile(sf); + + const sfPath = absoluteFromSourceFile(sf); + const fileData = this.getFileData(sfPath); + if (fileData.isComplete) { + continue; + } + + this.typeCheckAdapter.typeCheck(sf, ctx); + + fileData.isComplete = true; + } + + this.updateFromContext(ctx); + this.isComplete = true; + } + + private newContext(host: TypeCheckingHost): TypeCheckContextImpl { + return new TypeCheckContextImpl( + this.config, this.compilerHost, this.typeCheckingStrategy, this.refEmitter, this.reflector, + host); + } + + private updateFromContext(ctx: TypeCheckContextImpl): void { + const updates = ctx.finalize(); + this.typeCheckingStrategy.updateFiles(updates, UpdateMode.Incremental); + this.priorBuild.recordSuccessfulTypeCheck(this.state); + } + + getFileData(path: AbsoluteFsPath): FileTypeCheckingData { + if (!this.state.has(path)) { + this.state.set(path, { + hasInlines: false, + sourceManager: new TemplateSourceManager(), + isComplete: false, + shimData: new Map(), + }); + } + return this.state.get(path)!; + } } function convertDiagnostic( @@ -115,3 +176,65 @@ function convertDiagnostic( } return translateDiagnostic(diag, sourceResolver); } + +/** + * Data for template type-checking related to a specific input file in the user's program (which + * contains components to be checked). + */ +export interface FileTypeCheckingData { + /** + * Whether the type-checking shim required any inline changes to the original file, which affects + * whether the shim can be reused. + */ + hasInlines: boolean; + + /** + * Source mapping information for mapping diagnostics from inlined type check blocks back to the + * original template. + */ + sourceManager: TemplateSourceManager; + + /** + * Data for each shim generated from this input file. + * + * A single input file will generate one or more shim files that actually contain template + * type-checking code. + */ + shimData: Map; + + /** + * Whether the template type-checker is certain that all components from this input file have had + * type-checking code generated into shims. + */ + isComplete: boolean; +} + +/** + * Drives a `TypeCheckContext` to generate type-checking code for every component in the program. + */ +class WholeProgramTypeCheckingHost implements TypeCheckingHost { + constructor(private impl: TemplateTypeCheckerImpl) {} + + getSourceManager(sfPath: AbsoluteFsPath): TemplateSourceManager { + return this.impl.getFileData(sfPath).sourceManager; + } + + shouldCheckComponent(node: ts.ClassDeclaration): boolean { + const fileData = this.impl.getFileData(absoluteFromSourceFile(node.getSourceFile())); + const shimPath = this.impl.typeCheckingStrategy.shimPathForComponent(node); + // The component needs to be checked unless the shim which would contain it already exists. + return !fileData.shimData.has(shimPath); + } + + recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void { + const fileData = this.impl.getFileData(sfPath); + fileData.shimData.set(data.path, data); + if (data.hasInlines) { + fileData.hasInlines = true; + } + } + + recordComplete(sfPath: AbsoluteFsPath): void { + this.impl.getFileData(sfPath).isComplete = true; + } +} diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index c2045b253f..7e92d49a4e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -6,16 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {BoundTarget, ParseSourceFile, SchemaMetadata} from '@angular/compiler'; +import {ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; import * as ts from 'typescript'; import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; +import {ComponentToShimMappingStrategy, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api'; -import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig, TypeCtorMetadata} from './api'; -import {TemplateSourceResolver} from './diagnostics'; import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom'; import {Environment} from './environment'; import {OutOfBandDiagnosticRecorder, OutOfBandDiagnosticRecorderImpl} from './oob'; @@ -24,55 +23,6 @@ import {generateTypeCheckBlock, requiresInlineTypeCheckBlock} from './type_check import {TypeCheckFile} from './type_check_file'; import {generateInlineTypeCtor, requiresInlineTypeCtor} from './type_constructor'; -/** - * Complete type-checking code generated for the user's program, ready for input into the - * type-checking engine. - */ -export interface TypeCheckRequest { - /** - * Map of source filenames to new contents for those files. - * - * This includes both contents of type-checking shim files, as well as changes to any user files - * which needed to be made to support template type-checking. - */ - updates: Map; - - /** - * Map containing additional data for each type-checking shim that is required to support - * generation of diagnostics. - */ - perFileData: Map; -} - -/** - * Data for template type-checking related to a specific input file in the user's program (which - * contains components to be checked). - */ -export interface FileTypeCheckingData { - /** - * Whether the type-checking shim required any inline changes to the original file, which affects - * whether the shim can be reused. - */ - hasInlines: boolean; - - /** - * Source mapping information for mapping diagnostics from inlined type check blocks back to the - * original template. - */ - sourceResolver: TemplateSourceResolver; - - /** - * Data for each shim generated from this input file. - * - * A single input file will generate one or more shim files that actually contain template - * type-checking code. - */ - shimData: Map; -} - -/** - * Data specific to a single shim generated from an input file. - */ export interface ShimTypeCheckingData { /** * Path to the shim file. @@ -85,6 +35,11 @@ export interface ShimTypeCheckingData { * Some diagnostics are produced during creation time and are tracked here. */ genesisDiagnostics: ts.Diagnostic[]; + + /** + * Whether any inline operations for the input file were required to generate this shim. + */ + hasInlines: boolean; } /** @@ -126,38 +81,55 @@ export interface PendingShimData { } /** - * Abstracts the operation of determining which shim file will host a particular component's - * template type-checking code. + * Adapts the `TypeCheckContextImpl` to the larger template type-checking system. * - * Different consumers of the type checking infrastructure may choose different approaches to - * optimize for their specific use case (for example, the command-line compiler optimizes for - * efficient `ts.Program` reuse in watch mode). + * Through this interface, a single `TypeCheckContextImpl` (which represents one "pass" of template + * type-checking) requests information about the larger state of type-checking, as well as reports + * back its results once finalized. */ -export interface ComponentToShimMappingStrategy { +export interface TypeCheckingHost { /** - * Given a component, determine a path to the shim file into which that component's type checking - * code will be generated. - * - * A major constraint is that components in different input files must not share the same shim - * file. The behavior of the template type-checking system is undefined if this is violated. + * Retrieve the `TemplateSourceManager` responsible for components in the given input file path. */ - shimPathForComponent(node: ts.ClassDeclaration): AbsoluteFsPath; + getSourceManager(sfPath: AbsoluteFsPath): TemplateSourceManager; + + /** + * Whether a particular component class should be included in the current type-checking pass. + * + * Not all components offered to the `TypeCheckContext` for checking may require processing. For + * example, the component may have results already available from a prior pass or from a previous + * program. + */ + shouldCheckComponent(node: ts.ClassDeclaration): boolean; + + /** + * Report data from a shim generated from the given input file path. + */ + recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void; + + /** + * Record that all of the components within the given input file path had code generated - that + * is, coverage for the file can be considered complete. + */ + recordComplete(sfPath: AbsoluteFsPath): void; } + /** * A template type checking context for a program. * * The `TypeCheckContext` allows registration of components and their templates which need to be * type checked. */ -export class TypeCheckContext { +export class TypeCheckContextImpl implements TypeCheckContext { private fileMap = new Map(); constructor( private config: TypeCheckingConfig, private compilerHost: Pick, private componentMappingStrategy: ComponentToShimMappingStrategy, - private refEmitter: ReferenceEmitter, private reflector: ReflectionHost) {} + private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, + private host: TypeCheckingHost) {} /** * A `Map` of `ts.SourceFile`s that the context has seen to the operations (additions of methods @@ -171,22 +143,6 @@ export class TypeCheckContext { */ private typeCtorPending = new Set(); - /** - * Map of data for file paths which was adopted from a prior compilation. - * - * This data allows the `TypeCheckContext` to generate a `TypeCheckRequest` which can interpret - * diagnostics from type-checking shims included in the prior compilation. - */ - private adoptedFiles = new Map(); - - /** - * Record the `FileTypeCheckingData` from a previous program that's associated with a particular - * source file. - */ - adoptPriorResults(sf: ts.SourceFile, data: FileTypeCheckingData): void { - this.adoptedFiles.set(absoluteFromSourceFile(sf), data); - } - /** * Record a template for the given component `node`, with a `SelectorMatcher` for directive * matching. @@ -197,12 +153,17 @@ export class TypeCheckContext { */ addTemplate( ref: Reference>, - boundTarget: BoundTarget, + binder: R3TargetBinder, template: TmplAstNode[], pipes: Map>>, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, file: ParseSourceFile): void { + if (!this.host.shouldCheckComponent(ref.node)) { + return; + } + const fileData = this.dataForFile(ref.node.getSourceFile()); const shimData = this.pendingShimForComponent(ref.node); + const boundTarget = binder.bind({template}); // Get all of the directives used in the template and record type constructors for all of them. for (const dir of boundTarget.getUsedDirectives()) { const dirRef = dir.ref as Reference>; @@ -308,7 +269,7 @@ export class TypeCheckContext { return code; } - finalize(): TypeCheckRequest { + finalize(): Map { // First, build the map of updates to source files. const updates = new Map(); for (const originalSf of this.opMap.keys()) { @@ -318,39 +279,23 @@ export class TypeCheckContext { } } - const results: TypeCheckRequest = { - updates: updates, - perFileData: new Map(), - }; - // Then go through each input file that has pending code generation operations. for (const [sfPath, pendingFileData] of this.fileMap) { - const fileData: FileTypeCheckingData = { - hasInlines: pendingFileData.hasInlines, - shimData: new Map(), - sourceResolver: pendingFileData.sourceManager, - }; - // For each input file, consider generation operations for each of its shims. - for (const [shimPath, pendingShimData] of pendingFileData.shimData) { - updates.set(pendingShimData.file.fileName, pendingShimData.file.render()); - fileData.shimData.set(shimPath, { + for (const pendingShimData of pendingFileData.shimData.values()) { + this.host.recordShimData(sfPath, { genesisDiagnostics: [ ...pendingShimData.domSchemaChecker.diagnostics, ...pendingShimData.oobRecorder.diagnostics, ], + hasInlines: pendingFileData.hasInlines, path: pendingShimData.file.fileName, }); + updates.set(pendingShimData.file.fileName, pendingShimData.file.render()); } - - results.perFileData.set(sfPath, fileData); } - for (const [sfPath, fileData] of this.adoptedFiles.entries()) { - results.perFileData.set(sfPath, fileData); - } - - return results; + return updates; } private addInlineTypeCheckBlock( @@ -385,12 +330,10 @@ export class TypeCheckContext { private dataForFile(sf: ts.SourceFile): PendingFileTypeCheckingData { const sfPath = absoluteFromSourceFile(sf); - const sourceManager = new TemplateSourceManager(); - if (!this.fileMap.has(sfPath)) { const data: PendingFileTypeCheckingData = { hasInlines: false, - sourceManager, + sourceManager: this.host.getSourceManager(sfPath), shimData: new Map(), }; this.fileMap.set(sfPath, data); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts index c2b7c2b250..584dabd984 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts @@ -9,8 +9,8 @@ import {AbsoluteSourceSpan, ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; import {getTokenAtPosition} from '../../util/src/typescript'; +import {ExternalTemplateSourceMapping, TemplateId, TemplateSourceMapping} from '../api'; -import {ExternalTemplateSourceMapping, TemplateId, TemplateSourceMapping} from './api'; /** * A `ts.Diagnostic` with additional information about the diagnostic related to template @@ -28,6 +28,8 @@ export interface TemplateDiagnostic extends ts.Diagnostic { * in a TCB and map them back to original locations in the template. */ export interface TemplateSourceResolver { + getTemplateId(node: ts.ClassDeclaration): TemplateId; + /** * For the given template id, retrieve the original source mapping which describes how the offsets * in the template should be interpreted. @@ -140,6 +142,15 @@ export function translateDiagnostic( mapping, span, diagnostic.category, diagnostic.code, diagnostic.messageText); } +export function findTypeCheckBlock(file: ts.SourceFile, id: TemplateId): ts.Node|null { + for (const stmt of file.statements) { + if (ts.isFunctionDeclaration(stmt) && getTemplateId(stmt, file) === id) { + return stmt; + } + } + return null; +} + /** * Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template. */ diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts index 74686303d1..3a4f43af3a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts @@ -10,8 +10,8 @@ import {DomElementSchemaRegistry, ParseSourceSpan, SchemaMetadata, TmplAstElemen import * as ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; +import {TemplateId} from '../api'; -import {TemplateId} from './api'; import {makeTemplateDiagnostic, TemplateSourceResolver} from './diagnostics'; const REGISTRY = new DomElementSchemaRegistry(); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts index 597492f37c..e0d8bb1905 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -12,8 +12,8 @@ import * as ts from 'typescript'; import {ImportFlags, NOOP_DEFAULT_IMPORT_RECORDER, Reference, ReferenceEmitter} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager, translateExpression, translateType} from '../../translator'; +import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api'; -import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from './api'; import {tsDeclareVariable} from './ts_util'; import {generateTypeCtorDeclarationFn, requiresInlineTypeCtor} from './type_constructor'; import {TypeParameterEmitter} from './type_parameter_emitter'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index 2a1486c470..e068cecb87 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -8,8 +8,8 @@ import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler'; import * as ts from 'typescript'; +import {TypeCheckingConfig} from '../api'; -import {TypeCheckingConfig} from './api'; import {addParseSpanInfo, wrapForDiagnostics} from './diagnostics'; import {tsCastToAny} from './ts_util'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts index ff18c14153..77b0807f50 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts @@ -10,8 +10,8 @@ import {BindingPipe, PropertyWrite, TmplAstReference, TmplAstVariable} from '@an import * as ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; +import {TemplateId} from '../api'; -import {TemplateId} from './api'; import {makeTemplateDiagnostic, TemplateSourceResolver} from './diagnostics'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts index da5c08c472..2401cc8aae 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts @@ -8,8 +8,8 @@ import {AbsoluteSourceSpan, ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler'; import * as ts from 'typescript'; +import {TemplateId, TemplateSourceMapping} from '../api'; -import {TemplateId, TemplateSourceMapping} from './api'; import {TemplateSourceResolver} from './diagnostics'; import {computeLineStartsMap, getLineAndCharacterFromPosition} from './line_mappings'; @@ -55,6 +55,10 @@ export class TemplateSourceManager implements TemplateSourceResolver { */ private templateSources = new Map(); + getTemplateId(node: ts.ClassDeclaration): TemplateId { + return getTemplateId(node); + } + captureSource(node: ts.ClassDeclaration, mapping: TemplateSourceMapping, file: ParseSourceFile): TemplateId { const id = getTemplateId(node); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_semantics.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_semantics.ts index fd61794803..d51a075bd0 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_semantics.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_semantics.ts @@ -8,7 +8,8 @@ import {AST, BoundTarget, ImplicitReceiver, PropertyWrite, RecursiveAstVisitor, TmplAstVariable} from '@angular/compiler'; -import {TemplateId} from './api'; +import {TemplateId} from '../api'; + import {OutOfBandDiagnosticRecorder} from './oob'; /** 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 172ab518a5..014ff17e02 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 @@ -11,8 +11,8 @@ import * as ts from 'typescript'; import {Reference} from '../../imports'; import {ClassDeclaration} from '../../reflection'; +import {TemplateId, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata} from '../api'; -import {TemplateId, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata} from './api'; import {addParseSpanInfo, addTemplateId, ignoreDiagnostics, wrapForDiagnostics} from './diagnostics'; import {DomSchemaChecker} from './dom'; import {Environment} from './environment'; 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 index af0eaab646..767f78e861 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts @@ -11,8 +11,8 @@ import {AbsoluteFsPath, join} from '../../file_system'; import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; +import {TypeCheckBlockMetadata, TypeCheckingConfig} from '../api'; -import {TypeCheckBlockMetadata, TypeCheckingConfig} from './api'; import {DomSchemaChecker} from './dom'; import {Environment} from './environment'; import {OutOfBandDiagnosticRecorder} from './oob'; 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 e4c79fb3e7..139ec6a232 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts @@ -9,8 +9,8 @@ import * as ts from 'typescript'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; +import {TypeCtorMetadata} from '../api'; -import {TypeCtorMetadata} from './api'; import {TypeParameterEmitter} from './type_parameter_emitter'; export function generateTypeCtorDeclarationFn( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel index ce34bc6aa5..bbf0bb0cf6 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/BUILD.bazel @@ -19,6 +19,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "//packages/compiler-cli/src/ngtsc/util", "@npm//typescript", ], diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts index 156c2c8f4f..2aa048eeff 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts @@ -8,10 +8,11 @@ import * as ts from 'typescript'; +import {absoluteFrom, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem, TestFile} from '../../file_system/testing'; -import {TypeCheckingConfig} from '../src/api'; +import {TypeCheckingConfig} from '../api'; -import {ngForDeclaration, ngForDts, TestDeclaration, typecheck} from './test_utils'; +import {ngForDeclaration, ngForDts, setup, TestDeclaration} from './test_utils'; runInEachFileSystem(() => { describe('template diagnostics', () => { @@ -35,7 +36,7 @@ runInEachFileSystem(() => { }]); expect(messages).toEqual( - [`synthetic.html(1, 10): Type 'string' is not assignable to type 'number'.`]); + [`TestComponent.html(1, 10): Type 'string' is not assignable to type 'number'.`]); }); it('infers type of template variables', () => { @@ -49,7 +50,7 @@ runInEachFileSystem(() => { [ngForDeclaration()], [ngForDts()]); expect(messages).toEqual([ - `synthetic.html(1, 62): Argument of type 'number' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 62): Argument of type 'number' is not assignable to parameter of type 'string'.`, ]); }); @@ -83,7 +84,7 @@ runInEachFileSystem(() => { }]); expect(messages).toEqual([ - `synthetic.html(1, 24): Argument of type 'HTMLDivElement' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 24): Argument of type 'HTMLDivElement' is not assignable to parameter of type 'string'.`, ]); }); @@ -104,7 +105,7 @@ runInEachFileSystem(() => { }]); expect(messages).toEqual([ - `synthetic.html(1, 31): Argument of type 'Dir' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 31): Argument of type 'Dir' is not assignable to parameter of type 'string'.`, ]); }); @@ -115,7 +116,7 @@ runInEachFileSystem(() => { }`); expect(messages).toEqual([ - `synthetic.html(1, 30): Argument of type 'TemplateRef' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 30): Argument of type 'TemplateRef' is not assignable to parameter of type 'string'.`, ]); }); @@ -130,7 +131,7 @@ runInEachFileSystem(() => { [ngForDeclaration()], [ngForDts()]); expect(messages).toEqual([ - `synthetic.html(1, 47): Property 'namme' does not exist on type '{ name: string; }'. Did you mean 'name'?`, + `TestComponent.html(1, 47): Property 'namme' does not exist on type '{ name: string; }'. Did you mean 'name'?`, ]); }); @@ -151,8 +152,8 @@ runInEachFileSystem(() => { }`); expect(messages).toEqual([ - `synthetic.html(1, 29): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`, - `synthetic.html(1, 6): Can't bind to 'srcc' since it isn't a known property of 'img'.`, + `TestComponent.html(1, 29): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`, + `TestComponent.html(1, 6): Can't bind to 'srcc' since it isn't a known property of 'img'.`, ]); }); @@ -171,7 +172,7 @@ runInEachFileSystem(() => { }]); expect(messages).toEqual([ - `synthetic.html(1, 10): Type '"drak"' is not assignable to type '"dark" | "light"'.`, + `TestComponent.html(1, 10): Type '"drak"' is not assignable to type '"dark" | "light"'.`, ]); }); @@ -190,7 +191,7 @@ runInEachFileSystem(() => { [{type: 'pipe', name: 'Pipe', pipeName: 'pipe'}]); expect(messages).toEqual([ - `synthetic.html(1, 28): Argument of type 'number' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 28): Argument of type 'number' is not assignable to parameter of type 'string'.`, ]); }); @@ -204,8 +205,8 @@ runInEachFileSystem(() => { }`); expect(messages).toEqual([ - `synthetic.html(1, 4): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, - `synthetic.html(1, 24): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, + `TestComponent.html(1, 4): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, + `TestComponent.html(1, 24): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, ]); }); @@ -230,7 +231,7 @@ runInEachFileSystem(() => { }]); expect(messages).toEqual([ - `synthetic.html(1, 14): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, + `TestComponent.html(1, 14): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, ]); }); @@ -260,7 +261,7 @@ runInEachFileSystem(() => { [{type: 'directive', name: 'Dir', selector: '[dir]', outputs: {'out': 'event'}}]); expect(messages).toEqual([ - `synthetic.html(1, 31): Argument of type 'number' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 31): Argument of type 'number' is not assignable to parameter of type 'string'.`, ]); }); @@ -271,7 +272,7 @@ runInEachFileSystem(() => { }`); expect(messages).toEqual([ - `synthetic.html(1, 41): Argument of type 'AnimationEvent' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 41): Argument of type 'AnimationEvent' is not assignable to parameter of type 'string'.`, ]); }); @@ -283,7 +284,7 @@ runInEachFileSystem(() => { }`); expect(messages).toEqual([ - `synthetic.html(1, 27): Argument of type 'MouseEvent' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 27): Argument of type 'MouseEvent' is not assignable to parameter of type 'string'.`, ]); }); @@ -329,7 +330,7 @@ runInEachFileSystem(() => { }; }`); - expect(messages).toEqual([`synthetic.html(1, 41): Object is possibly 'undefined'.`]); + expect(messages).toEqual([`TestComponent.html(1, 41): Object is possibly 'undefined'.`]); }); it('does not produce diagnostic for checked property access', () => { @@ -362,8 +363,8 @@ class TestComponent { }`); expect(messages).toEqual([ - `synthetic.html(3, 15): Property 'srcc' does not exist on type 'TestComponent'. Did you mean 'src'?`, - `synthetic.html(4, 18): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`, + `TestComponent.html(3, 15): Property 'srcc' does not exist on type 'TestComponent'. Did you mean 'src'?`, + `TestComponent.html(4, 18): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`, ]); }); }); @@ -378,7 +379,7 @@ class TestComponent { }`); expect(messages).toEqual([ - `synthetic.html(1, 11): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?` + `TestComponent.html(1, 11): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?` ]); }); @@ -390,7 +391,7 @@ class TestComponent { }; }`); - expect(messages).toEqual([`synthetic.html(1, 19): Expected 0 arguments, but got 1.`]); + expect(messages).toEqual([`TestComponent.html(1, 19): Expected 0 arguments, but got 1.`]); }); }); @@ -404,7 +405,7 @@ class TestComponent { }`); expect(messages).toEqual([ - `synthetic.html(1, 12): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?` + `TestComponent.html(1, 12): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?` ]); }); @@ -416,7 +417,7 @@ class TestComponent { }; }`); - expect(messages).toEqual([`synthetic.html(1, 20): Expected 0 arguments, but got 1.`]); + expect(messages).toEqual([`TestComponent.html(1, 20): Expected 0 arguments, but got 1.`]); }); }); @@ -430,7 +431,7 @@ class TestComponent { }`); expect(messages).toEqual([ - `synthetic.html(1, 22): Property 'nname' does not exist on type '{ name: string; }'. Did you mean 'name'?` + `TestComponent.html(1, 22): Property 'nname' does not exist on type '{ name: string; }'. Did you mean 'name'?` ]); }); @@ -443,7 +444,7 @@ class TestComponent { }`); expect(messages).toEqual( - [`synthetic.html(1, 15): Type '2' is not assignable to type 'string'.`]); + [`TestComponent.html(1, 15): Type '2' is not assignable to type 'string'.`]); }); }); }); @@ -452,7 +453,26 @@ function diagnose( template: string, source: string, declarations?: TestDeclaration[], additionalSources: TestFile[] = [], config?: Partial, options?: ts.CompilerOptions): string[] { - const diagnostics = typecheck(template, source, declarations, additionalSources, config, options); + const sfPath = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup( + [ + { + fileName: sfPath, + templates: { + 'TestComponent': template, + }, + source, + declarations, + }, + ...additionalSources.map(testFile => ({ + fileName: testFile.name, + source: testFile.contents, + templates: {}, + })), + ], + {config, options}); + const sf = getSourceFileOrError(program, sfPath); + const diagnostics = templateTypeChecker.getDiagnosticsForFile(sf); return diagnostics.map(diag => { const text = typeof diag.messageText === 'string' ? diag.messageText : diag.messageText.messageText; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts index 1d14e84ea6..f367e1687e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts @@ -12,16 +12,23 @@ import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_sys import {runInEachFileSystem} from '../../file_system/testing'; import {sfExtensionData, ShimReferenceTagger} from '../../shims'; import {expectCompleteReuse, makeProgram} from '../../testing'; -import {UpdateMode} from '../src/api'; +import {UpdateMode} from '../api'; import {ReusedProgramStrategy} from '../src/augmented_program'; -import {createProgramWithNoTemplates} from './test_utils'; +import {setup} from './test_utils'; runInEachFileSystem(() => { describe('template type-checking program', () => { it('should not be created if no components need to be checked', () => { - const {program, templateTypeChecker, programStrategy} = createProgramWithNoTemplates(); - templateTypeChecker.refresh(); + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker, programStrategy} = setup([{ + fileName, + templates: {}, + source: `export class NotACmp {}`, + }]); + const sf = getSourceFileOrError(program, fileName); + + templateTypeChecker.getDiagnosticsForFile(sf); // expect() here would create a really long error message, so this is checked manually. if (programStrategy.getProgram() !== program) { fail('Template type-checking created a new ts.Program even though it had no changes.'); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index cdc76566f0..7fc09368fa 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -9,20 +9,22 @@ import {CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement, TmplAstReference, Type} from '@angular/compiler'; import * as ts from 'typescript'; -import {absoluteFrom, AbsoluteFsPath, LogicalFileSystem} from '../../file_system'; +import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system'; import {TestFile} from '../../file_system/testing'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {NOOP_INCREMENTAL_BUILD} from '../../incremental'; import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {makeProgram} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; -import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig, UpdateMode} from '../src/api'; +import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckContext} from '../api'; + +import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig, UpdateMode} from '../api/api'; import {ReusedProgramStrategy} from '../src/augmented_program'; -import {ProgramTypeCheckAdapter, TemplateTypeChecker} from '../src/checker'; -import {TypeCheckContext} from '../src/context'; +import {TemplateTypeCheckerImpl} from '../src/checker'; import {DomSchemaChecker} from '../src/dom'; import {Environment} from '../src/environment'; import {OutOfBandDiagnosticRecorder} from '../src/oob'; +import {TypeCheckShimGenerator} from '../src/shim'; import {generateTypeCheckBlock} from '../src/type_check_block'; export function typescriptLibDts(): TestFile { @@ -235,32 +237,88 @@ export function tcb( return res.replace(/\s+/g, ' '); } -export interface TemplateTestEnvironment { - sf: ts.SourceFile; - program: ts.Program; - templateTypeChecker: TemplateTypeChecker; - programStrategy: ReusedProgramStrategy; +/** + * A file in the test program, along with any template information for components within the file. + */ +export interface TypeCheckingTarget { + /** + * Path to the file in the virtual test filesystem. + */ + fileName: AbsoluteFsPath; + + /** + * Raw source code for the file. + * + * If this is omitted, source code for the file will be generated based on any expected component + * classes. + */ + source?: string; + + /** + * A map of component class names to string templates for that component. + */ + templates: {[className: string]: string}; + + /** + * Any declarations (e.g. directives) which should be considered as part of the scope for the + * components in this file. + */ + declarations?: TestDeclaration[]; } -function setupTemplateTypeChecking( - source: string, additionalSources: {name: AbsoluteFsPath; contents: string}[], - config: Partial, opts: ts.CompilerOptions, - makeTypeCheckAdapterFn: (program: ts.Program, sf: ts.SourceFile) => - ProgramTypeCheckAdapter): TemplateTestEnvironment { - const typeCheckFilePath = absoluteFrom('/main.ngtypecheck.ts'); +/** + * Create a testing environment for template type-checking which contains a number of given test + * targets. + * + * A full Angular environment is not necessary to exercise the template type-checking system. + * Components only need to be classes which exist, with templates specified in the target + * configuration. In many cases, it's not even necessary to include source code for test files, as + * that can be auto-generated based on the provided target configuration. + */ +export function setup(targets: TypeCheckingTarget[], overrides: { + config?: Partial, + options?: ts.CompilerOptions, +} = {}): { + templateTypeChecker: TemplateTypeChecker, + program: ts.Program, + programStrategy: ReusedProgramStrategy, +} { const files = [ typescriptLibDts(), angularCoreDts(), angularAnimationsDts(), - // Add the typecheck file to the program, as the typecheck program is created with the - // assumption that the typecheck file was already a root file in the original program. - {name: typeCheckFilePath, contents: 'export const TYPECHECK = true;'}, - {name: absoluteFrom('/main.ts'), contents: source}, - ...additionalSources, ]; - const {program, host, options} = - makeProgram(files, {strictNullChecks: true, noImplicitAny: true, ...opts}, undefined, false); - const sf = program.getSourceFile(absoluteFrom('/main.ts'))!; + + for (const target of targets) { + let contents: string; + if (target.source !== undefined) { + contents = target.source; + } else { + contents = `// generated from templates\n\nexport const MODULE = true;\n\n`; + for (const className of Object.keys(target.templates)) { + contents += `export class ${className} {}\n`; + } + } + + files.push({ + name: target.fileName, + contents, + }); + + if (!target.fileName.endsWith('.d.ts')) { + files.push({ + name: TypeCheckShimGenerator.shimFor(target.fileName), + contents: 'export const MODULE = true;', + }); + } + } + + const opts = overrides.options ?? {}; + const config = overrides.config ?? {}; + + const {program, host, options} = makeProgram( + files, {strictNullChecks: true, noImplicitAny: true, ...opts}, /* host */ undefined, + /* checkForErrors */ false); const checker = program.getTypeChecker(); const logicalFs = new LogicalFileSystem(getRootDirs(host, options), host); const reflectionHost = new TypeScriptReflectionHost(checker); @@ -274,22 +332,18 @@ function setupTemplateTypeChecking( ]); const fullConfig = {...ALL_ENABLED_CONFIG, ...config}; - const checkAdapter = makeTypeCheckAdapterFn(program, sf); - const programStrategy = new ReusedProgramStrategy(program, host, options, []); - const templateTypeChecker = new TemplateTypeChecker( - program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host, - NOOP_INCREMENTAL_BUILD); + const checkAdapter = createTypeCheckAdapter((sf, ctx) => { + for (const target of targets) { + if (getSourceFileOrError(program, target.fileName) !== sf) { + continue; + } - return {program, sf, templateTypeChecker, programStrategy}; -} + const declarations = target.declarations ?? []; -export function typecheck( - template: string, source: string, declarations: TestDeclaration[] = [], - additionalSources: {name: AbsoluteFsPath; contents: string}[] = [], - config: Partial = {}, opts: ts.CompilerOptions = {}): ts.Diagnostic[] { - const {sf, templateTypeChecker} = - setupTemplateTypeChecking(source, additionalSources, config, opts, (program, sf) => { - const templateUrl = 'synthetic.html'; + for (const className of Object.keys(target.templates)) { + const classDecl = getClass(sf, className); + const template = target.templates[className]; + const templateUrl = `${className}.html`; const templateFile = new ParseSourceFile(template, templateUrl); const {nodes, errors} = parseTemplate(template, templateUrl); if (errors !== undefined) { @@ -307,44 +361,38 @@ export function typecheck( return getClass(declFile, decl.name); }); const binder = new R3TargetBinder(matcher); - const boundTarget = binder.bind({template: nodes}); - const clazz = new Reference(getClass(sf, 'TestComponent')); + const classRef = new Reference(classDecl); const sourceMapping: TemplateSourceMapping = { type: 'external', template, templateUrl, - componentClass: clazz.node, + componentClass: classRef.node, // Use the class's name for error mappings. - node: clazz.node.name, + node: classRef.node.name, }; - return createTypeCheckAdapter((ctx: TypeCheckContext) => { - ctx.addTemplate(clazz, boundTarget, pipes, [], sourceMapping, templateFile); - }); - }); - - templateTypeChecker.refresh(); - return templateTypeChecker.getDiagnosticsForFile(sf); -} - -export function createProgramWithNoTemplates(): TemplateTestEnvironment { - return setupTemplateTypeChecking( - 'export const NOT_A_COMPONENT = true;', [], {}, {}, () => createTypeCheckAdapter(() => {})); -} - -function createTypeCheckAdapter(fn: (ctx: TypeCheckContext) => void): ProgramTypeCheckAdapter { - let called = false; - return { - typeCheck: (sf: ts.SourceFile, ctx: TypeCheckContext) => { - if (!called) { - fn(ctx); + ctx.addTemplate(classRef, binder, nodes, pipes, [], sourceMapping, templateFile); } - called = true; - }, + } + }); + + const programStrategy = new ReusedProgramStrategy(program, host, options, ['ngtypecheck']); + const templateTypeChecker = new TemplateTypeCheckerImpl( + program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host, + NOOP_INCREMENTAL_BUILD); + return { + templateTypeChecker, + program, + programStrategy, }; } +function createTypeCheckAdapter(fn: (sf: ts.SourceFile, ctx: TypeCheckContext) => void): + ProgramTypeCheckAdapter { + return {typeCheck: fn}; +} + function prepareDeclarations( declarations: TestDeclaration[], resolveDeclaration: (decl: TestDeclaration) => ClassDeclaration) { @@ -386,7 +434,7 @@ export function getClass(sf: ts.SourceFile, name: string): ClassDeclaration { + describe('TemplateTypeChecker', () => { + it('should batch diagnostic operations when requested in WholeProgram mode', () => { + const file1 = absoluteFrom('/file1.ts'); + const file2 = absoluteFrom('/file2.ts'); + const {program, templateTypeChecker, programStrategy} = setup([ + {fileName: file1, templates: {'Cmp1': '
'}}, + {fileName: file2, templates: {'Cmp2': ''}} + ]); + + templateTypeChecker.getDiagnosticsForFile(getSourceFileOrError(program, file1)); + const ttcProgram1 = programStrategy.getProgram(); + templateTypeChecker.getDiagnosticsForFile(getSourceFileOrError(program, file2)); + const ttcProgram2 = programStrategy.getProgram(); + + expect(ttcProgram1).toBe(ttcProgram2); + }); + + it('should allow access to the type-check block of a component', () => { + const file1 = absoluteFrom('/file1.ts'); + const file2 = absoluteFrom('/file2.ts'); + const {program, templateTypeChecker, programStrategy} = setup([ + {fileName: file1, templates: {'Cmp1': '
'}}, + {fileName: file2, templates: {'Cmp2': ''}} + ]); + + const cmp1 = getClass(getSourceFileOrError(program, file1), 'Cmp1'); + const block = templateTypeChecker.getTypeCheckBlock(cmp1); + expect(block).not.toBeNull(); + expect(block!.getText()).toMatch(/: i[0-9]\.Cmp1/); + expect(block!.getText()).toContain(`document.createElement("div")`); + }); + }); +}); 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 a846503320..57ec025f1d 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 @@ -10,16 +10,16 @@ import * as ts from 'typescript'; import {absoluteFrom, AbsoluteFsPath, getFileSystem, getSourceFileOrError, LogicalFileSystem, NgtscCompilerHost} from '../../file_system'; import {runInEachFileSystem, TestFile} from '../../file_system/testing'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; -import {isNamedClassDeclaration, ReflectionHost, TypeScriptReflectionHost} from '../../reflection'; +import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; -import {UpdateMode} from '../src/api'; +import {ComponentToShimMappingStrategy, UpdateMode} from '../api'; import {ReusedProgramStrategy} from '../src/augmented_program'; -import {ComponentToShimMappingStrategy, PendingFileTypeCheckingData, TypeCheckContext} from '../src/context'; +import {PendingFileTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from '../src/context'; import {TemplateSourceManager} from '../src/source'; import {TypeCheckFile} from '../src/type_check_file'; -import {ALL_ENABLED_CONFIG, NoopOobRecorder} from './test_utils'; +import {ALL_ENABLED_CONFIG} from './test_utils'; runInEachFileSystem(() => { describe('ngtsc typechecking', () => { @@ -72,8 +72,9 @@ TestClass.ngTypeCtor({value: 'test'}); new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); - const ctx = new TypeCheckContext( - ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost); + const ctx = new TypeCheckContextImpl( + ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost, + new TestTypeCheckingHost()); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); const pendingFile = makePendingFile(); @@ -110,8 +111,9 @@ TestClass.ngTypeCtor({value: 'test'}); new LogicalProjectStrategy(reflectionHost, logicalFs), ]); const pendingFile = makePendingFile(); - const ctx = new TypeCheckContext( - ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost); + const ctx = new TypeCheckContextImpl( + ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost, + new TestTypeCheckingHost()); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( @@ -126,7 +128,7 @@ TestClass.ngTypeCtor({value: 'test'}); coercedInputFields: new Set(), }); const programStrategy = new ReusedProgramStrategy(program, host, options, []); - programStrategy.updateFiles(ctx.finalize().updates, UpdateMode.Complete); + programStrategy.updateFiles(ctx.finalize(), UpdateMode.Complete); const TestClassWithCtor = getDeclaration( programStrategy.getProgram(), _('/main.ts'), 'TestClass', isNamedClassDeclaration); const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!; @@ -154,8 +156,9 @@ TestClass.ngTypeCtor({value: 'test'}); new LogicalProjectStrategy(reflectionHost, logicalFs), ]); const pendingFile = makePendingFile(); - const ctx = new TypeCheckContext( - ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost); + const ctx = new TypeCheckContextImpl( + ALL_ENABLED_CONFIG, host, new TestMappingStrategy(), emitter, reflectionHost, + new TestTypeCheckingHost()); const TestClass = getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); ctx.addInlineTypeCtor( @@ -170,7 +173,7 @@ TestClass.ngTypeCtor({value: 'test'}); coercedInputFields: new Set(['bar']), }); const programStrategy = new ReusedProgramStrategy(program, host, options, []); - programStrategy.updateFiles(ctx.finalize().updates, UpdateMode.Complete); + programStrategy.updateFiles(ctx.finalize(), UpdateMode.Complete); const TestClassWithCtor = getDeclaration( programStrategy.getProgram(), _('/main.ts'), 'TestClass', isNamedClassDeclaration); const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!; @@ -194,6 +197,25 @@ function makePendingFile(): PendingFileTypeCheckingData { }; } +class TestTypeCheckingHost implements TypeCheckingHost { + private sourceManager = new TemplateSourceManager(); + + getSourceManager(): TemplateSourceManager { + return this.sourceManager; + } + + shouldCheckComponent(): boolean { + return true; + } + + getTemplateOverride(): null { + return null; + } + recordShimData(): void {} + + recordComplete(): void {} +} + class TestMappingStrategy implements ComponentToShimMappingStrategy { shimPathForComponent(): AbsoluteFsPath { return absoluteFrom('/typecheck.ts'); diff --git a/packages/language-service/ivy/compiler/BUILD.bazel b/packages/language-service/ivy/compiler/BUILD.bazel index bd4910a42e..22c25e3ba1 100644 --- a/packages/language-service/ivy/compiler/BUILD.bazel +++ b/packages/language-service/ivy/compiler/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/incremental", "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/typecheck/api", "@npm//typescript", ], ) diff --git a/packages/language-service/ivy/compiler/compiler.ts b/packages/language-service/ivy/compiler/compiler.ts index 8a50aba578..91de6c4282 100644 --- a/packages/language-service/ivy/compiler/compiler.ts +++ b/packages/language-service/ivy/compiler/compiler.ts @@ -11,7 +11,8 @@ import {CompilerOptions} from '@angular/compiler-cli'; import {NgCompiler, NgCompilerHost} from '@angular/compiler-cli/src/ngtsc/core'; import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {PatchedProgramIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; -import {TypeCheckingProgramStrategy, TypeCheckShimGenerator, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck'; +import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck'; +import {TypeCheckingProgramStrategy, UpdateMode} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import * as ts from 'typescript/lib/tsserverlibrary'; import {makeCompilerHostFromProject} from './compiler_host';