Skip to content

Commit b024dce

Browse files
Brooooooklynclaude
andauthored
fix(angular): resolve e2e tests comparison mismatches (#58)
fix(angular): resolve e2e comparison mismatches Fix multiple compiler output divergences from Angular TS: - fix(variable_optimization): handle Oxc's split handler_expression architecture in optimizeSaveRestoreView, removing unnecessary restoreView/resetView wrapping - fix(variable_optimization): reorder optimization steps to match Angular TS (arrow functions and listener handlers before create/update ops) - fix(variable_optimization): include Animation handlers in save/restore optimization - fix(emitter): emit non-ASCII characters as raw UTF-8 instead of \uNNNN escapes - fix(ordering): add OpKind::Control to update op ordering phase (priority 8, last) - fix(reify): add missing name string literal as second argument to ɵɵcontrol() - fix(emit): emit host binding pool constants (pure functions) alongside template declarations, matching Angular TS's shared ConstantPool behavior - fix(entities): use greedy &-to-; matching in decode_entities_in_string to replicate Angular TS's /&([^;]+);/g regex behavior Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 39174a8 commit b024dce

File tree

16 files changed

+809
-188
lines changed

16 files changed

+809
-188
lines changed

crates/oxc_angular_compiler/src/component/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ pub use metadata::{
3535
pub use namespace_registry::NamespaceRegistry;
3636
pub use transform::{
3737
CompiledComponent, HmrTemplateCompileOutput, HostMetadataInput, ImportInfo, ImportMap,
38-
LinkerTemplateOutput, ResolvedResources, TemplateCompileOutput, TransformOptions,
39-
TransformResult, build_import_map, compile_component_template, compile_for_hmr,
40-
compile_host_bindings_for_linker, compile_template_for_hmr, compile_template_for_linker,
41-
compile_template_to_js, compile_template_to_js_with_options, transform_angular_file,
38+
LinkerHostBindingOutput, LinkerTemplateOutput, ResolvedResources, TemplateCompileOutput,
39+
TransformOptions, TransformResult, build_import_map, compile_component_template,
40+
compile_for_hmr, compile_host_bindings_for_linker, compile_template_for_hmr,
41+
compile_template_for_linker, compile_template_to_js, compile_template_to_js_with_options,
42+
transform_angular_file,
4243
};

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,10 +1569,20 @@ fn compile_component_full<'a>(
15691569
compile_component_host_bindings(allocator, metadata, template_pool_index);
15701570

15711571
// Extract the result and update pool index if host bindings were compiled
1572-
let (host_binding_result, host_binding_next_pool_index) = match host_binding_output {
1573-
Some(output) => (Some(output.result), Some(output.next_pool_index)),
1574-
None => (None, None),
1575-
};
1572+
let (host_binding_result, host_binding_next_pool_index, host_binding_declarations) =
1573+
match host_binding_output {
1574+
Some(output) => {
1575+
let declarations = output.result.declarations;
1576+
let result = HostBindingCompilationResult {
1577+
host_binding_fn: output.result.host_binding_fn,
1578+
host_attrs: output.result.host_attrs,
1579+
host_vars: output.result.host_vars,
1580+
declarations: OxcVec::new_in(allocator),
1581+
};
1582+
(Some(result), Some(output.next_pool_index), declarations)
1583+
}
1584+
None => (None, None, OxcVec::new_in(allocator)),
1585+
};
15761586

15771587
// Stage 7: Generate ɵcmp/ɵfac definitions
15781588
// The namespace registry is shared across all components in the file to ensure
@@ -1599,6 +1609,11 @@ fn compile_component_full<'a>(
15991609
declarations_js.push_str(&emitter.emit_statement(decl));
16001610
declarations_js.push('\n');
16011611
}
1612+
// Emit host binding declarations (pooled constants like pure functions)
1613+
for decl in host_binding_declarations.iter() {
1614+
declarations_js.push_str(&emitter.emit_statement(decl));
1615+
declarations_js.push('\n');
1616+
}
16021617

16031618
// For HMR, we emit the template separately using compile_template_to_js
16041619
// The ɵcmp already contains the template function inline
@@ -1972,6 +1987,11 @@ pub fn compile_template_to_js_with_options<'a>(
19721987
component_name,
19731988
options.selector.as_deref(),
19741989
) {
1990+
// Add host binding pool declarations (pure functions, etc.)
1991+
for decl in host_result.declarations {
1992+
all_statements.push(decl);
1993+
}
1994+
19751995
// Add the host bindings function as a declaration if present
19761996
if let Some(host_fn) = host_result.host_binding_fn {
19771997
if let Some(fn_name) = host_fn.name.clone() {
@@ -2556,6 +2576,16 @@ fn compile_host_bindings_from_input<'a>(
25562576
Some(result)
25572577
}
25582578

2579+
/// Result of compiling host bindings for the linker.
2580+
pub struct LinkerHostBindingOutput {
2581+
/// The host binding function as JS.
2582+
pub fn_js: String,
2583+
/// Number of host variables.
2584+
pub host_vars: u32,
2585+
/// Pool constant declarations (pure functions, etc.) as JS.
2586+
pub declarations_js: String,
2587+
}
2588+
25592589
/// Compile host bindings for the linker, returning the emitted JS function + hostVars count.
25602590
///
25612591
/// This takes host property/listener data extracted from a partial declaration and compiles
@@ -2566,7 +2596,7 @@ pub fn compile_host_bindings_for_linker(
25662596
host_input: &HostMetadataInput,
25672597
component_name: &str,
25682598
selector: Option<&str>,
2569-
) -> Option<(String, u32)> {
2599+
) -> Option<LinkerHostBindingOutput> {
25702600
let allocator = Allocator::default();
25712601
let result =
25722602
compile_host_bindings_from_input(&allocator, host_input, component_name, selector)?;
@@ -2575,12 +2605,19 @@ pub fn compile_host_bindings_for_linker(
25752605

25762606
let host_vars = result.host_vars.unwrap_or(0);
25772607

2608+
// Emit host binding pool declarations (pure functions, etc.)
2609+
let mut declarations_js = String::new();
2610+
for decl in result.declarations.iter() {
2611+
declarations_js.push_str(&emitter.emit_statement(decl));
2612+
declarations_js.push('\n');
2613+
}
2614+
25782615
let fn_js = result.host_binding_fn.map(|f| {
25792616
let expr = OutputExpression::Function(oxc_allocator::Box::new_in(f, &allocator));
25802617
emitter.emit_expression(&expr)
25812618
})?;
25822619

2583-
Some((fn_js, host_vars))
2620+
Some(LinkerHostBindingOutput { fn_js, host_vars, declarations_js })
25842621
}
25852622

25862623
/// Output from compiling a template for the linker.

crates/oxc_angular_compiler/src/directive/compiler.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn compile_directive_from_metadata<'a>(
7171
pool_starting_index: u32,
7272
) -> DirectiveCompileResult<'a> {
7373
// Build the base directive fields, passing pool_starting_index for host bindings
74-
let (definition_map, next_pool_index) =
74+
let (definition_map, next_pool_index, host_declarations) =
7575
build_base_directive_fields(allocator, metadata, pool_starting_index);
7676

7777
// Add features
@@ -81,22 +81,30 @@ pub fn compile_directive_from_metadata<'a>(
8181
// Create the expression: ɵɵdefineDirective(definitionMap)
8282
let expression = create_define_directive_call(allocator, definition_map);
8383

84-
DirectiveCompileResult { expression, statements: Vec::new_in(allocator), next_pool_index }
84+
// Convert host binding declarations to statements
85+
let mut statements = Vec::new_in(allocator);
86+
for decl in host_declarations {
87+
statements.push(decl);
88+
}
89+
90+
DirectiveCompileResult { expression, statements, next_pool_index }
8591
}
8692

8793
/// Builds the base directive definition map.
8894
///
8995
/// Corresponds to `baseDirectiveFields()` in Angular's compiler.
9096
///
91-
/// Returns a tuple of (entries, next_pool_index) where next_pool_index is the
92-
/// next available constant pool index after host binding compilation.
97+
/// Returns a tuple of (entries, next_pool_index, host_declarations) where next_pool_index is the
98+
/// next available constant pool index after host binding compilation, and host_declarations
99+
/// contains any pooled constants (pure functions) from host binding compilation.
93100
fn build_base_directive_fields<'a>(
94101
allocator: &'a Allocator,
95102
metadata: &R3DirectiveMetadata<'a>,
96103
pool_starting_index: u32,
97-
) -> (Vec<'a, LiteralMapEntry<'a>>, u32) {
104+
) -> (Vec<'a, LiteralMapEntry<'a>>, u32, oxc_allocator::Vec<'a, OutputStatement<'a>>) {
98105
let mut entries = Vec::new_in(allocator);
99106
let mut next_pool_index = pool_starting_index;
107+
let mut host_declarations = oxc_allocator::Vec::new_in(allocator);
100108

101109
// type: MyDirective
102110
entries.push(LiteralMapEntry {
@@ -196,6 +204,9 @@ fn build_base_directive_fields<'a>(
196204
quoted: false,
197205
});
198206
}
207+
208+
// Collect host binding pool declarations (pure functions, etc.)
209+
host_declarations = result.declarations;
199210
}
200211
}
201212

