tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 053240086b07dc2c3d461113c49d5c8bf0165519
parent 6d7307c0cd1e0d7d3904f9a0e7f5103a5187db1e
Author: David Shin <dshin@mozilla.com>
Date:   Thu,  9 Oct 2025 10:25:48 +0000

Bug 1882379: Generate warning for useless :scope selectors. r=firefox-style-system-reviewers,webidl,layout-reviewers,smaug,emilio

Differential Revision: https://phabricator.services.mozilla.com/D268045

Diffstat:
Mdom/webidl/CSSStyleRule.webidl | 1+
Mlayout/inspector/tests/chrome/test_getSelectorWarnings.html | 13+++++++++++++
Mlayout/style/CSSStyleRule.cpp | 2++
Mservo/components/style/error_reporting.rs | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 87 insertions(+), 1 deletion(-)

diff --git a/dom/webidl/CSSStyleRule.webidl b/dom/webidl/CSSStyleRule.webidl @@ -38,6 +38,7 @@ interface CSSStyleRule : CSSGroupingRule { enum SelectorWarningKind { "UnconstrainedHas", + "SiblingCombinatorAfterScope", }; dictionary SelectorWarning { diff --git a/layout/inspector/tests/chrome/test_getSelectorWarnings.html b/layout/inspector/tests/chrome/test_getSelectorWarnings.html @@ -9,6 +9,10 @@ function unconstrainedHas(index) { return { index: index, kind: "UnconstrainedHas" }; } +function siblingCombinatorAfterScope(index) { + return { index: index, kind: "SiblingCombinatorAfterScope" }; +} + function testRule(rule, expected) { const index = sheet.insertRule(rule); const warnings = sheet.rules[index].getSelectorWarnings(); @@ -40,4 +44,13 @@ testRule(".bar:has(:is(* .bar) .foo), .bar:has(:is(:where(* .baz) .bar) .foo) {} testRule(".bar:has(:is(*, .bar) .foo) {}", [unconstrainedHas(0)]); testRule(":is(:has(*) .bar):has(.foo) {}", [unconstrainedHas(0)]); testRule(":is(:has(*)):has(.foo) {}", [unconstrainedHas(0)]); +// `:scope` selector with sibling +testRule(":scope ~ .foo {}", [siblingCombinatorAfterScope(0)]); +testRule(":scope ~ .foo, :scope + .foo {}", [siblingCombinatorAfterScope(0), siblingCombinatorAfterScope(1)]); +testRule(":scope .foo, :scope ~ .foo {}", [siblingCombinatorAfterScope(1)]); +testRule(":is(.foo :scope) ~ .bar, :is(.foo :scope) + .bar {}", [siblingCombinatorAfterScope(0), siblingCombinatorAfterScope(1)]); +testRule(":is(:scope .foo) ~ .bar, :is(:scope > .foo) ~ .bar {}", []); +testRule(":is(.foo :is(.bar :is(.baz :scope))) ~ .baa {}", [siblingCombinatorAfterScope(0)]); +testRule(".foo:has(:scope ~ .bar) {}", []); +testRule(".foo:has(:scope) ~ .bar {}", []); </script> diff --git a/layout/style/CSSStyleRule.cpp b/layout/style/CSSStyleRule.cpp @@ -350,6 +350,8 @@ SelectorWarningKind ToWebIDLSelectorWarningKind( switch (aKind) { case StyleSelectorWarningKind::UnconstraintedRelativeSelector: return SelectorWarningKind::UnconstrainedHas; + case StyleSelectorWarningKind::SiblingCombinatorAfterScopeSelector: + return SelectorWarningKind::SiblingCombinatorAfterScope; } MOZ_ASSERT_UNREACHABLE("Unhandled selector warning kind"); // Return something for assert-disabled builds. diff --git a/servo/components/style/error_reporting.rs b/servo/components/style/error_reporting.rs @@ -9,7 +9,7 @@ use crate::selector_parser::SelectorImpl; use crate::stylesheets::UrlExtraData; use cssparser::{BasicParseErrorKind, ParseErrorKind, SourceLocation, Token}; -use selectors::parser::{Component, RelativeSelector, Selector}; +use selectors::parser::{Combinator, Component, RelativeSelector, Selector}; use selectors::visitor::{SelectorListKind, SelectorVisitor}; use selectors::SelectorList; use std::fmt; @@ -282,6 +282,9 @@ pub enum SelectorWarningKind { /// Relative Selector with not enough constraint, either outside or inside the selector. e.g. `*:has(.a)`, `.a:has(*)`. /// May cause expensive invalidations for every element inserted and/or removed. UnconstraintedRelativeSelector, + /// `:scope` can have 3 meanings, but in all cases, the relationship is defined strictly by an ancestor-descendant + /// relationship. This means that any presence of sibling selectors to its right would make it never match. + SiblingCombinatorAfterScopeSelector, } impl SelectorWarningKind { @@ -291,6 +294,9 @@ impl SelectorWarningKind { if UnconstrainedRelativeSelectorVisitor::has_warning(selector, 0, false) { result.push(SelectorWarningKind::UnconstraintedRelativeSelector); } + if SiblingCombinatorAfterScopeSelectorVisitor::has_warning(selector) { + result.push(SelectorWarningKind::SiblingCombinatorAfterScopeSelector); + } result } } @@ -446,3 +452,67 @@ impl SelectorVisitor for UnconstrainedRelativeSelectorVisitor { true } } + +struct SiblingCombinatorAfterScopeSelectorVisitor { + right_combinator_is_sibling: bool, + found: bool, +} + +impl SiblingCombinatorAfterScopeSelectorVisitor { + fn new(right_combinator_is_sibling: bool) -> Self { + Self { + right_combinator_is_sibling, + found: false, + } + } + fn has_warning(selector: &Selector<SelectorImpl>) -> bool { + if !selector.has_scope_selector() { + return false; + } + let visitor = SiblingCombinatorAfterScopeSelectorVisitor::new(false); + visitor.find_never_matching_scope_selector(selector) + } + + fn find_never_matching_scope_selector(mut self, selector: &Selector<SelectorImpl>) -> bool { + selector.visit(&mut self); + self.found + } +} + +impl SelectorVisitor for SiblingCombinatorAfterScopeSelectorVisitor { + type Impl = SelectorImpl; + + fn visit_simple_selector(&mut self, c: &Component<Self::Impl>) -> bool { + if !matches!(c, Component::Scope | Component::ImplicitScope) { + return true; + } + // e.g. `:scope ~ .a` will never match. + if self.right_combinator_is_sibling { + self.found = true; + } + true + } + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<Self::Impl>], + ) -> bool { + for s in list { + let list_visitor = Self::new(self.right_combinator_is_sibling); + self.found |= list_visitor.find_never_matching_scope_selector(s); + } + true + } + + fn visit_complex_selector(&mut self, combinator_to_right: Option<Combinator>) -> bool { + if let Some(c) = combinator_to_right { + // Subject compounds' state is determined by the outer visitor. e.g: When there's `:is(.a .b) ~ .c`, + // the inner visitor is assumed to be constructed with right_combinator_is_sibling == true. + self.right_combinator_is_sibling = c.is_sibling(); + } + true + } + + // It's harder to discern if use of :scope <sibling-combinator> is invalid - at least for now, defer. +}