Bug 1543808 - Refactor the selector parser to make implementing ::part() easier. r=xidorn
authorEmilio Cobos Álvarez <emilio@crisal.io>
Tue, 16 Apr 2019 13:16:56 +0000
changeset 469663 4b227d4b3377
parent 469662 6c23754dcb40
child 469664 2fcb6eeb5afd
push id35879
push usernerli@mozilla.com
push dateTue, 16 Apr 2019 22:01:48 +0000
treeherdermozilla-central@12a60898fdc1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersxidorn
bugs1543808
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1543808 - Refactor the selector parser to make implementing ::part() easier. r=xidorn ::slotted() is already weird in the sense that it supports a pseudo-element afterwards (so ::slotted(*)::before is valid for example). ::part() is weirder because you are supposed to allow stuff like ::part(foo):hover, ::part(foo):hover::before, etc. In order to avoid making the already-complex parse_compound_selector more complex, shuffle stuff so that we pass the progress of our current compound selector around, and is the parsing code for each selector which decides whether it's ok to parse at the given point. Differential Revision: https://phabricator.services.mozilla.com/D27158
servo/components/selectors/parser.rs
servo/components/style/gecko/pseudo_element.rs
servo/components/style/gecko/selector_parser.rs
--- a/servo/components/selectors/parser.rs
+++ b/servo/components/selectors/parser.rs
@@ -25,62 +25,110 @@ use thin_slice::ThinBoxedSlice;
 
 /// A trait that represents a pseudo-element.
 pub trait PseudoElement: Sized + ToCss {
     /// The `SelectorImpl` this pseudo-element is used for.
     type Impl: SelectorImpl;
 
     /// Whether the pseudo-element supports a given state selector to the right
     /// of it.
-    fn supports_pseudo_class(
-        &self,
-        _pseudo_class: &<Self::Impl as SelectorImpl>::NonTSPseudoClass,
-    ) -> bool {
-        false
-    }
+    fn accepts_state_pseudo_classes(&self) -> bool { false }
 
     /// Whether this pseudo-element is valid after a ::slotted(..) pseudo.
-    fn valid_after_slotted(&self) -> bool {
-        false
-    }
+    fn valid_after_slotted(&self) -> bool { false }
 }
 
 /// A trait that represents a pseudo-class.
 pub trait NonTSPseudoClass: Sized + ToCss {
     /// The `SelectorImpl` this pseudo-element is used for.
     type Impl: SelectorImpl;
 
     /// Whether this pseudo-class is :active or :hover.
     fn is_active_or_hover(&self) -> bool;
+
+    /// Whether this pseudo-class belongs to:
+    ///
+    /// https://drafts.csswg.org/selectors-4/#useraction-pseudos
+    fn is_user_action_state(&self) -> bool;
 }
 
 /// Returns a Cow::Borrowed if `s` is already ASCII lowercase, and a
 /// Cow::Owned if `s` had to be converted into ASCII lowercase.
 fn to_ascii_lowercase(s: &str) -> Cow<str> {
     if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') {
         let mut string = s.to_owned();
         string[first_uppercase..].make_ascii_lowercase();
         string.into()
     } else {
         s.into()
     }
 }
 
