Skip to content

Commit 4b53c16

Browse files
authored
Deprecate bogus combinators (#1740)
See sass/sass#3340 See #1727
1 parent fd4c50c commit 4b53c16

25 files changed

+1317
-666
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## 1.54.0
22

3+
* Deprecate selectors with leading or trailing combinators, or with multiple
4+
combinators in a row. If they're included in style rules after nesting is
5+
resolved, Sass will now produce a deprecation warning and, in most cases, omit
6+
the selector. Leading and trailing combinators can still be freely used for
7+
nesting purposes.
8+
9+
See https://sass-lang.com/d/bogus-combinators for more details.
10+
311
### JS API
412

513
* Add a `charset` option that controls whether or not Sass emits a

lib/src/ast/css/modifiable/node.dart

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
import 'dart:collection';
66

77
import '../../../visitor/interface/modifiable_css.dart';
8-
import '../at_rule.dart';
98
import '../node.dart';
10-
import '../style_rule.dart';
119

1210
/// A modifiable version of [CssNode].
1311
///
@@ -27,36 +25,11 @@ abstract class ModifiableCssNode extends CssNode {
2725
var isGroupEnd = false;
2826

2927
/// Whether this node has a visible sibling after it.
30-
bool get hasFollowingSibling {
31-
var parent = _parent;
32-
if (parent == null) return false;
33-
var siblings = parent.children;
34-
for (var i = _indexInParent! + 1; i < siblings.length; i++) {
35-
var sibling = siblings[i];
36-
if (!_isInvisible(sibling)) return true;
37-
}
38-
return false;
39-
}
40-
41-
/// Returns whether [node] is invisible for the purposes of
42-
/// [hasFollowingSibling].
43-
///
44-
/// This can return a false negative for a comment node in compressed mode,
45-
/// since the AST doesn't know the output style, but that's an extremely
46-
/// narrow edge case so we don't worry about it.
47-
bool _isInvisible(CssNode node) {
48-
if (node is CssParentNode) {
49-
// An unknown at-rule is never invisible. Because we don't know the
50-
// semantics of unknown rules, we can't guarantee that (for example)
51-
// `@foo {}` isn't meaningful.
52-
if (node is CssAtRule) return false;
53-
54-
if (node is CssStyleRule && node.selector.value.isInvisible) return true;
55-
return node.children.every(_isInvisible);
56-
} else {
57-
return false;
58-
}
59-
}
28+
bool get hasFollowingSibling =>
29+
_parent?.children
30+
.skip(_indexInParent! + 1)
31+
.any((sibling) => !sibling.isInvisible) ??
32+
false;
6033

6134
T accept<T>(ModifiableCssVisitor<T> visitor);
6235

lib/src/ast/css/node.dart

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import 'package:meta/meta.dart';
6+
7+
import '../../visitor/every_css.dart';
58
import '../../visitor/interface/css.dart';
69
import '../../visitor/serialize.dart';
710
import '../node.dart';
11+
import 'at_rule.dart';
12+
import 'comment.dart';
13+
import 'style_rule.dart';
814

915
/// A statement in a plain CSS syntax tree.
1016
abstract class CssNode extends AstNode {
@@ -15,6 +21,28 @@ abstract class CssNode extends AstNode {
1521
/// Calls the appropriate visit method on [visitor].
1622
T accept<T>(CssVisitor<T> visitor);
1723

24+
/// Whether this is invisible and won't be emitted to the compiled stylesheet.
25+
///
26+
/// Note that this doesn't consider nodes that contain loud comments to be
27+
/// invisible even though they're omitted in compressed mode.
28+
@internal
29+
bool get isInvisible => accept(
30+
const _IsInvisibleVisitor(includeBogus: true, includeComments: false));
31+
32+
// Whether this node would be invisible even if style rule selectors within it
33+
// didn't have bogus combinators.
34+
///
35+
/// Note that this doesn't consider nodes that contain loud comments to be
36+
/// invisible even though they're omitted in compressed mode.
37+
@internal
38+
bool get isInvisibleOtherThanBogusCombinators => accept(
39+
const _IsInvisibleVisitor(includeBogus: false, includeComments: false));
40+
41+
// Whether this node will be invisible when loud comments are stripped.
42+
@internal
43+
bool get isInvisibleHidingComments => accept(
44+
const _IsInvisibleVisitor(includeBogus: true, includeComments: true));
45+
1846
String toString() => serialize(this, inspect: true).css;
1947
}
2048

@@ -32,3 +60,29 @@ abstract class CssParentNode extends CssNode {
3260
/// like `@foo {}`, [children] is empty but [isChildless] is `false`.
3361
bool get isChildless;
3462
}
63+
64+
/// The visitor used to implement [CssNode.isInvisible]
65+
class _IsInvisibleVisitor extends EveryCssVisitor {
66+
/// Whether to consider selectors with bogus combinators invisible.
67+
final bool includeBogus;
68+
69+
/// Whether to consider comments invisible.
70+
final bool includeComments;
71+
72+
const _IsInvisibleVisitor(
73+
{required this.includeBogus, required this.includeComments});
74+
75+
// An unknown at-rule is never invisible. Because we don't know the semantics
76+
// of unknown rules, we can't guarantee that (for example) `@foo {}` isn't
77+
// meaningful.
78+
bool visitCssAtRule(CssAtRule rule) => false;
79+
80+
bool visitCssComment(CssComment comment) =>
81+
includeComments && !comment.isPreserved;
82+
83+
bool visitCssStyleRule(CssStyleRule rule) =>
84+
(includeBogus
85+
? rule.selector.value.isInvisible
86+
: rule.selector.value.isInvisibleOtherThanBogusCombinators) ||
87+
super.visitCssStyleRule(rule);
88+
}

lib/src/ast/selector.dart

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@
44

55
import 'package:meta/meta.dart';
66

7+
import '../evaluation_context.dart';
8+
import '../exception.dart';
9+
import '../visitor/any_selector.dart';
710
import '../visitor/interface/selector.dart';
811
import '../visitor/serialize.dart';
12+
import 'selector/complex.dart';
13+
import 'selector/list.dart';
14+
import 'selector/placeholder.dart';
15+
import 'selector/pseudo.dart';
916

1017
export 'selector/attribute.dart';
1118
export 'selector/class.dart';
19+
export 'selector/combinator.dart';
1220
export 'selector/complex.dart';
21+
export 'selector/complex_component.dart';
1322
export 'selector/compound.dart';
1423
export 'selector/id.dart';
1524
export 'selector/list.dart';
@@ -32,11 +41,131 @@ export 'selector/universal.dart';
3241
abstract class Selector {
3342
/// Whether this selector, and complex selectors containing it, should not be
3443
/// emitted.
44+
///
45+
/// @nodoc
3546
@internal
36-
bool get isInvisible => false;
47+
bool get isInvisible => accept(const _IsInvisibleVisitor(includeBogus: true));
48+
49+
// Whether this selector would be invisible even if it didn't have bogus
50+
// combinators.
51+
///
52+
/// @nodoc
53+
@internal
54+
bool get isInvisibleOtherThanBogusCombinators =>
55+
accept(const _IsInvisibleVisitor(includeBogus: false));
56+
57+
/// Whether this selector is not valid CSS.
58+
///
59+
/// This includes both selectors that are useful exclusively for build-time
60+
/// nesting (`> .foo)` and selectors with invalid combiantors that are still
61+
/// supported for backwards-compatibility reasons (`.foo + ~ .bar`).
62+
bool get isBogus =>
63+
accept(const _IsBogusVisitor(includeLeadingCombinator: true));
64+
65+
/// Whether this selector is bogus other than having a leading combinator.
66+
///
67+
/// @nodoc
68+
@internal
69+
bool get isBogusOtherThanLeadingCombinator =>
70+
accept(const _IsBogusVisitor(includeLeadingCombinator: false));
71+
72+
/// Whether this is a useless selector (that is, it's bogus _and_ it can't be
73+
/// transformed into valid CSS by `@extend` or nesting).
74+
///
75+
/// @nodoc
76+
@internal
77+
bool get isUseless => accept(const _IsUselessVisitor());
78+
79+
/// Prints a warning if [this] is a bogus selector.
80+
///
81+
/// This may only be called from within a custom Sass function. This will
82+
/// throw a [SassScriptException] in Dart Sass 2.0.0.
83+
void assertNotBogus({String? name}) {
84+
if (!isBogus) return;
85+
warn(
86+
(name == null ? '' : '\$$name: ') +
87+
'$this is not valid CSS.\n'
88+
'This will be an error in Dart Sass 2.0.0.\n'
89+
'\n'
90+
'More info: https://sass-lang.com/d/bogus-combinators',
91+
deprecation: true);
92+
}
3793

3894
/// Calls the appropriate visit method on [visitor].
3995
T accept<T>(SelectorVisitor<T> visitor);
4096

4197
String toString() => serializeSelector(this, inspect: true);
4298
}
99+
100+
/// The visitor used to implement [Selector.isInvisible].
101+
class _IsInvisibleVisitor extends AnySelectorVisitor {
102+
/// Whether to consider selectors with bogus combinators invisible.
103+
final bool includeBogus;
104+
105+
const _IsInvisibleVisitor({required this.includeBogus});
106+
107+
bool visitSelectorList(SelectorList list) =>
108+
list.components.every(visitComplexSelector);
109+
110+
bool visitComplexSelector(ComplexSelector complex) =>
111+
super.visitComplexSelector(complex) ||
112+
(includeBogus && complex.isBogusOtherThanLeadingCombinator);
113+
114+
bool visitPlaceholderSelector(PlaceholderSelector placeholder) => true;
115+
116+
bool visitPseudoSelector(PseudoSelector pseudo) {
117+
var selector = pseudo.selector;
118+
if (selector == null) return false;
119+
120+
// We don't consider `:not(%foo)` to be invisible because, semantically, it
121+
// means "doesn't match this selector that matches nothing", so it's
122+
// equivalent to *. If the entire compound selector is composed of `:not`s
123+
// with invisible lists, the serializer emits it as `*`.
124+
return pseudo.name == 'not'
125+
? (includeBogus && selector.isBogus)
126+
: selector.accept(this);
127+
}
128+
}
129+
130+
/// The visitor used to implement [Selector.isBogus].
131+
class _IsBogusVisitor extends AnySelectorVisitor {
132+
/// Whether to consider selectors with leading combinators as bogus.
133+
final bool includeLeadingCombinator;
134+
135+
const _IsBogusVisitor({required this.includeLeadingCombinator});
136+
137+
bool visitComplexSelector(ComplexSelector complex) {
138+
if (complex.components.isEmpty) {
139+
return complex.leadingCombinators.isNotEmpty;
140+
} else {
141+
return complex.leadingCombinators.length >
142+
(includeLeadingCombinator ? 0 : 1) ||
143+
complex.components.last.combinators.isNotEmpty ||
144+
complex.components.any((component) =>
145+
component.combinators.length > 1 ||
146+
component.selector.accept(this));
147+
}
148+
}
149+
150+
bool visitPseudoSelector(PseudoSelector pseudo) {
151+
var selector = pseudo.selector;
152+
if (selector == null) return false;
153+
154+
// The CSS spec specifically allows leading combinators in `:has()`.
155+
return pseudo.name == 'has'
156+
? selector.isBogusOtherThanLeadingCombinator
157+
: selector.isBogus;
158+
}
159+
}
160+
161+
/// The visitor used to implement [Selector.isUseless]
162+
class _IsUselessVisitor extends AnySelectorVisitor {
163+
const _IsUselessVisitor();
164+
165+
bool visitComplexSelector(ComplexSelector complex) =>
166+
complex.leadingCombinators.length > 1 ||
167+
complex.components.any((component) =>
168+
component.combinators.length > 1 || component.selector.accept(this));
169+
170+
bool visitPseudoSelector(PseudoSelector pseudo) => pseudo.isBogus;
171+
}

lib/src/ast/selector/combinator.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2022 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import 'package:meta/meta.dart';
6+
7+
/// A combinator that defines the relationship between selectors in a
8+
/// [ComplexSelector].
9+
///
10+
/// {@category Selector}
11+
@sealed
12+
class Combinator {
13+
/// Matches the right-hand selector if it's immediately adjacent to the
14+
/// left-hand selector in the DOM tree.
15+
static const nextSibling = Combinator._("+");
16+
17+
/// Matches the right-hand selector if it's a direct child of the left-hand
18+
/// selector in the DOM tree.
19+
static const child = Combinator._(">");
20+
21+
/// Matches the right-hand selector if it comes after the left-hand selector
22+
/// in the DOM tree.
23+
static const followingSibling = Combinator._("~");
24+
25+
/// The combinator's token text.
26+
final String _text;
27+
28+
const Combinator._(this._text);
29+
30+
String toString() => _text;
31+
}

0 commit comments

Comments
 (0)