refactor(compiler-cli): introduce the TemplateTypeChecker abstraction (#38105)

This commit significantly refactors the 'typecheck' package to introduce a
new abstraction, the `TemplateTypeChecker`. To achieve this:

* a 'typecheck:api' package is introduced, containing common interfaces that
  consumers of the template type-checking infrastructure can depend on
  without incurring a dependency on the template type-checking machinery as
  a whole.
* interfaces for `TemplateTypeChecker` and `TypeCheckContext` are introduced
  which contain the abstract operations supported by the implementation
  classes `TemplateTypeCheckerImpl` and `TypeCheckContextImpl` respectively.
* the `TemplateTypeChecker` interface supports diagnostics on a whole
  program basis to start with, but the implementation is purposefully
  designed to support incremental diagnostics at a per-file or per-component
  level.
* `TemplateTypeChecker` supports direct access to the type check block of a
  component.
* the testing utility is refactored to be a lot more useful, and new tests
  are added for the new abstraction.

PR Close #38105
This commit is contained in:
Alex Rickabaugh 2020-07-13 16:09:46 -07:00 committed by Misko Hevery
parent 736f6337b2
commit 16c7441c2f
43 changed files with 696 additions and 315 deletions

View File

@ -90,4 +90,6 @@ class NoIncrementalBuild implements IncrementalBuild<any, any> {
priorTypeCheckingResultsFor(): null {
return null;
}
recordSuccessfulTypeCheck(): void {}
}

View File

@ -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",

View File

@ -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<ComponentAnalysisData>):

View File

@ -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",
],

View File

@ -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);

View File

@ -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';

View File

@ -27,6 +27,12 @@ export interface IncrementalBuild<AnalysisT, FileTypeCheckDataT> {
* 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<AbsoluteFsPath, FileTypeCheckDataT>): void;
}
/**

View File

@ -11,4 +11,5 @@ import {IncrementalBuild} from '../api';
export const NOOP_INCREMENTAL_BUILD: IncrementalBuild<any, any> = {
priorWorkFor: () => null,
priorTypeCheckingResultsFor: () => null,
recordSuccessfulTypeCheck: () => {},
};

View File

@ -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';

View File

@ -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.

View File

@ -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",
],

View File

@ -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",
],

View File

@ -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 {
/**

View File

@ -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';

View File

@ -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",

View File

@ -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",
],
)

View File

@ -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.

View File

@ -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;
}

View File

@ -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<ClassDeclaration<ts.ClassDeclaration>>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
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;
}

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<AbsoluteFsPath, FileTypeCheckingData>();
export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
private state = new Map<AbsoluteFsPath, FileTypeCheckingData>();
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<ts.CompilerHost, 'getCanonicalFileName'>,
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>) {}
/**
* 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<AbsoluteFsPath, ShimTypeCheckingData>;
/**
* 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;
}
}

View File

@ -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<AbsoluteFsPath, string>;
/**
* Map containing additional data for each type-checking shim that is required to support
* generation of diagnostics.
*/
perFileData: Map<AbsoluteFsPath, FileTypeCheckingData>;
}
/**
* 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<AbsoluteFsPath, ShimTypeCheckingData>;
}
/**
* 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<AbsoluteFsPath, PendingFileTypeCheckingData>();
constructor(
private config: TypeCheckingConfig,
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
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<ts.ClassDeclaration>();
/**
* 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<AbsoluteFsPath, FileTypeCheckingData>();
/**
* 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<ClassDeclaration<ts.ClassDeclaration>>,
boundTarget: BoundTarget<TypeCheckableDirectiveMeta>,
binder: R3TargetBinder<TypeCheckableDirectiveMeta>, template: TmplAstNode[],
pipes: Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>,
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<ClassDeclaration<ts.ClassDeclaration>>;
@ -308,7 +269,7 @@ export class TypeCheckContext {
return code;
}
finalize(): TypeCheckRequest {
finalize(): Map<AbsoluteFsPath, string> {
// First, build the map of updates to source files.
const updates = new Map<AbsoluteFsPath, string>();
for (const originalSf of this.opMap.keys()) {
@ -318,39 +279,23 @@ export class TypeCheckContext {
}
}
const results: TypeCheckRequest = {
updates: updates,
perFileData: new Map<AbsoluteFsPath, FileTypeCheckingData>(),
};
// 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);

View File

@ -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.
*/

View File

@ -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();

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<TemplateId, TemplateSource>();
getTemplateId(node: ts.ClassDeclaration): TemplateId {
return getTemplateId(node);
}
captureSource(node: ts.ClassDeclaration, mapping: TemplateSourceMapping, file: ParseSourceFile):
TemplateId {
const id = getTemplateId(node);

View File

@ -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';
/**

View File

@ -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';

View File

@ -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';

View File

@ -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(

View File

@ -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",
],

View File

@ -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<any>' is not assignable to parameter of type 'string'.`,
`TestComponent.html(1, 30): Argument of type 'TemplateRef<any>' 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<TypeCheckingConfig>,
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;

View File

@ -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.');

View File

@ -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<TypeCheckingConfig>, 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<TypeCheckingConfig>,
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<TypeCheckingConfig> = {}, 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<ts.ClassDeclaration>) {
@ -386,7 +434,7 @@ export function getClass(sf: ts.SourceFile, name: string): ClassDeclaration<ts.C
return stmt;
}
}
throw new Error(`Class ${name} not found in file`);
throw new Error(`Class ${name} not found in file: ${sf.fileName}: ${sf.text}`);
}
class FakeEnvironment /* implements Environment */ {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {TypeCheckingConfig} from '../src/api';
import {TypeCheckingConfig} from '../api';
import {ALL_ENABLED_CONFIG, tcb, TestDeclaration, TestDirective} from './test_utils';

View File

@ -0,0 +1,47 @@
/**
* @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 {absoluteFrom, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {getClass, setup} from './test_utils';
runInEachFileSystem(() => {
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': '<div></div>'}},
{fileName: file2, templates: {'Cmp2': '<span></span>'}}
]);
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': '<div></div>'}},
{fileName: file2, templates: {'Cmp2': '<span></span>'}}
]);
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")`);
});
});
});

View File

@ -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');

View File

@ -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",
],
)

View File

@ -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';