diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index ab929dc38..3034eedd3 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -1553,19 +1553,26 @@ pub(crate) fn extract_host_bindings_in<'a>( let mut bindings = Vec::new_in(&allocator); for element in &class.body.body { - let (decorators, property_name) = match element { + let (decorators, property_name, is_static) = match element { ClassElement::PropertyDefinition(prop) => { - (&prop.decorators, get_property_key_name(&prop.key)) + (&prop.decorators, get_property_key_name(&prop.key), prop.r#static) } ClassElement::MethodDefinition(method) => { - (&method.decorators, get_property_key_name(&method.key)) + (&method.decorators, get_property_key_name(&method.key), method.r#static) } ClassElement::AccessorProperty(prop) => { - (&prop.decorators, get_property_key_name(&prop.key)) + (&prop.decorators, get_property_key_name(&prop.key), prop.r#static) } _ => continue, }; + // ngtsc's `filterToMembersWithDecorator` ignores static members: a + // `@HostBinding()` on one is not a host binding. Private (`#x`) members + // can't carry decorators, and `get_property_key_name` skips them anyway. + if is_static { + continue; + } + let Some(decorator) = find_decorator_by_name(decorators, "HostBinding", consts) else { continue; }; @@ -1636,16 +1643,22 @@ pub(crate) fn extract_host_listeners_in<'a>( for element in &class.body.body { // Handle both MethodDefinition and PropertyDefinition (for arrow function handlers) - let (decorators, property_name) = match element { + let (decorators, property_name, is_static) = match element { ClassElement::MethodDefinition(method) => { - (&method.decorators, get_property_key_name(&method.key)) + (&method.decorators, get_property_key_name(&method.key), method.r#static) } ClassElement::PropertyDefinition(prop) => { - (&prop.decorators, get_property_key_name(&prop.key)) + (&prop.decorators, get_property_key_name(&prop.key), prop.r#static) } _ => continue, }; + // ngtsc's `filterToMembersWithDecorator` ignores static members: a + // `@HostListener()` on one is not a host listener. + if is_static { + continue; + } + let Some(decorator) = find_decorator_by_name(decorators, "HostListener", consts) else { continue; }; diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 88e797959..55d898b4b 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -6137,6 +6137,55 @@ export class TestComponent { ); } +/// @HostBinding / @HostListener on `static` members are not host bindings: +/// ngtsc's `filterToMembersWithDecorator` filters `!member.isStatic` before +/// collecting them (typescript.ts). They must not appear in `hostBindings`, +/// `hostVars`, or `propDecorators`. +#[test] +fn test_static_host_binding_and_listener_are_ignored() { + let allocator = Allocator::default(); + let source = r" +import { Directive, HostBinding, HostListener } from '@angular/core'; + +@Directive({ selector: '[d]' }) +export class D { + @HostBinding('class.a') a = true; + @HostBinding('class.b') static b = true; + @HostListener('click') onClick() {} + @HostListener('scroll') static onScroll() {} +} +"; + + let result = transform_angular_file(&allocator, "test.ts", source, None, None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + let code = &result.code; + let compact: String = code.chars().filter(|c| !c.is_whitespace()).collect(); + + // Instance member still produces a binding and a listener. + assert!( + compact.contains(r#"ɵɵclassProp("a",ctx.a)"#), + "instance @HostBinding should emit a classProp. Got:\n{code}" + ); + assert!( + compact.contains(r#"ɵɵlistener("click""#), + "instance @HostListener should emit a listener. Got:\n{code}" + ); + + // Static members are ignored entirely. + assert!( + !compact.contains(r#"classProp("b""#), + "static @HostBinding should not emit a binding. Got:\n{code}" + ); + assert!( + !compact.contains(r#"listener("scroll""#), + "static @HostListener should not emit a listener. Got:\n{code}" + ); + assert!( + compact.contains("hostVars:2"), + "hostVars should count only the instance classProp. Got:\n{code}" + ); +} + /// `setClassMetadata`'s `propDecorators` mirrors ngtsc's `extractClassMetadata` /// (metadata.ts): static and ECMAScript-private members are excluded, and /// string-literal member keys are emitted quoted (`shouldQuoteName`).