diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb7992096..9d19a44a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
 
diff --git a/lib/src/ast/css/node.dart b/lib/src/ast/css/node.dart
index f50f8c9f5..7189e315e 100644
--- a/lib/src/ast/css/node.dart
+++ b/lib/src/ast/css/node.dart
@@ -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(
@@ -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);
 }
diff --git a/lib/src/ast/sass/parameter_list.dart b/lib/src/ast/sass/parameter_list.dart
index bdd01848a..989c2c387 100644
--- a/lib/src/ast/sass/parameter_list.dart
+++ b/lib/src/ast/sass/parameter_list.dart
@@ -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}
diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart
index 64420d5c7..d3eb349e6 100644
--- a/lib/src/ast/selector.dart
+++ b/lib/src/ast/selector.dart
@@ -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';
@@ -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);
 
@@ -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) {
@@ -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;
-}
diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart
index b118f38a8..c3ec5fcc4 100644
--- a/lib/src/ast/selector/complex.dart
+++ b/lib/src/ast/selector/complex.dart
@@ -5,8 +5,10 @@
 import 'package:meta/meta.dart';
 import 'package:source_span/source_span.dart';
 
+import '../../exception.dart';
 import '../../extend/functions.dart';
 import '../../parse/selector.dart';
+import '../../util/nullable.dart';
 import '../../utils.dart';
 import '../../visitor/interface/selector.dart';
 import '../css/value.dart';
