Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions crates/oxc_angular_compiler/src/directive/property_decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down
49 changes: 49 additions & 0 deletions crates/oxc_angular_compiler/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
Loading