Skip to content

Commit ecff05d

Browse files
authored
Remove the heuristic where long selector lists wouldn't be trimmed (#2255)
Testing this against the `@extend`-heavy stylesheets in vinceliuice/Colloid-gtk-theme, trimming everywhere actually *improves* performance rather than reducing it.
1 parent 5ddd7fc commit ecff05d

File tree

9 files changed

+59
-24
lines changed

9 files changed

+59
-24
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.77.5
2+
3+
* Fully trim redundant selectors generated by `@extend`.
4+
15
## 1.77.4
26

37
### Embedded Sass

lib/src/ast/selector/compound.dart

+12
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ final class CompoundSelector extends Selector {
4242
SimpleSelector? get singleSimple =>
4343
components.length == 1 ? components.first : null;
4444

45+
/// Whether any simple selector in this contains a selector that requires
46+
/// complex non-local reasoning to determine whether it's a super- or
47+
/// sub-selector.
48+
///
49+
/// This includes both pseudo-elements and pseudo-selectors that take
50+
/// selectors as arguments.
51+
///
52+
/// #nodoc
53+
@internal
54+
late final bool hasComplicatedSuperselectorSemantics = components
55+
.any((component) => component.hasComplicatedSuperselectorSemantics);
56+
4557
CompoundSelector(Iterable<SimpleSelector> components, super.span)
4658
: components = List.unmodifiable(components) {
4759
if (this.components.isEmpty) {

lib/src/ast/selector/pseudo.dart

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ final class PseudoSelector extends SimpleSelector {
6767
bool get isHostContext =>
6868
isClass && name == 'host-context' && selector != null;
6969

70+
@internal
71+
bool get hasComplicatedSuperselectorSemantics =>
72+
isElement || selector != null;
73+
7074
/// The non-selector argument passed to this selector.
7175
///
7276
/// This is `null` if there's no argument. If [argument] and [selector] are

lib/src/ast/selector/simple.dart

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ abstract base class SimpleSelector extends Selector {
3434
/// sequence will contain 1000 simple selectors.
3535
int get specificity => 1000;
3636

37+
/// Whether this requires complex non-local reasoning to determine whether
38+
/// it's a super- or sub-selector.
39+
///
40+
/// This includes both pseudo-elements and pseudo-selectors that take
41+
/// selectors as arguments.
42+
///
43+
/// #nodoc
44+
@internal
45+
bool get hasComplicatedSuperselectorSemantics => false;
46+
3747
SimpleSelector(super.span);
3848

3949
/// Parses a simple selector from [contents].

lib/src/extend/extension_store.dart

-7
Original file line numberDiff line numberDiff line change
@@ -901,13 +901,6 @@ class ExtensionStore {
901901
// document, and thus should never be trimmed.
902902
List<ComplexSelector> _trim(List<ComplexSelector> selectors,
903903
bool isOriginal(ComplexSelector complex)) {
904-
// Avoid truly horrific quadratic behavior.
905-
//
906-
// TODO(nweiz): I think there may be a way to get perfect trimming without
907-
// going quadratic by building some sort of trie-like data structure that
908-
// can be used to look up superselectors.
909-
if (selectors.length > 100) return selectors;
910-
911904
// This is n² on the sequences, but only comparing between separate
912905
// sequences should limit the quadratic behavior. We iterate from last to
913906
// first and reverse the result so that, if two selectors are identical, we

lib/src/extend/functions.dart

+22-14
Original file line numberDiff line numberDiff line change
@@ -646,24 +646,28 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
646646
var component1 = complex1[i1];
647647
if (component1.combinators.length > 1) return false;
648648
if (remaining1 == 1) {
649-
var parents = complex2.sublist(i2, complex2.length - 1);
650-
if (parents.any((parent) => parent.combinators.length > 1)) return false;
651-
652-
return compoundIsSuperselector(
653-
component1.selector, complex2.last.selector,
654-
parents: parents);
649+
if (complex2.any((parent) => parent.combinators.length > 1)) {
650+
return false;
651+
} else {
652+
return compoundIsSuperselector(
653+
component1.selector, complex2.last.selector,
654+
parents: component1.selector.hasComplicatedSuperselectorSemantics
655+
? complex2.sublist(i2, complex2.length - 1)
656+
: null);
657+
}
655658
}
656659

657660
// Find the first index [endOfSubselector] in [complex2] such that
658661
// `complex2.sublist(i2, endOfSubselector + 1)` is a subselector of
659662
// [component1.selector].
660663
var endOfSubselector = i2;
661-
List<ComplexSelectorComponent>? parents;
662664
while (true) {
663665
var component2 = complex2[endOfSubselector];
664666
if (component2.combinators.length > 1) return false;
665667
if (compoundIsSuperselector(component1.selector, component2.selector,
666-
parents: parents)) {
668+
parents: component1.selector.hasComplicatedSuperselectorSemantics
669+
? complex2.sublist(i2, endOfSubselector)
670+
: null)) {
667671
break;
668672
}
669673

@@ -675,13 +679,10 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
675679
// to match.
676680
return false;
677681
}
678-
679-
parents ??= [];
680-
parents.add(component2);
681682
}
682683

683684
if (!_compatibleWithPreviousCombinator(
684-
previousCombinator, parents ?? const [])) {
685+
previousCombinator, complex2.take(endOfSubselector).skip(i2))) {
685686
return false;
686687
}
687688

@@ -717,8 +718,8 @@ bool complexIsSuperselector(List<ComplexSelectorComponent> complex1,
717718
/// Returns whether [parents] are valid intersitial components between one
718719
/// complex superselector and another, given that the earlier complex
719720
/// superselector had the combinator [previous].
720-
bool _compatibleWithPreviousCombinator(
721-
CssValue<Combinator>? previous, List<ComplexSelectorComponent> parents) {
721+
bool _compatibleWithPreviousCombinator(CssValue<Combinator>? previous,
722+
Iterable<ComplexSelectorComponent> parents) {
722723
if (parents.isEmpty) return true;
723724
if (previous == null) return true;
724725

@@ -754,6 +755,13 @@ bool _isSupercombinator(
754755
bool compoundIsSuperselector(
755756
CompoundSelector compound1, CompoundSelector compound2,
756757
{Iterable<ComplexSelectorComponent>? parents}) {
758+
if (!compound1.hasComplicatedSuperselectorSemantics &&
759+
!compound2.hasComplicatedSuperselectorSemantics) {
760+
if (compound1.components.length > compound2.components.length) return false;
761+
return compound1.components
762+
.every((simple1) => compound2.components.any(simple1.isSuperselector));
763+
}
764+
757765
// Pseudo elements effectively change the target of a compound selector rather
758766
// than narrowing the set of elements to which it applies like other
759767
// selectors. As such, if either selector has a pseudo element, they both must

pkg/sass_api/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 10.4.5
2+
3+
* No user-visible changes.
4+
15
## 10.4.4
26

37
* No user-visible changes.

pkg/sass_api/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ name: sass_api
22
# Note: Every time we add a new Sass AST node, we need to bump the *major*
33
# version because it's a breaking change for anyone who's implementing the
44
# visitor interface(s).
5-
version: 10.4.4
5+
version: 10.4.5
66
description: Additional APIs for Dart Sass.
77
homepage: https://github.com/sass/dart-sass
88

99
environment:
1010
sdk: ">=3.0.0 <4.0.0"
1111

1212
dependencies:
13-
sass: 1.77.4
13+
sass: 1.77.5
1414

1515
dev_dependencies:
1616
dartdoc: ">=6.0.0 <9.0.0"

pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.77.4
2+
version: 1.77.5
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

0 commit comments

Comments
 (0)