diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index c4a6637d91..48857f59b7 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -423,6 +423,7 @@ export class NgCompiler { applyTemplateContextGuards: strictTemplates, checkQueries: false, checkTemplateBodies: true, + alwaysCheckSchemaInTemplateBodies: true, checkTypeOfInputBindings: strictTemplates, honorAccessModifiersForInputBindings: false, strictNullInputBindings: strictTemplates, @@ -451,6 +452,9 @@ export class NgCompiler { applyTemplateContextGuards: false, checkQueries: false, checkTemplateBodies: false, + // Enable deep schema checking in "basic" template type-checking mode only if Closure + // compilation is requested, which is a good proxy for "only in google3". + alwaysCheckSchemaInTemplateBodies: this.closureCompilerEnabled, checkTypeOfInputBindings: false, strictNullInputBindings: false, honorAccessModifiersForInputBindings: false, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index e9b3a609d1..2ff9d963df 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -230,6 +230,12 @@ export interface TypeCheckingConfig { */ checkTemplateBodies: boolean; + /** + * Whether to always apply DOM schema checks in template bodies, independently of the + * `checkTemplateBodies` setting. + */ + alwaysCheckSchemaInTemplateBodies: boolean; + /** * Whether to check resolvable queries. * diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index dbe40a0925..1bc4af9798 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -1298,6 +1298,8 @@ class Scope { this.templateCtxOpMap.set(node, ctxIndex); if (this.tcb.env.config.checkTemplateBodies) { this.opQueue.push(new TcbTemplateBodyOp(this.tcb, this, node)); + } else if (this.tcb.env.config.alwaysCheckSchemaInTemplateBodies) { + this.appendDeepSchemaChecks(node.children); } this.checkAndAppendReferencesOfNode(node); } else if (node instanceof TmplAstBoundText) { @@ -1401,6 +1403,33 @@ class Scope { this.opQueue.push(new TcbUnclaimedOutputsOp(this.tcb, this, node, claimedOutputs)); } } + + private appendDeepSchemaChecks(nodes: TmplAstNode[]): void { + for (const node of nodes) { + if (!(node instanceof TmplAstElement || node instanceof TmplAstTemplate)) { + continue; + } + + if (node instanceof TmplAstElement) { + const claimedInputs = new Set(); + const directives = this.tcb.boundTarget.getDirectivesOfNode(node); + let hasDirectives: boolean; + if (directives === null || directives.length === 0) { + hasDirectives = false; + } else { + hasDirectives = true; + for (const dir of directives) { + for (const propertyName of dir.inputs.propertyNames) { + claimedInputs.add(propertyName); + } + } + } + this.opQueue.push(new TcbDomSchemaCheckerOp(this.tcb, node, !hasDirectives, claimedInputs)); + } + + this.appendDeepSchemaChecks(node.children); + } + } } interface TcbBoundInput { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index 253e5b8193..b756e306c9 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -167,6 +167,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = { applyTemplateContextGuards: true, checkQueries: false, checkTemplateBodies: true, + alwaysCheckSchemaInTemplateBodies: true, checkTypeOfInputBindings: true, honorAccessModifiersForInputBindings: true, strictNullInputBindings: true, @@ -238,6 +239,7 @@ export function tcb( checkTypeOfNonDomReferences: true, checkTypeOfPipes: true, checkTemplateBodies: true, + alwaysCheckSchemaInTemplateBodies: true, strictSafeNavigationTypes: true, useContextGenericType: true, strictLiteralTypes: true, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 023503fb74..886a78f958 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -700,6 +700,7 @@ describe('type check blocks', () => { applyTemplateContextGuards: true, checkQueries: false, checkTemplateBodies: true, + alwaysCheckSchemaInTemplateBodies: true, checkTypeOfInputBindings: true, honorAccessModifiersForInputBindings: false, strictNullInputBindings: true, diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 3fc856389f..193373668b 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -390,6 +390,43 @@ runInEachFileSystem(os => { expect(jsContents).toContain('/** @nocollapse */ TestCmp.ɵcmp'); }); + it('should still perform schema checks in embedded views', () => { + env.tsconfig({ + 'fullTemplateTypeCheck': false, + 'annotateForClosureCompiler': true, + 'ivyTemplateTypeCheck': true, + }); + env.write('test.ts', ` + import {Component, Directive, NgModule} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: \` + + Has a directive, should be okay + Should trigger a schema error + + \` + }) + export class TestCmp {} + + @Directive({ + selector: 'some-dir', + }) + export class TestDir {} + + @NgModule({ + declarations: [TestCmp, TestDir], + }) + export class TestModule {} + `); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.SCHEMA_INVALID_ELEMENT)); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) + .toContain('not-a-cmp'); + }); /** * The following set of tests verify that after Tsickle run we do not have cases * which trigger automatic semicolon insertion, which breaks the code. In order