Skip to content

Commit ce591fd

Browse files
kdy1sokra
andauthored
fix: Fix purity lint for CSS Modules in swc mode (#7621)
### Description Closes PACK-2660 ### Testing Instructions I added a test. --------- Co-authored-by: Tobias Koppers <[email protected]>
1 parent 9c835a9 commit ce591fd

File tree

1 file changed

+134
-23
lines changed

1 file changed

+134
-23
lines changed

crates/turbopack-css/src/process.rs

+134-23
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ use swc_core::{
2020
base::sourcemap::SourceMapBuilder,
2121
common::{BytePos, FileName, LineCol, Span},
2222
css::{
23-
ast::{SubclassSelector, TypeSelector, UrlValue},
23+
ast::{
24+
ComplexSelector, ComplexSelectorChildren, CompoundSelector, ForgivingComplexSelector,
25+
ForgivingRelativeSelector, PseudoClassSelectorChildren, PseudoElementSelectorChildren,
26+
RelativeSelector, SubclassSelector, TypeSelector, UrlValue,
27+
},
2428
codegen::{writer::basic::BasicCssWriter, CodeGenerator},
2529
modules::{CssClassName, TransformConfig},
2630
visit::{VisitMut, VisitMutWith, VisitWith},
@@ -707,26 +711,100 @@ const CSS_MODULE_ERROR: &str =
707711

708712
/// We only vist top-level selectors.
709713
impl swc_core::css::visit::Visit for CssValidator {
710-
fn visit_complex_selector(&mut self, n: &swc_core::css::ast::ComplexSelector) {
711-
if n.children.iter().all(|sel| match sel {
712-
swc_core::css::ast::ComplexSelectorChildren::CompoundSelector(sel) => {
713-
sel.subclass_selectors.iter().all(|sel| {
714-
matches!(
715-
sel,
716-
SubclassSelector::Attribute { .. }
717-
| SubclassSelector::PseudoClass(..)
718-
| SubclassSelector::PseudoElement(..)
719-
)
720-
}) && match &sel.type_selector.as_deref() {
721-
Some(TypeSelector::TagName(tag)) => {
722-
!matches!(&*tag.name.value.value, "html" | "body")
723-
}
724-
Some(TypeSelector::Universal(..)) => true,
714+
fn visit_complex_selector(&mut self, n: &ComplexSelector) {
715+
fn is_complex_not_pure(sel: &ComplexSelector) -> bool {
716+
sel.children.iter().all(|sel| match sel {
717+
ComplexSelectorChildren::CompoundSelector(sel) => is_compound_not_pure(sel),
718+
ComplexSelectorChildren::Combinator(_) => true,
719+
})
720+
}
721+
722+
fn is_forgiving_selector_not_pure(sel: &ForgivingComplexSelector) -> bool {
723+
match sel {
724+
ForgivingComplexSelector::ComplexSelector(sel) => is_complex_not_pure(sel),
725+
ForgivingComplexSelector::ListOfComponentValues(_) => false,
726+
}
727+
}
728+
729+
fn is_forgiving_relative_selector_not_pure(sel: &ForgivingRelativeSelector) -> bool {
730+
match sel {
731+
ForgivingRelativeSelector::RelativeSelector(sel) => {
732+
is_relative_selector_not_pure(sel)
733+
}
734+
ForgivingRelativeSelector::ListOfComponentValues(_) => false,
735+
}
736+
}
737+
738+
fn is_relative_selector_not_pure(sel: &RelativeSelector) -> bool {
739+
is_complex_not_pure(&sel.selector)
740+
}
741+
742+
fn is_compound_not_pure(sel: &CompoundSelector) -> bool {
743+
sel.subclass_selectors.iter().all(|sel| match sel {
744+
SubclassSelector::Attribute { .. } => true,
745+
SubclassSelector::PseudoClass(cls) => {
746+
cls.name.value == "not"
747+
|| cls.name.value == "has"
748+
|| if let Some(c) = &cls.children {
749+
c.iter().all(|c| match c {
750+
PseudoClassSelectorChildren::ComplexSelector(sel) => {
751+
is_complex_not_pure(sel)
752+
}
753+
754+
PseudoClassSelectorChildren::CompoundSelector(sel) => {
755+
is_compound_not_pure(sel)
756+
}
757+
758+
PseudoClassSelectorChildren::SelectorList(sels) => {
759+
sels.children.iter().all(is_complex_not_pure)
760+
}
761+
PseudoClassSelectorChildren::ForgivingSelectorList(sels) => {
762+
sels.children.iter().all(is_forgiving_selector_not_pure)
763+
}
764+
PseudoClassSelectorChildren::CompoundSelectorList(sels) => {
765+
sels.children.iter().all(is_compound_not_pure)
766+
}
767+
PseudoClassSelectorChildren::RelativeSelectorList(sels) => {
768+
sels.children.iter().all(is_relative_selector_not_pure)
769+
}
770+
PseudoClassSelectorChildren::ForgivingRelativeSelectorList(
771+
sels,
772+
) => sels
773+
.children
774+
.iter()
775+
.all(is_forgiving_relative_selector_not_pure),
776+
777+
PseudoClassSelectorChildren::Ident(_)
778+
| PseudoClassSelectorChildren::Str(_)
779+
| PseudoClassSelectorChildren::Delimiter(_)
780+
| PseudoClassSelectorChildren::PreservedToken(_)
781+
| PseudoClassSelectorChildren::AnPlusB(_) => false,
782+
})
783+
} else {
784+
true
785+
}
786+
}
787+
SubclassSelector::PseudoElement(el) => match &el.children {
788+
Some(c) => c.iter().all(|c| match c {
789+
PseudoElementSelectorChildren::CompoundSelector(sel) => {
790+
is_compound_not_pure(sel)
791+
}
792+
793+
_ => false,
794+
}),
725795
None => true,
796+
},
797+
_ => false,
798+
}) && match &sel.type_selector.as_deref() {
799+
Some(TypeSelector::TagName(tag)) => {
800+
!matches!(&*tag.name.value.value, "html" | "body")
726801
}
802+
Some(TypeSelector::Universal(..)) => true,
803+
None => true,
727804
}
728-
swc_core::css::ast::ComplexSelectorChildren::Combinator(_) => true,
729-
}) {
805+
}
806+
807+
if is_complex_not_pure(n) {
730808
self.errors
731809
.push(CssError::SwcSelectorInModuleNotPure { span: n.span });
732810
}
@@ -747,9 +825,12 @@ impl lightningcss::visitor::Visitor<'_> for CssValidator {
747825
&mut self,
748826
selector: &mut lightningcss::selector::Selector<'_>,
749827
) -> Result<(), Self::Error> {
750-
if selector
751-
.iter_raw_parse_order_from(0)
752-
.all(|component| match component {
828+
fn is_selector_problematic(sel: &lightningcss::selector::Selector) -> bool {
829+
sel.iter_raw_parse_order_from(0).all(is_problematic)
830+
}
831+
832+
fn is_problematic(c: &lightningcss::selector::Component) -> bool {
833+
match c {
753834
parcel_selectors::parser::Component::ID(..)
754835
| parcel_selectors::parser::Component::Class(..) => false,
755836

@@ -760,13 +841,19 @@ impl lightningcss::visitor::Visitor<'_> for CssValidator {
760841
| parcel_selectors::parser::Component::ExplicitUniversalType
761842
| parcel_selectors::parser::Component::Negation(..) => true,
762843

844+
parcel_selectors::parser::Component::Where(sel) => {
845+
sel.iter().all(is_selector_problematic)
846+
}
847+
763848
parcel_selectors::parser::Component::LocalName(local) => {
764849
// Allow html and body. They are not pure selectors but are allowed.
765850
!matches!(&*local.name.0, "html" | "body")
766851
}
767852
_ => false,
768-
})
769-
{
853+
}
854+
}
855+
856+
if is_selector_problematic(selector) {
770857
self.errors
771858
.push(CssError::LightningCssSelectorInModuleNotPure {
772859
selector: format!("{selector:?}"),
@@ -1069,6 +1156,18 @@ mod tests {
10691156
}",
10701157
);
10711158

1159+
assert_lint_success(
1160+
":where(.main > *) {
1161+
color: red;
1162+
}",
1163+
);
1164+
1165+
assert_lint_success(
1166+
":where(.main > *, .root > *) {
1167+
color: red;
1168+
}",
1169+
);
1170+
10721171
assert_lint_failure(
10731172
"div {
10741173
color: red;
@@ -1128,5 +1227,17 @@ mod tests {
11281227
--foo: 1;
11291228
}",
11301229
);
1230+
1231+
assert_lint_failure(
1232+
":where(div > *) {
1233+
color: red;
1234+
}",
1235+
);
1236+
1237+
assert_lint_failure(
1238+
":where(div) {
1239+
color: red;
1240+
}",
1241+
);
11311242
}
11321243
}

0 commit comments

Comments
 (0)