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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 => ...)
Expand Down
43 changes: 12 additions & 31 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -2603,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,
Expand Down Expand Up @@ -2656,11 +2637,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)
Expand Down
55 changes: 45 additions & 10 deletions crates/oxc_angular_compiler/src/directive/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => ...)
Expand All @@ -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 =
Expand Down Expand Up @@ -959,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 };
Expand Down Expand Up @@ -1035,22 +1049,25 @@ 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, query members (`@ViewChild`, ...), 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 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<OxcDiagnostic> {
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(),
Expand All @@ -1070,7 +1087,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);
Expand Down Expand Up @@ -1134,12 +1151,30 @@ 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?;
let queries = super::parse_decorator_queries(
allocator,
config,
class,
source_text,
consts,
decorator_name,
);
queries.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(member_queries)
.or_else(queries)
.map(|(message, span)| OxcDiagnostic::error(message).with_label(span))
.into_iter()
.collect()
Expand Down
4 changes: 2 additions & 2 deletions crates/oxc_angular_compiler/src/directive/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 36 additions & 9 deletions crates/oxc_angular_compiler/src/directive/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,41 +411,67 @@ 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 =
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);
}

// 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);
}

// 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 =
Expand All @@ -456,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 =
Expand Down
11 changes: 7 additions & 4 deletions crates/oxc_angular_compiler/src/directive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -39,12 +41,13 @@ pub use metadata::{
QueryPredicate, R3DirectiveMetadata, R3DirectiveMetadataBuilder, R3HostDirectiveMetadata,
R3HostMetadata, R3InputMetadata, R3QueryMetadata,
};
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_input_metadata_in, extract_output_metadata,
extract_output_metadata_in, extract_view_queries,
};
pub(crate) use property_decorators::{
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,
};
Expand Down
Loading
Loading