diff --git a/crates/oxc_angular_compiler/src/i18n/ast.rs b/crates/oxc_angular_compiler/src/i18n/ast.rs index b80a6c1c5..ed8336b06 100644 --- a/crates/oxc_angular_compiler/src/i18n/ast.rs +++ b/crates/oxc_angular_compiler/src/i18n/ast.rs @@ -575,9 +575,7 @@ impl Visitor for CloneVisitor { Node::IcuPlaceholder(IcuPlaceholder::new(icu, ph.name.clone(), ph.source_span.clone())) } else { // visit_icu should always return Node::Icu by design. - // This is a compiler bug if we reach here. - debug_assert!(false, "visit_icu should return Node::Icu"); - // Return the original placeholder unchanged as fallback + // Return the original placeholder unchanged as a safe fallback. Node::IcuPlaceholder(ph.clone()) } } diff --git a/crates/oxc_angular_compiler/src/output/ast.rs b/crates/oxc_angular_compiler/src/output/ast.rs index 4f025cde1..5395e54a0 100644 --- a/crates/oxc_angular_compiler/src/output/ast.rs +++ b/crates/oxc_angular_compiler/src/output/ast.rs @@ -1389,12 +1389,7 @@ impl<'a> OutputExpression<'a> { OutputExpression::WrappedIrNode(_) => { // WrappedIrNode expressions wrap IR expressions for deferred processing. // They should be resolved during the reify phase before any cloning occurs. - // If we hit this, it's a compiler bug. - debug_assert!( - false, - "Cannot clone a WrappedIrExpr. WrappedIrExpr should be resolved before cloning." - ); - // Return a placeholder undefined literal + // Return a placeholder undefined literal as a safe fallback. OutputExpression::Literal(Box::new_in( LiteralExpr { value: LiteralValue::Undefined, source_span: None }, allocator, diff --git a/crates/oxc_angular_compiler/src/output/emitter.rs b/crates/oxc_angular_compiler/src/output/emitter.rs index 7e06755dd..286bc6e22 100644 --- a/crates/oxc_angular_compiler/src/output/emitter.rs +++ b/crates/oxc_angular_compiler/src/output/emitter.rs @@ -113,7 +113,8 @@ impl EmitterContext { /// Lines is always non-empty: initialized with one element in `new()`, /// and `println()` only pushes (never pops below 1). fn current_line(&self) -> &EmittedLine { - debug_assert!(!self.lines.is_empty(), "lines should never be empty"); + // Invariant: lines is always non-empty (initialized with one element in new(), + // and println() only pushes, never pops below 1). &self.lines[self.lines.len() - 1] } @@ -123,7 +124,8 @@ impl EmitterContext { /// Lines is always non-empty: initialized with one element in `new()`, /// and `println()` only pushes (never pops below 1). fn current_line_mut(&mut self) -> &mut EmittedLine { - debug_assert!(!self.lines.is_empty(), "lines should never be empty"); + // Invariant: lines is always non-empty (initialized with one element in new(), + // and println() only pushes, never pops below 1). let len = self.lines.len(); &mut self.lines[len - 1] } diff --git a/crates/oxc_angular_compiler/src/pipeline/ingest.rs b/crates/oxc_angular_compiler/src/pipeline/ingest.rs index 819408ab1..81c848eab 100644 --- a/crates/oxc_angular_compiler/src/pipeline/ingest.rs +++ b/crates/oxc_angular_compiler/src/pipeline/ingest.rs @@ -2349,10 +2349,17 @@ fn ingest_if_block<'a>( } } - // Extract i18n placeholder metadata from the branch - // Angular checks that branch.i18n is a BlockPlaceholder type - let i18n_placeholder = - convert_i18n_meta_to_placeholder(branch.i18n, &mut job.diagnostics, branch.source_span); + // Extract i18n placeholder metadata from the branch. + // Angular throws for unexpected types; we return early to avoid emitting broken IR. + let i18n_placeholder = match convert_i18n_meta_to_placeholder( + branch.i18n, + &mut job.diagnostics, + branch.source_span, + "@if", + ) { + Ok(placeholder) => placeholder, + Err(()) => return, + }; // Infer tag name from single root element for content projection let tag_name = @@ -2564,14 +2571,19 @@ fn ingest_for_block<'a>( } _ => { // User-defined alias (e.g., 'let i = $index', 'let isFirst = $first') - // Create an alias with the appropriate expression - let expression = get_computed_for_loop_variable_expression( + // Create an alias with the appropriate expression. + // Angular throws for unknown variables; we return early to avoid + // emitting broken IR. + let expression = match get_computed_for_loop_variable_expression( allocator, var.value.as_str(), &index_name, &count_name, &mut job.diagnostics, - ); + ) { + Ok(expr) => expr, + Err(()) => return, + }; aliases.push(AliasVariable { identifier: var.name.clone(), expression }); // Track in var_names for track expression variable replacement @@ -2611,19 +2623,15 @@ fn ingest_for_block<'a>( // Extract i18n placeholder from @empty block if present. // Per Angular's ingest.ts lines 970-974, only BlockPlaceholder is valid for @empty. - let empty_i18n_placeholder = match empty.i18n { - Some(I18nMeta::BlockPlaceholder(ref placeholder)) => Some(I18nPlaceholder::new( - placeholder.start_name.clone(), - Some(placeholder.close_name.clone()), - )), - Some(_) => { - job.diagnostics.push( - OxcDiagnostic::error("Unhandled i18n metadata type for @empty") - .with_label(empty.source_span), - ); - None - } - None => None, + // Angular throws for unexpected types; we return early to avoid emitting broken IR. + let empty_i18n_placeholder = match convert_i18n_meta_to_placeholder( + empty.i18n, + &mut job.diagnostics, + empty.source_span, + "@empty", + ) { + Ok(placeholder) => placeholder, + Err(()) => return, }; for child in empty.children { @@ -2636,19 +2644,15 @@ fn ingest_for_block<'a>( // Extract i18n placeholder from @for block if present. // Per Angular's ingest.ts lines 967-969, only BlockPlaceholder is valid for @for. - let i18n_placeholder = match for_block.i18n { - Some(I18nMeta::BlockPlaceholder(ref placeholder)) => Some(I18nPlaceholder::new( - placeholder.start_name.clone(), - Some(placeholder.close_name.clone()), - )), - Some(_) => { - job.diagnostics.push( - OxcDiagnostic::error("Unhandled i18n metadata type for @for") - .with_label(for_block.source_span), - ); - None - } - None => None, + // Angular throws for unexpected types; we return early to avoid emitting broken IR. + let i18n_placeholder = match convert_i18n_meta_to_placeholder( + for_block.i18n, + &mut job.diagnostics, + for_block.source_span, + "@for", + ) { + Ok(placeholder) => placeholder, + Err(()) => return, }; // Convert the track expression from the for block. @@ -2700,39 +2704,41 @@ fn ingest_for_block<'a>( /// Creates a computed expression for @for loop variables. /// /// Ported from Angular's `getComputedForLoopVariableExpression` in `ingest.ts`. +/// Returns `Ok(expression)` for known loop variables, or `Err(())` for unknown +/// variables (matching Angular's throw behavior). A diagnostic is pushed on error. fn get_computed_for_loop_variable_expression<'a>( allocator: &'a Allocator, value: &str, index_name: &Atom<'a>, count_name: &Atom<'a>, diagnostics: &mut std::vec::Vec, -) -> IrExpression<'a> { +) -> Result, ()> { match value { "$index" => { // Return LexicalRead of the index variable - IrExpression::LexicalRead(Box::new_in( + Ok(IrExpression::LexicalRead(Box::new_in( LexicalReadExpr { name: index_name.clone(), source_span: None }, allocator, - )) + ))) } "$count" => { // Return LexicalRead of the count variable - IrExpression::LexicalRead(Box::new_in( + Ok(IrExpression::LexicalRead(Box::new_in( LexicalReadExpr { name: count_name.clone(), source_span: None }, allocator, - )) + ))) } "$first" => { // $index === 0 - create_binary_identical( + Ok(create_binary_identical( allocator, create_lexical_read(allocator, index_name), create_number_literal(allocator, 0.0), - ) + )) } "$last" => { // $index === $count - 1 - create_binary_identical( + Ok(create_binary_identical( allocator, create_lexical_read(allocator, index_name), create_binary_minus( @@ -2740,11 +2746,11 @@ fn get_computed_for_loop_variable_expression<'a>( create_lexical_read(allocator, count_name), create_number_literal(allocator, 1.0), ), - ) + )) } "$even" => { // $index % 2 === 0 - create_binary_identical( + Ok(create_binary_identical( allocator, create_binary_modulo( allocator, @@ -2752,11 +2758,11 @@ fn get_computed_for_loop_variable_expression<'a>( create_number_literal(allocator, 2.0), ), create_number_literal(allocator, 0.0), - ) + )) } "$odd" => { // $index % 2 !== 0 - create_binary_not_identical( + Ok(create_binary_not_identical( allocator, create_binary_modulo( allocator, @@ -2764,15 +2770,17 @@ fn get_computed_for_loop_variable_expression<'a>( create_number_literal(allocator, 2.0), ), create_number_literal(allocator, 0.0), - ) + )) } _ => { // Angular throws: "AssertionError: unknown @for loop variable ${variable.value}" // This should not happen if the parser correctly validates loop variables. + // We report a diagnostic and return Err to stop ingestion of this block, + // matching Angular's fail-fast behavior. diagnostics.push(OxcDiagnostic::error(format!( "AssertionError: unknown @for loop variable {value}" ))); - IrExpression::empty(allocator, None) + Err(()) } } } @@ -2937,10 +2945,17 @@ fn ingest_switch_block<'a>( // Allocate a new view for this group let group_view_xref = job.allocate_view(Some(view_xref)); - // Extract i18n placeholder metadata from the group - // Angular checks that group.i18n is a BlockPlaceholder type - let i18n_placeholder = - convert_i18n_meta_to_placeholder(group.i18n, &mut job.diagnostics, group.source_span); + // Extract i18n placeholder metadata from the group. + // Angular throws for unexpected types; we return early to avoid emitting broken IR. + let i18n_placeholder = match convert_i18n_meta_to_placeholder( + group.i18n, + &mut job.diagnostics, + group.source_span, + "@switch", + ) { + Ok(placeholder) => placeholder, + Err(()) => return, + }; // Infer tag name from single root element for content projection let tag_name = @@ -3090,11 +3105,16 @@ fn ingest_defer_view<'a>( // Convert i18n metadata to placeholder, matching Angular's ingestDeferView which passes // i18nMeta through to createTemplateOp. This enables propagate_i18n_blocks to wrap the // deferred template with i18nStart/i18nEnd when inside an i18n context. - let i18n_placeholder = convert_i18n_meta_to_placeholder( + // Angular throws for unexpected types; we return early to avoid emitting broken IR. + let i18n_placeholder = match convert_i18n_meta_to_placeholder( i18n, &mut job.diagnostics, source_span.unwrap_or(oxc_span::SPAN), - ); + "@defer", + ) { + Ok(placeholder) => placeholder, + Err(()) => return None, + }; let template_op = CreateOp::Template(TemplateOp { base: CreateOpBase { source_span, ..Default::default() }, @@ -4213,29 +4233,34 @@ fn ingest_host_event<'a>(job: &mut HostBindingCompilationJob<'a>, event: R3Bound /// is specifically a BlockPlaceholder type and extract its start_name/close_name. /// /// Ported from Angular's i18n handling in `ingest.ts` (lines 531-537, 1088-1094). +/// Returns `Ok(Some(placeholder))` for valid BlockPlaceholder metadata, +/// `Ok(None)` when no i18n metadata is present, or `Err(())` when an +/// unexpected metadata type is encountered (matching Angular's throw behavior). fn convert_i18n_meta_to_placeholder<'a>( i18n: Option>, diagnostics: &mut std::vec::Vec, source_span: oxc_span::Span, -) -> Option> { + block_name: &str, +) -> Result>, ()> { match i18n { Some(I18nMeta::Node(I18nNode::BlockPlaceholder(bp))) => { - Some(I18nPlaceholder::new(bp.start_name, Some(bp.close_name))) + Ok(Some(I18nPlaceholder::new(bp.start_name, Some(bp.close_name)))) } Some(I18nMeta::BlockPlaceholder(bp)) => { - Some(I18nPlaceholder::new(bp.start_name, Some(bp.close_name))) + Ok(Some(I18nPlaceholder::new(bp.start_name, Some(bp.close_name)))) } // Reference: ingest.ts lines 533-537, 587-591 // Angular throws an assertion error for unexpected i18n metadata types. - // We report a diagnostic instead to avoid crashing the process. + // We report a diagnostic and return Err to stop ingestion of this block, + // matching Angular's fail-fast behavior. Some(_) => { diagnostics.push( - OxcDiagnostic::error("Unhandled i18n metadata type for conditional block") + OxcDiagnostic::error(format!("Unhandled i18n metadata type for {block_name}")) .with_label(source_span), ); - None + Err(()) } - None => None, + None => Ok(None), } } @@ -4459,3 +4484,110 @@ impl<'a, 'b> RootNodeRef<'a, 'b> { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::r3::I18nMessage; + use oxc_allocator::Allocator; + + /// Issue #1: convert_i18n_meta_to_placeholder should return Err for unexpected + /// i18n metadata types, matching Angular's throw behavior. + /// Angular reference: ingest.ts lines 533-537, 587-591, 970-974 + #[test] + fn convert_i18n_meta_to_placeholder_returns_err_for_unexpected_type() { + let allocator = Allocator::default(); + let mut diagnostics = std::vec::Vec::new(); + + // Create an unexpected i18n metadata type (Message instead of BlockPlaceholder). + // Control flow blocks should only have BlockPlaceholder metadata. + let unexpected_i18n = I18nMeta::Message(I18nMessage { + instance_id: 0, + nodes: Vec::new_in(&allocator), + meaning: Atom::from(""), + description: Atom::from(""), + custom_id: Atom::from(""), + id: Atom::from(""), + legacy_ids: Vec::new_in(&allocator), + message_string: Atom::from(""), + }); + + let result = convert_i18n_meta_to_placeholder( + Some(unexpected_i18n), + &mut diagnostics, + oxc_span::SPAN, + "@for", + ); + + assert!(result.is_err(), "Should return Err for unexpected i18n metadata type"); + assert_eq!(diagnostics.len(), 1, "Should push exactly one diagnostic"); + assert!( + diagnostics[0].message.contains("Unhandled i18n metadata type for @for"), + "Diagnostic message should name the specific block type, got: {}", + diagnostics[0].message, + ); + } + + /// Issue #1: convert_i18n_meta_to_placeholder should return Ok(None) when + /// no i18n metadata is present. + #[test] + fn convert_i18n_meta_to_placeholder_returns_none_for_absent_metadata() { + let mut diagnostics = std::vec::Vec::new(); + + let result = + convert_i18n_meta_to_placeholder(None, &mut diagnostics, oxc_span::SPAN, "@if"); + + assert!(result.is_ok(), "Should return Ok for absent metadata"); + assert!(result.unwrap().is_none(), "Should return None when no i18n metadata"); + assert!(diagnostics.is_empty(), "Should not push any diagnostics"); + } + + /// Issue #2: get_computed_for_loop_variable_expression should return Err for + /// unknown loop variables, matching Angular's AssertionError throw. + /// Angular reference: ingest.ts lines 1043-1044 + #[test] + fn get_computed_for_loop_variable_expression_returns_err_for_unknown_var() { + let allocator = Allocator::default(); + let mut diagnostics = std::vec::Vec::new(); + let index_name = Atom::from("ɵ$index_0"); + let count_name = Atom::from("ɵ$count_0"); + + let result = get_computed_for_loop_variable_expression( + &allocator, + "$unknown", + &index_name, + &count_name, + &mut diagnostics, + ); + + assert!(result.is_err(), "Should return Err for unknown loop variable"); + assert_eq!(diagnostics.len(), 1, "Should push exactly one diagnostic"); + assert!( + diagnostics[0].message.contains("unknown @for loop variable $unknown"), + "Diagnostic should name the unknown variable" + ); + } + + /// Issue #2: get_computed_for_loop_variable_expression should return Ok for + /// all known loop variables ($index, $count, $first, $last, $even, $odd). + #[test] + fn get_computed_for_loop_variable_expression_returns_ok_for_known_vars() { + let allocator = Allocator::default(); + let index_name = Atom::from("ɵ$index_0"); + let count_name = Atom::from("ɵ$count_0"); + + for var in &["$index", "$count", "$first", "$last", "$even", "$odd"] { + let mut diagnostics = std::vec::Vec::new(); + let result = get_computed_for_loop_variable_expression( + &allocator, + var, + &index_name, + &count_name, + &mut diagnostics, + ); + + assert!(result.is_ok(), "Should return Ok for known variable {var}"); + assert!(diagnostics.is_empty(), "Should not push diagnostics for {var}"); + } + } +} diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/const_collection.rs b/crates/oxc_angular_compiler/src/pipeline/phases/const_collection.rs index 86af8b906..2ec5d59e7 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/const_collection.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/const_collection.rs @@ -9,6 +9,7 @@ //! Ported from Angular's `template/pipeline/src/phases/const_collection.ts`. use oxc_allocator::Vec as OxcVec; +use oxc_diagnostics::OxcDiagnostic; use oxc_span::Atom; use rustc_hash::FxHashMap; @@ -889,10 +890,11 @@ pub fn collect_element_consts_for_host(job: &mut HostBindingCompilationJob<'_>) // if (xref !== job.root.xref) { // throw new Error("An attribute would be const collected..."); // } - debug_assert!( - attr.target == root_xref, - "Host binding attribute should target root xref" - ); + if attr.target != root_xref { + job.diagnostics.push(OxcDiagnostic::error( + "Host binding attribute should target root xref", + )); + } attrs.add_for_host(attr); true } else { diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs b/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs index 9d103add1..0d57a08d9 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs @@ -435,21 +435,19 @@ fn count_safe_ternary_depth(expr: &IrExpression<'_>) -> usize { } /// Navigates to the SafeTernary at the given depth and returns a mutable reference. +/// Returns `None` if the expression chain is shorter than expected (should not +/// happen if `count_safe_ternary_depth` was used to determine the depth). fn get_safe_ternary_at_depth<'a, 'b>( expr: &'b mut IrExpression<'a>, depth: usize, -) -> &'b mut SafeTernaryExpr<'a> { +) -> Option<&'b mut SafeTernaryExpr<'a>> { let mut current = expr; for _ in 0..depth - 1 { if let IrExpression::SafeTernary(st) = current { current = st.expr.as_mut(); } } - if let IrExpression::SafeTernary(st) = current { - st.as_mut() - } else { - unreachable!("Expected SafeTernary at depth") - } + if let IrExpression::SafeTernary(st) = current { Some(st.as_mut()) } else { None } } /// Finds and modifies the deepest SafeTernary in a SafeTernary chain. @@ -468,7 +466,10 @@ fn modify_deepest_safe_ternary<'a>( } // Then navigate to the deepest SafeTernary (mutable borrow) - let deepest = get_safe_ternary_at_depth(receiver, depth); + let Some(deepest) = get_safe_ternary_at_depth(receiver, depth) else { + // SafeTernary chain was shorter than expected; skip modification. + return new_expr; + }; // Take the current expr from the deepest SafeTernary let old_expr = std::mem::replace(deepest.expr.as_mut(), make_placeholder(allocator)); diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs b/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs index a1b0d6836..1138581fd 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/generate_arrow_functions.rs @@ -122,10 +122,7 @@ fn convert_output_arrow_to_ir<'a>( ArrowFunctionBody::Statements(_) => { // The expression syntax doesn't support multi-line arrow functions, // but the output AST does. We don't need to handle them here if - // the user isn't able to write one. - // Angular throws an assertion error here; we use debug_assert to - // catch any internal compiler bugs that produce this in debug builds. - debug_assert!(false, "unexpected multi-line arrow function in template expression"); + // the user isn't able to write one. Skip conversion and return None. return None; } }; diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs b/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs index 37dc93037..55cda4ea2 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs @@ -1341,7 +1341,15 @@ fn reify_track_by<'a>( let return_value = if let Some(OutputStatement::Return(ret)) = statements.first() { ret.value.clone_in(allocator) } else { - unreachable!("checked above that there's exactly one Return statement"); + // The condition at line 1331 guarantees a single Return statement here. + // Fall back to function expression if the invariant is somehow violated. + diagnostics.push(OxcDiagnostic::error( + "Expected single Return statement for arrow function conversion", + )); + return OutputExpression::Function(Box::new_in( + FunctionExpr { name: None, params, statements, source_span: None }, + allocator, + )); }; OutputExpression::ArrowFunction(Box::new_in( 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 61aa6d88b..ce409397e 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/variable_optimization.rs @@ -12,10 +12,11 @@ use std::collections::{HashMap, HashSet}; use std::ptr::NonNull; use oxc_allocator::{Box as OxcBox, Vec as OxcVec}; +use oxc_diagnostics::OxcDiagnostic; use crate::ir::enums::VariableFlags; use crate::ir::expression::IrExpression; -use crate::ir::ops::{CreateOp, StatementOp, UpdateOp, UpdateOpBase, UpdateVariableOp, XrefId}; +use crate::ir::ops::{CreateOp, Op, StatementOp, UpdateOp, UpdateOpBase, UpdateVariableOp, XrefId}; use crate::output::ast::{ ExpressionStatement, OutputExpression, OutputStatement, ReturnStatement, WrappedIrExpr, }; @@ -1671,6 +1672,7 @@ fn uncount_variable_usages_in_expr( /// save/restore view optimization after variable optimization. fn optimize_arrow_function_ops<'a>(job: &mut ComponentCompilationJob<'a>) { let allocator = job.allocator; + let mut local_diagnostics = Vec::new(); // Collect view xrefs let view_xrefs: Vec = @@ -1694,12 +1696,14 @@ fn optimize_arrow_function_ops<'a>(job: &mut ComponentCompilationJob<'a>) { // Step 1: Optimize variables in the arrow function's ops // (No handler_expression for arrow functions) - optimize_handler_ops(&mut func.ops, None, allocator); + optimize_handler_ops(&mut func.ops, None, allocator, &mut local_diagnostics); // Step 2: Apply save/restore view optimization optimize_save_restore_view(&mut func.ops, allocator); } } + + job.diagnostics.extend(local_diagnostics); } /// Optimize variables within listener handler_ops. @@ -1711,6 +1715,7 @@ fn optimize_arrow_function_ops<'a>(job: &mut ComponentCompilationJob<'a>) { fn optimize_listener_handler_ops<'a>(job: &mut ComponentCompilationJob<'a>) { // Get allocator reference let allocator = job.allocator; + let mut local_diagnostics = Vec::new(); // Collect view xrefs let view_xrefs: Vec = @@ -1736,19 +1741,35 @@ fn optimize_listener_handler_ops<'a>(job: &mut ComponentCompilationJob<'a>) { &mut listener.handler_ops, listener.handler_expression.as_ref().map(|e| e.as_ref()), allocator, + &mut local_diagnostics, ); optimize_save_restore_view(&mut listener.handler_ops, allocator); } CreateOp::TwoWayListener(listener) => { - optimize_handler_ops(&mut listener.handler_ops, None, allocator); + optimize_handler_ops( + &mut listener.handler_ops, + None, + allocator, + &mut local_diagnostics, + ); optimize_save_restore_view(&mut listener.handler_ops, allocator); } CreateOp::AnimationListener(listener) => { - optimize_handler_ops(&mut listener.handler_ops, None, allocator); + optimize_handler_ops( + &mut listener.handler_ops, + None, + allocator, + &mut local_diagnostics, + ); optimize_save_restore_view(&mut listener.handler_ops, allocator); } CreateOp::Animation(animation) => { - optimize_handler_ops(&mut animation.handler_ops, None, allocator); + optimize_handler_ops( + &mut animation.handler_ops, + None, + allocator, + &mut local_diagnostics, + ); // Note: We intentionally do NOT call optimize_save_restore_view on // Animation handler_ops. Angular's ngtsc output keeps restoreView/resetView // in animation callbacks even when the return value doesn't reference the @@ -1759,6 +1780,8 @@ fn optimize_listener_handler_ops<'a>(job: &mut ComponentCompilationJob<'a>) { } } } + + job.diagnostics.extend(local_diagnostics); } /// Optimize variables within a single handler_ops Vec. @@ -1777,10 +1800,12 @@ fn optimize_handler_ops<'a>( handler_ops: &mut OxcVec<'a, UpdateOp<'a>>, handler_expression: Option<&IrExpression<'a>>, allocator: &'a oxc_allocator::Allocator, + diagnostics: &mut Vec, ) { // Step 1: Remove unused variables (loop until stable) loop { - let changed = optimize_handler_ops_once(handler_ops, handler_expression, allocator); + let changed = + optimize_handler_ops_once(handler_ops, handler_expression, allocator, diagnostics); if !changed { break; } @@ -1789,7 +1814,7 @@ fn optimize_handler_ops<'a>( // Step 2: Inline context variables (nextContext()) into other variable ops. // Per TypeScript's allowConservativeInlining (lines 536-538): // "Context can only be inlined into other variables." - inline_context_vars_in_handler_ops(handler_ops, handler_expression, allocator); + inline_context_vars_in_handler_ops(handler_ops, handler_expression, allocator, diagnostics); } /// After variables have been optimized in nested ops (e.g. handlers or functions), we may end up @@ -1908,6 +1933,7 @@ fn optimize_handler_ops_once<'a>( handler_ops: &mut OxcVec<'a, UpdateOp<'a>>, handler_expression: Option<&IrExpression<'a>>, allocator: &'a oxc_allocator::Allocator, + diagnostics: &mut Vec, ) -> bool { // Build OpInfo for each operation: fences and variable usage // We need indices to track operations @@ -2125,7 +2151,7 @@ fn optimize_handler_ops_once<'a>( result_ops.push(replacement); } else { // Clone the original op - result_ops.push(clone_update_op(op, allocator)); + result_ops.push(clone_update_op(op, allocator, diagnostics)); } } @@ -2136,7 +2162,11 @@ fn optimize_handler_ops_once<'a>( } /// Clone an UpdateOp, focusing on the types we care about in handler_ops. -fn clone_update_op<'a>(op: &UpdateOp<'a>, allocator: &'a oxc_allocator::Allocator) -> UpdateOp<'a> { +fn clone_update_op<'a>( + op: &UpdateOp<'a>, + allocator: &'a oxc_allocator::Allocator, + diagnostics: &mut Vec, +) -> UpdateOp<'a> { match op { UpdateOp::Variable(var) => UpdateOp::Variable(UpdateVariableOp { base: UpdateOpBase::default(), @@ -2152,12 +2182,34 @@ fn clone_update_op<'a>(op: &UpdateOp<'a>, allocator: &'a oxc_allocator::Allocato base: UpdateOpBase::default(), statement: clone_statement_with_ir_nodes(&stmt.statement, allocator), }), - // For other op types that might appear in handler_ops, we'd need to clone them too - // For now, these are the main ones we care about + // For other op types that might appear in handler_ops, we'd need to clone them too. + // This shouldn't happen in practice for handler_ops but we handle it gracefully + // by emitting a no-op statement and reporting a diagnostic. _ => { - // This shouldn't happen in practice for handler_ops - // but we need to handle it somehow - panic!("Unexpected op type in handler_ops during clone") + diagnostics.push(OxcDiagnostic::error(format!( + "Unexpected UpdateOp variant {:?} in clone_update_op", + op.kind() + ))); + UpdateOp::Statement(StatementOp { + base: UpdateOpBase::default(), + statement: crate::output::ast::OutputStatement::Expression( + oxc_allocator::Box::new_in( + crate::output::ast::ExpressionStatement { + expr: crate::output::ast::OutputExpression::Literal( + oxc_allocator::Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::Undefined, + source_span: None, + }, + allocator, + ), + ), + source_span: None, + }, + allocator, + ), + ), + }) } } } @@ -3682,6 +3734,7 @@ fn inline_context_vars_in_handler_ops<'a>( handler_ops: &mut OxcVec<'a, UpdateOp<'a>>, handler_expression: Option<&IrExpression<'a>>, allocator: &'a oxc_allocator::Allocator, + diagnostics: &mut Vec, ) { use crate::ir::enums::SemanticVariableKind; @@ -3803,7 +3856,7 @@ fn inline_context_vars_in_handler_ops<'a>( continue; } } - new_ops.push(clone_update_op(op, allocator)); + new_ops.push(clone_update_op(op, allocator, diagnostics)); } *handler_ops = new_ops; diff --git a/crates/oxc_angular_compiler/src/styles/encapsulation.rs b/crates/oxc_angular_compiler/src/styles/encapsulation.rs index b0bad416d..17e1ed6c9 100644 --- a/crates/oxc_angular_compiler/src/styles/encapsulation.rs +++ b/crates/oxc_angular_compiler/src/styles/encapsulation.rs @@ -552,7 +552,17 @@ fn scope_keyframes_names( result.push_str(name); result.push_str(trailing); } - _ => unreachable!(), + Some(q) => { + // Other quote characters (shouldn't happen in practice, + // but handle gracefully). + result.push_str(prefix); + result.push(q); + result.push_str(scope_selector); + result.push('_'); + result.push_str(name); + result.push(q); + result.push_str(trailing); + } } i = trailing_end;