Skip to content

Commit 081d438

Browse files
authored
feat: add attribute, input, and output visitor methods to R3Visitor (#180)
1 parent 2ef4514 commit 081d438

File tree

1 file changed

+163
-2
lines changed
  • crates/oxc_angular_compiler/src/ast

1 file changed

+163
-2
lines changed

crates/oxc_angular_compiler/src/ast/r3.rs

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,13 +1395,31 @@ pub trait R3Visitor<'a> {
13951395
/// Visit a bound text node.
13961396
fn visit_bound_text(&mut self, _text: &R3BoundText<'a>) {}
13971397

1398+
/// Visit a static text attribute.
1399+
fn visit_text_attribute(&mut self, _attr: &R3TextAttribute<'a>) {}
1400+
1401+
/// Visit a bound attribute (input property).
1402+
fn visit_bound_attribute(&mut self, _attr: &R3BoundAttribute<'a>) {}
1403+
1404+
/// Visit a bound event (output).
1405+
fn visit_bound_event(&mut self, _event: &R3BoundEvent<'a>) {}
1406+
13981407
/// Visit an element.
13991408
fn visit_element(&mut self, element: &R3Element<'a>) {
14001409
self.visit_element_children(element);
14011410
}
14021411

1403-
/// Visit element children.
1412+
/// Visit element children, attributes, inputs, and outputs.
14041413
fn visit_element_children(&mut self, element: &R3Element<'a>) {
1414+
for attr in &element.attributes {
1415+
self.visit_text_attribute(attr);
1416+
}
1417+
for input in &element.inputs {
1418+
self.visit_bound_attribute(input);
1419+
}
1420+
for output in &element.outputs {
1421+
self.visit_bound_event(output);
1422+
}
14051423
for child in &element.children {
14061424
child.visit(self);
14071425
}
@@ -1412,15 +1430,27 @@ pub trait R3Visitor<'a> {
14121430
self.visit_template_children(template);
14131431
}
14141432

1415-
/// Visit template children.
1433+
/// Visit template children, attributes, inputs, and outputs.
14161434
fn visit_template_children(&mut self, template: &R3Template<'a>) {
1435+
for attr in &template.attributes {
1436+
self.visit_text_attribute(attr);
1437+
}
1438+
for input in &template.inputs {
1439+
self.visit_bound_attribute(input);
1440+
}
1441+
for output in &template.outputs {
1442+
self.visit_bound_event(output);
1443+
}
14171444
for child in &template.children {
14181445
child.visit(self);
14191446
}
14201447
}
14211448

14221449
/// Visit a content projection slot.
14231450
fn visit_content(&mut self, content: &R3Content<'a>) {
1451+
for attr in &content.attributes {
1452+
self.visit_text_attribute(attr);
1453+
}
14241454
for child in &content.children {
14251455
child.visit(self);
14261456
}
@@ -1531,6 +1561,15 @@ pub trait R3Visitor<'a> {
15311561

15321562
/// Visit a component.
15331563
fn visit_component(&mut self, component: &R3Component<'a>) {
1564+
for attr in &component.attributes {
1565+
self.visit_text_attribute(attr);
1566+
}
1567+
for input in &component.inputs {
1568+
self.visit_bound_attribute(input);
1569+
}
1570+
for output in &component.outputs {
1571+
self.visit_bound_event(output);
1572+
}
15341573
for child in &component.children {
15351574
child.visit(self);
15361575
}
@@ -1568,3 +1607,125 @@ pub struct R3ParseResult<'a> {
15681607
/// Comment nodes (if collected).
15691608
pub comment_nodes: Option<Vec<'a, R3Comment<'a>>>,
15701609
}
1610+
1611+
#[cfg(test)]
1612+
mod tests {
1613+
use oxc_allocator::Allocator;
1614+
1615+
use crate::ast::r3::{R3Visitor, visit_all};
1616+
use crate::parser::html::HtmlParser;
1617+
use crate::transform::html_to_r3::{TransformOptions, html_ast_to_r3_ast};
1618+
1619+
/// A visitor that collects names of visited attributes, inputs, and outputs.
1620+
struct AttributeCollector {
1621+
text_attributes: Vec<String>,
1622+
bound_attributes: Vec<String>,
1623+
bound_events: Vec<String>,
1624+
elements: Vec<String>,
1625+
}
1626+
1627+
impl AttributeCollector {
1628+
fn new() -> Self {
1629+
Self {
1630+
text_attributes: Vec::new(),
1631+
bound_attributes: Vec::new(),
1632+
bound_events: Vec::new(),
1633+
elements: Vec::new(),
1634+
}
1635+
}
1636+
}
1637+
1638+
impl<'a> R3Visitor<'a> for AttributeCollector {
1639+
fn visit_element(&mut self, element: &super::R3Element<'a>) {
1640+
self.elements.push(element.name.to_string());
1641+
self.visit_element_children(element);
1642+
}
1643+
1644+
fn visit_text_attribute(&mut self, attr: &super::R3TextAttribute<'a>) {
1645+
self.text_attributes.push(attr.name.to_string());
1646+
}
1647+
1648+
fn visit_bound_attribute(&mut self, attr: &super::R3BoundAttribute<'a>) {
1649+
self.bound_attributes.push(attr.name.to_string());
1650+
}
1651+
1652+
fn visit_bound_event(&mut self, event: &super::R3BoundEvent<'a>) {
1653+
self.bound_events.push(event.name.to_string());
1654+
}
1655+
}
1656+
1657+
#[test]
1658+
fn test_r3_visitor_visits_attributes_inputs_outputs() {
1659+
let allocator = Allocator::default();
1660+
let template =
1661+
r#"<button type="submit" [disabled]="isDisabled" (click)="onClick()">Save</button>"#;
1662+
1663+
let html_result = HtmlParser::new(&allocator, template, "test.html").parse();
1664+
assert!(html_result.errors.is_empty());
1665+
1666+
let r3_result = html_ast_to_r3_ast(
1667+
&allocator,
1668+
template,
1669+
&html_result.nodes,
1670+
TransformOptions::default(),
1671+
);
1672+
assert!(r3_result.errors.is_empty());
1673+
1674+
let mut collector = AttributeCollector::new();
1675+
visit_all(&mut collector, &r3_result.nodes);
1676+
1677+
assert_eq!(collector.elements, vec!["button"]);
1678+
assert_eq!(collector.text_attributes, vec!["type"]);
1679+
assert_eq!(collector.bound_attributes, vec!["disabled"]);
1680+
assert_eq!(collector.bound_events, vec!["click"]);
1681+
}
1682+
1683+
#[test]
1684+
fn test_r3_visitor_visits_nested_elements() {
1685+
let allocator = Allocator::default();
1686+
let template = r#"<div id="outer"><span class="inner" [title]="t" (mouseenter)="onHover()">text</span></div>"#;
1687+
1688+
let html_result = HtmlParser::new(&allocator, template, "test.html").parse();
1689+
assert!(html_result.errors.is_empty());
1690+
1691+
let r3_result = html_ast_to_r3_ast(
1692+
&allocator,
1693+
template,
1694+
&html_result.nodes,
1695+
TransformOptions::default(),
1696+
);
1697+
assert!(r3_result.errors.is_empty());
1698+
1699+
let mut collector = AttributeCollector::new();
1700+
visit_all(&mut collector, &r3_result.nodes);
1701+
1702+
assert_eq!(collector.elements, vec!["div", "span"]);
1703+
assert_eq!(collector.text_attributes, vec!["id", "class"]);
1704+
assert_eq!(collector.bound_attributes, vec!["title"]);
1705+
assert_eq!(collector.bound_events, vec!["mouseenter"]);
1706+
}
1707+
1708+
#[test]
1709+
fn test_r3_visitor_default_noop_does_not_break() {
1710+
let allocator = Allocator::default();
1711+
let template = r#"<input [value]="name" (change)="update()" required />"#;
1712+
1713+
let html_result = HtmlParser::new(&allocator, template, "test.html").parse();
1714+
assert!(html_result.errors.is_empty());
1715+
1716+
let r3_result = html_ast_to_r3_ast(
1717+
&allocator,
1718+
template,
1719+
&html_result.nodes,
1720+
TransformOptions::default(),
1721+
);
1722+
assert!(r3_result.errors.is_empty());
1723+
1724+
// A visitor with all default no-op methods should traverse without panic
1725+
struct NoopVisitor;
1726+
impl<'a> R3Visitor<'a> for NoopVisitor {}
1727+
1728+
let mut visitor = NoopVisitor;
1729+
visit_all(&mut visitor, &r3_result.nodes);
1730+
}
1731+
}

0 commit comments

Comments
 (0)