From eec956bf7ef90be9336adc4a60df75c663e73d81 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 22 Oct 2026 18:45:29 +0800 Subject: [PATCH] fix(host): report an unreadable argument on any stacked host decorator Every stacked `@HostListener` / `@HostBinding` is compiled now, so the lone-surrogate check looks at every one too. A bad argument on a later decorator is reported instead of compiling a fallback (the member name, no listener, no args). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/directive/property_decorators.rs | 52 ++++++++++++------- .../tests/stacked_host_decorators_test.rs | 38 ++++++++++++++ 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index b766c206b..e376181d6 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -1940,28 +1940,40 @@ pub(crate) fn lone_surrogate_host_error<'a>( consts: &super::StringConsts<'a>, ) -> Option<(String, Span)> { // The members `extract_host_bindings_in` / `extract_host_listeners_in` - // compile, with their decorator's call. + // compile, with the call of each of their decorators. let members = |name: &'static str| { - class.body.body.iter().filter_map(move |element| { - let (decorators, key, is_static) = match element { - ClassElement::PropertyDefinition(prop) => { - (&prop.decorators, &prop.key, prop.r#static) - } - ClassElement::MethodDefinition(method) => { - (&method.decorators, &method.key, method.r#static) - } - ClassElement::AccessorProperty(prop) => { - (&prop.decorators, &prop.key, prop.r#static) + class + .body + .body + .iter() + .filter_map(move |element| { + let (decorators, key, is_static) = match element { + ClassElement::PropertyDefinition(prop) => { + (&prop.decorators, &prop.key, prop.r#static) + } + ClassElement::MethodDefinition(method) => { + (&method.decorators, &method.key, method.r#static) + } + ClassElement::AccessorProperty(prop) => { + (&prop.decorators, &prop.key, prop.r#static) + } + _ => return None, + }; + if is_static || get_property_key_name(key).is_none() { + return None; } - _ => return None, - }; - if is_static || get_property_key_name(key).is_none() { - return None; - } - let decorator = find_decorator_by_name(decorators, name, Some(consts))?; - let Expression::CallExpression(call) = &decorator.expression else { return None }; - Some((decorator, call)) - }) + // Every stacked decorator is compiled, so every one is checked + Some(decorators.iter().filter_map(move |decorator| { + if angular_member_decorator(decorator, Some(consts)) != Some(name) { + return None; + } + let Expression::CallExpression(call) = &decorator.expression else { + return None; + }; + Some((decorator, call)) + })) + }) + .flatten() }; let suffix = Value::Dynamic.wrong_type_suffix(); let is_lone = |arg: &Argument<'_>| arg.as_expression().is_some_and(is_lone_surrogate_literal); diff --git a/crates/oxc_angular_compiler/tests/stacked_host_decorators_test.rs b/crates/oxc_angular_compiler/tests/stacked_host_decorators_test.rs index b8ee636df..b67bd77a4 100644 --- a/crates/oxc_angular_compiler/tests/stacked_host_decorators_test.rs +++ b/crates/oxc_angular_compiler/tests/stacked_host_decorators_test.rs @@ -42,3 +42,41 @@ export class CCmp { ); assert!(code.contains("hostVars:3"), "Got:\n{}", result.code); } + +#[test] +fn an_unreadable_argument_on_a_later_stacked_decorator_is_reported() { + // A lone surrogate (`'\uD800'`) can't be read. Like on the first decorator, it is + // reported instead of compiling a fallback (the member name, no listener, no args). + for (member, expected) in [ + ( + r"@HostBinding('title') @HostBinding('\uD800') v = 'a';", + "@HostBinding's argument must be a string Value could not be determined statically.", + ), + ( + r"@HostListener('click') @HostListener('\uD800') a() {}", + "@HostListener's event name argument must be a string Value could not be \ + determined statically.", + ), + ( + r"@HostListener('click') @HostListener('x', ['\uD800']) a(x: any) {}", + "Failed to resolve @HostListener.args at position 0 to a string Value could not be \ + determined statically.", + ), + ] { + let source = format!( + "import {{ Directive, HostBinding, HostListener }} from '@angular/core';\n\ + @Directive({{ selector: '[d]' }})\n\ + export class D {{\n {member}\n}}" + ); + let allocator = Allocator::default(); + let result = transform_angular_file( + &allocator, + "test.ts", + &source, + Some(&TransformOptions::default()), + None, + ); + let messages: Vec = result.diagnostics.iter().map(|d| format!("{d}")).collect(); + assert_eq!(messages, [expected], "`{member}`"); + } +}