Skip to content

Commit

Permalink
CSS Nesting: nested selector matching
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=249746
rdar://103147183

Reviewed by Antti Koivisto.

This patch implements nested selector matching by
creating the flat equivalent of the nested selector
at rule set builder time.
All the following steps (matching, selector jit compilation,...)
should be automatically working because they will operate on regular flat selector,
without any parent selector inside them.

* LayoutTests/TestExpectations:
* LayoutTests/imported/w3c/web-platform-tests/css/css-nesting/invalidation-001-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-nesting/invalidation-002-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/css-nesting/invalidation-003-expected.txt:
* Source/WebCore/css/CSSSelector.cpp:
(WebCore::simpleSelectorSpecificity):
(WebCore::CSSSelector::selectorText const):
(WebCore::CSSSelector::RareData::RareData):
(WebCore::CSSSelector::RareData::deepCopy const):
(WebCore::CSSSelector::CSSSelector):
(WebCore::CSSSelector::visitAllSimpleSelectors const):
(WebCore::CSSSelector::resolveNestingParentSelectors):
(WebCore::CSSSelector::hasExplicitNestingParent const):
* Source/WebCore/css/CSSSelector.h:
(WebCore::CSSSelector::selectorList):
(WebCore::CSSSelector::setNotLastInSelectorList):
(WebCore::CSSSelector::isFirstInTagHistory const):
(WebCore::CSSSelector::isLastInTagHistory const):
(WebCore::CSSSelector::setLastInTagHistory):
(WebCore::CSSSelector::tagHistory):
(WebCore::CSSSelector::CSSSelector): Deleted.
* Source/WebCore/css/CSSSelectorList.cpp:
(WebCore::CSSSelectorList::CSSSelectorList):
* Source/WebCore/css/SelectorChecker.cpp:
(WebCore::SelectorChecker::checkOne const):
* Source/WebCore/css/StyleRule.cpp:
(WebCore::StyleRule::StyleRule):
* Source/WebCore/css/StyleRule.h:
* Source/WebCore/css/parser/CSSParserSelector.cpp:
(WebCore::CSSParserSelector::CSSParserSelector):
* Source/WebCore/css/parser/CSSParserSelector.h:
(WebCore::CSSParserSelector::selector):
* Source/WebCore/css/parser/CSSSelectorParser.cpp:
(WebCore::CSSSelectorParser::consumeRelativeNestedSelector):
(WebCore::CSSSelectorParser::consumeNesting):
(WebCore::CSSSelectorParser::resolveNestingParent):
* Source/WebCore/css/parser/CSSSelectorParser.h:
* Source/WebCore/cssjit/SelectorCompiler.cpp:
(WebCore::SelectorCompiler::addPseudoClassType):
* Source/WebCore/style/RuleData.h:
(WebCore::Style::RuleData::selector const):
* Source/WebCore/style/RuleSetBuilder.cpp:
(WebCore::Style::RuleSetBuilder::addStyleRule):
* Source/WebCore/style/RuleSetBuilder.h:

Canonical link: https://commits.webkit.org/258367@main
  • Loading branch information
mdubet committed Dec 31, 2022
1 parent 763cf9e commit 8972793
Show file tree
Hide file tree
Showing 18 changed files with 214 additions and 57 deletions.
2 changes: 0 additions & 2 deletions LayoutTests/TestExpectations
Expand Up @@ -6183,5 +6183,3 @@ webkit.org/b/245905 imported/w3c/web-platform-tests/css/css-text/line-breaking/l
# CSS Nesting
imported/w3c/web-platform-tests/css/css-nesting/conditional-properties.html [ ImageOnlyFailure ]
imported/w3c/web-platform-tests/css/css-nesting/conditional-rules.html [ ImageOnlyFailure ]
imported/w3c/web-platform-tests/css/css-nesting/implicit-nesting.html [ ImageOnlyFailure ]
imported/w3c/web-platform-tests/css/css-nesting/nesting-basic.html [ ImageOnlyFailure ]
@@ -1,4 +1,4 @@
Test passes if color is green.

