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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/oxc_angular_compiler/src/component/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ impl AngularVersion {
self.major >= 22
}

/// Check if this version uses modern optional-chaining semantics (v22.0.0+).
///
/// Angular v22 changed the safe-navigation operator (`?.`) in template
/// expressions to yield `undefined` (native optional chaining) instead of the
/// legacy `null`. Earlier versions default to the legacy `== null ? null`
/// expansion. Users can opt back into legacy behavior with the
/// `legacyOptionalChaining` compiler option, or per-expression by wrapping it
/// in the `$safeNavigationMigration(...)` magic function.
///
/// See `angular/angular@2896c93cc1`.
pub fn supports_modern_optional_chaining(&self) -> bool {
self.major >= 22
}

/// Check if this version's runtime supports chained query instructions
/// (`ɵɵviewQuery(p1)(p2)`, `ɵɵcontentQuerySignal(...)(...)`).
///
Expand Down
43 changes: 37 additions & 6 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ pub struct TransformOptions {
/// When `None`, assumes latest Angular version (v19+ behavior).
pub angular_version: Option<AngularVersion>,

/// Override for the `legacyOptionalChaining` Angular compiler option.
///
/// Controls how the safe-navigation operator (`?.`) in template expressions is
/// emitted. When `Some(true)`, always uses the legacy `== null ? null` ternary;
/// when `Some(false)`, always emits native optional chaining (yielding
/// `undefined`). When `None`, the default is derived from `angular_version`
/// (legacy for < v22, modern for >= v22, legacy when the version is unknown).
///
/// See `angular/angular@2896c93cc1`.
pub legacy_optional_chaining: Option<bool>,

// Component metadata overrides for template-only compilation.
// These allow the build tool to pass component metadata when compiling
// templates in isolation (e.g., for testing or compare tool).
Expand Down Expand Up @@ -227,8 +238,9 @@ impl Default for TransformOptions {
jit: false,
hmr: false,
advanced_optimizations: false,
i18n_use_external_ids: true, // Angular's JIT default
angular_version: None, // None means assume latest (v19+ behavior)
i18n_use_external_ids: true, // Angular's JIT default
angular_version: None, // None means assume latest (v19+ behavior)
legacy_optional_chaining: None, // None: derive default from angular_version
// Metadata overrides default to None (use extracted/default values)
selector: None,
standalone: None,
Expand Down Expand Up @@ -3764,6 +3776,7 @@ fn compile_component_full<'a>(
pool_starting_index,
// Pass Angular version for feature-gated instruction selection
angular_version: options.angular_version,
legacy_optional_chaining: options.legacy_optional_chaining,
};

let mut job = ingest_component_with_options(
Expand Down Expand Up @@ -3827,6 +3840,7 @@ fn compile_component_full<'a>(
metadata,
template_pool_index,
options.angular_version,
options.legacy_optional_chaining,
);

// Extract the result and update pool index if host bindings were compiled
Expand Down Expand Up @@ -4217,6 +4231,7 @@ pub fn compile_template_to_js_with_options<'a>(
all_deferrable_deps_fn: None,
pool_starting_index: 0, // Standalone template compilation starts from 0
angular_version: options.angular_version,
legacy_optional_chaining: options.legacy_optional_chaining,
};

// Stage 3-5: Ingest and compile
Expand Down Expand Up @@ -4271,6 +4286,7 @@ pub fn compile_template_to_js_with_options<'a>(
options.selector.as_deref(),
host_pool_starting_index,
options.angular_version,
options.legacy_optional_chaining,
) {
// Add host binding pool declarations (pure functions, etc.)
for decl in host_result.declarations {
Expand Down Expand Up @@ -4390,6 +4406,7 @@ pub fn compile_template_for_hmr<'a>(
all_deferrable_deps_fn: None,
pool_starting_index: 0, // HMR template compilation starts from 0
angular_version: options.angular_version,
legacy_optional_chaining: options.legacy_optional_chaining,
};

// Stage 3-5: Ingest and compile
Expand Down Expand Up @@ -4535,6 +4552,7 @@ fn compile_component_host_bindings<'a>(
metadata: &ComponentMetadata<'a>,
pool_starting_index: u32,
angular_version: Option<AngularVersion>,
legacy_optional_chaining: Option<bool>,
) -> Option<HostBindingCompilationOutput<'a>> {
let host = metadata.host.as_ref()?;

Expand All @@ -4558,8 +4576,13 @@ fn compile_component_host_bindings<'a>(

