diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index 2b8566e21..15eaa94ac 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -23,6 +23,7 @@ use crate::output::oxc_converter::convert_oxc_expression; pub fn build_decorator_metadata_array<'a>( allocator: &'a Allocator, decorators: &[&Decorator<'a>], + source_text: Option<&'a str>, ) -> OutputExpression<'a> { let mut decorator_entries = AllocVec::new_in(allocator); @@ -38,7 +39,7 @@ pub fn build_decorator_metadata_array<'a>( ))), Expression::StaticMemberExpression(member) => { // Handle namespaced decorators like ng.Component - convert_oxc_expression(allocator, &member.object).map(|receiver| { + convert_oxc_expression(allocator, &member.object, source_text).map(|receiver| { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in(receiver, allocator), @@ -77,7 +78,7 @@ pub fn build_decorator_metadata_array<'a>( let mut args = AllocVec::new_in(allocator); for arg in &call.arguments { let expr = arg.to_expression(); - if let Some(converted) = convert_oxc_expression(allocator, expr) { + if let Some(converted) = convert_oxc_expression(allocator, expr, source_text) { args.push(converted); } } @@ -122,6 +123,7 @@ pub fn build_ctor_params_metadata<'a>( constructor_deps: Option<&[R3DependencyMetadata<'a>]>, namespace_registry: &mut NamespaceRegistry<'a>, import_map: &ImportMap<'a>, + source_text: Option<&'a str>, ) -> Option> { // Find constructor let constructor = class.body.body.iter().find_map(|element| { @@ -163,7 +165,8 @@ pub fn build_ctor_params_metadata<'a>( // Extract decorators from the parameter let param_decorators = extract_angular_decorators_from_param(param); if !param_decorators.is_empty() { - let decorators_array = build_decorator_metadata_array(allocator, ¶m_decorators); + let decorators_array = + build_decorator_metadata_array(allocator, ¶m_decorators, source_text); map_entries.push(LiteralMapEntry { key: Ident::from("decorators"), value: decorators_array, @@ -205,6 +208,7 @@ pub fn build_ctor_params_metadata<'a>( pub fn build_prop_decorators_metadata<'a>( allocator: &'a Allocator, class: &Class<'a>, + source_text: Option<&'a str>, ) -> Option> { const ANGULAR_PROP_DECORATORS: &[&str] = &[ "Input", @@ -251,7 +255,8 @@ pub fn build_prop_decorators_metadata<'a>( } // Build decorators array for this property - let decorators_array = build_decorator_metadata_array(allocator, &angular_decorators); + let decorators_array = + build_decorator_metadata_array(allocator, &angular_decorators, source_text); prop_entries.push(LiteralMapEntry { key: prop_name, diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index deca15ac6..3ae725ff5 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -50,6 +50,7 @@ pub fn extract_component_metadata<'a>( class: &'a Class<'a>, implicit_standalone: bool, import_map: &ImportMap<'a>, + source_text: Option<&'a str>, ) -> Option> { // Get the class name let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into(); @@ -130,7 +131,8 @@ pub fn extract_component_metadata<'a>( // 1. The identifier list for local analysis metadata.imports = extract_identifier_array(allocator, &prop.value); // 2. The raw expression to pass to ɵɵgetComponentDepsFactory in RuntimeResolved mode - metadata.raw_imports = convert_oxc_expression(allocator, &prop.value); + metadata.raw_imports = + convert_oxc_expression(allocator, &prop.value, source_text); } "exportAs" => { // exportAs can be comma-separated: "foo, bar" @@ -150,7 +152,8 @@ pub fn extract_component_metadata<'a>( "animations" => { // Extract animations expression as full OutputExpression // Handles both identifier references and complex array expressions - metadata.animations = convert_oxc_expression(allocator, &prop.value); + metadata.animations = + convert_oxc_expression(allocator, &prop.value, source_text); } "schemas" => { // Extract schemas identifiers (e.g., [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]) @@ -159,11 +162,13 @@ pub fn extract_component_metadata<'a>( "providers" => { // Extract providers as full OutputExpression // Handles complex expressions like [{provide: TOKEN, useFactory: Factory}] - metadata.providers = convert_oxc_expression(allocator, &prop.value); + metadata.providers = + convert_oxc_expression(allocator, &prop.value, source_text); } "viewProviders" => { // Extract view providers as full OutputExpression - metadata.view_providers = convert_oxc_expression(allocator, &prop.value); + metadata.view_providers = + convert_oxc_expression(allocator, &prop.value, source_text); } "hostDirectives" => { // Extract host directives array @@ -236,7 +241,7 @@ pub fn extract_component_metadata<'a>( extract_constructor_deps(allocator, class, import_map, has_superclass); // Extract inputs from @Input decorators on class members - metadata.inputs = extract_input_metadata(allocator, class); + metadata.inputs = extract_input_metadata(allocator, class, source_text); // Extract outputs from @Output decorators on class members metadata.outputs = extract_output_metadata(allocator, class); @@ -1134,9 +1139,13 @@ mod tests { }; if let Some(class) = class { - if let Some(metadata) = - extract_component_metadata(&allocator, class, implicit_standalone, &import_map) - { + if let Some(metadata) = extract_component_metadata( + &allocator, + class, + implicit_standalone, + &import_map, + Some(code), + ) { found_metadata = Some(metadata); break; } diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 767a4428c..bac6b68f4 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -1616,9 +1616,13 @@ pub fn transform_angular_file( // Compute implicit_standalone based on Angular version let implicit_standalone = options.implicit_standalone(); - if let Some(mut metadata) = - extract_component_metadata(allocator, class, implicit_standalone, &import_map) - { + if let Some(mut metadata) = extract_component_metadata( + allocator, + class, + implicit_standalone, + &import_map, + Some(source), + ) { // 3. Resolve external styles and merge into metadata resolve_styles(allocator, &mut metadata, resolved_resources); @@ -1632,11 +1636,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); + 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); + let content_queries = extract_content_queries(allocator, class, Some(source)); // Collect content query property names for .d.ts generation // (before content_queries is moved into compile_component_full) @@ -1696,7 +1700,8 @@ pub fn transform_angular_file( // Check if the class also has an @Injectable decorator. // @Injectable is SHARED precedence and can coexist with @Component. - let has_injectable = extract_injectable_metadata(allocator, class); + let has_injectable = + extract_injectable_metadata(allocator, class, Some(source)); if let Some(injectable_metadata) = &has_injectable { if let Some(span) = find_injectable_decorator_span(class) { decorator_spans_to_remove.push(span); @@ -1758,6 +1763,7 @@ pub fn transform_angular_file( decorators: build_decorator_metadata_array( allocator, &[decorator], + Some(source), ), ctor_parameters: build_ctor_params_metadata( allocator, @@ -1765,9 +1771,12 @@ pub fn transform_angular_file( ctor_deps_slice, &mut file_namespace_registry, &import_map, + Some(source), ), prop_decorators: build_prop_decorators_metadata( - allocator, class, + allocator, + class, + Some(source), ), }; @@ -1848,7 +1857,7 @@ pub fn transform_angular_file( // the directive and creating conflicting property definitions (like // ɵfac getters) that interfere with the AOT-compiled assignments. if let Some(mut directive_metadata) = - extract_directive_metadata(allocator, class, implicit_standalone) + extract_directive_metadata(allocator, class, implicit_standalone, Some(source)) { // Track decorator span for removal if let Some(span) = find_directive_decorator_span(class) { @@ -1906,7 +1915,8 @@ pub fn transform_angular_file( // Check if the class also has an @Injectable decorator. // @Injectable is SHARED precedence and can coexist with @Directive. - let has_injectable = extract_injectable_metadata(allocator, class); + let has_injectable = + extract_injectable_metadata(allocator, class, Some(source)); if let Some(injectable_metadata) = &has_injectable { if let Some(span) = find_injectable_decorator_span(class) { decorator_spans_to_remove.push(span); @@ -1939,7 +1949,7 @@ pub fn transform_angular_file( class_definitions .insert(class_name, (property_assignments, String::new(), String::new())); } else if let Some(mut pipe_metadata) = - extract_pipe_metadata(allocator, class, implicit_standalone) + extract_pipe_metadata(allocator, class, implicit_standalone, Some(source)) { // Not a @Component or @Directive - check if it's a @Pipe (PRIMARY) // We need to compile @Pipe classes to generate ɵpipe and ɵfac definitions. @@ -1980,7 +1990,8 @@ pub fn transform_angular_file( // Check if the class also has an @Injectable decorator (issue #65). // @Injectable is SHARED precedence and can coexist with @Pipe. - let has_injectable = extract_injectable_metadata(allocator, class); + let has_injectable = + extract_injectable_metadata(allocator, class, Some(source)); if let Some(injectable_metadata) = &has_injectable { if let Some(span) = find_injectable_decorator_span(class) { decorator_spans_to_remove.push(span); @@ -2017,7 +2028,7 @@ pub fn transform_angular_file( ); } } else if let Some(mut ng_module_metadata) = - extract_ng_module_metadata(allocator, class) + extract_ng_module_metadata(allocator, class, Some(source)) { // Not a @Component, @Directive, @Injectable, or @Pipe - check if it's an @NgModule // We need to compile @NgModule classes to generate ɵmod, ɵfac, and ɵinj definitions. @@ -2061,7 +2072,8 @@ pub fn transform_angular_file( // Check if the class also has an @Injectable decorator. // @Injectable is SHARED precedence and can coexist with @NgModule. - let has_injectable = extract_injectable_metadata(allocator, class); + let has_injectable = + extract_injectable_metadata(allocator, class, Some(source)); if let Some(injectable_metadata) = &has_injectable { if let Some(span) = find_injectable_decorator_span(class) { decorator_spans_to_remove.push(span); @@ -2108,7 +2120,7 @@ pub fn transform_angular_file( ); } } else if let Some(mut injectable_metadata) = - extract_injectable_metadata(allocator, class) + extract_injectable_metadata(allocator, class, Some(source)) { // Standalone @Injectable (no PRIMARY decorator on the class) // We need to compile @Injectable classes to generate ɵprov and ɵfac definitions. diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 834194333..6883754a6 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -77,6 +77,7 @@ pub fn extract_directive_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, implicit_standalone: bool, + source_text: Option<&'a str>, ) -> Option> { // Get the class name let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into(); @@ -142,7 +143,9 @@ pub fn extract_directive_metadata<'a>( } } "providers" => { - if let Some(providers) = convert_oxc_expression(allocator, &prop.value) { + if let Some(providers) = + convert_oxc_expression(allocator, &prop.value, source_text) + { builder = builder.providers(providers); } } @@ -164,7 +167,7 @@ pub fn extract_directive_metadata<'a>( } // Extract @Input/@Output/@HostBinding/@HostListener from class members - builder = builder.extract_from_class(allocator, class); + builder = builder.extract_from_class(allocator, class, source_text); // Detect if ngOnChanges lifecycle hook is implemented // Similar to Angular's: const usesOnChanges = members.some(member => ...) @@ -180,7 +183,7 @@ pub fn extract_directive_metadata<'a>( // Extract constructor dependencies for factory generation // This enables proper DI for directive constructors // See: packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts - let constructor_deps = extract_constructor_deps(allocator, class, has_superclass); + let constructor_deps = extract_constructor_deps(allocator, class, has_superclass, source_text); if let Some(deps) = constructor_deps { builder = builder.deps(deps); } @@ -252,6 +255,7 @@ fn extract_constructor_deps<'a>( allocator: &'a Allocator, class: &'a Class<'a>, has_superclass: bool, + source_text: Option<&'a str>, ) -> Option>> { // Find the constructor method let constructor = class.body.body.iter().find_map(|element| { @@ -270,7 +274,7 @@ fn extract_constructor_deps<'a>( let mut deps = Vec::with_capacity_in(params.items.len(), allocator); for param in ¶ms.items { - let dep = extract_param_dependency(allocator, param); + let dep = extract_param_dependency(allocator, param, source_text); deps.push(dep); } @@ -290,6 +294,7 @@ fn extract_constructor_deps<'a>( fn extract_param_dependency<'a>( allocator: &'a Allocator, param: &oxc_ast::ast::FormalParameter<'a>, + source_text: Option<&'a str>, ) -> R3DependencyMetadata<'a> { // Extract flags and @Inject token from decorators let mut optional = false; @@ -306,7 +311,8 @@ fn extract_param_dependency<'a>( // @Inject(TOKEN) - extract the token if let Expression::CallExpression(call) = &decorator.expression { if let Some(arg) = call.arguments.first() { - inject_token = convert_oxc_expression(allocator, arg.to_expression()); + inject_token = + convert_oxc_expression(allocator, arg.to_expression(), source_text); } } } @@ -885,7 +891,7 @@ mod tests { if let Some(class) = class { if let Some(metadata) = - extract_directive_metadata(&allocator, class, implicit_standalone) + extract_directive_metadata(&allocator, class, implicit_standalone, Some(code)) { found_metadata = Some(metadata); break; diff --git a/crates/oxc_angular_compiler/src/directive/metadata.rs b/crates/oxc_angular_compiler/src/directive/metadata.rs index aba78a5c7..140422ca0 100644 --- a/crates/oxc_angular_compiler/src/directive/metadata.rs +++ b/crates/oxc_angular_compiler/src/directive/metadata.rs @@ -410,9 +410,15 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { /// /// # Returns /// The builder with all extracted metadata added. - pub fn extract_from_class(mut self, allocator: &'a Allocator, class: &'a Class<'a>) -> Self { + pub fn extract_from_class( + mut self, + allocator: &'a Allocator, + class: &'a Class<'a>, + source_text: Option<&'a str>, + ) -> Self { // Extract inputs from @Input decorators - let inputs = super::property_decorators::extract_input_metadata(allocator, class); + let inputs = + super::property_decorators::extract_input_metadata(allocator, class, source_text); for input in inputs { self = self.add_input(input); } @@ -424,13 +430,15 @@ impl<'a> R3DirectiveMetadataBuilder<'a> { } // Extract view queries from @ViewChild/@ViewChildren - let view_queries = super::property_decorators::extract_view_queries(allocator, class); + let view_queries = + super::property_decorators::extract_view_queries(allocator, class, source_text); 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); + let content_queries = + super::property_decorators::extract_content_queries(allocator, class, source_text); for query in content_queries { self = self.add_query(query); } @@ -597,7 +605,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestDirective")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestDirective"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -635,7 +643,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestDirective")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestDirective"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -668,7 +676,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestComponent")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestComponent"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -701,7 +709,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestComponent")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestComponent"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -734,7 +742,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestDirective")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestDirective"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -769,7 +777,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestDirective")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestDirective"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -807,7 +815,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("TestComponent")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestComponent"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -837,7 +845,7 @@ mod tests { let builder = R3DirectiveMetadataBuilder::new(&allocator) .name(Ident::from("EmptyDirective")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("EmptyDirective"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); @@ -870,7 +878,7 @@ mod tests { .name(Ident::from("TestDirective")) .r#type(OutputAstBuilder::variable(&allocator, Ident::from("TestDirective"))) .add_input(R3InputMetadata::simple(Ident::from("existingInput"))) - .extract_from_class(&allocator, class.unwrap()); + .extract_from_class(&allocator, class.unwrap(), Some(code)); let metadata = builder.build(); assert!(metadata.is_some()); diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index cf8c0d61e..f9610de77 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -150,6 +150,7 @@ impl<'a> Default for InputConfig<'a> { fn parse_input_config<'a>( allocator: &'a Allocator, decorator: &'a Decorator<'a>, + source_text: Option<&'a str>, ) -> InputConfig<'a> { let Expression::CallExpression(call) = &decorator.expression else { return InputConfig::default(); @@ -183,7 +184,8 @@ fn parse_input_config<'a>( config.required = extract_boolean_value(&prop.value).unwrap_or(false); } "transform" => { - config.transform = convert_oxc_expression(allocator, &prop.value); + config.transform = + convert_oxc_expression(allocator, &prop.value, source_text); } _ => {} } @@ -484,6 +486,7 @@ fn try_parse_signal_input<'a>( pub fn extract_input_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, ) -> Vec<'a, R3InputMetadata<'a>> { let mut inputs = Vec::new_in(allocator); @@ -496,7 +499,7 @@ pub fn extract_input_metadata<'a>( continue; }; - let config = parse_input_config(allocator, decorator); + let config = parse_input_config(allocator, decorator, source_text); let binding_property_name = config.alias.unwrap_or_else(|| class_property_name.clone()); @@ -537,7 +540,7 @@ pub fn extract_input_metadata<'a>( continue; }; - let config = parse_input_config(allocator, decorator); + let config = parse_input_config(allocator, decorator, source_text); let binding_property_name = config.alias.unwrap_or_else(|| class_property_name.clone()); @@ -561,7 +564,7 @@ pub fn extract_input_metadata<'a>( continue; }; - let config = parse_input_config(allocator, decorator); + let config = parse_input_config(allocator, decorator, source_text); let binding_property_name = config.alias.unwrap_or_else(|| class_property_name.clone()); @@ -754,6 +757,7 @@ fn parse_query_config<'a>( allocator: &'a Allocator, decorator: &'a Decorator<'a>, decorator_name: &str, + source_text: Option<&'a str>, ) -> QueryConfig<'a> { let Expression::CallExpression(call) = &decorator.expression else { return QueryConfig::default_for(decorator_name); @@ -779,7 +783,9 @@ fn parse_query_config<'a>( 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) { + if let Some(output_expr) = + convert_oxc_expression(allocator, unwrapped_expr, source_text) + { config.predicate = Some(QueryPredicate::Type(output_expr)); } } @@ -799,7 +805,8 @@ fn parse_query_config<'a>( config.is_static = extract_boolean_value(&prop.value).unwrap_or(false); } "read" => { - config.read = convert_oxc_expression(allocator, &prop.value); + config.read = + convert_oxc_expression(allocator, &prop.value, source_text); } "descendants" => { // Use the decorator-specific default if not explicitly set @@ -866,6 +873,7 @@ fn try_parse_signal_query<'a>( allocator: &'a Allocator, value: &'a Expression<'a>, property_name: Ident<'a>, + source_text: Option<&'a str>, ) -> Option<(SignalQueryType, R3QueryMetadata<'a>)> { // Check if the value is a call expression let call_expr = match value { @@ -941,7 +949,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 output_expr = convert_oxc_expression(allocator, unwrapped_expr)?; + let output_expr = convert_oxc_expression(allocator, unwrapped_expr, source_text)?; QueryPredicate::Type(output_expr) } }; @@ -960,7 +968,7 @@ fn try_parse_signal_query<'a>( match key_name.as_str() { "read" => { - read = convert_oxc_expression(allocator, &prop.value); + read = convert_oxc_expression(allocator, &prop.value, source_text); } "descendants" => { descendants = extract_boolean_value(&prop.value).unwrap_or(descendants); @@ -1007,6 +1015,7 @@ fn try_parse_signal_query<'a>( pub fn extract_view_queries<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, ) -> Vec<'a, R3QueryMetadata<'a>> { // Use separate vectors to match Angular's ordering approach. // Angular groups queries by type, maintaining declaration order within each group: @@ -1026,7 +1035,7 @@ pub fn extract_view_queries<'a>( 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) + try_parse_signal_query(allocator, value, property_name, source_text) { if query_type.is_view_query() { signal_queries.push(metadata); @@ -1039,7 +1048,8 @@ 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"); + let config = + parse_query_config(allocator, decorator, "ViewChild", source_text); if let Some(predicate) = config.predicate { view_child_queries.push(R3QueryMetadata { property_name, @@ -1057,7 +1067,8 @@ 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"); + let config = + parse_query_config(allocator, decorator, "ViewChildren", source_text); if let Some(predicate) = config.predicate { view_children_queries.push(R3QueryMetadata { property_name, @@ -1079,7 +1090,8 @@ 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"); + let config = + parse_query_config(allocator, decorator, "ViewChild", source_text); if let Some(predicate) = config.predicate { view_child_queries.push(R3QueryMetadata { property_name, @@ -1097,7 +1109,8 @@ 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"); + let config = + parse_query_config(allocator, decorator, "ViewChildren", source_text); if let Some(predicate) = config.predicate { view_children_queries.push(R3QueryMetadata { property_name, @@ -1145,6 +1158,7 @@ pub fn extract_view_queries<'a>( pub fn extract_content_queries<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, ) -> Vec<'a, R3QueryMetadata<'a>> { // Use separate vectors to match Angular's ordering approach. // Angular groups queries by type, maintaining declaration order within each group: @@ -1164,7 +1178,7 @@ pub fn extract_content_queries<'a>( 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) + try_parse_signal_query(allocator, value, property_name, source_text) { if !query_type.is_view_query() { signal_queries.push(metadata); @@ -1177,7 +1191,8 @@ 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"); + let config = + parse_query_config(allocator, decorator, "ContentChild", source_text); if let Some(predicate) = config.predicate { content_child_queries.push(R3QueryMetadata { property_name, @@ -1195,7 +1210,12 @@ pub fn extract_content_queries<'a>( find_decorator_by_name(&prop.decorators, "ContentChildren") { if let Some(property_name) = get_property_key_name(&prop.key) { - let config = parse_query_config(allocator, decorator, "ContentChildren"); + let config = parse_query_config( + allocator, + decorator, + "ContentChildren", + source_text, + ); if let Some(predicate) = config.predicate { content_children_queries.push(R3QueryMetadata { property_name, @@ -1218,7 +1238,8 @@ 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"); + let config = + parse_query_config(allocator, decorator, "ContentChild", source_text); if let Some(predicate) = config.predicate { content_child_queries.push(R3QueryMetadata { property_name, @@ -1236,7 +1257,12 @@ pub fn extract_content_queries<'a>( find_decorator_by_name(&method.decorators, "ContentChildren") { if let Some(property_name) = get_property_key_name(&method.key) { - let config = parse_query_config(allocator, decorator, "ContentChildren"); + let config = parse_query_config( + allocator, + decorator, + "ContentChildren", + source_text, + ); if let Some(predicate) = config.predicate { content_children_queries.push(R3QueryMetadata { property_name, @@ -1493,7 +1519,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "value"); @@ -1514,7 +1540,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "inputAlias"); @@ -1532,7 +1558,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "myAlias"); @@ -1551,7 +1577,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "value"); @@ -1570,7 +1596,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "disabled"); assert!(inputs[0].transform_function.is_some()); @@ -1591,7 +1617,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 3); assert_eq!(inputs[0].class_property_name.as_str(), "name"); @@ -1619,7 +1645,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); } @@ -1640,7 +1666,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "formGroup"); assert_eq!(inputs[0].binding_property_name.as_str(), "formGroup"); @@ -1661,7 +1687,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "count"); assert_eq!(inputs[0].binding_property_name.as_str(), "count"); @@ -1681,7 +1707,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "name"); assert_eq!(inputs[0].binding_property_name.as_str(), "name"); @@ -1701,7 +1727,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "myAlias"); @@ -1721,7 +1747,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "requiredAlias"); @@ -1744,7 +1770,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 3); // Decorator input @@ -1776,7 +1802,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "realInput"); assert!(inputs[0].is_signal); @@ -1799,7 +1825,7 @@ mod tests { assert!(class.is_some()); // Check inputs - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "open"); assert_eq!(inputs[0].binding_property_name.as_str(), "open"); @@ -1826,7 +1852,7 @@ mod tests { assert!(class.is_some()); // Check inputs - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "count"); assert_eq!(inputs[0].binding_property_name.as_str(), "count"); @@ -1853,7 +1879,7 @@ mod tests { assert!(class.is_some()); // Check inputs - binding name should be alias - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "myAlias"); @@ -1880,7 +1906,7 @@ mod tests { assert!(class.is_some()); // Check inputs - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "value"); assert_eq!(inputs[0].binding_property_name.as_str(), "requiredAlias"); @@ -1911,7 +1937,7 @@ mod tests { assert!(class.is_some()); // Check inputs - should have all 4 inputs - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 4); // Decorator input @@ -1957,7 +1983,7 @@ mod tests { assert!(class.is_some()); // Only realModel should be detected as input - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); assert_eq!(inputs[0].class_property_name.as_str(), "realModel"); assert!(inputs[0].is_signal); @@ -2158,7 +2184,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); let outputs = extract_output_metadata(&allocator, class.as_ref().unwrap()); assert_eq!(inputs.len(), 1); @@ -2178,7 +2204,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); let outputs = extract_output_metadata(&allocator, class.as_ref().unwrap()); assert_eq!(inputs.len(), 0); @@ -2201,7 +2227,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "child"); assert!(queries[0].first); @@ -2223,7 +2249,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "child"); assert!(queries[0].first); @@ -2247,7 +2273,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "child"); assert!(queries[0].first); @@ -2267,7 +2293,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "items"); assert!(!queries[0].first); // ViewChildren returns multiple @@ -2290,7 +2316,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "panel"); assert!(queries[0].first); @@ -2310,7 +2336,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "header"); assert!(queries[0].first); @@ -2334,7 +2360,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "item"); assert!(!queries[0].descendants); @@ -2352,7 +2378,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "tabs"); assert!(!queries[0].first); // ContentChildren returns multiple @@ -2373,8 +2399,9 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap()); - let content_queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); + let content_queries = + extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(view_queries.len(), 2); assert_eq!(content_queries.len(), 2); @@ -2647,10 +2674,11 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); let outputs = extract_output_metadata(&allocator, class.as_ref().unwrap()); - let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap()); - let content_queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); + let content_queries = + extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); let host_bindings = extract_host_bindings(&allocator, class.as_ref().unwrap()); let host_listeners = extract_host_listeners(&allocator, class.as_ref().unwrap()); @@ -2678,7 +2706,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "content"); assert!(queries[0].first); // viewChild returns single @@ -2700,7 +2728,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "myRef"); assert!(queries[0].first); @@ -2725,7 +2753,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "child"); assert!(queries[0].first); @@ -2745,7 +2773,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "items"); assert!(!queries[0].first); // viewChildren returns multiple @@ -2765,7 +2793,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "panel"); assert!(queries[0].first); // contentChild returns single @@ -2785,7 +2813,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "header"); assert!(queries[0].first); @@ -2810,7 +2838,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "tabs"); assert!(!queries[0].first); // contentChildren returns multiple @@ -2831,7 +2859,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert!(queries[0].descendants); // Explicitly set to true } @@ -2848,7 +2876,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "portal"); assert!(queries[0].first); // viewChild returns single @@ -2868,7 +2896,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "myRef"); assert!(queries[0].first); @@ -2893,7 +2921,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 1); assert_eq!(queries[0].property_name.as_str(), "content"); assert!(queries[0].first); // contentChild returns single @@ -2914,7 +2942,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let queries = extract_view_queries(&allocator, class.as_ref().unwrap()); + let queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(queries.len(), 2); assert_eq!(queries[0].property_name.as_str(), "portal"); assert!(queries[0].is_signal); @@ -2940,8 +2968,9 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap()); - let content_queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); + let content_queries = + extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(view_queries.len(), 2); // Signal queries come first (Angular's ordering) @@ -2984,7 +3013,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 2); // counter: input(0) - optional signal input with default value @@ -3027,7 +3056,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 6); // Signal inputs first (in declaration order) @@ -3092,7 +3121,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 1); // Signal input transforms are NOT captured (transformFunction: null in Angular's output) @@ -3127,7 +3156,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); assert_eq!(inputs.len(), 4); // All should be required signal inputs with NO transform captured @@ -3176,8 +3205,9 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap()); - let content_queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); + let content_queries = + extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); // View queries: query1, query2, query5, query6, query7 assert_eq!(view_queries.len(), 5); @@ -3259,8 +3289,9 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap()); - let content_queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); + let content_queries = + extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); // Angular ordering: signal queries FIRST, then decorator queries // viewQueries: [signalViewChild, decoratorViewChild] @@ -3390,7 +3421,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); let outputs = extract_output_metadata(&allocator, class.as_ref().unwrap()); // Model creates inputs @@ -3443,7 +3474,7 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap()); + let inputs = extract_input_metadata(&allocator, class.as_ref().unwrap(), Some(code)); let outputs = extract_output_metadata(&allocator, class.as_ref().unwrap()); // Inputs: 2 from model + 2 from @Input = 4 @@ -3502,8 +3533,9 @@ mod tests { let class = parse_class(&allocator, code); assert!(class.is_some()); - let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap()); - let content_queries = extract_content_queries(&allocator, class.as_ref().unwrap()); + let view_queries = extract_view_queries(&allocator, class.as_ref().unwrap(), Some(code)); + let content_queries = + extract_content_queries(&allocator, class.as_ref().unwrap(), Some(code)); // viewChild.required() creates a required signal query assert_eq!(view_queries.len(), 1); diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index 4f478c17d..8f2161b69 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -217,6 +217,7 @@ pub fn find_injectable_decorator_span(class: &Class<'_>) -> Option { pub fn extract_injectable_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, ) -> Option> { let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into(); let class_span = class.span; @@ -240,7 +241,7 @@ pub fn extract_injectable_metadata<'a>( use_factory: None, use_value: None, use_existing: None, - deps: extract_constructor_deps(allocator, class), + deps: extract_constructor_deps(allocator, class, source_text), }); } @@ -251,22 +252,22 @@ pub fn extract_injectable_metadata<'a>( }; // Extract providedIn (None if not specified) - let provided_in = extract_provided_in(allocator, config_obj); + let provided_in = extract_provided_in(allocator, config_obj, source_text); // Extract useClass - let use_class = extract_use_class(allocator, config_obj); + let use_class = extract_use_class(allocator, config_obj, source_text); // Extract useFactory - let use_factory = extract_use_factory(allocator, config_obj); + let use_factory = extract_use_factory(allocator, config_obj, source_text); // Extract useValue - let use_value = extract_use_value(allocator, config_obj); + let use_value = extract_use_value(allocator, config_obj, source_text); // Extract useExisting - let use_existing = extract_use_existing(allocator, config_obj); + let use_existing = extract_use_existing(allocator, config_obj, source_text); // Extract constructor dependencies - let deps = extract_constructor_deps(allocator, class); + let deps = extract_constructor_deps(allocator, class, source_text); Some(InjectableMetadata { class_name, @@ -302,12 +303,13 @@ fn get_property_key_name<'a>(key: &'a PropertyKey<'a>) -> Option> { fn extract_provided_in<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop { if let Some(key_name) = get_property_key_name(&prop.key) { if key_name.as_str() == "providedIn" { - return parse_provided_in_value(allocator, &prop.value); + return parse_provided_in_value(allocator, &prop.value, source_text); } } } @@ -318,6 +320,7 @@ fn extract_provided_in<'a>( fn parse_provided_in_value<'a>( allocator: &'a Allocator, expr: &'a Expression<'a>, + source_text: Option<&'a str>, ) -> Option> { match expr { Expression::StringLiteral(s) => match s.value.as_str() { @@ -329,7 +332,8 @@ fn parse_provided_in_value<'a>( Expression::NullLiteral(_) => Some(ProvidedInValue::None), _ => { // Check for forwardRef - let (expression, is_forward_ref) = extract_forward_ref_or_expression(allocator, expr)?; + let (expression, is_forward_ref) = + extract_forward_ref_or_expression(allocator, expr, source_text)?; Some(ProvidedInValue::Module { expression, is_forward_ref }) } } @@ -338,14 +342,15 @@ fn parse_provided_in_value<'a>( fn extract_use_class<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop { if let Some(key_name) = get_property_key_name(&prop.key) { if key_name.as_str() == "useClass" { let (class_expr, is_forward_ref) = - extract_forward_ref_or_expression(allocator, &prop.value)?; - let deps = extract_deps_from_config(allocator, config_obj); + extract_forward_ref_or_expression(allocator, &prop.value, source_text)?; + let deps = extract_deps_from_config(allocator, config_obj, source_text); return Some(UseClassMetadata { class_expr, is_forward_ref, deps }); } } @@ -357,13 +362,14 @@ fn extract_use_class<'a>( fn extract_use_factory<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop { if let Some(key_name) = get_property_key_name(&prop.key) { if key_name.as_str() == "useFactory" { - let factory = convert_oxc_expression(allocator, &prop.value)?; - let deps = extract_deps_from_config(allocator, config_obj); + let factory = convert_oxc_expression(allocator, &prop.value, source_text)?; + let deps = extract_deps_from_config(allocator, config_obj, source_text); return Some(UseFactoryMetadata { factory, deps }); } } @@ -375,12 +381,13 @@ fn extract_use_factory<'a>( fn extract_use_value<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop { if let Some(key_name) = get_property_key_name(&prop.key) { if key_name.as_str() == "useValue" { - return convert_oxc_expression(allocator, &prop.value); + return convert_oxc_expression(allocator, &prop.value, source_text); } } } @@ -391,13 +398,14 @@ fn extract_use_value<'a>( fn extract_use_existing<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop { if let Some(key_name) = get_property_key_name(&prop.key) { if key_name.as_str() == "useExisting" { let (existing, is_forward_ref) = - extract_forward_ref_or_expression(allocator, &prop.value)?; + extract_forward_ref_or_expression(allocator, &prop.value, source_text)?; return Some(UseExistingMetadata { existing, is_forward_ref }); } } @@ -411,6 +419,7 @@ fn extract_use_existing<'a>( fn extract_forward_ref_or_expression<'a>( allocator: &'a Allocator, expr: &'a Expression<'a>, + source_text: Option<&'a str>, ) -> Option<(OutputExpression<'a>, bool)> { // Check if this is forwardRef(() => X) if let Expression::CallExpression(call) = expr { @@ -423,9 +432,11 @@ fn extract_forward_ref_or_expression<'a>( if let Some(oxc_ast::ast::Statement::ExpressionStatement(expr_stmt)) = arrow.body.statements.first() { - if let Some(expression) = - convert_oxc_expression(allocator, &expr_stmt.expression) - { + if let Some(expression) = convert_oxc_expression( + allocator, + &expr_stmt.expression, + source_text, + ) { return Some((expression, true)); } } @@ -434,12 +445,13 @@ fn extract_forward_ref_or_expression<'a>( } } } - convert_oxc_expression(allocator, expr).map(|e| (e, false)) + convert_oxc_expression(allocator, expr, source_text).map(|e| (e, false)) } fn extract_deps_from_config<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Vec<'a, DependencyMetadata<'a>> { let mut deps = Vec::new_in(allocator); @@ -449,7 +461,7 @@ fn extract_deps_from_config<'a>( if key_name.as_str() == "deps" { if let Expression::ArrayExpression(arr) = &prop.value { for element in &arr.elements { - if let Some(dep) = extract_dependency(allocator, element) { + if let Some(dep) = extract_dependency(allocator, element, source_text) { deps.push(dep); } } @@ -466,12 +478,13 @@ fn extract_deps_from_config<'a>( fn extract_dependency<'a>( allocator: &'a Allocator, element: &'a ArrayExpressionElement<'a>, + source_text: Option<&'a str>, ) -> Option> { match element { ArrayExpressionElement::SpreadElement(_) | ArrayExpressionElement::Elision(_) => None, _ => { let expr = element.to_expression(); - convert_oxc_expression(allocator, expr).map(|t| DependencyMetadata { + convert_oxc_expression(allocator, expr, source_text).map(|t| DependencyMetadata { token: t, optional: false, self_: false, @@ -509,6 +522,7 @@ fn extract_dependency<'a>( pub fn extract_constructor_deps<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, ) -> Option>> { // Find the constructor method let constructor = class.body.body.iter().find_map(|element| { @@ -525,7 +539,7 @@ pub fn extract_constructor_deps<'a>( let mut deps = Vec::with_capacity_in(params.items.len(), allocator); for param in ¶ms.items { - let dep = extract_param_dependency(allocator, param); + let dep = extract_param_dependency(allocator, param, source_text); deps.push(dep); } @@ -536,6 +550,7 @@ pub fn extract_constructor_deps<'a>( fn extract_param_dependency<'a>( allocator: &'a Allocator, param: &oxc_ast::ast::FormalParameter<'a>, + source_text: Option<&'a str>, ) -> R3DependencyMetadata<'a> { // Extract flags and @Inject token from decorators let mut optional = false; @@ -552,7 +567,8 @@ fn extract_param_dependency<'a>( // @Inject(TOKEN) - extract the token if let Expression::CallExpression(call) = &decorator.expression { if let Some(arg) = call.arguments.first() { - inject_token = convert_oxc_expression(allocator, arg.to_expression()); + inject_token = + convert_oxc_expression(allocator, arg.to_expression(), source_text); } } } @@ -663,7 +679,7 @@ mod tests { for stmt in &program.body { if let oxc_ast::ast::Statement::ClassDeclaration(class) = stmt { - return extract_injectable_metadata(allocator, class); + return extract_injectable_metadata(allocator, class, Some(source)); } } None diff --git a/crates/oxc_angular_compiler/src/injectable/definition.rs b/crates/oxc_angular_compiler/src/injectable/definition.rs index 2805bac60..91c52d69e 100644 --- a/crates/oxc_angular_compiler/src/injectable/definition.rs +++ b/crates/oxc_angular_compiler/src/injectable/definition.rs @@ -368,7 +368,7 @@ mod tests { }); let class = class.expect("Should find class declaration"); - let metadata = extract_injectable_metadata(&allocator, class); + let metadata = extract_injectable_metadata(&allocator, class, Some(code)); let metadata = metadata.expect("Should extract Injectable metadata"); // Verify deps are extracted @@ -424,7 +424,7 @@ mod tests { }); let class = class.expect("Should find class declaration"); - let metadata = extract_injectable_metadata(&allocator, class); + let metadata = extract_injectable_metadata(&allocator, class, Some(code)); let metadata = metadata.expect("Should extract Injectable metadata"); // Verify no deps (no constructor) @@ -469,7 +469,7 @@ mod tests { }); let class = class.expect("Should find class declaration"); - let metadata = extract_injectable_metadata(&allocator, class); + let metadata = extract_injectable_metadata(&allocator, class, Some(code)); let metadata = metadata.expect("Should extract Injectable metadata"); // Generate definition diff --git a/crates/oxc_angular_compiler/src/ir/expression.rs b/crates/oxc_angular_compiler/src/ir/expression.rs index 6f9f06ff9..76e2f9e43 100644 --- a/crates/oxc_angular_compiler/src/ir/expression.rs +++ b/crates/oxc_angular_compiler/src/ir/expression.rs @@ -2075,7 +2075,8 @@ fn transform_expressions_in_output_expression<'a, F>( | OutputExpression::External(_) | OutputExpression::LocalizedString(_) | OutputExpression::WrappedNode(_) - | OutputExpression::DynamicImport(_) => {} + | OutputExpression::DynamicImport(_) + | OutputExpression::RawSource(_) => {} } } diff --git a/crates/oxc_angular_compiler/src/ng_module/decorator.rs b/crates/oxc_angular_compiler/src/ng_module/decorator.rs index 63e0fa564..97def25ab 100644 --- a/crates/oxc_angular_compiler/src/ng_module/decorator.rs +++ b/crates/oxc_angular_compiler/src/ng_module/decorator.rs @@ -180,6 +180,7 @@ impl<'a> NgModuleMetadata<'a> { pub fn extract_ng_module_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, + source_text: Option<&'a str>, ) -> Option> { // Get the class name let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into(); @@ -235,7 +236,8 @@ pub fn extract_ng_module_metadata<'a>( // Also store the raw imports expression for ɵinj generation. // This preserves call expressions like StoreModule.forRoot(...) // and spread elements that are dropped by extract_reference_array. - metadata.raw_imports_expr = convert_oxc_expression(allocator, &prop.value); + metadata.raw_imports_expr = + convert_oxc_expression(allocator, &prop.value, source_text); } "exports" => { let (identifiers, has_forward_refs) = @@ -246,7 +248,8 @@ pub fn extract_ng_module_metadata<'a>( } } "providers" => { - metadata.providers = convert_oxc_expression(allocator, &prop.value); + metadata.providers = + convert_oxc_expression(allocator, &prop.value, source_text); } "bootstrap" => { let (identifiers, has_forward_refs) = @@ -571,7 +574,7 @@ mod tests { }; if let Some(class) = class { - if let Some(metadata) = extract_ng_module_metadata(&allocator, class) { + if let Some(metadata) = extract_ng_module_metadata(&allocator, class, Some(code)) { found_metadata = Some(metadata); break; } diff --git a/crates/oxc_angular_compiler/src/ng_module/definition.rs b/crates/oxc_angular_compiler/src/ng_module/definition.rs index 2cdc5aaaf..39bd9e4e1 100644 --- a/crates/oxc_angular_compiler/src/ng_module/definition.rs +++ b/crates/oxc_angular_compiler/src/ng_module/definition.rs @@ -538,7 +538,7 @@ mod tests { }); let class = class.expect("Should find class declaration"); - let metadata = extract_ng_module_metadata(&allocator, class); + let metadata = extract_ng_module_metadata(&allocator, class, Some(code)); let metadata = metadata.expect("Should extract NgModule metadata"); let definition = generate_ng_module_definition_from_decorator(&allocator, &metadata); @@ -578,7 +578,7 @@ mod tests { }); let class = class.expect("Should find class declaration"); - let metadata = extract_ng_module_metadata(&allocator, class); + let metadata = extract_ng_module_metadata(&allocator, class, Some(code)); let metadata = metadata.expect("Should extract NgModule metadata"); // Verify deps are extracted diff --git a/crates/oxc_angular_compiler/src/output/ast.rs b/crates/oxc_angular_compiler/src/output/ast.rs index 1ac69d59e..ce5ba5d6f 100644 --- a/crates/oxc_angular_compiler/src/output/ast.rs +++ b/crates/oxc_angular_compiler/src/output/ast.rs @@ -460,6 +460,12 @@ pub enum OutputExpression<'a> { // Spread element (for array spread) /// Spread element (...expr). SpreadElement(Box<'a, SpreadElementExpr<'a>>), + + // Raw source (preserved verbatim from source code) + /// Raw source expression, used when the expression contains constructs that + /// the output AST cannot represent (e.g., block-body arrow functions with + /// complex statements like variable declarations, if/else, for loops, etc.). + RawSource(Box<'a, RawSourceExpr<'a>>), } /// Literal expression. @@ -833,6 +839,21 @@ pub struct SpreadElementExpr<'a> { pub source_span: Option, } +/// Raw source expression (preserved verbatim from source code). +/// +/// Used when the expression contains constructs that the output AST cannot +/// represent, such as block-body arrow functions with variable declarations, +/// if/else statements, for loops, try/catch, etc. Rather than silently +/// dropping these constructs, the raw source text is preserved and emitted +/// verbatim. +#[derive(Debug)] +pub struct RawSourceExpr<'a> { + /// The raw source text of the expression. + pub source: Ident<'a>, + /// Source span. + pub source_span: Option, +} + // ============================================================================ // Statements // ============================================================================ @@ -1072,6 +1093,10 @@ impl<'a> OutputExpression<'a> { (OutputExpression::SpreadElement(a), OutputExpression::SpreadElement(b)) => { a.expr.is_equivalent(&b.expr) } + // Raw source + (OutputExpression::RawSource(a), OutputExpression::RawSource(b)) => { + a.source == b.source + } _ => false, } } @@ -1402,6 +1427,10 @@ impl<'a> OutputExpression<'a> { }, allocator, )), + OutputExpression::RawSource(e) => OutputExpression::RawSource(Box::new_in( + RawSourceExpr { source: e.source.clone(), source_span: e.source_span }, + allocator, + )), } } } @@ -1804,6 +1833,9 @@ pub trait RecursiveOutputAstVisitor<'a> { OutputExpression::WrappedNode(e) => self.visit_wrapped_node(e), OutputExpression::WrappedIrNode(e) => self.visit_wrapped_ir_node(e), OutputExpression::SpreadElement(e) => self.visit_spread_element(e), + OutputExpression::RawSource(_) => { + // Raw source is opaque — no sub-expressions to visit + } } } diff --git a/crates/oxc_angular_compiler/src/output/emitter.rs b/crates/oxc_angular_compiler/src/output/emitter.rs index 350b2ec4b..d2c69e8cc 100644 --- a/crates/oxc_angular_compiler/src/output/emitter.rs +++ b/crates/oxc_angular_compiler/src/output/emitter.rs @@ -355,6 +355,7 @@ fn get_source_span(expr: &OutputExpression<'_>) -> Option { OutputExpression::WrappedNode(e) => e.source_span, OutputExpression::WrappedIrNode(e) => e.source_span, OutputExpression::SpreadElement(e) => e.source_span, + OutputExpression::RawSource(e) => e.source_span, } } @@ -875,6 +876,9 @@ impl JsEmitter { ctx.print("..."); self.visit_expression(&e.expr, ctx); } + OutputExpression::RawSource(e) => { + ctx.print(&e.source); + } } } diff --git a/crates/oxc_angular_compiler/src/output/oxc_converter.rs b/crates/oxc_angular_compiler/src/output/oxc_converter.rs index cc6fd85af..615929964 100644 --- a/crates/oxc_angular_compiler/src/output/oxc_converter.rs +++ b/crates/oxc_angular_compiler/src/output/oxc_converter.rs @@ -16,15 +16,15 @@ use oxc_ast::ast::{ Argument, ArrayExpressionElement, BindingPattern, Expression, ObjectPropertyKind, PropertyKey, UnaryOperator as OxcUnaryOperator, }; -use oxc_span::Ident; +use oxc_span::{Ident, Span}; use super::ast::{ ArrowFunctionBody, ArrowFunctionExpr, BinaryOperator, BinaryOperatorExpr, CommaExpr, ConditionalExpr, FnParam, InstantiateExpr, InvokeFunctionExpr, LiteralArrayExpr, LiteralExpr, LiteralMapEntry, LiteralMapExpr, LiteralValue, NotExpr, OutputExpression, OutputStatement, - ParenthesizedExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, SpreadElementExpr, - TemplateLiteralElement, TemplateLiteralExpr, TypeofExpr, UnaryOperator, UnaryOperatorExpr, - VoidExpr, + ParenthesizedExpr, RawSourceExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, + SpreadElementExpr, TemplateLiteralElement, TemplateLiteralExpr, TypeofExpr, UnaryOperator, + UnaryOperatorExpr, VoidExpr, }; // ============================================================================ @@ -39,12 +39,16 @@ use super::ast::{ /// # Arguments /// * `allocator` - The allocator for creating new nodes /// * `expr` - The OXC expression to convert +/// * `source_text` - Optional source text for falling back to raw source when +/// complex expressions (e.g., block-body arrow functions with unsupported +/// statement types) cannot be fully represented in the output AST. /// /// # Returns /// `Some(OutputExpression)` if conversion succeeded, `None` otherwise. pub fn convert_oxc_expression<'a>( allocator: &'a Allocator, expr: &Expression<'a>, + source_text: Option<&'a str>, ) -> Option> { match expr { // Literals @@ -78,25 +82,30 @@ pub fn convert_oxc_expression<'a>( ))), // Array expressions - Expression::ArrayExpression(arr) => convert_array_expression(allocator, arr), + Expression::ArrayExpression(arr) => convert_array_expression(allocator, arr, source_text), // Object expressions - Expression::ObjectExpression(obj) => convert_object_expression(allocator, obj), + Expression::ObjectExpression(obj) => convert_object_expression(allocator, obj, source_text), // Call expressions - Expression::CallExpression(call) => convert_call_expression(allocator, call), + Expression::CallExpression(call) => convert_call_expression(allocator, call, source_text), // New expressions - Expression::NewExpression(new_expr) => convert_new_expression(allocator, new_expr), + Expression::NewExpression(new_expr) => { + convert_new_expression(allocator, new_expr, source_text) + } // Arrow function expressions Expression::ArrowFunctionExpression(arrow) => { - convert_arrow_function_expression(allocator, arrow) + convert_arrow_function_expression(allocator, arrow, source_text) } + // Function expressions - fall back to raw source if available + Expression::FunctionExpression(func) => make_raw_source(allocator, source_text, func.span), + // Member expressions Expression::StaticMemberExpression(member) => { - let receiver = convert_oxc_expression(allocator, &member.object)?; + let receiver = convert_oxc_expression(allocator, &member.object, source_text)?; Some(OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in(receiver, allocator), @@ -109,8 +118,8 @@ pub fn convert_oxc_expression<'a>( } Expression::ComputedMemberExpression(member) => { - let receiver = convert_oxc_expression(allocator, &member.object)?; - let index = convert_oxc_expression(allocator, &member.expression)?; + let receiver = convert_oxc_expression(allocator, &member.object, source_text)?; + let index = convert_oxc_expression(allocator, &member.expression, source_text)?; Some(OutputExpression::ReadKey(Box::new_in( ReadKeyExpr { receiver: Box::new_in(receiver, allocator), @@ -123,12 +132,14 @@ pub fn convert_oxc_expression<'a>( } // Chain expression (optional chaining) - Expression::ChainExpression(chain) => convert_chain_element(allocator, &chain.expression), + Expression::ChainExpression(chain) => { + convert_chain_element(allocator, &chain.expression, source_text) + } // Binary expressions Expression::BinaryExpression(bin) => { - let lhs = convert_oxc_expression(allocator, &bin.left)?; - let rhs = convert_oxc_expression(allocator, &bin.right)?; + let lhs = convert_oxc_expression(allocator, &bin.left, source_text)?; + let rhs = convert_oxc_expression(allocator, &bin.right, source_text)?; let operator = convert_oxc_binary_operator(bin.operator)?; Some(OutputExpression::BinaryOperator(Box::new_in( BinaryOperatorExpr { @@ -143,8 +154,8 @@ pub fn convert_oxc_expression<'a>( // Logical expressions (&&, ||, ??) Expression::LogicalExpression(logical) => { - let lhs = convert_oxc_expression(allocator, &logical.left)?; - let rhs = convert_oxc_expression(allocator, &logical.right)?; + let lhs = convert_oxc_expression(allocator, &logical.left, source_text)?; + let rhs = convert_oxc_expression(allocator, &logical.right, source_text)?; let operator = match logical.operator { oxc_ast::ast::LogicalOperator::And => BinaryOperator::And, oxc_ast::ast::LogicalOperator::Or => BinaryOperator::Or, @@ -162,13 +173,15 @@ pub fn convert_oxc_expression<'a>( } // Unary expressions - Expression::UnaryExpression(unary) => convert_unary_expression(allocator, unary), + Expression::UnaryExpression(unary) => { + convert_unary_expression(allocator, unary, source_text) + } // Conditional expressions (ternary) Expression::ConditionalExpression(cond) => { - let condition = convert_oxc_expression(allocator, &cond.test)?; - let true_case = convert_oxc_expression(allocator, &cond.consequent)?; - let false_case = convert_oxc_expression(allocator, &cond.alternate)?; + let condition = convert_oxc_expression(allocator, &cond.test, source_text)?; + let true_case = convert_oxc_expression(allocator, &cond.consequent, source_text)?; + let false_case = convert_oxc_expression(allocator, &cond.alternate, source_text)?; Some(OutputExpression::Conditional(Box::new_in( ConditionalExpr { condition: Box::new_in(condition, allocator), @@ -181,11 +194,11 @@ pub fn convert_oxc_expression<'a>( } // Template literals - Expression::TemplateLiteral(tpl) => convert_template_literal(allocator, tpl), + Expression::TemplateLiteral(tpl) => convert_template_literal(allocator, tpl, source_text), // Parenthesized expressions Expression::ParenthesizedExpression(paren) => { - let inner = convert_oxc_expression(allocator, &paren.expression)?; + let inner = convert_oxc_expression(allocator, &paren.expression, source_text)?; Some(OutputExpression::Parenthesized(Box::new_in( ParenthesizedExpr { expr: Box::new_in(inner, allocator), source_span: None }, allocator, @@ -196,7 +209,7 @@ pub fn convert_oxc_expression<'a>( Expression::SequenceExpression(seq) => { let mut parts = OxcVec::with_capacity_in(seq.expressions.len(), allocator); for expr in &seq.expressions { - parts.push(convert_oxc_expression(allocator, expr)?); + parts.push(convert_oxc_expression(allocator, expr, source_text)?); } Some(OutputExpression::Comma(Box::new_in( CommaExpr { parts, source_span: None }, @@ -212,18 +225,20 @@ pub fn convert_oxc_expression<'a>( // TypeScript type expressions - unwrap the inner expression // These don't affect runtime behavior, just compile-time type checking - Expression::TSAsExpression(ts_as) => convert_oxc_expression(allocator, &ts_as.expression), + Expression::TSAsExpression(ts_as) => { + convert_oxc_expression(allocator, &ts_as.expression, source_text) + } Expression::TSTypeAssertion(ts_assert) => { - convert_oxc_expression(allocator, &ts_assert.expression) + convert_oxc_expression(allocator, &ts_assert.expression, source_text) } Expression::TSSatisfiesExpression(ts_satisfies) => { - convert_oxc_expression(allocator, &ts_satisfies.expression) + convert_oxc_expression(allocator, &ts_satisfies.expression, source_text) } Expression::TSNonNullExpression(ts_non_null) => { - convert_oxc_expression(allocator, &ts_non_null.expression) + convert_oxc_expression(allocator, &ts_non_null.expression, source_text) } Expression::TSInstantiationExpression(ts_inst) => { - convert_oxc_expression(allocator, &ts_inst.expression) + convert_oxc_expression(allocator, &ts_inst.expression, source_text) } // Unsupported expressions - return None @@ -239,6 +254,7 @@ pub fn convert_oxc_expression<'a>( fn convert_array_expression<'a>( allocator: &'a Allocator, arr: &oxc_ast::ast::ArrayExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { let mut entries = OxcVec::with_capacity_in(arr.elements.len(), allocator); @@ -246,7 +262,7 @@ fn convert_array_expression<'a>( match element { ArrayExpressionElement::SpreadElement(spread) => { // Convert the inner expression and wrap it in SpreadElement - let inner_expr = convert_oxc_expression(allocator, &spread.argument)?; + let inner_expr = convert_oxc_expression(allocator, &spread.argument, source_text)?; let spread_expr = OutputExpression::SpreadElement(Box::new_in( SpreadElementExpr { expr: Box::new_in(inner_expr, allocator), @@ -266,7 +282,7 @@ fn convert_array_expression<'a>( _ => { // Regular expression element let expr_ref = element.to_expression(); - let converted = convert_oxc_expression(allocator, expr_ref)?; + let converted = convert_oxc_expression(allocator, expr_ref, source_text)?; entries.push(converted); } } @@ -282,6 +298,7 @@ fn convert_array_expression<'a>( fn convert_object_expression<'a>( allocator: &'a Allocator, obj: &oxc_ast::ast::ObjectExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { let mut entries = OxcVec::with_capacity_in(obj.properties.len(), allocator); @@ -304,7 +321,7 @@ fn convert_object_expression<'a>( }; // Convert the value - let value = convert_oxc_expression(allocator, &p.value)?; + let value = convert_oxc_expression(allocator, &p.value, source_text)?; entries.push(LiteralMapEntry { key, value, quoted }); } @@ -326,8 +343,9 @@ fn convert_object_expression<'a>( fn convert_call_expression<'a>( allocator: &'a Allocator, call: &oxc_ast::ast::CallExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { - convert_call_expression_with_optional(allocator, call, call.optional) + convert_call_expression_with_optional(allocator, call, call.optional, source_text) } /// Convert an OXC call expression to an OutputExpression with explicit optional flag. @@ -335,9 +353,10 @@ fn convert_call_expression_with_optional<'a>( allocator: &'a Allocator, call: &oxc_ast::ast::CallExpression<'a>, optional: bool, + source_text: Option<&'a str>, ) -> Option> { // Convert the callee - let fn_expr = convert_oxc_expression(allocator, &call.callee)?; + let fn_expr = convert_oxc_expression(allocator, &call.callee, source_text)?; // Convert arguments let mut args = OxcVec::with_capacity_in(call.arguments.len(), allocator); @@ -345,12 +364,12 @@ fn convert_call_expression_with_optional<'a>( match arg { Argument::SpreadElement(spread) => { // Handle spread arguments - let expr = convert_oxc_expression(allocator, &spread.argument)?; + let expr = convert_oxc_expression(allocator, &spread.argument, source_text)?; args.push(expr); } _ => { let expr = arg.to_expression(); - let converted = convert_oxc_expression(allocator, expr)?; + let converted = convert_oxc_expression(allocator, expr, source_text)?; args.push(converted); } } @@ -372,21 +391,22 @@ fn convert_call_expression_with_optional<'a>( fn convert_new_expression<'a>( allocator: &'a Allocator, new_expr: &oxc_ast::ast::NewExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { // Convert the callee (class expression) - let class_expr = convert_oxc_expression(allocator, &new_expr.callee)?; + let class_expr = convert_oxc_expression(allocator, &new_expr.callee, source_text)?; // Convert arguments let mut args = OxcVec::with_capacity_in(new_expr.arguments.len(), allocator); for arg in &new_expr.arguments { match arg { Argument::SpreadElement(spread) => { - let expr = convert_oxc_expression(allocator, &spread.argument)?; + let expr = convert_oxc_expression(allocator, &spread.argument, source_text)?; args.push(expr); } _ => { let expr = arg.to_expression(); - let converted = convert_oxc_expression(allocator, expr)?; + let converted = convert_oxc_expression(allocator, expr, source_text)?; args.push(converted); } } @@ -402,6 +422,7 @@ fn convert_new_expression<'a>( fn convert_arrow_function_expression<'a>( allocator: &'a Allocator, arrow: &oxc_ast::ast::ArrowFunctionExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { // Convert parameters let mut params = OxcVec::with_capacity_in(arrow.params.items.len(), allocator); @@ -410,8 +431,8 @@ fn convert_arrow_function_expression<'a>( if let BindingPattern::BindingIdentifier(id) = ¶m.pattern { params.push(FnParam { name: id.name.clone().into() }); } else { - // Complex patterns not supported - return None; + // Complex patterns not supported — fall back to raw source if available + return make_raw_source(allocator, source_text, arrow.span); } } @@ -420,17 +441,29 @@ fn convert_arrow_function_expression<'a>( // Expression body: () => expr let expr_body = arrow.body.statements.first()?; if let oxc_ast::ast::Statement::ExpressionStatement(expr_stmt) = expr_body { - let converted = convert_oxc_expression(allocator, &expr_stmt.expression)?; - ArrowFunctionBody::Expression(Box::new_in(converted, allocator)) + match convert_oxc_expression(allocator, &expr_stmt.expression, source_text) { + Some(converted) => ArrowFunctionBody::Expression(Box::new_in(converted, allocator)), + None => { + // Unsupported expression (e.g., await, yield, class) — + // fall back to raw source for the entire arrow + return make_raw_source(allocator, source_text, arrow.span); + } + } } else { return None; } } else { // Block body: () => { ... } + // If any statement cannot be converted, fall back to raw source to avoid + // silently dropping statements (which corrupts the function body). let mut statements = OxcVec::with_capacity_in(arrow.body.statements.len(), allocator); for stmt in &arrow.body.statements { - if let Some(output_stmt) = convert_statement(allocator, stmt) { - statements.push(output_stmt); + match convert_statement(allocator, stmt, source_text) { + Some(output_stmt) => statements.push(output_stmt), + None => { + // Unsupported statement type — fall back to raw source + return make_raw_source(allocator, source_text, arrow.span); + } } } ArrowFunctionBody::Statements(statements) @@ -443,9 +476,14 @@ fn convert_arrow_function_expression<'a>( } /// Convert an OXC statement to an OutputStatement (limited support). +/// +/// Returns `None` for unsupported statement types (e.g., variable declarations, +/// if/else, for loops, try/catch, etc.). The caller should fall back to raw +/// source preservation when this returns `None`. fn convert_statement<'a>( allocator: &'a Allocator, stmt: &oxc_ast::ast::Statement<'a>, + source_text: Option<&'a str>, ) -> Option> { match stmt { oxc_ast::ast::Statement::ReturnStatement(ret) => { @@ -453,7 +491,7 @@ fn convert_statement<'a>( let value = ret .argument .as_ref() - .and_then(|expr| convert_oxc_expression(allocator, expr)) + .and_then(|expr| convert_oxc_expression(allocator, expr, source_text)) .unwrap_or_else(|| { OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Undefined, source_span: None }, @@ -466,13 +504,13 @@ fn convert_statement<'a>( ))) } oxc_ast::ast::Statement::ExpressionStatement(expr_stmt) => { - let expr = convert_oxc_expression(allocator, &expr_stmt.expression)?; + let expr = convert_oxc_expression(allocator, &expr_stmt.expression, source_text)?; Some(OutputStatement::Expression(Box::new_in( super::ast::ExpressionStatement { expr, source_span: None }, allocator, ))) } - // Other statement types not supported for now + // Other statement types not supported — caller should fall back to raw source _ => None, } } @@ -481,8 +519,9 @@ fn convert_statement<'a>( fn convert_unary_expression<'a>( allocator: &'a Allocator, unary: &oxc_ast::ast::UnaryExpression<'a>, + source_text: Option<&'a str>, ) -> Option> { - let expr = convert_oxc_expression(allocator, &unary.argument)?; + let expr = convert_oxc_expression(allocator, &unary.argument, source_text)?; match unary.operator { OxcUnaryOperator::LogicalNot => Some(OutputExpression::Not(Box::new_in( @@ -524,6 +563,7 @@ fn convert_unary_expression<'a>( fn convert_template_literal<'a>( allocator: &'a Allocator, tpl: &oxc_ast::ast::TemplateLiteral<'a>, + source_text: Option<&'a str>, ) -> Option> { // Convert quasis to template literal elements let mut elements = OxcVec::with_capacity_in(tpl.quasis.len(), allocator); @@ -545,7 +585,7 @@ fn convert_template_literal<'a>( // Convert expressions let mut expressions = OxcVec::with_capacity_in(tpl.expressions.len(), allocator); for expr in &tpl.expressions { - expressions.push(convert_oxc_expression(allocator, expr)?); + expressions.push(convert_oxc_expression(allocator, expr, source_text)?); } Some(OutputExpression::TemplateLiteral(Box::new_in( @@ -561,21 +601,22 @@ fn convert_template_literal<'a>( fn convert_chain_element<'a>( allocator: &'a Allocator, element: &oxc_ast::ast::ChainElement<'a>, + source_text: Option<&'a str>, ) -> Option> { use oxc_ast::ast::ChainElement; match element { ChainElement::CallExpression(call) => { // For call expressions within a chain, the optional flag is already set on the call - convert_call_expression_with_optional(allocator, call, call.optional) + convert_call_expression_with_optional(allocator, call, call.optional, source_text) } ChainElement::TSNonNullExpression(ts_non_null) => { // TypeScript non-null assertion (!) - just convert the inner expression - convert_oxc_expression(allocator, &ts_non_null.expression) + convert_oxc_expression(allocator, &ts_non_null.expression, source_text) } ChainElement::ComputedMemberExpression(member) => { - let receiver = convert_oxc_expression(allocator, &member.object)?; - let index = convert_oxc_expression(allocator, &member.expression)?; + let receiver = convert_oxc_expression(allocator, &member.object, source_text)?; + let index = convert_oxc_expression(allocator, &member.expression, source_text)?; Some(OutputExpression::ReadKey(Box::new_in( ReadKeyExpr { receiver: Box::new_in(receiver, allocator), @@ -587,7 +628,7 @@ fn convert_chain_element<'a>( ))) } ChainElement::StaticMemberExpression(member) => { - let receiver = convert_oxc_expression(allocator, &member.object)?; + let receiver = convert_oxc_expression(allocator, &member.object, source_text)?; Some(OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in(receiver, allocator), @@ -634,6 +675,88 @@ fn convert_oxc_binary_operator(op: oxc_ast::ast::BinaryOperator) -> Option( + allocator: &'a Allocator, + source_text: Option<&'a str>, + span: Span, +) -> Option> { + let source = source_text?; + let raw = &source[span.start as usize..span.end as usize]; + let js = strip_expression_types(raw); + Some(OutputExpression::RawSource(Box::new_in( + RawSourceExpr { source: Ident::from(allocator.alloc_str(&js)), source_span: None }, + allocator, + ))) +} + +/// Strip TypeScript type annotations from an expression source string. +/// +/// Fast path: tries parsing as ESM JavaScript first. If the expression is +/// already valid JS (no type annotations), returns it as-is without running +/// the heavier semantic/transform/codegen pipeline. +/// +/// Slow path: if JS parsing fails (likely due to TS syntax), wraps the +/// expression, parses as TypeScript module, strips types via transformer, +/// and codegens to JavaScript. +fn strip_expression_types(expr_source: &str) -> String { + use std::path::Path; + + // Fast path: try parsing as JS module. If it succeeds, the expression + // is already valid JavaScript — return as-is without transformation. + { + let allocator = oxc_allocator::Allocator::default(); + let wrapped = format!("0,({expr_source})"); + let source_type = oxc_span::SourceType::mjs(); + let parser_ret = oxc_parser::Parser::new(&allocator, &wrapped, source_type).parse(); + if !parser_ret.panicked && parser_ret.errors.is_empty() { + return expr_source.to_string(); + } + } + + // Slow path: expression contains TypeScript syntax — run full pipeline. + let allocator = oxc_allocator::Allocator::default(); + let wrapped = format!("0,({expr_source})"); + // Use module TypeScript so import.meta and ESM syntax are valid. + let source_type = oxc_span::SourceType::ts().with_module(true); + let parser_ret = oxc_parser::Parser::new(&allocator, &wrapped, source_type).parse(); + + if parser_ret.panicked { + return expr_source.to_string(); + } + + let mut program = parser_ret.program; + let semantic_ret = + oxc_semantic::SemanticBuilder::new().with_excess_capacity(2.0).build(&program); + + let transform_options = oxc_transformer::TransformOptions { + typescript: oxc_transformer::TypeScriptOptions::default(), + ..Default::default() + }; + let transformer = + oxc_transformer::Transformer::new(&allocator, Path::new("_.mts"), &transform_options); + transformer.build_with_scoping(semantic_ret.semantic.into_scoping(), &mut program); + + let codegen_ret = oxc_codegen::Codegen::new().with_source_text(&wrapped).build(&program); + + // Strip the wrapper: codegen produces "0, (expr);\n" → extract "expr" + let code = codegen_ret.code.trim_end(); + if let Some(rest) = code.strip_prefix("0, (").or_else(|| code.strip_prefix("0,(")) { + if let Some(inner) = rest.strip_suffix(");") { + return inner.to_string(); + } + } + + // Fallback: return original + expr_source.to_string() +} + // ============================================================================ // Tests // ============================================================================ @@ -655,7 +778,7 @@ mod tests { fn test_convert_string_literal() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, r#""hello""#); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::Literal(lit)) = result { assert!(matches!(lit.value, LiteralValue::String(_))); @@ -668,7 +791,7 @@ mod tests { fn test_convert_number_literal() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "42"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::Literal(lit)) = result { if let LiteralValue::Number(n) = lit.value { @@ -685,7 +808,7 @@ mod tests { fn test_convert_identifier() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "myVar"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::ReadVar(var)) = result { assert_eq!(var.name.as_str(), "myVar"); @@ -698,7 +821,7 @@ mod tests { fn test_convert_array_expression() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "[1, 2, 3]"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::LiteralArray(arr)) = result { assert_eq!(arr.entries.len(), 3); @@ -711,7 +834,7 @@ mod tests { fn test_convert_object_expression() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "{ a: 1, b: 2 }"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::LiteralMap(map)) = result { assert_eq!(map.entries.len(), 2); @@ -724,7 +847,7 @@ mod tests { fn test_convert_call_expression() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "foo(1, 2)"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::InvokeFunction(call)) = result { assert_eq!(call.args.len(), 2); @@ -737,7 +860,7 @@ mod tests { fn test_convert_member_expression() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "obj.prop"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::ReadProp(prop)) = result { assert_eq!(prop.name.as_str(), "prop"); @@ -750,7 +873,7 @@ mod tests { fn test_convert_spread_in_array() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "[...arr, 1, 2]"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::LiteralArray(arr)) = result { assert_eq!(arr.entries.len(), 3); @@ -775,7 +898,7 @@ mod tests { fn test_convert_multiple_spreads_in_array() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "[...a, ...b, c]"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::LiteralArray(arr)) = result { assert_eq!(arr.entries.len(), 3); @@ -793,7 +916,7 @@ mod tests { fn test_convert_optional_chaining_property() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "obj?.prop"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::ReadProp(prop)) = result { assert_eq!(prop.name.as_str(), "prop"); @@ -807,7 +930,7 @@ mod tests { fn test_convert_optional_chaining_computed() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "obj?.[key]"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::ReadKey(key)) = result { assert!(key.optional, "Expected optional to be true for ?.["); @@ -820,7 +943,7 @@ mod tests { fn test_convert_optional_chaining_call() { let allocator = Allocator::default(); let expr = parse_expression(&allocator, "fn?.()"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some()); if let Some(OutputExpression::InvokeFunction(call)) = result { assert!(call.optional, "Expected optional to be true for ?.()"); @@ -834,7 +957,7 @@ mod tests { // Test a longer optional chain like: val?.trim().toLowerCase() let allocator = Allocator::default(); let expr = parse_expression(&allocator, "val?.trim().toLowerCase()"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some(), "Failed to convert optional chain expression"); // The expression structure is: @@ -855,7 +978,147 @@ mod tests { let source_type = SourceType::ts(); let parser = Parser::new(&allocator, "(val) => val?.trim().toLowerCase()", source_type); let expr = parser.parse_expression().expect("Failed to parse expression"); - let result = convert_oxc_expression(&allocator, &expr); + let result = convert_oxc_expression(&allocator, &expr, None); assert!(result.is_some(), "Failed to convert arrow function with optional chain"); } + + #[test] + fn test_block_body_arrow_with_unsupported_stmts_without_source_text() { + // Without source_text, a block-body arrow with unsupported statements returns None + let allocator = Allocator::default(); + let source = "() => { const x = 1; return x; }"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, None); + // Without source_text, conversion fails (returns None) because + // the const declaration cannot be represented + assert!(result.is_none(), "Should return None without source_text for unsupported stmts"); + } + + #[test] + fn test_block_body_arrow_with_unsupported_stmts_with_source_text() { + // With source_text, a block-body arrow with unsupported statements falls back to RawSource + let allocator = Allocator::default(); + let source = "() => { const x = 1; return x; }"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, Some(source)); + assert!(result.is_some(), "Should succeed with source_text"); + if let Some(OutputExpression::RawSource(raw)) = result { + let raw_str = raw.source.as_str(); + assert!( + raw_str.contains("const x = 1") && raw_str.contains("return x"), + "Should preserve the arrow function body. Got: {raw_str}" + ); + } else { + panic!("Expected RawSource expression, got {result:?}"); + } + } + + #[test] + fn test_block_body_arrow_with_if_statement() { + // The exact case from the issue: useFactory with if/const + let allocator = Allocator::default(); + let source = "() => { const config = inject(AppConfig); if (config.useMock) { return new MockService(); } return new RealService(config); }"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, Some(source)); + assert!(result.is_some(), "Should succeed with source_text for complex arrow"); + assert!( + matches!(result, Some(OutputExpression::RawSource(_))), + "Should be RawSource for arrow with if statement" + ); + } + + #[test] + fn test_block_body_arrow_with_only_return_still_converts() { + // A block-body arrow with only return and expression statements should still + // convert normally (not fall back to RawSource) + let allocator = Allocator::default(); + let source = "() => { return 42; }"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, Some(source)); + assert!(result.is_some()); + // Should NOT be RawSource since return statements are supported + assert!( + matches!(result, Some(OutputExpression::ArrowFunction(_))), + "Should be ArrowFunction, not RawSource, for simple block body" + ); + } + + #[test] + fn test_function_expression_falls_back_to_raw_source() { + // Function expressions should fall back to RawSource + let allocator = Allocator::default(); + let source = "function() { return 42; }"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, Some(source)); + assert!(result.is_some(), "Should succeed with source_text for function expression"); + if let Some(OutputExpression::RawSource(raw)) = result { + let raw_str = raw.source.as_str(); + assert!( + raw_str.contains("function()") && raw_str.contains("return 42"), + "Should preserve function expression. Got: {raw_str}" + ); + } else { + panic!("Expected RawSource expression for function expression"); + } + } + + #[test] + fn test_function_expression_without_source_text_returns_none() { + // Function expressions without source_text return None + let allocator = Allocator::default(); + let source = "function() { return 42; }"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, None); + assert!(result.is_none(), "Should return None without source_text"); + } + + #[test] + fn test_expression_body_arrow_unaffected_by_source_text() { + // Expression-body arrows should still work normally + let allocator = Allocator::default(); + let source = "() => 42"; + let expr = parse_expression(&allocator, source); + let result = convert_oxc_expression(&allocator, &expr, Some(source)); + assert!(result.is_some()); + assert!( + matches!(result, Some(OutputExpression::ArrowFunction(_))), + "Expression-body arrow should still be ArrowFunction" + ); + } + + #[test] + fn test_raw_source_strips_typescript_types() { + // RawSource should strip TypeScript type annotations to produce valid JS + let allocator = Allocator::default(); + // Parse as TypeScript (not mjs) to include type annotations + let source_type = SourceType::ts(); + let source = + "(dep: SomeType) => { const x: MyType = dep.getValue(); return new Service(x); }"; + let parser = Parser::new(&allocator, source, source_type); + let expr = parser.parse_expression().expect("Failed to parse expression"); + let result = convert_oxc_expression(&allocator, &expr, Some(source)); + assert!(result.is_some(), "Should succeed with source_text for typed arrow"); + if let Some(OutputExpression::RawSource(raw)) = result { + let raw_str = raw.source.as_str(); + // Type annotations should be stripped + assert!( + !raw_str.contains(": SomeType") && !raw_str.contains(": MyType"), + "TypeScript type annotations should be stripped. Got: {raw_str}" + ); + // But the actual code should be preserved + assert!( + raw_str.contains("dep.getValue()") && raw_str.contains("return new Service(x)"), + "JavaScript code should be preserved. Got: {raw_str}" + ); + } else { + panic!("Expected RawSource expression, got {result:?}"); + } + } + + #[test] + fn test_strip_expression_types_basic() { + let result = strip_expression_types("(x: number) => x + 1"); + assert!(!result.contains(": number"), "Should strip type annotation. Got: {result}"); + assert!(result.contains("=> x + 1"), "Should preserve expression. Got: {result}"); + } } diff --git a/crates/oxc_angular_compiler/src/pipe/decorator.rs b/crates/oxc_angular_compiler/src/pipe/decorator.rs index 43340a70f..c09ad5a62 100644 --- a/crates/oxc_angular_compiler/src/pipe/decorator.rs +++ b/crates/oxc_angular_compiler/src/pipe/decorator.rs @@ -107,6 +107,7 @@ pub fn extract_pipe_metadata<'a>( allocator: &'a Allocator, class: &'a Class<'a>, implicit_standalone: bool, + _source_text: Option<&'a str>, ) -> Option> { // Get the class name let class_name: Ident<'a> = class.id.as_ref()?.name.clone().into(); @@ -395,7 +396,7 @@ mod tests { if let Some(class) = class { if let Some(metadata) = - extract_pipe_metadata(&allocator, class, implicit_standalone) + extract_pipe_metadata(&allocator, class, implicit_standalone, Some(code)) { found_metadata = Some(metadata); break; diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/allocate_slots.rs b/crates/oxc_angular_compiler/src/pipeline/phases/allocate_slots.rs index 4a1fd74f4..c699af889 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/allocate_slots.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/allocate_slots.rs @@ -768,7 +768,8 @@ fn propagate_slots_to_expressions( | OutputExpression::External(_) | OutputExpression::ReadVar(_) | OutputExpression::RegularExpressionLiteral(_) - | OutputExpression::WrappedNode(_) => {} + | OutputExpression::WrappedNode(_) + | OutputExpression::RawSource(_) => {} } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs b/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs index e269a32a5..3fee3589f 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/chaining.rs @@ -816,6 +816,10 @@ fn clone_expression<'a>( }, allocator, )), + OutputExpression::RawSource(raw) => OutputExpression::RawSource(Box::new_in( + RawSourceExpr { source: raw.source.clone(), source_span: raw.source_span }, + allocator, + )), } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs b/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs index 98b5d353c..e67b612ff 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/generate_advance.rs @@ -292,7 +292,8 @@ fn get_slot_dependency_from_output_expr(expr: &OutputExpression<'_>) -> Option None, + | OutputExpression::LocalizedString(_) + | OutputExpression::RawSource(_) => None, } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs b/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs index ad97e1679..f577386e1 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/var_counting.rs @@ -841,7 +841,8 @@ fn assign_var_offsets_in_output_expression( | OutputExpression::External(_) | OutputExpression::ReadVar(_) | OutputExpression::RegularExpressionLiteral(_) - | OutputExpression::WrappedNode(_) => {} + | OutputExpression::WrappedNode(_) + | OutputExpression::RawSource(_) => {} } } diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs index 50848226b..0b37468cd 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs @@ -3095,7 +3095,8 @@ fn transform_expressions_in_output_expression<'a, F>( | OutputExpression::LocalizedString(_) | OutputExpression::WrappedNode(_) | OutputExpression::DynamicImport(_) - | OutputExpression::RegularExpressionLiteral(_) => {} + | OutputExpression::RegularExpressionLiteral(_) + | OutputExpression::RawSource(_) => {} } } diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 79960280c..f618a3246 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -7879,3 +7879,128 @@ export class MyComponent { insta::assert_snapshot!("host_directives_with_host_aliases", result.code); } + +// ============================================================================= +// Issue #203: useFactory with block-body functions silently dropped in providers +// ============================================================================= + +#[test] +fn test_use_factory_block_body_arrow_preserved() { + let allocator = Allocator::default(); + let source = r#" +import { Component, inject } from '@angular/core'; + +const MY_TOKEN = 'MY_TOKEN'; + +@Component({ + selector: 'my-component', + template: '
hello
', + providers: [ + { + provide: MY_TOKEN, + useFactory: () => { + const config = inject(AppConfig); + if (config.useMock) { + return new MockService(); + } + return new RealService(config); + } + } + ] +}) +export class MyComponent {} +"#; + + let result = transform_angular_file(&allocator, "test.component.ts", source, None, None); + + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + assert_eq!(result.component_count, 1); + + // The key assertion: the block-body arrow function should be preserved intact. + // Before the fix, `const config = inject(AppConfig)` and `if (config.useMock) { ... }` + // were silently dropped, leaving only `return new RealService(config)`. + assert!( + result.code.contains("const config = inject(AppConfig)"), + "Block-body arrow: const declaration should be preserved. Got:\n{}", + result.code + ); + assert!( + result.code.contains("if (config.useMock)"), + "Block-body arrow: if statement should be preserved. Got:\n{}", + result.code + ); + assert!( + result.code.contains("return new MockService()"), + "Block-body arrow: return inside if should be preserved. Got:\n{}", + result.code + ); + assert!( + result.code.contains("return new RealService(config)"), + "Block-body arrow: final return should be preserved. Got:\n{}", + result.code + ); +} + +#[test] +fn test_use_factory_expression_body_arrow_still_works() { + // Verify that expression-body arrows (which already worked) are not regressed + let allocator = Allocator::default(); + let source = r#" +import { Component, inject } from '@angular/core'; + +const MY_TOKEN = 'MY_TOKEN'; + +@Component({ + selector: 'my-component', + template: '
hello
', + providers: [ + { + provide: MY_TOKEN, + useFactory: () => new RealService(inject(AppConfig)) + } + ] +}) +export class MyComponent {} +"#; + + let result = transform_angular_file(&allocator, "test.component.ts", source, None, None); + + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + assert!( + result.code.contains("new RealService(inject(AppConfig))"), + "Expression-body arrow should be preserved. Got:\n{}", + result.code + ); +} + +#[test] +fn test_providers_with_function_expression_preserved() { + // function() expressions should also be preserved + let allocator = Allocator::default(); + let source = r#" +import { Component, inject } from '@angular/core'; + +const MY_TOKEN = 'MY_TOKEN'; + +@Component({ + selector: 'my-component', + template: '
hello
', + providers: [ + { + provide: MY_TOKEN, + useFactory: function() { return new RealService(); } + } + ] +}) +export class MyComponent {} +"#; + + let result = transform_angular_file(&allocator, "test.component.ts", source, None, None); + + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + assert!( + result.code.contains("function()"), + "function expression should be preserved. Got:\n{}", + result.code + ); +} diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index 7efc41667..231a1a99c 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -769,7 +769,8 @@ pub fn extract_component_urls_sync(source: String, filename: String) -> Componen if let Some(class) = class { // Extract metadata from @Component decorator // Use implicit_standalone=true (v19+ default) since it doesn't affect URL extraction - if let Some(metadata) = extract_component_metadata(&allocator, class, true, &import_map) + if let Some(metadata) = + extract_component_metadata(&allocator, class, true, &import_map, Some(&source)) { // Collect template URL if let Some(template_url) = &metadata.template_url { @@ -1225,7 +1226,9 @@ pub fn extract_pipe_metadata_sync( } // Extract metadata from @Pipe decorator - if let Some(metadata) = extract_pipe_metadata(&allocator, class, implicit_standalone) { + if let Some(metadata) = + extract_pipe_metadata(&allocator, class, implicit_standalone, Some(&source)) + { return Some(ExtractedPipeMetadata { class_name: metadata.class_name.to_string(), span_start: metadata.class_span.start, @@ -1300,7 +1303,9 @@ pub fn compile_pipe_sync( } // Extract metadata from @Pipe decorator - if let Some(metadata) = extract_pipe_metadata(&allocator, class, implicit_standalone) { + if let Some(metadata) = + extract_pipe_metadata(&allocator, class, implicit_standalone, Some(&source)) + { // Create type expression for the pipe class use oxc_allocator::Box; use oxc_angular_compiler::output::ast::{OutputExpression, ReadVarExpr}; @@ -1522,9 +1527,13 @@ pub fn extract_component_metadata_sync( if let Some(class) = class { // Extract metadata from @Component decorator - if let Some(metadata) = - extract_component_metadata(&allocator, class, implicit_standalone, &import_map) - { + if let Some(metadata) = extract_component_metadata( + &allocator, + class, + implicit_standalone, + &import_map, + Some(&source), + ) { // Convert encapsulation to string let encapsulation = match metadata.encapsulation { RustViewEncapsulation::Emulated => "Emulated", @@ -1588,7 +1597,7 @@ pub fn extract_component_metadata_sync( 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); + let rust_inputs = extract_input_metadata(&allocator, class, Some(&source)); let inputs: Option> = if rust_inputs.is_empty() { None } else { @@ -1642,7 +1651,7 @@ pub fn extract_component_metadata_sync( } // Extract view queries from @ViewChild/@ViewChildren decorators - let rust_view_queries = extract_view_queries(&allocator, class); + let rust_view_queries = extract_view_queries(&allocator, class, Some(&source)); let view_queries: Option> = if rust_view_queries.is_empty() { None @@ -1663,7 +1672,8 @@ pub fn extract_component_metadata_sync( }; // Extract content queries from @ContentChild/@ContentChildren decorators - let rust_content_queries = extract_content_queries(&allocator, class); + let rust_content_queries = + extract_content_queries(&allocator, class, Some(&source)); let queries: Option> = if rust_content_queries.is_empty() { None @@ -1965,7 +1975,8 @@ pub fn compile_class_metadata_sync( // Build decorators array: [{ type: DecoratorClass, args: [...] }] let decorator_ref = decorator; - let decorators_expr = core_build_decorator_metadata_array(&allocator, &[decorator_ref]); + let decorators_expr = + core_build_decorator_metadata_array(&allocator, &[decorator_ref], Some(&source)); // Build constructor parameters metadata // This standalone API doesn't have full transform pipeline context (constructor deps @@ -1979,10 +1990,12 @@ pub fn compile_class_metadata_sync( None, &mut namespace_registry, &empty_import_map, + Some(&source), ); // Build property decorators metadata - let prop_decorators_expr = core_build_prop_decorators_metadata(&allocator, class); + let prop_decorators_expr = + core_build_prop_decorators_metadata(&allocator, class, Some(&source)); // Create R3ClassMetadata let metadata = R3ClassMetadata {