Skip to content

Commit b67c924

Browse files
Brooooooklynclaude
andauthored
fix: linker handles :not() pseudo-class selectors and uses correct SelectorFlags (#80)
Replace the linker's custom parse_selector/parse_single_selector with the pipeline's parse_selector_to_r3_selector which correctly handles :not() pseudo-class selectors, SelectorFlags.CLASS (8) instead of "class" string, and proper ordering of element → attributes → classes → :not() blocks. - Close #69 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 029fcde commit b67c924

File tree

1 file changed

+88
-61
lines changed
  • crates/oxc_angular_compiler/src/linker

1 file changed

+88
-61
lines changed

crates/oxc_angular_compiler/src/linker/mod.rs

Lines changed: 88 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ use oxc_parser::Parser;
4141
use oxc_span::{GetSpan, SourceType};
4242

4343
use crate::optimizer::Edit;
44+
use crate::pipeline::selector::{R3SelectorElement, parse_selector_to_r3_selector};
4445

4546
/// Partial declaration function names to link.
4647
const DECLARE_FACTORY: &str = "\u{0275}\u{0275}ngDeclareFactory";
@@ -532,67 +533,24 @@ fn extract_deps_source(obj: &ObjectExpression<'_>, source: &str, ns: &str) -> St
532533
/// - `"[attr=value]"` → `[["", "attr", "value"]]`
533534
/// - `"div[ngClass]"` → `[["div", "ngClass", ""]]`
534535
/// - `"[a],[b]"` → `[["", "a", ""], ["", "b", ""]]`
535-
/// - `".cls"` → `[["", "class", "cls"]]`
536+
/// - `".cls"` → `[["", 8, "cls"]]`
537+
/// - `"ng-scrollbar:not([externalViewport])"` → `[["ng-scrollbar", 3, "externalViewport", ""]]`
536538
fn parse_selector(selector: &str) -> String {
537-
let selectors: Vec<String> =
538-
selector.split(',').map(|s| parse_single_selector(s.trim())).collect();
539-
format!("[{}]", selectors.join(", "))
540-
}
541-
542-
/// Parse a single selector (no commas) into Angular's array format.
543-
fn parse_single_selector(selector: &str) -> String {
544-
let mut parts: Vec<String> = Vec::new();
545-
let mut remaining = selector;
546-
547-
// Extract tag name (everything before first [ or . or :)
548-
let tag_end = remaining
549-
.find(|c: char| c == '[' || c == '.' || c == ':' || c == '#')
550-
.unwrap_or(remaining.len());
551-
let tag = &remaining[..tag_end];
552-
remaining = &remaining[tag_end..];
553-
554-
if !tag.is_empty() {
555-
parts.push(format!("\"{}\"", tag));
556-
} else {
557-
parts.push("\"\"".to_string());
558-
}
559-
560-
// Extract attribute selectors [attr] or [attr=value]
561-
while let Some(bracket_start) = remaining.find('[') {
562-
let bracket_end = remaining[bracket_start..].find(']').map(|i| bracket_start + i);
563-
if let Some(end) = bracket_end {
564-
let attr_content = &remaining[bracket_start + 1..end];
565-
if let Some(eq_pos) = attr_content.find('=') {
566-
let attr_name = &attr_content[..eq_pos];
567-
let attr_value = attr_content[eq_pos + 1..].trim_matches('"').trim_matches('\'');
568-
parts.push(format!("\"{}\"", attr_name));
569-
parts.push(format!("\"{}\"", attr_value));
570-
} else {
571-
parts.push(format!("\"{}\"", attr_content));
572-
parts.push("\"\"".to_string());
573-
}
574-
remaining = &remaining[end + 1..];
575-
} else {
576-
break;
577-
}
578-
}
579-
580-
// Extract class selectors .className
581-
let mut class_remaining = remaining;
582-
while let Some(dot_pos) = class_remaining.find('.') {
583-
let class_end = class_remaining[dot_pos + 1..]
584-
.find(|c: char| !c.is_alphanumeric() && c != '-' && c != '_')
585-
.map(|i| dot_pos + 1 + i)
586-
.unwrap_or(class_remaining.len());
587-
let class_name = &class_remaining[dot_pos + 1..class_end];
588-
if !class_name.is_empty() {
589-
parts.push("\"class\"".to_string());
590-
parts.push(format!("\"{}\"", class_name));
591-
}
592-
class_remaining = &class_remaining[class_end..];
593-
}
594-
595-
format!("[{}]", parts.join(", "))
539+
let r3_selectors = parse_selector_to_r3_selector(selector);
540+
let selector_strs: Vec<String> = r3_selectors
541+
.iter()
542+
.map(|elements| {
543+
let parts: Vec<String> = elements
544+
.iter()
545+
.map(|el| match el {
546+
R3SelectorElement::String(s) => format!("\"{}\"", s),
547+
R3SelectorElement::Flag(f) => f.to_string(),
548+
})
549+
.collect();
550+
format!("[{}]", parts.join(", "))
551+
})
552+
.collect();
553+
format!("[{}]", selector_strs.join(", "))
596554
}
597555

598556
/// Build the `hostAttrs` flat array from the partial declaration's `host` object.
@@ -1635,14 +1593,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImpor
16351593

16361594
#[test]
16371595
fn test_parse_selector_class() {
1638-
assert_eq!(parse_selector(".my-class"), r#"[["", "class", "my-class"]]"#);
1596+
// Classes use SelectorFlags.CLASS (8) instead of "class" string
1597+
assert_eq!(parse_selector(".my-class"), r#"[["", 8, "my-class"]]"#);
16391598
}
16401599

16411600
#[test]
16421601
fn test_parse_selector_multiple() {
16431602
assert_eq!(parse_selector("[a],[b]"), r#"[["", "a", ""], ["", "b", ""]]"#);
16441603
}
16451604

1605+
#[test]
1606+
fn test_parse_selector_not_attribute() {
1607+
// :not() with attribute - SelectorFlags.NOT | SelectorFlags.ATTRIBUTE = 3
1608+
assert_eq!(
1609+
parse_selector("ng-scrollbar:not([externalViewport])"),
1610+
r#"[["ng-scrollbar", 3, "externalViewport", ""]]"#
1611+
);
1612+
}
1613+
1614+
#[test]
1615+
fn test_parse_selector_not_attribute_with_value() {
1616+
assert_eq!(
1617+
parse_selector("input:not([type=checkbox])"),
1618+
r#"[["input", 3, "type", "checkbox"]]"#
1619+
);
1620+
}
1621+
1622+
#[test]
1623+
fn test_parse_selector_multiple_not() {
1624+
// Multiple :not() clauses
1625+
assert_eq!(
1626+
parse_selector("[ngModel]:not([formControlName]):not([formControl])"),
1627+
r#"[["", "ngModel", "", 3, "formControlName", "", 3, "formControl", ""]]"#
1628+
);
1629+
}
1630+
1631+
#[test]
1632+
fn test_parse_selector_not_element() {
1633+
// :not() with element - SelectorFlags.NOT | SelectorFlags.ELEMENT = 5
1634+
assert_eq!(parse_selector(":not(span)"), r#"[["", 5, "span"]]"#);
1635+
}
1636+
1637+
#[test]
1638+
fn test_parse_selector_not_class() {
1639+
// :not() with class - SelectorFlags.NOT | SelectorFlags.CLASS = 9
1640+
assert_eq!(parse_selector(":not(.hidden)"), r#"[["", 9, "hidden"]]"#);
1641+
}
1642+
1643+
#[test]
1644+
fn test_parse_selector_complex_not() {
1645+
// Complex: element + class + attribute + multiple :not()
1646+
assert_eq!(
1647+
parse_selector("div.foo[some-directive]:not([title]):not(.baz)"),
1648+
r#"[["div", "some-directive", "", 8, "foo", 3, "title", "", 9, "baz"]]"#
1649+
);
1650+
}
1651+
1652+
#[test]
1653+
fn test_parse_selector_element_with_class_and_attribute() {
1654+
// Class should come after attributes with CLASS flag
1655+
assert_eq!(parse_selector("div.active[role]"), r#"[["div", "role", "", 8, "active"]]"#);
1656+
}
1657+
1658+
#[test]
1659+
fn test_parse_selector_not_only() {
1660+
// Only :not() selectors - element becomes "*" but emitted as ""
1661+
assert_eq!(parse_selector(":not(.hidden)"), r#"[["", 9, "hidden"]]"#);
1662+
}
1663+
1664+
#[test]
1665+
fn test_parse_selector_comma_with_not() {
1666+
// Comma-separated selectors with :not()
1667+
assert_eq!(
1668+
parse_selector("form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]"),
1669+
r#"[["form", 3, "ngNoForm", "", 3, "formGroup", ""], ["ng-form"], ["", "ngForm", ""]]"#
1670+
);
1671+
}
1672+
16461673
#[test]
16471674
fn test_no_declarations() {
16481675
let allocator = Allocator::default();

0 commit comments

Comments
 (0)