// Ingest and compile the host bindings with the pool starting index
// This ensures constant names continue from where template compilation left off
let mut job =
ingest_host_binding_with_version(allocator, input, pool_starting_index, angular_version);
let mut job = ingest_host_binding_with_version(
allocator,
input,
pool_starting_index,
angular_version,
legacy_optional_chaining,
);
let result = compile_host_bindings(&mut job);

// Get the next pool index after host binding compilation
Expand Down Expand Up @@ -4883,6 +4906,7 @@ fn compile_host_bindings_from_input<'a>(
selector: Option<&str>,
pool_starting_index: u32,
angular_version: Option<crate::AngularVersion>,
legacy_optional_chaining: Option<bool>,
) -> Option<HostBindingCompilationResult<'a>> {
use oxc_allocator::FromIn;

Expand All @@ -4908,8 +4932,13 @@ fn compile_host_bindings_from_input<'a>(
// Convert to HostBindingInput and compile
let input =
convert_host_metadata_to_input(allocator, &host, component_name_atom, component_selector);
let mut job =
ingest_host_binding_with_version(allocator, input, pool_starting_index, angular_version);
let mut job = ingest_host_binding_with_version(
allocator,
input,
pool_starting_index,
angular_version,
legacy_optional_chaining,
);
let result = compile_host_bindings(&mut job);

Some(result)
Expand Down Expand Up @@ -4945,6 +4974,7 @@ pub fn compile_host_bindings_for_linker(
selector,
pool_starting_index,
None, // Linker always targets latest Angular version
None, // legacyOptionalChaining: derive from (absent) version
)?;

let emitter = JsEmitter::new();
Expand Down Expand Up @@ -5064,6 +5094,7 @@ pub fn compile_template_for_linker<'a>(
all_deferrable_deps_fn: None,
pool_starting_index: 0,
angular_version: None,
legacy_optional_chaining: None,
};

let component_name_atom = Ident::from_in(component_name, allocator);
Expand Down
21 changes: 21 additions & 0 deletions crates/oxc_angular_compiler/src/ir/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ impl<'a> IrExpression<'a> {
ResolvedPropertyReadExpr {
receiver: Box::new_in(e.receiver.clone_in(allocator), allocator),
name: e.name.clone(),
optional: e.optional,
source_span: e.source_span,
},
allocator,
Expand All @@ -615,6 +616,7 @@ impl<'a> IrExpression<'a> {
ResolvedCallExpr {
receiver: Box::new_in(e.receiver.clone_in(allocator), allocator),
args,
optional: e.optional,
source_span: e.source_span,
},
allocator,
Expand All @@ -624,6 +626,7 @@ impl<'a> IrExpression<'a> {
ResolvedKeyedReadExpr {
receiver: Box::new_in(e.receiver.clone_in(allocator), allocator),
key: Box::new_in(e.key.clone_in(allocator), allocator),
optional: e.optional,
source_span: e.source_span,
},
allocator,
Expand Down Expand Up @@ -923,6 +926,12 @@ pub struct ResolvedPropertyReadExpr<'a> {
pub receiver: Box<'a, IrExpression<'a>>,
/// Property name to read.
pub name: Ident<'a>,
/// Whether to read via native optional chaining (`receiver?.name`).
///
/// Set to `true` when a safe property read (`a?.b`) is expanded under modern
/// (Angular v22+) optional-chaining semantics, where `?.` yields `undefined`.
/// Legacy reads and plain (non-safe) reads leave this `false`.
pub optional: bool,
/// Source span.
pub source_span: Option<Span>,
}
Expand Down Expand Up @@ -955,6 +964,12 @@ pub struct ResolvedCallExpr<'a> {
pub receiver: Box<'a, IrExpression<'a>>,
/// The call arguments (resolved or original).
pub args: Vec<'a, IrExpression<'a>>,
/// Whether to invoke via native optional chaining (`receiver?.()`).
///
/// Set to `true` when a safe call (`a?.()`) is expanded under modern
/// (Angular v22+) optional-chaining semantics. Legacy and plain calls
/// leave this `false`.
pub optional: bool,
/// Source span.
pub source_span: Option<Span>,
}
Expand All @@ -970,6 +985,12 @@ pub struct ResolvedKeyedReadExpr<'a> {
pub receiver: Box<'a, IrExpression<'a>>,
/// The key expression (original, e.g., a number or expression).
pub key: Box<'a, IrExpression<'a>>,
/// Whether to read via native optional chaining (`receiver?.[key]`).
///
/// Set to `true` when a safe keyed read (`a?.[k]`) is expanded under modern
/// (Angular v22+) optional-chaining semantics. Legacy and plain keyed reads
/// leave this `false`.
pub optional: bool,
/// Source span.
pub source_span: Option<Span>,
}
Expand Down
39 changes: 39 additions & 0 deletions crates/oxc_angular_compiler/src/pipeline/compilation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ pub struct ComponentCompilationJob<'a> {
/// `ɵɵconditionalCreate`/`ɵɵconditionalBranchCreate` for `@if`/`@switch` blocks.
/// When `None`, assumes latest Angular version (v20+ behavior).
pub angular_version: Option<AngularVersion>,
/// Explicit override for the `legacyOptionalChaining` compiler option.
///
/// When `Some(true)`, safe navigation (`?.`) always expands to the legacy
/// `== null ? null` ternary. When `Some(false)`, it always emits native
/// optional chaining (`?.`, yielding `undefined`). When `None`, the default
/// is derived from `angular_version` (legacy for < v22, modern for >= v22,
/// legacy when the version is unknown). See `legacy_optional_chaining()`.
pub legacy_optional_chaining: Option<bool>,
/// Diagnostics collected during compilation.
pub diagnostics: std::vec::Vec<OxcDiagnostic>,
}
Expand Down Expand Up @@ -241,6 +249,7 @@ impl<'a> ComponentCompilationJob<'a> {
all_deferrable_deps_fn: None,
content_selectors: None,
angular_version: None,
legacy_optional_chaining: None,
diagnostics: std::vec::Vec::new(),
}
}
Expand Down Expand Up @@ -279,6 +288,21 @@ impl<'a> ComponentCompilationJob<'a> {
self.angular_version.map_or(true, |v: AngularVersion| v.supports_dom_property())
}