FAIL CSS Selectors nested invalidation on changed parent assert_equals: expected "rgb(0, 128, 0)" but got "rgb(255, 0, 0)"
PASS CSS Selectors nested invalidation on changed parent

@@ -1,4 +1,4 @@
Test passes if color is green.

FAIL CSS Selectors nested invalidation on changed child assert_equals: expected "rgb(255, 0, 0)" but got "rgb(0, 128, 0)"
PASS CSS Selectors nested invalidation on changed child

@@ -1,4 +1,4 @@
Test passes if color is green.

FAIL CSS Selectors nested invalidation with :has() assert_equals: expected "rgb(0, 128, 0)" but got "rgb(0, 0, 0)"
PASS CSS Selectors nested invalidation with :has()

99 changes: 97 additions & 2 deletions Source/WebCore/css/CSSSelector.cpp
Expand Up @@ -165,7 +165,7 @@ SelectorSpecificity simpleSelectorSpecificity(const CSSSelector& simpleSelector)
return SelectorSpecificityIncrement::ClassB + maxSpecificity(simpleSelector.selectorList());
case CSSSelector::PseudoClassRelativeScope:
return 0;
case CSSSelector::PseudoClassParent:
case CSSSelector::PseudoClassNestingParent:
ASSERT_NOT_REACHED();
return { };
default:
Expand Down Expand Up @@ -653,7 +653,7 @@ String CSSSelector::selectorText(StringView separator, StringView rightSide) con
case CSSSelector::PseudoClassOptional:
builder.append(":optional");
break;
case CSSSelector::PseudoClassParent:
case CSSSelector::PseudoClassNestingParent:
builder.append('&');
break;
case CSSSelector::PseudoClassIs: {
Expand Down Expand Up @@ -929,6 +929,25 @@ CSSSelector::RareData::RareData(AtomString&& value)
{
}

CSSSelector::RareData::RareData(const RareData& other)
: matchingValue(other.matchingValue)
, serializingValue(other.serializingValue)
, a(other.a)
, b(other.b)
, attribute(other.attribute)
, attributeCanonicalLocalName(other.attributeCanonicalLocalName)
, argument(other.argument)
, argumentList(other.argumentList)
{
if (other.selectorList)
this->selectorList = makeUnique<CSSSelectorList>(*other.selectorList);
};

auto CSSSelector::RareData::deepCopy() const -> Ref<RareData>
{
return adoptRef(*new RareData (*this));
}

CSSSelector::RareData::~RareData() = default;

auto CSSSelector::RareData::create(AtomString value) -> Ref<RareData>
Expand All @@ -945,4 +964,80 @@ bool CSSSelector::RareData::matchNth(int count)
return count == b;
}

CSSSelector::CSSSelector(const CSSSelector& other)
: m_relation(other.m_relation)
, m_match(other.m_match)
, m_pseudoType(other.m_pseudoType)
, m_isLastInSelectorList(other.m_isLastInSelectorList)
, m_isFirstInTagHistory(other.m_isFirstInTagHistory)
, m_isLastInTagHistory(other.m_isLastInTagHistory)
, m_hasRareData(other.m_hasRareData)
, m_hasNameWithCase(other.m_hasNameWithCase)
, m_isForPage(other.m_isForPage)
, m_tagIsForNamespaceRule(other.m_tagIsForNamespaceRule)
, m_caseInsensitiveAttributeValueMatching(other.m_caseInsensitiveAttributeValueMatching)
{
if (other.m_hasRareData) {
auto copied = other.m_data.rareData->deepCopy();
m_data.rareData = &copied.leakRef();
m_data.rareData->ref();
} else if (other.m_hasNameWithCase) {
m_data.nameWithCase = other.m_data.nameWithCase;
m_data.nameWithCase->ref();
} else if (other.match() == Tag) {
m_data.tagQName = other.m_data.tagQName;
m_data.tagQName->ref();
} else if (other.m_data.value) {
m_data.value = other.m_data.value;
m_data.value->ref();
}
}

void CSSSelector::visitAllSimpleSelectors(auto& apply) const
{
// Effective C++ advices for this cast to deal with generic const/non-const member function.
apply(*const_cast<CSSSelector*>(this));

// Visit the selector list member (if any) recursively (such as: :has(<list>), :is(<list>),...)
if (auto selectorList = this->selectorList()) {
auto next = selectorList->first();
while (next) {
next->visitAllSimpleSelectors(apply);
next = CSSSelectorList::next(next);
}
}

// Visit the next simple selector recursively
if (auto next = tagHistory())
next->visitAllSimpleSelectors(apply);
}

void CSSSelector::resolveNestingParentSelectors(const CSSSelectorList& parent)
{
auto replaceParentSelector = [&parent] (CSSSelector& selector) {
if (selector.match() == CSSSelector::PseudoClass && selector.pseudoClassType() == CSSSelector::PseudoClassNestingParent) {
selector.setMatch(Match::PseudoClass);
// FIXME: Optimize cases where we can include the parent selector directly instead of wrapping it in a ":is" pseudo class.
selector.setPseudoClassType(PseudoClassType::PseudoClassIs);
selector.setSelectorList(makeUnique<CSSSelectorList>(parent));
}
};

visitAllSimpleSelectors(replaceParentSelector);
}

bool CSSSelector::hasExplicitNestingParent() const
{
bool result = false;

auto checkForExplicitParent = [&result] (const CSSSelector& selector) {
if (selector.match() == CSSSelector::PseudoClass && selector.pseudoClassType() == CSSSelector::PseudoClassNestingParent)
result = true;
};

visitAllSimpleSelectors(checkForExplicitParent);

return result;
}

} // namespace WebCore
46 changes: 16 additions & 30 deletions Source/WebCore/css/CSSSelector.h
Expand Up @@ -63,6 +63,11 @@ struct PossiblyQuotedIdentifier {
std::array<uint8_t, 3> computeSpecificityTuple() const;
unsigned specificityForPage() const;

void visitAllSimpleSelectors(auto& apply) const;

bool hasExplicitNestingParent() const;
void resolveNestingParentSelectors(const CSSSelectorList& parent);

// How the attribute value has to match. Default is Exact.
enum Match {
Unknown = 0,
Expand Down Expand Up @@ -187,7 +192,7 @@ struct PossiblyQuotedIdentifier {
PseudoClassModal,
PseudoClassUserInvalid,
PseudoClassUserValid,
PseudoClassParent,
PseudoClassNestingParent,
};

enum PseudoElementType {
Expand Down Expand Up @@ -263,6 +268,7 @@ struct PossiblyQuotedIdentifier {
bool attributeValueMatchingIsCaseInsensitive() const;
const FixedVector<PossiblyQuotedIdentifier>* argumentList() const { return m_hasRareData ? &m_data.rareData->argumentList : nullptr; }
const CSSSelectorList* selectorList() const { return m_hasRareData ? m_data.rareData->selectorList.get() : nullptr; }
CSSSelectorList* selectorList() { return m_hasRareData ? m_data.rareData->selectorList.get() : nullptr; }

void setValue(const AtomString&, bool matchLowerCase = false);

Expand Down Expand Up @@ -305,10 +311,14 @@ struct PossiblyQuotedIdentifier {

bool isLastInSelectorList() const { return m_isLastInSelectorList; }
void setLastInSelectorList() { m_isLastInSelectorList = true; }
void setNotLastInSelectorList() { m_isLastInSelectorList = false; }

bool isFirstInTagHistory() const { return m_isFirstInTagHistory; }
bool isLastInTagHistory() const { return m_isLastInTagHistory; }
void setNotFirstInTagHistory() { m_isFirstInTagHistory = false; }

bool isLastInTagHistory() const { return m_isLastInTagHistory; }
void setNotLastInTagHistory() { m_isLastInTagHistory = false; }
void setLastInTagHistory() { m_isLastInTagHistory = true; }

bool isForPage() const { return m_isForPage; }
void setForPage() { m_isForPage = true; }
Expand All @@ -330,6 +340,7 @@ struct PossiblyQuotedIdentifier {
#endif

unsigned simpleSelectorSpecificityForPage() const;
CSSSelector* tagHistory() { return m_isLastInTagHistory ? nullptr : this + 1; }

CSSSelector& operator=(const CSSSelector&) = delete;

Expand All @@ -354,8 +365,11 @@ struct PossiblyQuotedIdentifier {
FixedVector<PossiblyQuotedIdentifier> argumentList; // Used for :lang and ::part arguments.
std::unique_ptr<CSSSelectorList> selectorList; // Used for :is(), :matches(), and :not().

Ref<RareData> deepCopy() const;

private:
RareData(AtomString&& value);
RareData(const RareData& other);
};
void createRareData();

Expand Down Expand Up @@ -493,34 +507,6 @@ inline void CSSSelector::setValue(const AtomString& value, bool matchLowerCase)
m_data.rareData->serializingValue = value;
}

inline CSSSelector::CSSSelector(const CSSSelector& o)
: m_relation(o.m_relation)
, m_match(o.m_match)
, m_pseudoType(o.m_pseudoType)
, m_isLastInSelectorList(o.m_isLastInSelectorList)
, m_isFirstInTagHistory(o.m_isFirstInTagHistory)
, m_isLastInTagHistory(o.m_isLastInTagHistory)
, m_hasRareData(o.m_hasRareData)
, m_hasNameWithCase(o.m_hasNameWithCase)
, m_isForPage(o.m_isForPage)
, m_tagIsForNamespaceRule(o.m_tagIsForNamespaceRule)
, m_caseInsensitiveAttributeValueMatching(o.m_caseInsensitiveAttributeValueMatching)
{
if (o.m_hasRareData) {
m_data.rareData = o.m_data.rareData;
m_data.rareData->ref();
} else if (o.m_hasNameWithCase) {
m_data.nameWithCase = o.m_data.nameWithCase;
m_data.nameWithCase->ref();
} else if (o.match() == Tag) {
m_data.tagQName = o.m_data.tagQName;
m_data.tagQName->ref();
} else if (o.m_data.value) {
m_data.value = o.m_data.value;
m_data.value->ref();
}
}

inline CSSSelector::~CSSSelector()
{
ASSERT_WITH_SECURITY_IMPLICATION(!m_destructorHasBeenCalled);
Expand Down
5 changes: 3 additions & 2 deletions Source/WebCore/css/CSSSelectorList.cpp
Expand Up @@ -36,7 +36,8 @@ namespace WebCore {
CSSSelectorList::CSSSelectorList(const CSSSelectorList& other)
{
unsigned otherComponentCount = other.componentCount();
ASSERT_WITH_SECURITY_IMPLICATION(otherComponentCount);
if (!otherComponentCount)
return;

m_selectorArray = makeUniqueArray<CSSSelector>(otherComponentCount);
for (unsigned i = 0; i < otherComponentCount; ++i)
Expand Down Expand Up @@ -70,7 +71,7 @@ CSSSelectorList::CSSSelectorList(Vector<std::unique_ptr<CSSParserSelector>>&& se
if (current != first)
m_selectorArray[arrayIndex].setNotFirstInTagHistory();
current = current->tagHistory();
ASSERT(!m_selectorArray[arrayIndex].isLastInSelectorList());
ASSERT(!m_selectorArray[arrayIndex].isLastInSelectorList() || (flattenedSize == arrayIndex + 1));
if (current)
m_selectorArray[arrayIndex].setNotLastInTagHistory();
++arrayIndex;
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/css/SelectorChecker.cpp
Expand Up @@ -723,7 +723,7 @@ bool SelectorChecker::checkOne(CheckingContext& checkingContext, const LocalCont
// Normal element pseudo class checking.
switch (selector.pseudoClassType()) {
// Pseudo classes:
case CSSSelector::PseudoClassParent:
case CSSSelector::PseudoClassNestingParent:
// This pseudo selector should have been replaced earlier.
case CSSSelector::PseudoClassNot:
ASSERT_NOT_REACHED();
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/css/StyleRule.cpp
Expand Up @@ -36,6 +36,7 @@
#include "CSSMediaRule.h"
#include "CSSNamespaceRule.h"
#include "CSSPageRule.h"
#include "CSSParserSelector.h"
#include "CSSPropertyRule.h"
#include "CSSStyleRule.h"
#include "CSSSupportsRule.h"
Expand Down Expand Up @@ -227,6 +228,7 @@ StyleRule::StyleRule(const StyleRule& o)
: StyleRuleBase(o)
, m_properties(o.properties().mutableCopy())
, m_selectorList(o.m_selectorList)
, m_resolvedSelectorList(o.m_resolvedSelectorList)
, m_nestedRules(o.m_nestedRules)
, m_isSplitRule(o.m_isSplitRule)
, m_isLastRuleInSplitRule(o.m_isLastRuleInSplitRule)
Expand Down
10 changes: 8 additions & 2 deletions Source/WebCore/css/StyleRule.h
Expand Up @@ -110,6 +110,12 @@ class StyleRule final : public StyleRuleBase {
~StyleRule();

const CSSSelectorList& selectorList() const { return m_selectorList; }
const CSSSelectorList& resolvedSelectorList() const
{
if (!m_resolvedSelectorList.isEmpty())
return m_resolvedSelectorList;
return m_selectorList;
}

const StyleProperties& properties() const { return m_properties.get(); }
MutableStyleProperties& mutableProperties();
Expand All @@ -133,6 +139,7 @@ class StyleRule final : public StyleRuleBase {
static unsigned averageSizeInBytes();
void setProperties(Ref<StyleProperties> properties) { m_properties = properties; }
void setNestedRules(Vector<Ref<StyleRule>> nestedRules) { m_nestedRules = nestedRules; }
void setResolvedSelectorList(CSSSelectorList&& resolvedSelectorList) const { m_resolvedSelectorList = WTFMove(resolvedSelectorList); }
const Vector<Ref<StyleRule>>& nestedRules() const { return m_nestedRules; }
void appendNestedRule(Ref<StyleRule> rule) { m_nestedRules.append(rule); }

Expand All @@ -145,8 +152,7 @@ class StyleRule final : public StyleRuleBase {

mutable Ref<StyleProperties> m_properties;
CSSSelectorList m_selectorList;

// CSS Nesting
mutable CSSSelectorList m_resolvedSelectorList { };
Vector<Ref<StyleRule>> m_nestedRules;

#if ENABLE(CSS_SELECTOR_JIT)
Expand Down
8 changes: 8 additions & 0 deletions Source/WebCore/css/parser/CSSParserSelector.cpp
Expand Up @@ -105,6 +105,14 @@ CSSParserSelector::CSSParserSelector(const QualifiedName& tagQName)
{
}

CSSParserSelector::CSSParserSelector(const CSSSelector& selector)
{
m_selector = makeUnique<CSSSelector>(selector);
if (auto next = selector.tagHistory())
m_tagHistory = makeUnique<CSSParserSelector>(*next);
}


CSSParserSelector::~CSSParserSelector()
{
if (!m_tagHistory)
Expand Down
7 changes: 7 additions & 0 deletions Source/WebCore/css/parser/CSSParserSelector.h
Expand Up @@ -40,10 +40,16 @@ class CSSParserSelector {
static std::unique_ptr<CSSParserSelector> parsePagePseudoSelector(StringView);

CSSParserSelector();

// Recursively copy the selector chain.
CSSParserSelector(const CSSSelector&);

explicit CSSParserSelector(const QualifiedName&);

~CSSParserSelector();

std::unique_ptr<CSSSelector> releaseSelector() { return WTFMove(m_selector); }
CSSSelector* selector() { return m_selector.get(); }

void setValue(const AtomString& value, bool matchLowerCase = false) { m_selector->setValue(value, matchLowerCase); }

Expand Down Expand Up @@ -91,6 +97,7 @@ class CSSParserSelector {
void prependTagSelector(const QualifiedName&, bool tagIsForNamespaceRule = false);
std::unique_ptr<CSSParserSelector> releaseTagHistory();


private:
std::unique_ptr<CSSSelector> m_selector;
std::unique_ptr<CSSParserSelector> m_tagHistory;
Expand Down

0 comments on commit 8972793

Please sign in to comment.