Skip to content

Commit 499e140

Browse files
authored
Drop support for bogus combinators (#2536)
This also improves the way SassExceptions thrown from within custom functions are handled. If the wrapped exception already has a span, it's turned into a MultiSpanSassException with an additional span pointing to the function invocation.
1 parent fdf31ad commit 499e140

23 files changed

+995
-792
lines changed

CHANGELOG.md

+34
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,40 @@
33
* **Breaking change:** The `@-moz-document` rule no longer has any special
44
parsing associated with it. It is now parsed like any other unknown plain CSS
55
at-rule, where Sass features are only allowed within `#{}` interpolation.
6+
7+
### Bogus Combinators
8+
9+
* **Breaking change:** Selectors with more than one combinator in a row, such as
10+
`.foo + ~ a`, are now syntax errors.
11+
12+
* **Breaking change:** It's now an error for a selector at the root of the
13+
document or in a psuedo selector to have a leading combinator, such as
14+
`+ .foo`. These are still allowed in nested selectors and in `:has()`.
15+
16+
* **Breaking change:** It's now an error for selectors with trailing
17+
combinators, such as `.foo +`, to contain declarations, non-bubbling plain-CSS
18+
at rules, or `@extend` rules.
19+
20+
* **Breaking change:** It's now an error for `@extend` rules to extend selectors
21+
with leading or trailing combinators.
22+
23+
* **Breaking change:** The `$extender` and `$extendee` arguments of
24+
`selector.extend()` and `selector.replace()`, as well as the `$super` and
25+
`$sub` arguments of `selector.is-superselector()`, no longer allow selectors
26+
with leading or trailing combinators.
27+
28+
* **Breaking change:** The `$selector` arguments of `selector.extend()` and
29+
`selector.replace()`, as well as the `$selector1` and `$selector2` arguments
30+
of `selector.unify()`, no longer allow selectors with trailing combinators.
31+
Leading combinators are still allowed for these functions because they may
32+
appear in a plain CSS nesting context.
33+
34+
### Dart API
35+
36+
* Remove `Value.assertSelector()`, `.assertSimpleSelector()`,
37+
`.assertCompoundSelector()`, and `.assertComplexSelector()`. This is now only
38+
available through the expanded `sass_api` package, since that package also
39+
exposes the selector AST that it returns.
640

741
## 1.85.2-dev
842

lib/src/ast/css/node.dart

+1-14
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,6 @@ abstract class CssNode implements AstNode {
3535
const _IsInvisibleVisitor(includeBogus: true, includeComments: false),
3636
);
3737

38-
// Whether this node would be invisible even if style rule selectors within it
39-
// didn't have bogus combinators.
40-
///
41-
/// Note that this doesn't consider nodes that contain loud comments to be
42-
/// invisible even though they're omitted in compressed mode.
43-
@internal
44-
bool get isInvisibleOtherThanBogusCombinators => accept(
45-
const _IsInvisibleVisitor(includeBogus: false, includeComments: false),
46-
);
47-
4838
// Whether this node will be invisible when loud comments are stripped.
4939
@internal
5040
bool get isInvisibleHidingComments => accept(
@@ -92,8 +82,5 @@ class _IsInvisibleVisitor with EveryCssVisitor {
9282
includeComments && !comment.isPreserved;
9383

9484
bool visitCssStyleRule(CssStyleRule rule) =>
95-
(includeBogus
96-
? rule.selector.isInvisible
97-
: rule.selector.isInvisibleOtherThanBogusCombinators) ||
98-
super.visitCssStyleRule(rule);
85+
rule.selector.isInvisible || super.visitCssStyleRule(rule);
9986
}

lib/src/ast/sass/parameter_list.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import '../../utils.dart';
1212
import 'parameter.dart';
1313
import 'node.dart';
1414

15-
/// An parameter declaration, as for a function or mixin definition.
15+
/// A parameter declaration, as for a function or mixin definition.
1616
///
1717
/// {@category AST}
1818
/// {@category Parsing}

lib/src/ast/selector.dart

+3-109
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@
55
import 'package:meta/meta.dart';
66
import 'package:source_span/source_span.dart';
77

8-
import '../deprecation.dart';
9-
import '../evaluation_context.dart';
10-
import '../exception.dart';
118
import '../visitor/any_selector.dart';
129
import '../visitor/interface/selector.dart';
1310
import '../visitor/serialize.dart';
1411
import 'node.dart';
15-
import 'selector/complex.dart';
1612
import 'selector/list.dart';
1713
import 'selector/placeholder.dart';
1814
import 'selector/pseudo.dart';
@@ -47,58 +43,12 @@ abstract base class Selector implements AstNode {
4743
///
4844
/// @nodoc
4945
@internal
50-
bool get isInvisible => accept(const _IsInvisibleVisitor(includeBogus: true));
51-
52-
// Whether this selector would be invisible even if it didn't have bogus
53-
// combinators.
54-
///
55-
/// @nodoc
56-
@internal
57-
bool get isInvisibleOtherThanBogusCombinators =>
58-
accept(const _IsInvisibleVisitor(includeBogus: false));
59-
60-
/// Whether this selector is not valid CSS.
61-
///
62-
/// This includes both selectors that are useful exclusively for build-time
63-
/// nesting (`> .foo)` and selectors with invalid combiantors that are still
64-
/// supported for backwards-compatibility reasons (`.foo + ~ .bar`).
65-
bool get isBogus =>
66-
accept(const _IsBogusVisitor(includeLeadingCombinator: true));
67-
68-
/// Whether this selector is bogus other than having a leading combinator.
69-
///
70-
/// @nodoc
71-
@internal
72-
bool get isBogusOtherThanLeadingCombinator =>
73-
accept(const _IsBogusVisitor(includeLeadingCombinator: false));
74-
75-
/// Whether this is a useless selector (that is, it's bogus _and_ it can't be
76-
/// transformed into valid CSS by `@extend` or nesting).
77-
///
78-
/// @nodoc
79-
@internal
80-
bool get isUseless => accept(const _IsUselessVisitor());
46+
bool get isInvisible => accept(const _IsInvisibleVisitor());
8147

8248
final FileSpan span;
8349

8450
Selector(this.span);
8551

86-
/// Prints a warning if `this` is a bogus selector.
87-
///
88-
/// This may only be called from within a custom Sass function. This will
89-
/// throw a [SassException] in Dart Sass 2.0.0.
90-
void assertNotBogus({String? name}) {
91-
if (!isBogus) return;
92-
warnForDeprecation(
93-
(name == null ? '' : '\$$name: ') +
94-
'$this is not valid CSS.\n'
95-
'This will be an error in Dart Sass 2.0.0.\n'
96-
'\n'
97-
'More info: https://sass-lang.com/d/bogus-combinators',
98-
Deprecation.bogusCombinators,
99-
);
100-
}
101-
10252
/// Calls the appropriate visit method on [visitor].
10353
T accept<T>(SelectorVisitor<T> visitor);
10454

@@ -107,18 +57,11 @@ abstract base class Selector implements AstNode {
10757

10858
/// The visitor used to implement [Selector.isInvisible].
10959
class _IsInvisibleVisitor with AnySelectorVisitor {
110-
/// Whether to consider selectors with bogus combinators invisible.
111-
final bool includeBogus;
112-
113-
const _IsInvisibleVisitor({required this.includeBogus});
60+
const _IsInvisibleVisitor();
11461

11562
bool visitSelectorList(SelectorList list) =>
11663
list.components.every(visitComplexSelector);
11764

118-
bool visitComplexSelector(ComplexSelector complex) =>
119-
super.visitComplexSelector(complex) ||
120-
(includeBogus && complex.isBogusOtherThanLeadingCombinator);
121-
12265
bool visitPlaceholderSelector(PlaceholderSelector placeholder) => true;
12366

12467
bool visitPseudoSelector(PseudoSelector pseudo) {
@@ -127,58 +70,9 @@ class _IsInvisibleVisitor with AnySelectorVisitor {
12770
// it means "doesn't match this selector that matches nothing", so it's
12871
// equivalent to *. If the entire compound selector is composed of `:not`s
12972
// with invisible lists, the serializer emits it as `*`.
130-
return pseudo.name == 'not'
131-
? (includeBogus && selector.isBogus)
132-
: selector.accept(this);
73+
return pseudo.name != 'not' && selector.accept(this);
13374
} else {
13475
return false;
13576
}
13677
}
13778
}
138-
139-
/// The visitor used to implement [Selector.isBogus].
140-
class _IsBogusVisitor with AnySelectorVisitor {
141-
/// Whether to consider selectors with leading combinators as bogus.
142-
final bool includeLeadingCombinator;
143-
144-
const _IsBogusVisitor({required this.includeLeadingCombinator});
145-
146-
bool visitComplexSelector(ComplexSelector complex) {
147-
if (complex.components.isEmpty) {
148-
return complex.leadingCombinators.isNotEmpty;
149-
} else {
150-
return complex.leadingCombinators.length >
151-
(includeLeadingCombinator ? 0 : 1) ||
152-
complex.components.last.combinators.isNotEmpty ||
153-
complex.components.any(
154-
(component) =>
155-
component.combinators.length > 1 ||
156-
component.selector.accept(this),
157-
);
158-
}
159-
}
160-
161-
bool visitPseudoSelector(PseudoSelector pseudo) {
162-
var selector = pseudo.selector;
163-
if (selector == null) return false;
164-
165-
// The CSS spec specifically allows leading combinators in `:has()`.
166-
return pseudo.name == 'has'
167-
? selector.isBogusOtherThanLeadingCombinator
168-
: selector.isBogus;
169-
}
170-
}
171-
172-
/// The visitor used to implement [Selector.isUseless]
173-
class _IsUselessVisitor with AnySelectorVisitor {
174-
const _IsUselessVisitor();
175-
176-
bool visitComplexSelector(ComplexSelector complex) =>
177-
complex.leadingCombinators.length > 1 ||
178-
complex.components.any(
179-
(component) =>
180-
component.combinators.length > 1 || component.selector.accept(this),
181-
);
182-
183-
bool visitPseudoSelector(PseudoSelector pseudo) => pseudo.isBogus;
184-
}

0 commit comments

Comments
 (0)