/// Resolve whether safe navigation (`?.`) should use legacy null semantics.
///
/// Returns `true` when `?.` must expand to the legacy `== null ? null` ternary,
/// `false` when it should emit native optional chaining (yielding `undefined`).
///
/// An explicit `legacy_optional_chaining` override wins. Otherwise the default
/// follows the Angular version: legacy for < v22, modern for >= v22, and legacy
/// when the version is unknown (the safe, conservative fallback Angular itself
/// uses for partial-compiled libraries targeting older runtimes).
pub fn legacy_optional_chaining(&self) -> bool {
self.legacy_optional_chaining.unwrap_or_else(|| {
self.angular_version.map_or(true, |v| !v.supports_modern_optional_chaining())
})
}

/// Allocates a new cross-reference ID.
pub fn allocate_xref_id(&mut self) -> XrefId {
let id = XrefId::new(self.next_xref_id);
Expand Down Expand Up @@ -621,6 +645,11 @@ pub struct HostBindingCompilationJob<'a> {
pub diagnostics: std::vec::Vec<OxcDiagnostic>,
/// Angular version for version-gated instruction emission.
pub angular_version: Option<AngularVersion>,
/// Explicit override for the `legacyOptionalChaining` compiler option.
///
/// See [`ComponentCompilationJob::legacy_optional_chaining`] for the resolution
/// rules; `None` derives the default from `angular_version`.
pub legacy_optional_chaining: Option<bool>,
}

impl<'a> HostBindingCompilationJob<'a> {
Expand Down Expand Up @@ -667,6 +696,7 @@ impl<'a> HostBindingCompilationJob<'a> {
fn_suffix: Ident::from("HostBindings"),
diagnostics: std::vec::Vec::new(),
angular_version: None,
legacy_optional_chaining: None,
}
}

Expand All @@ -685,6 +715,15 @@ impl<'a> HostBindingCompilationJob<'a> {
self.angular_version.map_or(true, |v| v.supports_dom_property())
}

/// Resolve whether safe navigation (`?.`) should use legacy null semantics.
///
/// See [`ComponentCompilationJob::legacy_optional_chaining`] for the rules.
pub fn legacy_optional_chaining(&self) -> bool {
self.legacy_optional_chaining.unwrap_or_else(|| {
self.angular_version.map_or(true, |v| !v.supports_modern_optional_chaining())
})
}

/// Allocates a new cross-reference ID.
pub fn allocate_xref_id(&mut self) -> XrefId {
let id = XrefId::new(self.next_xref_id);
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_angular_compiler/src/pipeline/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ pub fn convert_ast<'a>(
allocator,
),
name: pr.name.clone(),
optional: false,
source_span: convert_source_span(pr.source_span),
},
allocator,
Expand Down
Loading
Loading