Skip to content

Commit 56e4adb

Browse files
Brooooooklynclaude
andcommitted
fix(queries): read member query options like ngtsc, and check queries: with the source text
Member query decorators (`@ViewChild`, `@ContentChildren`, ...) now go through the same code as `queries:` (ngtsc uses one function for both, `extractDecoratorQueryMetadata`): - `static`, `descendants` and `emitDistinctChangesOnly` are evaluated, so `{emitDistinctChangesOnly: FLAG}` with `const FLAG = false` gives flag 1 like ngtsc (it was 5), and parenthesized or `as` options are read. - A non-boolean option, options that aren't an object literal (`@ViewChild('x', OPTS)`), a predicate that can't be interpreted, and missing or extra arguments report ngtsc's errors, at the node ngtsc points to. An imported option reports the other-module error. The public `extract_*` functions without the file's imports keep reading literals only. The diagnostic pass read `queries:` without the source text, so a function-expression predicate (`new ViewChild(function Token() {})`) was compiled but also reported as "predicate cannot be interpreted". It now reads it the way the class is compiled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d6df748 commit 56e4adb

5 files changed

Lines changed: 1466 additions & 33 deletions

File tree

‎crates/oxc_angular_compiler/src/component/transform.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2579,7 +2579,12 @@ pub fn transform_angular_file(
25792579
let implicit_standalone = options.implicit_standalone();
25802580

25812581
// `inputs:`/`outputs:` forms ngtsc rejects, rather than dropping them silently.
2582-
result.diagnostics.extend(decorator_io_errors(allocator, class, &string_consts));
2582+
result.diagnostics.extend(decorator_io_errors(
2583+
allocator,
2584+
class,
2585+
Some(source),
2586+
&string_consts,
2587+
));
25832588

25842589
if let Some(mut metadata) = extract_component_metadata(
25852590
&allocator,

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,19 +1033,22 @@ pub(crate) fn angular_decorator_config<'a>(
10331033
/// The first error ngtsc raises for the inputs, outputs and queries of a
10341034
/// `@Component` / `@Directive` on `class`, in the order it checks them
10351035
/// (`extractDirectiveMetadata`): `inputs:`, `@Input` members, `outputs:`,
1036-
/// output members, then `queries:`. ngtsc stops at the first one.
1036+
/// output members, query members (`@ViewChild`, ...), then `queries:`. ngtsc
1037+
/// stops at the first one.
10371038
///
10381039
/// Each error points where ngtsc's does: the `inputs:` / `outputs:` value, the
1039-
/// member, or the part of `queries:` at fault.
1040+
/// member, or the part of a query at fault. `source_text` is the file's, as
1041+
/// when the class is compiled, so both read the same queries.
10401042
pub fn decorator_io_errors<'a>(
10411043
allocator: &'a Allocator,
10421044
class: &'a Class<'a>,
1045+
source_text: Option<&'a str>,
10431046
consts: &StringConsts<'a>,
10441047
) -> std::vec::Vec<OxcDiagnostic> {
10451048
let Some((config, decorator_name)) = angular_decorator_config(class) else {
10461049
return std::vec::Vec::new();
10471050
};
1048-
let io = config.map(|config| parse_decorator_io(allocator, config, class, None, consts));
1051+
let io = config.map(|config| parse_decorator_io(allocator, config, class, source_text, consts));
10491052
let (meta_inputs, meta_outputs): (std::vec::Vec<&str>, std::vec::Vec<&str>) = match &io {
10501053
Some(io) => (
10511054
io.inputs.iter().map(|i| i.class_property_name.as_str()).collect(),
@@ -1123,16 +1126,29 @@ pub fn decorator_io_errors<'a>(
11231126
})
11241127
})
11251128
};
1129+
let member_queries =
1130+
|| super::property_decorators::member_query_error(allocator, class, source_text, consts);
1131+
// With the source text, like the compiled queries: a predicate is emitted
1132+
// as written, which some expressions (functions) need it for.
11261133
let queries = || {
11271134
let config = config?;
1128-
super::parse_decorator_queries(allocator, config, class, None, consts, decorator_name).error
1135+
let queries = super::parse_decorator_queries(
1136+
allocator,
1137+
config,
1138+
class,
1139+
source_text,
1140+
consts,
1141+
decorator_name,
1142+
);
1143+
queries.error
11291144
};
11301145

