Skip to content

Commit 2b04ec5

Browse files
Brooooooklynclaude
andcommitted
fix(directive): reject @Input / @Output on signal inputs, models and outputs like ngtsc
`@Input() value = input(0)` compiled as a plain input with no error; ngtsc reports "Using @input with a signal input is not allowed." oxc only looked at the initializer of a member also listed in `inputs:` (for a different error), and had no such check. The same went for `@Input` on `model()`, and `@Output` on `output()`, `outputFromObservable()` or `model()`. Each is now reported on the decorator with ngtsc's message, in its order: an input's check comes before its `@Input` arguments are read, an output's after its `@Output` arguments. The call counts only when it's Angular's API by its import (any alias, a namespace, `.required`), so a local or foreign `input()` is left alone. The compiled output is unchanged, as for other errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 71769d8 commit 2b04ec5

3 files changed

Lines changed: 560 additions & 5 deletions

File tree

‎crates/oxc_angular_compiler/src/directive/decorator.rs‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,20 @@ pub fn decorator_io_errors<'a>(
10551055
// `@Input(...)`, as ngtsc's `tryParseInputFieldMapping` reads it.
10561056
let decorator =
10571057
super::property_decorators::member_decorator(decorators, "Input", consts);
1058+
// ngtsc rejects `@Input` on a signal input or model before reading
1059+
// the decorator.
1060+
if let (Some(decorator), Some(value)) = (decorator, value) {
1061+
let message = if is_initializer_api_call(value, consts, &[INPUT_API]) {
1062+
Some("Using @Input with a signal input is not allowed.")
1063+
} else if is_initializer_api_call(value, consts, &[MODEL_API]) {
1064+
Some("Using @Input with a model input is not allowed.")
1065+
} else {
1066+
None
1067+
};
1068+
if let Some(message) = message {
1069+
return Some((message.to_string(), decorator.span));
1070+
}
1071+
}
10581072
let error =
10591073
decorator.and_then(|d| input_decorator_error(d, &name, class, consts, &evaluator));
10601074
if error.is_some() {
@@ -1075,17 +1089,32 @@ pub fn decorator_io_errors<'a>(
10751089
class.body.body.iter().find_map(|element| {
10761090
// `@Output(...)`, as ngtsc's `tryParseDecoratorOutput` reads it, on
10771091
// the members an output is compiled from.
1078-
let decorators = match element {
1079-
ClassElement::PropertyDefinition(p) => Some(&p.decorators),
1080-
ClassElement::AccessorProperty(p) => Some(&p.decorators),
1081-
_ => None,
1092+
let (decorators, value) = match element {
1093+
ClassElement::PropertyDefinition(p) => (Some(&p.decorators), p.value.as_ref()),
1094+
ClassElement::AccessorProperty(p) => (Some(&p.decorators), p.value.as_ref()),
1095+
_ => (None, None),
10821096
};
10831097
let decorator = decorators.and_then(|decorators| {
10841098
super::property_decorators::member_decorator(decorators, "Output", consts)
10851099
});
10861100
if let Some(error) = decorator.and_then(|d| output_decorator_error(d, &evaluator)) {
10871101
return Some(error);
10881102
}
1103+
// Then `@Output` on an `output()` or a model, like ngtsc's
1104+
// `parseOutputFields`.
1105+
if let (Some(decorator), Some(value)) = (decorator, value) {
1106+
let apis = [OUTPUT_API, OUTPUT_FROM_OBSERVABLE_API];
1107+
let message = if is_initializer_api_call(value, consts, &apis) {
1108+
Some("Using \"@Output\" with \"output()\" is not allowed.")
1109+
} else if is_initializer_api_call(value, consts, &[MODEL_API]) {
1110+
Some("Using @Output with a model input is not allowed.")
1111+
} else {
1112+
None
1113+
};
1114+
if let Some(message) = message {
1115+
return Some((message.to_string(), decorator.span));
1116+
}
1117+
}
10891118
let ClassElement::PropertyDefinition(prop) = element else { return None };
10901119
let (value, name) = (prop.value.as_ref()?, prop.key.static_name()?);
10911120
if !meta_outputs.contains(&name.as_ref()) {

‎crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ fn decorator_metadata_matches_ngtsc() {
307307
failures.len(),
308308
failures.join("\n\n")
309309
);
310-
assert_eq!(compared, 859, "fixtures compared");
310+
assert_eq!(compared, 902, "fixtures compared");
311311
}
312312

313313
fn transform(source: &str) -> TransformResult {

0 commit comments

Comments
 (0)