+bitflags! {
+    /// Flags that indicate at which point of parsing a selector are we.
+    struct SelectorParsingState: u8 {
+        /// Whether we're inside a negation. If we're inside a negation, we're
+        /// not allowed to add another negation or such, for example.
+        const INSIDE_NEGATION = 1 << 0;
+        /// Whether we've parsed an ::slotted() pseudo-element already.
+        ///
+        /// If so, then we can only parse a subset of pseudo-elements, and
+        /// whatever comes after them if so.
+        const AFTER_SLOTTED = 1 << 1;
+        /// Whether we've parsed a pseudo-element (as in, an
+        /// `Impl::PseudoElement` thus not accounting for `::slotted`) already.
+        ///
+        /// If so, then other pseudo-elements and most other selectors are
+        /// disallowed.
+        const AFTER_PSEUDO_ELEMENT = 1 << 2;
+        /// Whether we've parsed a non-stateful pseudo-element (again, as-in
+        /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are
+        /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set
+        /// as well.
+        const AFTER_NON_STATEFUL_PSEUDO_ELEMENT = 1 << 3;
+        /// Whether we are after any of the pseudo-like things.
+        const AFTER_PSEUDO = Self::AFTER_SLOTTED.bits | Self::AFTER_PSEUDO_ELEMENT.bits;
+    }
+}
+
+impl SelectorParsingState {
+    #[inline]
+    fn allows_functional_pseudo_classes(self) -> bool {
+        !self.intersects(SelectorParsingState::AFTER_PSEUDO)
+    }
+
+    #[inline]
+    fn allows_slotted(self) -> bool {
+        !self.intersects(SelectorParsingState::AFTER_PSEUDO)
+    }
+
+    #[inline]
+    fn allows_non_functional_pseudo_classes(self) -> bool {
+        !self.intersects(SelectorParsingState::AFTER_SLOTTED | SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT)
+    }
+
+    #[inline]
+    fn allows_tree_structural_pseudo_classes(self) -> bool {
+        !self.intersects(SelectorParsingState::AFTER_PSEUDO)
+    }
+}
+
 pub type SelectorParseError<'i> = ParseError<'i, SelectorParseErrorKind<'i>>;
 
 #[derive(Clone, Debug, PartialEq)]
 pub enum SelectorParseErrorKind<'i> {
     PseudoElementInComplexSelector,
     NoQualifiedNameInAttributeSelector(Token<'i>),
     EmptySelector,
     DanglingCombinator,
     NonSimpleSelectorInNegation,
     NonCompoundSelector,
     NonPseudoElementAfterSlotted,
     InvalidPseudoElementAfterSlotted,
+    InvalidState,
     UnexpectedTokenInAttributeSelector(Token<'i>),
     PseudoElementExpectedColon(Token<'i>),
     PseudoElementExpectedIdent(Token<'i>),
     NoIdentForPseudo(Token<'i>),
     UnsupportedPseudoClassOrElement(CowRcStr<'i>),
     UnexpectedIdent(CowRcStr<'i>),
     ExpectedNamespace(CowRcStr<'i>),
     ExpectedBarInAttr(Token<'i>),
@@ -1364,19 +1412,19 @@ where
 {
     let mut builder = SelectorBuilder::default();
 
     let mut has_pseudo_element;
     let mut slotted;
     'outer_loop: loop {
         // Parse a sequence of simple selectors.
         match parse_compound_selector(parser, input, &mut builder)? {
-            Some((has_pseudo, slot)) => {
-                has_pseudo_element = has_pseudo;
-                slotted = slot;
+            Some(state) => {
+                has_pseudo_element = state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
+                slotted = state.intersects(SelectorParsingState::AFTER_SLOTTED);
             },
             None => {
                 return Err(input.new_custom_error(if builder.has_combinators() {
                     SelectorParseErrorKind::DanglingCombinator
                 } else {
                     SelectorParseErrorKind::EmptySelector
                 }));
             },
@@ -1843,17 +1891,17 @@ where
         Ok(result) => result,
         Err(ParseError {
             kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput),
             ..
         }) => return Err(input.new_custom_error(SelectorParseErrorKind::EmptyNegation)),
         Err(e) => return Err(e.into()),
     };
     if !is_type_sel {
-        match parse_one_simple_selector(parser, input, /* inside_negation = */ true)? {
+        match parse_one_simple_selector(parser, input, SelectorParsingState::INSIDE_NEGATION)? {
             Some(SimpleSelectorParseResult::SimpleSelector(s)) => {
                 sequence.push(s);
             },
             None => {
                 return Err(input.new_custom_error(SelectorParseErrorKind::EmptyNegation));
             },
             Some(SimpleSelectorParseResult::PseudoElement(_)) |
             Some(SimpleSelectorParseResult::SlottedPseudo(_)) => {
@@ -1870,24 +1918,21 @@ where
 }
 
 /// simple_selector_sequence
 /// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]*
 /// | [ HASH | class | attrib | pseudo | negation ]+
 ///
 /// `Err(())` means invalid selector.
 /// `Ok(None)` is an empty selector
-///
-/// The booleans represent whether a pseudo-element has been parsed, and whether
-/// ::slotted() has been parsed, respectively.
 fn parse_compound_selector<'i, 't, P, Impl>(
     parser: &P,
     input: &mut CssParser<'i, 't>,
     builder: &mut SelectorBuilder<Impl>,
-) -> Result<Option<(bool, bool)>, ParseError<'i, P::Error>>
+) -> Result<Option<SelectorParsingState>, ParseError<'i, P::Error>>
 where
     P: Parser<'i, Impl = Impl>,
     Impl: SelectorImpl,
 {
     input.skip_whitespace();
 
     let mut empty = true;
     if !parse_type_selector(parser, input, builder)? {
@@ -1896,157 +1941,86 @@ where
             // default namespace, there is an implicit "<defaultns>|*" type
             // selector.
             builder.push_simple_selector(Component::DefaultNamespace(url))
         }
     } else {
         empty = false;
     }
 
-    let mut pseudo = false;
-    let mut slot = false;
+    let mut state = SelectorParsingState::empty();
     loop {
         let parse_result =
-            match parse_one_simple_selector(parser, input, /* inside_negation = */ false)? {
+            match parse_one_simple_selector(parser, input, state)? {
                 None => break,
                 Some(result) => result,
             };
 
         empty = false;
 
-        let slotted_selector;
-        let pseudo_element;
-
         match parse_result {
             SimpleSelectorParseResult::SimpleSelector(s) => {
                 builder.push_simple_selector(s);
-                continue;
-            },
-            SimpleSelectorParseResult::PseudoElement(p) => {
-                slotted_selector = None;
-                pseudo_element = Some(p);
             },
             SimpleSelectorParseResult::SlottedPseudo(selector) => {
-                slotted_selector = Some(selector);
-                let maybe_pseudo =
-                    parse_one_simple_selector(parser, input, /* inside_negation = */ false)?;
-
-                pseudo_element = match maybe_pseudo {
-                    None => None,
-                    Some(SimpleSelectorParseResult::PseudoElement(pseudo)) => {
-                        if !pseudo.valid_after_slotted() {
-                            return Err(input.new_custom_error(
-                                SelectorParseErrorKind::InvalidPseudoElementAfterSlotted,
-                            ));
-                        }
-                        Some(pseudo)
-                    },
-                    Some(SimpleSelectorParseResult::SimpleSelector(..)) |
-                    Some(SimpleSelectorParseResult::SlottedPseudo(..)) => {
-                        return Err(input.new_custom_error(
-                            SelectorParseErrorKind::NonPseudoElementAfterSlotted,
-                        ));
-                    },
-                };
+                state.insert(SelectorParsingState::AFTER_SLOTTED);
+                if !builder.is_empty() {
+                    builder.push_combinator(Combinator::SlotAssignment);
+                }
+                builder.push_simple_selector(Component::Slotted(selector));
+            },
+            SimpleSelectorParseResult::PseudoElement(p) => {
+                state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT);
+                if !p.accepts_state_pseudo_classes() {
+                    state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT);
+                }
+                if !builder.is_empty() {
+                    builder.push_combinator(Combinator::PseudoElement);
+                }
+                builder.push_simple_selector(Component::PseudoElement(p));
             },
         }
-
-        debug_assert!(slotted_selector.is_some() || pseudo_element.is_some());
-        // Try to parse state to the right of the pseudo-element.
-        //
-        // There are only 3 allowable state selectors that can go on
-        // pseudo-elements as of right now.
-        let mut state_selectors = SmallVec::<[Component<Impl>; 3]>::new();
-        if let Some(ref p) = pseudo_element {
-            loop {
-                let location = input.current_source_location();
-                match input.next_including_whitespace() {
-                    Ok(&Token::Colon) => {},
-                    Ok(&Token::WhiteSpace(_)) | Err(_) => break,
-                    Ok(t) => {
-                        let e = SelectorParseErrorKind::PseudoElementExpectedColon(t.clone());
-                        return Err(location.new_custom_error(e));
-                    },
-                }
-
-                let location = input.current_source_location();
-                // TODO(emilio): Functional pseudo-classes too?
-                // We don't need it for now.
-                let name = match input.next_including_whitespace()? {
-                    &Token::Ident(ref name) => name.clone(),
-                    t => {
-                        return Err(location.new_custom_error(
-                            SelectorParseErrorKind::NoIdentForPseudo(t.clone()),
-                        ));
-                    },
-                };
-
-                let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name.clone())?;
-                if !p.supports_pseudo_class(&pseudo_class) {
-                    return Err(input.new_custom_error(
-                        SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
-                    ));
-                }
-                state_selectors.push(Component::NonTSPseudoClass(pseudo_class));
-            }
-        }
-
-        if let Some(slotted) = slotted_selector {
-            slot = true;
-            if !builder.is_empty() {
-                builder.push_combinator(Combinator::SlotAssignment);
-            }
-            builder.push_simple_selector(Component::Slotted(slotted));
-        }
-
-        if let Some(p) = pseudo_element {
-            pseudo = true;
-            if !builder.is_empty() {
-                builder.push_combinator(Combinator::PseudoElement);
-            }
-
-            builder.push_simple_selector(Component::PseudoElement(p));
-
-            for state_selector in state_selectors.drain() {
-                builder.push_simple_selector(state_selector);
-            }
-        }
-
-        break;
     }
     if empty {
         // An empty selector is invalid.
         Ok(None)
     } else {
-        Ok(Some((pseudo, slot)))
+        Ok(Some(state))
     }
 }
 
 fn parse_functional_pseudo_class<'i, 't, P, Impl>(
     parser: &P,
     input: &mut CssParser<'i, 't>,
     name: CowRcStr<'i>,
-    inside_negation: bool,
+    state: SelectorParsingState,
 ) -> Result<Component<Impl>, ParseError<'i, P::Error>>
 where
     P: Parser<'i, Impl = Impl>,
     Impl: SelectorImpl,
 {
+    if !state.allows_functional_pseudo_classes() {
+        return Err(input.new_custom_error(
+            SelectorParseErrorKind::InvalidState
+        ));
+    }
+    debug_assert!(state.allows_tree_structural_pseudo_classes());
     match_ignore_ascii_case! { &name,
         "nth-child" => return Ok(parse_nth_pseudo_class(input, Component::NthChild)?),
         "nth-of-type" => return Ok(parse_nth_pseudo_class(input, Component::NthOfType)?),
         "nth-last-child" => return Ok(parse_nth_pseudo_class(input, Component::NthLastChild)?),
         "nth-last-of-type" => return Ok(parse_nth_pseudo_class(input, Component::NthLastOfType)?),
         "host" => return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input)?))),
         "not" => {
-            if inside_negation {
+            if state.intersects(SelectorParsingState::INSIDE_NEGATION) {
                 return Err(input.new_custom_error(
                     SelectorParseErrorKind::UnexpectedIdent("not".into())
                 ));
             }
+            debug_assert!(state.is_empty());
             return parse_negation(parser, input)
         },
         _ => {}
     }
     P::parse_non_ts_functional_pseudo_class(parser, name, input).map(Component::NonTSPseudoClass)
 }
 
 fn parse_nth_pseudo_class<'i, 't, Impl, F>(
@@ -2075,126 +2049,155 @@ pub fn is_css2_pseudo_element(name: &str
 /// Parse a simple selector other than a type selector.
 ///
 /// * `Err(())`: Invalid selector, abort
 /// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed.
 /// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element
 fn parse_one_simple_selector<'i, 't, P, Impl>(
     parser: &P,
     input: &mut CssParser<'i, 't>,
-    inside_negation: bool,
+    state: SelectorParsingState,
 ) -> Result<Option<SimpleSelectorParseResult<Impl>>, ParseError<'i, P::Error>>
 where
     P: Parser<'i, Impl = Impl>,
     Impl: SelectorImpl,
 {
     let start = input.state();
-    // FIXME: remove clone() when lifetimes are non-lexical
-    match input.next_including_whitespace().map(|t| t.clone()) {
-        Ok(Token::IDHash(id)) => {
+    let token = match input.next_including_whitespace().map(|t| t.clone()) {
+        Ok(t) => t,
+        Err(..) => {
+            input.reset(&start);
+            return Ok(None);
+        }
+    };
+
+    Ok(Some(match token {
+        Token::IDHash(id) => {
+            if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+                return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+            }
             let id = Component::ID(id.as_ref().into());
-            Ok(Some(SimpleSelectorParseResult::SimpleSelector(id)))
+            SimpleSelectorParseResult::SimpleSelector(id)
         },
-        Ok(Token::Delim('.')) => {
+        Token::Delim('.') => {
+            if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+                return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+            }
             let location = input.current_source_location();
-            match *input.next_including_whitespace()? {
-                Token::Ident(ref class) => {
-                    let class = Component::Class(class.as_ref().into());
-                    Ok(Some(SimpleSelectorParseResult::SimpleSelector(class)))
-                },
+            let class = match *input.next_including_whitespace()? {
+                Token::Ident(ref class) => class,
                 ref t => {
                     let e = SelectorParseErrorKind::ClassNeedsIdent(t.clone());
-                    Err(location.new_custom_error(e))
+                    return Err(location.new_custom_error(e))
                 },
-            }
+            };
+            let class = Component::Class(class.as_ref().into());
+            SimpleSelectorParseResult::SimpleSelector(class)
         },
-        Ok(Token::SquareBracketBlock) => {
+        Token::SquareBracketBlock => {
+            if state.intersects(SelectorParsingState::AFTER_PSEUDO) {
+                return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+            }
             let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?;
-            Ok(Some(SimpleSelectorParseResult::SimpleSelector(attr)))
+            SimpleSelectorParseResult::SimpleSelector(attr)
         },
-        Ok(Token::Colon) => {
+        Token::Colon => {
             let location = input.current_source_location();
             let (is_single_colon, next_token) = match input.next_including_whitespace()?.clone() {
                 Token::Colon => (false, input.next_including_whitespace()?.clone()),
                 t => (true, t),
             };
             let (name, is_functional) = match next_token {
                 Token::Ident(name) => (name, false),
                 Token::Function(name) => (name, true),
                 t => {
                     let e = SelectorParseErrorKind::PseudoElementExpectedIdent(t);
                     return Err(input.new_custom_error(e));
                 },
             };
             let is_pseudo_element =
                 !is_single_colon || P::pseudo_element_allows_single_colon(&name);
             if is_pseudo_element {
-                let parse_result = if is_functional {
+                if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) {
+                    return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+                }
+                let pseudo_element = if is_functional {
                     if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") {
+                        if !state.allows_slotted() {
+                            return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+                        }
                         let selector = input.parse_nested_block(|input| {
                             parse_inner_compound_selector(parser, input)
                         })?;
-                        SimpleSelectorParseResult::SlottedPseudo(selector)
-                    } else {
-                        let selector = input.parse_nested_block(|input| {
-                            P::parse_functional_pseudo_element(parser, name, input)
-                        })?;
-                        SimpleSelectorParseResult::PseudoElement(selector)
+                        return Ok(Some(SimpleSelectorParseResult::SlottedPseudo(selector)));
                     }
+                    input.parse_nested_block(|input| {
+                        P::parse_functional_pseudo_element(parser, name, input)
+                    })?
                 } else {
-                    SimpleSelectorParseResult::PseudoElement(P::parse_pseudo_element(
-                        parser, location, name,
-                    )?)
+                    P::parse_pseudo_element(parser, location, name)?
                 };
-                Ok(Some(parse_result))
+
+                if state.intersects(SelectorParsingState::AFTER_SLOTTED) && !pseudo_element.valid_after_slotted() {
+                    return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState));
+                }
+                SimpleSelectorParseResult::PseudoElement(pseudo_element)
             } else {
                 let pseudo_class = if is_functional {
                     input.parse_nested_block(|input| {
-                        parse_functional_pseudo_class(parser, input, name, inside_negation)
+                        parse_functional_pseudo_class(parser, input, name, state)
                     })?
                 } else {
-                    parse_simple_pseudo_class(parser, location, name)?
+                    parse_simple_pseudo_class(parser, location, name, state)?
                 };
-                Ok(Some(SimpleSelectorParseResult::SimpleSelector(
-                    pseudo_class,
-                )))
+                SimpleSelectorParseResult::SimpleSelector(pseudo_class)
             }
         },
         _ => {
             input.reset(&start);
-            Ok(None)
+            return Ok(None)
         },
-    }
+    }))
 }
 
 fn parse_simple_pseudo_class<'i, P, Impl>(
     parser: &P,
     location: SourceLocation,
     name: CowRcStr<'i>,
+    state: SelectorParsingState,
 ) -> Result<Component<Impl>, ParseError<'i, P::Error>>
 where
     P: Parser<'i, Impl = Impl>,
     Impl: SelectorImpl,
 {
-    (match_ignore_ascii_case! { &name,
-        "first-child" => Ok(Component::FirstChild),
-        "last-child"  => Ok(Component::LastChild),
-        "only-child"  => Ok(Component::OnlyChild),
-        "root" => Ok(Component::Root),
-        "empty" => Ok(Component::Empty),
-        "scope" => Ok(Component::Scope),
-        "host" if P::parse_host(parser) => Ok(Component::Host(None)),
-        "first-of-type" => Ok(Component::FirstOfType),
-        "last-of-type" => Ok(Component::LastOfType),
-        "only-of-type" => Ok(Component::OnlyOfType),
-        _ => Err(())
-    })
-    .or_else(|()| {
-        P::parse_non_ts_pseudo_class(parser, location, name).map(Component::NonTSPseudoClass)
-    })
+    if !state.allows_non_functional_pseudo_classes() {
+        return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState));
+    }
+
+    if state.allows_tree_structural_pseudo_classes() {
+        match_ignore_ascii_case! { &name,
+            "first-child" => return Ok(Component::FirstChild),
+            "last-child" => return Ok(Component::LastChild),
+            "only-child" => return Ok(Component::OnlyChild),
+            "root" => return Ok(Component::Root),
+            "empty" => return Ok(Component::Empty),
+            "scope" => return Ok(Component::Scope),
+            "host" if P::parse_host(parser) => return Ok(Component::Host(None)),
+            "first-of-type" => return Ok(Component::FirstOfType),
+            "last-of-type" => return Ok(Component::LastOfType),
+            "only-of-type" => return Ok(Component::OnlyOfType),
+            _ => {},
+        }
+    }
+
+    let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?;
+    if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) && !pseudo_class.is_user_action_state() {
+        return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState));
+    }
+    Ok(Component::NonTSPseudoClass(pseudo_class))
 }
 
 // NB: pub module in order to access the DummyParser
 #[cfg(test)]
 pub mod tests {
     use super::*;
     use crate::builder::HAS_PSEUDO_BIT;
     use crate::parser;
@@ -2213,35 +2216,33 @@ pub mod tests {
     pub enum PseudoElement {
         Before,
         After,
     }
 
     impl parser::PseudoElement for PseudoElement {
         type Impl = DummySelectorImpl;
 
-        fn supports_pseudo_class(&self, pc: &PseudoClass) -> bool {
-            match *pc {
-                PseudoClass::Hover => true,
-                PseudoClass::Active | PseudoClass::Lang(..) => false,
-            }
-        }
+        fn accepts_state_pseudo_classes(&self) -> bool { true }
 
-        fn valid_after_slotted(&self) -> bool {
-            true
-        }
+        fn valid_after_slotted(&self) -> bool { true }
     }
 
     impl parser::NonTSPseudoClass for PseudoClass {
         type Impl = DummySelectorImpl;
 
         #[inline]
         fn is_active_or_hover(&self) -> bool {
             matches!(*self, PseudoClass::Active | PseudoClass::Hover)
         }
+
+        #[inline]
+        fn is_user_action_state(&self) -> bool {
+            self.is_active_or_hover()
+        }
     }
 
     impl ToCss for PseudoClass {
         fn to_css<W>(&self, dest: &mut W) -> fmt::Result
         where
             W: fmt::Write,
         {
             match *self {
@@ -2784,21 +2785,21 @@ pub mod tests {
                 vec![
                     Component::PseudoElement(PseudoElement::Before),
                     Component::NonTSPseudoClass(PseudoClass::Hover),
                     Component::NonTSPseudoClass(PseudoClass::Hover),
                 ],
                 specificity(0, 2, 1) | HAS_PSEUDO_BIT,
             )]))
         );
-        assert!(parse("::before:hover:active").is_err());
+        assert!(parse("::before:hover:lang(foo)").is_err());
         assert!(parse("::before:hover .foo").is_err());
         assert!(parse("::before .foo").is_err());
         assert!(parse("::before ~ bar").is_err());
-        assert!(parse("::before:active").is_err());
+        assert!(parse("::before:active").is_ok());
 
         // https://github.com/servo/servo/issues/15335
         assert!(parse(":: before").is_err());
         assert_eq!(
             parse("div ::after"),
             Ok(SelectorList::from_vec(vec![Selector::from_vec(
                 vec![
                     Component::LocalName(LocalName {
--- a/servo/components/style/gecko/pseudo_element.rs
+++ b/servo/components/style/gecko/pseudo_element.rs
@@ -6,17 +6,17 @@
 //!
 //! Note that a few autogenerated bits of this live in
 //! `pseudo_element_definition.mako.rs`. If you touch that file, you probably
 //! need to update the checked-in files for Servo.
 
 use crate::gecko_bindings::structs::{self, PseudoStyleType};
 use crate::properties::longhands::display::computed_value::T as Display;
 use crate::properties::{ComputedValues, PropertyFlags};
-use crate::selector_parser::{NonTSPseudoClass, PseudoElementCascadeType, SelectorImpl};
+use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl};
 use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
 use crate::string_cache::Atom;
 use crate::values::serialize_atom_identifier;
 use cssparser::ToCss;
 use std::fmt;
 use thin_slice::ThinBoxedSlice;
 
 include!(concat!(
@@ -25,32 +25,30 @@ include!(concat!(
 ));
 
 impl ::selectors::parser::PseudoElement for PseudoElement {
     type Impl = SelectorImpl;
 
     // ::slotted() should support all tree-abiding pseudo-elements, see
     // https://drafts.csswg.org/css-scoping/#slotted-pseudo
     // https://drafts.csswg.org/css-pseudo-4/#treelike
+    #[inline]
     fn valid_after_slotted(&self) -> bool {
         matches!(
             *self,
             PseudoElement::Before |
                 PseudoElement::After |
                 PseudoElement::Marker |
                 PseudoElement::Placeholder
         )
     }
 
-    fn supports_pseudo_class(&self, pseudo_class: &NonTSPseudoClass) -> bool {
-        if !self.supports_user_action_state() {
-            return false;
-        }
-
-        return pseudo_class.is_safe_user_action_state();
+    #[inline]
+    fn accepts_state_pseudo_classes(&self) -> bool {
+        self.supports_user_action_state()
     }
 }
 
 impl PseudoElement {
     /// Returns the kind of cascade type that a given pseudo is going to use.
     ///
     /// In Gecko we only compute ::before and ::after eagerly. We save the rules
     /// for anonymous boxes separately, so we resolve them as precomputed
--- a/servo/components/style/gecko/selector_parser.rs
+++ b/servo/components/style/gecko/selector_parser.rs
@@ -179,26 +179,16 @@ impl NonTSPseudoClass {
             },
             // Otherwise, a pseudo-class is enabled in content when it
             // doesn't have any enabled flag.
             _ => !self
                 .has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME),
         }
     }
 
-    /// <https://drafts.csswg.org/selectors-4/#useraction-pseudos>
-    ///
-    /// We intentionally skip the link-related ones.
-    pub fn is_safe_user_action_state(&self) -> bool {
-        matches!(
-            *self,
-            NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus
-        )
-    }
-
     /// Get the state flag associated with a pseudo-class, if any.
     pub fn state_flag(&self) -> ElementState {
         macro_rules! flag {
             (_) => {
                 ElementState::empty()
             };
             ($state:ident) => {
                 ElementState::$state
@@ -274,16 +264,25 @@ impl NonTSPseudoClass {
 
 impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass {
     type Impl = SelectorImpl;
 
     #[inline]
     fn is_active_or_hover(&self) -> bool {
         matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover)
     }
+
+    /// We intentionally skip the link-related ones.
+    #[inline]
+    fn is_user_action_state(&self) -> bool {
+        matches!(
+            *self,
+            NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus
+        )
+    }
 }
 
 /// The dummy struct we use to implement our selector parsing.
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct SelectorImpl;
 
 impl ::selectors::SelectorImpl for SelectorImpl {
     type ExtraMatchingData = InvalidationMatchingData;