Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop support for bogus combinators #2536

Merged
merged 4 commits into from
Mar 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,40 @@
* **Breaking change:** The `@-moz-document` rule no longer has any special
parsing associated with it. It is now parsed like any other unknown plain CSS
at-rule, where Sass features are only allowed within `#{}` interpolation.

### Bogus Combinators

* **Breaking change:** Selectors with more than one combinator in a row, such as
`.foo + ~ a`, are now syntax errors.

* **Breaking change:** It's now an error for a selector at the root of the
document or in a psuedo selector to have a leading combinator, such as
`+ .foo`. These are still allowed in nested selectors and in `:has()`.

* **Breaking change:** It's now an error for selectors with trailing
combinators, such as `.foo +`, to contain declarations, non-bubbling plain-CSS
at rules, or `@extend` rules.

* **Breaking change:** It's now an error for `@extend` rules to extend selectors
with leading or trailing combinators.

* **Breaking change:** The `$extender` and `$extendee` arguments of
`selector.extend()` and `selector.replace()`, as well as the `$super` and
`$sub` arguments of `selector.is-superselector()`, no longer allow selectors
with leading or trailing combinators.

* **Breaking change:** The `$selector` arguments of `selector.extend()` and
`selector.replace()`, as well as the `$selector1` and `$selector2` arguments
of `selector.unify()`, no longer allow selectors with trailing combinators.
Leading combinators are still allowed for these functions because they may
appear in a plain CSS nesting context.

### Dart API

* Remove `Value.assertSelector()`, `.assertSimpleSelector()`,
`.assertCompoundSelector()`, and `.assertComplexSelector()`. This is now only
available through the expanded `sass_api` package, since that package also
exposes the selector AST that it returns.

## 1.85.2-dev

Expand Down
15 changes: 1 addition & 14 deletions lib/src/ast/css/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,6 @@ abstract class CssNode implements AstNode {
const _IsInvisibleVisitor(includeBogus: true, includeComments: false),
);

// Whether this node would be invisible even if style rule selectors within it
// didn't have bogus combinators.
///
/// Note that this doesn't consider nodes that contain loud comments to be
/// invisible even though they're omitted in compressed mode.
@internal
bool get isInvisibleOtherThanBogusCombinators => accept(
const _IsInvisibleVisitor(includeBogus: false, includeComments: false),
);

