feat(ivy): turn on template type-checking via fullTemplateTypeCheck (#26203)

This commit enables generation and checking of a type checking ts.Program
whenever the fullTemplateTypeCheck flag is enabled in tsconfig.json. It
puts together all the pieces built previously and causes diagnostics to be
emitted whenever type errors are discovered in a template.

Todos:

* map errors back to template HTML
* expand set of type errors covered in generated type-check blocks

PR Close #26203
This commit is contained in:
Alex Rickabaugh 2018-09-21 14:03:55 -07:00 committed by Jason Aden
parent 36d6e6076e
commit 19c4e705ff
8 changed files with 111 additions and 26 deletions

View File

@ -30,6 +30,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/factories",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck",
],
)

View File

@ -6,26 +6,33 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import {ConstantPool, CssSelector, Expression, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Decorator, ReflectionHost} from '../../host';
import {filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AbsoluteReference, Reference, ResolvedReference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {TypeCheckContext, TypeCheckableDirectiveMeta} from '../../typecheck';
import {ResourceLoader} from './api';
import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive';
import {SelectorScopeRegistry} from './selector_scope';
import {isAngularCore, unwrapExpression} from './util';
import {ScopeDirective, SelectorScopeRegistry} from './selector_scope';
import {extractDirectiveGuards, isAngularCore, unwrapExpression} from './util';
const EMPTY_MAP = new Map<string, Expression>();
export interface ComponentHandlerData {
meta: R3ComponentMetadata;
parsedTemplate: TmplAstNode[];
}
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata, Decorator> {
export class ComponentDecoratorHandler implements
DecoratorHandler<ComponentHandlerData, Decorator> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
@ -59,7 +66,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
return undefined;
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<ComponentHandlerData> {
const meta = this._resolveLiteral(decorator);
this.literalCache.delete(decorator);
@ -134,13 +141,17 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this component appears in an `@NgModule` scope, its selector can be determined.
if (metadata.selector !== null) {
const ref = new ResolvedReference(node, node.name !);
this.scopeRegistry.registerDirective(node, {
ref,
name: node.name !.text,
directive: ref,
selector: metadata.selector,
exportAs: metadata.exportAs,
inputs: metadata.inputs,
outputs: metadata.outputs,
queries: metadata.queries.map(query => query.propertyName),
isComponent: true,
isComponent: true, ...extractDirectiveGuards(node, this.reflector),
});
}
@ -181,26 +192,41 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
return {
analysis: {
...metadata,
template,
viewQueries,
encapsulation,
styles: styles || [],
meta: {
...metadata,
template,
viewQueries,
encapsulation,
styles: styles || [],
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
pipes: EMPTY_MAP,
directives: EMPTY_MAP,
wrapDirectivesInClosure: false, animations,
}
// These will be replaced during the compilation step, after all `NgModule`s have been
// analyzed and the full compilation scope for the component can be realized.
pipes: EMPTY_MAP,
directives: EMPTY_MAP,
wrapDirectivesInClosure: false, animations,
},
parsedTemplate: template.nodes,
},
typeCheck: true,
};
}
compile(node: ts.ClassDeclaration, analysis: R3ComponentMetadata, pool: ConstantPool):
typeCheck(ctx: TypeCheckContext, node: ts.Declaration, meta: ComponentHandlerData): void {
const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(node);
const matcher = new SelectorMatcher<ScopeDirective<any>>();
if (scope !== null) {
scope.directives.forEach(
(meta, selector) => { matcher.addSelectables(CssSelector.parse(selector), meta); });
ctx.addTemplate(node as ts.ClassDeclaration, meta.parsedTemplate, matcher);
}
}
compile(node: ts.ClassDeclaration, analysis: ComponentHandlerData, pool: ConstantPool):
CompileResult {
// Check whether this component was registered with an NgModule. If so, it should be compiled
// under that module's compilation scope.
const scope = this.scopeRegistry.lookupCompilationScope(node);
let metadata = analysis.meta;
if (scope !== null) {
// Replace the empty components and directives from the analyze() step with a fully expanded
// scope. This is possible now because during compile() the whole compilation unit has been
@ -209,10 +235,10 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
const directives = new Map<string, Expression>();
scope.directives.forEach((meta, selector) => directives.set(selector, meta.directive));
const wrapDirectivesInClosure: boolean = !!containsForwardDecls;
analysis = {...analysis, directives, pipes, wrapDirectivesInClosure};
metadata = {...metadata, directives, pipes, wrapDirectivesInClosure};
}
const res = compileComponentFromMetadata(analysis, pool, makeBindingParser());
const res = compileComponentFromMetadata(metadata, pool, makeBindingParser());
return {
name: 'ngComponentDef',
initializer: res.expression,

View File

@ -11,11 +11,11 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
import {Reference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {Reference, ResolvedReference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
import {extractDirectiveGuards, getConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
@ -40,13 +40,17 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
if (analysis && analysis.selector !== null) {
let ref = new ResolvedReference(node, node.name !);
this.scopeRegistry.registerDirective(node, {
ref,
directive: ref,
name: node.name !.text,
selector: analysis.selector,
exportAs: analysis.exportAs,
inputs: analysis.inputs,
outputs: analysis.outputs,
queries: analysis.queries.map(query => query.propertyName),
isComponent: false,
isComponent: false, ...extractDirectiveGuards(node, this.reflector),
});
}

View File

@ -10,7 +10,7 @@ import {Expression, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType,
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Decorator, ReflectionHost} from '../../host';
import {ClassMemberKind, Decorator, ReflectionHost} from '../../host';
import {AbsoluteReference, ImportMode, Reference} from '../../metadata';
export function getConstructorDependencies(
@ -176,3 +176,20 @@ export function forwardRefResolver(
}
return expandForwardRef(args[0]);
}
export function extractDirectiveGuards(node: ts.Declaration, reflector: ReflectionHost): {
ngTemplateGuards: string[],
hasNgTemplateContextGuard: boolean,
} {
const methods = nodeStaticMethodNames(node, reflector);
const ngTemplateGuards = methods.filter(method => method.startsWith('ngTemplateGuard_'))
.map(method => method.split('_', 2)[1]);
const hasNgTemplateContextGuard = methods.some(name => name === 'ngTemplateContextGuard');
return {hasNgTemplateContextGuard, ngTemplateGuards};
}
function nodeStaticMethodNames(node: ts.Declaration, reflector: ReflectionHost): string[] {
return reflector.getMembersOfClass(node)
.filter(member => member.kind === ClassMemberKind.Method && member.isStatic)
.map(member => member.name);
}

View File

@ -19,6 +19,7 @@ import {FactoryGenerator, FactoryInfo, GeneratedFactoryHostWrapper, generatedFac
import {TypeScriptReflectionHost} from './metadata';
import {FileResourceLoader, HostResourceLoader} from './resource_loader';
import {IvyCompilation, ivyTransformFactory} from './transform';
import {TypeCheckContext, TypeCheckProgramHost} from './typecheck';
export class NgtscProgram implements api.Program {
private tsProgram: ts.Program;
@ -103,7 +104,13 @@ export class NgtscProgram implements api.Program {
fileName?: string|undefined, cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic|api.Diagnostic> {
const compilation = this.ensureAnalyzed();
return compilation.diagnostics;
const diagnostics = [...compilation.diagnostics];
if (!!this.options.fullTemplateTypeCheck) {
const ctx = new TypeCheckContext();
compilation.typeCheck(ctx);
diagnostics.push(...this.compileTypeCheckProgram(ctx));
}
return diagnostics;
}
async loadNgStructureAsync(): Promise<void> {
@ -183,6 +190,17 @@ export class NgtscProgram implements api.Program {
return emitResult;
}
private compileTypeCheckProgram(ctx: TypeCheckContext): ReadonlyArray<ts.Diagnostic> {
const host = new TypeCheckProgramHost(this.tsProgram, this.host, ctx);
const auxProgram = ts.createProgram({
host,
rootNames: this.tsProgram.getRootFileNames(),
oldProgram: this.tsProgram,
options: this.options,
});
return auxProgram.getSemanticDiagnostics();
}
private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker();
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector);

View File

@ -15,6 +15,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
],
)

View File

@ -10,6 +10,7 @@ import {ConstantPool, Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../host';
import {TypeCheckContext} from '../../typecheck';
/**
@ -43,6 +44,8 @@ export interface DecoratorHandler<A, M> {
*/
analyze(node: ts.Declaration, metadata: M): AnalysisOutput<A>;
typeCheck?(ctx: TypeCheckContext, node: ts.Declaration, metadata: A): void;
/**
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
@ -60,6 +63,7 @@ export interface AnalysisOutput<A> {
analysis?: A;
diagnostics?: ts.Diagnostic[];
factorySymbolName?: string;
typeCheck?: boolean;
}
/**

View File

@ -12,10 +12,12 @@ import * as ts from 'typescript';
import {FatalDiagnosticError} from '../../diagnostics';
import {Decorator, ReflectionHost} from '../../host';
import {reflectNameOfDeclaration} from '../../metadata/src/reflector';
import {TypeCheckContext} from '../../typecheck';
import {AnalysisOutput, CompileResult, DecoratorHandler} from './api';
import {DtsFileTransformer} from './declaration';
/**
* Record of an adapter which decided to emit a static field, and the analysis it performed to
* prepare for that operation.
@ -38,6 +40,7 @@ export class IvyCompilation {
* information recorded about them for later compilation.
*/
private analysis = new Map<ts.Declaration, EmitFieldOperation<any, any>>();
private typeCheckMap = new Map<ts.Declaration, DecoratorHandler<any, any>>();
/**
* Tracks factory information which needs to be generated.
@ -107,6 +110,9 @@ export class IvyCompilation {
analysis: analysis.analysis,
metadata: metadata,
});
if (!!analysis.typeCheck) {
this.typeCheckMap.set(node, adapter);
}
}
if (analysis.diagnostics !== undefined) {
@ -156,6 +162,14 @@ export class IvyCompilation {
}
}
typeCheck(context: TypeCheckContext): void {
this.typeCheckMap.forEach((handler, node) => {
if (handler.typeCheck !== undefined) {
handler.typeCheck(context, node, this.analysis.get(node) !.analysis);
}
});
}
/**
* Perform a compilation operation on the given class declaration and return instructions to an
* AST transformer if any are available.