11311146
io.as_ref()
11321147
.and_then(|io| io.input_error.clone())
11331148
.or_else(input_members)
11341149
.or_else(|| io.as_ref().and_then(|io| io.output_error.clone()))
11351150
.or_else(output_members)
1151+
.or_else(member_queries)
11361152
.or_else(queries)
11371153
.map(|(message, span)| OxcDiagnostic::error(message).with_label(span))
11381154
.into_iter()

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

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,28 @@ fn parse_query_config<'a>(
859859
source_text: Option<&'a str>,
860860
consts: Option<&super::StringConsts<'a>>,
861861
) -> QueryConfig<'a> {
862+
// With the file, read like ngtsc does (see [`member_query`]). A query it
863+
// rejects isn't compiled; [`member_query_error`] reports it.
864+
if let Some(consts) = consts {
865+
return match member_query(
866+
allocator,
867+
decorator,
868+
decorator_name,
869+
Span::default(),
870+
source_text,
871+
consts,
872+
) {
873+
Ok(query) => QueryConfig {
874+
predicate: Some(query.predicate),
875+
is_static: query.is_static,
876+
read: query.read,
877+
descendants: query.descendants,
878+
emit_distinct_changes_only: query.emit_distinct_changes_only,
879+
},
880+
Err(_) => QueryConfig::default_for(decorator_name),
881+
};
882+
}
883+
862884
let Expression::CallExpression(call) = &decorator.expression else {
863885
return QueryConfig::default_for(decorator_name);
864886
};
@@ -869,27 +891,16 @@ fn parse_query_config<'a>(
869891

870892
let mut config = QueryConfig::default_for(decorator_name);
871893

872-
// The predicate: a string selector, a string array, or a type/token.
894+
// The predicate: a string selector or a type/token.
873895
// forwardRef isn't included in compiled output.
874-
let node = try_unwrap_forward_ref(first_arg, consts).unwrap_or(first_arg);
875-
let selectors = match (consts, node) {
876-
(Some(consts), _) => match super::evaluator::Evaluator::new(consts).evaluate(node) {
877-
super::evaluator::Value::String(s) => Some(std::vec![s]),
878-
super::evaluator::Value::Array(items) => {
879-
items.iter().map(|i| i.as_str().map(str::to_string)).collect()
880-
}
881-
_ => None,
882-
},
883-
(None, Expression::StringLiteral(lit)) => Some(std::vec![lit.value.to_string()]),
884-
_ => None,
885-
};
886-
config.predicate = match selectors {
887-
Some(selectors) => {
896+
let node = try_unwrap_forward_ref(first_arg, None).unwrap_or(first_arg);
897+
config.predicate = match node {
898+
Expression::StringLiteral(lit) => {
888899
let mut list = Vec::new_in(&allocator);
889-
list.extend(selectors.iter().map(|s| Ident::from(allocator.alloc_str(s))));
900+
list.push(lit.value.clone().into());
890901
Some(QueryPredicate::Selectors(list))
891902
}
892-
None => convert_oxc_expression(allocator, node, source_text).map(QueryPredicate::Type),
903+
_ => convert_oxc_expression(allocator, node, source_text).map(QueryPredicate::Type),
893904
};
894905

895906
// Parse options from second argument if present
@@ -1798,8 +1809,9 @@ pub(crate) fn parse_decorator_queries<'a>(
17981809
consts,
17991810
&evaluator,
18001811
type_name,
1801-
new_expr,
1802-
property_name,
1812+
&new_expr.arguments,
1813+
new_expr.span,
1814+
&property_name,
18031815
source_text,
18041816
) {
18051817
Ok(query) if type_name.starts_with("Content") => {
@@ -1849,21 +1861,71 @@ pub(crate) fn parse_decorator_queries<'a>(
18491861
queries
18501862
}
18511863

1852-
/// One `new ViewChild(predicate, options?)` and friends. An error comes with
1853-
/// the node ngtsc reports it on.
1864+
/// A query member decorator (`@ViewChild('el', {static: true})` on the
1865+
/// member at `span`), read like ngtsc's `extractDecoratorQueryMetadata` (see
1866+
/// [`decorator_query`]): the options are evaluated, so a same-file `const`
1867+
/// counts and a value of the wrong type is an error.
1868+
fn member_query<'a>(
1869+
allocator: &'a Allocator,
1870+
decorator: &'a Decorator<'a>,
1871+
name: &str,
1872+
span: Span,
1873+
source_text: Option<&'a str>,
1874+
consts: &super::StringConsts<'a>,
1875+
) -> Result<R3QueryMetadata<'a>, (String, Span)> {
1876+
// `@ViewChild` without a call has no arguments.
1877+
let args: &'a [Argument<'a>] = match &decorator.expression {
1878+
Expression::CallExpression(call) => &call.arguments,
1879+
_ => &[],
1880+
};
1881+
let evaluator = super::evaluator::Evaluator::new(consts);
1882+
decorator_query(allocator, consts, &evaluator, name, args, span, "", source_text)
1883+
}
1884+
1885+
/// The first error ngtsc raises for a class's query member decorators
1886+
/// (`parseQueriesOfClassFields`), in member order: the one
1887+
/// [`member_query`] reports, on the node ngtsc points at.
1888+
pub(crate) fn member_query_error<'a>(
1889+
allocator: &'a Allocator,
1890+
class: &'a Class<'a>,
1891+
source_text: Option<&'a str>,
1892+
consts: &super::StringConsts<'a>,
1893+
) -> Option<(String, Span)> {
1894+
class.body.body.iter().find_map(|element| {
1895+
let (decorators, span) = match element {
1896+
ClassElement::PropertyDefinition(prop) => (&prop.decorators, prop.span),
1897+
ClassElement::MethodDefinition(method)
1898+
if matches!(method.kind, MethodDefinitionKind::Set | MethodDefinitionKind::Get) =>
1899+
{
1900+
(&method.decorators, method.span)
1901+
}
1902+
_ => return None,
1903+
};
1904+
let (decorator, name) = QUERY_TYPES.iter().find_map(|name| {
1905+
Some((find_decorator_by_name(decorators, name, Some(consts))?, *name))
1906+
})?;
1907+
member_query(allocator, decorator, name, span, source_text, consts).err()
1908+
})
1909+
}
1910+
1911+
/// ngtsc's `extractDecoratorQueryMetadata`: one `@ViewChild(predicate,
1912+
/// options?)` member decorator, or `new ViewChild(...)` in `queries:`, and
1913+
/// friends. `args` are its arguments and `span` the member or the `new`
1914+
/// expression. An error comes with the node ngtsc reports it on.
1915+
#[expect(clippy::too_many_arguments)]
18541916
fn decorator_query<'a>(
18551917
allocator: &'a Allocator,
18561918
consts: &super::StringConsts<'a>,
18571919
evaluator: &super::evaluator::Evaluator<'_, 'a>,
18581920
name: &str,
1859-
new_expr: &'a oxc_ast::ast::NewExpression<'a>,
1860-
property_name: String,
1921+
args: &'a [Argument<'a>],
1922+
span: Span,
1923+
property_name: &str,
18611924
source_text: Option<&'a str>,
18621925
) -> Result<R3QueryMetadata<'a>, (String, Span)> {
18631926
use super::evaluator::Value;
1864-
let args = &new_expr.arguments;
18651927
let Some(first) = args.first().and_then(Argument::as_expression) else {
1866-
return Err((format!("@{name} must have arguments"), new_expr.span));
1928+
return Err((format!("@{name} must have arguments"), span));
18671929
};
18681930
let node = try_unwrap_forward_ref(first, Some(consts)).unwrap_or(first);
18691931
let at_node = |message: String| (message, node.span());
@@ -1940,7 +2002,7 @@ fn decorator_query<'a>(
19402002
}
19412003

19422004
Ok(R3QueryMetadata {
1943-
property_name: Ident::from(allocator.alloc_str(&property_name)),
2005+
property_name: Ident::from(allocator.alloc_str(property_name)),
19442006
first: name == "ViewChild" || name == "ContentChild",
19452007
predicate,
19462008
descendants: config.descendants,

‎crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() {
299299
failures.len(),
300300
failures.join("\n\n")
301301
);
302-
assert_eq!(compared, 513, "fixtures compared");
302+
assert_eq!(compared, 606, "fixtures compared");
303303
}
304304

305305
fn transform(source: &str) -> TransformResult {
@@ -909,7 +909,7 @@ export class Cmp {
909909
}
910910
}
911911

912-
/// `queries:` values imported from another file: ngtsc 22.1.7 reads the file
912+
/// Query values imported from another file: ngtsc 22.1.7 reads the file
913913
/// and compiles all of these (`probe: queries-imported*`, with
914914
/// `export const FLAG = true; export const SELS = ['a', 'b']; export const SEL
915915
/// = 'b';` in `./flags`). oxc can't, and says so rather than reporting
@@ -961,4 +961,44 @@ export class Cmp {{
961961
"{query}"
962962
);
963963
}
964+
965+
// The same options on member decorators, read by the same code as ngtsc
966+
// does (`probe: queries-member-*`; ngtsc 22.1.7 compiles these with
967+
// `export const FLAG = false` in the other file).
968+
let cases = [
969+
(
970+
"@ViewChildren('el', {emitDistinctChangesOnly: FLAG})",
971+
"@ViewChildren options.emitDistinctChangesOnly",
972+
"FLAG",
973+
),
974+
(
975+
"@ContentChildren('el', {descendants: flags.FLAG})",
976+
"@ContentChildren options.descendants",
977+
"flags.FLAG",
978+
),
979+
("@ViewChild('el', {static: FLAG})", "@ViewChild options.static", "'el'"),
980+
("@ContentChild(['a', SEL])", "@ContentChild predicate", "['a', SEL]"),
981+
];
982+
for (decorator, subject, span) in cases {
983+
let source = format!(
984+
"import {{Component, ViewChild, ViewChildren, ContentChild, ContentChildren}} from '@angular/core';
985+
import {{FLAG, SEL}} from './flags';
986+
import * as flags from './flags';
987+
@Component({{selector: 'c', template: ''}})
988+
export class Cmp {{
989+
{decorator} el: any;
990+
}}
991+
"
992+
);
993+
let name = if subject.ends_with("predicate") { "SEL" } else { "FLAG" };
994+
let message = format!(
995+
"{subject} depends on '{name}', which is imported from another module. \
996+
OXC compiles one file at a time and cannot evaluate values from other files."
997+
);
998+
assert_eq!(
999+
errors(&transform(&source), &source),
1000+
vec![(message, span.to_string())],
1001+
"{decorator}"
1002+
);
1003+
}
9641004
}

0 commit comments

Comments
 (0)