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:
parent
736f6337b2
commit
16c7441c2f
|
@ -90,4 +90,6 @@ class NoIncrementalBuild implements IncrementalBuild<any, any> {
|
|||
priorTypeCheckingResultsFor(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
recordSuccessfulTypeCheck(): void {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>):
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,4 +11,5 @@ import {IncrementalBuild} from '../api';
|
|||
export const NOOP_INCREMENTAL_BUILD: IncrementalBuild<any, any> = {
|
||||
priorWorkFor: () => null,
|
||||
priorTypeCheckingResultsFor: () => null,
|
||||
recordSuccessfulTypeCheck: () => {},
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -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 */ {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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")`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue