From ac09cf17eae7ef25f989f1b29c7c4c300bc3dae0 Mon Sep 17 00:00:00 2001 From: Ashley Hunter Date: Thu, 24 Sep 2026 08:18:24 +0100 Subject: [PATCH 1/6] fix(queries): compile decorator `queries:` and match ngtsc's query options `@Directive({ queries: { el: new ViewChild('el') } })` (and `@Component`) was ignored, so those queries never ran. It's now parsed like ngtsc's `extractQueriesFromDecorator`: `new ViewChild/ViewChildren/ContentChild/ ContentChildren(...)` imported from `@angular/core` (directly or through a namespace import), appended after the member queries, with ngtsc's diagnostics word for word (not an object literal, not a query type, missing/extra arguments, non-object options, non-boolean `descendants`/`static`/ `emitDistinctChangesOnly`, uninterpretable predicates, and a signal query declared for the same property). Member query decorators now match ngtsc too: - `descendants` and `emitDistinctChangesOnly` are honoured on `@ViewChild`/ `@ViewChildren` (they were hard-coded), which changes the emitted flags - a predicate referencing a same-file string const (`@ViewChild(SELECTOR)`) compiles to that selector rather than to a type predicate - `forwardRef(function () { return X; })`, and `forwardRef` behind parentheses or `as`, is unwrapped like the arrow form `extract_class_queries` returns a class's queries as they're compiled; the compiler and the NAPI `extractComponentMetadataSync` both use it, so the metadata API reports the same queries. --- .../src/component/transform.rs | 12 +- .../src/directive/decorator.rs | 33 +- .../src/directive/evaluator.rs | 4 +- .../src/directive/metadata.rs | 28 +- .../oxc_angular_compiler/src/directive/mod.rs | 12 +- .../src/directive/property_decorators.rs | 484 ++++++++++++++--- crates/oxc_angular_compiler/src/lib.rs | 7 +- .../tests/decorator_metadata_ngtsc_test.rs | 2 +- .../fixtures/decorator_metadata_ngtsc.json | 492 ++++++++++++++++++ napi/angular-compiler/src/lib.rs | 13 +- .../test/extract-component-metadata.test.ts | 39 ++ 11 files changed, 1017 insertions(+), 109 deletions(-) create mode 100644 napi/angular-compiler/test/extract-component-metadata.test.ts diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 2a2798183..8c7d4f9ba 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -41,7 +41,7 @@ use crate::class_metadata::{ use crate::directive::collect_string_consts; use crate::directive::{ R3QueryMetadata, create_content_queries_function, create_view_queries_function, - decorator_io_errors, extract_content_queries, extract_directive_metadata, extract_view_queries, + decorator_io_errors, extract_class_queries, extract_directive_metadata, find_directive_decorator, find_directive_decorator_span, generate_directive_definitions, }; use crate::dts; @@ -2656,11 +2656,11 @@ pub fn transform_angular_file( let template = allocator.alloc_str(&template_string); // 4.5 Extract view queries from the class (for @ViewChild/@ViewChildren) // These need to be passed to compile_component_full so predicates can be pooled - let view_queries = extract_view_queries(allocator, class, Some(source)); - - // 4.6 Extract content queries from the class (for @ContentChild/@ContentChildren) - // Signal-based queries (contentChild(), contentChildren()) are also detected here - let content_queries = extract_content_queries(allocator, class, Some(source)); + // 4.5/4.6 View and content queries: member decorators and signal + // queries, then `queries:` in the decorator, as ngtsc orders them. + // Predicates are pooled by compile_component_full. + let (view_queries, content_queries) = + extract_class_queries(allocator, class, Some(source), &string_consts); // Collect content query property names for .d.ts generation // (before content_queries is moved into compile_component_full) diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 38d2092f2..f8e5522ee 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -182,7 +182,7 @@ pub fn extract_directive_metadata<'a>( } // Extract @Input/@Output/@HostBinding/@HostListener from class members - builder = builder.extract_from_class(allocator, class, source_text); + builder = builder.extract_from_class_in(allocator, class, source_text, Some(consts)); // Detect if ngOnChanges lifecycle hook is implemented // Similar to Angular's: const usesOnChanges = members.some(member => ...) @@ -207,6 +207,20 @@ pub fn extract_directive_metadata<'a>( // The builder already has host data from extract_from_class, we need to merge the decorator host let mut metadata = builder.build()?; + // `queries:` from the decorator come after the member queries, as in ngtsc. + if let Some(config) = config_obj { + let queries = super::parse_decorator_queries( + allocator, + config, + class, + source_text, + consts, + "Directive", + ); + metadata.view_queries.extend(queries.view); + metadata.queries.extend(queries.content); + } + if let Some(io) = io { let fields = std::mem::replace(&mut metadata.inputs, Vec::new_in(&allocator)); metadata.inputs = @@ -1035,13 +1049,13 @@ pub(crate) fn angular_decorator_config<'a>( Some((config, name)) } -/// The first error ngtsc raises for the inputs and outputs of a `@Component` / -/// `@Directive` on `class`, in the order it checks them -/// (`extractDirectiveMetadata`): `inputs:`, `@Input` members, `outputs:`, then -/// output members. ngtsc stops at the first one. +/// The first error ngtsc raises for the inputs, outputs and queries of a +/// `@Component` / `@Directive` on `class`, in the order it checks them +/// (`extractDirectiveMetadata`): `inputs:`, `@Input` members, `outputs:`, +/// output members, then `queries:`. ngtsc stops at the first one. /// -/// Each error points where ngtsc's does: the `inputs:` / `outputs:` value, or -/// the member. +/// Each error points where ngtsc's does: the `inputs:` / `outputs:` value, the +/// member, or the part of `queries:` at fault. pub fn decorator_io_errors<'a>( allocator: &'a Allocator, class: &'a Class<'a>, @@ -1134,12 +1148,17 @@ pub fn decorator_io_errors<'a>( }) }) }; + let queries = || { + let config = config?; + super::parse_decorator_queries(allocator, config, class, None, consts, decorator_name).error + }; io.as_ref() .and_then(|io| io.input_error.clone()) .or_else(input_members) .or_else(|| io.as_ref().and_then(|io| io.output_error.clone())) .or_else(output_members) + .or_else(queries) .map(|(message, span)| OxcDiagnostic::error(message).with_label(span)) .into_iter() .collect() diff --git a/crates/oxc_angular_compiler/src/directive/evaluator.rs b/crates/oxc_angular_compiler/src/directive/evaluator.rs index a02c214c4..a4cae41a0 100644 --- a/crates/oxc_angular_compiler/src/directive/evaluator.rs +++ b/crates/oxc_angular_compiler/src/directive/evaluator.rs @@ -141,8 +141,8 @@ enum PathKey<'a> { Unknown, } -/// An import binding: the module it's imported from and, unless it's a -/// namespace import, the name it's exported under. +/// An import binding: the module it comes from and, unless it's a namespace +/// import, the name it's exported under. #[derive(Clone, Copy)] pub(crate) struct Import<'a> { pub module: &'a str, diff --git a/crates/oxc_angular_compiler/src/directive/metadata.rs b/crates/oxc_angular_compiler/src/directive/metadata.rs index 9c791d85a..d3486eaae 100644 --- a/crates/oxc_angular_compiler/src/directive/metadata.rs +++ b/crates/oxc_angular_compiler/src/directive/metadata.rs @@ -411,10 +411,22 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { /// # Returns /// The builder with all extracted metadata added. pub fn extract_from_class( + self, + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + ) -> Self { + self.extract_from_class_in(allocator, class, source_text, None) + } + + /// [`Self::extract_from_class`], resolving query predicates that reference + /// same-file consts (`@ViewChild(SELECTOR)`) the way ngtsc does. + pub(crate) fn extract_from_class_in( mut self, allocator: &'a Allocator, class: &'a Class<'a>, source_text: Option<&'a str>, + consts: Option<&super::StringConsts<'a>>, ) -> Self { // Extract inputs from @Input decorators let inputs = @@ -430,15 +442,23 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { } // Extract view queries from @ViewChild/@ViewChildren - let view_queries = - super::property_decorators::extract_view_queries(allocator, class, source_text); + let view_queries = super::property_decorators::extract_view_queries_in( + allocator, + class, + source_text, + consts, + ); for query in view_queries { self = self.add_view_query(query); } // Extract content queries from @ContentChild/@ContentChildren - let content_queries = - super::property_decorators::extract_content_queries(allocator, class, source_text); + let content_queries = super::property_decorators::extract_content_queries_in( + allocator, + class, + source_text, + consts, + ); for query in content_queries { self = self.add_query(query); } diff --git a/crates/oxc_angular_compiler/src/directive/mod.rs b/crates/oxc_angular_compiler/src/directive/mod.rs index 9a9f1cb12..eeecd6d61 100644 --- a/crates/oxc_angular_compiler/src/directive/mod.rs +++ b/crates/oxc_angular_compiler/src/directive/mod.rs @@ -30,7 +30,9 @@ pub use decorator::{ StringConsts, collect_string_consts, decorator_io_errors, extract_directive_metadata, find_directive_decorator_span, }; -pub(crate) use decorator::{extract_string_value, resolve_template_literal}; +pub(crate) use decorator::{ + angular_decorator_config, extract_string_value, resolve_template_literal, +}; pub(crate) use decorator::{ merge_by_class_property, parse_decorator_io, resolve_member_transforms, }; @@ -41,11 +43,11 @@ pub use metadata::{ }; pub(crate) use property_decorators::CoreNamespaces; pub use property_decorators::{ - extract_content_queries, extract_host_bindings, extract_host_listeners, extract_input_metadata, - extract_output_metadata, extract_view_queries, + extract_class_queries, extract_content_queries, extract_host_bindings, extract_host_listeners, + extract_input_metadata, extract_output_metadata, extract_view_queries, }; pub(crate) use property_decorators::{ - try_parse_signal_input, try_parse_signal_model, try_parse_signal_output, - unwrap_initializer_api_expr, + parse_decorator_queries, try_parse_signal_input, try_parse_signal_model, + try_parse_signal_output, unwrap_initializer_api_expr, }; pub use query::{create_content_queries_function, create_view_queries_function}; diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index c5873b64a..b2d3f8e8a 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -18,6 +18,7 @@ use oxc_ast::ast::{ Argument, ArrayExpressionElement, Class, ClassElement, Decorator, Expression, MethodDefinitionKind, ObjectPropertyKind, PropertyKey, }; +use oxc_span::{GetSpan, Span}; use oxc_str::Ident; use super::metadata::{QueryPredicate, R3InputMetadata, R3QueryMetadata}; @@ -127,42 +128,33 @@ fn extract_boolean_value(expr: &Expression<'_>) -> Option { /// Try to unwrap a forwardRef call and extract the inner expression. /// -/// For `forwardRef(() => MyClass)`, returns `Some(MyClass expression)`. -/// For non-forwardRef expressions, returns None. +/// Mirrors ngtsc's `tryUnwrapForwardRef`: `forwardRef(() => X)`, +/// `forwardRef(function () { return X; })`, looking through parentheses and +/// `as` casts. Returns `None` for anything else. fn try_unwrap_forward_ref<'a>(expr: &'a Expression<'a>) -> Option<&'a Expression<'a>> { - let call = match expr { - Expression::CallExpression(call) => call, + let Expression::CallExpression(call) = unwrap_expression(expr) else { return None }; + let callee = match &call.callee { + Expression::Identifier(id) => id.name.as_str(), + Expression::StaticMemberExpression(m) => m.property.name.as_str(), _ => return None, }; - - // Check if callee is forwardRef - let is_forward_ref = - matches!(&call.callee, Expression::Identifier(id) if id.name == "forwardRef"); - if !is_forward_ref { + if callee != "forwardRef" || call.arguments.len() != 1 { return None; } - - // Get the first argument (should be an arrow function) - let first_arg = call.arguments.first()?; - let arrow = match first_arg { - Argument::ArrowFunctionExpression(arrow) => arrow, - _ => return None, - }; - - // Expression body: () => MyClass - if let Some(expr) = arrow.get_expression() { - return Some(expr); - } - // Block body: () => { return MyClass; } - if let Some(body) = arrow.get_function_body() { - if body.statements.len() == 1 { - if let oxc_ast::ast::Statement::ReturnStatement(ret) = &body.statements[0] { - return ret.argument.as_ref(); + let body = match unwrap_expression(call.arguments[0].as_expression()?) { + Expression::ArrowFunctionExpression(arrow) => { + if let Some(expr) = arrow.get_expression() { + return Some(expr); } + arrow.get_function_body()? } + Expression::FunctionExpression(f) => &**f.body.as_ref()?, + _ => return None, + }; + match body.statements.as_slice() { + [oxc_ast::ast::Statement::ReturnStatement(ret)] => ret.argument.as_ref(), + _ => None, } - - None } // ============================================================================ @@ -784,13 +776,15 @@ struct QueryConfig<'a> { is_static: bool, /// Expression to read from matched elements. read: Option>, - /// Whether to include descendants (for content queries). + /// Whether to include descendants. descendants: bool, + /// Whether a `QueryList` only notifies when its contents change. + emit_distinct_changes_only: bool, } impl<'a> Default for QueryConfig<'a> { fn default() -> Self { - Self { predicate: None, is_static: false, read: None, descendants: true } + Self::default_for("") } } @@ -806,6 +800,7 @@ impl<'a> QueryConfig<'a> { read: None, // For @ContentChildren, default is false; for all others, default is true descendants: decorator_name != "ContentChildren", + emit_distinct_changes_only: true, } } } @@ -825,38 +820,40 @@ fn parse_query_config<'a>( decorator: &'a Decorator<'a>, decorator_name: &str, source_text: Option<&'a str>, + consts: Option<&super::StringConsts<'a>>, ) -> QueryConfig<'a> { let Expression::CallExpression(call) = &decorator.expression else { return QueryConfig::default_for(decorator_name); }; - let Some(first_arg) = call.arguments.first() else { + let Some(first_arg) = call.arguments.first().and_then(Argument::as_expression) else { return QueryConfig::default_for(decorator_name); }; let mut config = QueryConfig::default_for(decorator_name); - // Parse predicate from first argument - match first_arg { - // @ViewChild('refName') - string selector - Argument::StringLiteral(lit) => { - let mut selectors = Vec::new_in(&allocator); - selectors.push(lit.value.clone().into()); - config.predicate = Some(QueryPredicate::Selectors(selectors)); - } - - // Other expressions (identifiers, member expressions, forwardRef calls, etc.) - _ => { - let expr = first_arg.to_expression(); - // Unwrap forwardRef if present - Angular doesn't include forwardRef in compiled output - let unwrapped_expr = try_unwrap_forward_ref(expr).unwrap_or(expr); - if let Some(output_expr) = - convert_oxc_expression(allocator, unwrapped_expr, source_text) - { - config.predicate = Some(QueryPredicate::Type(output_expr)); + // The predicate: a string selector, a string array, or a type/token. + // forwardRef isn't included in compiled output. + let node = try_unwrap_forward_ref(first_arg).unwrap_or(first_arg); + let selectors = match (consts, node) { + (Some(consts), _) => match super::evaluator::Evaluator::new(consts).evaluate(node) { + super::evaluator::Value::String(s) => Some(std::vec![s]), + super::evaluator::Value::Array(items) => { + items.iter().map(|i| i.as_str().map(str::to_string)).collect() } + _ => None, + }, + (None, Expression::StringLiteral(lit)) => Some(std::vec![lit.value.to_string()]), + _ => None, + }; + config.predicate = match selectors { + Some(selectors) => { + let mut list = Vec::new_in(&allocator); + list.extend(selectors.iter().map(|s| Ident::from(allocator.alloc_str(s)))); + Some(QueryPredicate::Selectors(list)) } - } + None => convert_oxc_expression(allocator, node, source_text).map(QueryPredicate::Type), + }; // Parse options from second argument if present if let Some(second_arg) = call.arguments.get(1) { @@ -881,6 +878,10 @@ fn parse_query_config<'a>( config.descendants = extract_boolean_value(&prop.value).unwrap_or(default); } + "emitDistinctChangesOnly" => { + config.emit_distinct_changes_only = + extract_boolean_value(&prop.value).unwrap_or(true); + } _ => {} } } @@ -1083,6 +1084,17 @@ pub fn extract_view_queries<'a>( allocator: &'a Allocator, class: &'a Class<'a>, source_text: Option<&'a str>, +) -> Vec<'a, R3QueryMetadata<'a>> { + extract_view_queries_in(allocator, class, source_text, None) +} + +/// [`extract_view_queries`], resolving predicates that reference same-file +/// consts (`@ViewChild(SELECTOR)`) the way ngtsc's partial evaluator does. +pub(crate) fn extract_view_queries_in<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + consts: Option<&super::StringConsts<'a>>, ) -> Vec<'a, R3QueryMetadata<'a>> { // Use separate vectors to match Angular's ordering approach. // Angular groups queries by type, maintaining declaration order within each group: @@ -1115,15 +1127,20 @@ pub fn extract_view_queries<'a>( // Check for decorator-based queries (@ViewChild, @ViewChildren) if let Some(decorator) = find_decorator_by_name(&prop.decorators, "ViewChild") { if let Some(property_name) = get_property_key_name(&prop.key) { - let config = - parse_query_config(allocator, decorator, "ViewChild", source_text); + let config = parse_query_config( + allocator, + decorator, + "ViewChild", + source_text, + consts, + ); if let Some(predicate) = config.predicate { view_child_queries.push(R3QueryMetadata { property_name, first: true, predicate, - descendants: true, - emit_distinct_changes_only: true, + descendants: config.descendants, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1134,15 +1151,20 @@ pub fn extract_view_queries<'a>( find_decorator_by_name(&prop.decorators, "ViewChildren") { if let Some(property_name) = get_property_key_name(&prop.key) { - let config = - parse_query_config(allocator, decorator, "ViewChildren", source_text); + let config = parse_query_config( + allocator, + decorator, + "ViewChildren", + source_text, + consts, + ); if let Some(predicate) = config.predicate { view_children_queries.push(R3QueryMetadata { property_name, first: false, predicate, - descendants: true, - emit_distinct_changes_only: true, + descendants: config.descendants, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1157,15 +1179,20 @@ pub fn extract_view_queries<'a>( // Check for decorator-based queries on setters/getters if let Some(decorator) = find_decorator_by_name(&method.decorators, "ViewChild") { if let Some(property_name) = get_property_key_name(&method.key) { - let config = - parse_query_config(allocator, decorator, "ViewChild", source_text); + let config = parse_query_config( + allocator, + decorator, + "ViewChild", + source_text, + consts, + ); if let Some(predicate) = config.predicate { view_child_queries.push(R3QueryMetadata { property_name, first: true, predicate, - descendants: true, - emit_distinct_changes_only: true, + descendants: config.descendants, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1176,15 +1203,20 @@ pub fn extract_view_queries<'a>( find_decorator_by_name(&method.decorators, "ViewChildren") { if let Some(property_name) = get_property_key_name(&method.key) { - let config = - parse_query_config(allocator, decorator, "ViewChildren", source_text); + let config = parse_query_config( + allocator, + decorator, + "ViewChildren", + source_text, + consts, + ); if let Some(predicate) = config.predicate { view_children_queries.push(R3QueryMetadata { property_name, first: false, predicate, - descendants: true, - emit_distinct_changes_only: true, + descendants: config.descendants, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1226,6 +1258,17 @@ pub fn extract_content_queries<'a>( allocator: &'a Allocator, class: &'a Class<'a>, source_text: Option<&'a str>, +) -> Vec<'a, R3QueryMetadata<'a>> { + extract_content_queries_in(allocator, class, source_text, None) +} + +/// [`extract_content_queries`], resolving predicates that reference same-file +/// consts (`@ViewChild(SELECTOR)`) the way ngtsc's partial evaluator does. +pub(crate) fn extract_content_queries_in<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + consts: Option<&super::StringConsts<'a>>, ) -> Vec<'a, R3QueryMetadata<'a>> { // Use separate vectors to match Angular's ordering approach. // Angular groups queries by type, maintaining declaration order within each group: @@ -1258,15 +1301,20 @@ pub fn extract_content_queries<'a>( // Check for decorator-based queries (@ContentChild, @ContentChildren) if let Some(decorator) = find_decorator_by_name(&prop.decorators, "ContentChild") { if let Some(property_name) = get_property_key_name(&prop.key) { - let config = - parse_query_config(allocator, decorator, "ContentChild", source_text); + let config = parse_query_config( + allocator, + decorator, + "ContentChild", + source_text, + consts, + ); if let Some(predicate) = config.predicate { content_child_queries.push(R3QueryMetadata { property_name, first: true, predicate, descendants: config.descendants, - emit_distinct_changes_only: true, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1282,6 +1330,7 @@ pub fn extract_content_queries<'a>( decorator, "ContentChildren", source_text, + consts, ); if let Some(predicate) = config.predicate { content_children_queries.push(R3QueryMetadata { @@ -1289,7 +1338,7 @@ pub fn extract_content_queries<'a>( first: false, predicate, descendants: config.descendants, - emit_distinct_changes_only: true, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1305,15 +1354,20 @@ pub fn extract_content_queries<'a>( if let Some(decorator) = find_decorator_by_name(&method.decorators, "ContentChild") { if let Some(property_name) = get_property_key_name(&method.key) { - let config = - parse_query_config(allocator, decorator, "ContentChild", source_text); + let config = parse_query_config( + allocator, + decorator, + "ContentChild", + source_text, + consts, + ); if let Some(predicate) = config.predicate { content_child_queries.push(R3QueryMetadata { property_name, first: true, predicate, descendants: config.descendants, - emit_distinct_changes_only: true, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1329,6 +1383,7 @@ pub fn extract_content_queries<'a>( decorator, "ContentChildren", source_text, + consts, ); if let Some(predicate) = config.predicate { content_children_queries.push(R3QueryMetadata { @@ -1336,7 +1391,7 @@ pub fn extract_content_queries<'a>( first: false, predicate, descendants: config.descendants, - emit_distinct_changes_only: true, + emit_distinct_changes_only: config.emit_distinct_changes_only, read: config.read, is_static: config.is_static, is_signal: false, @@ -1534,6 +1589,287 @@ fn parse_host_listener_config<'a>( // Tests // ============================================================================ +/// A class's view and content queries as they're compiled: member decorators +/// and signal queries (predicates referencing same-file consts resolved), then +/// `queries:` from its `@Component` / `@Directive` metadata, in ngtsc's order. +pub fn extract_class_queries<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + consts: &super::StringConsts<'a>, +) -> (Vec<'a, R3QueryMetadata<'a>>, Vec<'a, R3QueryMetadata<'a>>) { + let mut view = extract_view_queries_in(allocator, class, source_text, Some(consts)); + let mut content = extract_content_queries_in(allocator, class, source_text, Some(consts)); + if let Some((Some(config), name)) = super::angular_decorator_config(class) { + let queries = parse_decorator_queries(allocator, config, class, source_text, consts, name); + view.extend(queries.view); + content.extend(queries.content); + } + (view, content) +} + +// ============================================================================ +// `queries:` in @Directive / @Component metadata +// ============================================================================ + +/// Queries declared in decorator metadata, e.g. +/// `@Directive({ queries: { el: new ViewChild('el') } })`. +pub(crate) struct DecoratorQueries<'a> { + pub view: Vec<'a, R3QueryMetadata<'a>>, + pub content: Vec<'a, R3QueryMetadata<'a>>, + /// The first error ngtsc reports for them, and the node it reports it on. + pub error: Option<(String, Span)>, +} + +const QUERY_TYPES: &[&str] = &["ViewChild", "ViewChildren", "ContentChild", "ContentChildren"]; + +/// ngtsc's `unwrapExpression`: parentheses and `as` casts. +fn unwrap_expression<'a>(mut expr: &'a Expression<'a>) -> &'a Expression<'a> { + loop { + expr = match expr { + Expression::ParenthesizedExpression(e) => &e.expression, + Expression::TSAsExpression(e) => &e.expression, + _ => return expr, + }; + } +} + +/// ngtsc's `reflectObjectLiteral`: property assignments with a static name and +/// shorthand properties; anything else is skipped. +fn reflect_object_literal<'a>( + obj: &'a oxc_ast::ast::ObjectExpression<'a>, +) -> std::vec::Vec<(String, &'a Expression<'a>)> { + let mut entries: std::vec::Vec<(String, &'a Expression<'a>)> = std::vec::Vec::new(); + for prop in &obj.properties { + let ObjectPropertyKind::ObjectProperty(prop) = prop else { continue }; + if prop.method || prop.computed || !matches!(prop.kind, oxc_ast::ast::PropertyKind::Init) { + continue; + } + let name = match &prop.key { + PropertyKey::StaticIdentifier(id) => id.name.to_string(), + PropertyKey::StringLiteral(s) => s.value.to_string(), + PropertyKey::NumericLiteral(n) => n.value.to_string(), + _ => continue, + }; + // A Map keeps the first key's position and the last value. + match entries.iter_mut().find(|(k, _)| *k == name) { + Some(entry) => entry.1 = &prop.value, + None => entries.push((name, &prop.value)), + } + } + entries +} + +/// Parse `queries:` from a decorator metadata object. +/// +/// Reference: `extractQueriesFromDecorator` / `extractDecoratorQueryMetadata` in +/// packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts +pub(crate) fn parse_decorator_queries<'a>( + allocator: &'a Allocator, + config: &'a oxc_ast::ast::ObjectExpression<'a>, + class: &'a Class<'a>, + source_text: Option<&'a str>, + consts: &super::StringConsts<'a>, + decorator_name: &str, +) -> DecoratorQueries<'a> { + let mut queries = DecoratorQueries { + view: Vec::new_in(&allocator), + content: Vec::new_in(&allocator), + error: None, + }; + let Some(query_data) = super::decorator::config_property(config, "queries", consts) else { + return queries; + }; + let Expression::ObjectExpression(query_data) = query_data else { + queries.error = Some(( + "Decorator queries metadata must be an object literal".into(), + query_data.span(), + )); + return queries; + }; + let evaluator = super::evaluator::Evaluator::new(consts); + // Where each query is written, for the collision error below. + let (mut content_exprs, mut view_exprs) = (std::vec::Vec::new(), std::vec::Vec::new()); + for (property_name, expr) in reflect_object_literal(query_data) { + let not_a_query = || { + Some(( + "Decorator query metadata must be an instance of a query type".into(), + query_data.span, + )) + }; + let Expression::NewExpression(new_expr) = unwrap_expression(expr) else { + queries.error = not_a_query(); + return queries; + }; + // `new ViewChild(...)` / `new core.ViewChild(...)`, imported from @angular/core. + let type_name = match &new_expr.callee { + Expression::Identifier(id) => consts + .scope() + .import(id.name.as_str()) + .filter(|i| i.module == "@angular/core") + .and_then(|i| i.imported), + Expression::StaticMemberExpression(m) => match &m.object { + Expression::Identifier(ns) => consts + .scope() + .import(ns.name.as_str()) + .filter(|i| i.module == "@angular/core" && i.imported.is_none()) + .map(|_| m.property.name.as_str()), + _ => None, + }, + _ => None, + }; + let Some(type_name) = type_name.filter(|t| QUERY_TYPES.contains(t)) else { + queries.error = not_a_query(); + return queries; + }; + match decorator_query( + allocator, + &evaluator, + type_name, + new_expr, + property_name, + source_text, + ) { + Ok(query) if type_name.starts_with("Content") => { + queries.content.push(query); + content_exprs.push(new_expr.span); + } + Ok(query) => { + queries.view.push(query); + view_exprs.push(new_expr.span); + } + Err(error) => { + queries.error = Some(error); + return queries; + } + } + } + + // A signal query member for the same property is an error. + let signal_queries: std::vec::Vec> = class + .body + .body + .iter() + .filter_map(|element| { + let ClassElement::PropertyDefinition(prop) = element else { return None }; + let name = get_property_key_name(&prop.key)?; + try_parse_signal_query(allocator, prop.value.as_ref()?, name.clone(), source_text) + .map(|_| name) + }) + .collect(); + // ngtsc checks the content queries first, and reports the `new` expression. + let collision = queries + .content + .iter() + .zip(content_exprs) + .chain(queries.view.iter().zip(view_exprs)) + .find(|(q, _)| signal_queries.contains(&q.property_name)); + if let Some((_, span)) = collision { + queries.error = Some(( + format!( + "Query is declared multiple times. \"@{decorator_name}\" declares a query for the same property." + ), + span, + )); + } + queries +} + +/// One `new ViewChild(predicate, options?)` and friends. An error comes with +/// the node ngtsc reports it on. +fn decorator_query<'a>( + allocator: &'a Allocator, + evaluator: &super::evaluator::Evaluator<'_, 'a>, + name: &str, + new_expr: &'a oxc_ast::ast::NewExpression<'a>, + property_name: String, + source_text: Option<&'a str>, +) -> Result, (String, Span)> { + use super::evaluator::Value; + let args = &new_expr.arguments; + let Some(first) = args.first().and_then(Argument::as_expression) else { + return Err((format!("@{name} must have arguments"), new_expr.span)); + }; + let node = try_unwrap_forward_ref(first).unwrap_or(first); + let at_node = |message: String| (message, node.span()); + let predicate = match evaluator.evaluate(node) { + Value::Reference { .. } | Value::Dynamic | Value::Function(_) => { + let expr = convert_oxc_expression(allocator, node, source_text) + .ok_or_else(|| at_node(format!("@{name} predicate cannot be interpreted")))?; + QueryPredicate::Type(expr) + } + Value::String(s) => { + let mut selectors = Vec::new_in(&allocator); + selectors.push(Ident::from(allocator.alloc_str(&s))); + QueryPredicate::Selectors(selectors) + } + Value::Array(items) => { + let mut selectors = Vec::new_in(&allocator); + for (i, item) in items.iter().enumerate() { + let Value::String(s) = item else { + return Err(at_node(format!( + "Failed to resolve @{name} predicate at position {i} to a string{}", + item.wrong_type_suffix() + ))); + }; + selectors.push(Ident::from(allocator.alloc_str(s))); + } + QueryPredicate::Selectors(selectors) + } + other => { + return Err(at_node(format!( + "@{name} predicate cannot be interpreted{}", + other.wrong_type_suffix() + ))); + } + }; + + let mut config = QueryConfig::default_for(name); + if args.len() == 2 { + let options = args[1].as_expression().map(unwrap_expression); + let Some(Expression::ObjectExpression(options)) = options else { + let span = options.map_or(args[1].span(), GetSpan::span); + return Err((format!("@{name} options must be an object literal"), span)); + }; + for (key, value) in reflect_object_literal(options) { + // ngtsc reports a bad `static` on the predicate, the others on their value. + let flag = |option: &str, span: Span| match evaluator.evaluate(value) { + Value::Bool(b) => Ok(b), + other => Err(( + format!( + "@{name} options.{option} must be a boolean{}", + other.wrong_type_suffix() + ), + span, + )), + }; + match key.as_str() { + "read" => config.read = convert_oxc_expression(allocator, value, source_text), + "descendants" => config.descendants = flag("descendants", value.span())?, + "emitDistinctChangesOnly" => { + config.emit_distinct_changes_only = + flag("emitDistinctChangesOnly", value.span())?; + } + "static" => config.is_static = flag("static", node.span())?, + _ => {} + } + } + } else if args.len() > 2 { + return Err(at_node(format!("@{name} has too many arguments"))); + } + + Ok(R3QueryMetadata { + property_name: Ident::from(allocator.alloc_str(&property_name)), + first: name == "ViewChild" || name == "ContentChild", + predicate, + descendants: config.descendants, + emit_distinct_changes_only: config.emit_distinct_changes_only, + read: config.read, + is_static: config.is_static, + is_signal: false, + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxc_angular_compiler/src/lib.rs b/crates/oxc_angular_compiler/src/lib.rs index 1ac650c5b..9311e5656 100644 --- a/crates/oxc_angular_compiler/src/lib.rs +++ b/crates/oxc_angular_compiler/src/lib.rs @@ -111,9 +111,10 @@ pub use directive::{ DirectiveCompileResult, DirectiveDefinitions, QueryPredicate, R3DirectiveMetadata, R3DirectiveMetadataBuilder, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3QueryMetadata, StringConsts, collect_string_consts, compile_directive, - compile_directive_from_metadata, extract_content_queries, extract_directive_metadata, - extract_host_bindings, extract_host_listeners, extract_input_metadata, extract_output_metadata, - extract_view_queries, find_directive_decorator_span, generate_directive_definitions, + compile_directive_from_metadata, extract_class_queries, extract_content_queries, + extract_directive_metadata, extract_host_bindings, extract_host_listeners, + extract_input_metadata, extract_output_metadata, extract_view_queries, + find_directive_decorator_span, generate_directive_definitions, }; // Re-export injectable types diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index a0d5987eb..826b59c7c 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() { failures.len(), failures.join("\n\n") ); - assert_eq!(compared, 575, "fixtures compared"); + assert_eq!(compared, 611, "fixtures compared"); } fn transform(source: &str) -> TransformResult { diff --git a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json index 94c50db12..fdbb84f0a 100644 --- a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json +++ b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json @@ -26,6 +26,39 @@ "dtsImports": [], "diagnosticSpans": [] }, + { + "name": "ngtsc_spec: should throw error if @Directive.queries field has wrong type", + "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", + "files": { + "test.ts": "\n import {Directive} from '@angular/core';\n\n @Directive({\n selector: 'test-dir',\n // @ts-ignore\n queries: 'invalid-queries-type'\n })\n export class TestDir {}\n " + }, + "diagnostics": ["Decorator queries metadata must be an object literal"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[157, 179]] + }, + { + "name": "ngtsc_spec: should throw error if @Directive.queries object has incorrect values", + "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", + "files": { + "test.ts": "\n import {Directive} from '@angular/core';\n\n @Directive({\n selector: 'test-dir',\n queries: {\n myViewQuery: 'invalid-query-type'\n }\n })\n export class TestDir {}\n " + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[131, 194]] + }, + { + "name": "ngtsc_spec: should throw error if @Directive.queries object has incorrect values (refs to other decorators)", + "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", + "files": { + "test.ts": "\n import {Directive, Input} from '@angular/core';\n\n @Directive({\n selector: 'test-dir',\n queries: {\n myViewQuery: new Input()\n }\n })\n export class TestDir {}\n " + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[154, 216]] + }, { "name": "ngtsc_spec: should produce a diagnostic if the transform value is not a function", "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", @@ -190,6 +223,53 @@ "dtsImports": [], "diagnosticSpans": [] }, + { + "name": "ngtsc_spec: should generate queries for components", + "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", + "files": { + "test.ts": "\n import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core';\n\n @Component({\n selector: 'test',\n template: '
',\n queries: {\n 'mview': new ViewChild('test1'),\n 'mcontent': new ContentChild('test2'),\n }\n })\n class FooCmp {\n @ContentChild('bar', {read: TemplateRef}) child: any;\n @ContentChildren(TemplateRef) children: any;\n get aview(): any { return null; }\n @ViewChild('accessor') set aview(value: any) {}\n }\n " + }, + "diagnostics": [], + "classes": { + "FooCmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"bar\"],5,TemplateRef)(dirIndex,TemplateRef,4)(dirIndex,[\"test2\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.child=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.children=_t);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.mcontent=_t.first);}}", + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"accessor\"],5)([\"test1\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.aview=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.mview=_t.first);}}" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "ngtsc_spec: should generate queries for directives", + "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", + "files": { + "test.ts": "\n import {Directive, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core';\n import * as core from '@angular/core';\n\n @Directive({\n selector: '[test]',\n queries: {\n 'mview': new ViewChild('test1'),\n 'mcontent': new core.ContentChild('test2'),\n }\n })\n class FooCmp {\n @ContentChild('bar', {read: TemplateRef}) child: any;\n @ContentChildren(TemplateRef) children: any;\n get aview(): any { return null; }\n @ViewChild('accessor') set aview(value: any) {}\n }\n " + }, + "diagnostics": [], + "classes": { + "FooCmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"bar\"],5,TemplateRef)(dirIndex,TemplateRef,4)(dirIndex,[\"test2\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.child=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.children=_t);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.mcontent=_t.first);}}", + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"accessor\"],5)([\"test1\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.aview=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.mview=_t.first);}}" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "ngtsc_spec: should handle queries that use forwardRef", + "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", + "files": { + "test.ts": "\n import {Component, ContentChild, TemplateRef, ViewContainerRef, forwardRef} from '@angular/core';\n\n @Component({\n selector: 'test',\n template: '
',\n })\n class FooCmp {\n @ContentChild(forwardRef(() => TemplateRef)) child: any;\n\n @ContentChild(forwardRef(function() { return ViewContainerRef; })) child2: any;\n\n @ContentChild((forwardRef((function() { return 'parens'; }) as any))) childInParens: any;\n }\n " + }, + "diagnostics": [], + "classes": { + "FooCmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,TemplateRef,5)(dirIndex,ViewContainerRef,5)(dirIndex,[\"parens\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.child=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.child2=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.childInParens=_t.first);}}" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, { "name": "ngtsc_spec: should handle queries that use an InjectionToken", "origin": "packages/compiler-cli/test/ngtsc/ngtsc_spec.ts", @@ -349,6 +429,43 @@ "dtsImports": [], "diagnosticSpans": [[160, 182]] }, + { + "name": "authoring_queries_spec: should report an error when declared in @Directive metadata", + "origin": "packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts", + "files": { + "test.ts": "\n import {Directive, ViewChild, viewChild} from '@angular/core';\n\n @Directive({\n selector: 'test',\n queries: {\n el: new ViewChild('myLocator'),\n },\n })\n export class TestDir {\n el = viewChild('myLocator');\n }\n " + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[159, 185]] + }, + { + "name": "authoring_queries_spec: should report an error when declared in @Component metadata", + "origin": "packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts", + "files": { + "test.ts": "\n import {Component, ViewChild, viewChild} from '@angular/core';\n\n @Component({\n selector: 'test',\n template: '',\n queries: {\n el: new ViewChild('myLocator'),\n },\n })\n export class TestComp {\n el = viewChild('myLocator');\n }\n " + }, + "diagnostics": [ + "Query is declared multiple times. \"@Component\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[183, 209]] + }, + { + "name": "authoring_queries_spec: should report an error when a signal-based query function is used in metadata", + "origin": "packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts", + "files": { + "test.ts": "\n import {Component, viewChild} from '@angular/core';\n\n @Component({\n selector: 'test',\n template: '',\n queries: {\n // @ts-ignore\n el: new viewChild('myLocator'),\n },\n })\n export class TestComp {}\n " + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[164, 253]] + }, { "name": "compliance/r3_view_compiler_bindings/order_bindings", "origin": "packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/order_bindings.ts", @@ -1088,6 +1205,381 @@ "dtsImports": [], "diagnosticSpans": [[158, 175]] }, + { + "name": "probe: queries-qBasic", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\"), b: new ViewChildren(TemplateRef, {read: ElementRef}), c: new ContentChild(\"y\", {descendants: false, static: true}), d: new ContentChildren(TemplateRef, {descendants: true, emitDistinctChangesOnly: false})}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"y\"],6)(dirIndex,TemplateRef,1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.c=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.d=_t);}}", + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5)(TemplateRef,5,ElementRef);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.b=_t);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qOrderWithFields", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {mc: new ContentChild(\"m\"), mv: new ViewChild(\"mv\")}})\nexport class Dir { @ContentChild(\"f\") fc: any; @ViewChild(\"fv\") fv: any; @ContentChildren(\"g\") gc: any; }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"f\"],5)(dirIndex,[\"g\"],4)(dirIndex,[\"m\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.fc=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.gc=_t);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.mc=_t.first);}}", + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"fv\"],5)([\"mv\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.fv=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.mv=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qNs", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new core.ViewChild(\"x\")}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qStrArr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChildren([\"x\", \"y\"])}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\",\"y\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qCommaStr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChildren(\"x, y\")}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\",\"y\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qConstStr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\nconst S = \"sel\";\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(S)}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"sel\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qForwardRef", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\nexport class T {}\n@Directive({selector: \"[d]\", queries: {a: new ContentChild(forwardRef(() => T))}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,T,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qViewDescFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\", {descendants: false})}})\nexport class Dir { }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-memberViewDescFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\"})\nexport class Dir { @ViewChild(\"x\", {descendants: false}) a: any; @ViewChildren(\"y\", {emitDistinctChangesOnly: false}) b: any; }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],4)([\"y\"],1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.b=_t);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-memberConstSel", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\nconst S = \"sel\";\n@Directive({selector: \"[d]\"})\nexport class Dir { @ViewChild(S) a: any; }\n" + }, + "diagnostics": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"sel\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: queries-qNotObject", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: \"bad\"})\nexport class Dir { }\n" + }, + "diagnostics": ["Decorator queries metadata must be an object literal"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[241, 246]] + }, + { + "name": "probe: queries-qStringValue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: \"bad\"}})\nexport class Dir { }\n" + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[241, 251]] + }, + { + "name": "probe: queries-qInput", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new Input()}})\nexport class Dir { }\n" + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[241, 257]] + }, + { + "name": "probe: queries-qSignalFn", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new (viewChild as any)(\"x\")}})\nexport class Dir { }\n" + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[241, 273]] + }, + { + "name": "probe: queries-qSignalFnBare", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new viewChild(\"x\")}})\nexport class Dir { }\n" + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[241, 264]] + }, + { + "name": "probe: queries-qCallNotNew", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: viewChild(\"x\")}})\nexport class Dir { }\n" + }, + "skip": "ngtsc also reports its separate check for signal query functions called outside a member initializer, which oxc doesn't implement" + }, + { + "name": "probe: queries-qNoArgs", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild()}})\nexport class Dir { }\n" + }, + "diagnostics": ["@ViewChild must have arguments"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[245, 260]] + }, + { + "name": "probe: queries-qTooMany", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\", {}, 1)}})\nexport class Dir { }\n" + }, + "diagnostics": ["@ViewChild has too many arguments"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[259, 262]] + }, + { + "name": "probe: queries-qOptsNotObj", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\", 1)}})\nexport class Dir { }\n" + }, + "diagnostics": [ + "@ViewChild options must be an object literal. Find more at https://next.angular.dev/errors/NG1001" + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[264, 265]] + }, + { + "name": "probe: queries-qDescNotBool", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\", {descendants: 1})}})\nexport class Dir { }\n" + }, + "diagnostics": [ + "@ViewChild options.descendants must be a boolean Value is of type 'number'." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[278, 279]] + }, + { + "name": "probe: queries-qStaticNotBool", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\", {static: \"y\"})}})\nexport class Dir { }\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[259, 262]] + }, + { + "name": "probe: queries-qEdcoNotBool", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(\"x\", {emitDistinctChangesOnly: 1})}})\nexport class Dir { }\n" + }, + "diagnostics": [ + "@ViewChild options.emitDistinctChangesOnly must be a boolean Value is of type 'number'." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[290, 291]] + }, + { + "name": "probe: queries-qPredNum", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChild(1)}})\nexport class Dir { }\n" + }, + "diagnostics": ["@ViewChild predicate cannot be interpreted Value is of type 'number'."], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[259, 260]] + }, + { + "name": "probe: queries-qPredArrMixed", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {a: new ViewChildren([\"x\", 1])}})\nexport class Dir { }\n" + }, + "diagnostics": [ + "Failed to resolve @ViewChildren predicate at position 1 to a string Value is of type 'number'." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[262, 270]] + }, + { + "name": "probe: queries-qSignalCollision", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {el: new ViewChild(\"x\")}})\nexport class Dir { el = viewChild(\"x\"); }\n" + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[246, 264]] + }, + { + "name": "probe: queries-qSignalCollisionContent", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Directive({selector: \"[d]\", queries: {el: new ContentChild(\"x\"), v: new ViewChild(\"y\")}})\nexport class Dir { el = core.contentChild(\"x\"); v = viewChild(\"y\"); }\n" + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[246, 267]] + }, + { + "name": "probe: queries-qLocalClass", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\nclass ViewChild2 {}\n@Directive({selector: \"[d]\", queries: {a: new ViewChild2(\"x\")}})\nexport class Dir { }\n" + }, + "diagnostics": ["Decorator query metadata must be an instance of a query type"], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[260, 284]] + }, + { + "name": "probe: queries-qComponent", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, ViewChild, ViewChildren, ContentChild, ContentChildren, TemplateRef, ElementRef, forwardRef, viewChild} from \"@angular/core\";\nimport * as core from \"@angular/core\";\n\n@Component({selector: \"c\", template: \"
\", queries: {v: new ViewChild(\"x\"), c: new ContentChild(\"y\")}})\nexport class Cmp {}\n" + }, + "diagnostics": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"y\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.c=_t.first);}}", + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.v=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, { "name": "probe: resolve-fnMultiStmt", "origin": "probe", diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index e4ef593ed..e632c6f85 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -1553,8 +1553,8 @@ pub fn extract_component_metadata_sync( use oxc_angular_compiler::{ ChangeDetectionStrategy as RustChangeDetection, QueryPredicate, ViewEncapsulation as RustViewEncapsulation, build_import_map, collect_string_consts, - extract_component_metadata, extract_content_queries, extract_input_metadata, - extract_output_metadata, extract_view_queries, + extract_class_queries, extract_component_metadata, extract_input_metadata, + extract_output_metadata, }; use oxc_ast::ast::{Declaration, ExportDefaultDeclarationKind, Statement}; use oxc_parser::Parser; @@ -1720,8 +1720,10 @@ pub fn extract_component_metadata_sync( } } - // Extract view queries from @ViewChild/@ViewChildren decorators - let rust_view_queries = extract_view_queries(&allocator, class, Some(&source)); + // View and content queries exactly as the component is compiled + // (member queries, then `queries:` metadata). + let (rust_view_queries, rust_content_queries) = + extract_class_queries(&allocator, class, Some(&source), &string_consts); let view_queries: Option> = if rust_view_queries.is_empty() { None @@ -1741,9 +1743,6 @@ pub fn extract_component_metadata_sync( ) }; - // Extract content queries from @ContentChild/@ContentChildren decorators - let rust_content_queries = - extract_content_queries(&allocator, class, Some(&source)); let queries: Option> = if rust_content_queries.is_empty() { None diff --git a/napi/angular-compiler/test/extract-component-metadata.test.ts b/napi/angular-compiler/test/extract-component-metadata.test.ts new file mode 100644 index 000000000..576ec4e84 --- /dev/null +++ b/napi/angular-compiler/test/extract-component-metadata.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' + +import { extractComponentMetadataSync } from '../index.js' + +// The queries reported must be the ones the component is compiled with. +describe('extractComponentMetadataSync queries', () => { + it('resolves const selectors and includes `queries:` metadata after member queries', () => { + const [component] = extractComponentMetadataSync( + ` +import { Component, ContentChild, ViewChild } from '@angular/core'; + +const SEL = 'ref'; + +@Component({ + selector: 'app-x', + template: '
', + queries: { + fromMetaView: new ViewChild('metaRef'), + fromMetaContent: new ContentChild('metaContent', { descendants: false }), + }, +}) +export class X { + @ViewChild(SEL) member: any; + @ContentChild('memberContent') memberContent: any; +} +`, + 'x.component.ts', + ) + + expect(component.viewQueries?.map((q) => [q.propertyName, q.predicate])).toEqual([ + ['member', '["ref"]'], + ['fromMetaView', '["metaRef"]'], + ]) + expect(component.queries?.map((q) => [q.propertyName, q.predicate, q.descendants])).toEqual([ + ['memberContent', '["memberContent"]', true], + ['fromMetaContent', '["metaContent"]', false], + ]) + }) +}) From 1dc1d7c0e73e2f9256c2fbc4d09c74ec8dd87aa1 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 30 Sep 2026 14:30:49 +0800 Subject: [PATCH 2/6] fix(queries): tighten forwardRef and imported-value handling - `ns.forwardRef(...)` is only unwrapped when `ns` is a namespace import of `@angular/core`, like ngtsc; `util.forwardRef(() => X)` on any other object is kept as the predicate. A bare `forwardRef` is still matched by name. - A `queries:` option or predicate element imported from another file (`{descendants: FLAG}`, `new ViewChild([...SELS])`) now says OXC can't read other files, as `inputs:`/`outputs:` do, instead of ngtsc's wrong-type error for a value it never read. - Member decorators through a namespace (`@core.Input()`) are matched with the file's imports passed in, not a thread-local set by the compiler, so `extractComponentMetadataSync` reads them too (and still ignores a namespace that isn't `@angular/core`). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/component/decorator.rs | 19 +- .../src/component/transform.rs | 24 -- .../src/directive/decorator.rs | 4 +- .../src/directive/metadata.rs | 17 +- .../oxc_angular_compiler/src/directive/mod.rs | 9 +- .../src/directive/property_decorators.rs | 220 ++++++++++++------ crates/oxc_angular_compiler/src/lib.rs | 5 +- .../tests/decorator_metadata_ngtsc_test.rs | 111 ++++++++- .../fixtures/decorator_metadata_ngtsc.json | 146 ++++++++++++ napi/angular-compiler/src/lib.rs | 19 +- .../test/extract-component-metadata.test.ts | 33 +++ 11 files changed, 478 insertions(+), 129 deletions(-) diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index a9ec45c00..0a093e6b6 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -18,8 +18,8 @@ use super::metadata::{ }; use super::transform::ImportMap; use crate::directive::{ - StringConsts, extract_host_bindings, extract_host_listeners, extract_input_metadata, - extract_output_metadata, merge_by_class_property, parse_decorator_io, + StringConsts, extract_host_bindings_in, extract_host_listeners_in, extract_input_metadata_in, + extract_output_metadata_in, merge_by_class_property, parse_decorator_io, resolve_member_transforms, }; use crate::output::oxc_converter::convert_oxc_expression; @@ -214,8 +214,8 @@ pub fn extract_component_metadata<'a>( // Extract host bindings and listeners from @HostBinding/@HostListener decorators on class members // These are merged with any host metadata from the @Component({ host: {} }) property - let host_bindings = extract_host_bindings(allocator, class); - let host_listeners = extract_host_listeners(allocator, class); + let host_bindings = extract_host_bindings_in(allocator, class, Some(consts)); + let host_listeners = extract_host_listeners_in(allocator, class, Some(consts)); if !host_bindings.is_empty() || !host_listeners.is_empty() { let host = metadata.host.get_or_insert_with(|| HostMetadata::new(allocator)); @@ -267,14 +267,15 @@ pub fn extract_component_metadata<'a>( let io = parse_decorator_io(allocator, config_obj, class, source_text, consts); metadata.inputs = merge_by_class_property( io.inputs, - extract_input_metadata(allocator, class, source_text), + extract_input_metadata_in(allocator, class, source_text, Some(consts)), |i| i.class_property_name.as_str(), ); resolve_member_transforms(allocator, class, source_text, consts, &mut metadata.inputs); - metadata.outputs = - merge_by_class_property(io.outputs, extract_output_metadata(allocator, class), |o| { - o.0.as_str() - }); + metadata.outputs = merge_by_class_property( + io.outputs, + extract_output_metadata_in(allocator, class, Some(consts)), + |o| o.0.as_str(), + ); // Detect if ngOnChanges lifecycle hook is implemented // Similar to Angular's: const usesOnChanges = members.some(member => ...) diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 8c7d4f9ba..a2e858078 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -2536,30 +2536,6 @@ pub fn transform_angular_file( } } - // `@core.Input()` and the other member decorators through a namespace - // import of `@angular/core`, as written: `resolved_imports` may have - // pointed the import map's entry at another path. - let _core_namespaces = crate::directive::CoreNamespaces::enter( - parser_ret - .program - .body - .iter() - .filter_map(|stmt| match stmt { - Statement::ImportDeclaration(import) if import.source.value == "@angular/core" => { - import.specifiers.as_ref() - } - _ => None, - }) - .flatten() - .filter_map(|spec| match spec { - ImportDeclarationSpecifier::ImportNamespaceSpecifier(ns) => { - Some(ns.local.name.to_string()) - } - _ => None, - }) - .collect(), - ); - // 2. Walk AST to find @Component decorated classes and extract metadata for stmt in &parser_ret.program.body { let (class, stmt_start) = match stmt { diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index f8e5522ee..151727002 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -973,7 +973,7 @@ pub(crate) fn resolve_member_transforms<'a>( _ => continue, }; let Some(name) = key.static_name() else { continue }; - let options = super::property_decorators::input_decorator_options(decorators); + let options = super::property_decorators::input_decorator_options(decorators, consts); let Some(options) = options else { continue }; let options = evaluator.evaluate(options); let Some(transform) = options.prop("transform") else { continue }; @@ -1084,7 +1084,7 @@ pub fn decorator_io_errors<'a>( }; let name = key.static_name()?; // `@Input({ transform })` - let options = super::property_decorators::input_decorator_options(decorators); + let options = super::property_decorators::input_decorator_options(decorators, consts); if let Some(options) = options { let span = options.span(); let options = evaluator.evaluate(options); diff --git a/crates/oxc_angular_compiler/src/directive/metadata.rs b/crates/oxc_angular_compiler/src/directive/metadata.rs index d3486eaae..a5365fd41 100644 --- a/crates/oxc_angular_compiler/src/directive/metadata.rs +++ b/crates/oxc_angular_compiler/src/directive/metadata.rs @@ -429,14 +429,19 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { consts: Option<&super::StringConsts<'a>>, ) -> Self { // Extract inputs from @Input decorators - let inputs = - super::property_decorators::extract_input_metadata(allocator, class, source_text); + let inputs = super::property_decorators::extract_input_metadata_in( + allocator, + class, + source_text, + consts, + ); for input in inputs { self = self.add_input(input); } // Extract outputs from @Output decorators - let outputs = super::property_decorators::extract_output_metadata(allocator, class); + let outputs = + super::property_decorators::extract_output_metadata_in(allocator, class, consts); for (class_name, binding_name) in outputs { self = self.add_output(class_name, binding_name); } @@ -465,7 +470,8 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { // Extract host bindings from @HostBinding // Wrap with brackets: "class.active" -> "[class.active]" - let host_bindings = super::property_decorators::extract_host_bindings(allocator, class); + let host_bindings = + super::property_decorators::extract_host_bindings_in(allocator, class, consts); for (host_prop, class_prop) in host_bindings { // Add to host.properties with wrapped key let wrapped_key = @@ -476,7 +482,8 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { // Extract host listeners from @HostListener // Wrap event name with parentheses and build method expression with args // Reference: Angular's shared.ts:713 - `bindings.listeners[eventName] = \`${member.name}(${args.join(',')})\`` - let host_listeners = super::property_decorators::extract_host_listeners(allocator, class); + let host_listeners = + super::property_decorators::extract_host_listeners_in(allocator, class, consts); for (event_name, method_name, args) in host_listeners { // Wrap event name: "click" -> "(click)" let wrapped_key = diff --git a/crates/oxc_angular_compiler/src/directive/mod.rs b/crates/oxc_angular_compiler/src/directive/mod.rs index eeecd6d61..f058580e3 100644 --- a/crates/oxc_angular_compiler/src/directive/mod.rs +++ b/crates/oxc_angular_compiler/src/directive/mod.rs @@ -41,13 +41,14 @@ pub use metadata::{ QueryPredicate, R3DirectiveMetadata, R3DirectiveMetadataBuilder, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3QueryMetadata, }; -pub(crate) use property_decorators::CoreNamespaces; pub use property_decorators::{ extract_class_queries, extract_content_queries, extract_host_bindings, extract_host_listeners, - extract_input_metadata, extract_output_metadata, extract_view_queries, + extract_input_metadata, extract_input_metadata_in, extract_output_metadata, + extract_output_metadata_in, extract_view_queries, }; pub(crate) use property_decorators::{ - parse_decorator_queries, try_parse_signal_input, try_parse_signal_model, - try_parse_signal_output, unwrap_initializer_api_expr, + extract_host_bindings_in, extract_host_listeners_in, parse_decorator_queries, + try_parse_signal_input, try_parse_signal_model, try_parse_signal_output, + unwrap_initializer_api_expr, }; pub use query::{create_content_queries_function, create_view_queries_function}; diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index b2d3f8e8a..c253866c0 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -11,8 +11,6 @@ //! These decorators are found on class properties and methods, and define //! how the directive/component interacts with its parent context. -use std::cell::RefCell; - use oxc_allocator::{Allocator, Vec}; use oxc_ast::ast::{ Argument, ArrayExpressionElement, Class, ClassElement, Decorator, Expression, @@ -29,30 +27,16 @@ use crate::output::oxc_converter::convert_oxc_expression; // Helper Functions // ============================================================================ -thread_local! { - /// The local names of `import * as ns from '@angular/core'` in the file - /// being compiled (see [`CoreNamespaces`]). - static CORE_NAMESPACES: RefCell> = - const { RefCell::new(std::vec::Vec::new()) }; -} - -/// While alive, member decorators written through these namespace imports of -/// `@angular/core` (`@core.Input()`) are recognised. ngtsc only reads a -/// namespaced member decorator whose namespace imports `@angular/core`, and the -/// functions here that extract member decorators don't see the file's imports. -pub(crate) struct CoreNamespaces(std::vec::Vec); - -impl CoreNamespaces { - pub(crate) fn enter(names: std::vec::Vec) -> Self { - Self(CORE_NAMESPACES.with(|current| current.replace(names))) - } -} - -impl Drop for CoreNamespaces { - fn drop(&mut self) { - let previous = std::mem::take(&mut self.0); - CORE_NAMESPACES.with(|current| *current.borrow_mut() = previous); - } +/// Whether `name` is a namespace import of `@angular/core` +/// (`import * as core from '@angular/core'`) in the file `consts` was collected +/// from. Without the file (`None`), no name is. +/// +/// ngtsc only reads a namespaced member decorator (`@core.Input()`) or +/// `core.forwardRef(...)` through such an import (`getImportOfIdentifier`). +fn is_core_namespace(consts: Option<&super::StringConsts<'_>>, name: &str) -> bool { + consts + .and_then(|consts| consts.scope().import(name)) + .is_some_and(|import| import.module == "@angular/core" && import.imported.is_none()) } /// Find a decorator by name from a list of decorators. @@ -60,21 +44,20 @@ impl Drop for CoreNamespaces { /// Searches for decorators that are either: /// - Simple identifiers: `@Input` /// - Call expressions: `@Input()` or `@Input('alias')` -/// - Either of those through a namespace import of `@angular/core` (see -/// [`CoreNamespaces`]): `@core.Input()` +/// - Either of those through a namespace import of `@angular/core` in the +/// file `consts` was collected from (see [`is_core_namespace`]): `@core.Input()` /// /// Returns the first matching decorator. fn find_decorator_by_name<'a>( decorators: &'a oxc_allocator::Vec<'a, Decorator<'a>>, name: &str, + consts: Option<&super::StringConsts<'_>>, ) -> Option<&'a Decorator<'a>> { - let is_core_namespace = - |ns: &str| CORE_NAMESPACES.with(|names| names.borrow().iter().any(|n| n == ns)); let is_name = |expr: &Expression<'_>| match expr { Expression::Identifier(id) => id.name == name, Expression::StaticMemberExpression(m) => { m.property.name == name - && matches!(&m.object, Expression::Identifier(ns) if is_core_namespace(&ns.name)) + && matches!(&m.object, Expression::Identifier(ns) if is_core_namespace(consts, &ns.name)) } _ => false, }; @@ -87,8 +70,9 @@ fn find_decorator_by_name<'a>( /// The options argument of an `@Input(...)` decorator, if any. pub(crate) fn input_decorator_options<'a>( decorators: &'a oxc_allocator::Vec<'a, Decorator<'a>>, + consts: &super::StringConsts<'_>, ) -> Option<&'a Expression<'a>> { - match &find_decorator_by_name(decorators, "Input")?.expression { + match &find_decorator_by_name(decorators, "Input", Some(consts))?.expression { Expression::CallExpression(call) => call.arguments.first()?.as_expression(), _ => None, } @@ -131,14 +115,25 @@ fn extract_boolean_value(expr: &Expression<'_>) -> Option { /// Mirrors ngtsc's `tryUnwrapForwardRef`: `forwardRef(() => X)`, /// `forwardRef(function () { return X; })`, looking through parentheses and /// `as` casts. Returns `None` for anything else. -fn try_unwrap_forward_ref<'a>(expr: &'a Expression<'a>) -> Option<&'a Expression<'a>> { +/// +/// Like ngtsc, `ns.forwardRef(...)` is only unwrapped when `ns` is a namespace +/// import of `@angular/core` (see [`is_core_namespace`]); `util.forwardRef` +/// on any other object is kept as written. A bare `forwardRef` is matched by +/// name. +fn try_unwrap_forward_ref<'a>( + expr: &'a Expression<'a>, + consts: Option<&super::StringConsts<'_>>, +) -> Option<&'a Expression<'a>> { let Expression::CallExpression(call) = unwrap_expression(expr) else { return None }; - let callee = match &call.callee { - Expression::Identifier(id) => id.name.as_str(), - Expression::StaticMemberExpression(m) => m.property.name.as_str(), - _ => return None, + let is_forward_ref = match &call.callee { + Expression::Identifier(id) => id.name == "forwardRef", + Expression::StaticMemberExpression(m) => { + m.property.name == "forwardRef" + && matches!(&m.object, Expression::Identifier(ns) if is_core_namespace(consts, &ns.name)) + } + _ => false, }; - if callee != "forwardRef" || call.arguments.len() != 1 { + if !is_forward_ref || call.arguments.len() != 1 { return None; } let body = match unwrap_expression(call.arguments[0].as_expression()?) { @@ -546,6 +541,18 @@ pub fn extract_input_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, source_text: Option<&'a str>, +) -> Vec<'a, R3InputMetadata<'a>> { + extract_input_metadata_in(allocator, class, source_text, None) +} + +/// [`extract_input_metadata`] for a class in the file `consts` was collected +/// from, which also recognises `@core.Input()` through a namespace import of +/// `@angular/core` (see [`is_core_namespace`]). +pub fn extract_input_metadata_in<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + consts: Option<&super::StringConsts<'a>>, ) -> Vec<'a, R3InputMetadata<'a>> { let mut inputs = Vec::new_in(&allocator); @@ -553,7 +560,7 @@ pub fn extract_input_metadata<'a>( match element { ClassElement::PropertyDefinition(prop) => { // First check for @Input decorator - if let Some(decorator) = find_decorator_by_name(&prop.decorators, "Input") { + if let Some(decorator) = find_decorator_by_name(&prop.decorators, "Input", consts) { let Some(class_property_name) = get_property_key_name(&prop.key) else { continue; }; @@ -591,7 +598,8 @@ pub fn extract_input_metadata<'a>( } ClassElement::AccessorProperty(prop) => { - let Some(decorator) = find_decorator_by_name(&prop.decorators, "Input") else { + let Some(decorator) = find_decorator_by_name(&prop.decorators, "Input", consts) + else { continue; }; @@ -615,7 +623,8 @@ pub fn extract_input_metadata<'a>( // Methods with @Input decorator (setter-based inputs) ClassElement::MethodDefinition(method) => { - let Some(decorator) = find_decorator_by_name(&method.decorators, "Input") else { + let Some(decorator) = find_decorator_by_name(&method.decorators, "Input", consts) + else { continue; }; @@ -697,6 +706,17 @@ fn parse_output_config<'a>(decorator: &'a Decorator<'a>) -> OutputConfig<'a> { pub fn extract_output_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, +) -> Vec<'a, (Ident<'a>, Ident<'a>)> { + extract_output_metadata_in(allocator, class, None) +} + +/// [`extract_output_metadata`] for a class in the file `consts` was collected +/// from, which also recognises `@core.Output()` through a namespace import of +/// `@angular/core` (see [`is_core_namespace`]). +pub fn extract_output_metadata_in<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + consts: Option<&super::StringConsts<'a>>, ) -> Vec<'a, (Ident<'a>, Ident<'a>)> { let mut outputs = Vec::new_in(&allocator); @@ -704,7 +724,8 @@ pub fn extract_output_metadata<'a>( match element { ClassElement::PropertyDefinition(prop) => { // First check for @Output decorator - if let Some(decorator) = find_decorator_by_name(&prop.decorators, "Output") { + if let Some(decorator) = find_decorator_by_name(&prop.decorators, "Output", consts) + { let Some(class_property_name) = get_property_key_name(&prop.key) else { continue; }; @@ -736,7 +757,8 @@ pub fn extract_output_metadata<'a>( } ClassElement::AccessorProperty(prop) => { - let Some(decorator) = find_decorator_by_name(&prop.decorators, "Output") else { + let Some(decorator) = find_decorator_by_name(&prop.decorators, "Output", consts) + else { continue; }; @@ -834,7 +856,7 @@ fn parse_query_config<'a>( // The predicate: a string selector, a string array, or a type/token. // forwardRef isn't included in compiled output. - let node = try_unwrap_forward_ref(first_arg).unwrap_or(first_arg); + let node = try_unwrap_forward_ref(first_arg, consts).unwrap_or(first_arg); let selectors = match (consts, node) { (Some(consts), _) => match super::evaluator::Evaluator::new(consts).evaluate(node) { super::evaluator::Value::String(s) => Some(std::vec![s]), @@ -942,6 +964,7 @@ fn try_parse_signal_query<'a>( value: &'a Expression<'a>, property_name: Ident<'a>, source_text: Option<&'a str>, + consts: Option<&super::StringConsts<'a>>, ) -> Option<(SignalQueryType, R3QueryMetadata<'a>)> { // Check if the value is a call expression (unwrapping `as`/parenthesized). let call_expr = match unwrap_initializer_api_expr(value) { @@ -1016,7 +1039,7 @@ fn try_parse_signal_query<'a>( _ => { let expr = predicate_arg.to_expression(); // Unwrap forwardRef if present - Angular doesn't include forwardRef in compiled output - let unwrapped_expr = try_unwrap_forward_ref(expr).unwrap_or(expr); + let unwrapped_expr = try_unwrap_forward_ref(expr, consts).unwrap_or(expr); let output_expr = convert_oxc_expression(allocator, unwrapped_expr, source_text)?; QueryPredicate::Type(output_expr) } @@ -1113,9 +1136,13 @@ pub(crate) fn extract_view_queries_in<'a>( // Check for signal-based view queries first (viewChild(), viewChildren()) if let Some(value) = &prop.value { if let Some(property_name) = get_property_key_name(&prop.key) { - if let Some((query_type, metadata)) = - try_parse_signal_query(allocator, value, property_name, source_text) - { + if let Some((query_type, metadata)) = try_parse_signal_query( + allocator, + value, + property_name, + source_text, + consts, + ) { if query_type.is_view_query() { signal_queries.push(metadata); continue; @@ -1125,7 +1152,9 @@ pub(crate) fn extract_view_queries_in<'a>( } // Check for decorator-based queries (@ViewChild, @ViewChildren) - if let Some(decorator) = find_decorator_by_name(&prop.decorators, "ViewChild") { + if let Some(decorator) = + find_decorator_by_name(&prop.decorators, "ViewChild", consts) + { if let Some(property_name) = get_property_key_name(&prop.key) { let config = parse_query_config( allocator, @@ -1148,7 +1177,7 @@ pub(crate) fn extract_view_queries_in<'a>( } } } else if let Some(decorator) = - find_decorator_by_name(&prop.decorators, "ViewChildren") + find_decorator_by_name(&prop.decorators, "ViewChildren", consts) { if let Some(property_name) = get_property_key_name(&prop.key) { let config = parse_query_config( @@ -1177,7 +1206,9 @@ pub(crate) fn extract_view_queries_in<'a>( if matches!(method.kind, MethodDefinitionKind::Set | MethodDefinitionKind::Get) => { // Check for decorator-based queries on setters/getters - if let Some(decorator) = find_decorator_by_name(&method.decorators, "ViewChild") { + if let Some(decorator) = + find_decorator_by_name(&method.decorators, "ViewChild", consts) + { if let Some(property_name) = get_property_key_name(&method.key) { let config = parse_query_config( allocator, @@ -1200,7 +1231,7 @@ pub(crate) fn extract_view_queries_in<'a>( } } } else if let Some(decorator) = - find_decorator_by_name(&method.decorators, "ViewChildren") + find_decorator_by_name(&method.decorators, "ViewChildren", consts) { if let Some(property_name) = get_property_key_name(&method.key) { let config = parse_query_config( @@ -1287,9 +1318,13 @@ pub(crate) fn extract_content_queries_in<'a>( // Check for signal-based content queries first (contentChild(), contentChildren()) if let Some(value) = &prop.value { if let Some(property_name) = get_property_key_name(&prop.key) { - if let Some((query_type, metadata)) = - try_parse_signal_query(allocator, value, property_name, source_text) - { + if let Some((query_type, metadata)) = try_parse_signal_query( + allocator, + value, + property_name, + source_text, + consts, + ) { if !query_type.is_view_query() { signal_queries.push(metadata); continue; @@ -1299,7 +1334,9 @@ pub(crate) fn extract_content_queries_in<'a>( } // Check for decorator-based queries (@ContentChild, @ContentChildren) - if let Some(decorator) = find_decorator_by_name(&prop.decorators, "ContentChild") { + if let Some(decorator) = + find_decorator_by_name(&prop.decorators, "ContentChild", consts) + { if let Some(property_name) = get_property_key_name(&prop.key) { let config = parse_query_config( allocator, @@ -1322,7 +1359,7 @@ pub(crate) fn extract_content_queries_in<'a>( } } } else if let Some(decorator) = - find_decorator_by_name(&prop.decorators, "ContentChildren") + find_decorator_by_name(&prop.decorators, "ContentChildren", consts) { if let Some(property_name) = get_property_key_name(&prop.key) { let config = parse_query_config( @@ -1351,7 +1388,8 @@ pub(crate) fn extract_content_queries_in<'a>( if matches!(method.kind, MethodDefinitionKind::Set | MethodDefinitionKind::Get) => { // Check for decorator-based queries on setters/getters - if let Some(decorator) = find_decorator_by_name(&method.decorators, "ContentChild") + if let Some(decorator) = + find_decorator_by_name(&method.decorators, "ContentChild", consts) { if let Some(property_name) = get_property_key_name(&method.key) { let config = parse_query_config( @@ -1375,7 +1413,7 @@ pub(crate) fn extract_content_queries_in<'a>( } } } else if let Some(decorator) = - find_decorator_by_name(&method.decorators, "ContentChildren") + find_decorator_by_name(&method.decorators, "ContentChildren", consts) { if let Some(property_name) = get_property_key_name(&method.key) { let config = parse_query_config( @@ -1433,6 +1471,16 @@ pub(crate) fn extract_content_queries_in<'a>( pub fn extract_host_bindings<'a>( allocator: &'a Allocator, class: &'a Class<'a>, +) -> Vec<'a, (Ident<'a>, Ident<'a>)> { + extract_host_bindings_in(allocator, class, None) +} + +/// [`extract_host_bindings`] for a class in the file `consts` was collected +/// from, which also recognises `@core.HostBinding()` (see [`is_core_namespace`]). +pub(crate) fn extract_host_bindings_in<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + consts: Option<&super::StringConsts<'a>>, ) -> Vec<'a, (Ident<'a>, Ident<'a>)> { let mut bindings = Vec::new_in(&allocator); @@ -1450,7 +1498,7 @@ pub fn extract_host_bindings<'a>( _ => continue, }; - let Some(decorator) = find_decorator_by_name(decorators, "HostBinding") else { + let Some(decorator) = find_decorator_by_name(decorators, "HostBinding", consts) else { continue; }; @@ -1505,6 +1553,16 @@ fn extract_host_binding_name<'a>(decorator: &'a Decorator<'a>) -> Option( allocator: &'a Allocator, class: &'a Class<'a>, +) -> Vec<'a, (Ident<'a>, Ident<'a>, Vec<'a, Ident<'a>>)> { + extract_host_listeners_in(allocator, class, None) +} + +/// [`extract_host_listeners`] for a class in the file `consts` was collected +/// from, which also recognises `@core.HostListener()` (see [`is_core_namespace`]). +pub(crate) fn extract_host_listeners_in<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + consts: Option<&super::StringConsts<'a>>, ) -> Vec<'a, (Ident<'a>, Ident<'a>, Vec<'a, Ident<'a>>)> { let mut listeners = Vec::new_in(&allocator); @@ -1520,7 +1578,7 @@ pub fn extract_host_listeners<'a>( _ => continue, }; - let Some(decorator) = find_decorator_by_name(decorators, "HostListener") else { + let Some(decorator) = find_decorator_by_name(decorators, "HostListener", consts) else { continue; }; @@ -1709,11 +1767,9 @@ pub(crate) fn parse_decorator_queries<'a>( .filter(|i| i.module == "@angular/core") .and_then(|i| i.imported), Expression::StaticMemberExpression(m) => match &m.object { - Expression::Identifier(ns) => consts - .scope() - .import(ns.name.as_str()) - .filter(|i| i.module == "@angular/core" && i.imported.is_none()) - .map(|_| m.property.name.as_str()), + Expression::Identifier(ns) if is_core_namespace(Some(consts), &ns.name) => { + Some(m.property.name.as_str()) + } _ => None, }, _ => None, @@ -1724,6 +1780,7 @@ pub(crate) fn parse_decorator_queries<'a>( }; match decorator_query( allocator, + consts, &evaluator, type_name, new_expr, @@ -1753,8 +1810,14 @@ pub(crate) fn parse_decorator_queries<'a>( .filter_map(|element| { let ClassElement::PropertyDefinition(prop) = element else { return None }; let name = get_property_key_name(&prop.key)?; - try_parse_signal_query(allocator, prop.value.as_ref()?, name.clone(), source_text) - .map(|_| name) + try_parse_signal_query( + allocator, + prop.value.as_ref()?, + name.clone(), + source_text, + Some(consts), + ) + .map(|_| name) }) .collect(); // ngtsc checks the content queries first, and reports the `new` expression. @@ -1779,6 +1842,7 @@ pub(crate) fn parse_decorator_queries<'a>( /// the node ngtsc reports it on. fn decorator_query<'a>( allocator: &'a Allocator, + consts: &super::StringConsts<'a>, evaluator: &super::evaluator::Evaluator<'_, 'a>, name: &str, new_expr: &'a oxc_ast::ast::NewExpression<'a>, @@ -1790,7 +1854,7 @@ fn decorator_query<'a>( let Some(first) = args.first().and_then(Argument::as_expression) else { return Err((format!("@{name} must have arguments"), new_expr.span)); }; - let node = try_unwrap_forward_ref(first).unwrap_or(first); + let node = try_unwrap_forward_ref(first, Some(consts)).unwrap_or(first); let at_node = |message: String| (message, node.span()); let predicate = match evaluator.evaluate(node) { Value::Reference { .. } | Value::Dynamic | Value::Function(_) => { @@ -1807,9 +1871,14 @@ fn decorator_query<'a>( let mut selectors = Vec::new_in(&allocator); for (i, item) in items.iter().enumerate() { let Value::String(s) = item else { - return Err(at_node(format!( - "Failed to resolve @{name} predicate at position {i} to a string{}", - item.wrong_type_suffix() + return Err(at_node(super::decorator::value_error( + &format!("@{name} predicate"), + || { + format!( + "Failed to resolve @{name} predicate at position {i} to a string" + ) + }, + item, ))); }; selectors.push(Ident::from(allocator.alloc_str(s))); @@ -1836,9 +1905,10 @@ fn decorator_query<'a>( let flag = |option: &str, span: Span| match evaluator.evaluate(value) { Value::Bool(b) => Ok(b), other => Err(( - format!( - "@{name} options.{option} must be a boolean{}", - other.wrong_type_suffix() + super::decorator::value_error( + &format!("@{name} options.{option}"), + || format!("@{name} options.{option} must be a boolean"), + &other, ), span, )), diff --git a/crates/oxc_angular_compiler/src/lib.rs b/crates/oxc_angular_compiler/src/lib.rs index 9311e5656..4e9375622 100644 --- a/crates/oxc_angular_compiler/src/lib.rs +++ b/crates/oxc_angular_compiler/src/lib.rs @@ -113,8 +113,9 @@ pub use directive::{ R3QueryMetadata, StringConsts, collect_string_consts, compile_directive, compile_directive_from_metadata, extract_class_queries, extract_content_queries, extract_directive_metadata, extract_host_bindings, extract_host_listeners, - extract_input_metadata, extract_output_metadata, extract_view_queries, - find_directive_decorator_span, generate_directive_definitions, + extract_input_metadata, extract_input_metadata_in, extract_output_metadata, + extract_output_metadata_in, extract_view_queries, find_directive_decorator_span, + generate_directive_definitions, }; // Re-export injectable types diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index 826b59c7c..426a4038e 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() { failures.len(), failures.join("\n\n") ); - assert_eq!(compared, 611, "fixtures compared"); + assert_eq!(compared, 620, "fixtures compared"); } fn transform(source: &str) -> TransformResult { @@ -926,7 +926,14 @@ export class Cmp { let foreign = "import {Component} from '@angular/core'; import * as NS from 'foreign-decorators'; @Component({selector: 'c', template: '
'})"; - for (header, compiled) in [(core, true), (foreign, false)] { + let core_directive = "import * as NS from '@angular/core'; +@NS.Directive({selector: '[d]'})"; + let foreign_directive = "import {Directive} from '@angular/core'; +import * as NS from 'foreign-decorators'; +@Directive({selector: '[d]'})"; + for (header, compiled) in + [(core, true), (foreign, false), (core_directive, true), (foreign_directive, false)] + { let code = transform(&format!("{header}{members}")).code; for part in ["viewQuery", "ɵɵclassProp(\"a\"", "ɵɵlistener(\"click\""] { assert_eq!(code.contains(part), compiled, "{part} in\n{code}"); @@ -961,3 +968,103 @@ export class Cmp { assert!(code.contains(r#"inputs:{x:"x"}"#), "{}", result.code); assert!(code.contains(r#"outputs:{y:"y"}"#), "{}", result.code); } + +/// The same through the extraction API (what the NAPI +/// `extractComponentMetadataSync` reports): given the file, `@NS.Input()` and +/// friends count only for a namespace import of `@angular/core`; without it, +/// none do. +#[test] +fn namespaced_member_decorators_through_the_extraction_api() { + use oxc_angular_compiler::{ + collect_string_consts, extract_class_queries, extract_input_metadata, + extract_input_metadata_in, extract_output_metadata_in, + }; + use oxc_ast::ast::{Declaration, Statement}; + let members = " +export class Cmp { + @NS.Input() value!: string; + @NS.Output() changed: any; + @NS.ViewChild('ref') ref: any; +} +"; + for (module, extracted) in [("@angular/core", 1), ("foreign-decorators", 0)] { + let source = format!("import * as NS from '{module}';\n{members}"); + let allocator = Allocator::default(); + let program = oxc_parser::Parser::new(&allocator, &source, oxc_span::SourceType::ts()) + .parse() + .program; + let program = allocator.alloc(program); + let class = program + .body + .iter() + .find_map(|stmt| match stmt { + Statement::ExportDeclaration(export) => match &export.declaration { + Declaration::ClassDeclaration(class) => Some(class.as_ref()), + _ => None, + }, + _ => None, + }) + .unwrap(); + let consts = collect_string_consts(&allocator, program); + let inputs = extract_input_metadata_in(&allocator, class, Some(&source), Some(&consts)); + let outputs = extract_output_metadata_in(&allocator, class, Some(&consts)); + let (view, _) = extract_class_queries(&allocator, class, Some(&source), &consts); + assert_eq!((inputs.len(), outputs.len(), view.len()), (extracted, extracted, extracted)); + // Without the file's imports no namespace is known to be Angular's. + assert!(extract_input_metadata(&allocator, class, Some(&source)).is_empty()); + } +} + +/// `queries:` values imported from another file: ngtsc 22.1.7 reads the file +/// and compiles all of these (`probe: queries-imported*`, with +/// `export const FLAG = true; export const SELS = ['a', 'b']; export const SEL +/// = 'b';` in `./flags`). oxc can't, and says so rather than reporting +/// ngtsc's wrong-type error for a value it hasn't read. +#[test] +fn imported_query_values_are_reported_as_unreadable() { + let cases = [ + ( + "new ContentChildren('el', {descendants: FLAG})", + "@ContentChildren options.descendants", + "FLAG", + "FLAG", + ), + ("new ViewChild('el', {static: FLAG})", "@ViewChild options.static", "FLAG", "'el'"), + ( + "new ContentChildren('el', {emitDistinctChangesOnly: FLAG})", + "@ContentChildren options.emitDistinctChangesOnly", + "FLAG", + "FLAG", + ), + ( + "new ContentChildren('el', {descendants: flags.FLAG})", + "@ContentChildren options.descendants", + "FLAG", + "flags.FLAG", + ), + ("new ViewChild([...SELS])", "@ViewChild predicate", "SELS", "[...SELS]"), + ("new ViewChild(['a', SEL])", "@ViewChild predicate", "SEL", "['a', SEL]"), + ("new ViewChild(['a', flags.SEL])", "@ViewChild predicate", "SEL", "['a', flags.SEL]"), + ]; + for (query, subject, name, span) in cases { + let source = format!( + "import {{Component, ViewChild, ContentChildren}} from '@angular/core'; +import {{FLAG, SELS, SEL}} from './flags'; +import * as flags from './flags'; +@Component({{selector: 'c', template: '', queries: {{el: {query}}}}}) +export class Cmp {{ + el: any; +}} +" + ); + let message = format!( + "{subject} depends on '{name}', which is imported from another module. \ + OXC compiles one file at a time and cannot evaluate values from other files." + ); + assert_eq!( + errors(&transform(&source), &source), + vec![(message, span.to_string())], + "{query}" + ); + } +} diff --git a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json index fdbb84f0a..c0a148940 100644 --- a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json +++ b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json @@ -6956,6 +6956,152 @@ "dtsImports": [], "diagnosticSpans": [[123, 124]] }, + { + "name": "probe: queries-forwardRef-localObjectMember", + "origin": "probe", + "files": { + "test.ts": "import {Component, ViewChild, ContentChildren, forwardRef, viewChild} from '@angular/core';\nclass Foo {}\nconst util = {forwardRef};\n@Component({selector: 'c', template: ''})\nexport class Cmp {\n @ViewChild(util.forwardRef(() => Foo)) foo: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(util.forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-localObjectQueries", + "origin": "probe", + "files": { + "test.ts": "import {Component, ViewChild, ContentChildren, forwardRef, viewChild} from '@angular/core';\nclass Foo {}\nconst util = {forwardRef};\n@Component({selector: 'c', template: '', queries: {foo: new ViewChild(util.forwardRef(() => Foo))}})\nexport class Cmp {\n foo: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(util.forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-localObjectSignal", + "origin": "probe", + "files": { + "test.ts": "import {Component, ViewChild, ContentChildren, forwardRef, viewChild} from '@angular/core';\nclass Foo {}\nconst util = {forwardRef};\n@Component({selector: 'c', template: ''})\nexport class Cmp {\n foo = viewChild(util.forwardRef(() => Foo));\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuerySignal(ctx.foo,util.forwardRef(()=>Foo),5);}if(rf&2){i0.ɵɵqueryAdvance();}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-coreNamespaceMember", + "origin": "probe", + "files": { + "test.ts": "import * as core from '@angular/core';\nclass Foo {}\n@core.Component({selector: 'c', template: ''})\nexport class Cmp {\n @core.ViewChild(core.forwardRef(() => Foo)) foo: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-coreNamespaceQueries", + "origin": "probe", + "files": { + "test.ts": "import * as core from '@angular/core';\nclass Foo {}\n@core.Component({selector: 'c', template: '', queries: {foo: new core.ViewChild(core.forwardRef(function () { return Foo; }))}})\nexport class Cmp {\n foo: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-coreNamespaceSignal", + "origin": "probe", + "files": { + "test.ts": "import * as core from '@angular/core';\nclass Foo {}\n@core.Component({selector: 'c', template: ''})\nexport class Cmp {\n foo = core.viewChild(core.forwardRef(() => Foo));\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuerySignal(ctx.foo,Foo,5);}if(rf&2){i0.ɵɵqueryAdvance();}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-foreignNamespaceMember", + "origin": "probe", + "files": { + "test.ts": "import {Component, ViewChild} from '@angular/core';\nimport * as other from './other';\nclass Foo {}\n@Component({selector: 'c', template: ''})\nexport class Cmp {\n @ViewChild(other.forwardRef(() => Foo)) foo: any;\n @ViewChild(other.forwardRef(function () { return Foo; })) bar: any;\n}\n", + "other.ts": "export function forwardRef(fn: () => any) { return fn(); }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(other.forwardRef(()=>Foo),5)(other.forwardRef(function(){returnFoo;}),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.bar=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-foreignNamespaceQueries", + "origin": "probe", + "files": { + "test.ts": "import {Component, ViewChild} from '@angular/core';\nimport * as other from './other';\nclass Foo {}\n@Component({selector: 'c', template: '', queries: {foo: new ViewChild(other.forwardRef(() => Foo))}})\nexport class Cmp {\n foo: any;\n}\n", + "other.ts": "export function forwardRef(fn: () => any) { return fn(); }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(other.forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-forwardRef-bareForwardRef", + "origin": "probe", + "files": { + "test.ts": "import {Component, ViewChild, ContentChildren, forwardRef, viewChild} from '@angular/core';\nclass Foo {}\n@Component({selector: 'c', template: ''})\nexport class Cmp {\n @ViewChild(forwardRef(() => Foo)) foo: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, { "name": "probe: transform-global-atob", "origin": "probe", diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index e632c6f85..c4601f87f 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -1553,8 +1553,8 @@ pub fn extract_component_metadata_sync( use oxc_angular_compiler::{ ChangeDetectionStrategy as RustChangeDetection, QueryPredicate, ViewEncapsulation as RustViewEncapsulation, build_import_map, collect_string_consts, - extract_class_queries, extract_component_metadata, extract_input_metadata, - extract_output_metadata, + extract_class_queries, extract_component_metadata, extract_input_metadata_in, + extract_output_metadata_in, }; use oxc_ast::ast::{Declaration, ExportDefaultDeclarationKind, Statement}; use oxc_parser::Parser; @@ -1666,8 +1666,14 @@ pub fn extract_component_metadata_sync( metadata.view_providers.as_ref().map(|e| emitter.emit_expression(e)); let animations = metadata.animations.as_ref().map(|e| emitter.emit_expression(e)); - // Extract inputs from @Input decorators - let rust_inputs = extract_input_metadata(&allocator, class, Some(&source)); + // Extract inputs from @Input decorators (also `@core.Input()` + // through a namespace import of @angular/core). + let rust_inputs = extract_input_metadata_in( + &allocator, + class, + Some(&source), + Some(&string_consts), + ); let inputs: Option> = if rust_inputs.is_empty() { None } else { @@ -1688,8 +1694,9 @@ pub fn extract_component_metadata_sync( ) }; - // Extract outputs from @Output decorators - let rust_outputs = extract_output_metadata(&allocator, class); + // Extract outputs from @Output decorators (also `@core.Output()`). + let rust_outputs = + extract_output_metadata_in(&allocator, class, Some(&string_consts)); let outputs: Option> = if rust_outputs.is_empty() { None } else { diff --git a/napi/angular-compiler/test/extract-component-metadata.test.ts b/napi/angular-compiler/test/extract-component-metadata.test.ts index 576ec4e84..741167030 100644 --- a/napi/angular-compiler/test/extract-component-metadata.test.ts +++ b/napi/angular-compiler/test/extract-component-metadata.test.ts @@ -37,3 +37,36 @@ export class X { ]) }) }) + +// ngtsc reads `@NS.Input()` and the other member decorators only when `NS` is a +// namespace import of `@angular/core`. +describe('extractComponentMetadataSync namespaced member decorators', () => { + const source = (module: string) => ` +import { Component } from '@angular/core'; +import * as NS from '${module}'; + +@Component({ selector: 'app-x', template: '
' }) +export class X { + @NS.Input() value!: string; + @NS.Output() changed: any; + @NS.ViewChild('ref') ref: any; + @NS.HostListener('click') onClick() {} +} +` + + it('reads them through a namespace import of @angular/core', () => { + const [component] = extractComponentMetadataSync(source('@angular/core'), 'x.component.ts') + expect(component.inputs?.map((i) => i.classPropertyName)).toEqual(['value']) + expect(component.outputs?.map((o) => o.classPropertyName)).toEqual(['changed']) + expect(component.viewQueries?.map((q) => q.propertyName)).toEqual(['ref']) + expect(component.host?.listeners).toEqual([['(click)', 'onClick()']]) + }) + + it('ignores them through any other namespace', () => { + const [component] = extractComponentMetadataSync(source('foreign-decorators'), 'x.component.ts') + expect(component.inputs ?? []).toEqual([]) + expect(component.outputs ?? []).toEqual([]) + expect(component.viewQueries ?? []).toEqual([]) + expect(component.host?.listeners ?? []).toEqual([]) + }) +}) From 137f8d20105fbf2afb2b104e4f59e10ed8749957 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 30 Sep 2026 20:59:28 +0800 Subject: [PATCH 3/6] fix(queries): unwrap only `@angular/core`'s `forwardRef` and flag only Angular's signal queries Checked against @angular/compiler-cli 22.1.7 (probes are in the fixture): - A bare `forwardRef(() => X)` in a query predicate was unwrapped by name. Like ngtsc's `tryUnwrapForwardRef`, it's now unwrapped only when it's imported from `@angular/core`, under any alias (`forwardRef as fr`); a local `forwardRef`, one from another module or an undeclared one is kept as written. Without the file's imports (the public `extract_view_queries` / `extract_content_queries`), it's still matched by name. - "Query is declared multiple times" fired for any member calling a function named `viewChild()`, `contentChild()`, ... It now fires only for Angular's, imported from `@angular/core` by name (under any alias) or through a namespace import, like the input/output checks. Signal query members are still compiled by name, as before. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/directive/property_decorators.rs | 39 ++- .../tests/decorator_metadata_ngtsc_test.rs | 32 ++- .../fixtures/decorator_metadata_ngtsc.json | 268 ++++++++++++++++++ 3 files changed, 324 insertions(+), 15 deletions(-) diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index c253866c0..69f90bd63 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -110,23 +110,38 @@ fn extract_boolean_value(expr: &Expression<'_>) -> Option { } } +/// Angular's signal query functions (ngtsc's `QUERY_INITIALIZER_FNS`). +const QUERY_APIS: [super::decorator::InitializerApi; 4] = [ + ("viewChild", "@angular/core"), + ("viewChildren", "@angular/core"), + ("contentChild", "@angular/core"), + ("contentChildren", "@angular/core"), +]; + /// Try to unwrap a forwardRef call and extract the inner expression. /// /// Mirrors ngtsc's `tryUnwrapForwardRef`: `forwardRef(() => X)`, /// `forwardRef(function () { return X; })`, looking through parentheses and /// `as` casts. Returns `None` for anything else. /// -/// Like ngtsc, `ns.forwardRef(...)` is only unwrapped when `ns` is a namespace -/// import of `@angular/core` (see [`is_core_namespace`]); `util.forwardRef` -/// on any other object is kept as written. A bare `forwardRef` is matched by -/// name. +/// Like ngtsc, it's only unwrapped when it's `@angular/core`'s `forwardRef`: +/// imported by name (under any alias), or `ns.forwardRef(...)` with `ns` a +/// namespace import of `@angular/core` (see [`is_core_namespace`]). Any other +/// `forwardRef` (a local function, one from another module, `util.forwardRef`) +/// is kept as written. Without the file's imports (`consts` is `None`), a bare +/// `forwardRef` is matched by name. fn try_unwrap_forward_ref<'a>( expr: &'a Expression<'a>, consts: Option<&super::StringConsts<'_>>, ) -> Option<&'a Expression<'a>> { let Expression::CallExpression(call) = unwrap_expression(expr) else { return None }; let is_forward_ref = match &call.callee { - Expression::Identifier(id) => id.name == "forwardRef", + Expression::Identifier(id) => match consts { + Some(consts) => consts.scope().import(&id.name).is_some_and(|import| { + import.module == "@angular/core" && import.imported == Some("forwardRef") + }), + None => id.name == "forwardRef", + }, Expression::StaticMemberExpression(m) => { m.property.name == "forwardRef" && matches!(&m.object, Expression::Identifier(ns) if is_core_namespace(consts, &ns.name)) @@ -1802,7 +1817,9 @@ pub(crate) fn parse_decorator_queries<'a>( } } - // A signal query member for the same property is an error. + // A signal query member for the same property is an error: one calling + // Angular's `viewChild()`, `contentChildren()`, ... (not just any function + // named like them). let signal_queries: std::vec::Vec> = class .body .body @@ -1810,14 +1827,8 @@ pub(crate) fn parse_decorator_queries<'a>( .filter_map(|element| { let ClassElement::PropertyDefinition(prop) = element else { return None }; let name = get_property_key_name(&prop.key)?; - try_parse_signal_query( - allocator, - prop.value.as_ref()?, - name.clone(), - source_text, - Some(consts), - ) - .map(|_| name) + super::decorator::is_initializer_api_call(prop.value.as_ref()?, consts, &QUERY_APIS) + .then_some(name) }) .collect(); // ngtsc checks the content queries first, and reports the `new` expression. diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index 426a4038e..97bebaaea 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() { failures.len(), failures.join("\n\n") ); - assert_eq!(compared, 620, "fixtures compared"); + assert_eq!(compared, 637, "fixtures compared"); } fn transform(source: &str) -> TransformResult { @@ -660,6 +660,36 @@ export class Dir {{ value = {initializer}; }} } } +/// The same for a signal query also declared in `queries:`: ngtsc 22.1.7 +/// reports it only for Angular's `viewChild()`, `contentChild()`, ... (the +/// snapshot's `initializerApi-viewChild*` probes), and compiles these, whose +/// functions aren't Angular's. oxc still compiles such a member as a signal +/// query too (it recognises them by name), so these compare only the +/// diagnostics. +#[test] +fn only_angular_signal_queries_collide_with_decorator_queries() { + let cases = [ + ("import {viewChild} from './other';", "viewChild('x')", "ViewChild"), + ("import * as other from './other';", "other.viewChild('x')", "ViewChild"), + ( + "function contentChild(x: string): any { return null; }", + "contentChild('x')", + "ContentChild", + ), + ]; + for (declaration, initializer, query) in cases { + let source = format!( + "import {{Directive, {query}}} from '@angular/core'; +{declaration} +@Directive({{selector: '[d]', queries: {{foo: new {query}('x')}}}}) +export class Dir {{ foo = {initializer}; }} +" + ); + let result = transform(&source); + assert_eq!(errors(&result, &source), vec![], "{declaration} {initializer}"); + } +} + /// Inputs ngtsc can't compile either (it overflows its stack on the recursive /// ones and on a chain of 3000 consts) must not overflow oxc's stack or hang. #[test] diff --git a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json index c0a148940..61fa900f9 100644 --- a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json +++ b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json @@ -7102,6 +7102,274 @@ }, "dtsImports": [] }, + { + "name": "probe: forwardRef-readCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, forwardRef } from '@angular/core';\nexport class Foo {}\nexport class Bar {}\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(Foo, { read: forwardRef(() => Bar) }) el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5,forwardRef(()=>Bar));}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-aliasFromCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, forwardRef as fr } from '@angular/core';\nexport class Foo {}\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(fr(() => Foo)) el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-otherModule", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild } from '@angular/core';\nimport { forwardRef } from './other';\nexport class Foo {}\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(forwardRef(() => Foo)) el: any; }\n", + "other.ts": "export function forwardRef(fn: () => any): any { return fn; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-localFunction", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild } from '@angular/core';\nexport class Foo {}\nfunction forwardRef(fn: () => any): any { return fn; }\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(forwardRef(() => Foo)) el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-declaredGlobal", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild } from '@angular/core';\nexport class Foo {}\ndeclare function forwardRef(fn: () => any): any;\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(forwardRef(() => Foo)) el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-core", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, forwardRef } from '@angular/core';\nexport class Foo {}\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(forwardRef(() => Foo)) el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-queriesAliasFromCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, forwardRef as fr } from '@angular/core';\nexport class Foo {}\n@Directive({ selector: '[d]', queries: { el: new ViewChild(fr(() => Foo)) } })\nexport class D { el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-queriesOtherModule", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild } from '@angular/core';\nimport { forwardRef } from './other';\nexport class Foo {}\n@Directive({ selector: '[d]', queries: { el: new ViewChild(forwardRef(() => Foo)) } })\nexport class D { el: any; }\n", + "other.ts": "export function forwardRef(fn: () => any): any { return fn; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(forwardRef(()=>Foo),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-contentChildrenAliasFromCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ContentChildren, forwardRef as fr } from '@angular/core';\nexport class Foo {}\n@Directive({ selector: '[d]' })\nexport class D { @ContentChildren(fr(() => Foo)) els: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,Foo,4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.els=_t);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-signalQueryAliasFromCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, viewChild, forwardRef as fr } from '@angular/core';\nexport class Foo {}\n@Directive({ selector: '[d]' })\nexport class D { el = viewChild(fr(() => Foo)); }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuerySignal(ctx.el,Foo,5);}if(rf&2){i0.ɵɵqueryAdvance();}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-signalQueryOtherModule", + "origin": "probe", + "files": { + "test.ts": "import { Directive, viewChild } from '@angular/core';\nimport { forwardRef } from './other';\nexport class Foo {}\n@Directive({ selector: '[d]' })\nexport class D { el = viewChild(forwardRef(() => Foo)); }\n", + "other.ts": "export function forwardRef(fn: () => any): any { return fn; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuerySignal(ctx.el,forwardRef(()=>Foo),5);}if(rf&2){i0.ɵɵqueryAdvance();}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: forwardRef-readAliasFromCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, forwardRef as fr } from '@angular/core';\nexport class Foo {}\nexport class Bar {}\n@Directive({ selector: '[d]' })\nexport class D { @ViewChild(Foo, { read: fr(() => Bar) }) el: any; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(Foo,5,fr(()=>Bar));}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.el=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: initializerApi-viewChildCore", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, viewChild } from '@angular/core';\n@Directive({ selector: '[d]', queries: { foo: new ViewChild('x') } })\nexport class D { foo = viewChild('x'); }\n", + "other.ts": "export function input(...a: any[]): any { return null; }\nexport namespace input { export function required(...a: any[]): any { return null; } }\nexport function model(...a: any[]): any { return null; }\nexport function output(...a: any[]): any { return null; }\nexport function outputFromObservable(...a: any[]): any { return null; }\nexport function viewChild(...a: any[]): any { return null; }\nexport function viewChildren(...a: any[]): any { return null; }\nexport function contentChild(...a: any[]): any { return null; }\nexport function contentChildren(...a: any[]): any { return null; }\n" + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[111, 129]] + }, + { + "name": "probe: initializerApi-viewChildCoreAlias", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild, viewChild as vc } from '@angular/core';\n@Directive({ selector: '[d]', queries: { foo: new ViewChild('x') } })\nexport class D { foo = vc('x'); }\n", + "other.ts": "export function input(...a: any[]): any { return null; }\nexport namespace input { export function required(...a: any[]): any { return null; } }\nexport function model(...a: any[]): any { return null; }\nexport function output(...a: any[]): any { return null; }\nexport function outputFromObservable(...a: any[]): any { return null; }\nexport function viewChild(...a: any[]): any { return null; }\nexport function viewChildren(...a: any[]): any { return null; }\nexport function contentChild(...a: any[]): any { return null; }\nexport function contentChildren(...a: any[]): any { return null; }\n" + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[117, 135]] + }, + { + "name": "probe: initializerApi-viewChildCoreNamespace", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild } from '@angular/core';\nimport * as core from '@angular/core';\n@Directive({ selector: '[d]', queries: { foo: new ViewChild('x') } })\nexport class D { foo = core.viewChild('x'); }\n", + "other.ts": "export function input(...a: any[]): any { return null; }\nexport namespace input { export function required(...a: any[]): any { return null; } }\nexport function model(...a: any[]): any { return null; }\nexport function output(...a: any[]): any { return null; }\nexport function outputFromObservable(...a: any[]): any { return null; }\nexport function viewChild(...a: any[]): any { return null; }\nexport function viewChildren(...a: any[]): any { return null; }\nexport function contentChild(...a: any[]): any { return null; }\nexport function contentChildren(...a: any[]): any { return null; }\n" + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[139, 157]] + }, + { + "name": "probe: initializerApi-viewChildRequiredOtherModuleCast", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ViewChild } from '@angular/core';\nimport { viewChild } from './other';\n@Directive({ selector: '[d]', queries: { foo: new ViewChild('x') } })\nexport class D { foo = (viewChild as any).required('x'); }\n", + "other.ts": "export function input(...a: any[]): any { return null; }\nexport namespace input { export function required(...a: any[]): any { return null; } }\nexport function model(...a: any[]): any { return null; }\nexport function output(...a: any[]): any { return null; }\nexport function outputFromObservable(...a: any[]): any { return null; }\nexport function viewChild(...a: any[]): any { return null; }\nexport function viewChildren(...a: any[]): any { return null; }\nexport function contentChild(...a: any[]): any { return null; }\nexport function contentChildren(...a: any[]): any { return null; }\n" + }, + "diagnostics": [], + "classes": { + "D": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.foo=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [], + "diagnosticSpans": [] + }, + { + "name": "probe: initializerApi-contentChildRequiredCoreNamespace", + "origin": "probe", + "files": { + "test.ts": "import { Directive, ContentChild } from '@angular/core';\nimport * as core from '@angular/core';\n@Directive({ selector: '[d]', queries: { foo: new ContentChild('x') } })\nexport class D { foo = core.contentChild.required('x'); }\n", + "other.ts": "export function input(...a: any[]): any { return null; }\nexport namespace input { export function required(...a: any[]): any { return null; } }\nexport function model(...a: any[]): any { return null; }\nexport function output(...a: any[]): any { return null; }\nexport function outputFromObservable(...a: any[]): any { return null; }\nexport function viewChild(...a: any[]): any { return null; }\nexport function viewChildren(...a: any[]): any { return null; }\nexport function contentChild(...a: any[]): any { return null; }\nexport function contentChildren(...a: any[]): any { return null; }\n" + }, + "diagnostics": [ + "Query is declared multiple times. \"@Directive\" declares a query for the same property." + ], + "classes": {}, + "dtsImports": [], + "diagnosticSpans": [[142, 163]] + }, { "name": "probe: transform-global-atob", "origin": "probe", From ee35d53e3d05b48a3070defcb733a3045aacee38 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 1 Oct 2026 01:19:21 +0800 Subject: [PATCH 4/6] 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) --- .../src/component/transform.rs | 7 +- .../src/directive/decorator.rs | 24 +- .../src/directive/property_decorators.rs | 114 +- .../tests/decorator_metadata_ngtsc_test.rs | 44 +- .../fixtures/decorator_metadata_ngtsc.json | 1310 +++++++++++++++++ 5 files changed, 1466 insertions(+), 33 deletions(-) diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index a2e858078..2f6184ae0 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -2579,7 +2579,12 @@ pub fn transform_angular_file( let implicit_standalone = options.implicit_standalone(); // `inputs:`/`outputs:` forms ngtsc rejects, rather than dropping them silently. - result.diagnostics.extend(decorator_io_errors(allocator, class, &string_consts)); + result.diagnostics.extend(decorator_io_errors( + allocator, + class, + Some(source), + &string_consts, + )); if let Some(mut metadata) = extract_component_metadata( &allocator, diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 151727002..8a0a7ef7d 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -1052,19 +1052,22 @@ pub(crate) fn angular_decorator_config<'a>( /// The first error ngtsc raises for the inputs, outputs and queries of a /// `@Component` / `@Directive` on `class`, in the order it checks them /// (`extractDirectiveMetadata`): `inputs:`, `@Input` members, `outputs:`, -/// output members, then `queries:`. ngtsc stops at the first one. +/// output members, query members (`@ViewChild`, ...), then `queries:`. ngtsc +/// stops at the first one. /// /// Each error points where ngtsc's does: the `inputs:` / `outputs:` value, the -/// member, or the part of `queries:` at fault. +/// member, or the part of a query at fault. `source_text` is the file's, as +/// when the class is compiled, so both read the same queries. pub fn decorator_io_errors<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, consts: &StringConsts<'a>, ) -> std::vec::Vec { let Some((config, decorator_name)) = angular_decorator_config(class) else { return std::vec::Vec::new(); }; - let io = config.map(|config| parse_decorator_io(allocator, config, class, None, consts)); + let io = config.map(|config| parse_decorator_io(allocator, config, class, source_text, consts)); let (meta_inputs, meta_outputs): (std::vec::Vec<&str>, std::vec::Vec<&str>) = match &io { Some(io) => ( io.inputs.iter().map(|i| i.class_property_name.as_str()).collect(), @@ -1148,9 +1151,21 @@ pub fn decorator_io_errors<'a>( }) }) }; + let member_queries = + || super::property_decorators::member_query_error(allocator, class, source_text, consts); + // With the source text, like the compiled queries: a predicate is emitted + // as written, which some expressions (functions) need it for. let queries = || { let config = config?; - super::parse_decorator_queries(allocator, config, class, None, consts, decorator_name).error + let queries = super::parse_decorator_queries( + allocator, + config, + class, + source_text, + consts, + decorator_name, + ); + queries.error }; io.as_ref() @@ -1158,6 +1173,7 @@ pub fn decorator_io_errors<'a>( .or_else(input_members) .or_else(|| io.as_ref().and_then(|io| io.output_error.clone())) .or_else(output_members) + .or_else(member_queries) .or_else(queries) .map(|(message, span)| OxcDiagnostic::error(message).with_label(span)) .into_iter() diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index 69f90bd63..1b78099d4 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -859,6 +859,28 @@ fn parse_query_config<'a>( source_text: Option<&'a str>, consts: Option<&super::StringConsts<'a>>, ) -> QueryConfig<'a> { + // With the file, read like ngtsc does (see [`member_query`]). A query it + // rejects isn't compiled; [`member_query_error`] reports it. + if let Some(consts) = consts { + return match member_query( + allocator, + decorator, + decorator_name, + Span::default(), + source_text, + consts, + ) { + Ok(query) => QueryConfig { + predicate: Some(query.predicate), + is_static: query.is_static, + read: query.read, + descendants: query.descendants, + emit_distinct_changes_only: query.emit_distinct_changes_only, + }, + Err(_) => QueryConfig::default_for(decorator_name), + }; + } + let Expression::CallExpression(call) = &decorator.expression else { return QueryConfig::default_for(decorator_name); }; @@ -869,27 +891,16 @@ fn parse_query_config<'a>( let mut config = QueryConfig::default_for(decorator_name); - // The predicate: a string selector, a string array, or a type/token. + // The predicate: a string selector or a type/token. // forwardRef isn't included in compiled output. - let node = try_unwrap_forward_ref(first_arg, consts).unwrap_or(first_arg); - let selectors = match (consts, node) { - (Some(consts), _) => match super::evaluator::Evaluator::new(consts).evaluate(node) { - super::evaluator::Value::String(s) => Some(std::vec![s]), - super::evaluator::Value::Array(items) => { - items.iter().map(|i| i.as_str().map(str::to_string)).collect() - } - _ => None, - }, - (None, Expression::StringLiteral(lit)) => Some(std::vec![lit.value.to_string()]), - _ => None, - }; - config.predicate = match selectors { - Some(selectors) => { + let node = try_unwrap_forward_ref(first_arg, None).unwrap_or(first_arg); + config.predicate = match node { + Expression::StringLiteral(lit) => { let mut list = Vec::new_in(&allocator); - list.extend(selectors.iter().map(|s| Ident::from(allocator.alloc_str(s)))); + list.push(lit.value.clone().into()); Some(QueryPredicate::Selectors(list)) } - None => convert_oxc_expression(allocator, node, source_text).map(QueryPredicate::Type), + _ => convert_oxc_expression(allocator, node, source_text).map(QueryPredicate::Type), }; // Parse options from second argument if present @@ -1798,8 +1809,9 @@ pub(crate) fn parse_decorator_queries<'a>( consts, &evaluator, type_name, - new_expr, - property_name, + &new_expr.arguments, + new_expr.span, + &property_name, source_text, ) { Ok(query) if type_name.starts_with("Content") => { @@ -1849,21 +1861,71 @@ pub(crate) fn parse_decorator_queries<'a>( queries } -/// One `new ViewChild(predicate, options?)` and friends. An error comes with -/// the node ngtsc reports it on. +/// A query member decorator (`@ViewChild('el', {static: true})` on the +/// member at `span`), read like ngtsc's `extractDecoratorQueryMetadata` (see +/// [`decorator_query`]): the options are evaluated, so a same-file `const` +/// counts and a value of the wrong type is an error. +fn member_query<'a>( + allocator: &'a Allocator, + decorator: &'a Decorator<'a>, + name: &str, + span: Span, + source_text: Option<&'a str>, + consts: &super::StringConsts<'a>, +) -> Result, (String, Span)> { + // `@ViewChild` without a call has no arguments. + let args: &'a [Argument<'a>] = match &decorator.expression { + Expression::CallExpression(call) => &call.arguments, + _ => &[], + }; + let evaluator = super::evaluator::Evaluator::new(consts); + decorator_query(allocator, consts, &evaluator, name, args, span, "", source_text) +} + +/// The first error ngtsc raises for a class's query member decorators +/// (`parseQueriesOfClassFields`), in member order: the one +/// [`member_query`] reports, on the node ngtsc points at. +pub(crate) fn member_query_error<'a>( + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + consts: &super::StringConsts<'a>, +) -> Option<(String, Span)> { + class.body.body.iter().find_map(|element| { + let (decorators, span) = match element { + ClassElement::PropertyDefinition(prop) => (&prop.decorators, prop.span), + ClassElement::MethodDefinition(method) + if matches!(method.kind, MethodDefinitionKind::Set | MethodDefinitionKind::Get) => + { + (&method.decorators, method.span) + } + _ => return None, + }; + let (decorator, name) = QUERY_TYPES.iter().find_map(|name| { + Some((find_decorator_by_name(decorators, name, Some(consts))?, *name)) + })?; + member_query(allocator, decorator, name, span, source_text, consts).err() + }) +} + +/// ngtsc's `extractDecoratorQueryMetadata`: one `@ViewChild(predicate, +/// options?)` member decorator, or `new ViewChild(...)` in `queries:`, and +/// friends. `args` are its arguments and `span` the member or the `new` +/// expression. An error comes with the node ngtsc reports it on. +#[expect(clippy::too_many_arguments)] fn decorator_query<'a>( allocator: &'a Allocator, consts: &super::StringConsts<'a>, evaluator: &super::evaluator::Evaluator<'_, 'a>, name: &str, - new_expr: &'a oxc_ast::ast::NewExpression<'a>, - property_name: String, + args: &'a [Argument<'a>], + span: Span, + property_name: &str, source_text: Option<&'a str>, ) -> Result, (String, Span)> { use super::evaluator::Value; - let args = &new_expr.arguments; let Some(first) = args.first().and_then(Argument::as_expression) else { - return Err((format!("@{name} must have arguments"), new_expr.span)); + return Err((format!("@{name} must have arguments"), span)); }; let node = try_unwrap_forward_ref(first, Some(consts)).unwrap_or(first); let at_node = |message: String| (message, node.span()); @@ -1940,7 +2002,7 @@ fn decorator_query<'a>( } Ok(R3QueryMetadata { - property_name: Ident::from(allocator.alloc_str(&property_name)), + property_name: Ident::from(allocator.alloc_str(property_name)), first: name == "ViewChild" || name == "ContentChild", predicate, descendants: config.descendants, diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index 97bebaaea..23092edd6 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() { failures.len(), failures.join("\n\n") ); - assert_eq!(compared, 637, "fixtures compared"); + assert_eq!(compared, 730, "fixtures compared"); } fn transform(source: &str) -> TransformResult { @@ -1045,7 +1045,7 @@ export class Cmp { } } -/// `queries:` values imported from another file: ngtsc 22.1.7 reads the file +/// Query values imported from another file: ngtsc 22.1.7 reads the file /// and compiles all of these (`probe: queries-imported*`, with /// `export const FLAG = true; export const SELS = ['a', 'b']; export const SEL /// = 'b';` in `./flags`). oxc can't, and says so rather than reporting @@ -1097,4 +1097,44 @@ export class Cmp {{ "{query}" ); } + + // The same options on member decorators, read by the same code as ngtsc + // does (`probe: queries-member-*`; ngtsc 22.1.7 compiles these with + // `export const FLAG = false` in the other file). + let cases = [ + ( + "@ViewChildren('el', {emitDistinctChangesOnly: FLAG})", + "@ViewChildren options.emitDistinctChangesOnly", + "FLAG", + ), + ( + "@ContentChildren('el', {descendants: flags.FLAG})", + "@ContentChildren options.descendants", + "flags.FLAG", + ), + ("@ViewChild('el', {static: FLAG})", "@ViewChild options.static", "'el'"), + ("@ContentChild(['a', SEL])", "@ContentChild predicate", "['a', SEL]"), + ]; + for (decorator, subject, span) in cases { + let source = format!( + "import {{Component, ViewChild, ViewChildren, ContentChild, ContentChildren}} from '@angular/core'; +import {{FLAG, SEL}} from './flags'; +import * as flags from './flags'; +@Component({{selector: 'c', template: ''}}) +export class Cmp {{ + {decorator} el: any; +}} +" + ); + let name = if subject.ends_with("predicate") { "SEL" } else { "FLAG" }; + let message = format!( + "{subject} depends on '{name}', which is imported from another module. \ + OXC compiles one file at a time and cannot evaluate values from other files." + ); + assert_eq!( + errors(&transform(&source), &source), + vec![(message, span.to_string())], + "{decorator}" + ); + } } diff --git a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json index 61fa900f9..bbfc953c7 100644 --- a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json +++ b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json @@ -7370,6 +7370,1316 @@ "dtsImports": [], "diagnosticSpans": [[142, 163]] }, + { + "name": "probe: queries-member-ViewChildren-emitDistinctChangesOnly-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {emitDistinctChangesOnly: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-emitDistinctChangesOnly-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {emitDistinctChangesOnly: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-emitDistinctChangesOnly-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {emitDistinctChangesOnly: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChildren options.emitDistinctChangesOnly must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[314, 319]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-emitDistinctChangesOnly-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {emitDistinctChangesOnly: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChildren options.emitDistinctChangesOnly must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[334, 338]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-emitDistinctChangesOnly-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {emitDistinctChangesOnly: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-emitDistinctChangesOnly-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {emitDistinctChangesOnly: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],0);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-emitDistinctChangesOnly-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {emitDistinctChangesOnly: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-emitDistinctChangesOnly-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {emitDistinctChangesOnly: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChildren options.emitDistinctChangesOnly must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[317, 322]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-emitDistinctChangesOnly-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {emitDistinctChangesOnly: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChildren options.emitDistinctChangesOnly must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[337, 341]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-emitDistinctChangesOnly-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {emitDistinctChangesOnly: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-descendants-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-descendants-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-descendants-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {descendants: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChildren options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[305, 310]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-descendants-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChildren options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[325, 329]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChildren-descendants-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren('x', {descendants: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-descendants-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-descendants-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-descendants-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChildren options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[302, 307]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-descendants-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChildren options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[322, 326]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChildren-descendants-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-descendants-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-descendants-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-descendants-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChild options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[302, 307]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-descendants-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChild options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[322, 326]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-descendants-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChild-static-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChild-static-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChild-static-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChild-static-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: FLAG}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[300, 303]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ViewChild-static-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-static-constFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {static: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-static-constTrue", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {static: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-static-string", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {static: 'yes'}) q!: any;\n}\n" + }, + "diagnostics": ["@ContentChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[283, 286]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-static-constString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = 'yes';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {static: FLAG}) q!: any;\n}\n" + }, + "diagnostics": ["@ContentChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[303, 306]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-ContentChild-static-not", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {static: !0}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-number", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 1}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'number'."], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-null", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: null}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'null'."], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-undefined", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: undefined}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'undefined'."], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-unknownIdent", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\ndeclare const someGlobal: boolean;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: someGlobal}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChild options.static must be a boolean Value is a reference to 'someGlobal'." + ], + "diagnosticSpans": [[315, 318]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-desc-letUninit", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nlet FLAG;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: FLAG}) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChildren options.descendants must be a boolean Value is of type 'undefined'." + ], + "diagnosticSpans": [[312, 316]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-ternary", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst A = 1;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: A ? true : false}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-enum", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nenum E { A }\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: E.A}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'E'."], + "diagnosticSpans": [[293, 296]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-static-predTypeRef", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(ElementRef, {static: 'no'}) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[280, 290]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsConst", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst OPTS = {static: true};\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', OPTS) q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChild options must be an object literal. Find more at https://next.angular.dev/errors/NG1001" + ], + "diagnosticSpans": [[314, 318]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsParens", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', ({static: true})) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsAs", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: true} as any) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsSpread", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst OPTS = {static: true};\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {...OPTS}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsSpreadThen", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst OPTS = {static: true, descendants: true};\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {...OPTS, descendants: false}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', 'str') q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChild options must be an object literal. Find more at https://next.angular.dev/errors/NG1001" + ], + "diagnosticSpans": [[285, 290]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-readConst", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst R = ElementRef;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {read: R}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5,R);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-readNumber", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {read: 1}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5,1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-quotedKey", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {'descendants': false, \"emitDistinctChangesOnly\": false}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],0);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-computedKey", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {['descendants']: false}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-shorthand", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst descendants = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants}) q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-multiErr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {descendants: 'a', emitDistinctChangesOnly: 'b'}) q!: any;\n @ViewChild('x', {static: 'c'}) r!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChildren options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[302, 305]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-setter", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren('x', {emitDistinctChangesOnly: FLAG}) set q(v: any) {}\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-options-ConstFalse", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = false;\n@Component({selector: 'c', template: '
', queries: {q: new ViewChildren('x', {emitDistinctChangesOnly: FLAG})}})\nexport class Cmp {\n q!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-options-OptsConst", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst OPTS = {static: true};\n@Component({selector: 'c', template: '
', queries: {q: new ViewChild('x', OPTS)}})\nexport class Cmp {\n q!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChild options must be an object literal. Find more at https://next.angular.dev/errors/NG1001" + ], + "diagnosticSpans": [[308, 312]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-options-StaticString", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {q: new ViewChild('x', {static: 'c'})}})\nexport class Cmp {\n q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[274, 277]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesFnExpr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(function Token() {})}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(functionToken(){},5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesArrow", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(() => ElementRef)}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(()=>ElementRef,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesCall", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nfunction mk(): any { return ElementRef; }\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(mk())}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(mk(),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesCallArg", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\ndeclare function mk(v: any): any;\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(mk(1))}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(mk(1),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesAs", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(ElementRef as any)}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(ElementRef,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesMember", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst T = {ref: ElementRef};\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(T.ref)}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(T.ref,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesGlobal", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(window)}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(window,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesTemplate", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(`x`)}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesNumber", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(1)}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": ["@ViewChild predicate cannot be interpreted Value is of type 'number'."], + "diagnosticSpans": [[277, 278]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesObject", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild({})}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": ["@ViewChild predicate cannot be interpreted Value is of type '{}'."], + "diagnosticSpans": [[277, 279]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-queriesNew", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(new ElementRef(null))}})\nexport class Cmp {\n item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(newElementRef(null),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-memberFnExpr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(function Token() {}) item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(functionToken(){},5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-memberArrow", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(() => ElementRef) item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(()=>ElementRef,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-memberCall", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nfunction mk(): any { return ElementRef; }\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(mk()) item: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(mk(),5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-predicate-memberNumber", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(1 as any) item: any;\n}\n" + }, + "diagnostics": ["@ViewChild predicate cannot be interpreted Value is of type 'number'."], + "diagnosticSpans": [[280, 288]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-noArgs", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild() q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild must have arguments"], + "diagnosticSpans": [[269, 290]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-noCall", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild must have arguments"], + "diagnosticSpans": [[269, 288]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-tooMany", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {}, 1 as any) q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild has too many arguments"], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-noArgsSetter", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChildren() set q(v: any) {}\n}\n" + }, + "diagnostics": ["@ContentChildren must have arguments"], + "diagnosticSpans": [[269, 304]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-twoMembers", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 'a'}) a!: any;\n @ContentChild('x', {descendants: 'b'}) b!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-contentFirstInOrder", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: 'b'}) b!: any;\n @ViewChild('x', {static: 'a'}) a!: any;\n}\n" + }, + "diagnostics": [ + "@ContentChild options.descendants must be a boolean Value is of type 'string'." + ], + "diagnosticSpans": [[302, 305]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-memberVsQueries", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', queries: {q: new ViewChild('x', {static: 'c'})}})\nexport class Cmp {\n @ViewChild('x', {static: 'a'}) a!: any;\n q!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[329, 332]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-inputVsMember", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 'a'}) a!: any;\n @Input({transform: 1 as any}) i!: any;\n}\n" + }, + "diagnostics": ["Input transform must be a function Value is of type 'number'."], + "diagnosticSpans": [[318, 339]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-outputsMetaVsMember", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
', outputs: 1 as any})\nexport class Cmp {\n @ViewChild('x', {static: 'a'}) a!: any;\n}\n" + }, + "diagnostics": [ + "Failed to resolve @Directive.outputs to a string array Value is of type 'number'." + ], + "diagnosticSpans": [[256, 264]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-forwardRefStatic", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {forwardRef} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(forwardRef(() => ElementRef), {static: 'a'}) a!: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[339, 349]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-predicateArrayConst", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst SELS = ['a', 'b'];\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren(SELS, {emitDistinctChangesOnly: false}) a!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"a\",\"b\"],1);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-predicateBadArray", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChildren(['a', 1] as any) a!: any;\n}\n" + }, + "diagnostics": [ + "Failed to resolve @ViewChildren predicate at position 1 to a string Value is of type 'number'." + ], + "diagnosticSpans": [[283, 298]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsNull", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', null) a!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChild options must be an object literal. Find more at https://next.angular.dev/errors/NG1001" + ], + "diagnosticSpans": [[285, 289]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-optsUndefinedArg", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', undefined) a!: any;\n}\n" + }, + "diagnostics": [ + "@ViewChild options must be an object literal. Find more at https://next.angular.dev/errors/NG1001" + ], + "diagnosticSpans": [[285, 294]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-readAndStatic", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {read: ElementRef, static: FLAG}) a!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7,ElementRef);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-dir", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Directive({selector: '[d]'})\nexport class Dir {\n @ContentChildren('x', {descendants: FLAG, emitDistinctChangesOnly: FLAG}) a!: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-signalPlusDecorator", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {viewChild} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 'a'}) a = viewChild('x');\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[321, 324]], + "classes": {}, + "dtsImports": [] + }, + { + "name": "probe: queries-member-getter", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nconst FLAG = true;\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: FLAG}) get a(): any { return null; }\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.a=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: queries-member-staticMember", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ViewChildren, ContentChild, ContentChildren, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 'a'}) static a: any;\n}\n" + }, + "diagnostics": ["@ViewChild options.static must be a boolean Value is of type 'string'."], + "diagnosticSpans": [[280, 283]], + "classes": {}, + "dtsImports": [] + }, { "name": "probe: transform-global-atob", "origin": "probe", From 28d62a8875d8c9c2f0fdefe77019aa75931498e4 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 1 Oct 2026 03:00:07 +0800 Subject: [PATCH 5/6] test(queries): cover `&&` / `||` with an import they don't need in query options `queries: {q: new ViewChild('x' || NAME)}`, `@ViewChild('x', {static: true || FLAG})` and `{descendants: false && FLAG}` now compile like ngtsc (the evaluator fix below this PR); these snapshot probes pin it. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/decorator_metadata_ngtsc_test.rs | 2 +- .../fixtures/decorator_metadata_ngtsc.json | 170 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index 23092edd6..99337f3dc 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() { failures.len(), failures.join("\n\n") ); - assert_eq!(compared, 730, "fixtures compared"); + assert_eq!(compared, 740, "fixtures compared"); } fn transform(source: &str) -> TransformResult { diff --git a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json index bbfc953c7..ec34e5e6f 100644 --- a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json +++ b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json @@ -9445,6 +9445,176 @@ }, "dtsImports": [] }, + { + "name": "probe: shortCircuit-qMetaOr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild('x' || NAME)}})\nexport class Cmp {\n item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMetaCond", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(true ? 'x' : NAME)}})\nexport class Cmp {\n item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMetaStaticOr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild('x', {static: true || FLAG})}})\nexport class Cmp {\n item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMetaReadOr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild('x', {read: 0 || Token})}})\nexport class Cmp {\n item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5,0||Token);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMetaPredOrToken", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
', queries: {item: new ViewChild(0 || Token)}})\nexport class Cmp {\n item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(0||Token,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMemberOr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x' || NAME) item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMemberCond", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild(false ? NAME : 'x') item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMemberStaticOr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: true || FLAG}) item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMemberStaticCond", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ViewChild('x', {static: 1 ? true : FLAG}) item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"x\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: shortCircuit-qMemberDescOr", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Component, Input, Output, ViewChild, ContentChild, ElementRef, booleanAttribute, EventEmitter} from '@angular/core';\nimport {NAME, FLAG, TRUE_FLAG, Token, EMPTY, toBool} from './other';\nconst LOCAL: string = 'localAlias';\nconst EMPTY_LOCAL: string = '';\n@Component({selector: 'c', template: '
'})\nexport class Cmp {\n @ContentChild('x', {descendants: false && FLAG}) item: any;\n}\n", + "other.ts": "import {booleanAttribute} from '@angular/core';\nexport const NAME = 'y';\nexport const EMPTY = '';\nexport const FLAG = false;\nexport const TRUE_FLAG = true;\nexport class Token {}\nexport function toBool(v: string) { return !!v; }\nexport interface Other { a: string }\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Cmp": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"x\"],4);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.item=_t.first);}}", + "dts:ɵcmp": "staticɵcmp:i0.ɵɵComponentDeclaration;" + } + }, + "dtsImports": [] + }, { "name": "probe: scope-typeOnly-returnType", "origin": "probe", From 167ea920333221f1c4a678cbd62b4707ef211e5e Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 1 Oct 2026 04:32:54 +0800 Subject: [PATCH 6/6] test(queries): compare query options read from same-file namespaces with ngtsc Query selectors, `read` and `static` options read through `typeof NS.X` or `import S = NS.X` (in `queries:` and on members) now evaluate like ngtsc, and `NS.C` in a value position stays the expression it is. Add those cases to the ngtsc snapshot. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/decorator_metadata_ngtsc_test.rs | 2 +- .../fixtures/decorator_metadata_ngtsc.json | 224 ++++++++++++++++++ 2 files changed, 225 insertions(+), 1 deletion(-) diff --git a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs index 99337f3dc..41ab4d0c2 100644 --- a/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs +++ b/crates/oxc_angular_compiler/tests/decorator_metadata_ngtsc_test.rs @@ -299,7 +299,7 @@ fn decorator_metadata_matches_ngtsc() { failures.len(), failures.join("\n\n") ); - assert_eq!(compared, 740, "fixtures compared"); + assert_eq!(compared, 754, "fixtures compared"); } fn transform(source: &str) -> TransformResult { diff --git a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json index ec34e5e6f..acfbb4dee 100644 --- a/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json +++ b/crates/oxc_angular_compiler/tests/fixtures/decorator_metadata_ngtsc.json @@ -11378,6 +11378,230 @@ "diagnosticSpans": [[159, 160]], "classes": {}, "dtsImports": [] + }, + { + "name": "probe: r6-ns-queries-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SEL = 'foo'; }\ndeclare const S: typeof NS.SEL;\n@Directive({selector: '[d]', queries: {q: new ContentChild(S)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"foo\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-ns-queries-plain", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SEL = 'foo'; }\n@Directive({selector: '[d]', queries: {q: new ContentChild(NS.SEL)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,NS.SEL,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-ns-member-query-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SEL = 'foo'; }\ndeclare const S: typeof NS.SEL;\n@Directive({selector: '[d]', })\nexport class Dir {\n @ContentChild(S) q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"foo\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-class-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export class C {} }\ndeclare const K: typeof NS.C;\n@Directive({selector: '[d]', queries: {q: new ContentChild(K)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,K,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-class-plain", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export class C {} }\n@Directive({selector: '[d]', queries: {q: new ContentChild(NS.C)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,NS.C,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-member-class-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export class C {} }\ndeclare const K: typeof NS.C;\n@Directive({selector: '[d]', })\nexport class Dir {\n @ViewChild(K) q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery(K,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-sel-array-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SELS = ['a', 'b']; }\ndeclare const S: typeof NS.SELS;\n@Directive({selector: '[d]', queries: {q: new ContentChild(S)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"a\",\"b\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-read-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export class R {} }\ndeclare const K: typeof NS.R;\n@Directive({selector: '[d]', queries: {q: new ContentChild('foo', {read: K})}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"foo\"],5,K);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-static-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const ST = true; }\ndeclare const S: typeof NS.ST;\n@Directive({selector: '[d]', queries: {q: new ViewChild('foo', {static: S})}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"foo\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-member-static-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const ST = true; }\ndeclare const S: typeof NS.ST;\n@Directive({selector: '[d]', })\nexport class Dir {\n @ViewChild('foo', {static: S}) q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "viewQuery": "{if(rf&1){i0.ɵɵviewQuery([\"foo\"],7);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-queries-typeof", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SEL = 'foo'; }\ndeclare const S: typeof NS.SEL;\n@Directive({selector: '[d]', queries: {q: new ContentChild(S, {descendants: true})}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"foo\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-alias-sel", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SEL = 'foo'; }\nimport S = NS.SEL;\n@Directive({selector: '[d]', queries: {q: new ContentChild(S)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"foo\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-member-alias-sel", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export const SEL = 'foo'; }\nimport S = NS.SEL;\n@Directive({selector: '[d]', })\nexport class Dir {\n @ContentChild(S) q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,[\"foo\"],5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] + }, + { + "name": "probe: r6-q495-alias-class", + "origin": "probe", + "files": { + "test.ts": "import {Directive, Input, ContentChild, ViewChild} from '@angular/core';\nnamespace NS { export class C {} }\nimport K = NS.C;\n@Directive({selector: '[d]', queries: {q: new ContentChild(K)}})\nexport class Dir {\n q: any;\n}\n" + }, + "diagnostics": [], + "diagnosticSpans": [], + "classes": { + "Dir": { + "contentQueries": "{if(rf&1){i0.ɵɵcontentQuery(dirIndex,K,5);}if(rf&2){let_t;i0.ɵɵqueryRefresh(_t=i0.ɵɵloadQuery())&&(ctx.q=_t.first);}}", + "dts:ɵdir": "staticɵdir:i0.ɵɵDirectiveDeclaration;" + } + }, + "dtsImports": [] } ] }