Skip to content

Commit

Permalink
Fix superselector bugs for pseudo-elements and universal selectors (#…
Browse files Browse the repository at this point in the history
…1753)

Closes #790
Closes sass/sass#2728
  • Loading branch information
nex3 committed Jul 22, 2022
1 parent eeedebc commit d159d83
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 44 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -18,6 +18,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.

### Dart API

* Add a top-level `fakeFromImport()` function for testing custom importers
Expand Down
19 changes: 19 additions & 0 deletions lib/src/ast/selector/pseudo.dart
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down
31 changes: 31 additions & 0 deletions lib/src/ast/selector/simple.dart
Expand Up @@ -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 AST}
Expand Down Expand Up @@ -92,4 +105,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;
}
}
6 changes: 6 additions & 0 deletions lib/src/ast/selector/type.dart
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions lib/src/ast/selector/universal.dart
Expand Up @@ -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;

Expand Down
94 changes: 50 additions & 44 deletions lib/src/extend/functions.dart
Expand Up @@ -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].
///
Expand Down Expand Up @@ -689,6 +677,29 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
bool compoundIsSuperselector(
CompoundSelector compound1, CompoundSelector compound2,
{Iterable<ComplexSelectorComponent>? 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) {
Expand All @@ -697,49 +708,44 @@ 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;
}
}

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<PseudoSelector, int>? _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<SimpleSelector> compound1, Iterable<SimpleSelector> compound2,
{Iterable<ComplexSelectorComponent>? 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`.
///
Expand Down

0 comments on commit d159d83

Please sign in to comment.