diff --git a/Cargo.lock b/Cargo.lock index 7e0b98472..7cc33281d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9aa4f6ef44f0524537e718a3951e95ca7a8ba2bea672a81c52f1366354edb9b" +checksum = "45c8f46c8923b4afb4dd588d4684d5dd32332f1341225c066cc6af31094e7e3d" dependencies = [ "allocator-api2", "hashbrown 0.17.1", @@ -1071,9 +1071,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89fe3919efdcf81eee5d98091887365fa1c5c27ae9681da0d0ee4b0b8fc579d5" +checksum = "35465751cfc99739fee42d8345b57d5b5087b462bea7bc5bb17d1f248e1efa22" dependencies = [ "bitflags", "oxc_allocator", @@ -1089,9 +1089,9 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652d079d347e1744ff21e20b27488aef613d2bf0014c1a04f7cf70b22814db55" +checksum = "e78e5d50ecf9fbf6c00afb29dbd670784b2ac407fd676f625d1f3515a1a2d577" dependencies = [ "phf 0.14.0", "proc-macro2", @@ -1101,9 +1101,9 @@ dependencies = [ [[package]] name = "oxc_ast_visit" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbabedf039acdd87320086a8892916c573da5262cc75d5e69b5ab4158d9fc3c4" +checksum = "6836d2fde033c44eae57ab1be3fcc7bd6a57465783bdfce654ca28064a334dbe" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1113,9 +1113,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15235f6894f27cf9b368962c86ec231358cf06fac94ed7af23bab6119c03cb6d" +checksum = "75db0d260bb5493e31be0ab17651d428da5cd8c49ec2cf78d857e605d66385dd" dependencies = [ "bitflags", "cow-utils", @@ -1135,9 +1135,9 @@ dependencies = [ [[package]] name = "oxc_compat" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e732d4667a4610d6555962578b5fc92ff12df2815b3dfc636b2a5493994fcd5a" +checksum = "90809c31d39f49d2023b090ffcfd13b5f2e07b156abde430f1b1454ceef51bb1" dependencies = [ "cow-utils", "oxc-browserslist", @@ -1148,18 +1148,18 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86aa2e513fa7df9a64ef794b6ed16c273dde6ce903a940c7bb7e758de006f550" +checksum = "a46b759a8cb13a1111649370f1d769322b082edabd625466254f238818bdc9e9" dependencies = [ "ropey", ] [[package]] name = "oxc_diagnostics" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7b8cc3cb29cbf85829b2478fbf24eac3b52ff42723f2b5b748d750129d0038" +checksum = "5b80f4b05a80879518ebe85a8df9368758ebc1e176327b659e1e97389f8561c7" dependencies = [ "bytecount", "cow-utils", @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0caa01bfdeaf6d236c2389ac7d2f957c093b1697112394386298a31fa43af79a" +checksum = "f8c634665e0be5dde95549146734ab4939b8d1b5d2e7cc006d38b9f5d17a58a2" dependencies = [ "dragonbox_ecma", "itoa", @@ -1186,14 +1186,15 @@ dependencies = [ "oxc_ast", "oxc_data_structures", "oxc_span", + "oxc_str", "oxc_syntax", ] [[package]] name = "oxc_estree" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316ee8ab64fe4999ebde74ca1122ad0ed257b61619b133f79390d4854e4b6f68" +checksum = "5dcdd8235762cc9305d619842b73c1e89a0ebec8ccf2f27142ef597521443c1f" dependencies = [ "dragonbox_ecma", "itoa", @@ -1212,9 +1213,9 @@ dependencies = [ [[package]] name = "oxc_napi" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7afcce543669f6ae2c06b94489929d92d9bd2baabb69952deb408ba03311cc1d" +checksum = "5cc87e0f0e314b3795fe42a9113d2e50d22c80ac80da5be7319ce655354a7be6" dependencies = [ "napi", "napi-build", @@ -1228,9 +1229,9 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3317e026b10f295d00c78bc66976c89eff66a35d668e9ca124051fc293805851" +checksum = "635c4e1bb876a9d4ee0e338fa68547a1d75add58637e5c3921fb81ae10a60439" dependencies = [ "bitflags", "cow-utils", @@ -1252,9 +1253,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62315d61cf9085079998525f744ad309529bb749a72c46bc7ffaeed7b0ee6da8" +checksum = "3ae3f2c42773d16336a75d5c61dae45a34aea399f0b5399a7a25c8509d79662f" dependencies = [ "bitflags", "oxc_allocator", @@ -1296,9 +1297,9 @@ dependencies = [ [[package]] name = "oxc_semantic" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6621f8913ed2cbac9767ac29af45e474b41783fbf1558aa708847bc607eb4f" +checksum = "b24ee2095e2ddac60c2063b59693bbf2b80936c8a9e036463b153a203857831b" dependencies = [ "itertools 0.15.0", "memchr", @@ -1332,9 +1333,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd97e39f49cecf328dffed6e7f5453c02cc939ce712d078225fc6cb7cce47ac7" +checksum = "0cb5bee6f7a9829ddc4d8c05102fe1aa7e2ca31c7ff0bcfbb7069ca977334840" dependencies = [ "compact_str", "oxc_allocator", @@ -1346,12 +1347,13 @@ dependencies = [ [[package]] name = "oxc_str" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715ee1d0b6ec3aae7688b5a70bd72fd8fc67b3c489f8db670efcb970b5ea3e6a" +checksum = "c04d966af690d3b780ee4c47436174e558750eed6ca40b07d695fa4910ad8e33" dependencies = [ "compact_str", "hashbrown 0.17.1", + "memchr", "oxc_allocator", "oxc_estree", "serde", @@ -1359,9 +1361,9 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527d7d8a87d575889c5de61869b47a252347db0afe30352fa84e3ef008a24f30" +checksum = "684d5f6db25b6e3d026582b3926166c80eeda9aac8a879782292c6a33b7d13a0" dependencies = [ "bitflags", "cow-utils", @@ -1380,9 +1382,9 @@ dependencies = [ [[package]] name = "oxc_transformer" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad48d02089691663aa40c2b9f1d0d7a5823a0324217dc4f9bb6c731305e67eb3" +checksum = "fec592c64f6455678cbdb76635144f76825fcaf85f9509d814bd6a9e2bfde160" dependencies = [ "base64", "compact_str", @@ -1410,9 +1412,9 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.154.0" +version = "0.155.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604b12d0c175ab136f0309ae9155de00113d2a6be063a464236ece336bf22f03" +checksum = "b2ba98ff0dc34cb2e45bcbe3cee395e792298fad77ca63c574766571f859cc56" dependencies = [ "itoa", "oxc_allocator", diff --git a/Cargo.toml b/Cargo.toml index dd249ca10..7287b4eab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,19 +87,19 @@ multiple_crate_versions = "allow" [workspace.dependencies] # External oxc crates from crates.io -oxc_allocator = "0.154" -oxc_ast = "0.154" -oxc_ast_visit = "0.154" -oxc_diagnostics = "0.154" -oxc_napi = "0.154" -oxc_parser = "0.154" -oxc_semantic = "0.154" -oxc_span = "0.154" -oxc_syntax = "0.154" +oxc_allocator = "0.155" +oxc_ast = "0.155" +oxc_ast_visit = "0.155" +oxc_diagnostics = "0.155" +oxc_napi = "0.155" +oxc_parser = "0.155" +oxc_semantic = "0.155" +oxc_span = "0.155" +oxc_syntax = "0.155" oxc_sourcemap = "9.0.0" -oxc_str = "0.154" -oxc_transformer = "0.154" -oxc_codegen = "0.154" +oxc_str = "0.155" +oxc_transformer = "0.155" +oxc_codegen = "0.155" # Internal oxc_angular_compiler = { path = "crates/oxc_angular_compiler" } diff --git a/crates/angular_conformance/src/extractor/util.rs b/crates/angular_conformance/src/extractor/util.rs index 78f22ede0..97a557d24 100644 --- a/crates/angular_conformance/src/extractor/util.rs +++ b/crates/angular_conformance/src/extractor/util.rs @@ -4,6 +4,7 @@ //! and options parsing from AST expressions. use oxc_ast::ast::{Argument, CallExpression, Expression}; +use oxc_str::JSStr; use crate::test_case::{HtmlLexerOptions, HtmlParserOptions, LexerRange, ParseOptions}; @@ -13,11 +14,11 @@ impl SpecExtractor { /// Extract a string from a string literal expression pub(super) fn extract_string(&self, expr: &Expression<'_>) -> Option { match expr { - Expression::StringLiteral(lit) => Some(lit.value.to_string()), + Expression::StringLiteral(lit) => lit.value.as_str().map(str::to_string), Expression::TemplateLiteral(lit) if lit.expressions.is_empty() => { // Template literal with no expressions - use cooked value (with escapes processed) if lit.quasis.len() == 1 { - lit.quasis[0].value.cooked.as_ref().map(std::string::ToString::to_string) + lit.quasis[0].value.cooked.and_then(JSStr::as_str).map(str::to_string) } else { None } @@ -44,17 +45,17 @@ impl SpecExtractor { /// Resolve a string value directly from an expression pub(super) fn resolve_string_from_expression(&self, expr: &Expression<'_>) -> Option { match expr { - Expression::StringLiteral(lit) => Some(lit.value.to_string()), + Expression::StringLiteral(lit) => lit.value.as_str().map(str::to_string), Expression::TemplateLiteral(lit) if lit.expressions.is_empty() => { // Template literal with no expressions - use cooked value if lit.quasis.len() == 1 { - lit.quasis[0].value.cooked.as_ref().map(std::string::ToString::to_string) + lit.quasis[0].value.cooked.and_then(JSStr::as_str).map(str::to_string) } else { // Multiple quasis but no expressions means we can concatenate them let parts: Vec<&str> = lit .quasis .iter() - .filter_map(|q| q.value.cooked.as_ref().map(oxc_str::Str::as_str)) + .filter_map(|q| q.value.cooked.and_then(JSStr::as_str)) .collect(); Some(parts.join("")) } @@ -135,7 +136,7 @@ impl SpecExtractor { pub(super) fn extract_json_value(&self, expr: &Expression<'_>) -> Option { match expr { Expression::StringLiteral(lit) => { - Some(serde_json::Value::String(lit.value.to_string())) + lit.value.as_str().map(|s| serde_json::Value::String(s.to_string())) } Expression::NumericLiteral(lit) => { serde_json::Number::from_f64(lit.value.into()).map(serde_json::Value::Number) @@ -183,7 +184,7 @@ impl SpecExtractor { lit.quasis[0] .value .cooked - .as_ref() + .and_then(JSStr::as_str) .map(|s| serde_json::Value::String(s.to_string())) } else { None diff --git a/crates/oxc_angular_compiler/src/class_metadata/builders.rs b/crates/oxc_angular_compiler/src/class_metadata/builders.rs index 9e22b8566..050d459bd 100644 --- a/crates/oxc_angular_compiler/src/class_metadata/builders.rs +++ b/crates/oxc_angular_compiler/src/class_metadata/builders.rs @@ -1386,10 +1386,13 @@ fn get_decorator_name<'a>(decorator: &Decorator<'a>) -> Option<&'a str> { } /// Get property key name as an Atom. +/// +/// A string key holding a lone surrogate has no `&str` form, so it returns +/// `None` (and [`convert_plain_properties`] skips such entries too). fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.into()), - PropertyKey::StringLiteral(lit) => Some(lit.value.into()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => None, } } @@ -1400,13 +1403,15 @@ fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { /// string-literal key (`'x-y'`, `'x'`) is always quoted in the emitted object /// literal; a bare identifier is not. A no-substitution template literal /// counts too; private (`#x`), computed and numeric keys have no usable name -/// and return `None`. +/// and return `None`, as does a string key holding a lone surrogate (it has +/// no `&str` form, so the member is left out). fn member_key_name<'a>(key: &PropertyKey<'a>) -> Option<(Ident<'a>, bool)> { + let quoted = |value: oxc_str::JSStr<'a>| value.as_str().map(|name| (Ident::from(name), true)); match key { PropertyKey::StaticIdentifier(id) => Some((id.name.into(), false)), - PropertyKey::StringLiteral(lit) => Some((lit.value.into(), true)), + PropertyKey::StringLiteral(lit) => quoted(lit.value), PropertyKey::TemplateLiteral(tpl) if tpl.expressions.is_empty() => { - tpl.quasis.first().and_then(|q| q.value.cooked.clone().map(|v| (v.into(), true))) + tpl.quasis.first().and_then(|q| q.value.cooked.and_then(quoted)) } _ => None, } diff --git a/crates/oxc_angular_compiler/src/component/cross_file_elision.rs b/crates/oxc_angular_compiler/src/component/cross_file_elision.rs index 3d29f4e22..dd2b54c9a 100644 --- a/crates/oxc_angular_compiler/src/component/cross_file_elision.rs +++ b/crates/oxc_angular_compiler/src/component/cross_file_elision.rs @@ -871,6 +871,9 @@ impl Inner { return; } + // A source with a lone surrogate (`'\uD800'`) can't be + // resolved: keep the specifier's own info, unchased. + let source = decl.source.value.as_str(); for spec in &decl.specifiers { let exported_name = spec.exported.name().to_string(); let local_name = spec.local.name().to_string(); @@ -878,7 +881,7 @@ impl Inner { exported_name, ExportInfo { is_type_only: spec.export_kind.is_type(), - re_export_source: Some((decl.source.value.to_string(), local_name)), + re_export_source: source.map(|source| (source.to_string(), local_name)), value_local: None, }, ); @@ -901,7 +904,9 @@ impl Inner { ); } Statement::ExportAllDeclaration(decl) => { - let source_module = decl.source.value.to_string(); + // A source with a lone surrogate (`'\uD800'`) can't be resolved. + let Some(source_module) = decl.source.value.as_str() else { return }; + let source_module = source_module.to_string(); if let Some(exported) = &decl.exported { // export * as X from './other' exports.insert( diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index c87edd129..d4ceea707 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -86,6 +86,12 @@ pub fn extract_component_metadata<'a>( if let ObjectPropertyKind::ObjectProperty(prop) = prop && is_metadata_property(prop) { + // A string key with a lone surrogate (`'\uD800'`) has no `&str` + // form and names no option, so it is ignored like any unknown key. + if matches!(&prop.key, PropertyKey::StringLiteral(lit) if lit.value.has_lone_surrogate()) + { + continue; + } let key_name = get_property_key_name(&prop.key, consts)?; match key_name.as_str() { @@ -410,7 +416,7 @@ fn get_property_key_name<'a>( ) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(lit) => Some(lit.value.clone().into()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(Ident::from), // Computed identifier reference: `[FOO]: bar` — resolve against same-file consts. PropertyKey::Identifier(id) => consts.get(id.name.as_str()).cloned(), _ => None, @@ -782,7 +788,7 @@ fn parse_mapping_element<'a>( match element { // Simple string: "color" - same public and internal name ArrayExpressionElement::StringLiteral(lit) => { - let value = lit.value.as_str(); + let value = lit.value.as_str()?; // Check for mapping format: "internalName: publicName" if let Some(colon_pos) = value.find(':') { @@ -794,7 +800,7 @@ fn parse_mapping_element<'a>( )) } else { // Same name for both - Some((lit.value.clone().into(), lit.value.clone().into())) + Some((Ident::from(value), Ident::from(value))) } } @@ -804,13 +810,13 @@ fn parse_mapping_element<'a>( if elements.len() >= 2 { let internal = match elements.first() { Some(ArrayExpressionElement::StringLiteral(lit)) => { - Some(lit.value.clone().into()) + lit.value.as_str().map(Ident::from) } _ => None, }; let public = match elements.get(1) { Some(ArrayExpressionElement::StringLiteral(lit)) => { - Some(lit.value.clone().into()) + lit.value.as_str().map(Ident::from) } _ => None, }; @@ -968,8 +974,10 @@ fn extract_param_dependency<'a>( "Attribute" => { // @Attribute('attrName') - extract the attribute name if let Expression::CallExpression(call) = &decorator.expression { - if let Some(Argument::StringLiteral(s)) = call.arguments.first() { - attribute_name = Some(s.value.clone().into()); + if let Some(Argument::StringLiteral(s)) = call.arguments.first() + && let Some(name) = s.value.as_str() + { + attribute_name = Some(name.into()); } } } diff --git a/crates/oxc_angular_compiler/src/component/import_elision.rs b/crates/oxc_angular_compiler/src/component/import_elision.rs index 53d3b89b7..76731155b 100644 --- a/crates/oxc_angular_compiler/src/component/import_elision.rs +++ b/crates/oxc_angular_compiler/src/component/import_elision.rs @@ -43,7 +43,7 @@ use std::path::Path; use oxc_ast::ast::{ BindingIdentifier, ClassElement, Expression, ImportDeclarationSpecifier, MethodDefinitionKind, - Program, Statement, TSType, + Program, Statement, StringLiteral, TSType, }; use oxc_semantic::{Semantic, SemanticBuilder, SymbolFlags}; use oxc_str::Ident; @@ -106,6 +106,13 @@ impl<'a> ImportElisionAnalyzer<'a> { continue; } + // A source with a lone surrogate (`'\uD800'`) can't be named in a + // generated import (`build_import_map` leaves it out), so generated + // code references its bindings as written: keep them all. + if import_decl.source.value.has_lone_surrogate() { + continue; + } + let Some(specifiers) = &import_decl.specifiers else { continue; }; @@ -906,7 +913,10 @@ impl<'a> ImportElisionAnalyzer<'a> { continue; } - let source = import_decl.source.value.as_str(); + // A source with a lone surrogate (`'\uD800'`) can't be resolved. + let Some(source) = import_decl.source.value.as_str() else { + continue; + }; let Some(specifiers) = &import_decl.specifiers else { continue; }; @@ -1049,7 +1059,6 @@ pub fn import_elision_edits<'a>( } // Build the new import statement - let source_str = import_decl.source.value.as_str(); let mut new_import = String::from("import "); if let Some(default_name) = default_import { @@ -1069,9 +1078,9 @@ pub fn import_elision_edits<'a>( } } - new_import.push_str(" from \""); - new_import.push_str(source_str); - new_import.push_str("\";"); + new_import.push_str(" from "); + push_module_specifier(&mut new_import, &import_decl.source, source); + new_import.push(';'); // Replace the entire import declaration let start = import_decl.span.start as usize; @@ -1100,14 +1109,14 @@ pub fn import_elision_edits<'a>( export_decl.span.end, export_decl.export_kind.is_type(), &export_decl.specifiers, - None::<&str>, + None, ), Statement::ExportFromDeclaration(export_decl) => ( export_decl.span.start, export_decl.span.end, export_decl.export_kind.is_type(), &export_decl.specifiers, - Some(export_decl.source.value.as_str()), + Some(&export_decl.source), ), _ => continue, }; @@ -1160,9 +1169,8 @@ pub fn import_elision_edits<'a>( new_export.push_str(" }"); if let Some(source_mod) = source_module { - new_export.push_str(" from \""); - new_export.push_str(source_mod); - new_export.push('"'); + new_export.push_str(" from "); + push_module_specifier(&mut new_export, source_mod, source); } new_export.push(';'); @@ -1177,6 +1185,20 @@ pub fn import_elision_edits<'a>( edits } +/// Write a rebuilt declaration's module specifier: its value in double quotes, +/// or, for a value with a lone surrogate (`'\uD800'`), which has no `&str`, +/// the literal as written. +fn push_module_specifier(out: &mut String, specifier: &StringLiteral<'_>, source: &str) { + match specifier.value.as_str() { + Some(value) => { + out.push('"'); + out.push_str(value); + out.push('"'); + } + None => out.push_str(specifier.span.source_text(source)), + } +} + #[cfg(test)] mod tests { use super::*; @@ -2527,4 +2549,23 @@ class TestComponent { filtered ); } + + /// A module specifier with a lone surrogate has no `&str` form: an import + /// from it keeps all its bindings (generated code can't import them + /// again), and a rebuilt re-export keeps the specifier as written. + #[test] + fn test_lone_surrogate_module_specifier_is_kept_as_written() { + let source = r#"import { Used, OnlyType } from '\uD800'; +export { type T, V } from '\uDC00'; +let x: OnlyType = Used; +"#; + let filtered = filter_source(source); + assert_eq!( + filtered, + r#"import { Used, OnlyType } from '\uD800'; +export { V } from '\uDC00'; +let x: OnlyType = Used; +"# + ); + } } diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 4ca4f9736..f9ad30d81 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -540,7 +540,11 @@ pub fn build_import_map<'a>( continue; }; - let default_source_module = import_decl.source.value.clone(); + // A source with a lone surrogate (`'\uD800'`) can't be named in a + // generated import: unless `resolved_imports` names another path, its + // bindings stay unmapped and are referenced as written (import elision + // keeps them, see `ImportElisionAnalyzer::analyze`). + let default_source_module = import_decl.source.value.as_str().map(Ident::from); // `import type { ... }` makes all specifiers type-only let decl_is_type_only = import_decl.import_kind == ImportOrExportKind::Type; @@ -566,10 +570,13 @@ pub fn build_import_map<'a>( decl_is_type_only || spec.import_kind == ImportOrExportKind::Type; // Check if we have a resolved path for this identifier - let source_module = resolved_imports + let Some(source_module) = resolved_imports .and_then(|m| m.get(local_name.as_str())) .map(|resolved| Ident::from(allocator.alloc_str(resolved))) - .unwrap_or_else(|| default_source_module.clone().into()); + .or(default_source_module) + else { + continue; + }; // `import { Foo as Bar }` → export name `Foo` for `i1.Foo` / @defer. // Path rewrite via `resolved_imports` does not change the export @@ -597,10 +604,13 @@ pub fn build_import_map<'a>( let local_name: Ident<'a> = spec.local.name.clone().into(); // Check if we have a resolved path for this identifier - let source_module = resolved_imports + let Some(source_module) = resolved_imports .and_then(|m| m.get(local_name.as_str())) .map(|resolved| Ident::from(allocator.alloc_str(resolved))) - .unwrap_or_else(|| default_source_module.clone().into()); + .or(default_source_module) + else { + continue; + }; import_map.insert( local_name, @@ -622,10 +632,13 @@ pub fn build_import_map<'a>( let local_name: Ident<'a> = spec.local.name.clone().into(); // Check if we have a resolved path for this identifier - let source_module = resolved_imports + let Some(source_module) = resolved_imports .and_then(|m| m.get(local_name.as_str())) .map(|resolved| Ident::from(allocator.alloc_str(resolved))) - .unwrap_or_else(|| default_source_module.clone().into()); + .or(default_source_module) + else { + continue; + }; import_map.insert( local_name, @@ -1143,7 +1156,7 @@ fn decorator_forces_jit(decorator: &oxc_ast::ast::Decorator<'_>) -> bool { } let is_jit = match &prop.key { PropertyKey::StaticIdentifier(id) => id.name.as_str() == "jit", - PropertyKey::StringLiteral(s) => s.value.as_str() == "jit", + PropertyKey::StringLiteral(s) => s.value == "jit", _ => false, }; if is_jit { @@ -1274,6 +1287,7 @@ fn extract_all_jit_member_decorators( class: &oxc_ast::ast::Class<'_>, string_consts: &crate::directive::StringConsts<'_>, core_namespace: &str, + diagnostics: &mut std::vec::Vec, ) -> (std::vec::Vec, std::vec::Vec) { use oxc_ast::ast::{ClassElement, MethodDefinitionKind, PropertyKey}; @@ -1281,12 +1295,17 @@ fn extract_all_jit_member_decorators( let mut non_angular_members: std::vec::Vec = std::vec::Vec::new(); for element in &class.body.body { + // A string key with a lone surrogate (`'\uD800'`) has no `&str` form; + // like a computed or numeric key, it is skipped. let (member_name, quoted_key, is_static, is_property, decorators, initializer) = match element { ClassElement::PropertyDefinition(prop) => { let (name, quoted) = match &prop.key { PropertyKey::StaticIdentifier(id) => (id.name.to_string(), false), - PropertyKey::StringLiteral(s) => (s.value.to_string(), true), + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(name) => (name.to_string(), true), + None => continue, + }, _ => continue, }; (name, quoted, prop.r#static, true, &prop.decorators, prop.value.as_ref()) @@ -1297,7 +1316,10 @@ fn extract_all_jit_member_decorators( } let (name, quoted) = match &method.key { PropertyKey::StaticIdentifier(id) => (id.name.to_string(), false), - PropertyKey::StringLiteral(s) => (s.value.to_string(), true), + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(name) => (name.to_string(), true), + None => continue, + }, _ => continue, }; (name, quoted, method.r#static, false, &method.decorators, None) @@ -1305,7 +1327,10 @@ fn extract_all_jit_member_decorators( ClassElement::AccessorProperty(accessor) => { let (name, quoted) = match &accessor.key { PropertyKey::StaticIdentifier(id) => (id.name.to_string(), false), - PropertyKey::StringLiteral(s) => (s.value.to_string(), true), + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(name) => (name.to_string(), true), + None => continue, + }, _ => continue, }; (name, quoted, accessor.r#static, false, &accessor.decorators, None) @@ -1376,6 +1401,7 @@ fn extract_all_jit_member_decorators( &explicit_field_decorators, string_consts, core_namespace, + diagnostics, ); // Upstream inserts the synthesized decorator ahead of the member's // existing decorators (`[newDecorator, ...member.node.modifiers]`), @@ -1638,12 +1664,23 @@ fn synthesize_signal_api_decorators( existing: &rustc_hash::FxHashSet, string_consts: &crate::directive::StringConsts<'_>, core_namespace: &str, + diagnostics: &mut std::vec::Vec, ) -> std::vec::Vec { let unwrapped = unwrap_jit_initializer(initializer); let Expression::CallExpression(call) = unwrapped else { return std::vec::Vec::new() }; let Some(kind) = classify_initializer_api(&call.callee, string_consts) else { return std::vec::Vec::new(); }; + // The `alias` option, or the field name; `None` (after reporting the + // error) when the alias can't be read. + let mut alias_or_field_name = + |options_index: usize| match extract_string_option(call, options_index, "alias") { + Ok(alias) => Some(alias.unwrap_or_else(|| field_name.to_string())), + Err(error) => { + diagnostics.push(error); + None + } + }; match kind { InitializerApiKind::Input | InitializerApiKind::InputRequired => { @@ -1653,8 +1690,9 @@ fn synthesize_signal_api_decorators( let required = matches!(kind, InitializerApiKind::InputRequired); // input(initial, options?) → options is args[1]; input.required(options?) → args[0]. let options_index = if required { 0 } else { 1 }; - let alias = extract_string_option(call, options_index, "alias") - .unwrap_or_else(|| field_name.to_string()); + let Some(alias) = alias_or_field_name(options_index) else { + return std::vec::Vec::new(); + }; let args = format!( "{{ isSignal: true, alias: \"{alias}\", required: {required}, transform: undefined }}", alias = escape_js_string(&alias), @@ -1672,8 +1710,9 @@ fn synthesize_signal_api_decorators( // output(options?) → args[0]; outputFromObservable(source, options?) → args[1]. let options_index = if matches!(kind, InitializerApiKind::OutputFromObservable) { 1 } else { 0 }; - let alias = extract_string_option(call, options_index, "alias") - .unwrap_or_else(|| field_name.to_string()); + let Some(alias) = alias_or_field_name(options_index) else { + return std::vec::Vec::new(); + }; let args = format!("\"{}\"", escape_js_string(&alias)); std::vec::Vec::from([JitParamDecorator { name: format!("{core_namespace}.Output"), @@ -1688,8 +1727,9 @@ fn synthesize_signal_api_decorators( let required = matches!(kind, InitializerApiKind::ModelRequired); // Same arg layout as input. let options_index = if required { 0 } else { 1 }; - let alias = extract_string_option(call, options_index, "alias") - .unwrap_or_else(|| field_name.to_string()); + let Some(alias) = alias_or_field_name(options_index) else { + return std::vec::Vec::new(); + }; let input_args = format!( "{{ isSignal: true, alias: \"{alias}\", required: {required}, transform: undefined }}", alias = escape_js_string(&alias), @@ -1768,29 +1808,38 @@ fn jit_classes_need_angular_core_namespace( } /// Pull a string option (e.g. `alias`) from the object literal at `args[options_index]`. -/// Returns `None` if the argument isn't an object literal or the option is missing/non-string. +/// Returns `Ok(None)` if the argument isn't an object literal or the option is missing/non-string. +/// +/// A string with a lone surrogate (`'\uD800'`) has no `&str` form: it is an +/// error, as the AOT path's `literal_alias_error` reports it. fn extract_string_option( call: &oxc_ast::ast::CallExpression<'_>, options_index: usize, option_name: &str, -) -> Option { - let arg = call.arguments.get(options_index)?; - let Argument::ObjectExpression(obj) = arg else { return None }; +) -> Result, OxcDiagnostic> { + let Some(arg) = call.arguments.get(options_index) else { return Ok(None) }; + let Argument::ObjectExpression(obj) = arg else { return Ok(None) }; for prop in &obj.properties { let ObjectPropertyKind::ObjectProperty(prop) = prop else { continue }; let key_matches = match &prop.key { PropertyKey::StaticIdentifier(id) => id.name.as_str() == option_name, - PropertyKey::StringLiteral(s) => s.value.as_str() == option_name, + PropertyKey::StringLiteral(s) => s.value == option_name, _ => false, }; if !key_matches { continue; } if let Expression::StringLiteral(s) = &prop.value { - return Some(s.value.to_string()); + return match s.value.as_str() { + Some(value) => Ok(Some(value.to_string())), + None => Err(OxcDiagnostic::error( + "Alias needs to be a string that is statically analyzable.", + ) + .with_label(s.span)), + }; } } - None + Ok(None) } /// Minimal JS-string escape for values embedded in synthesized propDecorator text. @@ -1964,7 +2013,7 @@ fn build_jit_decorator_text<'a>( if let ObjectPropertyKind::ObjectProperty(prop) = prop { let key_name = match &prop.key { PropertyKey::StaticIdentifier(id) => Some(id.name.as_str()), - PropertyKey::StringLiteral(s) => Some(s.value.as_str()), + PropertyKey::StringLiteral(s) => s.value.as_str(), _ => None, }; @@ -2493,6 +2542,7 @@ fn collect_jit_class_info<'a>( resource_imports: &mut std::vec::Vec<(String, String)>, string_consts: &crate::directive::StringConsts<'a>, core_namespace: &str, + diagnostics: &mut std::vec::Vec, ) -> JitClassInfo { let mut all_class_decorator_spans: std::vec::Vec = std::vec::Vec::new(); let mut all_class_decorator_texts: std::vec::Vec = std::vec::Vec::new(); @@ -2527,8 +2577,13 @@ fn collect_jit_class_info<'a>( extract_jit_ctor_params(source, class, string_consts); // Extract Angular and non-Angular member decorators - let (member_decorators, non_angular_member_decorators) = - extract_all_jit_member_decorators(source, class, string_consts, core_namespace); + let (member_decorators, non_angular_member_decorators) = extract_all_jit_member_decorators( + source, + class, + string_consts, + core_namespace, + diagnostics, + ); JitClassInfo { class_name, @@ -2840,6 +2895,7 @@ fn transform_angular_file_jit( &mut resource_imports, &string_consts, core_namespace.as_str(), + &mut result.diagnostics, )); result.component_count += @@ -3110,7 +3166,10 @@ pub fn transform_angular_file( continue; }; - let source = import_decl.source.value.as_str(); + // A source with a lone surrogate (`'\uD800'`) can't be resolved. + let Some(source) = import_decl.source.value.as_str() else { + continue; + }; let Some(specifiers) = &import_decl.specifiers else { continue; }; @@ -3268,6 +3327,7 @@ pub fn transform_angular_file( &mut jit_resource_imports, &string_consts, core_namespace.as_str(), + &mut result.diagnostics, ); // A co-located @Injectable still compiles: upstream's diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 782f762a3..2c2cae5fd 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -388,8 +388,10 @@ fn extract_param_dependency<'a>( "Attribute" => { // @Attribute('attrName') - extract the attribute name if let Expression::CallExpression(call) = &decorator.expression { - if let Some(Argument::StringLiteral(s)) = call.arguments.first() { - attribute_name = Some(s.value.clone().into()); + if let Some(Argument::StringLiteral(s)) = call.arguments.first() + && let Some(name) = s.value.as_str() + { + attribute_name = Some(name.into()); } } } @@ -643,9 +645,9 @@ pub fn collect_string_consts<'a>( /// Returns `None` for anything that isn't a plain string at compile time. fn literal_string_from_expression<'a>(expr: &Expression<'a>) -> Option> { match expr { - Expression::StringLiteral(lit) => Some(lit.value.clone().into()), + Expression::StringLiteral(lit) => lit.value.as_str().map(Ident::from), Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => { - tpl.quasis.first().and_then(|q| q.value.cooked.clone().map(Into::into)) + tpl.quasis.first().and_then(|q| q.value.cooked?.as_str()).map(Ident::from) } _ => None, } @@ -661,14 +663,14 @@ pub(crate) fn resolve_template_literal<'a>( consts: &StringConsts<'a>, ) -> Option> { if tpl.expressions.is_empty() { - return tpl.quasis.first().and_then(|q| q.value.cooked.clone().map(Into::into)); + return tpl.quasis.first().and_then(|q| q.value.cooked?.as_str()).map(Ident::from); } let mut buf = String::new(); // A TemplateLiteral has `quasis.len() == expressions.len() + 1`: the // interleaving is quasi[0], expr[0], quasi[1], expr[1], …, quasi[n]. for (i, quasi) in tpl.quasis.iter().enumerate() { - let cooked = quasi.value.cooked.as_ref()?; - buf.push_str(cooked.as_str()); + let cooked = quasi.value.cooked?.as_str()?; + buf.push_str(cooked); if let Some(expr) = tpl.expressions.get(i) { let resolved = extract_string_value(allocator, expr, consts)?; buf.push_str(resolved.as_str()); @@ -687,7 +689,7 @@ fn get_property_key_name<'a>( ) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(lit) => Some(lit.value.clone().into()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(Ident::from), // Computed identifier reference: `[FOO]: bar` — resolve against same-file consts. PropertyKey::Identifier(id) => consts.get(id.name.as_str()).cloned(), _ => None, @@ -774,6 +776,14 @@ fn io_error(field: &str, message: impl FnOnce() -> String, value: &Value<'_>) -> value_error(&format!("@Directive.{field}"), message, value) } +/// Whether an object property's value is dynamic because it reads a string +/// with a lone surrogate (`'\uD800'`), as opposed to being absent or truly +/// dynamic. +fn reads_lone_surrogate<'a>(prop: &super::evaluator::Prop<'a>, consts: &StringConsts<'a>) -> bool { + prop.value.is_dynamic() + && prop.expr.is_some_and(|expr| Evaluator::reads_lone_surrogate(consts, expr)) +} + /// oxc's error for a transform written inside a function that the metadata /// calls, using that function's parameters (`transform: (v) => v + name`), so /// it would mean something else, or nothing, where the metadata is compiled. @@ -997,6 +1007,13 @@ fn parse_input_object<'a>( return Some((io_error("inputs", String::new, value), span)); } } + // Nor an alias with a lone surrogate (`'\uD800'`), which evaluates as + // dynamic: an unreadable string item gets this error too. + if item.prop("alias").is_some_and(|alias| reads_lone_surrogate(alias, consts)) { + let message = + || "@Directive.inputs array can only contain strings or object literals".into(); + return Some((io_error("inputs", message, &Value::Dynamic), span)); + } let alias = item.prop("alias").and_then(|p| p.value.as_str()).unwrap_or(name); let required = matches!(item.prop("required").map(|p| &p.value), Some(Value::Bool(true))); let (transform_function, error) = match item.prop("transform") { @@ -1465,8 +1482,11 @@ pub fn decorator_io_errors<'a>( // @HostListener argument checks in the same step (arity, "argument must // be a string") aren't ported — `extract_host_binding_name` falls back // to the member name and `parse_host_listener_config` drops a non-string - // argument instead of reporting it. + // argument instead of reporting it — except for a string with a lone + // surrogate, which oxc can't read. let host = || host_metadata_error(allocator, config, decorator_name, consts).map(at); + let host_members = + || super::property_decorators::lone_surrogate_host_error(class, consts).map(at); io.as_ref() .and_then(|io| io.input_error.clone()) @@ -1478,6 +1498,7 @@ pub fn decorator_io_errors<'a>( .or_else(queries) .or_else(selector) .or_else(host) + .or_else(host_members) .map(|(message, span, related)| { let diagnostic = OxcDiagnostic::error(message).with_label(span); match related { @@ -1703,6 +1724,16 @@ fn input_decorator_error<'a>( ); return Some((message, decorator.span)); } + // An `alias` with a lone surrogate (`'\uD800'`) evaluates as dynamic, + // which would read as no alias: report it like `@Input('\uD800')` rather + // than compile the class property name. + if options.prop("alias").is_some_and(|alias| reads_lone_surrogate(alias, consts)) { + let message = format!( + "{subject} decorator argument must resolve to a string or an object literal{}", + Value::Dynamic.wrong_type_suffix() + ); + return Some((message, decorator.span)); + } let transform = options.prop("transform")?; if let Some(error) = imported_transform_error(&subject, transform, span) { return Some(error); @@ -2279,7 +2310,7 @@ fn parse_mapping_element<'a>( match element { // Simple string: "color" - same public and internal name ArrayExpressionElement::StringLiteral(lit) => { - let value = lit.value.as_str(); + let value = lit.value.as_str()?; // Check for mapping format: "internalName: publicName" if let Some(colon_pos) = value.find(':') { @@ -2291,7 +2322,7 @@ fn parse_mapping_element<'a>( )) } else { // Same name for both - Some((lit.value.clone().into(), lit.value.clone().into())) + Some((Ident::from(value), Ident::from(value))) } } @@ -2301,13 +2332,13 @@ fn parse_mapping_element<'a>( if elements.len() >= 2 { let internal = match elements.first() { Some(ArrayExpressionElement::StringLiteral(lit)) => { - Some(lit.value.clone().into()) + lit.value.as_str().map(Ident::from) } _ => None, }; let public = match elements.get(1) { Some(ArrayExpressionElement::StringLiteral(lit)) => { - Some(lit.value.clone().into()) + lit.value.as_str().map(Ident::from) } _ => None, }; diff --git a/crates/oxc_angular_compiler/src/directive/dts_type.rs b/crates/oxc_angular_compiler/src/directive/dts_type.rs index 1f0539bec..112c19db2 100644 --- a/crates/oxc_angular_compiler/src/directive/dts_type.rs +++ b/crates/oxc_angular_compiler/src/directive/dts_type.rs @@ -2089,28 +2089,12 @@ pub(crate) fn quote(s: &str) -> String { } /// [`quote`] for a string literal from the source, whose value can hold lone -/// surrogates (`'\uD800'`): the parser encodes each as `\u{FFFD}` and its -/// code unit in hex (and a `\u{FFFD}` as `\u{FFFD}fffd`). -fn quote_literal(literal: &StringLiteral<'_>) -> String { - if !literal.lone_surrogates { - return quote(&literal.value); - } - let mut units = Vec::new(); - let mut chars = literal.value.chars(); - while let Some(c) = chars.next() { - if c == '\u{FFFD}' { - let hex: String = chars.by_ref().take(4).collect(); - if let Ok(unit) = u16::from_str_radix(&hex, 16) { - units.push(unit); - continue; - } - } - units.extend(c.encode_utf16(&mut [0; 2]).iter()); - } - quote_units(units) +/// surrogates (`'\uD800'`). +pub(crate) fn quote_literal(literal: &StringLiteral<'_>) -> String { + quote_units(literal.value.encode_utf16()) } -fn quote_units(units: impl IntoIterator) -> String { +pub(crate) fn quote_units(units: impl IntoIterator) -> String { let units: Vec = units.into_iter().collect(); let mut out = String::with_capacity(units.len() + 2); out.push('"'); diff --git a/crates/oxc_angular_compiler/src/directive/evaluator.rs b/crates/oxc_angular_compiler/src/directive/evaluator.rs index e7d381615..9ec8660f3 100644 --- a/crates/oxc_angular_compiler/src/directive/evaluator.rs +++ b/crates/oxc_angular_compiler/src/directive/evaluator.rs @@ -12,6 +12,7 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; +use oxc_ast::StaticPropertyName; use oxc_ast::ast::{ Argument, ArrayExpression, ArrayExpressionElement, ArrowFunctionBody, ArrowFunctionExpression, BinaryExpression, BindingPattern, BlockStatement, CallExpression, CatchClause, ChainElement, @@ -32,6 +33,7 @@ use oxc_ast::ast::{ }; use oxc_ast_visit::{Visit, walk}; use oxc_span::{GetSpan, Span}; +use oxc_str::JSStr; use oxc_syntax::operator::{BinaryOperator, LogicalOperator, UnaryOperator}; use oxc_syntax::scope::ScopeFlags; @@ -199,7 +201,7 @@ impl<'a> AliasTarget<'a> { } Some(match reference { TSModuleReference::ExternalModuleReference(m) => { - AliasTarget::Module(m.expression.value.as_str()) + AliasTarget::Module(m.expression.value.as_str()?) } TSModuleReference::IdentifierReference(id) => { AliasTarget::Entity(std::vec![id.name.as_str()]) @@ -314,14 +316,17 @@ impl<'a> FileScope<'a> { for stmt in &program.body { match stmt { Statement::ImportDeclaration(import) => { - let module = import.source.value.as_str(); + let Some(module) = import.source.value.as_str() else { continue }; for spec in import.specifiers.iter().flatten() { let (local, imported) = match spec { ImportDeclarationSpecifier::ImportSpecifier(s) => { let imported = match &s.imported { ModuleExportName::IdentifierName(n) => n.name.as_str(), ModuleExportName::IdentifierReference(n) => n.name.as_str(), - ModuleExportName::StringLiteral(n) => n.value.as_str(), + ModuleExportName::StringLiteral(n) => match n.value.as_str() { + Some(name) => name, + None => continue, + }, }; (s.local.name.as_str(), Some(imported)) } @@ -1637,11 +1642,45 @@ pub(crate) struct Evaluator<'s, 'a> { /// reference dynamic (ngtsc overflows its stack on those). variables: RefCell, Option<(Value<'a>, u32)>>>, fuel: Cell, + /// Set once a string with a lone surrogate (`'\uD800'`) is read: it has + /// no `&str` form and evaluates as dynamic. + lone_surrogate: Cell, } impl<'s, 'a> Evaluator<'s, 'a> { pub(crate) fn new(consts: &'s super::StringConsts<'a>) -> Self { - Self { consts, variables: RefCell::default(), fuel: Cell::new(FUEL) } + Self { + consts, + variables: RefCell::default(), + fuel: Cell::new(FUEL), + lone_surrogate: Cell::new(false), + } + } + + /// Whether evaluating `expr` reads a string with a lone surrogate + /// (`'\uD800'`), directly or through a `const`. Such a string evaluates + /// as dynamic; a reader that takes a dynamic value as absent (an input's + /// `alias`) reports it instead, so a string the source gives is never + /// silently replaced. + pub(crate) fn reads_lone_surrogate( + consts: &'s super::StringConsts<'a>, + expr: &'a Expression<'a>, + ) -> bool { + // A fresh evaluator, whose cache can't hold the `const` already. + let evaluator = Evaluator::new(consts); + evaluator.evaluate(expr); + evaluator.lone_surrogate.get() + } + + /// A string value, or dynamic for one with a lone surrogate. + fn string(&self, value: JSStr<'_>) -> Value<'a> { + match value.as_str() { + Some(value) => Value::String(value.to_string()), + None => { + self.lone_surrogate.set(true); + Value::Dynamic + } + } } pub(crate) fn evaluate(&self, expr: &'a Expression<'a>) -> Value<'a> { @@ -1786,7 +1825,7 @@ impl<'s, 'a> Evaluator<'s, 'a> { Expression::NullLiteral(_) => Value::Null, Expression::BooleanLiteral(b) => Value::Bool(b.value), Expression::NumericLiteral(n) => Value::Number(n.value), - Expression::StringLiteral(s) => Value::String(s.value.to_string()), + Expression::StringLiteral(s) => self.string(s.value), Expression::TemplateLiteral(tpl) => self.template(tpl, depth, frame), Expression::Identifier(id) => self.identifier(id.name.as_str(), depth, frame), Expression::ParenthesizedExpression(e) => self.eval(&e.expression, depth, frame), @@ -1821,7 +1860,11 @@ impl<'s, 'a> Evaluator<'s, 'a> { fn template(&self, tpl: &'a TemplateLiteral<'a>, depth: u16, frame: &Frame<'a>) -> Value<'a> { let mut out = String::new(); for (i, quasi) in tpl.quasis.iter().enumerate() { - let Some(cooked) = &quasi.value.cooked else { return Value::Dynamic }; + let Some(cooked) = quasi.value.cooked else { return Value::Dynamic }; + let Some(cooked) = cooked.as_str() else { + self.lone_surrogate.set(true); + return Value::Dynamic; + }; out.push_str(cooked); if let Some(e) = tpl.expressions.get(i) { match literal(self.eval(e, depth, frame)) { @@ -2091,7 +2134,7 @@ impl<'s, 'a> Evaluator<'s, 'a> { } match key { PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()), - PropertyKey::StringLiteral(s) => Some(s.value.to_string()), + PropertyKey::StringLiteral(s) => s.value.as_str().map(str::to_string), PropertyKey::NumericLiteral(n) => Some(format_number_like_js(n.value)), _ => None, } @@ -2347,11 +2390,9 @@ impl<'s, 'a> Evaluator<'s, 'a> { ) -> Value<'a> { let name_of = |member: &'a TSEnumMemberName<'a>| match member { TSEnumMemberName::Identifier(id) => Some(id.name.as_str()), - TSEnumMemberName::String(s) | TSEnumMemberName::ComputedString(s) => { - Some(s.value.as_str()) - } + TSEnumMemberName::String(s) | TSEnumMemberName::ComputedString(s) => s.value.as_str(), TSEnumMemberName::ComputedTemplateString(t) => { - t.quasis.first().and_then(|q| q.value.cooked.as_ref()).map(oxc_str::Str::as_str) + t.quasis.first().and_then(|q| q.value.cooked?.as_str()) } }; // Inside the initializers, a member's name refers to the member. @@ -2396,12 +2437,12 @@ impl<'s, 'a> Evaluator<'s, 'a> { TSType::TSLiteralType(lit) => match &lit.literal { TSLiteral::BooleanLiteral(b) => Value::Bool(b.value), TSLiteral::NumericLiteral(n) => Value::Number(n.value), - TSLiteral::StringLiteral(s) => Value::String(s.value.to_string()), + TSLiteral::StringLiteral(s) => self.string(s.value), TSLiteral::TemplateLiteral(t) if t.expressions.is_empty() => t .quasis .first() - .and_then(|q| q.value.cooked.as_ref()) - .map_or(Value::Dynamic, |c| Value::String(c.to_string())), + .and_then(|q| q.value.cooked) + .map_or(Value::Dynamic, |c| self.string(c)), TSLiteral::UnaryExpression(u) => { match self.eval(&u.argument, depth, &Frame::at(block)) { Value::Dynamic => Value::Dynamic, @@ -2784,7 +2825,7 @@ pub(crate) fn transform_error<'a>( && el .property_key() .and_then(PropertyKey::static_name) - .is_some_and(|n| n == conflicting) + .is_some_and(|n| n == conflicting.as_str()) }) .then(|| { ( @@ -3610,7 +3651,9 @@ pub fn input_transform_types<'a>( ClassElement::MethodDefinition(m) => (&m.key, &m.decorators), _ => continue, }; - let Some(name) = key.static_name() else { continue }; + let Some(name) = key.static_name().and_then(StaticPropertyName::into_cow_str) else { + continue; + }; if let Some(options) = super::property_decorators::input_decorator_options(decorators, consts) && let Some(transform) = evaluator.evaluate(options).prop("transform") diff --git a/crates/oxc_angular_compiler/src/directive/mod.rs b/crates/oxc_angular_compiler/src/directive/mod.rs index 0ab2a0663..a0725e1ed 100644 --- a/crates/oxc_angular_compiler/src/directive/mod.rs +++ b/crates/oxc_angular_compiler/src/directive/mod.rs @@ -41,7 +41,9 @@ pub use decorator::{ }; pub(crate) use decorator::{merge_by_class_property, parse_decorator_io}; pub use definition::{DirectiveDefinitions, generate_directive_definitions}; -pub(crate) use dts_type::quote as ts_string_literal; +pub(crate) use dts_type::{ + quote as ts_string_literal, quote_literal as quote_string_literal, quote_units, +}; pub use evaluator::input_transform_types; #[cfg(feature = "cross_file_elision")] pub(crate) use evaluator::{Evaluator, ImportValueResolver, StaticValue}; diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index 97955ac9e..1066abc0b 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -296,7 +296,7 @@ pub(crate) fn member_name(element: &ClassElement<'_>) -> Option { } match key { PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()), - PropertyKey::StringLiteral(lit) => Some(lit.value.to_string()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(str::to_string), PropertyKey::PrivateIdentifier(id) => Some(format!("#{}", id.name)), _ => None, } @@ -391,13 +391,43 @@ pub(crate) fn literal_alias_error<'a>( Ok(None) } +/// The predicate for a string selector with a lone surrogate (`'\uD800'`), +/// which has no `&str`: the selector list the emitter builds from a string +/// (split on commas, each part trimmed), each part with its exact UTF-16 +/// escapes. +fn lone_surrogate_selectors<'a>( + allocator: &'a Allocator, + lit: &oxc_ast::ast::StringLiteral<'_>, +) -> QueryPredicate<'a> { + use crate::output::ast::{LiteralArrayExpr, RawSourceExpr}; + + let units: std::vec::Vec = lit.value.encode_utf16().collect(); + let is_space = |unit: &u16| char::from_u32(u32::from(*unit)).is_some_and(char::is_whitespace); + let mut entries = Vec::new_in(&allocator); + for part in units.split(|&unit| unit == u16::from(b',')) { + let start = part.iter().position(|unit| !is_space(unit)).unwrap_or(part.len()); + let end = part.iter().rposition(|unit| !is_space(unit)).map_or(start, |i| i + 1); + if start < end { + let source = allocator.alloc_str(&super::quote_units(part[start..end].iter().copied())); + entries.push(OutputExpression::RawSource(oxc_allocator::Box::new_in( + RawSourceExpr { source: Ident::from(&*source), source_span: None }, + &allocator, + ))); + } + } + QueryPredicate::Type(OutputExpression::LiteralArray(oxc_allocator::Box::new_in( + LiteralArrayExpr { entries, source_span: None }, + &allocator, + ))) +} + /// Get the property key name as an Atom. /// /// Handles both identifier keys and string literal keys. fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(lit) => Some(lit.value.clone().into()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => None, } } @@ -407,9 +437,9 @@ fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { /// Handles string literals and simple template literals (no expressions). fn extract_string_value<'a>(expr: &Expression<'a>) -> Option> { match expr { - Expression::StringLiteral(lit) => Some(lit.value.clone().into()), + Expression::StringLiteral(lit) => lit.value.as_str().map(Ident::from), Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => { - tpl.quasis.first().and_then(|q| q.value.cooked.clone().map(Into::into)) + tpl.quasis.first().and_then(|q| q.value.cooked?.as_str()).map(Ident::from) } _ => None, } @@ -557,7 +587,7 @@ fn parse_literal_input_config<'a>( match first_arg { // @Input('alias') Argument::StringLiteral(lit) => { - InputConfig { alias: Some(lit.value.clone().into()), ..Default::default() } + InputConfig { alias: lit.value.as_str().map(Ident::from), ..Default::default() } } // @Input({ alias: 'name', required: true, transform: fn }) @@ -936,7 +966,7 @@ fn parse_output_config<'a>( _ => None, }, None => match argument { - Expression::StringLiteral(lit) => Some(lit.value.clone().into()), + Expression::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => None, }, }; @@ -1163,11 +1193,18 @@ fn parse_query_config<'a>( config.is_forward_ref = unwrapped.is_some(); let node = unwrapped.unwrap_or(first_arg); config.predicate = match node { - Expression::StringLiteral(lit) => { - let mut list = Vec::new_in(&allocator); - list.push(lit.value.clone().into()); - Some(QueryPredicate::Selectors(list)) - } + Expression::StringLiteral(lit) => match lit.value.as_str() { + Some(selector) => { + let mut list = Vec::new_in(&allocator); + list.push(Ident::from(selector)); + Some(QueryPredicate::Selectors(list)) + } + None => { + // Like a selector, a string isn't wrapped in `forwardRef`. + config.is_forward_ref = false; + Some(lone_surrogate_selectors(allocator, lit)) + } + }, _ => convert_oxc_expression(allocator, node, source_text) .or_else(|| make_raw_source(allocator, source_text, node.span())) .map(QueryPredicate::Type), @@ -1281,11 +1318,14 @@ fn try_parse_signal_query<'a>( let mut is_forward_ref = false; let predicate = match predicate_arg { // String selector: viewChild('myRef') - Argument::StringLiteral(lit) => { - let mut selectors = oxc_allocator::Vec::new_in(&allocator); - selectors.push(lit.value.clone().into()); - QueryPredicate::Selectors(selectors) - } + Argument::StringLiteral(lit) => match lit.value.as_str() { + Some(selector) => { + let mut selectors = oxc_allocator::Vec::new_in(&allocator); + selectors.push(Ident::from(selector)); + QueryPredicate::Selectors(selectors) + } + None => lone_surrogate_selectors(allocator, lit), + }, // Type predicate: viewChild(TemplateRef) or viewChild(forwardRef(() => MyClass)) _ => { // ngtsc emits a non-string locator as written (`WrappedNodeExpr`); @@ -1819,7 +1859,7 @@ fn extract_host_binding_name<'a>(decorator: &'a Decorator<'a>) -> Option Some(lit.value.clone().into()), + Argument::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => { let expr = first_arg.to_expression(); extract_string_value(expr) @@ -1902,6 +1942,83 @@ pub(crate) fn extract_host_listeners_in<'a>( listeners } +/// ngtsc's error for a `@HostBinding` / `@HostListener` argument that is a +/// string with a lone surrogate (`'\uD800'`). It has no `&str`, so oxc can't +/// read it; rather than fall back (to the member name, no listener, no +/// arguments) it reports what ngtsc reports for an argument it can't read. +/// Upstream's other argument checks aren't ported. Like `extractHostBindings`, +/// the `@HostBinding`s come before the `@HostListener`s. +pub(crate) fn lone_surrogate_host_error<'a>( + class: &'a Class<'a>, + consts: &super::StringConsts<'a>, +) -> Option<(String, Span)> { + // The members `extract_host_bindings_in` / `extract_host_listeners_in` + // compile, with their decorator's call. + let members = |name: &'static str| { + class.body.body.iter().filter_map(move |element| { + let (decorators, key, is_static) = match element { + ClassElement::PropertyDefinition(prop) => { + (&prop.decorators, &prop.key, prop.r#static) + } + ClassElement::MethodDefinition(method) => { + (&method.decorators, &method.key, method.r#static) + } + ClassElement::AccessorProperty(prop) => { + (&prop.decorators, &prop.key, prop.r#static) + } + _ => return None, + }; + if is_static || get_property_key_name(key).is_none() { + return None; + } + let decorator = find_decorator_by_name(decorators, name, Some(consts))?; + let Expression::CallExpression(call) = &decorator.expression else { return None }; + Some((decorator, call)) + }) + }; + let suffix = Value::Dynamic.wrong_type_suffix(); + let is_lone = |arg: &Argument<'_>| arg.as_expression().is_some_and(is_lone_surrogate_literal); + for (decorator, call) in members("HostBinding") { + if call.arguments.first().is_some_and(is_lone) { + return Some(( + format!("@HostBinding's argument must be a string{suffix}"), + decorator.span, + )); + } + } + for (_, call) in members("HostListener") { + let Some(event_name) = call.arguments.first() else { continue }; + if is_lone(event_name) { + let message = format!("@HostListener's event name argument must be a string{suffix}"); + return Some((message, event_name.span())); + } + if let Some(Argument::ArrayExpression(args)) = call.arguments.get(1) + && let Some(position) = args + .elements + .iter() + .position(|elem| elem.as_expression().is_some_and(is_lone_surrogate_literal)) + { + let message = format!( + "Failed to resolve @HostListener.args at position {position} to a string{suffix}" + ); + return Some((message, args.span)); + } + } + None +} + +/// A string literal, or a template literal without substitutions, holding a +/// lone surrogate (`'\uD800'`). +fn is_lone_surrogate_literal(expr: &Expression<'_>) -> bool { + match expr { + Expression::StringLiteral(lit) => lit.value.has_lone_surrogate(), + Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => { + tpl.quasis.first().and_then(|q| q.value.cooked).is_some_and(|c| c.has_lone_surrogate()) + } + _ => false, + } +} + /// Parse the configuration from a @HostListener decorator. /// /// Returns the event name and list of argument expressions. @@ -1917,20 +2034,30 @@ fn parse_host_listener_config<'a>( // First argument: event name let event_name = call.arguments.first().and_then(|arg| match arg { - Argument::StringLiteral(lit) => Some(lit.value.clone().into()), + Argument::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => { let expr = arg.to_expression(); extract_string_value(expr) } }); - // Second argument: array of argument strings + // Second argument: array of argument strings. One with a lone surrogate + // (`'\uD800'`) can't be read; leaving out only that one would shift the + // later ones, so the whole list is left out, as for a second argument + // that isn't an array literal ([`lone_surrogate_host_error`] reports it). if let Some(second_arg) = call.arguments.get(1) { - if let Argument::ArrayExpression(arr) = second_arg { + if let Argument::ArrayExpression(arr) = second_arg + && !arr + .elements + .iter() + .any(|elem| elem.as_expression().is_some_and(is_lone_surrogate_literal)) + { for elem in &arr.elements { match elem { ArrayExpressionElement::StringLiteral(lit) => { - args.push(lit.value.clone().into()); + if let Some(arg) = lit.value.as_str() { + args.push(arg.into()); + } } ArrayExpressionElement::Elision(_) => {} _ => { @@ -2009,7 +2136,10 @@ fn reflect_object_literal<'a>( } let name = match &prop.key { PropertyKey::StaticIdentifier(id) => id.name.to_string(), - PropertyKey::StringLiteral(s) => s.value.to_string(), + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(name) => name.to_string(), + None => continue, + }, PropertyKey::NumericLiteral(n) => n.value.to_string(), _ => continue, }; @@ -2322,7 +2452,7 @@ fn decorator_query<'a>( return Err((format!("@{name} must have arguments"), span)); }; let unwrapped = try_unwrap_forward_ref(first, Some(consts)); - let is_forward_ref = unwrapped.is_some(); + let mut is_forward_ref = unwrapped.is_some(); let node = unwrapped.unwrap_or(first); let at_node = |message: String| (message, node.span()); let predicate = match evaluator.evaluate(node) { @@ -2330,12 +2460,21 @@ fn decorator_query<'a>( // evaluated is wrapped in a `WrappedNodeExpr` and emitted as written, // so the source text is the fallback for nodes the output AST can't // represent (`class Foo {}`, an import, ...). - Value::Reference { .. } | Value::Dynamic | Value::Function(_) => { - let expr = convert_oxc_expression(allocator, node, source_text) - .or_else(|| make_raw_source(allocator, source_text, node.span())) - .ok_or_else(|| at_node(format!("@{name} predicate cannot be interpreted")))?; - QueryPredicate::Type(expr) - } + Value::Reference { .. } | Value::Dynamic | Value::Function(_) => match node { + // A string with a lone surrogate (`'\uD800'`) has no `&str`, so + // the evaluator can't read it, but it is still a selector. Like + // one, it isn't wrapped in `forwardRef`. + Expression::StringLiteral(lit) if lit.value.has_lone_surrogate() => { + is_forward_ref = false; + lone_surrogate_selectors(allocator, lit) + } + _ => { + let expr = convert_oxc_expression(allocator, node, source_text) + .or_else(|| make_raw_source(allocator, source_text, node.span())) + .ok_or_else(|| at_node(format!("@{name} predicate cannot be interpreted")))?; + QueryPredicate::Type(expr) + } + }, Value::String(s) => { let mut selectors = Vec::new_in(&allocator); selectors.push(Ident::from(allocator.alloc_str(&s))); diff --git a/crates/oxc_angular_compiler/src/dts_inject.rs b/crates/oxc_angular_compiler/src/dts_inject.rs index dbb8578f4..46f2c349a 100644 --- a/crates/oxc_angular_compiler/src/dts_inject.rs +++ b/crates/oxc_angular_compiler/src/dts_inject.rs @@ -103,8 +103,12 @@ fn namespace_imports_of(program: &Program<'_>) -> FxHashMap { let Statement::ImportDeclaration(decl) = stmt else { continue }; let Some(specifiers) = &decl.specifiers else { continue }; for spec in specifiers { - if let ImportDeclarationSpecifier::ImportNamespaceSpecifier(ns) = spec { - aliases.insert(ns.local.name.to_string(), decl.source.value.to_string()); + // A source with a lone surrogate (`'\uD800'`) never matches a + // generated one (`build_import_map` leaves it unmapped). + if let ImportDeclarationSpecifier::ImportNamespaceSpecifier(ns) = spec + && let Some(source) = decl.source.value.as_str() + { + aliases.insert(ns.local.name.to_string(), source.to_string()); } } } @@ -223,7 +227,7 @@ fn property_key_name(key: &PropertyKey<'_>) -> Option { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()), _ => match key.as_expression() { - Some(Expression::StringLiteral(lit)) => Some(lit.value.to_string()), + Some(Expression::StringLiteral(lit)) => lit.value.as_str().map(str::to_string), Some(Expression::NumericLiteral(lit)) => Some(lit.value.to_string()), _ => None, }, diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index 5bec531b7..b923593a9 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -332,7 +332,7 @@ fn is_injectable_decorator(decorator: &Decorator<'_>) -> bool { fn get_property_key_name<'a>(key: &'a PropertyKey<'a>) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(s) => Some(s.value.clone().into()), + PropertyKey::StringLiteral(s) => s.value.as_str().map(Ident::from), _ => None, } } @@ -363,9 +363,9 @@ fn parse_provided_in_value<'a>( ) -> Option> { match expr { Expression::StringLiteral(s) => match s.value.as_str() { - "root" => Some(ProvidedInValue::Root), - "platform" => Some(ProvidedInValue::Platform), - "any" => Some(ProvidedInValue::Any), + Some("root") => Some(ProvidedInValue::Root), + Some("platform") => Some(ProvidedInValue::Platform), + Some("any") => Some(ProvidedInValue::Any), _ => None, }, Expression::NullLiteral(_) => Some(ProvidedInValue::None), @@ -625,8 +625,10 @@ fn extract_param_dependency<'a>( "Attribute" => { // @Attribute('attrName') - extract the attribute name if let Expression::CallExpression(call) = &decorator.expression { - if let Some(Argument::StringLiteral(s)) = call.arguments.first() { - attribute_name = Some(s.value.clone().into()); + if let Some(Argument::StringLiteral(s)) = call.arguments.first() + && let Some(name) = s.value.as_str() + { + attribute_name = Some(name.into()); } } } diff --git a/crates/oxc_angular_compiler/src/linker/mod.rs b/crates/oxc_angular_compiler/src/linker/mod.rs index 514401547..d85c51762 100644 --- a/crates/oxc_angular_compiler/src/linker/mod.rs +++ b/crates/oxc_angular_compiler/src/linker/mod.rs @@ -460,14 +460,32 @@ fn get_metadata_object<'a>(call: &'a CallExpression<'a>) -> Option<&'a ObjectExp /// Handles both regular string literals (`"..."`) and template literals with no expressions (`` `...` ``). fn as_static_string<'a>(expr: &'a Expression<'a>) -> Option<&'a str> { match expr { - Expression::StringLiteral(lit) => Some(lit.value.as_str()), + Expression::StringLiteral(lit) => lit.value.as_str(), Expression::TemplateLiteral(tl) if tl.expressions.is_empty() => { - tl.quasis.first()?.value.cooked.as_deref() + tl.quasis.first()?.value.cooked?.as_str() } _ => None, } } +/// A static string (see [`as_static_string`]) as a JS string literal: its +/// value quoted and escaped, or, for a value with a lone surrogate +/// (`'\uD800'`), which has no `&str`, the literal as written. +fn static_string_literal<'a>(expr: &'a Expression<'a>, source: &str) -> Option { + if let Some(value) = as_static_string(expr) { + return Some(crate::output::emitter::escape_string(value, false)); + } + let has_lone_surrogate = match expr { + Expression::StringLiteral(lit) => lit.value.has_lone_surrogate(), + Expression::TemplateLiteral(tl) if tl.expressions.is_empty() => { + tl.quasis.first().and_then(|q| q.value.cooked).is_some_and(|c| c.has_lone_surrogate()) + } + _ => false, + }; + has_lone_surrogate + .then(|| source[expr.span().start as usize..expr.span().end as usize].to_string()) +} + /// Extract a string property value from an object expression. fn get_string_property<'a>(obj: &'a ObjectExpression<'a>, name: &str) -> Option<&'a str> { obj.properties.iter().find_map(|prop| match prop { @@ -882,8 +900,15 @@ fn build_host_attrs(host_obj: &ObjectExpression<'_>, source: &str) -> String { for prop in &attr_obj.properties { if let ObjectPropertyKind::ObjectProperty(p) = prop { let key = match &p.key { - PropertyKey::StaticIdentifier(ident) => ident.name.to_string(), - PropertyKey::StringLiteral(s) => s.value.to_string(), + PropertyKey::StaticIdentifier(ident) => { + crate::output::emitter::escape_string(&ident.name, false) + } + // A lone surrogate (`'\uD800'`) has no `&str`: keep the key + // as written. + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(key) => crate::output::emitter::escape_string(key, false), + None => source[s.span.start as usize..s.span.end as usize].to_string(), + }, _ => continue, }; // Static strings (including template literals a bundler may have @@ -895,7 +920,7 @@ fn build_host_attrs(host_obj: &ObjectExpression<'_>, source: &str) -> String { }, |s| crate::output::emitter::escape_string(s, false), ); - attrs.push(crate::output::emitter::escape_string(&key, false)); + attrs.push(key); attrs.push(value); } } @@ -1377,9 +1402,20 @@ fn convert_inputs_to_definition_format(inputs_obj: &ObjectExpression<'_>, source for prop in &inputs_obj.properties { let ObjectPropertyKind::ObjectProperty(p) = prop else { continue }; - let key = match &p.key { - PropertyKey::StaticIdentifier(ident) => ident.name.to_string(), - PropertyKey::StringLiteral(s) => s.value.to_string(), + // The key for the output object, and the key as a string literal (the + // public and declared name when the object form leaves them out). + let (quoted_key, key_literal) = match &p.key { + PropertyKey::StaticIdentifier(ident) => { + (quote_key(&ident.name), crate::output::emitter::escape_string(&ident.name, false)) + } + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(key) => (quote_key(key), crate::output::emitter::escape_string(key, false)), + // A lone surrogate (`'\uD800'`) has no `&str`: use the key as written. + None => { + let key = source[s.span.start as usize..s.span.end as usize].to_string(); + (key.clone(), key) + } + }, _ => { // Fallback: use source text let span = p.span(); @@ -1388,15 +1424,10 @@ fn convert_inputs_to_definition_format(inputs_obj: &ObjectExpression<'_>, source } }; - let quoted_key = quote_key(&key); - // Simple string: propertyName: "publicName" → keep as is. The value may // arrive as a template literal when a bundler rewrote the string. - if let Some(public_name) = as_static_string(&p.value) { - entries.push(format!( - "{quoted_key}: {}", - crate::output::emitter::escape_string(public_name, false) - )); + if let Some(public_name) = static_string_literal(&p.value, source) { + entries.push(format!("{quoted_key}: {public_name}")); continue; } @@ -1409,7 +1440,7 @@ fn convert_inputs_to_definition_format(inputs_obj: &ObjectExpression<'_>, source .elements .first() .and_then(ArrayExpressionElement::as_expression) - .and_then(as_static_string) + .and_then(|first| static_string_literal(first, source)) .is_some(); if !first_is_string { @@ -1433,14 +1464,20 @@ fn convert_inputs_to_definition_format(inputs_obj: &ObjectExpression<'_>, source } // Object: Angular 16+ format with classPropertyName, publicName, isRequired, etc. Expression::ObjectExpression(obj) => { - let public_name = crate::output::emitter::escape_string( - get_string_property(obj, "publicName").unwrap_or(&key), - false, - ); - let declared_name = crate::output::emitter::escape_string( - get_string_property(obj, "classPropertyName").unwrap_or(&key), - false, - ); + // `get_string_property`, as a string literal. + let name_literal = |name: &str| { + obj.properties.iter().find_map(|prop| match prop { + ObjectPropertyKind::ObjectProperty(prop) + if matches!(&prop.key, PropertyKey::StaticIdentifier(ident) if ident.name == name) => + { + static_string_literal(&prop.value, source) + } + _ => None, + }) + }; + let public_name = name_literal("publicName").unwrap_or_else(|| key_literal.clone()); + let declared_name = + name_literal("classPropertyName").unwrap_or_else(|| key_literal.clone()); let is_signal = get_bool_property(obj, "isSignal").unwrap_or(false); let is_required = get_bool_property(obj, "isRequired").unwrap_or(false); // Angular emits `transformFunction: null` for signal inputs without @@ -1617,7 +1654,10 @@ fn extract_host_metadata_input( let ObjectPropertyKind::ObjectProperty(p) = prop else { continue }; let key = match &p.key { PropertyKey::StaticIdentifier(ident) => ident.name.to_string(), - PropertyKey::StringLiteral(s) => s.value.to_string(), + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(key) => key.to_string(), + None => continue, + }, _ => continue, }; let Some(value) = as_static_string(&p.value) else { continue }; @@ -1630,7 +1670,10 @@ fn extract_host_metadata_input( let ObjectPropertyKind::ObjectProperty(p) = prop else { continue }; let key = match &p.key { PropertyKey::StaticIdentifier(ident) => ident.name.to_string(), - PropertyKey::StringLiteral(s) => s.value.to_string(), + PropertyKey::StringLiteral(s) => match s.value.as_str() { + Some(key) => key.to_string(), + None => continue, + }, _ => continue, }; let Some(value) = as_static_string(&p.value) else { continue }; diff --git a/crates/oxc_angular_compiler/src/ng_module/decorator.rs b/crates/oxc_angular_compiler/src/ng_module/decorator.rs index 37575ad58..b62773803 100644 --- a/crates/oxc_angular_compiler/src/ng_module/decorator.rs +++ b/crates/oxc_angular_compiler/src/ng_module/decorator.rs @@ -337,7 +337,7 @@ fn is_ng_module_call(callee: &Expression<'_>) -> bool { fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(lit) => Some(lit.value.clone().into()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => None, } } @@ -345,9 +345,9 @@ fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { /// Extract a string value from an expression. fn extract_string_value<'a>(expr: &Expression<'a>) -> Option> { match expr { - Expression::StringLiteral(lit) => Some(lit.value.clone().into()), + Expression::StringLiteral(lit) => lit.value.as_str().map(Ident::from), Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => { - tpl.quasis.first().and_then(|q| q.value.cooked.clone().map(Into::into)) + tpl.quasis.first().and_then(|q| q.value.cooked?.as_str()).map(Ident::from) } _ => None, } @@ -502,8 +502,10 @@ fn extract_param_dependency<'a>( "Attribute" => { // @Attribute('attrName') - extract the attribute name if let Expression::CallExpression(call) = &decorator.expression { - if let Some(Argument::StringLiteral(s)) = call.arguments.first() { - attribute_name = Some(s.value.clone().into()); + if let Some(Argument::StringLiteral(s)) = call.arguments.first() + && let Some(name) = s.value.as_str() + { + attribute_name = Some(name.into()); } } } diff --git a/crates/oxc_angular_compiler/src/optimizer/elide_metadata.rs b/crates/oxc_angular_compiler/src/optimizer/elide_metadata.rs index b29778aff..115227aa0 100644 --- a/crates/oxc_angular_compiler/src/optimizer/elide_metadata.rs +++ b/crates/oxc_angular_compiler/src/optimizer/elide_metadata.rs @@ -184,7 +184,7 @@ impl ElideMetadataTransformer { // Computed member: i0["ɵsetClassMetadata"](...) Expression::ComputedMemberExpression(member) => { if let Expression::StringLiteral(lit) = &member.expression { - Some(lit.value.as_str()) + lit.value.as_str() } else { None } diff --git a/crates/oxc_angular_compiler/src/output/oxc_converter.rs b/crates/oxc_angular_compiler/src/output/oxc_converter.rs index fa72e7dd6..5fa5069f6 100644 --- a/crates/oxc_angular_compiler/src/output/oxc_converter.rs +++ b/crates/oxc_angular_compiler/src/output/oxc_converter.rs @@ -17,7 +17,7 @@ use oxc_ast::ast::{ PropertyKind, UnaryOperator as OxcUnaryOperator, }; use oxc_span::Span; -use oxc_str::Ident; +use oxc_str::{Ident, JSStr}; use super::ast::{ ArrowFunctionBody, ArrowFunctionExpr, BinaryOperator, BinaryOperatorExpr, CommaExpr, @@ -27,6 +27,7 @@ use super::ast::{ SpreadElementExpr, TemplateLiteralElement, TemplateLiteralExpr, TypeofExpr, UnaryOperator, UnaryOperatorExpr, VoidExpr, }; +use crate::directive::quote_string_literal; // ============================================================================ // Main Conversion Function @@ -68,13 +69,24 @@ pub fn convert_oxc_expression<'a>( &allocator, ))), - Expression::StringLiteral(lit) => Some(OutputExpression::Literal(Box::new_in( - LiteralExpr { - value: LiteralValue::String(lit.value.clone().into()), - source_span: None, - }, - &allocator, - ))), + Expression::StringLiteral(lit) => match lit.value.as_str() { + Some(value) => Some(OutputExpression::Literal(Box::new_in( + LiteralExpr { value: LiteralValue::String(value.into()), source_span: None }, + &allocator, + ))), + // A lone surrogate (`'\uD800'`) has no `&str`: keep the literal as + // written, or (no source text) print its exact UTF-16 escapes. + None => { + let source = match lit.raw { + Some(raw) => raw.into(), + None => Ident::from(allocator.alloc_str("e_string_literal(lit))), + }; + Some(OutputExpression::RawSource(Box::new_in( + RawSourceExpr { source, source_span: None }, + &allocator, + ))) + } + }, // Identifiers Expression::Identifier(id) => Some(OutputExpression::ReadVar(Box::new_in( @@ -307,13 +319,12 @@ fn convert_object_expression<'a>( ) -> Option> { // A `LiteralMap` entry is a plain `key: value` pair, so methods (`a() {}`), // accessors (`get a() {}`), computed keys (`[a]: 1`) and other key kinds - // cannot be represented. Keep such objects as written rather than emitting - // an altered object. - if obj - .properties - .iter() - .any(|prop| matches!(prop, ObjectPropertyKind::ObjectProperty(p) if !is_plain_property(p))) - { + // cannot be represented, nor can a key with a lone surrogate (`'\uD800'`). + // Keep such objects as written rather than emitting an altered object. + if obj.properties.iter().any(|prop| { + matches!(prop, ObjectPropertyKind::ObjectProperty(p) if !is_plain_property(p) + || matches!(&p.key, PropertyKey::StringLiteral(lit) if lit.value.has_lone_surrogate())) + }) { return make_raw_source(allocator, source_text, obj.span); } convert_plain_properties(allocator, obj, source_text) @@ -350,7 +361,12 @@ pub fn convert_plain_properties<'a>( } let (key, quoted) = match &p.key { PropertyKey::StaticIdentifier(id) => (id.name.clone().into(), false), - PropertyKey::StringLiteral(lit) => (lit.value.clone().into(), true), + // A key with a lone surrogate (`'\uD800'`) has no `&str` + // form and the emitter re-quotes keys, so leave it out. + PropertyKey::StringLiteral(lit) => match lit.value.as_str() { + Some(key) => (Ident::from(key), true), + None => continue, + }, PropertyKey::NumericLiteral(lit) => { (Ident::from(allocator.alloc_str(&lit.value.to_string())), true) } @@ -623,11 +639,9 @@ fn convert_template_literal<'a>( // Convert quasis to template literal elements let mut elements = OxcVec::with_capacity_in(tpl.quasis.len(), &allocator); for quasi in &tpl.quasis { - let text = quasi - .value - .cooked - .as_ref() - .map_or_else(|| quasi.value.raw.clone(), |cooked| cooked.clone()); + // A cooked value with a lone surrogate (`\uD800`) has no `&str`: use + // `raw`, as for an invalid escape. The emitter prints `raw_text` anyway. + let text = quasi.value.cooked.and_then(JSStr::as_arena_str).unwrap_or(quasi.value.raw); let raw_text = quasi.value.raw.clone(); elements.push(TemplateLiteralElement { @@ -1225,4 +1239,44 @@ mod tests { assert!(!raw.contains(": string"), "Should strip type annotations. Got: {raw}"); assert!(raw.contains("attach(x)") && raw.contains("[key]: 1"), "Got: {raw}"); } + + /// A string with a lone surrogate has no `&str`: the literal, and an + /// object keyed by one, are kept as written. + #[test] + fn test_lone_surrogate_strings_are_kept_as_written() { + let allocator = Allocator::default(); + let source = r"['\uD800', { '\uDC00': 1 }, 'ok']"; + let expr = parse_expression(&allocator, source); + let Some(OutputExpression::LiteralArray(array)) = + convert_oxc_expression(&allocator, &expr, Some(source)) + else { + panic!("Expected LiteralArray expression"); + }; + let entries: std::vec::Vec<_> = array + .entries + .iter() + .map(|entry| match entry { + OutputExpression::RawSource(raw) => format!("raw {}", raw.source), + OutputExpression::Literal(lit) => format!("literal {:?}", lit.value), + other => panic!("unexpected {other:?}"), + }) + .collect(); + assert_eq!(entries, [r"raw '\uD800'", r"raw { '\uDC00': 1 }", r#"literal String("ok")"#]); + } + + /// A synthesized literal (no source text) with a lone surrogate prints + /// its exact UTF-16 escapes. + #[test] + fn test_lone_surrogate_string_without_raw_is_escaped() { + let allocator = Allocator::default(); + let mut expr = parse_expression(&allocator, r"'a\uD800\u00e9'"); + let Expression::StringLiteral(lit) = &mut expr else { panic!("Expected StringLiteral") }; + lit.raw = None; + let Some(OutputExpression::RawSource(raw)) = + convert_oxc_expression(&allocator, &expr, None) + else { + panic!("Expected RawSource expression"); + }; + assert_eq!(raw.source.as_str(), r#""a\uD800\u00E9""#); + } } diff --git a/crates/oxc_angular_compiler/src/pipe/decorator.rs b/crates/oxc_angular_compiler/src/pipe/decorator.rs index 61bd42f83..4ee13e739 100644 --- a/crates/oxc_angular_compiler/src/pipe/decorator.rs +++ b/crates/oxc_angular_compiler/src/pipe/decorator.rs @@ -233,7 +233,7 @@ fn is_pipe_call(callee: &Expression<'_>) -> bool { fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(lit) => Some(lit.value.clone().into()), + PropertyKey::StringLiteral(lit) => lit.value.as_str().map(Ident::from), _ => None, } } @@ -241,9 +241,9 @@ fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option> { /// Extract a string value from an expression. fn extract_string_value<'a>(expr: &Expression<'a>) -> Option> { match expr { - Expression::StringLiteral(lit) => Some(lit.value.clone().into()), + Expression::StringLiteral(lit) => lit.value.as_str().map(Ident::from), Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => { - tpl.quasis.first().and_then(|q| q.value.cooked.clone().map(Into::into)) + tpl.quasis.first().and_then(|q| q.value.cooked?.as_str()).map(Ident::from) } _ => None, } @@ -325,8 +325,10 @@ fn extract_param_dependency<'a>( "Attribute" => { // @Attribute('attrName') - extract the attribute name if let Expression::CallExpression(call) = &decorator.expression { - if let Some(Argument::StringLiteral(s)) = call.arguments.first() { - attribute_name = Some(s.value.clone().into()); + if let Some(Argument::StringLiteral(s)) = call.arguments.first() + && let Some(name) = s.value.as_str() + { + attribute_name = Some(name.into()); } } } diff --git a/crates/oxc_angular_compiler/src/service/decorator.rs b/crates/oxc_angular_compiler/src/service/decorator.rs index c5a8840a7..c36d753fa 100644 --- a/crates/oxc_angular_compiler/src/service/decorator.rs +++ b/crates/oxc_angular_compiler/src/service/decorator.rs @@ -151,7 +151,7 @@ fn is_service_decorator(decorator: &Decorator<'_>) -> bool { fn get_property_key_name<'a>(key: &'a PropertyKey<'a>) -> Option> { match key { PropertyKey::StaticIdentifier(id) => Some(id.name.clone().into()), - PropertyKey::StringLiteral(s) => Some(s.value.clone().into()), + PropertyKey::StringLiteral(s) => s.value.as_str().map(Ident::from), _ => None, } } diff --git a/crates/oxc_angular_compiler/src/validation.rs b/crates/oxc_angular_compiler/src/validation.rs index 1989779d6..21716e427 100644 --- a/crates/oxc_angular_compiler/src/validation.rs +++ b/crates/oxc_angular_compiler/src/validation.rs @@ -34,7 +34,7 @@ const APIS: [InitializerApi; 8] = [ fn should_check(program: &Program<'_>) -> bool { program.body.iter().any(|stmt| { let Statement::ImportDeclaration(import) = stmt else { return false }; - let module = import.source.value.as_str(); + let Some(module) = import.source.value.as_str() else { return false }; if module != "@angular/core" && module != "@angular/core/rxjs-interop" { return false; } @@ -58,7 +58,7 @@ fn importing_module<'a>(declaration: &AstNode<'a>, semantic: &Semantic<'a>) -> O let AstKind::ImportDeclaration(import) = semantic.nodes().parent_kind(declaration.id()) else { return None; }; - Some(import.source.value.as_str()) + import.source.value.as_str() } /// The declaration `id`'s reference resolves to when it's the symbol's only diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index c52f33f6b..88cc64509 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -17644,3 +17644,120 @@ fn test_unnameable_members_emit_nothing() { ); } } + +#[test] +fn test_component_with_lone_surrogate_key_still_compiles() { + // A `@Component` key with a lone surrogate (`'\uD800'`) has no `&str` + // form and names no option: it is ignored like any unknown key, rather + // than dropping the whole component. + let result = expect_result( + "import { Component } from '@angular/core';\n\ + @Component({ selector: 'app-c', '\\uD800': 1, template: '

hi

' })\n\ + export class C {}", + ); + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); + assert!(result.code.contains("ɵɵdefineComponent"), "{}", result.code); + assert!(result.code.contains("selectors:[[\"app-c\"]]"), "{}", result.code); + // The metadata keeps the decorator's object as written. + assert!(result.code.contains(r"args:[{ selector: 'app-c', '\uD800': 1,"), "{}", result.code); +} + +#[test] +fn test_lone_surrogate_query_selector_kept_exact() { + // A string query predicate with a lone surrogate (`'\uD800'`) has no + // `&str` form: it still becomes a selector list (split on commas, each + // part trimmed), with exact escapes, not a lossy rendering or a bare + // string token. + let result = expect_result( + "import { Component, ViewChild, viewChild } from '@angular/core';\n\ + @Component({ selector: 'app-c', template: '' })\n\ + export class C {\n\ + @ViewChild('\\uD800') a: any;\n\ + b = viewChild(' x ,\\uDC00 ');\n\ + }", + ); + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); + assert!( + result.code.contains(r#"i0.ɵɵviewQuerySignal(ctx.b,["x","\uDC00"],5);"#), + "{}", + result.code + ); + assert!(result.code.contains(r#"i0.ɵɵviewQuery(["\uD800"],5);"#), "{}", result.code); +} + +#[test] +fn test_lone_surrogate_input_alias_is_diagnostic() { + // An `alias` with a lone surrogate (`'\uD800'`) has no `&str` form. It is + // present but unreadable, not absent: report it like the positional + // `@Input('\uD800')` instead of compiling the class property name. + let input_error = "@Input decorator argument must resolve to a string or an object \ + literal Value could not be determined statically."; + let inputs_error = "@Directive.inputs array can only contain strings or object literals \ + Value could not be determined statically."; + for (decorator, member, expected) in [ + ("", r"@Input('\uD800') value: any;", input_error), + ("", r"@Input({ alias: '\uD800', required: true }) value: any;", input_error), + ("", r"@Input({ alias: `\uD800` }) value: any;", input_error), + ("", "@Input({ alias: C }) value: any;", input_error), + ("", "@Input(OPTS) value: any;", input_error), + (r", inputs: [{ name: 'value', alias: '\uD800' }]", "value: any;", inputs_error), + ] { + let source = format!( + "import {{ Directive, Input }} from '@angular/core';\n\ + const C = '\\uD800';\n\ + const OPTS = {{ alias: '\\uD800' }};\n\ + @Directive({{ selector: '[d]'{decorator} }})\n\ + export class D {{\n {member}\n}}" + ); + assert_eq!(expect_diagnostics(&source), [expected], "`{decorator}` `{member}`"); + } + // A truly dynamic alias still reads as no alias, like ngtsc. + let diagnostics = expect_diagnostics( + "import { Directive, Input } from '@angular/core';\n\ + @Directive({ selector: '[d]' })\n\ + export class D {\n @Input({ alias: y }) value: any;\n}", + ); + assert!(diagnostics.is_empty(), "{diagnostics:?}"); +} + +#[test] +fn test_lone_surrogate_host_listener_argument_is_diagnostic() { + // A `@HostListener` argument with a lone surrogate (`'\uD800'`) can't be + // read. It is reported, and the call never shifts the later arguments + // into its place. + let result = expect_result( + "import { Directive, HostListener } from '@angular/core';\n\ + @Directive({ selector: '[d]' })\n\ + export class D {\n\ + @HostListener('click', ['$event.a', '\\uD800', '$event.c'])\n\ + onClick(a: any, b: any, c: any) {}\n\ + }", + ); + let messages: Vec = result.diagnostics.iter().map(|d| format!("{d}")).collect(); + assert_eq!( + messages, + ["Failed to resolve @HostListener.args at position 1 to a string Value could not be \ + determined statically."] + ); + assert!(!result.code.contains("ctx.onClick($event.a,$event.c)"), "{}", result.code); + assert!(result.code.contains("ctx.onClick()"), "{}", result.code); + + for (member, expected) in [ + ( + r"@HostBinding('\uD800') b = 1;", + "@HostBinding's argument must be a string Value could not be determined statically.", + ), + ( + r"@HostListener('\uD800') onX() {}", + "@HostListener's event name argument must be a string Value could not be \ + determined statically.", + ), + ] { + let source = format!( + "import {{ Directive, HostBinding, HostListener }} from '@angular/core';\n\ + @Directive({{ selector: '[d]' }})\n\ + export class D {{\n {member}\n}}" + ); + assert_eq!(expect_diagnostics(&source), [expected], "`{member}`"); + } +} diff --git a/crates/oxc_angular_compiler/tests/jit_signal_initializer_test.rs b/crates/oxc_angular_compiler/tests/jit_signal_initializer_test.rs index 7903ef844..12a68d6c5 100644 --- a/crates/oxc_angular_compiler/tests/jit_signal_initializer_test.rs +++ b/crates/oxc_angular_compiler/tests/jit_signal_initializer_test.rs @@ -415,3 +415,34 @@ fn synthesized_decorator_precedes_explicit_member_decorators() { "synthesized model() decorators should precede the explicit @HostBinding. Got:\n{model_out}" ); } + +#[test] +fn lone_surrogate_alias_is_rejected_like_aot() { + // An alias with a lone surrogate (`'\uD800'`) has no `&str` form: JIT + // rejects it with the AOT path's error rather than lowering a lossy alias. + for (field, imports) in [ + (r" readonly value = input(0, { alias: '\uD800' });", "input"), + (r" readonly ready = output({ alias: '\uDC00' });", "output"), + (r" readonly m = model.required({ alias: '\uDBFF' });", "model"), + ] { + let source = component(field, imports); + for jit in [true, false] { + let allocator = Allocator::default(); + let options = ComponentTransformOptions { jit, ..Default::default() }; + let result = transform_angular_file( + &allocator, + "test.component.ts", + &source, + Some(&options), + None, + ); + let messages: Vec = result.diagnostics.iter().map(|d| format!("{d}")).collect(); + assert_eq!( + messages, + ["Alias needs to be a string that is statically analyzable."], + "jit: {jit}, `{field}`\n{}", + result.code + ); + } + } +} diff --git a/crates/oxc_angular_compiler/tests/linker_test.rs b/crates/oxc_angular_compiler/tests/linker_test.rs index 7edef244e..b0598c00b 100644 --- a/crates/oxc_angular_compiler/tests/linker_test.rs +++ b/crates/oxc_angular_compiler/tests/linker_test.rs @@ -385,3 +385,26 @@ fn test_link_angular_version_keeps_legacy_safe_navigation() { assert!(!code.contains("ctx.item?.label"), "{version:?}:\n{code}"); } } + +/// Keys and values with a lone surrogate (`'\uD800'`) have no `&str`: the +/// linker keeps them as written instead of a lossy rendering. +#[test] +fn test_link_lone_surrogate_keys_kept_as_written() { + let allocator = Allocator::default(); + let code = r#"import * as i0 from "@angular/core"; +export class MyDir {} +MyDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.0", type: MyDir, selector: "[myDir]", inputs: { '\uD800': "pub", ok: '\uDC00', '\uD801': ["pub", "priv"], '\uD802': { classPropertyName: '\uD802', publicName: "p", isSignal: true }, '\uD803': { isRequired: true, isSignal: true }, named: { classPropertyName: "named", publicName: '\uDC01', isSignal: false } }, host: { attributes: { '\uD800': "v", title: '\uDBFF' } } });"#; + let result = link(&allocator, code, "test.mjs"); + assert!( + result.code.contains( + r#"inputs: { '\uD800': "pub", ok: '\uDC00', '\uD801': [0, "pub", "priv"], '\uD802': [1, "p", '\uD802'], '\uD803': [1, '\uD803', '\uD803'], named: [0, '\uDC01', "named"] }"# + ), + "inputs not kept as written:\n{}", + result.code + ); + assert!( + result.code.contains(r#"hostAttrs: ['\uD800', "v", "title", '\uDBFF']"#), + "hostAttrs not kept as written:\n{}", + result.code + ); +} diff --git a/napi/angular-compiler/e2e/compare/package.json b/napi/angular-compiler/e2e/compare/package.json index 6ddd7bb09..7816b8086 100644 --- a/napi/angular-compiler/e2e/compare/package.json +++ b/napi/angular-compiler/e2e/compare/package.json @@ -34,8 +34,8 @@ "@oxc-node/cli": "catalog:", "@oxc-node/core": "catalog:", "@types/node": "catalog:", - "oxc-parser": "^0.154.0", - "oxc-transform": "^0.154.0", + "oxc-parser": "^0.155.0", + "oxc-transform": "^0.155.0", "oxfmt": "catalog:", "typescript": "catalog:", "vitest": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd820ccab..35e4a2945 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -404,11 +404,11 @@ importers: specifier: 'catalog:' version: 24.1.0 oxc-parser: - specifier: ^0.154.0 - version: 0.154.0 + specifier: ^0.155.0 + version: 0.155.0 oxc-transform: - specifier: ^0.154.0 - version: 0.154.0 + specifier: ^0.155.0 + version: 0.155.0 oxfmt: specifier: 'catalog:' version: 0.73.0 @@ -1757,8 +1757,8 @@ packages: cpu: [arm] os: [android] - '@oxc-parser/binding-android-arm-eabi@0.154.0': - resolution: {integrity: sha512-sDdEtDl7vKdCvf54j72RJbowE3jCmyRz/+oOB6IAwQxAHZ0eI+Dcj4hvVnm0b9Dy31plUkdmfds9C+/Zt895Tg==} + '@oxc-parser/binding-android-arm-eabi@0.155.0': + resolution: {integrity: sha512-A6xv7hDtnTm+xlZ7utn6rRQpRMJHRhVr3gjRGqlx0RUX84mNk0pAsy6YN/wWhuOTkaZhRrrzaLxf0NfbH77j2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] @@ -1769,8 +1769,8 @@ packages: cpu: [arm64] os: [android] - '@oxc-parser/binding-android-arm64@0.154.0': - resolution: {integrity: sha512-UHrDPELzvWZoVkIDdSycj4bzcTMCJG3IbN++mSUgVBaUtNtWHjQ98U6Vz5bpF/1Ic3FYJyYhMr74huO5tVvpZw==} + '@oxc-parser/binding-android-arm64@0.155.0': + resolution: {integrity: sha512-T1wWwH9/JI5g7Z4kEQwDOwhToxdDhRyOTklH+uuNpJ16m3MZFmGl+l2rKc0NHQdbVRG2LTOBsNjpW3h1BWytPA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -1781,8 +1781,8 @@ packages: cpu: [arm64] os: [darwin] - '@oxc-parser/binding-darwin-arm64@0.154.0': - resolution: {integrity: sha512-mokYzajOSXxtSRDWCdHARGdntrKH+rZZ24Jzo8AtafyNzz0NARmpMm26Tc5B6qiGrr6UliWibHCOif1qRN2vaA==} + '@oxc-parser/binding-darwin-arm64@0.155.0': + resolution: {integrity: sha512-ZoRB3qJxJ8DWA41jHja+YeB0lLfjBy4T+Kr15vnN1k65X3zbJt3rPZftEwN4Ju4tmA4K/T9dWWD7Z8ewQLnrVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -1793,8 +1793,8 @@ packages: cpu: [x64] os: [darwin] - '@oxc-parser/binding-darwin-x64@0.154.0': - resolution: {integrity: sha512-WbtUBrclOVItWfLRSvs+Z/WLtXxJ2Ctv50PyuX894HzDN5/e0qzxVpTNPIVMzOerBhgKtFsSMcfxlYIAbvdVgg==} + '@oxc-parser/binding-darwin-x64@0.155.0': + resolution: {integrity: sha512-vxtL86BtM+IgrpejA86nbTk5j0HKTIMeuaJ1fQciw6p9RfavPtIPjI7gN34owap0yG9txXBaAWpGdxokNwdqIw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -1805,8 +1805,8 @@ packages: cpu: [x64] os: [freebsd] - '@oxc-parser/binding-freebsd-x64@0.154.0': - resolution: {integrity: sha512-YooHZSLawOe2fqm+lDdG8oAaQh2NYV/RMYTYzWRLy72stm2BgE9O/8Al6kFENjkmzyyBccDLLBirGxVtpzQfrw==} + '@oxc-parser/binding-freebsd-x64@0.155.0': + resolution: {integrity: sha512-tN6Ru5e0UVnWaIuiH8jEwnvxuxg7FvhWwSd3oe5oDhOD4ObsSr9/N6WfHLjilUWlPPj65ptwrSDH0Y7lzwTzmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1817,8 +1817,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-gnueabihf@0.154.0': - resolution: {integrity: sha512-hKOwhZbrXdTwc4bGVfbLCXl9f2swl6zWdISocLQSNX9MgPmwHE3qGzOUkCrkuGpwdCVHPnkfOwsqTuYsrP5uNg==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.155.0': + resolution: {integrity: sha512-h4ozA5n6L6lwolsz9iwunEhKWnIVbmFRots5QxEix0H6nJ5ICpuDa7PuShuhfpIDFe3+Qvyscioyiv+9g+4Syg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1829,8 +1829,8 @@ packages: cpu: [arm] os: [linux] - '@oxc-parser/binding-linux-arm-musleabihf@0.154.0': - resolution: {integrity: sha512-tnjiooXEU013349O/yxKbF5Cusp1CfHWQ8kYR8tGeRzR/P4sOpQGQo21fsS71M0fMD6ESLq+lbvXFWi43PuKeQ==} + '@oxc-parser/binding-linux-arm-musleabihf@0.155.0': + resolution: {integrity: sha512-058/+LardpfuUlsKIvBGk/CYoakEVcMgqYx8GgyUZeOxFWTojYLlCc1Gb60/y21v3esDhHidgiY/Zbq9LpBhaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -1842,8 +1842,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-arm64-gnu@0.154.0': - resolution: {integrity: sha512-Cao2kj+8d7BT6XJU1gV9eVQjV+Cr+e8vQm3tddDRZiM2hHrMDxwy3j6t+ofaSl0idyuPPCZ4AlpmQoj8yOT1QA==} + '@oxc-parser/binding-linux-arm64-gnu@0.155.0': + resolution: {integrity: sha512-nrOt2Y3xIAzu+lSPzH0864ieO+iCCXgoo9qFlGANE8Xh0cQ+RP8Wstfp+/wXN2YCyb98cAGzX0VruKYw7it/jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1856,8 +1856,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-arm64-musl@0.154.0': - resolution: {integrity: sha512-uiwA0V3q8llWA7/8r+a9BBjk4j5YMDmi6wfOsyQfGOJMQSLHEiZ3HI8ulea766ykUa9wJFWk9XWEl4uJrZLQqw==} + '@oxc-parser/binding-linux-arm64-musl@0.155.0': + resolution: {integrity: sha512-KYAayyN5hQbJPtGKtRxlyc8XYmyntOplyXSQzTDrmbz7l/GCrzQbSswvyJFBTH1LGtsZrbXOAfN52i/Kz6kLKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -1870,8 +1870,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-ppc64-gnu@0.154.0': - resolution: {integrity: sha512-LICfN0Xs4Zdko+bJy6eOAEQojDuIR26suF2JtOsMaXjgbCrpRSH7/NCvJhY13YvYBx12V6hnwrL8n0lk0rbNJQ==} + '@oxc-parser/binding-linux-ppc64-gnu@0.155.0': + resolution: {integrity: sha512-CSuSY5hfjtsBnHXydOj8jiNfS1qHbdPZuPz1YL1cSgK7VmNYP+QlZIbG0BBYYBgX/vYEgZwbG9mBdW4YairZ5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -1884,8 +1884,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-riscv64-gnu@0.154.0': - resolution: {integrity: sha512-gbbSYxLF8ssRbQ2qQPy+2EhGNwNH1bAoRdkk6cnWaKHqmCikpSIPtpXjFbCCtjTi0uojqOM3ghFQwQPbDQsU0Q==} + '@oxc-parser/binding-linux-riscv64-gnu@0.155.0': + resolution: {integrity: sha512-7KCfOQm+Nz2N8trAXbQkal4TQ5756WVEmhm35y816YHvyFnEfVsICsiV5k0pEszNf/fRm4cm2i6i18SJ6/iCWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -1898,8 +1898,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-riscv64-musl@0.154.0': - resolution: {integrity: sha512-nSnRYYDLCcRVjSQzF3g6rKqCNYwo5o+aQgnn8BBXfe+ofCgMGlGqkfRyvXm6ptZvoxNkkWtLlqM8W7Xc3j6ZnQ==} + '@oxc-parser/binding-linux-riscv64-musl@0.155.0': + resolution: {integrity: sha512-JSlhgoK+/g1TGJI5NuADx2+6syhxnQNLCWLXyO5QLFyetomcr/vIxSdiNlmyAv963YXUzCuwAwRG6npZ5xyJ1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] @@ -1912,8 +1912,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-s390x-gnu@0.154.0': - resolution: {integrity: sha512-BxBQoJKtzRQgZEKQIt6CDUm8RGKC5T8Qo4576yDa5/xgc10X3fBDItMbRabzLtbUBfTqKLl+8AVr/H6CF7W22g==} + '@oxc-parser/binding-linux-s390x-gnu@0.155.0': + resolution: {integrity: sha512-WFIX+dPokWp64mi434Md3U3lJaTwNxAphvGjpltoyQjGUsdXavOIRrnp9dOS34Hd75HOAwDxAI+YwWOwqmJbtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -1926,8 +1926,8 @@ packages: os: [linux] libc: [glibc] - '@oxc-parser/binding-linux-x64-gnu@0.154.0': - resolution: {integrity: sha512-/whkust2vYy+GjRUVL5cWaJmEu6jkdFfo5mkxgckyeOX61eMwo4iBoiwzeJZeMWAdzb/XOtQyLApHhFht3sOWA==} + '@oxc-parser/binding-linux-x64-gnu@0.155.0': + resolution: {integrity: sha512-g6xSDC/JpbnVOz5uafMGyK3txBVRFMQTx8opv/yHXGxqV3ZMVl3bf7W8XrSnF68dmy3gM4dgReNryuE53+WkeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1940,8 +1940,8 @@ packages: os: [linux] libc: [musl] - '@oxc-parser/binding-linux-x64-musl@0.154.0': - resolution: {integrity: sha512-EXURTyW9riw4B/kZnnE+58emAtw6+3VEszQfXUZkRqGaKIcmWDYkZtKB+xY/blHvuZ+Qy/fDclc31nmUCgtkSA==} + '@oxc-parser/binding-linux-x64-musl@0.155.0': + resolution: {integrity: sha512-6/4xlxKlZJWrQ0uwDFPc0H9+xLu7068CKhX3vIDW3SbvP/lVgnVuT5v6dyugeWskWpuxqF/WJ4WKwhxjU3pG2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -1953,8 +1953,8 @@ packages: cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-openharmony-arm64@0.154.0': - resolution: {integrity: sha512-ZRFooyH3qsR6vuR4jFGlG3XM36490SvvXtNaUahBK4zHpooMdTHNaOMtGGJI0EW38m5hi4rOco9kw7ZwCIp28w==} + '@oxc-parser/binding-openharmony-arm64@0.155.0': + resolution: {integrity: sha512-tltklL0J0XjDxIRTBXWOBhvc2mitcmES45pIxhwDWfxRyqz7Vkeu0NekTZEIxGE4f/lXgq3vZcjjffteQOVorg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -1965,8 +1965,8 @@ packages: cpu: [arm64] os: [win32] - '@oxc-parser/binding-win32-arm64-msvc@0.154.0': - resolution: {integrity: sha512-U0Vp+4lvwUa/PnOCJTZOjydj1rX2+VLHz4Y2vSFqlN8/7LTiURgD7z4uQ9yNv9O/NlU3mELBnPDlu7aaSYG1zQ==} + '@oxc-parser/binding-win32-arm64-msvc@0.155.0': + resolution: {integrity: sha512-alAb9TzjatUEsOeoncAziUnfdTDxfyswhalFZ9sC6plUgATIUx2NIZ0+bMn6aJ28AduS1OpG/OAisr/YISn4Ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -1977,8 +1977,8 @@ packages: cpu: [ia32] os: [win32] - '@oxc-parser/binding-win32-ia32-msvc@0.154.0': - resolution: {integrity: sha512-Hc8QgsFY5eUUP25we8TISUruibG2cDt0H4IcPG9RKdabb6odkCzIqjV6Gk9beGWGuOwMVCr81H/Su1q363LCCg==} + '@oxc-parser/binding-win32-ia32-msvc@0.155.0': + resolution: {integrity: sha512-yc5cbSfcGmRv/KpxDFuyk8adEBaWd/9HilWvBbQ4bFbus1V2PUh2mGB2Cfq9rqZTqLeiMT8x0skV+LKp9hOMgQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] @@ -1989,8 +1989,8 @@ packages: cpu: [x64] os: [win32] - '@oxc-parser/binding-win32-x64-msvc@0.154.0': - resolution: {integrity: sha512-hD7judkUMA/hZPoamvKADs6+an1smp8UTK0aMYRijkvi00K/1vl7vEtmyn2JyQ9S+vibtnfcNzxqTLGyJUZCRQ==} + '@oxc-parser/binding-win32-x64-msvc@0.155.0': + resolution: {integrity: sha512-8heq6jGq+rfpTDRDjoQiBHw0maQZf3IXkS99pdHQZPoTL5kyF5qyPS0RidUlSdnWDIqxXb5nux7GBD9cT+imUw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2004,127 +2004,127 @@ packages: '@oxc-project/types@0.152.0': resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==} - '@oxc-project/types@0.154.0': - resolution: {integrity: sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==} + '@oxc-project/types@0.155.0': + resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} - '@oxc-transform/binding-android-arm-eabi@0.154.0': - resolution: {integrity: sha512-Wqd1sJDGwaDPlO2BFdLD7+OMQR4T3Y9PaqS1D+kjjHPEJZ2lotY8cm67d6Otp0IaS7uKGU76S8aa5mHJwTvwXQ==} + '@oxc-transform/binding-android-arm-eabi@0.155.0': + resolution: {integrity: sha512-PO5e/2UnD5I7UyEyp6qBwjtFCFXVyOmVo+LQvSdwxGz2W72gLAa2aAxlPuiP1blrrp9lnVGBHjuKvuYhTcO8XA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-transform/binding-android-arm64@0.154.0': - resolution: {integrity: sha512-X236HnhL/WzibySNF8UA5Mk9sLtx8OWSKOhzpHDfmR0gLNK9Rhnr6bCO35+bdgiUWpBZEdxxe9/a5brRZzuMww==} + '@oxc-transform/binding-android-arm64@0.155.0': + resolution: {integrity: sha512-Kfj9jyhftDM+bvYlkxm6mJ2nVk8zxAoiab/I0Ecroh3RRTP9rgmGm7o+Z8N0DUm0Gb8LpkeYUdxLS9SfZOaQrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-transform/binding-darwin-arm64@0.154.0': - resolution: {integrity: sha512-CRewaU+ThBKqDthUOOrmwEu8XfPsnKRuwiO6a/OXMJbQ7l9/247N1WTxMMWZWBkJObK5pjd/oDAaGxvmJjTcWQ==} + '@oxc-transform/binding-darwin-arm64@0.155.0': + resolution: {integrity: sha512-x9YvbuXHrgcZTiXAary1Gs/45Yv+N7EYT1rY4nFLOd0ygFTV6etR3U2KkA6gys3/zrM67Fygk0dC1ZsZEswTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-transform/binding-darwin-x64@0.154.0': - resolution: {integrity: sha512-dQhIDr8+AtgIimzFX1XNEsnQfFGVH/aBGoR4u5HgCXu1B9ISAgbDr91N72ehN769eNrNnRUP18weNuNVzHL5kw==} + '@oxc-transform/binding-darwin-x64@0.155.0': + resolution: {integrity: sha512-bqqbHcEkuOUr0Nreoy/Lk7i2AxoGH2aRoS70zWzkJCu4ugxHpu1ysvqTMxcFSQYqK5AwpnJqFWhc1Nah2K2MoA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-transform/binding-freebsd-x64@0.154.0': - resolution: {integrity: sha512-QWEPzmEPPIzZ/y6k0vsiwcUqCtrOHuRNovmW6VeG4y07fBSfVp35uweGQrxoJL/XebkR19M4sIpnFTwcniV6pA==} + '@oxc-transform/binding-freebsd-x64@0.155.0': + resolution: {integrity: sha512-iIw3rWpfqRxvn+2t/6S1F2c6ur7sNSFJo9PyA9DUM+MlLwrcY1mohouUrPlEhiRRYJ6tqxjZn+5RDG1bkfHk2Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-transform/binding-linux-arm-gnueabihf@0.154.0': - resolution: {integrity: sha512-C+tHdgh6dDMgJMkxYSv+5pC8XtHySJx5G1POi5GmADZITPXhvZT6NDUkzToOs9Ja8BMYgejhOeK6G3++AwoF+A==} + '@oxc-transform/binding-linux-arm-gnueabihf@0.155.0': + resolution: {integrity: sha512-1uX81MobR6G4p6nnCgCqhjGhYM2nZJDOpLiS7HhfBiw38NyLSNRNZ3BYRlKLuCL/xJHWDSMzP1v7IvXuu45DAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-transform/binding-linux-arm-musleabihf@0.154.0': - resolution: {integrity: sha512-odNOaCqjIJUPM7h3y3+ZgBVZTbPWzFyuMSG2uRRIFB3If5q3d5d7hCK3/CMIeVy7g4JT8IRPoBDttYW/lX6cAg==} + '@oxc-transform/binding-linux-arm-musleabihf@0.155.0': + resolution: {integrity: sha512-npw8MbFrCRpuQ3lgIQqi88wk5rvvN0OBekjYWvWSLM3w10vsC3LRqCTL9euT4PCGdn0s8Jsrz5mh80jmtRqxJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-transform/binding-linux-arm64-gnu@0.154.0': - resolution: {integrity: sha512-clfcZaasfgkzqkrT2zwMDtLIUDoiu7f3tyoV5QFnlYKFQvJ5zA5tWhjacPZWKnRh7W9UjfILGo4W/Z/4zOXoCA==} + '@oxc-transform/binding-linux-arm64-gnu@0.155.0': + resolution: {integrity: sha512-sqLbGtgvKz/d1o6hebqO9pZ3iSEyrUd6inwmuhAF/baA3SmRvxa8vUNduCUYKEvJvYF4q1hzJjyTXWHcp0tZrg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-transform/binding-linux-arm64-musl@0.154.0': - resolution: {integrity: sha512-60PLk9egLY1QyZhuC8fEI5yuDSSch9BzQn8B7YYEwRtXtHnvk9U3Rjk7x9TvN+QeDLwPSDQm+nOGyKyuYFusUQ==} + '@oxc-transform/binding-linux-arm64-musl@0.155.0': + resolution: {integrity: sha512-DXnCgTIfyFNSs3Ut6dzwr1xRxx3N62+yWWioByUbb+XvBWv9xqZKniXTMMKbHxWHb40QrebEzW7ZZJncIkRZuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-transform/binding-linux-ppc64-gnu@0.154.0': - resolution: {integrity: sha512-Tvh6NamG/Qkg1V+X+o9zO38GGnbImAvtLR5eappfdImOh6cHHq9Lp7HLTY3LO9OTaiLEPejFo/hpAPMPxQ3SYw==} + '@oxc-transform/binding-linux-ppc64-gnu@0.155.0': + resolution: {integrity: sha512-ctdkG6AO0/nvTjl8P/3BHD0GQAzac+kzZI6Bop4qYjn1i0Klmow+DFKL8NT2AYTy0vy75ah5C6P7Ta8JUwXrMQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-transform/binding-linux-riscv64-gnu@0.154.0': - resolution: {integrity: sha512-RF0+/kQzlnvN0PPbWP1E2Zn6ayYztkPj97K/N9yLa1t+zWdfppBve8ww0lAszkd60J1akYW4TfUhQJiIYXVWGQ==} + '@oxc-transform/binding-linux-riscv64-gnu@0.155.0': + resolution: {integrity: sha512-P4TcKFEuDG9o5unpgpph7MDi3jQMGP4UwWlzrKS5A99mUlp33B0JKp6f93bcFw9kEqJNAGTaN9iKTLNTAfarmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-transform/binding-linux-riscv64-musl@0.154.0': - resolution: {integrity: sha512-vU0UF00U21tcIINr/3BKSNJCyrUigHjo/8bF5msZnZ3R61Yyh8sTHrunbw6sQIDYZykb9N9tHKlrt35N5iEhRQ==} + '@oxc-transform/binding-linux-riscv64-musl@0.155.0': + resolution: {integrity: sha512-aizvyWK/n5ba63wsc/ITfmuVsHdm1mbEe9QBUL51rRq3zu8QQBosfYuTmwFLydDp4TimQKecg/uUk30KAuxb/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-transform/binding-linux-s390x-gnu@0.154.0': - resolution: {integrity: sha512-1e8afsXnXzsgirRRhJx4pV7e2a9xfFMGT9wSNeCEuBCY7pFXE6t81Wp3K6gbIMkrHbdZLPzjvvudvrBcSUpZKA==} + '@oxc-transform/binding-linux-s390x-gnu@0.155.0': + resolution: {integrity: sha512-bDpXSaVVhhDtAzG+ezV7Mb8ibelPmEkD2HKBilX9YrPrrH2hHUAU1AfoCJwf4buH3R6TxsbBz/bJKkILUljzbA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-transform/binding-linux-x64-gnu@0.154.0': - resolution: {integrity: sha512-+k8nMWTqEPPwTt3pdNaH1Ek/HY5Cf7idDOUhoLkWErWiUBCGGpYND47xad+mbN1v0j7CaPP2qHWTsRhN8LReKw==} + '@oxc-transform/binding-linux-x64-gnu@0.155.0': + resolution: {integrity: sha512-c3gk2AkScRwzy4bAcQWrfC3d9kIDxD/DlXandowYkm7ySYhcF7Kcuao4ttMTD5AI6Z+qHaS9LqtBFUXSdNBPKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-transform/binding-linux-x64-musl@0.154.0': - resolution: {integrity: sha512-wSgC5avEHzEzdSNAO4E7Sq3JppVJCLYgSK1IOuS4f6VOU5eui+HtMdQIeRYsKTc+vANG5YaQ+0c0dHuEo4/kZQ==} + '@oxc-transform/binding-linux-x64-musl@0.155.0': + resolution: {integrity: sha512-ovOy1wIccACAdLDzDDIos/6uDr9xjCYCt1w08R2H1IqRqNiEeAlPwkKVDvo+1H5bwS8lTNj31QjuRHwB14/l9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-transform/binding-openharmony-arm64@0.154.0': - resolution: {integrity: sha512-4kyIYf1EUdMa2x1PebyHNQF+BVkSWBd2fniMubBZZsPCUwtKv89wOzIjQ7EJLrvWFYWLIa4KdUNoKbuvunIJLQ==} + '@oxc-transform/binding-openharmony-arm64@0.155.0': + resolution: {integrity: sha512-/MLJ/zHv9CZQuQFLYydu47iesobb46xcxDfKhqPl9294uDWEgTkSrDslaLwBn5DReDMWPcntKRqc8Ej/3sVGRg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-transform/binding-win32-arm64-msvc@0.154.0': - resolution: {integrity: sha512-ftqAPU2DcYT9bonTogmNIOFDoILnCehdL2DFz8OCY9+z0LZaPY1F4v/Es0VE22m8++C/hbuNPVHpTYaUPNMrNw==} + '@oxc-transform/binding-win32-arm64-msvc@0.155.0': + resolution: {integrity: sha512-YkMbvDXpU3C+8CCIimK2fEgrjX+2lzrb9WIHCf97azMCWPJ9ieZzMuZwxy/o/3crcQEs3SnXr+vQZIi8hbo4NA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-transform/binding-win32-ia32-msvc@0.154.0': - resolution: {integrity: sha512-NdX4YzkjY6zkq/61WZYfdTBu5d+LsuQLmJf/P3wNPgTmfHeO9V7K7CHrfBrU7VbwtA26qM28+zr29LXPhEbGEw==} + '@oxc-transform/binding-win32-ia32-msvc@0.155.0': + resolution: {integrity: sha512-LTvYdFdECJJZ4UHHNH4puVqWL371WLP6N35ChxRb6PnuN6IKXGpsqaoWXDIaS4+GQ3JoqViJIUhWbZl+Jjtfxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-transform/binding-win32-x64-msvc@0.154.0': - resolution: {integrity: sha512-gdkSxCHFdO3w/QPmooRORWAr/HxIwAKSnnN4QLPnf2cR2156Ux5oYLU63u2PnE64402KtX2pBhr+Psgq/peDxg==} + '@oxc-transform/binding-win32-x64-msvc@0.155.0': + resolution: {integrity: sha512-jndw3L1I1LMRpCvZJwUMwqbyDelPALyuc4g6Yq39kDvR8Hto5CA0XI6ZW+aiyRkTt/JsLSLnkJtflYdou7DeOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4124,12 +4124,12 @@ packages: resolution: {integrity: sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-parser@0.154.0: - resolution: {integrity: sha512-Gr/6fkjZwi937c4Tsz8y0iO4U0FLPkZI6JSVucLAIvQyqh379px7RS8ORIc0HOORbFjXZ6zyqWLl53qxhSeofw==} + oxc-parser@0.155.0: + resolution: {integrity: sha512-C/ReSf1JUFukK/zvk6jhcNibSssvas5+ktIzK7fbKVd3CedWe59C3QwHEOrEZj/4Rt9+xg0Jeis/Jv1F/KM1mw==} engines: {node: ^20.19.0 || >=22.12.0} - oxc-transform@0.154.0: - resolution: {integrity: sha512-f1i5S4wm+a5t6baY5mw/+LFLt95VpOwwcU56TlSGfd23c5DstR2k9RKj0vv16/7/ra+ptU09D83sNOL4fza1NA==} + oxc-transform@0.155.0: + resolution: {integrity: sha512-Z1qL0TaO06hqZjx/4UXeyspxBiyfNGR8r8vBn48MlIUWWPdTry0DCgo/1AOsvVnS6KgJieok/yfdCSCHia+ojA==} engines: {node: ^20.19.0 || >=22.12.0} oxfmt@0.73.0: @@ -5905,115 +5905,115 @@ snapshots: '@oxc-parser/binding-android-arm-eabi@0.150.0': optional: true - '@oxc-parser/binding-android-arm-eabi@0.154.0': + '@oxc-parser/binding-android-arm-eabi@0.155.0': optional: true '@oxc-parser/binding-android-arm64@0.150.0': optional: true - '@oxc-parser/binding-android-arm64@0.154.0': + '@oxc-parser/binding-android-arm64@0.155.0': optional: true '@oxc-parser/binding-darwin-arm64@0.150.0': optional: true - '@oxc-parser/binding-darwin-arm64@0.154.0': + '@oxc-parser/binding-darwin-arm64@0.155.0': optional: true '@oxc-parser/binding-darwin-x64@0.150.0': optional: true - '@oxc-parser/binding-darwin-x64@0.154.0': + '@oxc-parser/binding-darwin-x64@0.155.0': optional: true '@oxc-parser/binding-freebsd-x64@0.150.0': optional: true - '@oxc-parser/binding-freebsd-x64@0.154.0': + '@oxc-parser/binding-freebsd-x64@0.155.0': optional: true '@oxc-parser/binding-linux-arm-gnueabihf@0.150.0': optional: true - '@oxc-parser/binding-linux-arm-gnueabihf@0.154.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.155.0': optional: true '@oxc-parser/binding-linux-arm-musleabihf@0.150.0': optional: true - '@oxc-parser/binding-linux-arm-musleabihf@0.154.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.155.0': optional: true '@oxc-parser/binding-linux-arm64-gnu@0.150.0': optional: true - '@oxc-parser/binding-linux-arm64-gnu@0.154.0': + '@oxc-parser/binding-linux-arm64-gnu@0.155.0': optional: true '@oxc-parser/binding-linux-arm64-musl@0.150.0': optional: true - '@oxc-parser/binding-linux-arm64-musl@0.154.0': + '@oxc-parser/binding-linux-arm64-musl@0.155.0': optional: true '@oxc-parser/binding-linux-ppc64-gnu@0.150.0': optional: true - '@oxc-parser/binding-linux-ppc64-gnu@0.154.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.155.0': optional: true '@oxc-parser/binding-linux-riscv64-gnu@0.150.0': optional: true - '@oxc-parser/binding-linux-riscv64-gnu@0.154.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.155.0': optional: true '@oxc-parser/binding-linux-riscv64-musl@0.150.0': optional: true - '@oxc-parser/binding-linux-riscv64-musl@0.154.0': + '@oxc-parser/binding-linux-riscv64-musl@0.155.0': optional: true '@oxc-parser/binding-linux-s390x-gnu@0.150.0': optional: true - '@oxc-parser/binding-linux-s390x-gnu@0.154.0': + '@oxc-parser/binding-linux-s390x-gnu@0.155.0': optional: true '@oxc-parser/binding-linux-x64-gnu@0.150.0': optional: true - '@oxc-parser/binding-linux-x64-gnu@0.154.0': + '@oxc-parser/binding-linux-x64-gnu@0.155.0': optional: true '@oxc-parser/binding-linux-x64-musl@0.150.0': optional: true - '@oxc-parser/binding-linux-x64-musl@0.154.0': + '@oxc-parser/binding-linux-x64-musl@0.155.0': optional: true '@oxc-parser/binding-openharmony-arm64@0.150.0': optional: true - '@oxc-parser/binding-openharmony-arm64@0.154.0': + '@oxc-parser/binding-openharmony-arm64@0.155.0': optional: true '@oxc-parser/binding-win32-arm64-msvc@0.150.0': optional: true - '@oxc-parser/binding-win32-arm64-msvc@0.154.0': + '@oxc-parser/binding-win32-arm64-msvc@0.155.0': optional: true '@oxc-parser/binding-win32-ia32-msvc@0.150.0': optional: true - '@oxc-parser/binding-win32-ia32-msvc@0.154.0': + '@oxc-parser/binding-win32-ia32-msvc@0.155.0': optional: true '@oxc-parser/binding-win32-x64-msvc@0.150.0': optional: true - '@oxc-parser/binding-win32-x64-msvc@0.154.0': + '@oxc-parser/binding-win32-x64-msvc@0.155.0': optional: true '@oxc-project/types@0.149.0': {} @@ -6022,63 +6022,63 @@ snapshots: '@oxc-project/types@0.152.0': {} - '@oxc-project/types@0.154.0': {} + '@oxc-project/types@0.155.0': {} - '@oxc-transform/binding-android-arm-eabi@0.154.0': + '@oxc-transform/binding-android-arm-eabi@0.155.0': optional: true - '@oxc-transform/binding-android-arm64@0.154.0': + '@oxc-transform/binding-android-arm64@0.155.0': optional: true - '@oxc-transform/binding-darwin-arm64@0.154.0': + '@oxc-transform/binding-darwin-arm64@0.155.0': optional: true - '@oxc-transform/binding-darwin-x64@0.154.0': + '@oxc-transform/binding-darwin-x64@0.155.0': optional: true - '@oxc-transform/binding-freebsd-x64@0.154.0': + '@oxc-transform/binding-freebsd-x64@0.155.0': optional: true - '@oxc-transform/binding-linux-arm-gnueabihf@0.154.0': + '@oxc-transform/binding-linux-arm-gnueabihf@0.155.0': optional: true - '@oxc-transform/binding-linux-arm-musleabihf@0.154.0': + '@oxc-transform/binding-linux-arm-musleabihf@0.155.0': optional: true - '@oxc-transform/binding-linux-arm64-gnu@0.154.0': + '@oxc-transform/binding-linux-arm64-gnu@0.155.0': optional: true - '@oxc-transform/binding-linux-arm64-musl@0.154.0': + '@oxc-transform/binding-linux-arm64-musl@0.155.0': optional: true - '@oxc-transform/binding-linux-ppc64-gnu@0.154.0': + '@oxc-transform/binding-linux-ppc64-gnu@0.155.0': optional: true - '@oxc-transform/binding-linux-riscv64-gnu@0.154.0': + '@oxc-transform/binding-linux-riscv64-gnu@0.155.0': optional: true - '@oxc-transform/binding-linux-riscv64-musl@0.154.0': + '@oxc-transform/binding-linux-riscv64-musl@0.155.0': optional: true - '@oxc-transform/binding-linux-s390x-gnu@0.154.0': + '@oxc-transform/binding-linux-s390x-gnu@0.155.0': optional: true - '@oxc-transform/binding-linux-x64-gnu@0.154.0': + '@oxc-transform/binding-linux-x64-gnu@0.155.0': optional: true - '@oxc-transform/binding-linux-x64-musl@0.154.0': + '@oxc-transform/binding-linux-x64-musl@0.155.0': optional: true - '@oxc-transform/binding-openharmony-arm64@0.154.0': + '@oxc-transform/binding-openharmony-arm64@0.155.0': optional: true - '@oxc-transform/binding-win32-arm64-msvc@0.154.0': + '@oxc-transform/binding-win32-arm64-msvc@0.155.0': optional: true - '@oxc-transform/binding-win32-ia32-msvc@0.154.0': + '@oxc-transform/binding-win32-ia32-msvc@0.155.0': optional: true - '@oxc-transform/binding-win32-x64-msvc@0.154.0': + '@oxc-transform/binding-win32-x64-msvc@0.155.0': optional: true '@oxfmt/binding-android-arm-eabi@0.73.0': @@ -7620,51 +7620,51 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.150.0 '@oxc-parser/binding-win32-x64-msvc': 0.150.0 - oxc-parser@0.154.0: + oxc-parser@0.155.0: dependencies: - '@oxc-project/types': 0.154.0 + '@oxc-project/types': 0.155.0 optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.154.0 - '@oxc-parser/binding-android-arm64': 0.154.0 - '@oxc-parser/binding-darwin-arm64': 0.154.0 - '@oxc-parser/binding-darwin-x64': 0.154.0 - '@oxc-parser/binding-freebsd-x64': 0.154.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.154.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.154.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.154.0 - '@oxc-parser/binding-linux-arm64-musl': 0.154.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.154.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.154.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.154.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.154.0 - '@oxc-parser/binding-linux-x64-gnu': 0.154.0 - '@oxc-parser/binding-linux-x64-musl': 0.154.0 - '@oxc-parser/binding-openharmony-arm64': 0.154.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.154.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.154.0 - '@oxc-parser/binding-win32-x64-msvc': 0.154.0 - - oxc-transform@0.154.0: + '@oxc-parser/binding-android-arm-eabi': 0.155.0 + '@oxc-parser/binding-android-arm64': 0.155.0 + '@oxc-parser/binding-darwin-arm64': 0.155.0 + '@oxc-parser/binding-darwin-x64': 0.155.0 + '@oxc-parser/binding-freebsd-x64': 0.155.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.155.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.155.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.155.0 + '@oxc-parser/binding-linux-arm64-musl': 0.155.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.155.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.155.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.155.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.155.0 + '@oxc-parser/binding-linux-x64-gnu': 0.155.0 + '@oxc-parser/binding-linux-x64-musl': 0.155.0 + '@oxc-parser/binding-openharmony-arm64': 0.155.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.155.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.155.0 + '@oxc-parser/binding-win32-x64-msvc': 0.155.0 + + oxc-transform@0.155.0: optionalDependencies: - '@oxc-transform/binding-android-arm-eabi': 0.154.0 - '@oxc-transform/binding-android-arm64': 0.154.0 - '@oxc-transform/binding-darwin-arm64': 0.154.0 - '@oxc-transform/binding-darwin-x64': 0.154.0 - '@oxc-transform/binding-freebsd-x64': 0.154.0 - '@oxc-transform/binding-linux-arm-gnueabihf': 0.154.0 - '@oxc-transform/binding-linux-arm-musleabihf': 0.154.0 - '@oxc-transform/binding-linux-arm64-gnu': 0.154.0 - '@oxc-transform/binding-linux-arm64-musl': 0.154.0 - '@oxc-transform/binding-linux-ppc64-gnu': 0.154.0 - '@oxc-transform/binding-linux-riscv64-gnu': 0.154.0 - '@oxc-transform/binding-linux-riscv64-musl': 0.154.0 - '@oxc-transform/binding-linux-s390x-gnu': 0.154.0 - '@oxc-transform/binding-linux-x64-gnu': 0.154.0 - '@oxc-transform/binding-linux-x64-musl': 0.154.0 - '@oxc-transform/binding-openharmony-arm64': 0.154.0 - '@oxc-transform/binding-win32-arm64-msvc': 0.154.0 - '@oxc-transform/binding-win32-ia32-msvc': 0.154.0 - '@oxc-transform/binding-win32-x64-msvc': 0.154.0 + '@oxc-transform/binding-android-arm-eabi': 0.155.0 + '@oxc-transform/binding-android-arm64': 0.155.0 + '@oxc-transform/binding-darwin-arm64': 0.155.0 + '@oxc-transform/binding-darwin-x64': 0.155.0 + '@oxc-transform/binding-freebsd-x64': 0.155.0 + '@oxc-transform/binding-linux-arm-gnueabihf': 0.155.0 + '@oxc-transform/binding-linux-arm-musleabihf': 0.155.0 + '@oxc-transform/binding-linux-arm64-gnu': 0.155.0 + '@oxc-transform/binding-linux-arm64-musl': 0.155.0 + '@oxc-transform/binding-linux-ppc64-gnu': 0.155.0 + '@oxc-transform/binding-linux-riscv64-gnu': 0.155.0 + '@oxc-transform/binding-linux-riscv64-musl': 0.155.0 + '@oxc-transform/binding-linux-s390x-gnu': 0.155.0 + '@oxc-transform/binding-linux-x64-gnu': 0.155.0 + '@oxc-transform/binding-linux-x64-musl': 0.155.0 + '@oxc-transform/binding-openharmony-arm64': 0.155.0 + '@oxc-transform/binding-win32-arm64-msvc': 0.155.0 + '@oxc-transform/binding-win32-ia32-msvc': 0.155.0 + '@oxc-transform/binding-win32-x64-msvc': 0.155.0 oxfmt@0.73.0: dependencies: