From 724ca617134fa78ecf289dc4ba247f043cb64a54 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 21 Jul 2022 16:51:40 -0700 Subject: [PATCH] Fix superselector bugs for pseudo-elements and universal selectors Closes #790 Closes sass/sass#2728 --- CHANGELOG.md | 5 ++ lib/sass.dart | 3 +- lib/src/ast/selector/pseudo.dart | 19 ++++++ lib/src/ast/selector/simple.dart | 31 ++++++++++ lib/src/ast/selector/type.dart | 6 ++ lib/src/ast/selector/universal.dart | 7 +++ lib/src/extend/functions.dart | 94 +++++++++++++++-------------- 7 files changed, 120 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f828ad04..d6d566d9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ * Deprecate passing non-`deg` units to `color.hwb()`'s `$hue` argument. +* Fix a number of bugs when determining whether selectors with pseudo-elements + are superselectors. + +* Treat `*` as a superselector of all selectors. + ### JS API * Add a `charset` option that controls whether or not Sass emits a diff --git a/lib/sass.dart b/lib/sass.dart index 0f77eaea0..e6d428646 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -26,7 +26,8 @@ export 'src/exception.dart' show SassException; export 'src/importer.dart'; export 'src/logger.dart'; export 'src/syntax.dart'; -export 'src/value.dart' hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat; +export 'src/value.dart' + hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat; export 'src/visitor/serialize.dart' show OutputStyle; export 'src/evaluation_context.dart' show warn; diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index c8f7927bf..c81c9afd5 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -8,6 +8,7 @@ import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; import '../../utils.dart'; +import '../../util/nullable.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -174,6 +175,24 @@ class PseudoSelector extends SimpleSelector { return result; } + bool isSuperselector(SimpleSelector other) { + if (super.isSuperselector(other)) return true; + + var selector = this.selector; + if (selector == null) return this == other; + if (other is PseudoSelector && + isElement && + other.isElement && + normalizedName == 'slotted' && + other.name == name) { + return other.selector.andThen(selector.isSuperselector) ?? false; + } + + // Fall back to the logic defined in functions.dart, which knows how to + // compare selector pseudoclasses against raw selectors. + return CompoundSelector([this]).isSuperselector(CompoundSelector([other])); + } + /// Computes [_minSpecificity] and [_maxSpecificity]. void _computeSpecificity() { if (isElement) { diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index fb980ec18..a4c0c6e96 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -9,6 +9,19 @@ import '../../logger.dart'; import '../../parse/selector.dart'; import '../selector.dart'; +/// Names of pseudo-classes that take selectors as arguments, and that are +/// subselectors of the union of their arguments. +/// +/// For example, `.foo` is a superselector of `:matches(.foo)`. +final _subselectorPseudos = { + 'is', + 'matches', + 'where', + 'any', + 'nth-child', + 'nth-last-child' +}; + /// An abstract superclass for simple selectors. /// /// {@category Selector} @@ -91,4 +104,22 @@ abstract class SimpleSelector extends Selector { return result; } + + /// Whether this is a superselector of [other]. + /// + /// That is, whether this matches every element that [other] matches, as well + /// as possibly additional elements. + bool isSuperselector(SimpleSelector other) { + if (this == other) return true; + if (other is PseudoSelector && other.isClass) { + var list = other.selector; + if (list != null && _subselectorPseudos.contains(other.normalizedName)) { + return list.components.every((complex) => + complex.components.isNotEmpty && + complex.components.last.selector.components + .any((simple) => isSuperselector(simple))); + } + } + return false; + } } diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart index 0d9276ce0..84304ae61 100644 --- a/lib/src/ast/selector/type.dart +++ b/lib/src/ast/selector/type.dart @@ -41,6 +41,12 @@ class TypeSelector extends SimpleSelector { } } + bool isSuperselector(SimpleSelector other) => + super.isSuperselector(other) || + (other is TypeSelector && + name.name == other.name.name && + (name.namespace == '*' || name.namespace == other.name.namespace)); + bool operator ==(Object other) => other is TypeSelector && other.name == name; int get hashCode => name.hashCode; diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index f42cf4015..ad03505da 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -47,6 +47,13 @@ class UniversalSelector extends SimpleSelector { return [this]; } + bool isSuperselector(SimpleSelector other) { + if (namespace == '*') return true; + if (other is TypeSelector) return namespace == other.name.namespace; + if (other is UniversalSelector) return namespace == other.namespace; + return namespace == null || super.isSuperselector(other); + } + bool operator ==(Object other) => other is UniversalSelector && other.namespace == namespace; diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index 2f7c5b33f..266d4cc2e 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -13,23 +13,11 @@ import 'dart:collection'; import 'package:collection/collection.dart'; +import 'package:tuple/tuple.dart'; import '../ast/selector.dart'; import '../utils.dart'; -/// Names of pseudo selectors that take selectors as arguments, and that are -/// subselectors of their arguments. -/// -/// For example, `.foo` is a superselector of `:matches(.foo)`. -final _subselectorPseudos = { - 'is', - 'matches', - 'where', - 'any', - 'nth-child', - 'nth-last-child' -}; - /// Returns the contents of a [SelectorList] that matches only elements that are /// matched by every complex selector in [complexes]. /// @@ -689,6 +677,29 @@ bool complexIsSuperselector(List complex1, bool compoundIsSuperselector( CompoundSelector compound1, CompoundSelector compound2, {Iterable? parents}) { + // Pseudo elements effectively change the target of a compound selector rather + // than narrowing the set of elements to which it applies like other + // selectors. As such, if either selector has a pseudo element, they both must + // have the _same_ pseudo element. + // + // In addition, order matters when pseudo-elements are involved. The selectors + // before them must + var tuple1 = _findPseudoElementIndexed(compound1); + var tuple2 = _findPseudoElementIndexed(compound2); + if (tuple1 != null && tuple2 != null) { + return tuple1.item1.isSuperselector(tuple2.item1) && + _compoundComponentsIsSuperselector( + compound1.components.take(tuple1.item2), + compound2.components.take(tuple2.item2), + parents: parents) && + _compoundComponentsIsSuperselector( + compound1.components.skip(tuple1.item2 + 1), + compound2.components.skip(tuple2.item2 + 1), + parents: parents); + } else if (tuple1 != null || tuple2 != null) { + return false; + } + // Every selector in [compound1.components] must have a matching selector in // [compound2.components]. for (var simple1 in compound1.components) { @@ -697,18 +708,7 @@ bool compoundIsSuperselector( parents: parents)) { return false; } - } else if (!_simpleIsSuperselectorOfCompound(simple1, compound2)) { - return false; - } - } - - // [compound1] can't be a superselector of a selector with non-selector - // pseudo-elements that [compound2] doesn't share. - for (var simple2 in compound2.components) { - if (simple2 is PseudoSelector && - simple2.isElement && - simple2.selector == null && - !_simpleIsSuperselectorOfCompound(simple2, compound1)) { + } else if (!compound2.components.any(simple1.isSuperselector)) { return false; } } @@ -716,30 +716,36 @@ bool compoundIsSuperselector( return true; } -/// Returns whether [simple] is a superselector of [compound]. +/// If [compound] contains a pseudo-element, returns it and its index in +/// [compound.components]. +Tuple2? _findPseudoElementIndexed( + CompoundSelector compound) { + for (var i = 0; i < compound.components.length; i++) { + var simple = compound.components[i]; + if (simple is PseudoSelector && simple.isElement) return Tuple2(simple, i); + } + return null; +} + +/// Like [compoundIsSuperselector] but operates on the underlying lists of +/// simple selectors. /// -/// That is, whether [simple] matches every element that [compound] matches, as -/// well as possibly additional elements. -bool _simpleIsSuperselectorOfCompound( - SimpleSelector simple, CompoundSelector compound) { - return compound.components.any((theirSimple) { - if (simple == theirSimple) return true; - - // Some selector pseudoclasses can match normal selectors. - if (theirSimple is! PseudoSelector) return false; - var selector = theirSimple.selector; - if (selector == null) return false; - if (!_subselectorPseudos.contains(theirSimple.normalizedName)) return false; - - return selector.components.every((complex) => - complex.singleCompound?.components.contains(simple) ?? false); - }); +/// The [compound1] and [compound2] are expected to have efficient +/// [Iterable.length] fields. +bool _compoundComponentsIsSuperselector( + Iterable compound1, Iterable compound2, + {Iterable? parents}) { + if (compound1.isEmpty) return true; + if (compound2.isEmpty) compound2 = [UniversalSelector(namespace: '*')]; + return compoundIsSuperselector( + CompoundSelector(compound1), CompoundSelector(compound2), + parents: parents); } /// Returns whether [pseudo1] is a superselector of [compound2]. /// -/// That is, whether [pseudo1] matches every element that [compound2] matches, as well -/// as possibly additional elements. +/// That is, whether [pseudo1] matches every element that [compound2] matches, +/// as well as possibly additional elements. /// /// This assumes that [pseudo1]'s `selector` argument is not `null`. ///