Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix superselector bugs for pseudo-elements and universal selectors #1753

Merged
merged 2 commits into from Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/sass.dart
Expand Up @@ -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;

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 Selector}
Expand Down Expand Up @@ -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;
}
}
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is incomplete.

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