@@ -20,16 +22,12 @@ import '../selector.dart';
 /// {@category AST}
 /// {@category Parsing}
 final class ComplexSelector extends Selector {
-  /// This selector's leading combinators.
-  ///
-  /// If this is empty, that indicates that it has no leading combinator. If
-  /// it's more than one element, that means it's invalid CSS; however, we still
-  /// support this for backwards-compatibility purposes.
-  final List<CssValue<Combinator>> leadingCombinators;
+  /// This selector's leading combinator, if it has one.
+  final CssValue<Combinator>? leadingCombinator;
 
   /// The components of this selector.
   ///
-  /// This is only empty if [leadingCombinators] is not empty.
+  /// This is only empty if [leadingCombinator] is not null.
   ///
   /// Descendant combinators aren't explicitly represented here. If two
   /// [CompoundSelector]s are adjacent to one another, there's an implicit
@@ -55,6 +53,50 @@ final class ComplexSelector extends Selector {
     (sum, component) => sum + component.selector.specificity,
   );
 
+  /// Whether `this` is a CSS selector that's valid on its own at the root of
+  /// the CSS document.
+  ///
+  /// Selectors with leading or trailing combinators are *not* stand-alone.
+  bool get isStandAlone =>
+      leadingCombinator == null && components.last.combinator == null;
+
+  /// Whether `this` is a valid [relative selector].
+  ///
+  /// This allows leading combinators but not trailing combinators. For any
+  /// selector where this returns true, [isStandAlone] will also return true.
+  ///
+  /// [relative selector]: https://www.w3.org/TR/selectors-4/#relative-selector
+  bool get isRelative => switch (components) {
+        [] => false,
+        [..., ComplexSelectorComponent(combinator: var _?)] => false,
+        _ => true,
+      };
+
+  /// Throws a [SassException] if `this` isn't a CSS selector that's valid in
+  /// various places in the document, depending on the arguments passed.
+  ///
+  /// If [allowLeadingCombinator] or [allowTrailingCombinator] is `true`, this
+  /// allows selectors with leading or trailing selector combinators,
+  /// respectively. Otherwise, they produce errors after parsing. If both are
+  /// true, all selectors are allowed and this does nothing.
+  ///
+  /// If this came from a function argument, [name] is the argument name
+  /// (without the `$`). It's used for error reporting.
+  void assertValid({
+    String? name,
+    bool allowLeadingCombinator = false,
+    bool allowTrailingCombinator = false,
+  }) {
+    if ((!allowLeadingCombinator && leadingCombinator != null) ||
+        (!allowTrailingCombinator && !isRelative)) {
+      throw SassScriptException(
+        'Selectors that aren\'t valid on their own aren\'t allowed '
+        'in this function.',
+        name,
+      ).withSpan(span);
+    }
+  }
+
   /// If this compound selector is composed of a single compound selector with
   /// no combinators, returns it.
   ///
@@ -63,23 +105,22 @@ final class ComplexSelector extends Selector {
   /// @nodoc
   @internal
   CompoundSelector? get singleCompound {
-    if (leadingCombinators.isNotEmpty) return null;
+    if (leadingCombinator != null) return null;
     return switch (components) {
-      [ComplexSelectorComponent(:var selector, combinators: [])] => selector,
+      [ComplexSelectorComponent(:var selector, combinator: null)] => selector,
       _ => null,
     };
   }
 
   ComplexSelector(
-    Iterable<CssValue<Combinator>> leadingCombinators,
     Iterable<ComplexSelectorComponent> components,
     super.span, {
+    this.leadingCombinator,
     this.lineBreak = false,
-  })  : leadingCombinators = List.unmodifiable(leadingCombinators),
-        components = List.unmodifiable(components) {
-    if (this.leadingCombinators.isEmpty && this.components.isEmpty) {
+  }) : components = List.unmodifiable(components) {
+    if (leadingCombinator == null && this.components.isEmpty) {
       throw ArgumentError(
-        "leadingCombinators and components may not both be empty.",
+        "components may only empty if leadingCombinator is non-null.",
       );
     }
   }
@@ -109,38 +150,55 @@ final class ComplexSelector extends Selector {
   /// That is, whether this matches every element that [other] matches, as well
   /// as possibly matching more.
   bool isSuperselector(ComplexSelector other) =>
-      leadingCombinators.isEmpty &&
-      other.leadingCombinators.isEmpty &&
+      leadingCombinator == null &&
+      other.leadingCombinator == null &&
       complexIsSuperselector(components, other.components);
 
-  /// Returns a copy of `this` with [combinators] added to the end of the final
+  /// Returns a copy of `this` with [combinator] added to the beginning.
+  ///
+  /// Returns `null` if this already has a leading combinator.
+  ///
+  /// @nodoc
+  @internal
+  ComplexSelector? prependCombinator(CssValue<Combinator>? combinator) {
+    if (combinator == null) return this;
+    if (leadingCombinator != null) return null;
+    return ComplexSelector(
+      components,
+      span,
+      leadingCombinator: combinator,
+      lineBreak: lineBreak,
+    );
+  }
+
+  /// Returns a copy of `this` with [combinator] added to the end of the final
   /// component in [components].
   ///
   /// If [forceLineBreak] is `true`, this will mark the new complex selector as
   /// having a line break.
   ///
+  /// Returns `null` if this already has a trailing combinator.
+  ///
   /// @nodoc
   @internal
-  ComplexSelector withAdditionalCombinators(
-    List<CssValue<Combinator>> combinators, {
+  ComplexSelector? withAdditionalCombinator(
+    CssValue<Combinator>? combinator, {
     bool forceLineBreak = false,
-  }) {
-    if (combinators.isEmpty) return this;
-    return switch (components) {
-      [...var initial, var last] => ComplexSelector(
-          leadingCombinators,
-          [...initial, last.withAdditionalCombinators(combinators)],
-          span,
-          lineBreak: lineBreak || forceLineBreak,
-        ),
-      [] => ComplexSelector(
-          [...leadingCombinators, ...combinators],
-          const [],
-          span,
-          lineBreak: lineBreak || forceLineBreak,
-        ),
-    };
-  }
+  }) =>
+      combinator == null
+          ? this
+          : switch (components) {
+              [...var initial, var last] =>
+                last.withAdditionalCombinator(combinator).andThen(
+                      (newLast) => ComplexSelector(
+                        [...initial, newLast],
+                        span,
+                        leadingCombinator: leadingCombinator,
+                        lineBreak: lineBreak || forceLineBreak,
+                      ),
+                    ),
+              [] => null,
+            };
 
   /// Returns a copy of `this` with an additional [component] added to the end.
   ///
@@ -152,21 +210,24 @@ final class ComplexSelector extends Selector {
   /// @nodoc
   @internal
   ComplexSelector withAdditionalComponent(
-    ComplexSelectorComponent component,
+    ComplexSelectorComponent? component,
     FileSpan span, {
     bool forceLineBreak = false,
   }) =>
-      ComplexSelector(
-        leadingCombinators,
-        [...components, component],
-        span,
-        lineBreak: lineBreak || forceLineBreak,
-      );
+      component == null
+          ? this
+          : ComplexSelector(
+              [...components, component],
+              span,
+              leadingCombinator: leadingCombinator,
+              lineBreak: lineBreak || forceLineBreak,
+            );
 
-  /// Returns a copy of `this` with [child]'s combinators added to the end.
+  /// Returns a copy of `this` with [child] added to the end.
   ///
-  /// If [child] has [leadingCombinators], they're appended to `this`'s last
-  /// combinator. This does _not_ resolve parent selectors.
+  /// If [child] has [leadingCombinator], they're appended to `this`'s last
+  /// combinator. If that would produce an invalid selector, this returns `null`
+  /// instead. This does _not_ resolve parent selectors.
   ///
   /// The [span] is used for the new selector.
   ///
@@ -175,43 +236,38 @@ final class ComplexSelector extends Selector {
   ///
   /// @nodoc
   @internal
-  ComplexSelector concatenate(
+  ComplexSelector? concatenate(
     ComplexSelector child,
     FileSpan span, {
     bool forceLineBreak = false,
-  }) {
-    if (child.leadingCombinators.isEmpty) {
-      return ComplexSelector(
-        leadingCombinators,
-        [...components, ...child.components],
-        span,
-        lineBreak: lineBreak || child.lineBreak || forceLineBreak,
-      );
-    } else if (components case [...var initial, var last]) {
-      return ComplexSelector(
-        leadingCombinators,
-        [
-          ...initial,
-          last.withAdditionalCombinators(child.leadingCombinators),
-          ...child.components,
-        ],
-        span,
-        lineBreak: lineBreak || child.lineBreak || forceLineBreak,
-      );
-    } else {
-      return ComplexSelector(
-        [...leadingCombinators, ...child.leadingCombinators],
-        child.components,
-        span,
-        lineBreak: lineBreak || child.lineBreak || forceLineBreak,
-      );
-    }
-  }
+  }) =>
+      switch (child.leadingCombinator) {
+        null => ComplexSelector(
+            [...components, ...child.components],
+            span,
+            leadingCombinator: leadingCombinator,
+            lineBreak: lineBreak || child.lineBreak || forceLineBreak,
+          ),
+        var childCombinator => switch (components) {
+            [...var initial, var last] =>
+              last.withAdditionalCombinator(childCombinator).andThen(
+                    (newLast) => ComplexSelector(
+                      [...initial, newLast, ...child.components],
+                      span,
+                      leadingCombinator: leadingCombinator,
+                      lineBreak: lineBreak || child.lineBreak || forceLineBreak,
+                    ),
+                  ),
+            // If components is empty, this must have a leading combinator, which
+            // isn't compatible with [childCombinator].
+            _ => null,
+          },
+      };
 
-  int get hashCode => listHash(leadingCombinators) ^ listHash(components);
+  int get hashCode => leadingCombinator.hashCode ^ listHash(components);
 
   bool operator ==(Object other) =>
       other is ComplexSelector &&
-      listEquals(leadingCombinators, other.leadingCombinators) &&
+      leadingCombinator == other.leadingCombinator &&
       listEquals(components, other.components);
 }
diff --git a/lib/src/ast/selector/complex_component.dart b/lib/src/ast/selector/complex_component.dart
index fa9d85deb..f2c35325c 100644
--- a/lib/src/ast/selector/complex_component.dart
+++ b/lib/src/ast/selector/complex_component.dart
@@ -5,7 +5,6 @@
 import 'package:meta/meta.dart';
 import 'package:source_span/source_span.dart';
 
-import '../../utils.dart';
 import '../css/value.dart';
 import '../selector.dart';
 
@@ -18,47 +17,39 @@ final class ComplexSelectorComponent {
   /// This component's compound selector.
   final CompoundSelector selector;
 
-  /// This selector's combinators.
+  /// This selector's trailing combinator, if it has one.
   ///
-  /// If this is empty, that indicates that it has an implicit descendent
-  /// combinator. If it's more than one element, that means it's invalid CSS;
-  /// however, we still support this for backwards-compatibility purposes.
-  final List<CssValue<Combinator>> combinators;
+  /// If this is null, that indicates that it has an implicit descendent
+  /// combinator.
+  final CssValue<Combinator>? combinator;
 
   final FileSpan span;
 
-  ComplexSelectorComponent(
-    this.selector,
-    Iterable<CssValue<Combinator>> combinators,
-    this.span,
-  ) : combinators = List.unmodifiable(combinators);
+  ComplexSelectorComponent(this.selector, this.span, {this.combinator});
 
-  /// Returns a copy of `this` with [combinators] added to the end of
-  /// [this.combinators].
+  /// Returns a copy of `this` with [combinator] added to the end.
+  ///
+  /// Returns `null` if this already has a combinator.
   ///
   /// @nodoc
   @internal
-  ComplexSelectorComponent withAdditionalCombinators(
-    List<CssValue<Combinator>> combinators,
+  ComplexSelectorComponent? withAdditionalCombinator(
+    CssValue<Combinator>? combinator,
   ) =>
-      combinators.isEmpty
-          ? this
-          : ComplexSelectorComponent(
-              selector,
-              [
-                ...this.combinators,
-                ...combinators,
-              ],
-              span);
+      switch ((this.combinator, combinator)) {
+        (_, null) => this,
+        (null, var combinator?) =>
+          ComplexSelectorComponent(selector, span, combinator: combinator),
+        _ => null,
+      };
 
-  int get hashCode => selector.hashCode ^ listHash(combinators);
+  int get hashCode => selector.hashCode ^ combinator.hashCode;
 
   bool operator ==(Object other) =>
       other is ComplexSelectorComponent &&
       selector == other.selector &&
-      listEquals(combinators, other.combinators);
+      combinator == other.combinator;
 
   String toString() =>
-      selector.toString() +
-      combinators.map((combinator) => ' $combinator').join('');
+      selector.toString() + (combinator == null ? '' : ' $combinator');
 }
diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart
index 3f745b512..8f20c51f0 100644
--- a/lib/src/ast/selector/list.dart
+++ b/lib/src/ast/selector/list.dart
@@ -37,11 +37,11 @@ final class SelectorList extends Selector {
     return SassList(
       components.map((complex) {
         return SassList([
-          for (var combinator in complex.leadingCombinators)
+          if (complex.leadingCombinator case var combinator?)
             SassString(combinator.toString(), quotes: false),
           for (var component in complex.components) ...[
             SassString(component.selector.toString(), quotes: false),
-            for (var combinator in component.combinators)
+            if (component.combinator case var combinator?)
               SassString(combinator.toString(), quotes: false),
           ],
         ], ListSeparator.space);
@@ -50,6 +50,43 @@ final class SelectorList extends Selector {
     );
   }
 
+  /// Whether `this` is a CSS selector that's valid on its own at the root of
+  /// the CSS document.
+  ///
+  /// Selectors with leading or trailing combinators are *not* stand-alone.
+  bool get isStandAlone => components.every((complex) => complex.isStandAlone);
+
+  /// Whether `this` is a valid [relative selector].
+  ///
+  /// This allows leading combinators but not trailing combinators.
+  ///
+  /// [relative selector]: https://www.w3.org/TR/selectors-4/#relative-selector
+  bool get isRelative => components.every((complex) => complex.isRelative);
+
+  /// Throws a [SassException] if `this` isn't a CSS selector that's valid in
+  /// various places in the document, depending on the arguments passed.
+  ///
+  /// If [allowLeadingCombinator] or [allowTrailingCombinator] is `true`, this
+  /// allows selectors with leading or trailing selector combinators,
+  /// respectively. Otherwise, they produce errors after parsing. If both are
+  /// true, all selectors are allowed and this does nothing.
+  ///
+  /// If this came from a function argument, [name] is the argument name
+  /// (without the `$`). It's used for error reporting.
+  void assertValid({
+    String? name,
+    bool allowLeadingCombinator = false,
+    bool allowTrailingCombinator = false,
+  }) {
+    if (allowLeadingCombinator && allowTrailingCombinator) return;
+    for (var complex in components) {
+      complex.assertValid(
+          name: name,
+          allowLeadingCombinator: allowLeadingCombinator,
+          allowTrailingCombinator: allowTrailingCombinator);
+    }
+  }
+
   SelectorList(Iterable<ComplexSelector> components, super.span)
       : components = List.unmodifiable(components) {
     if (this.components.isEmpty) {
@@ -133,10 +170,19 @@ final class SelectorList extends Selector {
         components.map((complex) {
           if (preserveParentSelectors || !_containsParentSelector(complex)) {
             if (!implicitParent) return [complex];
-            return parent.components.map(
-              (parentComplex) =>
-                  parentComplex.concatenate(complex, complex.span),
-            );
+            return [
+              for (var parentComplex in parent.components)
+                if (parentComplex.concatenate(complex, complex.span)
+                    case var newComplex?)
+                  newComplex
+                else
+                  throw MultiSpanSassException(
+                    'The selector "$parentComplex $complex" is invalid CSS.',
+                    complex.span.trimRight(),
+                    "inner selector",
+                    {parentComplex.span.trimRight(): "outer selector"},
+                  ),
+            ];
           }
 
           var newComplexes = <ComplexSelector>[];
@@ -146,9 +192,9 @@ final class SelectorList extends Selector {
               if (newComplexes.isEmpty) {
                 newComplexes.add(
                   ComplexSelector(
-                    complex.leadingCombinators,
                     [component],
-                    complex.span,
+                    complex.span.trimRight(),
+                    leadingCombinator: complex.leadingCombinator,
                     lineBreak: false,
                   ),
                 );
@@ -161,29 +207,38 @@ final class SelectorList extends Selector {
                 }
               }
             } else if (newComplexes.isEmpty) {
-              newComplexes.addAll(
-                complex.leadingCombinators.isEmpty
-                    ? resolved
-                    : resolved.map(
-                        (resolvedComplex) => ComplexSelector(
-                          resolvedComplex.leadingCombinators.isEmpty
-                              ? complex.leadingCombinators
-                              : [
-                                  ...complex.leadingCombinators,
-                                  ...resolvedComplex.leadingCombinators,
-                                ],
-                          resolvedComplex.components,
-                          complex.span,
-                          lineBreak: resolvedComplex.lineBreak,
+              newComplexes.addAll(switch (complex.leadingCombinator) {
+                null => resolved,
+                var leadingCombinator => [
+                    for (var resolvedComplex in resolved)
+                      if (resolvedComplex.prependCombinator(leadingCombinator)
+                          case var newResolved?)
+                        newResolved
+                      else
+                        throw MultiSpanSassException(
+                          'The selector "$leadingCombinator $resolvedComplex" is '
+                              'invalid CSS.',
+                          complex.span.trimRight(),
+                          "inner selector",
+                          {parent.span.trimRight(): "outer selector"},
                         ),
-                      ),
-              );
+                  ],
+              });
             } else {
-              var previousComplexes = newComplexes;
               newComplexes = [
-                for (var newComplex in previousComplexes)
+                for (var newComplex in newComplexes)
                   for (var resolvedComplex in resolved)
-                    newComplex.concatenate(resolvedComplex, newComplex.span),
+                    if (newComplex.concatenate(resolvedComplex, newComplex.span)
+                        case var newResolved?)
+                      newResolved
+                    else
+                      throw MultiSpanSassException(
+                        'The selector "$newComplex $resolvedComplex" is invalid '
+                            'CSS.',
+                        resolvedComplex.span.trimRight(),
+                        "inner selector",
+                        {newComplex.span.trimRight(): "outer selector"},
+                      ),
               ];
             }
           }
@@ -214,56 +269,67 @@ final class SelectorList extends Selector {
     }
 
     var resolvedSimples = containsSelectorPseudo
-        ? simples.map(
-            (simple) => switch (simple) {
-              PseudoSelector(:var selector?)
-                  when _containsParentSelector(selector) =>
-                simple.withSelector(
-                  selector.nestWithin(parent, implicitParent: false),
-                ),
-              _ => simple,
-            },
-          )
+        ? simples.map((simple) {
+            if (simple
+                case PseudoSelector(
+                  :var selector?,
+                ) when _containsParentSelector(selector)) {
+              var nested = selector.nestWithin(parent, implicitParent: false);
+              var result = simple.withSelector(nested);
+              if (result != null) return result;
+
+              var invalid = simple.toString().replaceFirst(
+                    RegExp(r"\(.*\)"),
+                    "($nested)",
+                  );
+              throw MultiSpanSassException(
+                  'The selector "$invalid" is invalid CSS.',
+                  simple.accept(_ParentSelectorVisitor())!.span.trimRight(),
+                  "parent selector",
+                  {parent.span.trimRight(): "outer selector"});
+            } else {
+              return simple;
+            }
+          }).toList()
         : simples;
 
     var parentSelector = simples.first;
-    try {
-      if (parentSelector is! ParentSelector) {
-        return [
-          ComplexSelector(const [], [
-            ComplexSelectorComponent(
-              CompoundSelector(resolvedSimples, component.selector.span),
-              component.combinators,
-              component.span,
-            ),
-          ], component.span),
-        ];
-      } else if (simples.length == 1 && parentSelector.suffix == null) {
-        return parent
-            .withAdditionalCombinators(component.combinators)
-            .components;
-      }
-    } on SassException catch (error, stackTrace) {
-      throwWithTrace(
-        error.withAdditionalSpan(parentSelector.span, "parent selector"),
-        error,
-        stackTrace,
-      );
+    if (parentSelector is! ParentSelector) {
+      return [
+        ComplexSelector([
+          ComplexSelectorComponent(
+            CompoundSelector(resolvedSimples, component.selector.span),
+            component.span,
+            combinator: component.combinator,
+          ),
+        ], component.span),
+      ];
+    } else if (simples.length == 1 && parentSelector.suffix == null) {
+      return switch (parent.withAdditionalCombinator(component.combinator)) {
+        var list? => list.components,
+        _ => throw MultiSpanSassException(
+            'The selector "${parent.components.first} ${component.combinator}" '
+                'is invalid CSS.',
+            parentSelector.span,
+            "parent selector",
+            {parent.span.trimRight(): "outer selector"},
+          )
+      };
     }
 
     return parent.components.map((complex) {
-      try {
-        var lastComponent = complex.components.last;
-        if (lastComponent.combinators.isNotEmpty) {
-          throw MultiSpanSassException(
-            'Selector "$complex" can\'t be used as a parent in a compound '
-                'selector.',
-            lastComponent.span.trimRight(),
-            "outer selector",
-            {parentSelector.span: "parent selector"},
-          );
-        }
+      var lastComponent = complex.components.last;
+      if (lastComponent.combinator != null) {
+        throw MultiSpanSassException(
+          'Selector "$complex" can\'t be used as a parent in a compound '
+              'selector.',
+          lastComponent.span.trimRight(),
+          "outer selector",
+          {parentSelector.span: "parent selector"},
+        );
+      }
 
+      try {
         var suffix = parentSelector.suffix;
         var lastSimples = lastComponent.selector.components;
         var last = CompoundSelector(
@@ -278,21 +344,24 @@ final class SelectorList extends Selector {
         );
 
         return ComplexSelector(
-          complex.leadingCombinators,
           [
             ...complex.components.exceptLast,
             ComplexSelectorComponent(
               last,
-              component.combinators,
               component.span,
+              combinator: component.combinator,
             ),
           ],
           component.span,
+          leadingCombinator: complex.leadingCombinator,
           lineBreak: complex.lineBreak,
         );
       } on SassException catch (error, stackTrace) {
         throwWithTrace(
-          error.withAdditionalSpan(parentSelector.span, "parent selector"),
+          error
+              .withAdditionalSpan(
+                  lastComponent.span.trimRight(), "outer selector")
+              .withAdditionalSpan(parentSelector.span, "parent selector"),
           error,
           stackTrace,
         );
@@ -307,22 +376,22 @@ final class SelectorList extends Selector {
   bool isSuperselector(SelectorList other) =>
       listIsSuperselector(components, other.components);
 
-  /// Returns a copy of `this` with [combinators] added to the end of each
+  /// Returns a copy of `this` with [combinator] added to the end of each
   /// complex selector in [components].
   ///
+  /// Returns `null` if this would produce an invalid selector.
+  ///
   /// @nodoc
   @internal
-  SelectorList withAdditionalCombinators(
-    List<CssValue<Combinator>> combinators,
-  ) =>
-      combinators.isEmpty
-          ? this
-          : SelectorList(
-              components.map(
-                (complex) => complex.withAdditionalCombinators(combinators),
-              ),
-              span,
-            );
+  SelectorList? withAdditionalCombinator(CssValue<Combinator>? combinator) {
+    if (combinator == null) return this;
+    var newComponents = [
+      for (var complex in components)
+        if (complex.withAdditionalCombinator(combinator) case var newComplex?)
+          newComplex,
+    ];
+    return newComponents.isEmpty ? null : SelectorList(newComponents, span);
+  }
 
   int get hashCode => listHash(components);
 
diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart
index f3f3b8ceb..839e71347 100644
--- a/lib/src/ast/selector/pseudo.dart
+++ b/lib/src/ast/selector/pseudo.dart
@@ -144,13 +144,27 @@ final class PseudoSelector extends SimpleSelector {
 
   /// Returns a new [PseudoSelector] based on this, but with the selector
   /// replaced with [selector].
-  PseudoSelector withSelector(SelectorList selector) => PseudoSelector(
-        name,
-        span,
-        element: isElement,
-        argument: argument,
-        selector: selector,
-      );
+  ///
+  /// Returns `null` if this wouldn't produce a valid selector.
+  PseudoSelector? withSelector(SelectorList selector) {
+    // :has() allows selectors with leading combinators
+    var has = equalsIgnoreCase(name, "has");
+    if (has ? !selector.isRelative : !selector.isStandAlone) {
+      var validComplex = [
+        for (var complex in selector.components)
+          if (has ? complex.isRelative : complex.isStandAlone) complex,
+      ];
+      if (validComplex.isEmpty) return null;
+      selector = SelectorList(validComplex, selector.span);
+    }
+    return PseudoSelector(
+      name,
+      span,
+      element: isElement,
+      argument: argument,
+      selector: selector,
+    );
+  }
 
   /// @nodoc
   @internal
diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart
index b19ac4561..525c35ec9 100644
--- a/lib/src/deprecation.dart
+++ b/lib/src/deprecation.dart
@@ -15,7 +15,7 @@ enum Deprecation {
   // DO NOT EDIT. This section was generated from the language repo.
   // See tool/grind/generate_deprecations.dart for details.
   //
-  // Checksum: 47c97f7824eb25d7f1e64e3230938b88330d40b4
+  // Checksum: f34f224d924705c05c56bfc57a398706597f6c64
 
   /// Deprecation for passing a string directly to meta.call().
   callString('call-string',
@@ -27,7 +27,9 @@ enum Deprecation {
 
   /// Deprecation for @-moz-document.
   mozDocument('moz-document',
-      deprecatedIn: '1.7.2', description: '@-moz-document.'),
+      deprecatedIn: '1.7.2',
+      obsoleteIn: '2.0.0',
+      description: '@-moz-document.'),
 
   /// Deprecation for imports using relative canonical URLs.
   relativeCanonical('relative-canonical',
@@ -52,6 +54,7 @@ enum Deprecation {
   /// Deprecation for leading, trailing, and repeated combinators.
   bogusCombinators('bogus-combinators',
       deprecatedIn: '1.54.0',
+      obsoleteIn: '2.0.0',
       description: 'Leading, trailing, and repeated combinators.'),
 
   /// Deprecation for ambiguous + and - operators.
diff --git a/lib/src/exception.dart b/lib/src/exception.dart
index 6a46fcc25..4bbdd2d8e 100644
--- a/lib/src/exception.dart
+++ b/lib/src/exception.dart
@@ -9,6 +9,7 @@ import 'package:stack_trace/stack_trace.dart';
 import 'package:term_glyph/term_glyph.dart' as term_glyph;
 
 import 'util/nullable.dart';
+import 'util/span.dart';
 import 'utils.dart';
 import 'value.dart';
 
@@ -32,6 +33,13 @@ class SassException extends SourceSpanException {
       : loadedUrls =
             loadedUrls == null ? const {} : Set.unmodifiable(loadedUrls);
 
+  /// Creates a copy of this exception associated with the given [message].
+  ///
+  /// @nodoc
+  @internal
+  SassException withMessage(String message) =>
+      SassFormatException(message, span, loadedUrls);
+
   /// Converts this to a [MultiSpanSassException] with the additional [span] and
   /// [label].
   ///
@@ -53,6 +61,11 @@ class SassException extends SourceSpanException {
   SassException withLoadedUrls(Iterable<Uri> loadedUrls) =>
       SassException(message, span, loadedUrls);
 
+  /// Returns whether any of the spans associated with this exception exactly
+  /// match [span].
+  @internal
+  bool hasSpan(FileSpan span) => this.span.equals(span);
+
   String toString({Object? color}) {
     var buffer = StringBuffer()
       ..writeln("Error: $message")
@@ -128,6 +141,20 @@ class MultiSpanSassException extends SassException
   ])  : secondarySpans = Map.unmodifiable(secondarySpans),
         super(message, span, loadedUrls);
 
+  @internal
+  bool hasSpan(FileSpan span) =>
+      super.hasSpan(span) || secondarySpans.keys.any((key) => key.equals(span));
+
+  /// Creates a copy of this exception with the given [message].
+  ///
+  /// @nodoc
+  @internal
+  MultiSpanSassException withMessage(String message) =>
+      MultiSpanSassFormatException(
+          message, span, primaryLabel, secondarySpans, loadedUrls);
+
+  /// @nodoc
+  @internal
   MultiSpanSassException withAdditionalSpan(FileSpan span, String label) =>
       MultiSpanSassException(
           message,
@@ -139,6 +166,8 @@ class MultiSpanSassException extends SassException
           },
           loadedUrls);
 
+  /// @nodoc
+  @internal
   MultiSpanSassRuntimeException withTrace(Trace trace) =>
       MultiSpanSassRuntimeException(
         message,
@@ -149,6 +178,8 @@ class MultiSpanSassException extends SassException
         loadedUrls,
       );
 
+  /// @nodoc
+  @internal
   MultiSpanSassException withLoadedUrls(Iterable<Uri> loadedUrls) =>
       MultiSpanSassException(
         message,
@@ -193,6 +224,13 @@ class MultiSpanSassException extends SassException
 class SassRuntimeException extends SassException {
   final Trace trace;
 
+  /// @nodoc
+  @internal
+  SassRuntimeException withMessage(String message) =>
+      SassRuntimeException(message, span, trace, loadedUrls);
+
+  /// @nodoc
+  @internal
   MultiSpanSassRuntimeException withAdditionalSpan(
     FileSpan span,
     String label,
@@ -206,6 +244,8 @@ class SassRuntimeException extends SassException {
         loadedUrls,
       );
 
+  /// @nodoc
+  @internal
   SassRuntimeException withLoadedUrls(Iterable<Uri> loadedUrls) =>
       SassRuntimeException(message, span, trace, loadedUrls);
 
@@ -231,6 +271,14 @@ class MultiSpanSassRuntimeException extends MultiSpanSassException
     Iterable<Uri>? loadedUrls,
   ]) : super(message, span, primaryLabel, secondarySpans, loadedUrls);
 
+  /// @nodoc
+  @internal
+  MultiSpanSassRuntimeException withMessage(String message) =>
+      MultiSpanSassRuntimeException(
+          message, span, primaryLabel, secondarySpans, trace, loadedUrls);
+
+  /// @nodoc
+  @internal
   MultiSpanSassRuntimeException withAdditionalSpan(
     FileSpan span,
     String label,
@@ -244,6 +292,8 @@ class MultiSpanSassRuntimeException extends MultiSpanSassException
         loadedUrls,
       );
 
+  /// @nodoc
+  @internal
   MultiSpanSassRuntimeException withLoadedUrls(Iterable<Uri> loadedUrls) =>
       MultiSpanSassRuntimeException(
         message,
@@ -265,6 +315,11 @@ class SassFormatException extends SassException
 
   int get offset => span.start.offset;
 
+  /// @nodoc
+  @internal
+  SassFormatException withMessage(String message) =>
+      SassFormatException(message, span, loadedUrls);
+
   /// @nodoc
   @internal
   MultiSpanSassFormatException withAdditionalSpan(
@@ -297,6 +352,14 @@ class MultiSpanSassFormatException extends MultiSpanSassException
 
   int get offset => span.start.offset;
 
+  /// @nodoc
+  @internal
+  MultiSpanSassFormatException withMessage(String message) =>
+      MultiSpanSassFormatException(
+          message, span, primaryLabel, secondarySpans, loadedUrls);
+
+  /// @nodoc
+  @internal
   MultiSpanSassFormatException withAdditionalSpan(
     FileSpan span,
     String label,
@@ -311,6 +374,8 @@ class MultiSpanSassFormatException extends MultiSpanSassException
           },
           loadedUrls);
 
+  /// @nodoc
+  @internal
   MultiSpanSassFormatException withLoadedUrls(Iterable<Uri> loadedUrls) =>
       MultiSpanSassFormatException(
         message,
diff --git a/lib/src/extend/extension_store.dart b/lib/src/extend/extension_store.dart
index 379e45d73..b5fe2e9bf 100644
--- a/lib/src/extend/extension_store.dart
+++ b/lib/src/extend/extension_store.dart
@@ -251,8 +251,6 @@ class ExtensionStore {
     Map<ComplexSelector, Extension>? newExtensions;
     var sources = _extensions.putIfAbsent(target, () => {});
     for (var complex in extender.components) {
-      if (complex.isUseless) continue;
-
       var extension = Extension(
         complex,
         target,
@@ -539,8 +537,6 @@ class ExtensionStore {
     Map<SimpleSelector, Map<ComplexSelector, Extension>> extensions,
     List<CssMediaQuery>? mediaQueryContext,
   ) {
-    if (complex.leadingCombinators.length > 1) return null;
-
     // The complex selectors that each compound selector in [complex.components]
     // can expand to.
     //
@@ -576,7 +572,6 @@ class ExtensionStore {
       if (extended == null) {
         extendedNotExpanded?.add([
           ComplexSelector(
-            const [],
             [component],
             complex.span,
             lineBreak: complex.lineBreak,
@@ -588,29 +583,26 @@ class ExtensionStore {
         extendedNotExpanded = [
           [
             ComplexSelector(
-              complex.leadingCombinators,
               complex.components.take(i),
               complex.span,
+              leadingCombinator: complex.leadingCombinator,
               lineBreak: complex.lineBreak,
             ),
           ],
           extended,
         ];
-      } else if (complex.leadingCombinators.isEmpty) {
+      } else if (complex.leadingCombinator == null) {
         extendedNotExpanded = [extended];
       } else {
         extendedNotExpanded = [
           [
             for (var newComplex in extended)
-              if (newComplex.leadingCombinators.isEmpty ||
-                  listEquals(
-                    complex.leadingCombinators,
-                    newComplex.leadingCombinators,
-                  ))
+              if (newComplex.leadingCombinator == null ||
+                  complex.leadingCombinator == newComplex.leadingCombinator)
                 ComplexSelector(
-                  complex.leadingCombinators,
                   newComplex.components,
                   complex.span,
+                  leadingCombinator: complex.leadingCombinator,
                   lineBreak: complex.lineBreak || newComplex.lineBreak,
                 ),
           ],
@@ -620,10 +612,15 @@ class ExtensionStore {
     if (extendedNotExpanded == null) return null;
 
     var first = true;
-    return paths(extendedNotExpanded).expand((path) {
-      return weave(path, complex.span, forceLineBreak: complex.lineBreak).map((
-        outputComplex,
-      ) {
+    var result = paths(extendedNotExpanded).expand<ComplexSelector>((path) {
+      var woven = weave(
+        path,
+        complex.span,
+        forceLineBreak: complex.lineBreak,
+      );
+      if (woven == null) return [];
+
+      return woven.map((outputComplex) {
         // Make sure that copies of [complex] retain their status as "original"
         // selectors. This includes selectors that are modified because a :not()
         // was extended into.
@@ -635,6 +632,7 @@ class ExtensionStore {
         return outputComplex;
       });
     }).toList();
+    return result.isEmpty ? null : result;
   }
 
   /// Extends [component] using [extensions], and returns the contents of a
@@ -703,12 +701,11 @@ class ExtensionStore {
       List<ComplexSelector>? result;
       for (var extender in extenders) {
         extender.assertCompatibleMediaContext(mediaQueryContext);
-        var complex = extender.selector.withAdditionalCombinators(
-          component.combinators,
-        );
-        if (complex.isUseless) continue;
-        result ??= [];
-        result.add(complex);
+        if (extender.selector.withAdditionalCombinator(component.combinator)
+            case var complex?) {
+          result ??= [];
+          result.add(complex);
+        }
       }
       return result;
     }
@@ -753,7 +750,7 @@ class ExtensionStore {
         // The first path is always the original selector. We can't just return
         // [component] directly because selector pseudos may be modified, but we
         // don't have to do any unification.
-        ComplexSelector(const [], [
+        ComplexSelector([
           ComplexSelectorComponent(
             CompoundSelector(
               extenderPaths.first.expand((extender) {
@@ -762,7 +759,7 @@ class ExtensionStore {
               }),
               component.selector.span,
             ),
-            component.combinators,
+            combinator: component.combinator,
             component.span,
           ),
         ], component.span),
@@ -773,10 +770,10 @@ class ExtensionStore {
       if (extended == null) continue;
 
       for (var complex in extended) {
-        var withCombinators = complex.withAdditionalCombinators(
-          component.combinators,
-        );
-        if (!withCombinators.isUseless) result.add(withCombinators);
+        if (complex.withAdditionalCombinator(component.combinator)
+            case var withCombinator?) {
+          result.add(withCombinator);
+        }
       }
     }
 
@@ -807,11 +804,9 @@ class ExtensionStore {
       if (extender.isOriginal) {
         originals ??= [];
         var finalExtenderComponent = extender.selector.components.last;
-        assert(finalExtenderComponent.combinators.isEmpty);
+        assert(finalExtenderComponent.combinator == null);
         originals.addAll(finalExtenderComponent.selector.components);
         originalsLineBreak = originalsLineBreak || extender.selector.lineBreak;
-      } else if (extender.selector.isUseless) {
-        return null;
       } else {
         toUnify.add(extender.selector);
       }
@@ -820,14 +815,7 @@ class ExtensionStore {
     if (originals != null) {
       toUnify.addFirst(
         ComplexSelector(
-          const [],
-          [
-            ComplexSelectorComponent(
-              CompoundSelector(originals, span),
-              const [],
-              span,
-            ),
-          ],
+          [ComplexSelectorComponent(CompoundSelector(originals, span), span)],
           span,
           lineBreak: originalsLineBreak,
         ),
@@ -888,9 +876,7 @@ class ExtensionStore {
   ) {
     var compound = CompoundSelector(simples, span);
     return Extender(
-      ComplexSelector(const [], [
-        ComplexSelectorComponent(compound, const [], span),
-      ], span),
+      ComplexSelector([ComplexSelectorComponent(compound, span)], span),
       specificity: _sourceSpecificityFor(compound),
       original: true,
     );
@@ -898,10 +884,9 @@ class ExtensionStore {
 
   /// Returns an [Extender] composed solely of [simple].
   Extender _extenderForSimple(SimpleSelector simple) => Extender(
-        ComplexSelector(const [], [
+        ComplexSelector([
           ComplexSelectorComponent(
             CompoundSelector([simple], simple.span),
-            const [],
             simple.span,
           ),
         ], simple.span),
@@ -995,15 +980,17 @@ class ExtensionStore {
     // In order to support those browsers, we break up the contents of a `:not`
     // unless it originally contained a selector list.
     if (pseudo.normalizedName == 'not' && selector.components.length == 1) {
-      var result = complexes
-          .map(
-            (complex) =>
-                pseudo.withSelector(SelectorList([complex], selector.span)),
-          )
-          .toList();
+      var result = [
+        for (var complex in complexes)
+          if (pseudo.withSelector(SelectorList([complex], selector.span))
+              case var newPseudo?)
+            newPseudo,
+      ];
       return result.isEmpty ? null : result;
     } else {
-      return [pseudo.withSelector(SelectorList(complexes, selector.span))];
+      return pseudo
+          .withSelector(SelectorList(complexes, selector.span))
+          .andThen((newPseudo) => [newPseudo]);
     }
   }
 
diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart
index 767f75c83..81b03291d 100644
--- a/lib/src/extend/functions.dart
+++ b/lib/src/extend/functions.dart
@@ -19,6 +19,8 @@ import 'package:source_span/source_span.dart';
 import '../ast/css/value.dart';
 import '../ast/selector.dart';
 import '../util/iterable.dart';
+import '../util/nullable.dart';
+import '../util/option.dart';
 import '../util/span.dart';
 import '../utils.dart';
 
@@ -42,12 +44,10 @@ List<ComplexSelector>? unifyComplex(
   CssValue<Combinator>? leadingCombinator;
   CssValue<Combinator>? trailingCombinator;
   for (var complex in complexes) {
-    if (complex.isUseless) return null;
-
     if (complex
         case ComplexSelector(
           components: [_],
-          leadingCombinators: [var newLeadingCombinator],
+          leadingCombinator: var newLeadingCombinator?,
         )) {
       if (leadingCombinator == null) {
         leadingCombinator = newLeadingCombinator;
@@ -59,7 +59,7 @@ List<ComplexSelector>? unifyComplex(
     var base = complex.components.last;
     if (base
         case ComplexSelectorComponent(
-          combinators: [var newTrailingCombinator],
+          combinator: var newTrailingCombinator?,
         )) {
       if (trailingCombinator != null &&
           trailingCombinator != newTrailingCombinator) {
@@ -80,35 +80,32 @@ List<ComplexSelector>? unifyComplex(
     for (var complex in complexes)
       if (complex.components.length > 1)
         ComplexSelector(
-          complex.leadingCombinators,
           complex.components.exceptLast,
           complex.span,
+          leadingCombinator: complex.leadingCombinator,
           lineBreak: complex.lineBreak,
         ),
   ];
 
   var base = ComplexSelector(
-    leadingCombinator == null ? const [] : [leadingCombinator],
     [
       ComplexSelectorComponent(
         unifiedBase!,
-        trailingCombinator == null ? const [] : [trailingCombinator],
         span,
+        combinator: trailingCombinator,
       ),
     ],
     span,
+    leadingCombinator: leadingCombinator,
     lineBreak: complexes.any((complex) => complex.lineBreak),
   );
 
-  return weave(
-    withoutBases.isEmpty
-        ? [base]
-        : [
-            ...withoutBases.exceptLast,
-            withoutBases.last.concatenate(base, span),
-          ],
-    span,
-  );
+  return switch (withoutBases) {
+    [] => [base],
+    [...var initial, var last] => last
+        .concatenate(base, span)
+        .andThen((combined) => weave([...initial, combined], span)),
+  };
 }
 
 /// Returns a [CompoundSelector] that matches only elements that are matched by
@@ -228,7 +225,9 @@ SimpleSelector? unifyUniversalAndElement(
 ///
 /// If [forceLineBreak] is `true`, this will mark all returned complex selectors
 /// as having line breaks.
-List<ComplexSelector> weave(
+///
+/// If no selectors can be meaninfully combined, returns `null`.
+List<ComplexSelector>? weave(
   List<ComplexSelector> complexes,
   FileSpan span, {
   bool forceLineBreak = false,
@@ -237,9 +236,9 @@ List<ComplexSelector> weave(
     if (!forceLineBreak || complex.lineBreak) return complexes;
     return [
       ComplexSelector(
-        complex.leadingCombinators,
         complex.components,
         complex.span,
+        leadingCombinator: complex.leadingCombinator,
         lineBreak: true,
       ),
     ];
@@ -247,30 +246,30 @@ List<ComplexSelector> weave(
 
   var prefixes = [complexes.first];
   for (var complex in complexes.skip(1)) {
-    if (complex.components.length == 1) {
-      for (var i = 0; i < prefixes.length; i++) {
-        prefixes[i] = prefixes[i].concatenate(
-          complex,
-          span,
-          forceLineBreak: forceLineBreak,
-        );
-      }
-      continue;
-    }
-
-    prefixes = [
-      for (var prefix in prefixes)
-        for (var parentPrefix in _weaveParents(prefix, complex, span) ??
-            const <ComplexSelector>[])
-          parentPrefix.withAdditionalComponent(
-            complex.components.last,
-            span,
-            forceLineBreak: forceLineBreak,
-          ),
-    ];
+    prefixes = complex.components.length == 1
+        ? [
+            for (var prefix in prefixes)
+              if (prefix.concatenate(
+                complex,
+                span,
+                forceLineBreak: forceLineBreak,
+              )
+                  case var combined?)
+                combined,
+          ]
+        : [
+            for (var prefix in prefixes)
+              for (var parentPrefix in _weaveParents(prefix, complex, span) ??
+                  const <ComplexSelector>[])
+                parentPrefix.withAdditionalComponent(
+                  complex.components.last,
+                  span,
+                  forceLineBreak: forceLineBreak,
+                ),
+          ];
   }
 
-  return prefixes;
+  return prefixes.isEmpty ? null : prefixes;
 }
 
 /// Interweaves [prefix]'s components with [base]'s components _other than
@@ -297,11 +296,17 @@ Iterable<ComplexSelector>? _weaveParents(
   ComplexSelector base,
   FileSpan span,
 ) {
-  var leadingCombinators = _mergeLeadingCombinators(
-    prefix.leadingCombinators,
-    base.leadingCombinators,
-  );
-  if (leadingCombinators == null) return null;
+  CssValue<Combinator>? leadingCombinator;
+  switch (_mergeLeadingCombinators(
+    prefix.leadingCombinator,
+    base.leadingCombinator,
+  )) {
+    case (var combinator,):
+      leadingCombinator = combinator;
+
+    case null:
+      return null;
+  }
 
   // Make queues of _only_ the parent selectors. The prefix only contains
   // parents, but the complex selector has a target that we don't want to weave
@@ -319,10 +324,18 @@ Iterable<ComplexSelector>? _weaveParents(
       var rootish = unifyCompound(rootish1.selector, rootish2.selector);
       if (rootish == null) return null;
       queue1.addFirst(
-        ComplexSelectorComponent(rootish, rootish1.combinators, rootish1.span),
+        ComplexSelectorComponent(
+          rootish,
+          rootish1.span,
+          combinator: rootish1.combinator,
+        ),
       );
       queue2.addFirst(
-        ComplexSelectorComponent(rootish, rootish2.combinators, rootish1.span),
+        ComplexSelectorComponent(
+          rootish,
+          rootish1.span,
+          combinator: rootish2.combinator,
+        ),
       );
 
     case (var rootish?, null):
@@ -346,8 +359,8 @@ Iterable<ComplexSelector>? _weaveParents(
       if (!_mustUnify(group1, group2)) return null;
 
       var unified = unifyComplex([
-        ComplexSelector(const [], group1, span),
-        ComplexSelector(const [], group2, span),
+        ComplexSelector(group1, span),
+        ComplexSelector(group2, span),
       ], span);
       return unified?.singleOrNull?.components;
     },
@@ -376,9 +389,9 @@ Iterable<ComplexSelector>? _weaveParents(
   return [
     for (var path in paths(choices.where((choice) => choice.isNotEmpty)))
       ComplexSelector(
-        leadingCombinators,
         [for (var components in path) ...components],
         span,
+        leadingCombinator: leadingCombinator,
         lineBreak: prefix.lineBreak || base.lineBreak,
       ),
   ];
@@ -406,20 +419,20 @@ ComplexSelectorComponent? _firstIfRootish(
   return null;
 }
 
-/// Returns a leading combinator list that's compatible with both [combinators1]
-/// and [combinators2].
+/// Returns a leading combinator that's compatible with both [combinator1] and
+/// [combinator2].
 ///
-/// Returns `null` if the combinator lists can't be unified.
-List<CssValue<Combinator>>? _mergeLeadingCombinators(
-  List<CssValue<Combinator>>? combinators1,
-  List<CssValue<Combinator>>? combinators2,
+/// Returns a none option if the combinators can't be unified, and a some option
+/// with value `null` if the combinators are unified into a descendant
+/// combinator.
+Option<CssValue<Combinator>?> _mergeLeadingCombinators(
+  CssValue<Combinator>? combinator1,
+  CssValue<Combinator>? combinator2,
 ) =>
-// Allow null arguments just to make calls to `Iterable.reduce()` easier.
-    switch ((combinators1, combinators2)) {
-      (null, _) || (_, null) => null,
-      (List(length: > 1), _) || (_, List(length: > 1)) => null,
-      ([], var combinators) || (var combinators, []) => combinators,
-      _ => listEquals(combinators1, combinators2) ? combinators1 : null,
+    switch ((combinator1, combinator2)) {
+      (null, var combinator) || (var combinator, null) => some(combinator),
+      _ when combinator1 == combinator2 => some(combinator1),
+      _ => none(),
     };
 
 /// Extracts trailing [ComplexSelectorComponent]s with trailing combinators from
@@ -428,8 +441,9 @@ List<CssValue<Combinator>>? _mergeLeadingCombinators(
 /// Each element in the returned list is a set of choices for a particular
 /// position in a complex selector. Each choice is the contents of a complex
 /// selector, which is to say a list of complex selector components. The union
-/// of each path through these choices will match the full set of necessary
-/// elements.
+/// of each path through these choices will match the intersection of `E1 *` and
+/// `E2 *`, where `E1` and `E2` are the trailing sublists extracted from
+/// [components1] and [components2] respectively.
 ///
 /// If there are no combinators to be merged, returns an empty list. If the
 /// sequences can't be merged, returns `null`.
@@ -443,22 +457,21 @@ List<List<List<ComplexSelectorComponent>>>? _mergeTrailingCombinators(
 ]) {
   result ??= QueueList();
 
-  var combinators1 = switch (components1) {
-    [..., var last] => last.combinators,
-    _ => const <CssValue<Combinator>>[],
+  var combinator1 = switch (components1) {
+    [..., var last] => last.combinator,
+    _ => null,
   };
-  var combinators2 = switch (components2) {
-    [..., var last] => last.combinators,
-    _ => const <CssValue<Combinator>>[],
+  var combinator2 = switch (components2) {
+    [..., var last] => last.combinator,
+    _ => null,
   };
-  if (combinators1.isEmpty && combinators2.isEmpty) return result;
-  if (combinators1.length > 1 || combinators2.length > 1) return null;
+  if (combinator1 == null && combinator2 == null) return result;
 
   // This code looks complicated, but it's actually just a bunch of special
   // cases for interactions between different combinators.
   switch ((
-    combinators1.firstOrNull?.value,
-    combinators2.firstOrNull?.value,
+    combinator1?.value,
+    combinator2?.value,
     // Include the component lists in the pattern match so we can easily
     // generalize cases across different orderings of the two combinators.
     components1,
@@ -484,7 +497,7 @@ List<List<List<ComplexSelectorComponent>>>? _mergeTrailingCombinators(
         if (unifyCompound(component1.selector, component2.selector)
             case var unified?) {
           choices.add([
-            ComplexSelectorComponent(unified, [combinators1.first], span),
+            ComplexSelectorComponent(unified, span, combinator: combinator1),
           ]);
         }
 
@@ -514,7 +527,13 @@ List<List<List<ComplexSelectorComponent>>>? _mergeTrailingCombinators(
           [following, next],
           if (unifyCompound(following.selector, next.selector)
               case var unified?)
-            [ComplexSelectorComponent(unified, next.combinators, span)],
+            [
+              ComplexSelectorComponent(
+                unified,
+                span,
+                combinator: next.combinator,
+              ),
+            ],
         ]);
       }
 
@@ -534,17 +553,16 @@ List<List<List<ComplexSelectorComponent>>>? _mergeTrailingCombinators(
         [siblingComponents.removeLast()],
       ]);
 
-    case (var combinator1?, var combinator2?, _, _)
+    case (var combinator1?, var combinator2?, var components1, _)
         when combinator1 == combinator2:
+      var combinator = components1.last.combinator!;
       var unified = unifyCompound(
         components1.removeLast().selector,
         components2.removeLast().selector,
       );
       if (unified == null) return null;
       result.addFirst([
-        [
-          ComplexSelectorComponent(unified, [combinators1.first], span),
-        ],
+        [ComplexSelectorComponent(unified, span, combinator: combinator)],
       ]);
 
     case (
@@ -670,7 +688,7 @@ QueueList<List<ComplexSelectorComponent>> _groupSelectors(
   var group = <ComplexSelectorComponent>[];
   for (var component in complex) {
     group.add(component);
-    if (component.combinators.isEmpty) {
+    if (component.combinator == null) {
       groups.add(group);
       group = [];
     }
@@ -708,7 +726,6 @@ bool _complexIsParentSuperselector(
   // allocations...
   var base = ComplexSelectorComponent(
     CompoundSelector([PlaceholderSelector('<temp>', bogusSpan)], bogusSpan),
-    const [],
     bogusSpan,
   );
   return complexIsSuperselector([...complex1, base], [...complex2, base]);
@@ -722,10 +739,10 @@ bool complexIsSuperselector(
   List<ComplexSelectorComponent> complex1,
   List<ComplexSelectorComponent> complex2,
 ) {
-  // Selectors with trailing operators are neither superselectors nor
+  // Selectors with trailing combinators are neither superselectors nor
   // subselectors.
-  if (complex1.last.combinators.isNotEmpty) return false;
-  if (complex2.last.combinators.isNotEmpty) return false;
+  if (complex1.last.combinator != null) return false;
+  if (complex2.last.combinator != null) return false;
 
   var i1 = 0;
   var i2 = 0;
@@ -739,19 +756,14 @@ bool complexIsSuperselector(
     if (remaining1 > remaining2) return false;
 
     var component1 = complex1[i1];
-    if (component1.combinators.length > 1) return false;
     if (remaining1 == 1) {
-      if (complex2.any((parent) => parent.combinators.length > 1)) {
-        return false;
-      } else {
-        return compoundIsSuperselector(
-          component1.selector,
-          complex2.last.selector,
-          parents: component1.selector.hasComplicatedSuperselectorSemantics
-              ? complex2.sublist(i2, complex2.length - 1)
-              : null,
-        );
-      }
+      return compoundIsSuperselector(
+        component1.selector,
+        complex2.last.selector,
+        parents: component1.selector.hasComplicatedSuperselectorSemantics
+            ? complex2.sublist(i2, complex2.length - 1)
+            : null,
+      );
     }
 
     // Find the first index [endOfSubselector] in [complex2] such that
@@ -760,7 +772,6 @@ bool complexIsSuperselector(
     var endOfSubselector = i2;
     while (true) {
       var component2 = complex2[endOfSubselector];
-      if (component2.combinators.length > 1) return false;
       if (compoundIsSuperselector(
         component1.selector,
         component2.selector,
@@ -789,8 +800,8 @@ bool complexIsSuperselector(
     }
 
     var component2 = complex2[endOfSubselector];
-    var combinator1 = component1.combinators.firstOrNull;
-    var combinator2 = component2.combinators.firstOrNull;
+    var combinator1 = component1.combinator;
+    var combinator2 = component2.combinator;
     if (!_isSupercombinator(combinator1, combinator2)) {
       return false;
     }
@@ -804,10 +815,8 @@ bool complexIsSuperselector(
         // The selector `.foo ~ .bar` is only a superselector of selectors that
         // *exclusively* contain subcombinators of `~`.
         if (!complex2.take(complex2.length - 1).skip(i2).every(
-              (component) => _isSupercombinator(
-                combinator1,
-                component.combinators.firstOrNull,
-              ),
+              (component) =>
+                  _isSupercombinator(combinator1, component.combinator),
             )) {
           return false;
         }
@@ -838,9 +847,8 @@ bool _compatibleWithPreviousCombinator(
   // only if they're all siblings.
   return parents.every(
     (component) =>
-        component.combinators.firstOrNull?.value ==
-            Combinator.followingSibling ||
-        component.combinators.firstOrNull?.value == Combinator.nextSibling,
+        component.combinator?.value == Combinator.followingSibling ||
+        component.combinator?.value == Combinator.nextSibling,
   );
 }
 
@@ -986,10 +994,10 @@ bool _selectorPseudoIsSuperselector(
           ) ||
           selector1.components.any(
             (complex1) =>
-                complex1.leadingCombinators.isEmpty &&
+                complex1.leadingCombinator == null &&
                 complexIsSuperselector(complex1.components, [
                   ...?parents,
-                  ComplexSelectorComponent(compound2, const [], compound2.span),
+                  ComplexSelectorComponent(compound2, compound2.span),
                 ]),
           );
 
@@ -1009,10 +1017,8 @@ bool _selectorPseudoIsSuperselector(
       ).any((selector2) => selector1.isSuperselector(selector2));
 
     case 'not':
-      return selector1.components.every((complex) {
-        if (complex.isBogus) return false;
-
-        return compound2.components.any(
+      return selector1.components.every(
+        (complex) => compound2.components.any(
           (simple2) => switch (simple2) {
             TypeSelector() => complex.components.last.selector.components.any(
                 (simple1) => simple1 is TypeSelector && simple1 != simple2,
@@ -1025,8 +1031,8 @@ bool _selectorPseudoIsSuperselector(
               listIsSuperselector(selector2.components, [complex]),
             _ => false,
           },
-        );
-      });
+        ),
+      );
 
     case 'current':
       return _selectorPseudoArgs(
diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart
index cd499e4fe..38ad01c2f 100644
--- a/lib/src/functions/selector.dart
+++ b/lib/src/functions/selector.dart
@@ -52,7 +52,11 @@ final _nest = _function("nest", r"$selectors...", (arguments) {
   var first = true;
   return selectors
       .map((selector) {
-        var result = selector.assertSelector(allowParent: !first);
+        var result = selector.assertSelector(
+          allowParent: !first,
+          allowLeadingCombinator: true,
+          allowTrailingCombinator: true,
+        );
         first = false;
         return result;
       })
@@ -69,13 +73,20 @@ final _append = _function("append", r"$selectors...", (arguments) {
   }
 
   var span = EvaluationContext.current.currentCallableSpan;
-  return selectors.map((selector) => selector.assertSelector()).reduce((
-    parent,
-    child,
-  ) {
-    return SelectorList(
+  SelectorList? parent;
+  for (var i = 0; i < selectors.length; i++) {
+    var child = selectors[i].assertSelector(
+      allowLeadingCombinator: true,
+      allowTrailingCombinator: true,
+    );
+    if (parent == null) {
+      parent = child;
+      continue;
+    }
+
+    parent = SelectorList(
       child.components.map((complex) {
-        if (complex.leadingCombinators.isNotEmpty) {
+        if (complex.leadingCombinator != null || complex.components.isEmpty) {
           throw SassScriptException("Can't append $complex to $parent.");
         }
 
@@ -85,25 +96,29 @@ final _append = _function("append", r"$selectors...", (arguments) {
           throw SassScriptException("Can't append $complex to $parent.");
         }
 
-        return ComplexSelector(const [], [
-          ComplexSelectorComponent(newCompound, component.combinators, span),
+        return ComplexSelector([
+          ComplexSelectorComponent(
+            newCompound,
+            span,
+            combinator: component.combinator,
+          ),
           ...rest,
         ], span);
       }),
       span,
     ).nestWithin(parent);
-  }).asSassList;
+  }
+
+  return parent!.asSassList;
 });
 
 final _extend = _function("extend", r"$selector, $extendee, $extender", (
   arguments,
 ) {
-  var selector = arguments[0].assertSelector(name: "selector")
-    ..assertNotBogus(name: "selector");
-  var target = arguments[1].assertSelector(name: "extendee")
-    ..assertNotBogus(name: "extendee");
-  var source = arguments[2].assertSelector(name: "extender")
-    ..assertNotBogus(name: "extender");
+  var selector = arguments[0]
+      .assertSelector(name: "selector", allowLeadingCombinator: true);
+  var target = arguments[1].assertSelector(name: "extendee");
+  var source = arguments[2].assertSelector(name: "extender");
 
   return ExtensionStore.extend(
     selector,
@@ -116,12 +131,10 @@ final _extend = _function("extend", r"$selector, $extendee, $extender", (
 final _replace = _function("replace", r"$selector, $original, $replacement", (
   arguments,
 ) {
-  var selector = arguments[0].assertSelector(name: "selector")
-    ..assertNotBogus(name: "selector");
-  var target = arguments[1].assertSelector(name: "original")
-    ..assertNotBogus(name: "original");
-  var source = arguments[2].assertSelector(name: "replacement")
-    ..assertNotBogus(name: "replacement");
+  var selector = arguments[0]
+      .assertSelector(name: "selector", allowLeadingCombinator: true);
+  var target = arguments[1].assertSelector(name: "original");
+  var source = arguments[2].assertSelector(name: "replacement");
 
   return ExtensionStore.replace(
     selector,
@@ -132,10 +145,10 @@ final _replace = _function("replace", r"$selector, $original, $replacement", (
 });
 
 final _unify = _function("unify", r"$selector1, $selector2", (arguments) {
-  var selector1 = arguments[0].assertSelector(name: "selector1")
-    ..assertNotBogus(name: "selector1");
-  var selector2 = arguments[1].assertSelector(name: "selector2")
-    ..assertNotBogus(name: "selector2");
+  var selector1 = arguments[0]
+      .assertSelector(name: "selector1", allowLeadingCombinator: true);
+  var selector2 = arguments[1]
+      .assertSelector(name: "selector2", allowLeadingCombinator: true);
 
   return selector1.unify(selector2)?.asSassList ?? sassNull;
 });
@@ -143,10 +156,8 @@ final _unify = _function("unify", r"$selector1, $selector2", (arguments) {
 final _isSuperselector = _function("is-superselector", r"$super, $sub", (
   arguments,
 ) {
-  var selector1 = arguments[0].assertSelector(name: "super")
-    ..assertNotBogus(name: "super");
-  var selector2 = arguments[1].assertSelector(name: "sub")
-    ..assertNotBogus(name: "sub");
+  var selector1 = arguments[0].assertSelector(name: "super");
+  var selector2 = arguments[1].assertSelector(name: "sub");
 
   return SassBoolean(selector1.isSuperselector(selector2));
 });
@@ -167,7 +178,13 @@ final _simpleSelectors = _function("simple-selectors", r"$selector", (
 final _parse = _function(
   "parse",
   r"$selector",
-  (arguments) => arguments[0].assertSelector(name: "selector").asSassList,
+  (arguments) => arguments[0]
+      .assertSelector(
+        name: "selector",
+        allowLeadingCombinator: true,
+        allowTrailingCombinator: true,
+      )
+      .asSassList,
 );
 
 /// Adds a [ParentSelector] to the beginning of [compound], or returns `null` if
diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart
index e7380daab..9210dbf25 100644
--- a/lib/src/parse/selector.dart
+++ b/lib/src/parse/selector.dart
@@ -83,10 +83,18 @@ class SelectorParser extends Parser {
   }
 
   /// Consumes a selector list.
-  SelectorList _selectorList() {
+  SelectorList _selectorList({
+    bool allowLeadingCombinator = true,
+    bool allowTrailingCombinator = true,
+  }) {
     var start = scanner.state;
     var previousLine = scanner.line;
-    var components = <ComplexSelector>[_complexSelector()];
+    var components = <ComplexSelector>[
+      _complexSelector(
+        allowLeadingCombinator: allowLeadingCombinator,
+        allowTrailingCombinator: allowTrailingCombinator,
+      ),
+    ];
 
     _whitespace();
     while (scanner.scanChar($comma)) {
@@ -96,7 +104,13 @@ class SelectorParser extends Parser {
 
       var lineBreak = scanner.line != previousLine;
       if (lineBreak) previousLine = scanner.line;
-      components.add(_complexSelector(lineBreak: lineBreak));
+      components.add(
+        _complexSelector(
+          allowLeadingCombinator: allowLeadingCombinator,
+          allowTrailingCombinator: allowTrailingCombinator,
+          lineBreak: lineBreak,
+        ),
+      );
     }
 
     return SelectorList(components, spanFrom(start));
@@ -104,42 +118,52 @@ class SelectorParser extends Parser {
 
   /// Consumes a complex selector.
   ///
+  /// If [allowLeadingCombinator] or [allowTrailingCombinator] is false, leading
+  /// or trailing combinators will be syntax errors, respectively. Both are
+  /// allowed by default.
+  ///
   /// If [lineBreak] is `true`, that indicates that there was a line break
   /// before this selector.
-  ComplexSelector _complexSelector({bool lineBreak = false}) {
+  ComplexSelector _complexSelector({
+    bool allowLeadingCombinator = true,
+    bool allowTrailingCombinator = true,
+    bool lineBreak = false,
+  }) {
     var start = scanner.state;
 
     var componentStart = scanner.state;
     CompoundSelector? lastCompound;
-    var combinators = <CssValue<Combinator>>[];
+    CssValue<Combinator>? combinator;
 
-    List<CssValue<Combinator>>? initialCombinators;
+    CssValue<Combinator>? leadingCombinator;
     var components = <ComplexSelectorComponent>[];
 
     loop:
     while (true) {
       _whitespace();
 
+      var allowCombinator = combinator == null &&
+          (allowLeadingCombinator || lastCompound != null);
       switch (scanner.peekChar()) {
-        case $plus:
+        case $plus when allowCombinator:
           var combinatorStart = scanner.state;
           scanner.readChar();
-          combinators.add(
-            CssValue(Combinator.nextSibling, spanFrom(combinatorStart)),
+          combinator = CssValue(
+            Combinator.nextSibling,
+            spanFrom(combinatorStart),
           );
 
-        case $gt:
+        case $gt when allowCombinator:
           var combinatorStart = scanner.state;
           scanner.readChar();
-          combinators.add(
-            CssValue(Combinator.child, spanFrom(combinatorStart)),
-          );
+          combinator = CssValue(Combinator.child, spanFrom(combinatorStart));
 
-        case $tilde:
+        case $tilde when allowCombinator:
           var combinatorStart = scanner.state;
           scanner.readChar();
-          combinators.add(
-            CssValue(Combinator.followingSibling, spanFrom(combinatorStart)),
+          combinator = CssValue(
+            Combinator.followingSibling,
+            spanFrom(combinatorStart),
           );
 
         case null:
@@ -158,18 +182,18 @@ class SelectorParser extends Parser {
             components.add(
               ComplexSelectorComponent(
                 lastCompound,
-                combinators,
                 spanFrom(componentStart),
+                combinator: combinator,
               ),
             );
-          } else if (combinators.isNotEmpty) {
-            assert(initialCombinators == null);
-            initialCombinators = combinators;
+          } else if (combinator != null) {
+            assert(leadingCombinator == null);
+            leadingCombinator = combinator;
             componentStart = scanner.state;
           }
 
           lastCompound = _compoundSelector();
-          combinators = [];
+          combinator = null;
           if (scanner.peekChar() == $ampersand) {
             scanner.error(
               '"&" may only used at the beginning of a compound selector.',
@@ -181,26 +205,26 @@ class SelectorParser extends Parser {
       }
     }
 
-    if (combinators.isNotEmpty && _plainCss) {
+    if (combinator != null && (_plainCss || !allowTrailingCombinator)) {
       scanner.error("expected selector.");
     } else if (lastCompound != null) {
       components.add(
         ComplexSelectorComponent(
           lastCompound,
-          combinators,
           spanFrom(componentStart),
+          combinator: combinator,
         ),
       );
-    } else if (combinators.isNotEmpty) {
-      initialCombinators = combinators;
+    } else if (combinator != null) {
+      leadingCombinator = combinator;
     } else {
       scanner.error("expected selector.");
     }
 
     return ComplexSelector(
-      initialCombinators ?? const [],
       components,
       spanFrom(start),
+      leadingCombinator: leadingCombinator,
       lineBreak: lineBreak,
     );
   }
@@ -403,12 +427,18 @@ class SelectorParser extends Parser {
     SelectorList? selector;
     if (element) {
       if (_selectorPseudoElements.contains(unvendored)) {
-        selector = _selectorList();
+        selector = _selectorList(
+          allowLeadingCombinator: false,
+          allowTrailingCombinator: false,
+        );
       } else {
         argument = declarationValue(allowEmpty: true);
       }
     } else if (_selectorPseudoClasses.contains(unvendored)) {
-      selector = _selectorList();
+      selector = _selectorList(
+        allowLeadingCombinator: equalsIgnoreCase(name, 'has'),
+        allowTrailingCombinator: false,
+      );
     } else if (unvendored == "nth-child" || unvendored == "nth-last-child") {
       argument = _aNPlusB();
       _whitespace();
@@ -417,7 +447,10 @@ class SelectorParser extends Parser {
         argument += " of";
         _whitespace();
 
-        selector = _selectorList();
+        selector = _selectorList(
+          allowLeadingCombinator: false,
+          allowTrailingCombinator: false,
+        );
       }
     } else {
       argument = declarationValue(allowEmpty: true).trimRight();
diff --git a/lib/src/util/map.dart b/lib/src/util/map.dart
index 865b213bc..8ff278a26 100644
--- a/lib/src/util/map.dart
+++ b/lib/src/util/map.dart
@@ -21,5 +21,6 @@ extension MapExtensions<K, V> on Map<K, V> {
 
   /// Returns an option that contains the value at [key] if one exists and null
   /// otherwise.
-  Option<V> getOption(K key) => containsKey(key) ? (this[key] as V,) : null;
+  Option<V> getOption(K key) =>
+      containsKey(key) ? some(this[key] as V) : none();
 }
diff --git a/lib/src/util/option.dart b/lib/src/util/option.dart
index 84d296a80..cf272fa82 100644
--- a/lib/src/util/option.dart
+++ b/lib/src/util/option.dart
@@ -5,8 +5,14 @@
 /// A type that represents either the presence of a value of type `T` or its
 /// absence.
 ///
-/// When the option is present, this will be a single-element tuple that
-/// contains the value. If it's absent, it will be null. This allows callers to
-/// distinguish between a present null value and a value that's absent
-/// altogether.
+/// When the option is present (also known as "some"), this will be a
+/// single-element tuple that contains the value. If it's absent (also known as
+/// "none"), it will be null. This allows callers to distinguish between a
+/// present null value and a value that's absent altogether.
 typedef Option<T> = (T,)?;
+
+/// Creates a present option with the given [value].
+Option<T> some<T>(T value) => (value,);
+
+/// Creates an absent option.
+Option<T> none<T>() => null;
diff --git a/lib/src/util/span.dart b/lib/src/util/span.dart
index 7d84cbd63..fe0d2ad7e 100644
--- a/lib/src/util/span.dart
+++ b/lib/src/util/span.dart
@@ -136,6 +136,12 @@ extension SpanExtensions on FileSpan {
       file.url == target.file.url &&
       start.offset <= target.start.offset &&
       end.offset >= target.end.offset;
+
+  /// Whether this [FileSpan] covers the same region as [other].
+  bool equals(FileSpan other) =>
+      file.url == other.file.url &&
+      start.offset == other.start.offset &&
+      end.offset == other.end.offset;
 }
 
 /// Consumes an identifier from [scanner].
diff --git a/lib/src/value.dart b/lib/src/value.dart
index 915b0e680..7154c9dd3 100644
--- a/lib/src/value.dart
+++ b/lib/src/value.dart
@@ -448,25 +448,33 @@ extension SassApiValue on Value {
   /// Parses `this` as a selector list, in the same manner as the
   /// `selector-parse()` function.
   ///
-  /// Throws a [SassScriptException] if this isn't a type that can be parsed as a
-  /// selector, or if parsing fails. If [allowParent] is `true`, this allows
-  /// [ParentSelector]s. Otherwise, they're considered parse errors.
+  /// Throws a [SassException] if this isn't a type that can be parsed as a
+  /// selector, or if parsing fails.
+  ///
+  /// If [allowParent] is `true`, this allows [ParentSelector]s. Otherwise,
+  /// they're considered parse errors.
+  ///
+  /// If [allowLeadingCombinator] or [allowTrailingCombinator] is `true`, this
+  /// allows selectors with leading or trailing selector combinators,
+  /// respectively. Otherwise, they produce errors after parsing. Both must be
+  /// true to allow a selector that consists only of a combinator.
   ///
   /// If this came from a function argument, [name] is the argument name
   /// (without the `$`). It's used for error reporting.
-  SelectorList assertSelector({String? name, bool allowParent = false}) {
+  SelectorList assertSelector({
+    String? name,
+    bool allowParent = false,
+    bool allowLeadingCombinator = false,
+    bool allowTrailingCombinator = false,
+  }) {
     var string = _selectorString(name);
-    try {
-      return SelectorList.parse(string, allowParent: allowParent);
-    } on SassFormatException catch (error, stackTrace) {
-      // TODO(nweiz): colorize this if we're running in an environment where
-      // that works.
-      throwWithTrace(
-        SassScriptException(error.toString().replaceFirst("Error: ", ""), name),
-        error,
-        stackTrace,
+    return _addNameToFormatException(
+        name, () => SelectorList.parse(string, allowParent: allowParent))
+      ..assertValid(
+        name: name,
+        allowLeadingCombinator: allowLeadingCombinator,
+        allowTrailingCombinator: allowTrailingCombinator,
       );
-    }
   }
 
   /// Parses `this` as a simple selector, in the same manner as the
@@ -483,17 +491,8 @@ extension SassApiValue on Value {
     bool allowParent = false,
   }) {
     var string = _selectorString(name);
-    try {
-      return SimpleSelector.parse(string, allowParent: allowParent);
-    } on SassFormatException catch (error, stackTrace) {
-      // TODO(nweiz): colorize this if we're running in an environment where
-      // that works.
-      throwWithTrace(
-        SassScriptException(error.toString().replaceFirst("Error: ", ""), name),
-        error,
-        stackTrace,
-      );
-    }
+    return _addNameToFormatException(
+        name, () => SimpleSelector.parse(string, allowParent: allowParent));
   }
 
   /// Parses `this` as a compound selector, in the same manner as the
@@ -510,17 +509,8 @@ extension SassApiValue on Value {
     bool allowParent = false,
   }) {
     var string = _selectorString(name);
-    try {
-      return CompoundSelector.parse(string, allowParent: allowParent);
-    } on SassFormatException catch (error, stackTrace) {
-      // TODO(nweiz): colorize this if we're running in an environment where
-      // that works.
-      throwWithTrace(
-        SassScriptException(error.toString().replaceFirst("Error: ", ""), name),
-        error,
-        stackTrace,
-      );
-    }
+    return _addNameToFormatException(
+        name, () => CompoundSelector.parse(string, allowParent: allowParent));
   }
 
   /// Parses `this` as a complex selector, in the same manner as the
@@ -530,23 +520,38 @@ extension SassApiValue on Value {
   /// selector, or if parsing fails. If [allowParent] is `true`, this allows
   /// [ParentSelector]s. Otherwise, they're considered parse errors.
   ///
+  /// If [allowLeadingCombinator] or [allowTrailingCombinator] is `true`, this
+  /// allows selectors with leading or trailing selector combinators,
+  /// respectively. Otherwise, they produce errors after parsing. Both must be
+  /// true to allow a selector that consists only of a combinator.
+  ///
   /// If this came from a function argument, [name] is the argument name
   /// (without the `$`). It's used for error reporting.
   ComplexSelector assertComplexSelector({
     String? name,
     bool allowParent = false,
+    bool allowLeadingCombinator = false,
+    bool allowTrailingCombinator = false,
   }) {
     var string = _selectorString(name);
-    try {
-      return ComplexSelector.parse(string, allowParent: allowParent);
-    } on SassFormatException catch (error, stackTrace) {
-      // TODO(nweiz): colorize this if we're running in an environment where
-      // that works.
-      throwWithTrace(
-        SassScriptException(error.toString().replaceFirst("Error: ", ""), name),
-        error,
-        stackTrace,
+    return _addNameToFormatException(
+        name, () => ComplexSelector.parse(string, allowParent: allowParent))
+      ..assertValid(
+        name: name,
+        allowLeadingCombinator: allowLeadingCombinator,
+        allowTrailingCombinator: allowTrailingCombinator,
       );
-    }
+  }
+}
+
+/// Runs [callback], and if it throws a [SassFormatException], adds [name] to
+/// tbe beginning of the message.
+T _addNameToFormatException<T>(String? name, T callback()) {
+  try {
+    return callback();
+  } on SassFormatException catch (error, stackTrace) {
+    if (name == null) rethrow;
+    throwWithTrace(
+        error.withMessage("\$$name: ${error.message}"), error, stackTrace);
   }
 }
diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart
index 70dfb415a..8728c5fad 100644
--- a/lib/src/visitor/async_evaluate.dart
+++ b/lib/src/visitor/async_evaluate.dart
@@ -477,7 +477,7 @@ final class _EvaluateVisitor
             );
             if (local != null || namespace != null) return local;
             return _builtInFunctions[normalizedName];
-          });
+          }, label: "function call");
           if (callable == null) throw "Function not found: $name";
 
           return SassFunction(callable);
@@ -497,6 +497,7 @@ final class _EvaluateVisitor
             name.text.replaceAll("_", "-"),
             namespace: module?.text,
           ),
+          label: "mixin include",
         );
         if (callable == null) throw "Mixin not found: $name";
 
@@ -767,6 +768,8 @@ final class _EvaluateVisitor
         );
       }
 
+      // TODO: Is this exception span necessary?
+
       // Always consider built-in stylesheets to be "already loaded", since they
       // never require additional execution to load and never produce CSS.
       await _addExceptionSpanAsync(
@@ -819,6 +822,7 @@ final class _EvaluateVisitor
         _inDependency = oldInDependency;
       }
 
+      // TODO: Is this exception span necessary?
       await _addExceptionSpanAsync(
         nodeWithSpan,
         () => callback(module, firstLoad),
@@ -1472,18 +1476,13 @@ final class _EvaluateVisitor
     }
 
     for (var complex in styleRule.originalSelector.components) {
-      if (!complex.isBogus) continue;
-      _warn(
-        'The selector "${complex.toString().trim()}" is invalid CSS and ' +
-            (complex.isUseless ? "can't" : "shouldn't") +
-            ' be an extender.\n'
-                'This will be an error in Dart Sass 2.0.0.\n'
-                '\n'
-                'More info: https://sass-lang.com/d/bogus-combinators',
-        MultiSpan(complex.span.trimRight(), 'invalid selector', {
-          node.span: '@extend rule',
-        }),
-        Deprecation.bogusCombinators,
+      if (complex.isStandAlone) continue;
+      throw MultiSpanSassRuntimeException(
+        "This selector is invalid CSS and can't be an extender.",
+        complex.span.trimRight(),
+        'invalid selector',
+        {node.span: '@extend rule'},
+        _stackTrace(complex.span),
       );
     }
 
@@ -2153,6 +2152,7 @@ final class _EvaluateVisitor
     var mixin = _addExceptionSpan(
       node,
       () => _environment.getMixin(node.name, namespace: node.namespace),
+      label: "mixin include",
     );
     if (node.originalName.startsWith('--') &&
         mixin is UserDefinedCallable &&
@@ -2377,14 +2377,11 @@ final class _EvaluateVisitor
     if (nest) {
       if (_stylesheet.plainCss) {
         for (var complex in parsedSelector.components) {
-          if (complex.leadingCombinators
-              case [
-                var first,
-                ...,
-              ] when _stylesheet.plainCss) {
+          if (complex.leadingCombinator case var combinator?
+              when _stylesheet.plainCss) {
             throw _exception(
               "Top-level leading combinators aren't allowed in plain CSS.",
-              first.span,
+              combinator.span,
             );
           }
         }
@@ -2420,7 +2417,7 @@ final class _EvaluateVisitor
     );
     _atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
 
-    _warnForBogusCombinators(rule);
+    _checkBogusCombinators(rule);
 
     if (_styleRule == null && _parent.children.isNotEmpty) {
       var lastChild = _parent.children.last;
@@ -2430,54 +2427,37 @@ final class _EvaluateVisitor
     return null;
   }
 
-  /// Emits deprecation warnings for any bogus combinators in [rule].
-  void _warnForBogusCombinators(CssStyleRule rule) {
-    if (!rule.isInvisibleOtherThanBogusCombinators) {
-      for (var complex in rule.selector.components) {
-        if (!complex.isBogus) continue;
-
-        if (complex.isUseless) {
-          _warn(
-            'The selector "${complex.toString().trim()}" is invalid CSS. It '
-            'will be omitted from the generated CSS.\n'
-            'This will be an error in Dart Sass 2.0.0.\n'
-            '\n'
-            'More info: https://sass-lang.com/d/bogus-combinators',
-            complex.span.trimRight(),
-            Deprecation.bogusCombinators,
-          );
-        } else if (complex.leadingCombinators.isNotEmpty) {
-          if (!_stylesheet.plainCss) {
-            _warn(
-              'The selector "${complex.toString().trim()}" is invalid CSS.\n'
-              'This will be an error in Dart Sass 2.0.0.\n'
-              '\n'
-              'More info: https://sass-lang.com/d/bogus-combinators',
-              complex.span.trimRight(),
-              Deprecation.bogusCombinators,
-            );
-          }
-        } else {
-          _warn(
-            'The selector "${complex.toString().trim()}" is only valid for '
-                    "nesting and shouldn't\n"
-                    'have children other than style rules.' +
-                (complex.isBogusOtherThanLeadingCombinator
-                    ? ' It will be omitted from the generated CSS.'
-                    : '') +
-                '\n'
-                    'This will be an error in Dart Sass 2.0.0.\n'
-                    '\n'
-                    'More info: https://sass-lang.com/d/bogus-combinators',
-            MultiSpan(complex.span.trimRight(), 'invalid selector', {
-              rule.children.first.span: "this is not a style rule" +
-                  (rule.children.every((child) => child is CssComment)
-                      ? '\n(try converting to a //-style comment)'
-                      : ''),
-            }),
-            Deprecation.bogusCombinators,
+  /// Throw errors for any bogus combinators in [rule].
+  void _checkBogusCombinators(CssStyleRule rule) {
+    if (rule.isInvisible) return;
+
+    for (var complex in rule.selector.components) {
+      if (complex.isStandAlone) continue;
+
+      if (complex.leadingCombinator != null) {
+        if (!_stylesheet.plainCss) {
+          var span = complex.span.trimRight();
+          throw SassRuntimeException(
+            'This selector is invalid CSS.',
+            span,
+            _stackTrace(span),
           );
         }
+      } else {
+        throw MultiSpanSassRuntimeException(
+          "This selector is only valid for nesting and shouldn't have "
+              "children other than\n"
+              'style rules.',
+          complex.span.trimRight(),
+          'invalid selector',
+          {
+            rule.children.first.span: "this is not a style rule" +
+                (rule.children.every((child) => child is CssComment)
+                    ? '\n(try converting to a //-style comment)'
+                    : ''),
+          },
+          _stackTrace(complex.span),
+        );
       }
     }
   }
@@ -2596,7 +2576,7 @@ final class _EvaluateVisitor
               override.assignmentNode,
               global: true,
             );
-          });
+          }, label: "variable declaration");
           return null;
         }
       }
@@ -2604,6 +2584,7 @@ final class _EvaluateVisitor
       var value = _addExceptionSpan(
         node,
         () => _environment.getVariable(node.name, namespace: node.namespace),
+        label: "variable declaration",
       );
       if (value != null && value != sassNull) return null;
     }
@@ -2639,7 +2620,7 @@ final class _EvaluateVisitor
         namespace: node.namespace,
         global: node.isGlobal,
       );
-    });
+    }, label: "variable declaration");
     return null;
   }
 
@@ -2671,10 +2652,7 @@ final class _EvaluateVisitor
   }
 
   Future<Value?> visitWarnRule(WarnRule node) async {
-    var value = await _addExceptionSpanAsync(
-      node,
-      () => node.expression.accept(this),
-    );
+    var value = await node.expression.accept(this);
     _logger.warn(
       value is SassString ? value.text : _serialize(value, node.expression),
       trace: _stackTrace(node.span),
@@ -2815,6 +2793,7 @@ final class _EvaluateVisitor
     var result = _addExceptionSpan(
       node,
       () => _environment.getVariable(node.name, namespace: node.namespace),
+      label: "variable use",
     );
     if (result != null) return result;
     throw _exception("Undefined variable.", node.span);
@@ -2839,7 +2818,7 @@ final class _EvaluateVisitor
 
   Future<Value> visitIfExpression(IfExpression node) async {
     var (positional, named) = await _evaluateMacroArguments(node);
-    _verifyArguments(positional.length, named, IfExpression.declaration, node);
+    _verifyParameters(positional.length, named, IfExpression.declaration, node);
 
     // ignore: prefer_is_empty
     var condition = positional.elementAtOrNull(0) ?? named["condition"]!;
@@ -2907,6 +2886,7 @@ final class _EvaluateVisitor
               node.name,
               namespace: node.namespace,
             ),
+            label: "function call",
           );
     if (function == null) {
       if (node.namespace != null) {
@@ -3388,7 +3368,7 @@ final class _EvaluateVisitor
       // don't affect the underlying environment closure.
       return _withEnvironment(callable.environment.closure(), () {
         return _environment.scope(() async {
-          _verifyArguments(
+          _verifyParameters(
             evaluated.positional.length,
             evaluated.named,
             callable.declaration.parameters,
@@ -3564,10 +3544,9 @@ final class _EvaluateVisitor
       evaluated.positional.length,
       namedSet,
     );
-    _addExceptionSpan(
-      nodeWithSpan,
-      () => overload.verify(evaluated.positional.length, namedSet),
-    );
+    _addExceptionSpan(nodeWithSpan,
+        () => overload.verify(evaluated.positional.length, namedSet),
+        label: "invocation");
 
     var parameters = overload.parameters;
     for (var i = evaluated.positional.length; i < parameters.length; i++) {
@@ -3607,6 +3586,7 @@ final class _EvaluateVisitor
       result = await _addExceptionSpanAsync(
         nodeWithSpan,
         () => callback(evaluated.positional),
+        label: "invocation",
       );
     } on SassException {
       rethrow;
@@ -3834,16 +3814,14 @@ final class _EvaluateVisitor
 
   /// Throws a [SassRuntimeException] if [positional] and [named] aren't valid
   /// when applied to [parameters].
-  void _verifyArguments(
+  void _verifyParameters(
     int positional,
     Map<String, dynamic> named,
     ParameterList parameters,
     AstNode nodeWithSpan,
   ) =>
       _addExceptionSpan(
-        nodeWithSpan,
-        () => parameters.verify(positional, MapKeySet(named)),
-      );
+          nodeWithSpan, () => parameters.verify(positional, MapKeySet(named)));
 
   Future<Value> visitSelectorExpression(SelectorExpression node) async =>
       _styleRuleIgnoringAtRoot?.originalSelector.asSassList ?? sassNull;
@@ -4324,6 +4302,7 @@ final class _EvaluateVisitor
               expression.name,
               namespace: expression.namespace,
             ),
+            label: "variable",
           ) ??
           expression;
     } else {
@@ -4547,11 +4526,15 @@ final class _EvaluateVisitor
   /// [AstNode.span] if the span isn't required, since some nodes need to do
   /// real work to manufacture a source span.
   ///
+  /// The [label] is used for [nodeWithSpan]'s span if the error already has a
+  /// source span.
+  ///
   /// If [addStackFrame] is true (the default), this will add an innermost stack
   /// frame for [nodeWithSpan]. Otherwise, it will use the existing stack as-is.
   T _addExceptionSpan<T>(
     AstNode nodeWithSpan,
     T callback(), {
+    String? label,
     bool addStackFrame = true,
   }) {
     try {
@@ -4564,6 +4547,9 @@ final class _EvaluateVisitor
         error,
         stackTrace,
       );
+    } on SassException catch (error, stackTrace) {
+      throwWithTrace(_adjustException(error, nodeWithSpan.span, label: label),
+          error, stackTrace);
     }
   }
 
@@ -4571,6 +4557,7 @@ final class _EvaluateVisitor
   Future<T> _addExceptionSpanAsync<T>(
     AstNode nodeWithSpan,
     FutureOr<T> callback(), {
+    String? label,
     bool addStackFrame = true,
   }) async {
     try {
@@ -4583,9 +4570,32 @@ final class _EvaluateVisitor
         error,
         stackTrace,
       );
+    } on SassException catch (error, stackTrace) {
+      throwWithTrace(_adjustException(error, nodeWithSpan.span, label: label),
+          error, stackTrace);
     }
   }
 
+  /// Adjusts [exception] for [_addExceptionSpan].
+  SassException _adjustException(
+    SassException error,
+    FileSpan span, {
+    String? label,
+  }) {
+    // Don't add a duplicate span. This can happen when using `meta.call()` or
+    // `meta.include()`, since those involve two nested calls to
+    // [_addExceptionSpan] with the same span.
+    if (label != null && !error.hasSpan(span)) {
+      error = error.withAdditionalSpan(span, label);
+    }
+
+    if (error is! SassRuntimeException) {
+      error = error.withTrace(_stackTrace(error.span));
+    }
+
+    return error;
+  }
+
   /// Runs [callback], and converts any [SassException]s that aren't already
   /// [SassRuntimeException]s to [SassRuntimeException]s with the current stack
   /// trace.
diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart
index be4707f2b..892293411 100644
--- a/lib/src/visitor/evaluate.dart
+++ b/lib/src/visitor/evaluate.dart
@@ -5,7 +5,7 @@
 // DO NOT EDIT. This file was generated from async_evaluate.dart.
 // See tool/grind/synchronize.dart for details.
 //
-// Checksum: 25aa2d050126950ea37dc1c53539f0b041356e8e
+// Checksum: 6954fd19b9353445b3cf014505a4f3552cf6b0f0
 //
 // ignore_for_file: unused_import
 
@@ -485,7 +485,7 @@ final class _EvaluateVisitor
             );
             if (local != null || namespace != null) return local;
             return _builtInFunctions[normalizedName];
-          });
+          }, label: "function call");
           if (callable == null) throw "Function not found: $name";
 
           return SassFunction(callable);
@@ -505,6 +505,7 @@ final class _EvaluateVisitor
             name.text.replaceAll("_", "-"),
             namespace: module?.text,
           ),
+          label: "mixin include",
         );
         if (callable == null) throw "Mixin not found: $name";
 
@@ -775,6 +776,8 @@ final class _EvaluateVisitor
         );
       }
 
+      // TODO: Is this exception span necessary?
+
       // Always consider built-in stylesheets to be "already loaded", since they
       // never require additional execution to load and never produce CSS.
       _addExceptionSpan(
@@ -827,6 +830,7 @@ final class _EvaluateVisitor
         _inDependency = oldInDependency;
       }
 
+      // TODO: Is this exception span necessary?
       _addExceptionSpan(
         nodeWithSpan,
         () => callback(module, firstLoad),
@@ -1480,18 +1484,13 @@ final class _EvaluateVisitor
     }
 
     for (var complex in styleRule.originalSelector.components) {
-      if (!complex.isBogus) continue;
-      _warn(
-        'The selector "${complex.toString().trim()}" is invalid CSS and ' +
-            (complex.isUseless ? "can't" : "shouldn't") +
-            ' be an extender.\n'
-                'This will be an error in Dart Sass 2.0.0.\n'
-                '\n'
-                'More info: https://sass-lang.com/d/bogus-combinators',
-        MultiSpan(complex.span.trimRight(), 'invalid selector', {
-          node.span: '@extend rule',
-        }),
-        Deprecation.bogusCombinators,
+      if (complex.isStandAlone) continue;
+      throw MultiSpanSassRuntimeException(
+        "This selector is invalid CSS and can't be an extender.",
+        complex.span.trimRight(),
+        'invalid selector',
+        {node.span: '@extend rule'},
+        _stackTrace(complex.span),
       );
     }
 
@@ -2160,6 +2159,7 @@ final class _EvaluateVisitor
     var mixin = _addExceptionSpan(
       node,
       () => _environment.getMixin(node.name, namespace: node.namespace),
+      label: "mixin include",
     );
     if (node.originalName.startsWith('--') &&
         mixin is UserDefinedCallable &&
@@ -2384,14 +2384,11 @@ final class _EvaluateVisitor
     if (nest) {
       if (_stylesheet.plainCss) {
         for (var complex in parsedSelector.components) {
-          if (complex.leadingCombinators
-              case [
-                var first,
-                ...,
-              ] when _stylesheet.plainCss) {
+          if (complex.leadingCombinator case var combinator?
+              when _stylesheet.plainCss) {
             throw _exception(
               "Top-level leading combinators aren't allowed in plain CSS.",
-              first.span,
+              combinator.span,
             );
           }
         }
@@ -2427,7 +2424,7 @@ final class _EvaluateVisitor
     );
     _atRootExcludingStyleRule = oldAtRootExcludingStyleRule;
 
-    _warnForBogusCombinators(rule);
+    _checkBogusCombinators(rule);
 
     if (_styleRule == null && _parent.children.isNotEmpty) {
       var lastChild = _parent.children.last;
@@ -2437,54 +2434,37 @@ final class _EvaluateVisitor
     return null;
   }
 
-  /// Emits deprecation warnings for any bogus combinators in [rule].
-  void _warnForBogusCombinators(CssStyleRule rule) {
-    if (!rule.isInvisibleOtherThanBogusCombinators) {
-      for (var complex in rule.selector.components) {
-        if (!complex.isBogus) continue;
-
-        if (complex.isUseless) {
-          _warn(
-            'The selector "${complex.toString().trim()}" is invalid CSS. It '
-            'will be omitted from the generated CSS.\n'
-            'This will be an error in Dart Sass 2.0.0.\n'
-            '\n'
-            'More info: https://sass-lang.com/d/bogus-combinators',
-            complex.span.trimRight(),
-            Deprecation.bogusCombinators,
-          );
-        } else if (complex.leadingCombinators.isNotEmpty) {
-          if (!_stylesheet.plainCss) {
-            _warn(
-              'The selector "${complex.toString().trim()}" is invalid CSS.\n'
-              'This will be an error in Dart Sass 2.0.0.\n'
-              '\n'
-              'More info: https://sass-lang.com/d/bogus-combinators',
-              complex.span.trimRight(),
-              Deprecation.bogusCombinators,
-            );
-          }
-        } else {
-          _warn(
-            'The selector "${complex.toString().trim()}" is only valid for '
-                    "nesting and shouldn't\n"
-                    'have children other than style rules.' +
-                (complex.isBogusOtherThanLeadingCombinator
-                    ? ' It will be omitted from the generated CSS.'
-                    : '') +
-                '\n'
-                    'This will be an error in Dart Sass 2.0.0.\n'
-                    '\n'
-                    'More info: https://sass-lang.com/d/bogus-combinators',
-            MultiSpan(complex.span.trimRight(), 'invalid selector', {
-              rule.children.first.span: "this is not a style rule" +
-                  (rule.children.every((child) => child is CssComment)
-                      ? '\n(try converting to a //-style comment)'
-                      : ''),
-            }),
-            Deprecation.bogusCombinators,
+  /// Throw errors for any bogus combinators in [rule].
+  void _checkBogusCombinators(CssStyleRule rule) {
+    if (rule.isInvisible) return;
+
+    for (var complex in rule.selector.components) {
+      if (complex.isStandAlone) continue;
+
+      if (complex.leadingCombinator != null) {
+        if (!_stylesheet.plainCss) {
+          var span = complex.span.trimRight();
+          throw SassRuntimeException(
+            'This selector is invalid CSS.',
+            span,
+            _stackTrace(span),
           );
         }
+      } else {
+        throw MultiSpanSassRuntimeException(
+          "This selector is only valid for nesting and shouldn't have "
+              "children other than\n"
+              'style rules.',
+          complex.span.trimRight(),
+          'invalid selector',
+          {
+            rule.children.first.span: "this is not a style rule" +
+                (rule.children.every((child) => child is CssComment)
+                    ? '\n(try converting to a //-style comment)'
+                    : ''),
+          },
+          _stackTrace(complex.span),
+        );
       }
     }
   }
@@ -2601,7 +2581,7 @@ final class _EvaluateVisitor
               override.assignmentNode,
               global: true,
             );
-          });
+          }, label: "variable declaration");
           return null;
         }
       }
@@ -2609,6 +2589,7 @@ final class _EvaluateVisitor
       var value = _addExceptionSpan(
         node,
         () => _environment.getVariable(node.name, namespace: node.namespace),
+        label: "variable declaration",
       );
       if (value != null && value != sassNull) return null;
     }
@@ -2644,7 +2625,7 @@ final class _EvaluateVisitor
         namespace: node.namespace,
         global: node.isGlobal,
       );
-    });
+    }, label: "variable declaration");
     return null;
   }
 
@@ -2676,10 +2657,7 @@ final class _EvaluateVisitor
   }
 
   Value? visitWarnRule(WarnRule node) {
-    var value = _addExceptionSpan(
-      node,
-      () => node.expression.accept(this),
-    );
+    var value = node.expression.accept(this);
     _logger.warn(
       value is SassString ? value.text : _serialize(value, node.expression),
       trace: _stackTrace(node.span),
@@ -2818,6 +2796,7 @@ final class _EvaluateVisitor
     var result = _addExceptionSpan(
       node,
       () => _environment.getVariable(node.name, namespace: node.namespace),
+      label: "variable use",
     );
     if (result != null) return result;
     throw _exception("Undefined variable.", node.span);
@@ -2842,7 +2821,7 @@ final class _EvaluateVisitor
 
   Value visitIfExpression(IfExpression node) {
     var (positional, named) = _evaluateMacroArguments(node);
-    _verifyArguments(positional.length, named, IfExpression.declaration, node);
+    _verifyParameters(positional.length, named, IfExpression.declaration, node);
 
     // ignore: prefer_is_empty
     var condition = positional.elementAtOrNull(0) ?? named["condition"]!;
@@ -2908,6 +2887,7 @@ final class _EvaluateVisitor
               node.name,
               namespace: node.namespace,
             ),
+            label: "function call",
           );
     if (function == null) {
       if (node.namespace != null) {
@@ -3389,7 +3369,7 @@ final class _EvaluateVisitor
       // don't affect the underlying environment closure.
       return _withEnvironment(callable.environment.closure(), () {
         return _environment.scope(() {
-          _verifyArguments(
+          _verifyParameters(
             evaluated.positional.length,
             evaluated.named,
             callable.declaration.parameters,
@@ -3565,10 +3545,9 @@ final class _EvaluateVisitor
       evaluated.positional.length,
       namedSet,
     );
-    _addExceptionSpan(
-      nodeWithSpan,
-      () => overload.verify(evaluated.positional.length, namedSet),
-    );
+    _addExceptionSpan(nodeWithSpan,
+        () => overload.verify(evaluated.positional.length, namedSet),
+        label: "invocation");
 
     var parameters = overload.parameters;
     for (var i = evaluated.positional.length; i < parameters.length; i++) {
@@ -3608,6 +3587,7 @@ final class _EvaluateVisitor
       result = _addExceptionSpan(
         nodeWithSpan,
         () => callback(evaluated.positional),
+        label: "invocation",
       );
     } on SassException {
       rethrow;
@@ -3835,16 +3815,14 @@ final class _EvaluateVisitor
 
   /// Throws a [SassRuntimeException] if [positional] and [named] aren't valid
   /// when applied to [parameters].
-  void _verifyArguments(
+  void _verifyParameters(
     int positional,
     Map<String, dynamic> named,
     ParameterList parameters,
     AstNode nodeWithSpan,
   ) =>
       _addExceptionSpan(
-        nodeWithSpan,
-        () => parameters.verify(positional, MapKeySet(named)),
-      );
+          nodeWithSpan, () => parameters.verify(positional, MapKeySet(named)));
 
   Value visitSelectorExpression(SelectorExpression node) =>
       _styleRuleIgnoringAtRoot?.originalSelector.asSassList ?? sassNull;
@@ -4325,6 +4303,7 @@ final class _EvaluateVisitor
               expression.name,
               namespace: expression.namespace,
             ),
+            label: "variable",
           ) ??
           expression;
     } else {
@@ -4548,11 +4527,15 @@ final class _EvaluateVisitor
   /// [AstNode.span] if the span isn't required, since some nodes need to do
   /// real work to manufacture a source span.
   ///
+  /// The [label] is used for [nodeWithSpan]'s span if the error already has a
+  /// source span.
+  ///
   /// If [addStackFrame] is true (the default), this will add an innermost stack
   /// frame for [nodeWithSpan]. Otherwise, it will use the existing stack as-is.
   T _addExceptionSpan<T>(
     AstNode nodeWithSpan,
     T callback(), {
+    String? label,
     bool addStackFrame = true,
   }) {
     try {
@@ -4565,9 +4548,32 @@ final class _EvaluateVisitor
         error,
         stackTrace,
       );
+    } on SassException catch (error, stackTrace) {
+      throwWithTrace(_adjustException(error, nodeWithSpan.span, label: label),
+          error, stackTrace);
     }
   }
 
+  /// Adjusts [exception] for [_addExceptionSpan].
+  SassException _adjustException(
+    SassException error,
+    FileSpan span, {
+    String? label,
+  }) {
+    // Don't add a duplicate span. This can happen when using `meta.call()` or
+    // `meta.include()`, since those involve two nested calls to
+    // [_addExceptionSpan] with the same span.
+    if (label != null && !error.hasSpan(span)) {
+      error = error.withAdditionalSpan(span, label);
+    }
+
+    if (error is! SassRuntimeException) {
+      error = error.withTrace(_stackTrace(error.span));
+    }
+
+    return error;
+  }
+
   /// Runs [callback], and converts any [SassException]s that aren't already
   /// [SassRuntimeException]s to [SassRuntimeException]s with the current stack
   /// trace.
diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart
index 2856a24fa..65bf4a701 100644
--- a/lib/src/visitor/serialize.dart
+++ b/lib/src/visitor/serialize.dart
@@ -1598,10 +1598,12 @@ final class _SerializeVisitor
   }
 
   void visitComplexSelector(ComplexSelector complex) {
-    _writeCombinators(complex.leadingCombinators);
+    if (complex.leadingCombinator case var combinator?) {
+      _buffer.write(combinator);
+    }
     if (complex
         case ComplexSelector(
-          leadingCombinators: [_, ...],
+          leadingCombinator: var _?,
           components: [_, ...],
         )) {
       _writeOptionalSpace();
@@ -1610,20 +1612,19 @@ final class _SerializeVisitor
     for (var i = 0; i < complex.components.length; i++) {
       var component = complex.components[i];
       visitCompoundSelector(component.selector);
-      if (component.combinators.isNotEmpty) _writeOptionalSpace();
-      _writeCombinators(component.combinators);
+
+      if (component.combinator case var combinator?) {
+        _writeOptionalSpace();
+        _buffer.write(combinator);
+      }
+
       if (i != complex.components.length - 1 &&
-          (!_isCompressed || component.combinators.isEmpty)) {
+          (!_isCompressed || component.combinator == null)) {
         _buffer.writeCharCode($space);
       }
     }
   }
 
-  /// Writes [combinators] to [_buffer], with spaces in between in expanded
-  /// mode.
-  void _writeCombinators(List<CssValue<Combinator>> combinators) =>
-      _writeBetween(combinators, _isCompressed ? '' : ' ', _buffer.write);
-
   void visitCompoundSelector(CompoundSelector compound) {
     var start = _buffer.length;
     for (var simple in compound.components) {
diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md
index e71149ee9..1c334397d 100644
--- a/pkg/sass_api/CHANGELOG.md
+++ b/pkg/sass_api/CHANGELOG.md
@@ -1,6 +1,33 @@
 ## 16.0.0
 
-* No user-visible changes.
+### Bogus Selectors
+
+* Drop support for bogus selectors that can never become valid CSS through
+  nesting, including complex selectors with multiple combinators in a row and
+  selector pseudos whose selectors contain leading and trailing combinators
+  (except for `:has()`, which explicitly allows a leading combinator).
+
+* Replace `ComplexSelector.leadingCombinators` with a nullable
+  `ComplexSelector.leadingCombinator`.
+
+* Remove the `leadingCombinators` argument from `ComplexSelector()` and replace
+  it with a named `leadingCombinator` argument.
+
+* Replace `ComplexSelectorComponent.combinators` with a nullable
+  `ComplexSelectorComponent.combinator`.
+
+* Remove the `combinators` argument from `ComplexSelectorComponent()` and replace
+  it with a named `combinator` argument.
+
+* Replace `Selector.isBogus` with `SelectorList.isStandAlone`,
+  `SelectorList.isRelative`, `ComplexSelector.isStandAlone`, and
+  `ComplexSelector.isRelative`.
+
+* Replace `Selector.assertNotBogus()` with `SelectorList.assertValid()` and
+  `ComplexSelector.assertValid()`.
+
+* `Value.assertSelector()` and `Value.assertComplexSelector()` now forbid
+  selectors with leading or trailing combinators by default.
 
 ## 15.2.2-dev
 
diff --git a/test/deprecations_test.dart b/test/deprecations_test.dart
index 032260df9..d0f830ef2 100644
--- a/test/deprecations_test.dart
+++ b/test/deprecations_test.dart
@@ -54,21 +54,6 @@ void main() {
     _expectDeprecation(r"a {b: (4/2)}", Deprecation.slashDiv);
   });
 
-  // Deprecated in 1.54.0
-  group("bogusCombinators is violated by", () {
-    test("adjacent combinators", () {
-      _expectDeprecation("a > > a {b: c}", Deprecation.bogusCombinators);
-    });
-
-    test("leading combinators", () {
-      _expectDeprecation("a > {b: c}", Deprecation.bogusCombinators);
-    });
-
-    test("trailing combinators", () {
-      _expectDeprecation("> a {b: c}", Deprecation.bogusCombinators);
-    });
-  });
-
   // Deprecated in 1.55.0
   group("strictUnary is violated by", () {
     test("an ambiguous + operator", () {