From fa2c7030e1c3287e4b6a7c5f5f9c31f281f31b82 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 2 Apr 2026 15:33:42 +0800 Subject: [PATCH 1/5] fix(angular): preserve block-body functions in decorator providers Block-body arrow functions and function expressions in decorator properties (e.g., useFactory) were silently having unsupported statements dropped. Only return and expression statements survived, corrupting the function body and causing runtime errors. Add RawSource fallback: when convert_oxc_expression encounters a block-body arrow with unsupported statement types (const, if, for, try/catch, etc.) or a function expression, it preserves the complete source text verbatim via span slicing instead of silently dropping statements. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/class_metadata/builders.rs | 4 +- .../src/component/decorator.rs | 23 +- .../src/component/transform.rs | 30 +- .../src/directive/decorator.rs | 16 +- .../src/directive/property_decorators.rs | 10 +- .../src/injectable/decorator.rs | 58 ++-- .../src/injectable/definition.rs | 6 +- .../oxc_angular_compiler/src/ir/expression.rs | 3 +- .../src/ng_module/decorator.rs | 9 +- .../src/ng_module/definition.rs | 4 +- crates/oxc_angular_compiler/src/output/ast.rs | 32 ++ .../src/output/emitter.rs | 4 + .../src/output/oxc_converter.rs | 287 +++++++++++++----- .../src/pipe/decorator.rs | 3 +- .../src/pipeline/phases/allocate_slots.rs | 3 +- .../src/pipeline/phases/chaining.rs | 4 + .../src/pipeline/phases/generate_advance.rs | 3 +- .../src/pipeline/phases/var_counting.rs | 3 +- .../pipeline/phases/variable_optimization.rs | 3 +- .../tests/integration_test.rs | 125 ++++++++ napi/angular-compiler/src/lib.rs | 21 +- 21 files changed, 511 insertions(+), 140 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index 2b8566e21..149378a64 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -38,7 +38,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, None).map(|receiver| { OutputExpression::ReadProp(Box::new_in( ReadPropExpr { receiver: Box::new_in(receiver, allocator), @@ -77,7 +77,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, None) { args.push(converted); } } diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index deca15ac6..bb603d96b 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 @@ -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..117e2ee03 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); @@ -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); @@ -1848,7 +1853,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 +1911,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 +1945,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 +1986,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 +2024,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 +2068,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 +2116,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..4de2daf68 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); } } @@ -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/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index cf8c0d61e..290c9e703 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -183,7 +183,7 @@ 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, None); } _ => {} } @@ -779,7 +779,7 @@ 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, None) { config.predicate = Some(QueryPredicate::Type(output_expr)); } } @@ -799,7 +799,7 @@ 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, None); } "descendants" => { // Use the decorator-specific default if not explicitly set @@ -941,7 +941,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, None)?; QueryPredicate::Type(output_expr) } }; @@ -960,7 +960,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, None); } "descendants" => { descendants = extract_boolean_value(&prop.value).unwrap_or(descendants); diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index 4f478c17d..70b2e90f4 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), }); } @@ -254,19 +255,19 @@ pub fn extract_injectable_metadata<'a>( let provided_in = extract_provided_in(allocator, config_obj); // 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, @@ -329,7 +330,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, None)?; Some(ProvidedInValue::Module { expression, is_forward_ref }) } } @@ -338,14 +340,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 +360,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 +379,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 +396,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 +417,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 +430,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 +443,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 +459,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 +476,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 +520,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 +537,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 +548,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 +565,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 +677,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..2fdb9c0d6 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,23 @@ 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)?; + let converted = convert_oxc_expression(allocator, &expr_stmt.expression, source_text)?; ArrowFunctionBody::Expression(Box::new_in(converted, allocator)) } 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 +470,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 +485,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 +498,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 +513,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 +557,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 +579,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 +595,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 +622,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 +669,22 @@ 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]; + Some(OutputExpression::RawSource(Box::new_in( + RawSourceExpr { source: Ident::from(allocator.alloc_str(raw)), source_span: None }, + allocator, + ))) +} + // ============================================================================ // Tests // ============================================================================ @@ -655,7 +706,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 +719,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 +736,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 +749,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 +762,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 +775,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 +788,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 +801,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 +826,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 +844,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 +858,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 +871,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 +885,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 +906,107 @@ 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 { + assert_eq!( + raw.source.as_str(), + "() => { const x = 1; return x; }", + "Should preserve the full arrow function source" + ); + } 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 { + assert_eq!(raw.source.as_str(), "function() { return 42; }"); + } 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" + ); + } } 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..4044633f2 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", From f6eabfac5cce98590d0d8f70d143c60fef9caa6f Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 2 Apr 2026 16:21:16 +0800 Subject: [PATCH 2/5] fix(angular): pass source_text through build_decorator_metadata_array Thread source_text into class metadata builder so decorator arguments containing block-body arrows/function expressions are preserved via RawSource fallback instead of being silently dropped. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../oxc_angular_compiler/src/class_metadata/builders.rs | 9 +++++---- crates/oxc_angular_compiler/src/component/transform.rs | 1 + napi/angular-compiler/src/lib.rs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index 149378a64..db3b3d6ab 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, None).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, None) { + if let Some(converted) = convert_oxc_expression(allocator, expr, source_text) { args.push(converted); } } @@ -163,7 +164,7 @@ 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, None); map_entries.push(LiteralMapEntry { key: Ident::from("decorators"), value: decorators_array, @@ -251,7 +252,7 @@ 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, None); prop_entries.push(LiteralMapEntry { key: prop_name, diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 117e2ee03..7ab2ebc1b 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -1763,6 +1763,7 @@ pub fn transform_angular_file( decorators: build_decorator_metadata_array( allocator, &[decorator], + Some(source), ), ctor_parameters: build_ctor_params_metadata( allocator, diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index 4044633f2..d5db16413 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -1974,7 +1974,7 @@ 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 From 90ccab0a13112f2119d8c692033028b2a3f93e0e Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 2 Apr 2026 16:50:24 +0800 Subject: [PATCH 3/5] fix(angular): thread source_text through remaining metadata builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build_ctor_params_metadata: pass source_text so @Inject(...) args with complex expressions are preserved in ɵsetClassMetadata - build_prop_decorators_metadata: pass source_text so @Input({...}) with complex transform functions are preserved - extract_provided_in: propagate source_text from parent extract_injectable_metadata into forwardRef extraction Co-Authored-By: Claude Opus 4.6 (1M context) --- .../oxc_angular_compiler/src/class_metadata/builders.rs | 8 ++++++-- crates/oxc_angular_compiler/src/component/transform.rs | 5 ++++- crates/oxc_angular_compiler/src/injectable/decorator.rs | 8 +++++--- napi/angular-compiler/src/lib.rs | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index db3b3d6ab..15eaa94ac 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -123,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| { @@ -164,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, None); + let decorators_array = + build_decorator_metadata_array(allocator, ¶m_decorators, source_text); map_entries.push(LiteralMapEntry { key: Ident::from("decorators"), value: decorators_array, @@ -206,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", @@ -252,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, None); + 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/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 7ab2ebc1b..b2314b186 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -1771,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), ), }; diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index 70b2e90f4..8f2161b69 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -252,7 +252,7 @@ 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, source_text); @@ -303,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); } } } @@ -319,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() { @@ -331,7 +333,7 @@ fn parse_provided_in_value<'a>( _ => { // Check for forwardRef let (expression, is_forward_ref) = - extract_forward_ref_or_expression(allocator, expr, None)?; + extract_forward_ref_or_expression(allocator, expr, source_text)?; Some(ProvidedInValue::Module { expression, is_forward_ref }) } } diff --git a/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index d5db16413..6bd4dc39e 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -1974,7 +1974,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], Some(&source)); + 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 @@ -1988,10 +1989,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 { From d70418d6135966a564c4298f0dde804f8771a8d1 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 2 Apr 2026 17:25:32 +0800 Subject: [PATCH 4/5] fix(angular): strip TS types from RawSource, add expression-body fallback, thread source_text through property decorators Three reviewer fixes: 1. P1 - RawSource now strips TypeScript type annotations via parse-transform-codegen pipeline, preventing invalid JS output like `(dep: Dep) => { ... }` in generated code. 2. Medium - Expression-body arrows with unsupported inner expressions (e.g., `() => someUnsupportedExpr`) now fall back to RawSource instead of returning None. 3. P2 - Thread source_text through property_decorators.rs so @Input({transform}), @ViewChild, @ContentChild arguments with complex expressions are preserved via RawSource fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/component/decorator.rs | 2 +- .../src/component/transform.rs | 4 +- .../src/directive/decorator.rs | 2 +- .../src/directive/metadata.rs | 34 ++-- .../src/directive/property_decorators.rs | 190 ++++++++++-------- .../src/output/oxc_converter.rs | 114 ++++++++++- napi/angular-compiler/src/lib.rs | 7 +- 7 files changed, 246 insertions(+), 107 deletions(-) diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index bb603d96b..3ae725ff5 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -241,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); diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index b2314b186..bac6b68f4 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -1636,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) diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 4de2daf68..6883754a6 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -167,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 => ...) 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 290c9e703..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, None); + 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, None) { + 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, None); + 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, None)?; + 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, None); + 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/output/oxc_converter.rs b/crates/oxc_angular_compiler/src/output/oxc_converter.rs index 2fdb9c0d6..e5fbaf0df 100644 --- a/crates/oxc_angular_compiler/src/output/oxc_converter.rs +++ b/crates/oxc_angular_compiler/src/output/oxc_converter.rs @@ -441,8 +441,14 @@ 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, source_text)?; - 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; } @@ -671,6 +677,10 @@ fn convert_oxc_binary_operator(op: oxc_ast::ast::BinaryOperator) -> Option( allocator: &'a Allocator, @@ -679,12 +689,60 @@ fn make_raw_source<'a>( ) -> 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(raw)), source_span: None }, + RawSourceExpr { source: Ident::from(allocator.alloc_str(&js)), source_span: None }, allocator, ))) } +/// Strip TypeScript type annotations from an expression source string. +/// +/// Wraps the expression in a minimal program (`0,(expr)`), parses as TypeScript, +/// runs the TypeScript transformer to strip types, and codegens to JavaScript. +/// Returns the original source if stripping fails. +fn strip_expression_types(expr_source: &str) -> String { + use std::path::Path; + + let allocator = oxc_allocator::Allocator::default(); + // Use comma operator + parens to create a valid single expression statement. + // This handles expressions that would be ambiguous at statement level + // (e.g., arrow functions, object literals). + let wrapped = format!("0,({expr_source})"); + let source_type = oxc_span::SourceType::ts(); + 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("_.ts"), &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(); + // The codegen may format as "0, (expr);" or "0, (expr);\n" + 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 // ============================================================================ @@ -931,10 +989,10 @@ mod tests { 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 { - assert_eq!( - raw.source.as_str(), - "() => { const x = 1; return x; }", - "Should preserve the full arrow function source" + 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:?}"); @@ -980,7 +1038,11 @@ mod tests { 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 { - assert_eq!(raw.source.as_str(), "function() { return 42; }"); + 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"); } @@ -1009,4 +1071,40 @@ mod tests { "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/napi/angular-compiler/src/lib.rs b/napi/angular-compiler/src/lib.rs index 6bd4dc39e..231a1a99c 100644 --- a/napi/angular-compiler/src/lib.rs +++ b/napi/angular-compiler/src/lib.rs @@ -1597,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 { @@ -1651,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 @@ -1672,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 From 3b9955ebe471e7909a4be675feb398db9b0253e5 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 2 Apr 2026 18:37:14 +0800 Subject: [PATCH 5/5] fix(angular): use module source type and add fast path for TS stripping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use SourceType::ts().with_module(true) instead of script-mode ts() so import.meta and ESM-only syntax parse correctly in RawSource fallback expressions. - Add fast path: try parsing as .mjs first. If the expression is already valid JavaScript (no type annotations), return it as-is without running the heavier semantic→transform→codegen pipeline. Only expressions with actual TypeScript syntax pay the full cost. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/output/oxc_converter.rs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/oxc_angular_compiler/src/output/oxc_converter.rs b/crates/oxc_angular_compiler/src/output/oxc_converter.rs index e5fbaf0df..615929964 100644 --- a/crates/oxc_angular_compiler/src/output/oxc_converter.rs +++ b/crates/oxc_angular_compiler/src/output/oxc_converter.rs @@ -698,18 +698,33 @@ fn make_raw_source<'a>( /// Strip TypeScript type annotations from an expression source string. /// -/// Wraps the expression in a minimal program (`0,(expr)`), parses as TypeScript, -/// runs the TypeScript transformer to strip types, and codegens to JavaScript. -/// Returns the original source if stripping fails. +/// 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(); - // Use comma operator + parens to create a valid single expression statement. - // This handles expressions that would be ambiguous at statement level - // (e.g., arrow functions, object literals). let wrapped = format!("0,({expr_source})"); - let source_type = oxc_span::SourceType::ts(); + // 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 { @@ -725,14 +740,13 @@ fn strip_expression_types(expr_source: &str) -> String { ..Default::default() }; let transformer = - oxc_transformer::Transformer::new(&allocator, Path::new("_.ts"), &transform_options); + 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(); - // The codegen may format as "0, (expr);" or "0, (expr);\n" 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();