From 153846757a79e9a762d3b651b98ceb2f3fc756c4 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 9 Sep 2025 09:36:54 +0200 Subject: [PATCH] feat(compiler-cli): enable type checking of host bindings by default Type checking of host bindings was added in v20. We're now confident enough in it to enable it by default. BREAKING CHANGE: * Previously hidden type issues in host bindings may show up in your builds. Either resolve the type issues or set `"typeCheckHostBindings": false` in the `angularCompilerOptions` section of your tsconfig. --- .../app/features/update/update.component.ts | 6 +- modules/tsconfig.json | 3 +- .../src/ngtsc/core/src/compiler.ts | 2 +- .../GOLDEN_PARTIAL.js | 6 +- .../r3_view_compiler_bindings/TEST_CASES.json | 33 +++---- .../host_bindings/GOLDEN_PARTIAL.js | 89 ++++++++++++------- .../chain_synthetic_listeners.ts | 17 ++-- .../chain_synthetic_listeners_mixed.ts | 26 +++--- .../host_bindings_with_temporaries.ts | 16 ++-- .../host_bindings/sanitization.js | 19 ++-- .../host_bindings/sanitization.ts | 21 +++-- .../order_bindings.js | 2 +- .../order_bindings.ts | 7 +- .../host_bindings/GOLDEN_PARTIAL.js | 6 +- .../host_bindings/important.ts | 11 ++- .../host_bindings/important_template.js | 6 +- .../ngtsc/host_bindings_type_check_spec.ts | 3 - .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 70 ++++++++------- .../language-service/test/completions_spec.ts | 20 +---- .../language-service/test/definitions_spec.ts | 12 +-- packages/language-service/test/gettcb_spec.ts | 12 +-- .../language-service/test/quick_info_spec.ts | 8 +- .../test/references_and_rename_spec.ts | 4 +- packages/tsconfig.json | 3 +- 24 files changed, 204 insertions(+), 198 deletions(-) diff --git a/adev/src/app/features/update/update.component.ts b/adev/src/app/features/update/update.component.ts index 859823f94696..3ece43fd12b4 100644 --- a/adev/src/app/features/update/update.component.ts +++ b/adev/src/app/features/update/update.component.ts @@ -128,8 +128,10 @@ export default class UpdateComponent { } } - @HostListener('click', ['$event.target']) - copyCode({tagName, textContent}: Element) { + @HostListener('click', ['$event']) + copyCode(event: Event) { + const {tagName, textContent} = event.target as Element; + if (tagName === 'CODE') { this.clipboard.copy(textContent!); this.snackBar.open('Copied to clipboard', '', {duration: 2000}); diff --git a/modules/tsconfig.json b/modules/tsconfig.json index 06843a9c265c..2a4d54e4ad57 100644 --- a/modules/tsconfig.json +++ b/modules/tsconfig.json @@ -29,7 +29,6 @@ ], "angularCompilerOptions": { "strictTemplates": true, - "skipTemplateCodegen": true, - "typeCheckHostBindings": true + "skipTemplateCodegen": true } } diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 6b454d135756..473cfc980766 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -1461,7 +1461,7 @@ export class NgCompiler { const supportJitMode = this.options['supportJitMode'] ?? true; const supportTestBed = this.options['supportTestBed'] ?? true; const externalRuntimeStyles = this.options['externalRuntimeStyles'] ?? false; - const typeCheckHostBindings = this.options.typeCheckHostBindings ?? false; + const typeCheckHostBindings = this.options.typeCheckHostBindings ?? true; // Libraries compiled in partial mode could potentially be used with TestBed within an // application. Since this is not known at library compilation time, support is required to diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/GOLDEN_PARTIAL.js index 7be10608361c..30471a3d347f 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/GOLDEN_PARTIAL.js @@ -18,7 +18,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE export class MyCmp { } MyCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyCmp, deps: [], target: i0.ɵɵFactoryTarget.Component }); -MyCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyCmp, isStandalone: true, selector: "my-cmp", host: { attributes: { "literal1": "foo" }, listeners: { "event1": "foo()" }, properties: { "attr.attr1": "foo", "prop1": "foo", "class.class1": "false", "style.style1": "true", "class": "foo", "style": "foo" } }, ngImport: i0, template: ` +MyCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyCmp, isStandalone: true, selector: "my-cmp", host: { attributes: { "literal1": "foo" }, listeners: { "event1": "foo()" }, properties: { "attr.attr1": "foo", "id": "foo", "class.class1": "false", "style.style1": "true", "class": "foo", "style": "foo" } }, ngImport: i0, template: ` - ` + `, }] }] }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/TEST_CASES.json index 4e6d7d97036b..bc1d30d9ce1a 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/TEST_CASES.json @@ -1,20 +1,15 @@ { - "$schema": "../test_case_schema.json", - "cases": [ - { - "description": "should emit bindings in the correct order", - "inputFiles": [ - "order_bindings.ts" - ], - "expectations": [ - { - "failureMessage": "Invalid binding code", - "files": [ - "order_bindings.js" - ] - } - ] - } - ] - } - \ No newline at end of file + "$schema": "../test_case_schema.json", + "cases": [ + { + "description": "should emit bindings in the correct order", + "inputFiles": ["order_bindings.ts"], + "expectations": [ + { + "failureMessage": "Invalid binding code", + "files": ["order_bindings.js"] + } + ] + } + ] +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js index 86d3d4163abd..f8bc2487c9de 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js @@ -51,14 +51,18 @@ export declare class MyModule { import { Directive, NgModule } from '@angular/core'; import * as i0 from "@angular/core"; export class HostBindingDir { + constructor() { + this.getData = () => undefined; + } } HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: false, selector: "[hostBindingDir]", host: { properties: { "id": "getData()?.id" } }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{ type: Directive, args: [{ - selector: '[hostBindingDir]', host: { '[id]': 'getData()?.id' }, - standalone: false + selector: '[hostBindingDir]', + host: { '[id]': 'getData()?.id' }, + standalone: false, }] }] }); export class MyModule { @@ -76,9 +80,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE ****************************************************************************************************/ import * as i0 from "@angular/core"; export declare class HostBindingDir { - getData?: () => { + getData: () => { id: number; - }; + } | undefined; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵdir: i0.ɵɵDirectiveDeclaration; } @@ -620,8 +624,8 @@ export declare class MyDirective { import { Component, HostListener } from '@angular/core'; import * as i0 from "@angular/core"; export class MyComponent { - start() { - } + start() { } + done() { } } MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: false, selector: "my-comp", host: { listeners: { "@animation.done": "done()", "@animation.start": "start()" } }, ngImport: i0, template: '', isInline: true }); @@ -633,7 +637,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE host: { '(@animation.done)': 'done()', }, - standalone: false + standalone: false, }] }], propDecorators: { start: [{ type: HostListener, @@ -646,6 +650,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE import * as i0 from "@angular/core"; export declare class MyComponent { start(): void; + done(): void; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -656,10 +661,11 @@ export declare class MyComponent { import { Component, HostListener } from '@angular/core'; import * as i0 from "@angular/core"; export class MyComponent { - start() { - } - click() { - } + start() { } + click() { } + mousedown() { } + done() { } + mouseup() { } } MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: false, selector: "my-comp", host: { listeners: { "mousedown": "mousedown()", "@animation.done": "done()", "mouseup": "mouseup()", "@animation.start": "start()", "click": "click()" } }, ngImport: i0, template: '', isInline: true }); @@ -673,7 +679,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE '(@animation.done)': 'done()', '(mouseup)': 'mouseup()', }, - standalone: false + standalone: false, }] }], propDecorators: { start: [{ type: HostListener, @@ -690,6 +696,9 @@ import * as i0 from "@angular/core"; export declare class MyComponent { start(): void; click(): void; + mousedown(): void; + done(): void; + mouseup(): void; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵcmp: i0.ɵɵComponentDeclaration; } @@ -837,40 +846,55 @@ export declare class MyModule { ****************************************************************************************************/ import { Directive } from '@angular/core'; import * as i0 from "@angular/core"; -export class HostBindingDir { +export class HostBindingLinkDir { constructor() { this.evil = 'evil'; } } -HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); -HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { properties: { "innerHtml": "evil", "href": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil" } }, ngImport: i0 }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{ +HostBindingLinkDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingLinkDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +HostBindingLinkDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingLinkDir, isStandalone: true, selector: "a[hostBindingLinkDir]", host: { properties: { "innerHtml": "evil", "href": "evil", "attr.style": "evil" } }, ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingLinkDir, decorators: [{ type: Directive, args: [{ - selector: '[hostBindingDir]', + selector: 'a[hostBindingLinkDir]', host: { '[innerHtml]': 'evil', '[href]': 'evil', '[attr.style]': 'evil', + }, + }] + }] }); +export class HostBindingImageDir { + constructor() { + this.evil = 'evil'; + } +} +HostBindingImageDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingImageDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +HostBindingImageDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingImageDir, isStandalone: true, selector: "img[hostBindingImgDir]", host: { properties: { "innerHtml": "evil", "attr.style": "evil", "src": "evil" } }, ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingImageDir, decorators: [{ + type: Directive, + args: [{ + selector: 'img[hostBindingImgDir]', + host: { + '[innerHtml]': 'evil', + '[attr.style]': 'evil', '[src]': 'evil', - '[sandbox]': 'evil', }, }] }] }); -export class HostBindingDir2 { +export class HostBindingIframeDir { constructor() { this.evil = 'evil'; } } -HostBindingDir2.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, deps: [], target: i0.ɵɵFactoryTarget.Directive }); -HostBindingDir2.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir2, isStandalone: true, selector: "a", host: { properties: { "innerHtml": "evil", "href": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil" } }, ngImport: i0 }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir2, decorators: [{ +HostBindingIframeDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingIframeDir, deps: [], target: i0.ɵɵFactoryTarget.Directive }); +HostBindingIframeDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingIframeDir, isStandalone: true, selector: "iframe[hostBindingIframeDir]", host: { properties: { "innerHtml": "evil", "attr.style": "evil", "src": "evil", "sandbox": "evil" } }, ngImport: i0 }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingIframeDir, decorators: [{ type: Directive, args: [{ - selector: 'a', + selector: 'iframe[hostBindingIframeDir]', host: { '[innerHtml]': 'evil', - '[href]': 'evil', '[attr.style]': 'evil', '[src]': 'evil', '[sandbox]': 'evil', @@ -882,15 +906,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE * PARTIAL FILE: sanitization.d.ts ****************************************************************************************************/ import * as i0 from "@angular/core"; -export declare class HostBindingDir { +export declare class HostBindingLinkDir { evil: string; - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; } -export declare class HostBindingDir2 { +export declare class HostBindingImageDir { evil: string; - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵdir: i0.ɵɵDirectiveDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; +} +export declare class HostBindingIframeDir { + evil: string; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵdir: i0.ɵɵDirectiveDeclaration; } /**************************************************************************************************** diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners.ts index fc4e87ab3b37..eb48b47bff00 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners.ts @@ -1,15 +1,16 @@ import {Component, HostListener} from '@angular/core'; @Component({ - selector: 'my-comp', - template: '', - host: { - '(@animation.done)': 'done()', - }, - standalone: false + selector: 'my-comp', + template: '', + host: { + '(@animation.done)': 'done()', + }, + standalone: false, }) export class MyComponent { @HostListener('@animation.start') - start() { - } + start() {} + + done() {} } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners_mixed.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners_mixed.ts index ee760cfbf78d..9ab3ebb2354f 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners_mixed.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/chain_synthetic_listeners_mixed.ts @@ -1,21 +1,23 @@ import {Component, HostListener} from '@angular/core'; @Component({ - selector: 'my-comp', - template: '', - host: { - '(mousedown)': 'mousedown()', - '(@animation.done)': 'done()', - '(mouseup)': 'mouseup()', - }, - standalone: false + selector: 'my-comp', + template: '', + host: { + '(mousedown)': 'mousedown()', + '(@animation.done)': 'done()', + '(mouseup)': 'mouseup()', + }, + standalone: false, }) export class MyComponent { @HostListener('@animation.start') - start() { - } + start() {} @HostListener('click') - click() { - } + click() {} + + mousedown() {} + done() {} + mouseup() {} } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/host_bindings_with_temporaries.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/host_bindings_with_temporaries.ts index 03d899d1bf44..990aa2c5f0e5 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/host_bindings_with_temporaries.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/host_bindings_with_temporaries.ts @@ -1,15 +1,17 @@ import {Directive, NgModule} from '@angular/core'; @Directive({ - selector: '[hostBindingDir]', host: { '[id]': 'getData()?.id' }, - standalone: false + selector: '[hostBindingDir]', + host: {'[id]': 'getData()?.id'}, + standalone: false, }) export class HostBindingDir { - getData?: () => { - id: number - }; + getData: () => + | { + id: number; + } + | undefined = () => undefined; } @NgModule({declarations: [HostBindingDir]}) -export class MyModule { -} +export class MyModule {} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js index 8af169be19ab..ca216aacac71 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.js @@ -1,13 +1,20 @@ -hostBindings: function HostBindingDir_HostBindings(rf, ctx) { +hostBindings: function HostBindingLinkDir_HostBindings(rf, ctx) { if (rf & 2) { - $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, $r3$.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute); + $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrl); $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle); - } + } } … -hostBindings: function HostBindingDir2_HostBindings(rf, ctx) { +hostBindings: function HostBindingImageDir_HostBindings(rf, ctx) { if (rf & 2) { - $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("href", ctx.evil, $r3$.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute); + $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, $r3$.ɵɵsanitizeUrl); $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle); - } + } +} +… +hostBindings: function HostBindingIframeDir_HostBindings(rf, ctx) { + if (rf & 2) { + $r3$.ɵɵdomProperty("innerHTML", ctx.evil, $r3$.ɵɵsanitizeHtml)("src", ctx.evil, $r3$.ɵɵsanitizeResourceUrl)("sandbox", ctx.evil, $r3$.ɵɵvalidateIframeAttribute); + $r3$.ɵɵattribute("style", ctx.evil, $r3$.ɵɵsanitizeStyle); + } } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts index 4e63f7287d8c..7afa74caf8a1 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/sanitization.ts @@ -1,29 +1,38 @@ import {Directive} from '@angular/core'; @Directive({ - selector: '[hostBindingDir]', + selector: 'a[hostBindingLinkDir]', host: { '[innerHtml]': 'evil', '[href]': 'evil', '[attr.style]': 'evil', + }, +}) +export class HostBindingLinkDir { + evil = 'evil'; +} + +@Directive({ + selector: 'img[hostBindingImgDir]', + host: { + '[innerHtml]': 'evil', + '[attr.style]': 'evil', '[src]': 'evil', - '[sandbox]': 'evil', }, }) -export class HostBindingDir { +export class HostBindingImageDir { evil = 'evil'; } @Directive({ - selector: 'a', + selector: 'iframe[hostBindingIframeDir]', host: { '[innerHtml]': 'evil', - '[href]': 'evil', '[attr.style]': 'evil', '[src]': 'evil', '[sandbox]': 'evil', }, }) -export class HostBindingDir2 { +export class HostBindingIframeDir { evil = 'evil'; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.js index e6f46855999e..5f9d09436666 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.js @@ -5,7 +5,7 @@ function MyCmp_HostBindings(rf, ctx) { $r3$.ɵɵlistener("event1", function MyCmp_event1_HostBindingHandler() { return ctx.foo(); }); } if (rf & 2) { - $r3$.ɵɵdomProperty("prop1", ctx.foo); + $r3$.ɵɵdomProperty("id", ctx.foo); $r3$.ɵɵattribute("attr1", ctx.foo); $r3$.ɵɵstyleMap(ctx.foo); $r3$.ɵɵclassMap(ctx.foo); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.ts index 5129ae86b742..544261691bbb 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.ts @@ -5,8 +5,7 @@ import {Component} from '@angular/core'; template: ``, inputs: ['attr1', 'prop1', 'attrInterp1', 'propInterp1'], }) -export class SomeCmp { -} +export class SomeCmp {} @Component({ selector: 'my-cmp', @@ -15,7 +14,7 @@ export class SomeCmp { 'literal1': 'foo', '(event1)': 'foo()', '[attr.attr1]': 'foo', - '[prop1]': 'foo', + '[id]': 'foo', '[class.class1]': 'false', '[style.style1]': 'true', '[class]': 'foo', @@ -34,7 +33,7 @@ export class SomeCmp { attr.attrInterp1="interp {{foo}}" propInterp1="interp {{foo}}" /> - ` + `, }) export class MyCmp { foo: any; diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/GOLDEN_PARTIAL.js index ab139dc5bde4..f60c678473b5 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/GOLDEN_PARTIAL.js @@ -153,7 +153,7 @@ export class MyComponent { } } MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); -MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: false, selector: "my-component", host: { properties: { "style!important": "myStyleExp", "class!important": "myClassExp", "class.foo!important": "this.myFooClassExp", "style.width!important": "this.myWidthExp" } }, ngImport: i0, template: ` +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: false, selector: "my-component", host: { properties: { "style.width!important": "this.myWidthExp", "class.baz!important": "myClassExp", "class.foo!important": "this.myFooClassExp" } }, ngImport: i0, template: `
`, isInline: true }); @@ -165,8 +165,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
`, - host: { '[style!important]': 'myStyleExp', '[class!important]': 'myClassExp' }, - standalone: false + host: { '[style.width!important]': 'myStyleExp', '[class.baz!important]': 'myClassExp' }, + standalone: false, }] }], propDecorators: { myFooClassExp: [{ type: HostBinding, diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important.ts index f1c5f4cf987e..43a8bcdc7c0e 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important.ts @@ -1,13 +1,13 @@ import {Component, HostBinding, NgModule} from '@angular/core'; @Component({ - selector: 'my-component', - template: ` + selector: 'my-component', + template: `
`, - host: { '[style!important]': 'myStyleExp', '[class!important]': 'myClassExp' }, - standalone: false + host: {'[style.width!important]': 'myStyleExp', '[class.baz!important]': 'myClassExp'}, + standalone: false, }) export class MyComponent { myStyleExp = ''; @@ -22,5 +22,4 @@ export class MyComponent { } @NgModule({declarations: [MyComponent]}) -export class MyModule { -} +export class MyModule {} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important_template.js index 3e6efb395c5b..b6463c99c559 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/host_bindings/important_template.js @@ -1,10 +1,8 @@ -hostVars: 8, +hostVars: 6, hostBindings: function MyComponent_HostBindings(rf, ctx) { if (rf & 2) { - $r3$.ɵɵstyleMap(ctx.myStyleExp); - $r3$.ɵɵclassMap(ctx.myClassExp); $r3$.ɵɵstyleProp("width", ctx.myWidthExp); - $r3$.ɵɵclassProp("foo", ctx.myFooClassExp); + $r3$.ɵɵclassProp("baz", ctx.myClassExp)("foo", ctx.myFooClassExp); } }, diff --git a/packages/compiler-cli/test/ngtsc/host_bindings_type_check_spec.ts b/packages/compiler-cli/test/ngtsc/host_bindings_type_check_spec.ts index d1e76b91a069..d1aa2a8797af 100644 --- a/packages/compiler-cli/test/ngtsc/host_bindings_type_check_spec.ts +++ b/packages/compiler-cli/test/ngtsc/host_bindings_type_check_spec.ts @@ -24,9 +24,6 @@ runInEachFileSystem(() => { beforeEach(() => { env = NgtscTestEnvironment.setup(testFiles); env.tsconfig({ - // Necessary for testing host bindings. - typeCheckHostBindings: true, - // Not required for host bindings, but they allow us to // exercise more parts of the type checker. strictTemplates: true, diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 6937bdb921c1..5b079aff995e 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -5340,13 +5340,13 @@ runInEachFileSystem((os: string) => { }) class FooCmp { @HostListener('click') - onClick(event: any): void {} + onClick(): void {} @HostListener('document:click', ['$event.target']) - onDocumentClick(eventTarget: HTMLElement): void {} + onDocumentClick(eventTarget: EventTarget | null): void {} @HostListener('window:scroll') - onWindowScroll(event: any): void {} + onWindowScroll(): void {} } `, ); @@ -5375,7 +5375,7 @@ runInEachFileSystem((os: string) => { }) class FooCmp { @HostListener('UnknownTarget:click') - onClick(event: any): void {} + onClick(): void {} } `, ); @@ -5466,17 +5466,24 @@ runInEachFileSystem((os: string) => { '[attr.hello]': 'foo', '(click)': 'onClick($event)', '(body:click)': 'onBodyClick($event)', - '[prop]': 'bar', + '[id]': 'bar', }, }) class FooCmp { + arg1: any; + arg2: any; + arg3: any; + foo: any; + bar: any; + onClick(event: any): void {} + onBodyClick(event: any): void {} @HostBinding('class.someclass') get someClass(): boolean { return false; } - @HostListener('change', ['arg1', 'arg2', 'arg3']) - onChange(event: any, arg: any): void {} + @HostListener('change', ['$event', 'arg1', 'arg2', 'arg3']) + onChange(event: any, arg1: any, arg2: any, arg3: any): void {} } `, ); @@ -5487,10 +5494,10 @@ runInEachFileSystem((os: string) => { hostVars: 4, hostBindings: function FooCmp_HostBindings(rf, ctx) { if (rf & 1) { - i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler() { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange($event, ctx.arg1, ctx.arg2, ctx.arg3); }); } if (rf & 2) { - i0.ɵɵdomProperty("prop", ctx.bar); + i0.ɵɵdomProperty("id", ctx.bar); i0.ɵɵattribute("hello", ctx.foo); i0.ɵɵclassProp("someclass", ctx.someClass); } @@ -5608,6 +5615,8 @@ runInEachFileSystem((os: string) => { selector: '[test]', }) class Dir { + arg: any; + @HostListener('change', ['$event', 'arg']) onChange(event: any, arg: any): void {} } @@ -8471,29 +8480,19 @@ runInEachFileSystem((os: string) => { expect(trim(jsContents)).toContain(trim(hostBindingsFn)); }); - it('should generate sanitizers for unsafe properties in hostBindings fn in Directives', () => { + it('should generate sanitizers for unsafe properties in hostBindings function in Directives', () => { env.write( `test.ts`, ` - import {Component, Directive, HostBinding, Input, NgModule} from '@angular/core'; + import {Component, Directive, HostBinding, Input} from '@angular/core'; @Directive({ - selector: '[unsafeProps]', - standalone: false, + selector: 'a[unsafeProps]', }) class UnsafePropsDirective { @HostBinding('href') propHref: string; - @HostBinding('src') - propSrc: string; - - @HostBinding('action') - propAction: string; - - @HostBinding('profile') - propProfile: string; - @HostBinding('innerHTML') propInnerHTML: string; @@ -8506,24 +8505,21 @@ runInEachFileSystem((os: string) => { @Component({ selector: 'foo', template: 'Link Title', - standalone: false, + imports: [UnsafePropsDirective] }) class FooCmp { ctxProp = ''; } - - @NgModule({declarations: [FooCmp, UnsafePropsDirective]}) - class MyModule {} `, ); env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` - hostVars: 6, + hostVars: 3, hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx) { if (rf & 2) { - i0.ɵɵdomProperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.propAction, i0.ɵɵsanitizeUrl)("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle); + i0.ɵɵdomProperty("href", ctx.propHref, i0.ɵɵsanitizeUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle); } } `; @@ -8537,10 +8533,9 @@ runInEachFileSystem((os: string) => { import {Component} from '@angular/core'; @Component({ - selector: 'foo', + selector: 'a[foo]', template: 'Link Title', host: { - '[src]': 'srcProp', '[href]': 'hrefProp', '[title]': 'titleProp', '[attr.src]': 'srcAttr', @@ -8548,18 +8543,24 @@ runInEachFileSystem((os: string) => { '[attr.title]': 'titleAttr', } }) - class FooCmp {} + class FooCmp { + hrefProp: any; + titleProp: any; + srcAttr: any; + hrefAttr: any; + titleAttr: any; + } `, ); env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` - hostVars: 6, + hostVars: 5, hostBindings: function FooCmp_HostBindings(rf, ctx) { if (rf & 2) { - i0.ɵɵdomProperty("src", ctx.srcProp)("href", ctx.hrefProp)("title", ctx.titleProp); - i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr)("title", ctx.titleAttr); + i0.ɵɵdomProperty("href", ctx.hrefProp, i0.ɵɵsanitizeUrl)("title", ctx.titleProp); + i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr, i0.ɵɵsanitizeUrl)("title", ctx.titleAttr); } } `; @@ -9759,6 +9760,7 @@ runInEachFileSystem((os: string) => { import {Directive} from '@angular/core'; @Directive({ + selector: 'iframe[someDir]', host: { '[sandbox]': "''", '[attr.allow]': "''", diff --git a/packages/language-service/test/completions_spec.ts b/packages/language-service/test/completions_spec.ts index 9bfccf57690e..7710d31f5f75 100644 --- a/packages/language-service/test/completions_spec.ts +++ b/packages/language-service/test/completions_spec.ts @@ -2157,9 +2157,6 @@ describe('completions', () => { `title!: string; hero!: number;`, undefined, `host: {'[title]': 'ti'},`, - { - typeCheckHostBindings: true, - }, ); appFile.moveCursorToText(`'ti¦'`); const completions = appFile.getCompletionsAtPosition(); @@ -2172,9 +2169,6 @@ describe('completions', () => { `title!: string; hero!: number;`, undefined, `host: {'(click)': 't'},`, - { - typeCheckHostBindings: true, - }, ); appFile.moveCursorToText(`'(click)': 't¦'`); const completions = appFile.getCompletionsAtPosition(); @@ -2182,11 +2176,8 @@ describe('completions', () => { }); it('should be able to complete inside `host` of a directive', () => { - const {appFile} = setupInlineTemplate( - '', - '', - { - 'Dir': ` + const {appFile} = setupInlineTemplate('', '', { + 'Dir': ` @Directive({ host: {'[title]': 'ti'}, }) @@ -2195,12 +2186,7 @@ describe('completions', () => { hero!: number; } `, - }, - undefined, - { - typeCheckHostBindings: true, - }, - ); + }); appFile.moveCursorToText(`'ti¦'`); const completions = appFile.getCompletionsAtPosition(); expectContain(completions, ts.ScriptElementKind.memberVariableElement, ['title', 'hero']); diff --git a/packages/language-service/test/definitions_spec.ts b/packages/language-service/test/definitions_spec.ts index 130c9182add5..81bb653674fa 100644 --- a/packages/language-service/test/definitions_spec.ts +++ b/packages/language-service/test/definitions_spec.ts @@ -834,9 +834,7 @@ describe('definitions', () => { }; const env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); project.expectNoSourceDiagnostics(); @@ -868,9 +866,7 @@ describe('definitions', () => { }; const env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); project.expectNoSourceDiagnostics(); @@ -901,9 +897,7 @@ describe('definitions', () => { }; const env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); const dirFile = project.openFile('dir.ts'); project.expectNoSourceDiagnostics(); diff --git a/packages/language-service/test/gettcb_spec.ts b/packages/language-service/test/gettcb_spec.ts index 76bdebbb1f59..9411178c0c15 100644 --- a/packages/language-service/test/gettcb_spec.ts +++ b/packages/language-service/test/gettcb_spec.ts @@ -93,9 +93,7 @@ describe('get typecheck block', () => { }`, }; const env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); project.expectNoSourceDiagnostics(); const appFile = project.openFile('app.ts'); @@ -129,9 +127,7 @@ describe('get typecheck block', () => { }`, }; const env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); project.expectNoSourceDiagnostics(); const appFile = project.openFile('app.ts'); @@ -165,9 +161,7 @@ describe('get typecheck block', () => { }`, }; const env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); project.expectNoSourceDiagnostics(); const appFile = project.openFile('app.ts'); diff --git a/packages/language-service/test/quick_info_spec.ts b/packages/language-service/test/quick_info_spec.ts index 7dfd2054efe6..fead6e2af1e0 100644 --- a/packages/language-service/test/quick_info_spec.ts +++ b/packages/language-service/test/quick_info_spec.ts @@ -951,13 +951,7 @@ describe('quick info', () => { expectedDisplayString: string; expectedSpanText: string; }) { - const project = env.addProject( - 'host-bindings', - {'host-bindings.ts': source}, - { - typeCheckHostBindings: true, - }, - ); + const project = env.addProject('host-bindings', {'host-bindings.ts': source}); const appFile = project.openFile('host-bindings.ts'); appFile.moveCursorToText(moveTo); diff --git a/packages/language-service/test/references_and_rename_spec.ts b/packages/language-service/test/references_and_rename_spec.ts index b5f7eda3a9c2..e54aaa28c11c 100644 --- a/packages/language-service/test/references_and_rename_spec.ts +++ b/packages/language-service/test/references_and_rename_spec.ts @@ -1540,9 +1540,7 @@ describe('find references and rename locations', () => { `, }; env = LanguageServiceTestEnv.setup(); - const project = createModuleAndProjectWithDeclarations(env, 'test', files, { - typeCheckHostBindings: true, - }); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); appFile = project.openFile('app.ts'); }); diff --git a/packages/tsconfig.json b/packages/tsconfig.json index da86abb88502..46955a117ca3 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -37,8 +37,7 @@ "suppressTsconfigOverrideWarnings": true }, "angularCompilerOptions": { - "strictTemplates": true, - "typeCheckHostBindings": true + "strictTemplates": true }, "exclude": [ "common/locales",