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", () {