@@ -264,7 +275,7 @@ fn build_base_directive_fields<'a>(
264275
});
265276
}
266277

267-
(entries, next_pool_index)
278+
(entries, next_pool_index, host_declarations)
268279
}
269280

270281
/// Adds features to the definition map.

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,6 +1362,7 @@ fn link_component(
13621362

13631363
// Build the defineComponent properties
13641364
let mut parts: Vec<String> = Vec::new();
1365+
let mut host_binding_declarations_js = String::new();
13651366

13661367
// 1. type
13671368
parts.push(format!("type: {type_name}"));
@@ -1398,13 +1399,16 @@ fn link_component(
13981399
// through the full Angular expression parser for correct output.
13991400
let host_input = extract_host_metadata_input(host_obj);
14001401
let selector = get_string_property(meta, "selector");
1401-
if let Some((host_fn, host_vars)) =
1402+
if let Some(host_output) =
14021403
crate::component::compile_host_bindings_for_linker(&host_input, type_name, selector)
14031404
{
1404-
if host_vars > 0 {
1405-
parts.push(format!("hostVars: {host_vars}"));
1405+
if host_output.host_vars > 0 {
1406+
parts.push(format!("hostVars: {}", host_output.host_vars));
1407+
}
1408+
parts.push(format!("hostBindings: {}", host_output.fn_js));
1409+
if !host_output.declarations_js.is_empty() {
1410+
host_binding_declarations_js = host_output.declarations_js;
14061411
}
1407-
parts.push(format!("hostBindings: {host_fn}"));
14081412
}
14091413
}
14101414

@@ -1533,8 +1537,11 @@ fn link_component(
15331537
let define_component =
15341538
format!("{ns}.\u{0275}\u{0275}defineComponent({{ {} }})", parts.join(", "));
15351539

1536-
// Wrap in IIFE with template declarations
1537-
let declarations = &template_output.declarations_js;
1540+
// Wrap in IIFE with template and host binding declarations
1541+
let mut declarations = template_output.declarations_js;
1542+
if !host_binding_declarations_js.is_empty() {
1543+
declarations.push_str(&host_binding_declarations_js);
1544+
}
15381545
if declarations.trim().is_empty() {
15391546
Some(define_component)
15401547
} else {

crates/oxc_angular_compiler/src/output/emitter.rs

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,11 +1234,10 @@ fn is_nullish_coalesce(expr: &OutputExpression<'_>) -> bool {
12341234
/// Escape a string for JavaScript output.
12351235
///
12361236
/// Uses double quotes to match Angular's output style.
1237-
/// Escapes `"`, `\`, `\n`, `\r`, `$` (when requested), ASCII control characters,
1238-
/// and all non-ASCII characters (code point > 0x7E) as `\uNNNN` sequences.
1239-
/// Characters above the BMP (U+10000+) are encoded as UTF-16 surrogate pairs
1240-
/// (`\uXXXX\uXXXX`). This matches TypeScript's emitter behavior, which escapes
1241-
/// non-ASCII characters in string literals.
1237+
/// Escapes `"`, `\`, `\n`, `\r`, `$` (when requested), and ASCII control characters
1238+
/// as `\uNNNN` sequences. Non-ASCII characters (code point > 0x7E) are emitted as
1239+
/// raw UTF-8 to match Angular's TypeScript emitter behavior (see `escapeIdentifier`
1240+
/// in `abstract_emitter.ts`), which only escapes `'`, `\`, `\n`, `\r`, and `$`.
12421241
pub(crate) fn escape_string(input: &str, escape_dollar: bool) -> String {
12431242
let mut result = String::with_capacity(input.len() + 2);
12441243
result.push('"');
@@ -1251,18 +1250,14 @@ pub(crate) fn escape_string(input: &str, escape_dollar: bool) -> String {
12511250
'$' if escape_dollar => result.push_str("\\$"),
12521251
// ASCII printable characters (0x20-0x7E) are emitted literally
12531252
c if (' '..='\x7E').contains(&c) => result.push(c),
1254-
// Everything else (ASCII control chars, non-ASCII) is escaped as \uNNNN.
1255-
// Characters above the BMP are encoded as UTF-16 surrogate pairs.
1253+
// DEL (0x7F) is an ASCII control character and must be escaped
1254+
'\x7F' => push_unicode_escape(&mut result, 0x7F),
1255+
// Non-ASCII characters (> 0x7F) are emitted as raw UTF-8 to match
1256+
// Angular's TypeScript emitter, which does not escape them.
1257+
c if (c as u32) > 0x7F => result.push(c),
1258+
// ASCII control characters (0x00-0x1F) are escaped as \uNNNN.
12561259
c => {
1257-
let code = c as u32;
1258-
if code <= 0xFFFF {
1259-
push_unicode_escape(&mut result, code);
1260-
} else {
1261-
let hi = 0xD800 + ((code - 0x10000) >> 10);
1262-
let lo = 0xDC00 + ((code - 0x10000) & 0x3FF);
1263-
push_unicode_escape(&mut result, hi);
1264-
push_unicode_escape(&mut result, lo);
1265-
}
1260+
push_unicode_escape(&mut result, c as u32);
12661261
}
12671262
}
12681263
}
@@ -1514,35 +1509,35 @@ mod tests {
15141509

15151510
#[test]
15161511
fn test_escape_string_unicode_literals() {
1517-
// Non-ASCII characters should be escaped as \uNNNN to match
1518-
// TypeScript's emitter behavior.
1512+
// Non-ASCII characters should be emitted as raw UTF-8 to match
1513+
// Angular's TypeScript emitter behavior (escapeIdentifier in abstract_emitter.ts).
15191514

1520-
// &times; (multiplication sign U+00D7) -> \u00D7
1521-
assert_eq!(escape_string("\u{00D7}", false), "\"\\u00D7\"");
1515+
// &times; (multiplication sign U+00D7) -> raw UTF-8
1516+
assert_eq!(escape_string("\u{00D7}", false), "\"\u{00D7}\"");
15221517

1523-
// &nbsp; (non-breaking space U+00A0) -> \u00A0
1524-
assert_eq!(escape_string("\u{00A0}", false), "\"\\u00A0\"");
1518+
// &nbsp; (non-breaking space U+00A0) -> raw UTF-8
1519+
assert_eq!(escape_string("\u{00A0}", false), "\"\u{00A0}\"");
15251520

15261521
// Mixed ASCII and non-ASCII
1527-
assert_eq!(escape_string("a\u{00D7}b", false), "\"a\\u00D7b\"");
1522+
assert_eq!(escape_string("a\u{00D7}b", false), "\"a\u{00D7}b\"");
15281523

15291524
// Multiple non-ASCII characters
1530-
assert_eq!(escape_string("\u{00D7}\u{00A0}", false), "\"\\u00D7\\u00A0\"");
1525+
assert_eq!(escape_string("\u{00D7}\u{00A0}", false), "\"\u{00D7}\u{00A0}\"");
15311526

1532-
// Characters outside BMP (emoji) -> surrogate pair
1533-
assert_eq!(escape_string("\u{1F600}", false), "\"\\uD83D\\uDE00\"");
1527+
// Characters outside BMP (emoji) -> raw UTF-8
1528+
assert_eq!(escape_string("\u{1F600}", false), "\"\u{1F600}\"");
15341529

1535-
// Common HTML entities -> all escaped as \uNNNN
1536-
assert_eq!(escape_string("\u{00A9}", false), "\"\\u00A9\""); // &copy; ©
1537-
assert_eq!(escape_string("\u{00AE}", false), "\"\\u00AE\""); // &reg; ®
1538-
assert_eq!(escape_string("\u{2014}", false), "\"\\u2014\""); // &mdash; —
1539-
assert_eq!(escape_string("\u{2013}", false), "\"\\u2013\""); // &ndash; –
1530+
// Common HTML entities -> all emitted as raw UTF-8
1531+
assert_eq!(escape_string("\u{00A9}", false), "\"\u{00A9}\""); // &copy; ©
1532+
assert_eq!(escape_string("\u{00AE}", false), "\"\u{00AE}\""); // &reg; ®
1533+
assert_eq!(escape_string("\u{2014}", false), "\"\u{2014}\""); // &mdash; —
1534+
assert_eq!(escape_string("\u{2013}", false), "\"\u{2013}\""); // &ndash; –
15401535

15411536
// Greek letter alpha
1542-
assert_eq!(escape_string("\u{03B1}", false), "\"\\u03B1\""); // α
1537+
assert_eq!(escape_string("\u{03B1}", false), "\"\u{03B1}\""); // α
15431538

15441539
// Accented Latin letter
1545-
assert_eq!(escape_string("\u{00E9}", false), "\"\\u00E9\""); // é
1540+
assert_eq!(escape_string("\u{00E9}", false), "\"\u{00E9}\""); // é
15461541
}
15471542

15481543
#[test]
@@ -1561,34 +1556,33 @@ mod tests {
15611556
}
15621557

15631558
#[test]
1564-
fn test_escape_string_non_ascii_as_unicode_escapes() {
1565-
// Non-ASCII characters should be escaped as \uNNNN to match
1566-
// TypeScript's emitter behavior (which escapes non-ASCII in string literals).
1559+
fn test_escape_string_non_ascii_as_raw_utf8() {
1560+
// Non-ASCII characters should be emitted as raw UTF-8 to match
1561+
// Angular's TypeScript emitter behavior (escapeIdentifier in abstract_emitter.ts).
15671562

15681563
// Non-breaking space U+00A0
1569-
assert_eq!(escape_string("\u{00A0}", false), "\"\\u00A0\"");
1564+
assert_eq!(escape_string("\u{00A0}", false), "\"\u{00A0}\"");
15701565

15711566
// En dash U+2013
1572-
assert_eq!(escape_string("\u{2013}", false), "\"\\u2013\"");
1567+
assert_eq!(escape_string("\u{2013}", false), "\"\u{2013}\"");
15731568

15741569
// Trademark U+2122
1575-
assert_eq!(escape_string("\u{2122}", false), "\"\\u2122\"");
1570+
assert_eq!(escape_string("\u{2122}", false), "\"\u{2122}\"");
15761571

15771572
// Infinity U+221E
1578-
assert_eq!(escape_string("\u{221E}", false), "\"\\u221E\"");
1573+
assert_eq!(escape_string("\u{221E}", false), "\"\u{221E}\"");
15791574

15801575
// Mixed ASCII and non-ASCII
1581-
assert_eq!(escape_string("a\u{00D7}b", false), "\"a\\u00D7b\"");
1576+
assert_eq!(escape_string("a\u{00D7}b", false), "\"a\u{00D7}b\"");
15821577

15831578
// Multiple non-ASCII characters
1584-
assert_eq!(escape_string("\u{00D7}\u{00A0}", false), "\"\\u00D7\\u00A0\"");
1579+
assert_eq!(escape_string("\u{00D7}\u{00A0}", false), "\"\u{00D7}\u{00A0}\"");
15851580

1586-
// Characters above BMP should use surrogate pairs
1587-
// U+1F600 (grinning face) = surrogate pair D83D DE00
1588-
assert_eq!(escape_string("\u{1F600}", false), "\"\\uD83D\\uDE00\"");
1581+
// Characters above BMP (emoji) -> raw UTF-8
1582+
assert_eq!(escape_string("\u{1F600}", false), "\"\u{1F600}\"");
15891583

1590-
// U+10000 (first supplementary char) = surrogate pair D800 DC00
1591-
assert_eq!(escape_string("\u{10000}", false), "\"\\uD800\\uDC00\"");
1584+
// U+10000 (first supplementary char) -> raw UTF-8
1585+
assert_eq!(escape_string("\u{10000}", false), "\"\u{10000}\"");
15921586

15931587
// ASCII printable chars (0x20-0x7E) should remain literal
15941588
assert_eq!(escape_string(" ~", false), "\" ~\"");

0 commit comments

Comments
 (0)