Skip to content

Commit 015599a

Browse files
committed
Deprecate bogus combinators
See sass/sass#3340 See #1727
1 parent e5a3d5e commit 015599a

25 files changed

+1316
-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: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44

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

7+
import '../evaluation_context.dart';
8+
import '../visitor/any_selector.dart';
79
import '../visitor/interface/selector.dart';
810
import '../visitor/serialize.dart';
11+
import 'selector/complex.dart';
12+
import 'selector/list.dart';
13+
import 'selector/placeholder.dart';
14+
import 'selector/pseudo.dart';
915

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

3893
/// Calls the appropriate visit method on [visitor].
3994
T accept<T>(SelectorVisitor<T> visitor);
4095

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