From 19c4e705ffcbffe67701ff5154e38db1d59d87db Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 21 Sep 2018 14:03:55 -0700 Subject: [PATCH] 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 --- packages/compiler-cli/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/component.ts | 68 +++++++++++++------ .../src/ngtsc/annotations/src/directive.ts | 10 ++- .../src/ngtsc/annotations/src/util.ts | 19 +++++- packages/compiler-cli/src/ngtsc/program.ts | 20 +++++- .../src/ngtsc/transform/BUILD.bazel | 1 + .../src/ngtsc/transform/src/api.ts | 4 ++ .../src/ngtsc/transform/src/compilation.ts | 14 ++++ 8 files changed, 111 insertions(+), 26 deletions(-) diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index d88a524840..6b63722291 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -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", ], ) diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 0b76cc9ec1..2ea9bb325c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -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(); +export interface ComponentHandlerData { + meta: R3ComponentMetadata; + parsedTemplate: TmplAstNode[]; +} + /** * `DecoratorHandler` which handles the `@Component` annotation. */ -export class ComponentDecoratorHandler implements DecoratorHandler { +export class ComponentDecoratorHandler implements + DecoratorHandler { constructor( private checker: ts.TypeChecker, private reflector: ReflectionHost, private scopeRegistry: SelectorScopeRegistry, private isCore: boolean, @@ -59,7 +66,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler { + analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { const meta = this._resolveLiteral(decorator); this.literalCache.delete(decorator); @@ -134,13 +141,17 @@ export class ComponentDecoratorHandler implements DecoratorHandler query.propertyName), - isComponent: true, + isComponent: true, ...extractDirectiveGuards(node, this.reflector), }); } @@ -181,26 +192,41 @@ export class ComponentDecoratorHandler implements DecoratorHandler>(); + 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(); 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, diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index dea96ccaa7..b47568b7f1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -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 query.propertyName), - isComponent: false, + isComponent: false, ...extractDirectiveGuards(node, this.reflector), }); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 68cde9b25f..427fffa52f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -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); +} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 8d311901d7..e3e35d65f6 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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 { 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 { @@ -183,6 +190,17 @@ export class NgtscProgram implements api.Program { return emitResult; } + private compileTypeCheckProgram(ctx: TypeCheckContext): ReadonlyArray { + const host = new TypeCheckProgramHost(this.tsProgram, this.host, ctx); + const auxProgram = ts.createProgram({ + host, + rootNames: this.tsProgram.getRootFileNames(), + oldProgram: this.tsProgram, + options: this.options, + }); + return auxProgram.getSemanticDiagnostics(); + } + private makeCompilation(): IvyCompilation { const checker = this.tsProgram.getTypeChecker(); const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector); diff --git a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel index 74a7ef8b0c..f1851cddb0 100644 --- a/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/transform/BUILD.bazel @@ -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", ], ) diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index e4d995583c..59a3182350 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -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 { */ analyze(node: ts.Declaration, metadata: M): AnalysisOutput; + 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 { analysis?: A; diagnostics?: ts.Diagnostic[]; factorySymbolName?: string; + typeCheck?: boolean; } /** diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index e8f7ca2ec0..f42e828a43 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -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>(); + private typeCheckMap = new Map>(); /** * 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.