Skip to content

Commit 67fecff

Browse files
authored
unifyComound() and unifyComplex() no longer move pseudo-classes across pseudo-element boundaries (#2350)
* unifyComound() and unifyComplex() no longer move pseudo-classes across pseudo-element boundaries * Fix crash in TypeSelector.unify() when unifying an empty list
1 parent f84e867 commit 67fecff

File tree

8 files changed

+62
-19
lines changed

8 files changed

+62
-19
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## 1.79.5
2+
3+
* Changes to how `selector.unify()` and `@extend` combine selectors:
4+
5+
* The relative order of pseudo-classes (like `:hover`) and pseudo-elements
6+
(like `::before`) within each original selector is now preserved when
7+
they're combined.
8+
9+
* Pseudo selectors are now consistently placed at the end of the combined
10+
selector, regardless of which selector they came from. Previously, this
11+
reordering only applied to pseudo-selectors in the second selector.
12+
113
## 1.79.4
214

315
### JS API

lib/src/ast/selector/type.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class TypeSelector extends SimpleSelector {
3232
/// @nodoc
3333
@internal
3434
List<SimpleSelector>? unify(List<SimpleSelector> compound) {
35-
if (compound.first case UniversalSelector() || TypeSelector()) {
35+
if (compound.firstOrNull case UniversalSelector() || TypeSelector()) {
3636
var unified = unifyUniversalAndElement(this, compound.first);
3737
if (unified == null) return null;
3838
return [unified, ...compound.skip(1)];

lib/src/extend/functions.dart

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ List<ComplexSelector>? unifyComplex(
3636
List<ComplexSelector> complexes, FileSpan span) {
3737
if (complexes.length == 1) return complexes;
3838

39-
List<SimpleSelector>? unifiedBase;
39+
CompoundSelector? unifiedBase;
4040
CssValue<Combinator>? leadingCombinator;
4141
CssValue<Combinator>? trailingCombinator;
4242
for (var complex in complexes) {
@@ -67,12 +67,10 @@ List<ComplexSelector>? unifyComplex(
6767
}
6868

6969
if (unifiedBase == null) {
70-
unifiedBase = base.selector.components;
70+
unifiedBase = base.selector;
7171
} else {
72-
for (var simple in base.selector.components) {
73-
unifiedBase = simple.unify(unifiedBase!); // dart-lang/sdk#45348
74-
if (unifiedBase == null) return null;
75-
}
72+
unifiedBase = unifyCompound(unifiedBase, base.selector);
73+
if (unifiedBase == null) return null;
7674
}
7775
}
7876

@@ -87,7 +85,7 @@ List<ComplexSelector>? unifyComplex(
8785
var base = ComplexSelector(
8886
leadingCombinator == null ? const [] : [leadingCombinator],
8987
[
90-
ComplexSelectorComponent(CompoundSelector(unifiedBase!, span),
88+
ComplexSelectorComponent(unifiedBase!,
9189
trailingCombinator == null ? const [] : [trailingCombinator], span)
9290
],
9391
span,
@@ -106,19 +104,44 @@ List<ComplexSelector>? unifyComplex(
106104
/// Returns a [CompoundSelector] that matches only elements that are matched by
107105
/// both [compound1] and [compound2].
108106
///
109-
/// The [span] will be used for the new unified selector.
107+
/// The [compound1]'s `span` will be used for the new unified selector.
108+
///
109+
/// This function ensures that the relative order of pseudo-classes (`:`) and
110+
/// pseudo-elements (`::`) within each input selector is preserved in the
111+
/// resulting combined selector.
112+
///
113+
/// This function enforces a general rule that pseudo-classes (`:`) should come
114+
/// before pseudo-elements (`::`), but it won't change their order if they were
115+
/// originally interleaved within a single input selector. This prevents
116+
/// unintended changes to the selector's meaning. For example, unifying
117+
/// `::foo:bar` and `:baz` results in `:baz::foo:bar`. `:baz` is a pseudo-class,
118+
/// so it is moved before the pseudo-class `::foo`. Meanwhile, `:bar` is not
119+
/// moved before `::foo` because it appeared after `::foo` in the original
120+
/// selector.
110121
///
111122
/// If no such selector can be produced, returns `null`.
112123
CompoundSelector? unifyCompound(
113124
CompoundSelector compound1, CompoundSelector compound2) {
114-
var result = compound2.components;
115-
for (var simple in compound1.components) {
116-
var unified = simple.unify(result);
117-
if (unified == null) return null;
118-
result = unified;
125+
var result = compound1.components;
126+
var pseudoResult = <SimpleSelector>[];
127+
var pseudoElementFound = false;
128+
129+
for (var simple in compound2.components) {
130+
// All pseudo-classes are unified separately after a pseudo-element to
131+
// preserve their relative order with the pseudo-element.
132+
if (pseudoElementFound && simple is PseudoSelector) {
133+
var unified = simple.unify(pseudoResult);
134+
if (unified == null) return null;
135+
pseudoResult = unified;
136+
} else {
137+
pseudoElementFound |= simple is PseudoSelector && simple.isElement;
138+
var unified = simple.unify(result);
139+
if (unified == null) return null;
140+
result = unified;
141+
}
119142
}
120143

121-
return CompoundSelector(result, compound1.span);
144+
return CompoundSelector([...result, ...pseudoResult], compound1.span);
122145
}
123146

124147
/// Returns a [SimpleSelector] that matches only elements that are matched by

pkg/sass-parser/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.2.4
2+
3+
* No user-visible changes.
4+
15
## 0.2.3
26

37
* No user-visible changes.

pkg/sass-parser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sass-parser",
3-
"version": "0.2.3",
3+
"version": "0.2.4",
44
"description": "A PostCSS-compatible wrapper of the official Sass parser",
55
"repository": "sass/sass",
66
"author": "Google Inc.",

pkg/sass_api/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 12.0.5
2+
3+
* No user-visible changes.
4+
15
## 12.0.4
26

37
* No user-visible changes.

pkg/sass_api/pubspec.yaml

Lines changed: 2 additions & 2 deletions
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: 12.0.4
5+
version: 12.0.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.79.4
13+
sass: 1.79.5
1414

1515
dev_dependencies:
1616
dartdoc: ^8.0.14

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: sass
2-
version: 1.79.4
2+
version: 1.79.5
33
description: A Sass implementation in Dart.
44
homepage: https://github.com/sass/dart-sass
55

0 commit comments

Comments
 (0)