Skip to content

Commit 24f0370

Browse files
Brooooooklynclaude
andcommitted
Angular compare tool: Major polish and full semantics improvements
## Rust Compiler Improvements - Add hostDirectives parsing to component decorator extraction - Wire @HostBinding/@HostListener extraction to component metadata - Fix key format: wrap with []/() for proper classification - Fix @HostListener args preservation (e.g., handleClick($event)) - Fix HTML lexer escapable raw text entity handling ## Compare Tool Enhancements - Add full-file transformation support (transformAngularFile API) - Add const value normalization for fair comparison - Improve template function extraction (AST-based, handles const/arrow) - Replace Set-based diff with LCS algorithm (preserves ordering) - Add version metadata to reports (oxcVersion, angularVersion) ## Metadata Pass-through Fixes - Fix host keys double-wrapping issue - Pass providers, viewProviders, animations, schemas, styles, exportAs - Make preserveWhitespaces and i18nUseExternalIds configurable - Fix hostDirectives forward-ref flag pass-through ## Test Infrastructure - Add new fixture categories: full-transform, host-directives, schemas, etc. - Fix decorator detection (@angular/core instead of @component string) - Add comprehensive test coverage for host decorators Results: 92.4% material-angular match rate (587/635), 92.8% fixtures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 90591bc commit 24f0370

File tree

42 files changed

+10103
-430
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+10103
-430
lines changed

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 744 additions & 1 deletion
Large diffs are not rendered by default.

crates/oxc_angular_compiler/src/component/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ pub use metadata::{
2626
TemplateDependencyKind, ViewEncapsulation,
2727
};
2828
pub use transform::{
29-
CompiledComponent, HmrTemplateCompileOutput, ResolvedResources, TemplateCompileOutput,
30-
TransformOptions, TransformResult, compile_component_template, compile_for_hmr,
31-
compile_template_for_hmr, compile_template_to_js, compile_template_to_js_with_options,
32-
transform_angular_file,
29+
CompiledComponent, HmrTemplateCompileOutput, HostMetadataInput, ResolvedResources,
30+
TemplateCompileOutput, TransformOptions, TransformResult, compile_component_template,
31+
compile_for_hmr, compile_template_for_hmr, compile_template_to_js,
32+
compile_template_to_js_with_options, transform_angular_file,
3333
};

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,30 @@ pub struct TransformOptions {
9292

9393
/// Override the preserve whitespaces setting.
9494
pub preserve_whitespaces: Option<bool>,
95+
96+
/// Host bindings metadata for the component.
97+
/// Contains property bindings, attribute bindings, and event listeners.
98+
pub host: Option<HostMetadataInput>,
99+
}
100+
101+
/// Input for host metadata when passed via TransformOptions.
102+
/// Uses owned String types for easier NAPI interop.
103+
#[derive(Debug, Clone, Default)]
104+
pub struct HostMetadataInput {
105+
/// Host property bindings: `{ "[class.active]": "isActive" }`
106+
pub properties: Vec<(String, String)>,
107+
108+
/// Host attribute bindings: `{ "role": "button" }`
109+
pub attributes: Vec<(String, String)>,
110+
111+
/// Host event listeners: `{ "(click)": "onClick()" }`
112+
pub listeners: Vec<(String, String)>,
113+
114+
/// Special attribute for static class binding.
115+
pub class_attr: Option<String>,
116+
117+
/// Special attribute for static style binding.
118+
pub style_attr: Option<String>,
95119
}
96120

97121
impl Default for TransformOptions {
@@ -110,6 +134,7 @@ impl Default for TransformOptions {
110134
encapsulation: None,
111135
change_detection: None,
112136
preserve_whitespaces: None,
137+
host: None,
113138
}
114139
}
115140
}
@@ -931,6 +956,34 @@ pub fn compile_template_to_js_with_options<'a>(
931956
all_statements.push(main_fn_stmt);
932957
}
933958

