From 19c4e705ffcbffe67701ff5154e38db1d59d87db Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh <alx+alxhub@alxandria.net> 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<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, 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<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), }); } 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<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); 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<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; } /** 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<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.