// Whether this node will be invisible when loud comments are stripped.
@internal
bool get isInvisibleHidingComments => accept(
Expand Down Expand Up @@ -92,8 +82,5 @@ class _IsInvisibleVisitor with EveryCssVisitor {
includeComments && !comment.isPreserved;

bool visitCssStyleRule(CssStyleRule rule) =>
(includeBogus
? rule.selector.isInvisible
: rule.selector.isInvisibleOtherThanBogusCombinators) ||
super.visitCssStyleRule(rule);
rule.selector.isInvisible || super.visitCssStyleRule(rule);
}
2 changes: 1 addition & 1 deletion lib/src/ast/sass/parameter_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import '../../utils.dart';
import 'parameter.dart';
import 'node.dart';

/// An parameter declaration, as for a function or mixin definition.
/// A parameter declaration, as for a function or mixin definition.
///
/// {@category AST}
/// {@category Parsing}
Expand Down
112 changes: 3 additions & 109 deletions lib/src/ast/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';

import '../deprecation.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../visitor/any_selector.dart';
import '../visitor/interface/selector.dart';
import '../visitor/serialize.dart';
import 'node.dart';
import 'selector/complex.dart';
import 'selector/list.dart';
import 'selector/placeholder.dart';
import 'selector/pseudo.dart';
Expand Down Expand Up @@ -47,58 +43,12 @@ abstract base class Selector implements AstNode {
///
/// @nodoc
@internal
bool get isInvisible => accept(const _IsInvisibleVisitor(includeBogus: true));

// Whether this selector would be invisible even if it didn't have bogus
// combinators.
///
/// @nodoc
@internal
bool get isInvisibleOtherThanBogusCombinators =>
accept(const _IsInvisibleVisitor(includeBogus: false));

/// Whether this selector is not valid CSS.
///
/// This includes both selectors that are useful exclusively for build-time
/// nesting (`> .foo)` and selectors with invalid combiantors that are still
/// supported for backwards-compatibility reasons (`.foo + ~ .bar`).
bool get isBogus =>
accept(const _IsBogusVisitor(includeLeadingCombinator: true));

/// Whether this selector is bogus other than having a leading combinator.
///
/// @nodoc
@internal
bool get isBogusOtherThanLeadingCombinator =>
accept(const _IsBogusVisitor(includeLeadingCombinator: false));

/// Whether this is a useless selector (that is, it's bogus _and_ it can't be
/// transformed into valid CSS by `@extend` or nesting).
///
/// @nodoc
@internal
bool get isUseless => accept(const _IsUselessVisitor());
bool get isInvisible => accept(const _IsInvisibleVisitor());

final FileSpan span;

Selector(this.span);

/// Prints a warning if `this` is a bogus selector.
///
/// This may only be called from within a custom Sass function. This will
/// throw a [SassException] in Dart Sass 2.0.0.
void assertNotBogus({String? name}) {
if (!isBogus) return;
warnForDeprecation(
(name == null ? '' : '\$$name: ') +
'$this is not valid CSS.\n'
'This will be an error in Dart Sass 2.0.0.\n'
'\n'
'More info: https://sass-lang.com/d/bogus-combinators',
Deprecation.bogusCombinators,
);
}

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

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

/// The visitor used to implement [Selector.isInvisible].
class _IsInvisibleVisitor with AnySelectorVisitor {
/// Whether to consider selectors with bogus combinators invisible.
final bool includeBogus;

const _IsInvisibleVisitor({required this.includeBogus});
const _IsInvisibleVisitor();

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

bool visitComplexSelector(ComplexSelector complex) =>
super.visitComplexSelector(complex) ||
(includeBogus && complex.isBogusOtherThanLeadingCombinator);

bool visitPlaceholderSelector(PlaceholderSelector placeholder) => true;

bool visitPseudoSelector(PseudoSelector pseudo) {
Expand All @@ -127,58 +70,9 @@ class _IsInvisibleVisitor with AnySelectorVisitor {
// it means "doesn't match this selector that matches nothing", so it's
// equivalent to *. If the entire compound selector is composed of `:not`s
// with invisible lists, the serializer emits it as `*`.
return pseudo.name == 'not'
? (includeBogus && selector.isBogus)
: selector.accept(this);
return pseudo.name != 'not' && selector.accept(this);
} else {
return false;
}
}
}

/// The visitor used to implement [Selector.isBogus].
class _IsBogusVisitor with AnySelectorVisitor {
/// Whether to consider selectors with leading combinators as bogus.
final bool includeLeadingCombinator;

const _IsBogusVisitor({required this.includeLeadingCombinator});

bool visitComplexSelector(ComplexSelector complex) {
if (complex.components.isEmpty) {
return complex.leadingCombinators.isNotEmpty;
} else {
return complex.leadingCombinators.length >
(includeLeadingCombinator ? 0 : 1) ||
complex.components.last.combinators.isNotEmpty ||
complex.components.any(
(component) =>
component.combinators.length > 1 ||
component.selector.accept(this),
);
}
}

bool visitPseudoSelector(PseudoSelector pseudo) {
var selector = pseudo.selector;
if (selector == null) return false;

// The CSS spec specifically allows leading combinators in `:has()`.
return pseudo.name == 'has'
? selector.isBogusOtherThanLeadingCombinator
: selector.isBogus;
}
}

/// The visitor used to implement [Selector.isUseless]
class _IsUselessVisitor with AnySelectorVisitor {
const _IsUselessVisitor();

bool visitComplexSelector(ComplexSelector complex) =>
complex.leadingCombinators.length > 1 ||
complex.components.any(
(component) =>
component.combinators.length > 1 || component.selector.accept(this),
);

bool visitPseudoSelector(PseudoSelector pseudo) => pseudo.isBogus;
}
Loading
Loading