959+
// Stage 6: Compile host bindings if provided via options
960+
if let Some(ref host_input) = options.host {
961+
if let Some(host_result) = compile_host_bindings_from_input(
962+
allocator,
963+
host_input,
964+
component_name,
965+
options.selector.as_deref(),
966+
) {
967+
// Add the host bindings function as a declaration if present
968+
if let Some(host_fn) = host_result.host_binding_fn {
969+
if let Some(fn_name) = host_fn.name.clone() {
970+
let host_fn_stmt =
971+
OutputStatement::DeclareFunction(oxc_allocator::Box::new_in(
972+
DeclareFunctionStmt {
973+
name: fn_name,
974+
params: host_fn.params,
975+
statements: host_fn.statements,
976+
modifiers: StmtModifier::NONE,
977+
source_span: host_fn.source_span,
978+
},
979+
allocator,
980+
));
981+
all_statements.push(host_fn_stmt);
982+
}
983+
}
984+
}
985+
}
986+
934987
// Generate code with optional source map
935988
if options.sourcemap {
936989
let source_file = Arc::new(ParseSourceFile::new(template, file_path));
@@ -1285,6 +1338,80 @@ fn parse_event_target(event_name: &str) -> (&str, Option<&str>) {
12851338
}
12861339
}
12871340

1341+
/// Convert HostMetadataInput (owned strings) to HostMetadata<'a> (with Atom types).
1342+
///
1343+
/// This function is used when compiling templates in isolation (e.g., for the compare tool)
1344+
/// where the host metadata comes from NAPI options rather than from parsing a decorator.
1345+
fn convert_host_metadata_input_to_host_metadata<'a>(
1346+
allocator: &'a Allocator,
1347+
input: &HostMetadataInput,
1348+
) -> HostMetadata<'a> {
1349+
use oxc_allocator::FromIn;
1350+
1351+
let mut properties: OxcVec<'a, (Atom<'a>, Atom<'a>)> = OxcVec::new_in(allocator);
1352+
for (k, v) in &input.properties {
1353+
properties
1354+
.push((Atom::from_in(k.as_str(), allocator), Atom::from_in(v.as_str(), allocator)));
1355+
}
1356+
1357+
let mut attributes: OxcVec<'a, (Atom<'a>, Atom<'a>)> = OxcVec::new_in(allocator);
1358+
for (k, v) in &input.attributes {
1359+
attributes
1360+
.push((Atom::from_in(k.as_str(), allocator), Atom::from_in(v.as_str(), allocator)));
1361+
}
1362+
1363+
let mut listeners: OxcVec<'a, (Atom<'a>, Atom<'a>)> = OxcVec::new_in(allocator);
1364+
for (k, v) in &input.listeners {
1365+
listeners
1366+
.push((Atom::from_in(k.as_str(), allocator), Atom::from_in(v.as_str(), allocator)));
1367+
}
1368+
1369+
HostMetadata {
1370+
properties,
1371+
attributes,
1372+
listeners,
1373+
class_attr: input.class_attr.as_ref().map(|s| Atom::from_in(s.as_str(), allocator)),
1374+
style_attr: input.style_attr.as_ref().map(|s| Atom::from_in(s.as_str(), allocator)),
1375+
}
1376+
}
1377+
1378+
/// Compile host bindings from HostMetadataInput (owned strings).
1379+
///
1380+
/// This is used by `compile_template_to_js_with_options` when host metadata is provided
1381+
/// via TransformOptions for isolated template compilation.
1382+
fn compile_host_bindings_from_input<'a>(
1383+
allocator: &'a Allocator,
1384+
host_input: &HostMetadataInput,
1385+
component_name: &str,
1386+
selector: Option<&str>,
1387+
) -> Option<HostBindingCompilationResult<'a>> {
1388+
use oxc_allocator::FromIn;
1389+
1390+
// Check if there are any host bindings at all
1391+
if host_input.properties.is_empty()
1392+
&& host_input.attributes.is_empty()
1393+
&& host_input.listeners.is_empty()
1394+
{
1395+
return None;
1396+
}
1397+
1398+
// Convert to HostMetadata
1399+
let host = convert_host_metadata_input_to_host_metadata(allocator, host_input);
1400+
1401+
// Get component name and selector as atoms
1402+
let component_name_atom = Atom::from_in(component_name, allocator);
1403+
let component_selector =
1404+
selector.map(|s| Atom::from_in(s, allocator)).unwrap_or_else(|| Atom::from(""));
1405+
1406+
// Convert to HostBindingInput and compile
1407+
let input =
1408+
convert_host_metadata_to_input(allocator, &host, component_name_atom, component_selector);
1409+
let mut job = ingest_host_binding(allocator, input);
1410+
let result = compile_host_bindings(&mut job);
1411+
1412+
Some(result)
1413+
}
1414+
12881415
#[cfg(test)]
12891416
mod tests {
12901417
use super::*;

crates/oxc_angular_compiler/src/directive/metadata.rs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -436,17 +436,34 @@ impl<'a> R3DirectiveMetadataBuilder<'a> {
436436
}
437437

438438
// Extract host bindings from @HostBinding
439+
// Wrap with brackets: "class.active" -> "[class.active]"
439440
let host_bindings = super::property_decorators::extract_host_bindings(allocator, class);
440441
for (host_prop, class_prop) in host_bindings {
441-
// Add to host.properties
442-
self.host.properties.push((host_prop, class_prop));
442+
// Add to host.properties with wrapped key
443+
let wrapped_key = Atom::from(allocator.alloc_str(&format!("[{}]", host_prop.as_str())));
444+
self.host.properties.push((wrapped_key, class_prop));
443445
}
444446

445447
// Extract host listeners from @HostListener
448+
// Wrap event name with parentheses and build method expression with args
449+
// Reference: Angular's shared.ts:713 - `bindings.listeners[eventName] = \`${member.name}(${args.join(',')})\``
446450
let host_listeners = super::property_decorators::extract_host_listeners(allocator, class);
447-
for (event_name, method_name, _args) in host_listeners {
451+
for (event_name, method_name, args) in host_listeners {
452+
// Wrap event name: "click" -> "(click)"
453+
let wrapped_key =
454+
Atom::from(allocator.alloc_str(&format!("({})", event_name.as_str())));
455+
456+
// Build method expression with args: "handleClick" + ["$event"] -> "handleClick($event)"
457+
let method_expr = if args.is_empty() {
458+
Atom::from(allocator.alloc_str(&format!("{}()", method_name.as_str())))
459+
} else {
460+
let args_str: String =
461+
args.iter().map(|a| a.as_str()).collect::<std::vec::Vec<_>>().join(",");
462+
Atom::from(allocator.alloc_str(&format!("{}({})", method_name.as_str(), args_str)))
463+
};
464+
448465
// Add to host.listeners
449-
self.host.listeners.push((event_name, method_name));
466+
self.host.listeners.push((wrapped_key, method_expr));
450467
}
451468

452469
self
@@ -723,11 +740,11 @@ mod tests {
723740
let metadata = metadata.unwrap();
724741
assert_eq!(metadata.host.properties.len(), 2);
725742

726-
// Check host bindings
727-
assert_eq!(metadata.host.properties[0].0.as_str(), "class.active");
743+
// Check host bindings - keys are wrapped with brackets
744+
assert_eq!(metadata.host.properties[0].0.as_str(), "[class.active]");
728745
assert_eq!(metadata.host.properties[0].1.as_str(), "isActive");
729746

730-
assert_eq!(metadata.host.properties[1].0.as_str(), "attr.role");
747+
assert_eq!(metadata.host.properties[1].0.as_str(), "[attr.role]");
731748
assert_eq!(metadata.host.properties[1].1.as_str(), "role");
732749
}
733750

@@ -758,12 +775,12 @@ mod tests {
758775
let metadata = metadata.unwrap();
759776
assert_eq!(metadata.host.listeners.len(), 2);
760777

761-
// Check host listeners
762-
assert_eq!(metadata.host.listeners[0].0.as_str(), "click");
763-
assert_eq!(metadata.host.listeners[0].1.as_str(), "onClick");
778+
// Check host listeners - keys are wrapped with parentheses, method expressions include ()
779+
assert_eq!(metadata.host.listeners[0].0.as_str(), "(click)");
780+
assert_eq!(metadata.host.listeners[0].1.as_str(), "onClick()");
764781

765-
assert_eq!(metadata.host.listeners[1].0.as_str(), "mouseenter");
766-
assert_eq!(metadata.host.listeners[1].1.as_str(), "onMouseEnter");
782+
assert_eq!(metadata.host.listeners[1].0.as_str(), "(mouseenter)");
783+
assert_eq!(metadata.host.listeners[1].1.as_str(), "onMouseEnter()");
767784
}
768785

769786
#[test]

0 commit comments

Comments
 (0)