diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6ef13e6..e0e116bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Improve error messages for invalid CSS values passed to plain CSS functions. +* Improve error messages involving selectors. + ### Embedded Sass * Improve the performance of starting up a compilation. diff --git a/lib/src/ast/css/media_query.dart b/lib/src/ast/css/media_query.dart index 8a095622d..9f2d49dbc 100644 --- a/lib/src/ast/css/media_query.dart +++ b/lib/src/ast/css/media_query.dart @@ -2,6 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import '../../interpolation_map.dart'; import '../../logger.dart'; import '../../parse/media_query.dart'; import '../../utils.dart'; @@ -43,8 +44,10 @@ class CssMediaQuery { /// /// Throws a [SassFormatException] if parsing fails. static List parseList(String contents, - {Object? url, Logger? logger}) => - MediaQueryParser(contents, url: url, logger: logger).parse(); + {Object? url, Logger? logger, InterpolationMap? interpolationMap}) => + MediaQueryParser(contents, + url: url, logger: logger, interpolationMap: interpolationMap) + .parse(); /// Creates a media query specifies a type and, optionally, conditions. /// diff --git a/lib/src/ast/css/modifiable.dart b/lib/src/ast/css/modifiable.dart index 3d9ec990d..c2518811b 100644 --- a/lib/src/ast/css/modifiable.dart +++ b/lib/src/ast/css/modifiable.dart @@ -12,4 +12,3 @@ export 'modifiable/node.dart'; export 'modifiable/style_rule.dart'; export 'modifiable/stylesheet.dart'; export 'modifiable/supports_rule.dart'; -export 'modifiable/value.dart'; diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart index 41400be70..d182e917f 100644 --- a/lib/src/ast/css/modifiable/style_rule.dart +++ b/lib/src/ast/css/modifiable/style_rule.dart @@ -4,30 +4,35 @@ import 'package:source_span/source_span.dart'; +import '../../../util/box.dart'; import '../../../visitor/interface/modifiable_css.dart'; import '../../selector.dart'; import '../style_rule.dart'; import 'node.dart'; -import 'value.dart'; /// A modifiable version of [CssStyleRule] for use in the evaluation step. class ModifiableCssStyleRule extends ModifiableCssParentNode implements CssStyleRule { - final ModifiableCssValue selector; + SelectorList get selector => _selector.value; + + /// A reference to the modifiable selector list provided by the extension + /// store, which may update it over time as new extensions are applied. + final Box _selector; + final SelectorList originalSelector; final FileSpan span; /// Creates a new [ModifiableCssStyleRule]. /// - /// If [originalSelector] isn't passed, it defaults to [selector.value]. - ModifiableCssStyleRule(this.selector, this.span, + /// If [originalSelector] isn't passed, it defaults to [_selector.value]. + ModifiableCssStyleRule(this._selector, this.span, {SelectorList? originalSelector}) - : originalSelector = originalSelector ?? selector.value; + : originalSelector = originalSelector ?? _selector.value; T accept(ModifiableCssVisitor visitor) => visitor.visitCssStyleRule(this); ModifiableCssStyleRule copyWithoutChildren() => - ModifiableCssStyleRule(selector, span, + ModifiableCssStyleRule(_selector, span, originalSelector: originalSelector); } diff --git a/lib/src/ast/css/modifiable/value.dart b/lib/src/ast/css/modifiable/value.dart deleted file mode 100644 index 2f29676be..000000000 --- a/lib/src/ast/css/modifiable/value.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 Google Inc. Use of this source code is governed by an -// MIT-style license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import 'package:source_span/source_span.dart'; - -import '../value.dart'; - -/// A modifiable version of [CssValue] for use in the evaluation step. -class ModifiableCssValue implements CssValue { - T value; - final FileSpan span; - - ModifiableCssValue(this.value, this.span); - - String toString() => value.toString(); -} diff --git a/lib/src/ast/css/node.dart b/lib/src/ast/css/node.dart index 0ca31890c..0e37f5ced 100644 --- a/lib/src/ast/css/node.dart +++ b/lib/src/ast/css/node.dart @@ -82,7 +82,7 @@ class _IsInvisibleVisitor with EveryCssVisitor { bool visitCssStyleRule(CssStyleRule rule) => (includeBogus - ? rule.selector.value.isInvisible - : rule.selector.value.isInvisibleOtherThanBogusCombinators) || + ? rule.selector.isInvisible + : rule.selector.isInvisibleOtherThanBogusCombinators) || super.visitCssStyleRule(rule); } diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart index 2a902efc3..bf19eeaa7 100644 --- a/lib/src/ast/css/style_rule.dart +++ b/lib/src/ast/css/style_rule.dart @@ -5,7 +5,6 @@ import '../../visitor/interface/css.dart'; import '../selector.dart'; import 'node.dart'; -import 'value.dart'; /// A plain CSS style rule. /// @@ -14,7 +13,7 @@ import 'value.dart'; /// contain placeholder selectors. abstract class CssStyleRule extends CssParentNode { /// The selector for this rule. - CssValue get selector; + SelectorList get selector; /// The selector for this rule, before any extensions were applied. SelectorList get originalSelector; diff --git a/lib/src/ast/css/value.dart b/lib/src/ast/css/value.dart index ce8ee2689..c10d5e665 100644 --- a/lib/src/ast/css/value.dart +++ b/lib/src/ast/css/value.dart @@ -9,7 +9,7 @@ import '../node.dart'; /// A value in a plain CSS tree. /// /// This is used to associate a span with a value that doesn't otherwise track -/// its span. +/// its span. It has value equality semantics. class CssValue implements AstNode { /// The value. final T value; @@ -19,5 +19,10 @@ class CssValue implements AstNode { CssValue(this.value, this.span); + bool operator ==(Object other) => + other is CssValue && other.value == value; + + int get hashCode => value.hashCode; + String toString() => value.toString(); } diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart index c00665e4b..1e3328db4 100644 --- a/lib/src/ast/sass/at_root_query.dart +++ b/lib/src/ast/sass/at_root_query.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import 'package:collection/collection.dart'; import '../../exception.dart'; +import '../../interpolation_map.dart'; import '../../logger.dart'; import '../../parse/at_root_query.dart'; import '../css.dart'; @@ -53,8 +54,12 @@ class AtRootQuery { /// /// If passed, [url] is the name of the file from which [contents] comes. /// + /// If passed, [interpolationMap] maps the text of [contents] back to the + /// original location of the selector in the source file. + /// /// Throws a [SassFormatException] if parsing fails. - factory AtRootQuery.parse(String contents, {Object? url, Logger? logger}) => + factory AtRootQuery.parse(String contents, + {Object? url, Logger? logger, InterpolationMap? interpolationMap}) => AtRootQueryParser(contents, url: url, logger: logger).parse(); /// Returns whether [this] excludes [node]. diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart index 8b694e430..0af8ac0b3 100644 --- a/lib/src/ast/selector.dart +++ b/lib/src/ast/selector.dart @@ -3,12 +3,14 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.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'; @@ -38,7 +40,7 @@ export 'selector/universal.dart'; /// Selectors have structural equality semantics. /// /// {@category AST} -abstract class Selector { +abstract class Selector implements AstNode { /// Whether this selector, and complex selectors containing it, should not be /// emitted. /// @@ -76,10 +78,14 @@ abstract class Selector { @internal bool get isUseless => accept(const _IsUselessVisitor()); + 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 [SassScriptException] in Dart Sass 2.0.0. + /// throw a [SassException] in Dart Sass 2.0.0. void assertNotBogus({String? name}) { if (!isBogus) return; warn( diff --git a/lib/src/ast/selector/attribute.dart b/lib/src/ast/selector/attribute.dart index 0fbae6a29..3254ab20c 100644 --- a/lib/src/ast/selector/attribute.dart +++ b/lib/src/ast/selector/attribute.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -44,15 +45,17 @@ class AttributeSelector extends SimpleSelector { /// Creates an attribute selector that matches any element with a property of /// the given name. - AttributeSelector(this.name) + AttributeSelector(this.name, FileSpan span) : op = null, value = null, - modifier = null; + modifier = null, + super(span); /// Creates an attribute selector that matches an element with a property /// named [name], whose value matches [value] based on the semantics of [op]. - AttributeSelector.withOperator(this.name, this.op, this.value, - {this.modifier}); + AttributeSelector.withOperator(this.name, this.op, this.value, FileSpan span, + {this.modifier}) + : super(span); T accept(SelectorVisitor visitor) => visitor.visitAttributeSelector(this); diff --git a/lib/src/ast/selector/class.dart b/lib/src/ast/selector/class.dart index 513d46d4e..60124c5c4 100644 --- a/lib/src/ast/selector/class.dart +++ b/lib/src/ast/selector/class.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -18,7 +19,7 @@ class ClassSelector extends SimpleSelector { /// The class name this selects for. final String name; - ClassSelector(this.name); + ClassSelector(this.name, FileSpan span) : super(span); bool operator ==(Object other) => other is ClassSelector && other.name == name; @@ -27,7 +28,7 @@ class ClassSelector extends SimpleSelector { /// @nodoc @internal - ClassSelector addSuffix(String suffix) => ClassSelector(name + suffix); + ClassSelector addSuffix(String suffix) => ClassSelector(name + suffix, span); int get hashCode => name.hashCode; } diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart index e5eb6cd25..708785fe3 100644 --- a/lib/src/ast/selector/complex.dart +++ b/lib/src/ast/selector/complex.dart @@ -3,12 +3,14 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../logger.dart'; import '../../parse/selector.dart'; import '../../utils.dart'; import '../../visitor/interface/selector.dart'; +import '../css/value.dart'; import '../selector.dart'; /// A complex selector. @@ -25,7 +27,7 @@ class ComplexSelector extends Selector { /// 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 leadingCombinators; + final List> leadingCombinators; /// The components of this selector. /// @@ -66,11 +68,12 @@ class ComplexSelector extends Selector { ? components.first.selector : null; - ComplexSelector(Iterable leadingCombinators, - Iterable components, + ComplexSelector(Iterable> leadingCombinators, + Iterable components, FileSpan span, {this.lineBreak = false}) : leadingCombinators = List.unmodifiable(leadingCombinators), - components = List.unmodifiable(components) { + components = List.unmodifiable(components), + super(span) { if (this.leadingCombinators.isEmpty && this.components.isEmpty) { throw ArgumentError( "leadingCombinators and components may not both be empty."); @@ -109,12 +112,14 @@ class ComplexSelector extends Selector { /// /// @nodoc @internal - ComplexSelector withAdditionalCombinators(List combinators, + ComplexSelector withAdditionalCombinators( + List> combinators, {bool forceLineBreak = false}) { if (combinators.isEmpty) { return this; } else if (components.isEmpty) { - return ComplexSelector([...leadingCombinators, ...combinators], const [], + return ComplexSelector( + [...leadingCombinators, ...combinators], const [], span, lineBreak: lineBreak || forceLineBreak); } else { return ComplexSelector( @@ -123,6 +128,7 @@ class ComplexSelector extends Selector { ...components.exceptLast, components.last.withAdditionalCombinators(combinators) ], + span, lineBreak: lineBreak || forceLineBreak); } } @@ -132,11 +138,14 @@ class ComplexSelector extends Selector { /// If [forceLineBreak] is `true`, this will mark the new complex selector as /// having a line break. /// + /// The [span] is used for the new selector. + /// /// @nodoc @internal - ComplexSelector withAdditionalComponent(ComplexSelectorComponent component, + ComplexSelector withAdditionalComponent( + ComplexSelectorComponent component, FileSpan span, {bool forceLineBreak = false}) => - ComplexSelector(leadingCombinators, [...components, component], + ComplexSelector(leadingCombinators, [...components, component], span, lineBreak: lineBreak || forceLineBreak); /// Returns a copy of `this` with [child]'s combinators added to the end. @@ -144,21 +153,24 @@ class ComplexSelector extends Selector { /// If [child] has [leadingCombinators], they're appended to `this`'s last /// combinator. This does _not_ resolve parent selectors. /// + /// The [span] is used for the new selector. + /// /// If [forceLineBreak] is `true`, this will mark the new complex selector as /// having a line break. /// /// @nodoc @internal - ComplexSelector concatenate(ComplexSelector child, + ComplexSelector concatenate(ComplexSelector child, FileSpan span, {bool forceLineBreak = false}) { if (child.leadingCombinators.isEmpty) { return ComplexSelector( - leadingCombinators, [...components, ...child.components], + leadingCombinators, [...components, ...child.components], span, lineBreak: lineBreak || child.lineBreak || forceLineBreak); } else if (components.isEmpty) { return ComplexSelector( [...leadingCombinators, ...child.leadingCombinators], child.components, + span, lineBreak: lineBreak || child.lineBreak || forceLineBreak); } else { return ComplexSelector( @@ -168,6 +180,7 @@ class ComplexSelector extends Selector { components.last.withAdditionalCombinators(child.leadingCombinators), ...child.components ], + span, lineBreak: lineBreak || child.lineBreak || forceLineBreak); } } diff --git a/lib/src/ast/selector/complex_component.dart b/lib/src/ast/selector/complex_component.dart index 61bdd9330..a3142f6eb 100644 --- a/lib/src/ast/selector/complex_component.dart +++ b/lib/src/ast/selector/complex_component.dart @@ -3,8 +3,10 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../utils.dart'; +import '../css/value.dart'; import '../selector.dart'; /// A component of a [ComplexSelector]. @@ -22,9 +24,12 @@ class ComplexSelectorComponent { /// 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 combinators; + final List> combinators; - ComplexSelectorComponent(this.selector, Iterable combinators) + final FileSpan span; + + ComplexSelectorComponent( + this.selector, Iterable> combinators, this.span) : combinators = List.unmodifiable(combinators); /// Returns a copy of `this` with [combinators] added to the end of @@ -33,11 +38,11 @@ class ComplexSelectorComponent { /// @nodoc @internal ComplexSelectorComponent withAdditionalCombinators( - List combinators) => + List> combinators) => combinators.isEmpty ? this : ComplexSelectorComponent( - selector, [...this.combinators, ...combinators]); + selector, [...this.combinators, ...combinators], span); int get hashCode => selector.hashCode ^ listHash(combinators); diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index 1c3905154..19e5d7ade 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../logger.dart'; @@ -43,8 +44,9 @@ class CompoundSelector extends Selector { SimpleSelector? get singleSimple => components.length == 1 ? components.first : null; - CompoundSelector(Iterable components) - : components = List.unmodifiable(components) { + CompoundSelector(Iterable components, FileSpan span) + : components = List.unmodifiable(components), + super(span) { if (this.components.isEmpty) { throw ArgumentError("components may not be empty."); } diff --git a/lib/src/ast/selector/id.dart b/lib/src/ast/selector/id.dart index 010bd2161..9d9442c71 100644 --- a/lib/src/ast/selector/id.dart +++ b/lib/src/ast/selector/id.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -21,13 +22,13 @@ class IDSelector extends SimpleSelector { int get specificity => math.pow(super.specificity, 2) as int; - IDSelector(this.name); + IDSelector(this.name, FileSpan span) : super(span); T accept(SelectorVisitor visitor) => visitor.visitIDSelector(this); /// @nodoc @internal - IDSelector addSuffix(String suffix) => IDSelector(name + suffix); + IDSelector addSuffix(String suffix) => IDSelector(name + suffix, span); /// @nodoc @internal diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index f87a52daa..20f2b77b5 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -3,14 +3,19 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; +import '../../exception.dart'; import '../../extend/functions.dart'; +import '../../interpolation_map.dart'; import '../../logger.dart'; import '../../parse/selector.dart'; import '../../utils.dart'; -import '../../exception.dart'; +import '../../util/span.dart'; import '../../value.dart'; import '../../visitor/interface/selector.dart'; +import '../../visitor/selector_search.dart'; +import '../css/value.dart'; import '../selector.dart'; /// A selector list. @@ -27,10 +32,6 @@ class SelectorList extends Selector { /// This is never empty. final List components; - /// Whether this contains a [ParentSelector]. - bool get _containsParentSelector => - components.any(_complexContainsParentSelector); - /// Returns a SassScript list that represents this selector. /// /// This has the same format as a list returned by `selector-parse()`. @@ -48,8 +49,9 @@ class SelectorList extends Selector { }), ListSeparator.comma); } - SelectorList(Iterable components) - : components = List.unmodifiable(components) { + SelectorList(Iterable components, FileSpan span) + : components = List.unmodifiable(components), + super(span) { if (this.components.isEmpty) { throw ArgumentError("components may not be empty."); } @@ -61,15 +63,20 @@ class SelectorList extends Selector { /// [allowParent] and [allowPlaceholder] control whether [ParentSelector]s or /// [PlaceholderSelector]s are allowed in this selector, respectively. /// + /// If passed, [interpolationMap] maps the text of [contents] back to the + /// original location of the selector in the source file. + /// /// Throws a [SassFormatException] if parsing fails. factory SelectorList.parse(String contents, {Object? url, Logger? logger, + InterpolationMap? interpolationMap, bool allowParent = true, bool allowPlaceholder = true}) => SelectorParser(contents, url: url, logger: logger, + interpolationMap: interpolationMap, allowParent: allowParent, allowPlaceholder: allowPlaceholder) .parse(); @@ -84,10 +91,10 @@ class SelectorList extends Selector { var contents = [ for (var complex1 in components) for (var complex2 in other.components) - ...?unifyComplex([complex1, complex2]) + ...?unifyComplex([complex1, complex2], complex1.span) ]; - return contents.isEmpty ? null : SelectorList(contents); + return contents.isEmpty ? null : SelectorList(contents, span); } /// Returns a new list with all [ParentSelector]s replaced with [parent]. @@ -101,16 +108,18 @@ class SelectorList extends Selector { SelectorList resolveParentSelectors(SelectorList? parent, {bool implicitParent = true}) { if (parent == null) { - if (!_containsParentSelector) return this; - throw SassScriptException( - 'Top-level selectors may not contain the parent selector "&".'); + var parentSelector = accept(const _ParentSelectorVisitor()); + if (parentSelector == null) return this; + throw SassException( + 'Top-level selectors may not contain the parent selector "&".', + parentSelector.span); } return SelectorList(flattenVertically(components.map((complex) { - if (!_complexContainsParentSelector(complex)) { + if (!_containsParentSelector(complex)) { if (!implicitParent) return [complex]; - return parent.components - .map((parentComplex) => parentComplex.concatenate(complex)); + return parent.components.map((parentComplex) => + parentComplex.concatenate(complex, complex.span)); } var newComplexes = []; @@ -119,12 +128,12 @@ class SelectorList extends Selector { if (resolved == null) { if (newComplexes.isEmpty) { newComplexes.add(ComplexSelector( - complex.leadingCombinators, [component], + complex.leadingCombinators, [component], complex.span, lineBreak: false)); } else { for (var i = 0; i < newComplexes.length; i++) { - newComplexes[i] = - newComplexes[i].withAdditionalComponent(component); + newComplexes[i] = newComplexes[i] + .withAdditionalComponent(component, complex.span); } } } else if (newComplexes.isEmpty) { @@ -134,25 +143,15 @@ class SelectorList extends Selector { newComplexes = [ for (var newComplex in previousComplexes) for (var resolvedComplex in resolved) - newComplex.concatenate(resolvedComplex) + newComplex.concatenate(resolvedComplex, newComplex.span) ]; } } return newComplexes; - }))); + })), span); } - /// Returns whether [complex] contains a [ParentSelector]. - bool _complexContainsParentSelector(ComplexSelector complex) => - complex.components - .any((component) => component.selector.components.any((simple) { - if (simple is ParentSelector) return true; - if (simple is! PseudoSelector) return false; - var selector = simple.selector; - return selector != null && selector._containsParentSelector; - })); - /// Returns a new selector list based on [component] with all /// [ParentSelector]s replaced with [parent]. /// @@ -163,7 +162,7 @@ class SelectorList extends Selector { var containsSelectorPseudo = simples.any((simple) { if (simple is! PseudoSelector) return false; var selector = simple.selector; - return selector != null && selector._containsParentSelector; + return selector != null && _containsParentSelector(selector); }); if (!containsSelectorPseudo && simples.first is! ParentSelector) { return null; @@ -174,48 +173,72 @@ class SelectorList extends Selector { if (simple is! PseudoSelector) return simple; var selector = simple.selector; if (selector == null) return simple; - if (!selector._containsParentSelector) return simple; + if (!_containsParentSelector(selector)) return simple; return simple.withSelector( selector.resolveParentSelectors(parent, implicitParent: false)); }) : simples; var parentSelector = simples.first; - if (parentSelector is! ParentSelector) { - return [ - ComplexSelector(const [], [ - ComplexSelectorComponent( - CompoundSelector(resolvedSimples), component.combinators) - ]) - ]; - } else if (simples.length == 1 && parentSelector.suffix == null) { - return parent.withAdditionalCombinators(component.combinators).components; + 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"), + stackTrace); } return parent.components.map((complex) { - var lastComponent = complex.components.last; - if (lastComponent.combinators.isNotEmpty) { - throw SassScriptException( - 'Parent "$complex" is incompatible with this selector.'); - } + 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 suffix = parentSelector.suffix; + var lastSimples = lastComponent.selector.components; + var last = CompoundSelector( + suffix == null + ? [...lastSimples, ...resolvedSimples.skip(1)] + : [ + ...lastSimples.exceptLast, + lastSimples.last.addSuffix(suffix), + ...resolvedSimples.skip(1) + ], + component.selector.span); - var suffix = parentSelector.suffix; - var lastSimples = lastComponent.selector.components; - var last = CompoundSelector(suffix == null - ? [...lastSimples, ...resolvedSimples.skip(1)] - : [ - ...lastSimples.exceptLast, - lastSimples.last.addSuffix(suffix), - ...resolvedSimples.skip(1) - ]); - - return ComplexSelector( - complex.leadingCombinators, - [ - ...complex.components.exceptLast, - ComplexSelectorComponent(last, component.combinators) - ], - lineBreak: complex.lineBreak); + return ComplexSelector( + complex.leadingCombinators, + [ + ...complex.components.exceptLast, + ComplexSelectorComponent( + last, component.combinators, component.span) + ], + component.span, + lineBreak: complex.lineBreak); + } on SassException catch (error, stackTrace) { + throwWithTrace( + error.withAdditionalSpan(parentSelector.span, "parent selector"), + stackTrace); + } }); } @@ -229,14 +252,28 @@ class SelectorList extends Selector { /// Returns a copy of `this` with [combinators] added to the end of each /// complex selector in [components]. @internal - SelectorList withAdditionalCombinators(List combinators) => + SelectorList withAdditionalCombinators( + List> combinators) => combinators.isEmpty ? this - : SelectorList(components.map( - (complex) => complex.withAdditionalCombinators(combinators))); + : SelectorList( + components.map( + (complex) => complex.withAdditionalCombinators(combinators)), + span); int get hashCode => listHash(components); bool operator ==(Object other) => other is SelectorList && listEquals(components, other.components); } + +/// Returns whether [selector] recursively contains a parent selector. +bool _containsParentSelector(Selector selector) => + selector.accept(const _ParentSelectorVisitor()) != null; + +/// A visitor for finding the first [ParentSelector] in a given selector. +class _ParentSelectorVisitor with SelectorSearchVisitor { + const _ParentSelectorVisitor(); + + ParentSelector visitParentSelector(ParentSelector selector) => selector; +} diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart index 461d5e480..1cde57f9d 100644 --- a/lib/src/ast/selector/parent.dart +++ b/lib/src/ast/selector/parent.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -22,7 +23,7 @@ class ParentSelector extends SimpleSelector { /// indicating that the parent selector will not be modified. final String? suffix; - ParentSelector({this.suffix}); + ParentSelector(FileSpan span, {this.suffix}) : super(span); T accept(SelectorVisitor visitor) => visitor.visitParentSelector(this); diff --git a/lib/src/ast/selector/placeholder.dart b/lib/src/ast/selector/placeholder.dart index a7b935322..97ef14e4f 100644 --- a/lib/src/ast/selector/placeholder.dart +++ b/lib/src/ast/selector/placeholder.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../util/character.dart' as character; import '../../visitor/interface/selector.dart'; @@ -24,7 +25,7 @@ class PlaceholderSelector extends SimpleSelector { /// with `-` or `_`). bool get isPrivate => character.isPrivate(name); - PlaceholderSelector(this.name); + PlaceholderSelector(this.name, FileSpan span) : super(span); T accept(SelectorVisitor visitor) => visitor.visitPlaceholderSelector(this); @@ -32,7 +33,7 @@ class PlaceholderSelector extends SimpleSelector { /// @nodoc @internal PlaceholderSelector addSuffix(String suffix) => - PlaceholderSelector(name + suffix); + PlaceholderSelector(name + suffix, span); bool operator ==(Object other) => other is PlaceholderSelector && other.name == name; diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index 7840eccab..2b3078b24 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -5,6 +5,7 @@ import 'package:charcode/charcode.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../utils.dart'; import '../../util/nullable.dart'; @@ -104,11 +105,12 @@ class PseudoSelector extends SimpleSelector { } }(); - PseudoSelector(this.name, + PseudoSelector(this.name, FileSpan span, {bool element = false, this.argument, this.selector}) : isClass = !element && !_isFakePseudoElement(name), isSyntacticClass = !element, - normalizedName = unvendor(name); + normalizedName = unvendor(name), + super(span); /// Returns whether [name] is the name of a pseudo-element that can be written /// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or @@ -135,14 +137,15 @@ class PseudoSelector extends SimpleSelector { /// Returns a new [PseudoSelector] based on this, but with the selector /// replaced with [selector]. - PseudoSelector withSelector(SelectorList selector) => PseudoSelector(name, - element: isElement, argument: argument, selector: selector); + PseudoSelector withSelector(SelectorList selector) => + PseudoSelector(name, span, + element: isElement, argument: argument, selector: selector); /// @nodoc @internal PseudoSelector addSuffix(String suffix) { if (argument != null || selector != null) super.addSuffix(suffix); - return PseudoSelector(name + suffix, element: isElement); + return PseudoSelector(name + suffix, span, element: isElement); } /// @nodoc @@ -200,7 +203,8 @@ class PseudoSelector extends SimpleSelector { // Fall back to the logic defined in functions.dart, which knows how to // compare selector pseudoclasses against raw selectors. - return CompoundSelector([this]).isSuperselector(CompoundSelector([other])); + return CompoundSelector([this], span) + .isSuperselector(CompoundSelector([other], span)); } T accept(SelectorVisitor visitor) => visitor.visitPseudoSelector(this); diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index 599b43c8e..d2da89fdc 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../exception.dart'; import '../../logger.dart'; @@ -34,7 +35,7 @@ abstract class SimpleSelector extends Selector { /// sequence will contain 1000 simple selectors. int get specificity => 1000; - SimpleSelector(); + SimpleSelector(FileSpan span) : super(span); /// Parses a simple selector from [contents]. /// @@ -57,8 +58,8 @@ abstract class SimpleSelector extends Selector { /// /// @nodoc @internal - SimpleSelector addSuffix(String suffix) => - throw SassScriptException('Invalid parent selector "$this"'); + SimpleSelector addSuffix(String suffix) => throw MultiSpanSassException( + 'Selector "$this" can\'t have a suffix', span, "outer selector", {}); /// Returns the components of a [CompoundSelector] that matches only elements /// matched by both this and [compound]. diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart index 0430de768..b021a6383 100644 --- a/lib/src/ast/selector/type.dart +++ b/lib/src/ast/selector/type.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../visitor/interface/selector.dart'; @@ -20,14 +21,14 @@ class TypeSelector extends SimpleSelector { int get specificity => 1; - TypeSelector(this.name); + TypeSelector(this.name, FileSpan span) : super(span); T accept(SelectorVisitor visitor) => visitor.visitTypeSelector(this); /// @nodoc @internal TypeSelector addSuffix(String suffix) => TypeSelector( - QualifiedName(name.name + suffix, namespace: name.namespace)); + QualifiedName(name.name + suffix, namespace: name.namespace), span); /// @nodoc @internal diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index 2937a78f6..d3d8fa2a5 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../visitor/interface/selector.dart'; @@ -23,7 +24,7 @@ class UniversalSelector extends SimpleSelector { int get specificity => 0; - UniversalSelector({this.namespace}); + UniversalSelector(FileSpan span, {this.namespace}) : super(span); T accept(SelectorVisitor visitor) => visitor.visitUniversalSelector(this); diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 0c4c20701..388ae9054 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -26,6 +26,22 @@ class SassException extends SourceSpanException { SassException(String message, FileSpan span) : super(message, span); + /// Converts this to a [MultiSpanSassException] with the additional [span] and + /// [label]. + /// + /// @nodoc + @internal + MultiSpanSassException withAdditionalSpan(FileSpan span, String label) => + MultiSpanSassException(message, this.span, "", {span: label}); + + /// Returns a copy of this as a [SassRuntimeException] with [trace] as its + /// Sass stack trace. + /// + /// @nodoc + @internal + SassRuntimeException withTrace(Trace trace) => + SassRuntimeException(message, span, trace); + String toString({Object? color}) { var buffer = StringBuffer() ..writeln("Error: $message") @@ -97,6 +113,14 @@ class MultiSpanSassException extends SassException : secondarySpans = Map.unmodifiable(secondarySpans), super(message, span); + MultiSpanSassException withAdditionalSpan(FileSpan span, String label) => + MultiSpanSassException( + message, this.span, primaryLabel, {...secondarySpans, span: label}); + + MultiSpanSassRuntimeException withTrace(Trace trace) => + MultiSpanSassRuntimeException( + message, span, primaryLabel, secondarySpans, trace); + String toString({Object? color, String? secondaryColor}) { var useColor = false; String? primaryColor; @@ -129,6 +153,11 @@ class MultiSpanSassException extends SassException class SassRuntimeException extends SassException { final Trace trace; + MultiSpanSassRuntimeException withAdditionalSpan( + FileSpan span, String label) => + MultiSpanSassRuntimeException( + message, this.span, "", {span: label}, trace); + SassRuntimeException(String message, FileSpan span, this.trace) : super(message, span); } @@ -141,6 +170,11 @@ class MultiSpanSassRuntimeException extends MultiSpanSassException MultiSpanSassRuntimeException(String message, FileSpan span, String primaryLabel, Map secondarySpans, this.trace) : super(message, span, primaryLabel, secondarySpans); + + MultiSpanSassRuntimeException withAdditionalSpan( + FileSpan span, String label) => + MultiSpanSassRuntimeException(message, this.span, primaryLabel, + {...secondarySpans, span: label}, trace); } /// An exception thrown when Sass parsing has failed. @@ -153,9 +187,35 @@ class SassFormatException extends SassException int get offset => span.start.offset; + /// @nodoc + @internal + MultiSpanSassFormatException withAdditionalSpan( + FileSpan span, String label) => + MultiSpanSassFormatException(message, this.span, "", {span: label}); + SassFormatException(String message, FileSpan span) : super(message, span); } +/// A [SassFormatException] that's also a [MultiSpanFormatException]. +/// +/// {@category Parsing} +@sealed +class MultiSpanSassFormatException extends MultiSpanSassException + implements MultiSourceSpanFormatException, SassFormatException { + String get source => span.file.getText(0); + + int get offset => span.start.offset; + + MultiSpanSassFormatException withAdditionalSpan( + FileSpan span, String label) => + MultiSpanSassFormatException( + message, this.span, primaryLabel, {...secondarySpans, span: label}); + + MultiSpanSassFormatException(String message, FileSpan span, + String primaryLabel, Map secondarySpans) + : super(message, span, primaryLabel, secondarySpans); +} + /// An exception thrown by SassScript. /// /// This doesn't extends [SassException] because it doesn't (yet) have a @@ -173,6 +233,9 @@ class SassScriptException { SassScriptException(String message, [String? argumentName]) : message = argumentName == null ? message : "\$$argumentName: $message"; + /// Converts this to a [SassException] with the given [span]. + SassException withSpan(FileSpan span) => SassException(message, span); + String toString() => "$message\n\nBUG: This should include a source span!"; } @@ -189,4 +252,8 @@ class MultiSpanSassScriptException extends SassScriptException { String message, this.primaryLabel, Map secondarySpans) : secondarySpans = Map.unmodifiable(secondarySpans), super(message); + + /// Converts this to a [SassException] with the given primary [span]. + MultiSpanSassException withSpan(FileSpan span) => + MultiSpanSassException(message, span, primaryLabel, secondarySpans); } diff --git a/lib/src/extend/empty_extension_store.dart b/lib/src/extend/empty_extension_store.dart index 4bb9e7a29..46aef4d93 100644 --- a/lib/src/extend/empty_extension_store.dart +++ b/lib/src/extend/empty_extension_store.dart @@ -3,16 +3,17 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; -import 'package:source_span/source_span.dart'; import 'package:tuple/tuple.dart'; import '../ast/css.dart'; -import '../ast/css/modifiable.dart'; import '../ast/selector.dart'; import '../ast/sass.dart'; +import '../util/box.dart'; import 'extension_store.dart'; import 'extension.dart'; +/// An [ExtensionStore] that contains no extensions and can have no extensions +/// added. class EmptyExtensionStore implements ExtensionStore { bool get isEmpty => true; @@ -24,15 +25,14 @@ class EmptyExtensionStore implements ExtensionStore { bool callback(SimpleSelector target)) => const []; - ModifiableCssValue addSelector( - SelectorList selector, FileSpan span, + Box addSelector(SelectorList selector, [List? mediaContext]) { throw UnsupportedError( "addSelector() can't be called for a const ExtensionStore."); } void addExtension( - CssValue extender, SimpleSelector target, ExtendRule extend, + SelectorList extender, SimpleSelector target, ExtendRule extend, [List? mediaContext]) { throw UnsupportedError( "addExtension() can't be called for a const ExtensionStore."); @@ -43,7 +43,6 @@ class EmptyExtensionStore implements ExtensionStore { "addExtensions() can't be called for a const ExtensionStore."); } - Tuple2, ModifiableCssValue>> - clone() => const Tuple2(EmptyExtensionStore(), {}); + Tuple2>> clone() => + const Tuple2(EmptyExtensionStore(), {}); } diff --git a/lib/src/extend/extender.dart b/lib/src/extend/extender.dart deleted file mode 100644 index 441ca5fd7..000000000 --- a/lib/src/extend/extender.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2021 Google Inc. Use of this source code is governed by an -// MIT-style license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import 'package:source_span/source_span.dart'; - -import '../ast/css.dart'; -import '../ast/selector.dart'; -import '../exception.dart'; -import '../utils.dart'; - -/// A selector that's extending another selector, such as `A` in `A {@extend -/// B}`. -class Extender { - /// The selector in which the `@extend` appeared. - final ComplexSelector selector; - - /// The minimum specificity required for any selector generated from this - /// extender. - final int specificity; - - /// Whether this extender represents a selector that was originally in the - /// document, rather than one defined with `@extend`. - final bool isOriginal; - - /// The media query context to which this extension is restricted, or `null` - /// if it can apply within any context. - final List? mediaContext; - - /// The span in which this selector was defined. - final FileSpan span; - - /// Creates a new extender. - /// - /// If [specificity] isn't passed, it defaults to `extender.specificity`. - Extender(this.selector, this.span, - {this.mediaContext, int? specificity, bool original = false}) - : specificity = specificity ?? selector.specificity, - isOriginal = original; - - /// Asserts that the [mediaContext] for a selector is compatible with the - /// query context for this extender. - void assertCompatibleMediaContext(List? mediaContext) { - if (this.mediaContext == null) return; - if (mediaContext != null && listEquals(this.mediaContext, mediaContext)) { - return; - } - - throw SassException( - "You may not @extend selectors across media queries.", span); - } - - Extender withSelector(ComplexSelector newSelector) => - Extender(newSelector, span, - mediaContext: mediaContext, - specificity: specificity, - original: isOriginal); - - String toString() => selector.toString(); -} diff --git a/lib/src/extend/extension.dart b/lib/src/extend/extension.dart index 96a901e1f..3323dabf1 100644 --- a/lib/src/extend/extension.dart +++ b/lib/src/extend/extension.dart @@ -34,16 +34,15 @@ class Extension { final FileSpan span; /// Creates a new extension. - Extension( - ComplexSelector extender, FileSpan extenderSpan, this.target, this.span, + Extension(ComplexSelector extender, this.target, this.span, {this.mediaContext, bool optional = false}) - : extender = Extender(extender, extenderSpan), + : extender = Extender(extender), isOptional = optional { this.extender._extension = this; } Extension withExtender(ComplexSelector newExtender) => - Extension(newExtender, extender.span, target, span, + Extension(newExtender, target, span, mediaContext: mediaContext, optional: isOptional); String toString() => @@ -70,13 +69,10 @@ class Extender { /// original selectors that exist in the document. Extension? _extension; - /// The span in which this selector was defined. - final FileSpan span; - /// Creates a new extender. /// /// If [specificity] isn't passed, it defaults to `extender.specificity`. - Extender(this.selector, this.span, {int? specificity, bool original = false}) + Extender(this.selector, {int? specificity, bool original = false}) : specificity = specificity ?? selector.specificity, isOriginal = original; diff --git a/lib/src/extend/extension_store.dart b/lib/src/extend/extension_store.dart index cfeae78a4..24bdc43ad 100644 --- a/lib/src/extend/extension_store.dart +++ b/lib/src/extend/extension_store.dart @@ -9,11 +9,11 @@ import 'package:source_span/source_span.dart'; import 'package:tuple/tuple.dart'; import '../ast/css.dart'; -import '../ast/css/modifiable.dart'; import '../ast/selector.dart'; import '../ast/sass.dart'; import '../exception.dart'; import '../utils.dart'; +import '../util/box.dart'; import '../util/nullable.dart'; import 'empty_extension_store.dart'; import 'extension.dart'; @@ -23,7 +23,8 @@ import 'mode.dart'; /// Tracks selectors and extensions, and applies the latter to the former. class ExtensionStore { - /// An [ExtensionStore] that contains no extensions and can have no extensions added. + /// An [ExtensionStore] that contains no extensions and can have no extensions + /// added. static const empty = EmptyExtensionStore(); /// A map from all simple selectors in the stylesheet to the selector lists @@ -31,7 +32,7 @@ class ExtensionStore { /// /// This is used to find which selectors an `@extend` applies to and adjust /// them. - final Map>> _selectors; + final Map>> _selectors; /// A map from all extended simple selectors to the sources of those /// extensions. @@ -45,8 +46,7 @@ class ExtensionStore { /// /// This tracks the contexts in which each selector's style rule is defined. /// If a rule is defined at the top level, it doesn't have an entry. - final Map, List> - _mediaContexts; + final Map, List> _mediaContexts; /// A map from [SimpleSelector]s to the specificity of their source /// selectors. @@ -106,11 +106,11 @@ class ExtensionStore { throw SassScriptException("Can't extend complex selector $complex."); } - selector = extender._extendList(selector, span, { + selector = extender._extendList(selector, { for (var simple in compound.components) simple: { for (var complex in source.components) - complex: Extension(complex, span, simple, span, optional: true) + complex: Extension(complex, simple, span, optional: true) } }); } @@ -166,15 +166,13 @@ class ExtensionStore { /// Adds [selector] to this extender. /// - /// Extends [selector] using any registered extensions, then returns an empty - /// [ModifiableCssValue] containing the resulting selector. If any more - /// relevant extensions are added, the returned selector is automatically - /// updated. + /// Extends [selector] using any registered extensions, then returns a [Box] + /// containing the resulting selector. If any more relevant extensions are + /// added, the returned selector is automatically updated. /// /// The [mediaContext] is the media query context in which the selector was /// defined, or `null` if it was defined at the top level of the document. - ModifiableCssValue addSelector( - SelectorList selector, FileSpan selectorSpan, + Box addSelector(SelectorList selector, [List? mediaContext]) { var originalSelector = selector; if (!originalSelector.isInvisible) { @@ -185,8 +183,7 @@ class ExtensionStore { if (_extensions.isNotEmpty) { try { - selector = _extendList( - originalSelector, selectorSpan, _extensions, mediaContext); + selector = _extendList(originalSelector, _extensions, mediaContext); } on SassException catch (error, stackTrace) { throwWithTrace( SassException( @@ -197,17 +194,17 @@ class ExtensionStore { } } - var modifiableSelector = ModifiableCssValue(selector, selectorSpan); + var modifiableSelector = ModifiableBox(selector); if (mediaContext != null) _mediaContexts[modifiableSelector] = mediaContext; _registerSelector(selector, modifiableSelector); - return modifiableSelector; + return modifiableSelector.seal(); } /// Registers the [SimpleSelector]s in [list] to point to [selector] in /// [_selectors]. void _registerSelector( - SelectorList list, ModifiableCssValue selector) { + SelectorList list, ModifiableBox selector) { for (var complex in list.components) { for (var component in complex.components) { for (var simple in component.selector.components) { @@ -233,17 +230,17 @@ class ExtensionStore { /// is defined. It can only extend selectors within the same context. A `null` /// context indicates no media queries. void addExtension( - CssValue extender, SimpleSelector target, ExtendRule extend, + SelectorList extender, SimpleSelector target, ExtendRule extend, [List? mediaContext]) { var selectors = _selectors[target]; var existingExtensions = _extensionsByExtender[target]; Map? newExtensions; var sources = _extensions.putIfAbsent(target, () => {}); - for (var complex in extender.value.components) { + for (var complex in extender.components) { if (complex.isUseless) continue; - var extension = Extension(complex, extender.span, target, extend.span, + var extension = Extension(complex, target, extend.span, mediaContext: mediaContext, optional: extend.isOptional); var existingExtension = sources[complex]; @@ -327,15 +324,13 @@ class ExtensionStore { List? selectors; try { - selectors = _extendComplex(extension.extender.selector, - extension.extender.span, newExtensions, extension.mediaContext); + selectors = _extendComplex( + extension.extender.selector, newExtensions, extension.mediaContext); if (selectors == null) continue; } on SassException catch (error, stackTrace) { throwWithTrace( - SassException( - "From ${extension.extender.span.message('')}\n" - "${error.message}", - error.span), + error.withAdditionalSpan( + extension.extender.selector.span, "target selector"), stackTrace); } @@ -384,18 +379,18 @@ class ExtensionStore { } /// Extend [extensions] using [newExtensions]. - void _extendExistingSelectors(Set> selectors, + void _extendExistingSelectors(Set> selectors, Map> newExtensions) { for (var selector in selectors) { var oldValue = selector.value; try { - selector.value = _extendList(selector.value, selector.span, - newExtensions, _mediaContexts[selector]); + selector.value = _extendList( + selector.value, newExtensions, _mediaContexts[selector]); } on SassException catch (error, stackTrace) { // TODO(nweiz): Make this a MultiSpanSassException. throwWithTrace( SassException( - "From ${selector.span.message('')}\n" + "From ${selector.value.span.message('')}\n" "${error.message}", error.span), stackTrace); @@ -419,7 +414,7 @@ class ExtensionStore { // Selectors that contain simple selectors that are extended by // [extensions], and thus which need to be extended themselves. - Set>? selectorsToExtend; + Set>? selectorsToExtend; // An extension map with the same structure as [_extensions] that only // includes extensions from [extensionStores]. @@ -478,7 +473,7 @@ class ExtensionStore { } /// Extends [list] using [extensions]. - SelectorList _extendList(SelectorList list, FileSpan listSpan, + SelectorList _extendList(SelectorList list, Map> extensions, [List? mediaQueryContext]) { // This could be written more simply using [List.map], but we want to avoid @@ -486,8 +481,7 @@ class ExtensionStore { List? extended; for (var i = 0; i < list.components.length; i++) { var complex = list.components[i]; - var result = - _extendComplex(complex, listSpan, extensions, mediaQueryContext); + var result = _extendComplex(complex, extensions, mediaQueryContext); assert( result?.isNotEmpty ?? true, '_extendComplex($complex) should return null rather than [] if ' @@ -501,14 +495,13 @@ class ExtensionStore { } if (extended == null) return list; - return SelectorList(_trim(extended, _originals.contains)); + return SelectorList(_trim(extended, _originals.contains), list.span); } /// Extends [complex] using [extensions], and returns the contents of a /// [SelectorList]. List? _extendComplex( ComplexSelector complex, - FileSpan complexSpan, Map> extensions, List? mediaQueryContext) { if (complex.leadingCombinators.length > 1) return null; @@ -534,8 +527,7 @@ class ExtensionStore { var isOriginal = _originals.contains(complex); for (var i = 0; i < complex.components.length; i++) { var component = complex.components[i]; - var extended = _extendCompound( - component, complexSpan, extensions, mediaQueryContext, + var extended = _extendCompound(component, extensions, mediaQueryContext, inOriginal: isOriginal); assert( extended?.isNotEmpty ?? true, @@ -543,15 +535,16 @@ class ExtensionStore { 'extension fails'); if (extended == null) { extendedNotExpanded?.add([ - ComplexSelector(const [], [component], lineBreak: complex.lineBreak) + ComplexSelector(const [], [component], complex.span, + lineBreak: complex.lineBreak) ]); } else if (extendedNotExpanded != null) { extendedNotExpanded.add(extended); } else if (i != 0) { extendedNotExpanded = [ [ - ComplexSelector( - complex.leadingCombinators, complex.components.take(i), + ComplexSelector(complex.leadingCombinators, + complex.components.take(i), complex.span, lineBreak: complex.lineBreak) ], extended @@ -565,8 +558,8 @@ class ExtensionStore { if (newComplex.leadingCombinators.isEmpty || listEquals(complex.leadingCombinators, newComplex.leadingCombinators)) - ComplexSelector( - complex.leadingCombinators, newComplex.components, + ComplexSelector(complex.leadingCombinators, + newComplex.components, complex.span, lineBreak: complex.lineBreak || newComplex.lineBreak) ] ]; @@ -576,7 +569,7 @@ class ExtensionStore { var first = true; return paths(extendedNotExpanded).expand((path) { - return weave(path, forceLineBreak: complex.lineBreak) + return weave(path, complex.span, forceLineBreak: complex.lineBreak) .map((outputComplex) { // Make sure that copies of [complex] retain their status as "original" // selectors. This includes selectors that are modified because a :not() @@ -601,7 +594,6 @@ class ExtensionStore { /// complex selector with a line break. List? _extendCompound( ComplexSelectorComponent component, - FileSpan componentSpan, Map> extensions, List? mediaQueryContext, {required bool inOriginal}) { @@ -617,19 +609,20 @@ class ExtensionStore { List>? options; for (var i = 0; i < simples.length; i++) { var simple = simples[i]; - var extended = _extendSimple( - simple, componentSpan, extensions, mediaQueryContext, targetsUsed); + var extended = + _extendSimple(simple, extensions, mediaQueryContext, targetsUsed); assert( extended?.isNotEmpty ?? true, '_extendSimple($simple) should return null rather than [] if ' 'extension fails'); if (extended == null) { - options?.add([_extenderForSimple(simple, componentSpan)]); + options?.add([_extenderForSimple(simple)]); } else { if (options == null) { options = []; if (i != 0) { - options.add([_extenderForCompound(simples.take(i), componentSpan)]); + options + .add([_extenderForCompound(simples.take(i), component.span)]); } } @@ -702,14 +695,16 @@ class ExtensionStore { ComplexSelector(const [], [ ComplexSelectorComponent( CompoundSelector(extenderPaths.first.expand((extender) { - assert(extender.selector.components.length == 1); - return extender.selector.components.last.selector.components; - })), component.combinators) - ]) + assert(extender.selector.components.length == 1); + return extender.selector.components.last.selector.components; + }), component.selector.span), + component.combinators, + component.span) + ], component.span) ]; for (var path in extenderPaths.skip(_mode == ExtendMode.replace ? 0 : 1)) { - var extended = _unifyExtenders(path, mediaQueryContext); + var extended = _unifyExtenders(path, mediaQueryContext, component.span); if (extended == null) continue; for (var complex in extended) { @@ -732,8 +727,10 @@ class ExtensionStore { /// Returns a list of [ComplexSelector]s that match the intersection of /// elements matched by all of [extenders]' selectors. - List? _unifyExtenders( - List extenders, List? mediaQueryContext) { + /// + /// The [span] will be used for the new selectors. + List? _unifyExtenders(List extenders, + List? mediaQueryContext, FileSpan span) { var toUnify = QueueList(); List? originals; var originalsLineBreak = false; @@ -753,11 +750,12 @@ class ExtensionStore { if (originals != null) { toUnify.addFirst(ComplexSelector(const [], [ - ComplexSelectorComponent(CompoundSelector(originals), const []) - ], lineBreak: originalsLineBreak)); + ComplexSelectorComponent( + CompoundSelector(originals, span), const [], span) + ], span, lineBreak: originalsLineBreak)); } - var complexes = unifyComplex(toUnify); + var complexes = unifyComplex(toUnify, span); if (complexes == null) return null; for (var extender in extenders) { @@ -774,7 +772,6 @@ class ExtensionStore { /// combined using [paths]. Iterable>? _extendSimple( SimpleSelector simple, - FileSpan simpleSpan, Map> extensions, List? mediaQueryContext, Set? targetsUsed) { @@ -786,17 +783,16 @@ class ExtensionStore { targetsUsed?.add(simple); return [ - if (_mode != ExtendMode.replace) _extenderForSimple(simple, simpleSpan), + if (_mode != ExtendMode.replace) _extenderForSimple(simple), for (var extension in extensionsForSimple.values) extension.extender ]; } if (simple is PseudoSelector && simple.selector != null) { - var extended = - _extendPseudo(simple, simpleSpan, extensions, mediaQueryContext); + var extended = _extendPseudo(simple, extensions, mediaQueryContext); if (extended != null) { - return extended.map((pseudo) => - withoutPseudo(pseudo) ?? [_extenderForSimple(pseudo, simpleSpan)]); + return extended.map( + (pseudo) => withoutPseudo(pseudo) ?? [_extenderForSimple(pseudo)]); } } @@ -807,21 +803,20 @@ class ExtensionStore { /// [simples]. Extender _extenderForCompound( Iterable simples, FileSpan span) { - var compound = CompoundSelector(simples); + var compound = CompoundSelector(simples, span); return Extender( - ComplexSelector( - const [], [ComplexSelectorComponent(compound, const [])]), - span, + ComplexSelector(const [], + [ComplexSelectorComponent(compound, const [], span)], span), specificity: _sourceSpecificityFor(compound), original: true); } /// Returns an [Extender] composed solely of [simple]. - Extender _extenderForSimple(SimpleSelector simple, FileSpan span) => Extender( + Extender _extenderForSimple(SimpleSelector simple) => Extender( ComplexSelector(const [], [ - ComplexSelectorComponent(CompoundSelector([simple]), const []) - ]), - span, + ComplexSelectorComponent( + CompoundSelector([simple], simple.span), const [], simple.span) + ], simple.span), specificity: _sourceSpecificity[simple] ?? 0, original: true); @@ -831,7 +826,6 @@ class ExtensionStore { /// This requires that [pseudo] have a selector argument. List? _extendPseudo( PseudoSelector pseudo, - FileSpan pseudoSpan, Map> extensions, List? mediaQueryContext) { var selector = pseudo.selector; @@ -839,8 +833,7 @@ class ExtensionStore { throw ArgumentError("Selector $pseudo must have a selector argument."); } - var extended = - _extendList(selector, pseudoSpan, extensions, mediaQueryContext); + var extended = _extendList(selector, extensions, mediaQueryContext); if (identical(extended, selector)) return null; // For `:not()`, we usually want to get rid of any complex selectors because @@ -909,11 +902,12 @@ class ExtensionStore { // unless it originally contained a selector list. if (pseudo.normalizedName == 'not' && selector.components.length == 1) { var result = complexes - .map((complex) => pseudo.withSelector(SelectorList([complex]))) + .map((complex) => + pseudo.withSelector(SelectorList([complex], selector.span))) .toList(); return result.isEmpty ? null : result; } else { - return [pseudo.withSelector(SelectorList(complexes))]; + return [pseudo.withSelector(SelectorList(complexes, selector.span))]; } } @@ -996,26 +990,22 @@ class ExtensionStore { return specificity; } - /// Returns a copy of [this] that extends new selectors, as well as a map from - /// the selectors extended by [this] to the selectors extended by the new - /// [ExtensionStore]. - Tuple2, ModifiableCssValue>> clone() { - var newSelectors = - >>{}; - var newMediaContexts = - , List>{}; - var oldToNewSelectors = - , ModifiableCssValue>{}; + /// Returns a copy of [this] that extends new selectors, as well as a map + /// (with reference equality) from the selectors extended by [this] to the + /// selectors extended by the new [ExtensionStore]. + Tuple2>> clone() { + var newSelectors = >>{}; + var newMediaContexts = , List>{}; + var oldToNewSelectors = Map>.identity(); _selectors.forEach((simple, selectors) { - var newSelectorSet = >{}; + var newSelectorSet = >{}; newSelectors[simple] = newSelectorSet; for (var selector in selectors) { - var newSelector = ModifiableCssValue(selector.value, selector.span); + var newSelector = ModifiableBox(selector.value); newSelectorSet.add(newSelector); - oldToNewSelectors[selector] = newSelector; + oldToNewSelectors[selector.value] = newSelector.seal(); var mediaContext = _mediaContexts[selector]; if (mediaContext != null) newMediaContexts[newSelector] = mediaContext; diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index dc11b399a..047e48174 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -13,9 +13,12 @@ import 'dart:collection'; import 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; import 'package:tuple/tuple.dart'; +import '../ast/css/value.dart'; import '../ast/selector.dart'; +import '../util/span.dart'; import '../utils.dart'; /// Pseudo-selectors that can only meaningfully appear in the first component of @@ -25,13 +28,16 @@ final _rootishPseudoClasses = {'root', 'scope', 'host', 'host-context'}; /// Returns the contents of a [SelectorList] that matches only elements that are /// matched by every complex selector in [complexes]. /// +/// The [span] is used for the unified complex selectors. +/// /// If no such list can be produced, returns `null`. -List? unifyComplex(List complexes) { +List? unifyComplex( + List complexes, FileSpan span) { if (complexes.length == 1) return complexes; List? unifiedBase; - Combinator? leadingCombinator; - Combinator? trailingCombinator; + CssValue? leadingCombinator; + CssValue? trailingCombinator; for (var complex in complexes) { if (complex.isUseless) return null; @@ -68,44 +74,54 @@ List? unifyComplex(List complexes) { var withoutBases = [ for (var complex in complexes) if (complex.components.length > 1) - ComplexSelector( - complex.leadingCombinators, complex.components.exceptLast, + ComplexSelector(complex.leadingCombinators, + complex.components.exceptLast, complex.span, lineBreak: complex.lineBreak), ]; var base = ComplexSelector( leadingCombinator == null ? const [] : [leadingCombinator], [ - ComplexSelectorComponent(CompoundSelector(unifiedBase!), - trailingCombinator == null ? const [] : [trailingCombinator]) + ComplexSelectorComponent(CompoundSelector(unifiedBase!, span), + trailingCombinator == null ? const [] : [trailingCombinator], span) ], + span, lineBreak: complexes.any((complex) => complex.lineBreak)); - return weave(withoutBases.isEmpty - ? [base] - : [...withoutBases.exceptLast, withoutBases.last.concatenate(base)]); + return weave( + withoutBases.isEmpty + ? [base] + : [ + ...withoutBases.exceptLast, + withoutBases.last.concatenate(base, span) + ], + span); } /// Returns a [CompoundSelector] that matches only elements that are matched by /// both [compound1] and [compound2]. /// +/// The [span] will be used for the new unified selector. +/// /// If no such selector can be produced, returns `null`. CompoundSelector? unifyCompound( - List compound1, List compound2) { - var result = compound2; - for (var simple in compound1) { + CompoundSelector compound1, CompoundSelector compound2) { + var result = compound2.components; + for (var simple in compound1.components) { var unified = simple.unify(result); if (unified == null) return null; result = unified; } - return CompoundSelector(result); + return CompoundSelector(result, compound1.span); } /// Returns a [SimpleSelector] that matches only elements that are matched by /// both [selector1] and [selector2], which must both be either /// [UniversalSelector]s or [TypeSelector]s. /// +/// The [span] will be used for the new unified selector. +/// /// If no such selector can be produced, returns `null`. SimpleSelector? unifyUniversalAndElement( SimpleSelector selector1, SimpleSelector selector2) { @@ -152,8 +168,8 @@ SimpleSelector? unifyUniversalAndElement( } return name == null - ? UniversalSelector(namespace: namespace) - : TypeSelector(QualifiedName(name, namespace: namespace)); + ? UniversalSelector(selector1.span, namespace: namespace) + : TypeSelector(QualifiedName(name, namespace: namespace), selector1.span); } /// Expands "parenthesized selectors" in [complexes]. @@ -166,15 +182,18 @@ SimpleSelector? unifyUniversalAndElement( /// /// The selector `.D (.A .B)` is represented as the list `[.D, .A .B]`. /// +/// The [span] will be used for any new combined selectors. +/// /// If [forceLineBreak] is `true`, this will mark all returned complex selectors /// as having line breaks. -List weave(List complexes, +List weave(List complexes, FileSpan span, {bool forceLineBreak = false}) { if (complexes.length == 1) { var complex = complexes.first; if (!forceLineBreak || complex.lineBreak) return complexes; return [ - ComplexSelector(complex.leadingCombinators, complex.components, + ComplexSelector( + complex.leadingCombinators, complex.components, complex.span, lineBreak: true) ]; } @@ -182,20 +201,19 @@ List weave(List complexes, var prefixes = [complexes.first]; for (var complex in complexes.skip(1)) { - var target = complex.components.last; if (complex.components.length == 1) { for (var i = 0; i < prefixes.length; i++) { - prefixes[i] = - prefixes[i].concatenate(complex, forceLineBreak: forceLineBreak); + prefixes[i] = prefixes[i] + .concatenate(complex, span, forceLineBreak: forceLineBreak); } continue; } prefixes = [ for (var prefix in prefixes) - for (var parentPrefix - in _weaveParents(prefix, complex) ?? const []) - parentPrefix.withAdditionalComponent(target, + for (var parentPrefix in _weaveParents(prefix, complex, span) ?? + const []) + parentPrefix.withAdditionalComponent(complex.components.last, span, forceLineBreak: forceLineBreak), ]; } @@ -219,9 +237,11 @@ List weave(List complexes, /// elements matched by `P`. Some `PC_i` are elided to reduce the size of the /// output. /// +/// The [span] will be used for any new combined selectors. +/// /// Returns `null` if this intersection is empty. Iterable? _weaveParents( - ComplexSelector prefix, ComplexSelector base) { + ComplexSelector prefix, ComplexSelector base, FileSpan span) { var leadingCombinators = _mergeLeadingCombinators( prefix.leadingCombinators, base.leadingCombinators); if (leadingCombinators == null) return null; @@ -232,7 +252,7 @@ Iterable? _weaveParents( var queue1 = Queue.of(prefix.components); var queue2 = Queue.of(base.components.exceptLast); - var trailingCombinators = _mergeTrailingCombinators(queue1, queue2); + var trailingCombinators = _mergeTrailingCombinators(queue1, queue2, span); if (trailingCombinators == null) return null; // Make sure all selectors that are required to be at the root are unified @@ -240,11 +260,12 @@ Iterable? _weaveParents( var rootish1 = _firstIfRootish(queue1); var rootish2 = _firstIfRootish(queue2); if (rootish1 != null && rootish2 != null) { - var rootish = unifyCompound( - rootish1.selector.components, rootish2.selector.components); + var rootish = unifyCompound(rootish1.selector, rootish2.selector); if (rootish == null) return null; - queue1.addFirst(ComplexSelectorComponent(rootish, rootish1.combinators)); - queue2.addFirst(ComplexSelectorComponent(rootish, rootish2.combinators)); + queue1.addFirst( + ComplexSelectorComponent(rootish, rootish1.combinators, rootish1.span)); + queue2.addFirst( + ComplexSelectorComponent(rootish, rootish2.combinators, rootish1.span)); } else if (rootish1 != null || rootish2 != null) { // If there's only one rootish selector, it should only appear in the first // position of the resulting selector. We can ensure that happens by adding @@ -263,8 +284,10 @@ Iterable? _weaveParents( if (_complexIsParentSuperselector(group2, group1)) return group1; if (!_mustUnify(group1, group2)) return null; - var unified = unifyComplex( - [ComplexSelector(const [], group1), ComplexSelector(const [], group2)]); + var unified = unifyComplex([ + ComplexSelector(const [], group1, span), + ComplexSelector(const [], group2, span) + ], span); if (unified == null) return null; if (unified.length > 1) return null; return unified.first.components; @@ -291,8 +314,8 @@ Iterable? _weaveParents( return [ for (var path in paths(choices.where((choice) => choice.isNotEmpty))) - ComplexSelector( - leadingCombinators, [for (var components in path) ...components], + ComplexSelector(leadingCombinators, + [for (var components in path) ...components], span, lineBreak: prefix.lineBreak || base.lineBreak) ]; } @@ -319,8 +342,9 @@ ComplexSelectorComponent? _firstIfRootish( /// and [combinators2]. /// /// Returns `null` if the combinator lists can't be unified. -List? _mergeLeadingCombinators( - List? combinators1, List? combinators2) { +List>? _mergeLeadingCombinators( + List>? combinators1, + List>? combinators2) { // Allow null arguments just to make calls to `Iterable.reduce()` easier. if (combinators1 == null) return null; if (combinators2 == null) return null; @@ -342,16 +366,21 @@ List? _mergeLeadingCombinators( /// /// If there are no combinators to be merged, returns an empty list. If the /// sequences can't be merged, returns `null`. +/// +/// The [span] will be used for any new combined selectors. List>>? _mergeTrailingCombinators( Queue components1, Queue components2, + FileSpan span, [QueueList>>? result]) { result ??= QueueList(); - var combinators1 = - components1.isEmpty ? const [] : components1.last.combinators; - var combinators2 = - components2.isEmpty ? const [] : components2.last.combinators; + var combinators1 = components1.isEmpty + ? const >[] + : components1.last.combinators; + var combinators2 = components2.isEmpty + ? const >[] + : components2.last.combinators; if (combinators1.isEmpty && combinators2.isEmpty) return result; if (combinators1.length > 1 || combinators2.length > 1) return null; @@ -364,8 +393,8 @@ List>>? _mergeTrailingCombinators( var component1 = components1.removeLast(); var component2 = components2.removeLast(); - if (combinator1 == Combinator.followingSibling && - combinator2 == Combinator.followingSibling) { + if (combinator1.value == Combinator.followingSibling && + combinator2.value == Combinator.followingSibling) { if (component1.selector.isSuperselector(component2.selector)) { result.addFirst([ [component2] @@ -380,25 +409,27 @@ List>>? _mergeTrailingCombinators( [component2, component1] ]; - var unified = unifyCompound( - component1.selector.components, component2.selector.components); + var unified = unifyCompound(component1.selector, component2.selector); if (unified != null) { choices.add([ - ComplexSelectorComponent( - unified, const [Combinator.followingSibling]) + ComplexSelectorComponent(unified, [combinator1], span) ]); } result.addFirst(choices); } - } else if ((combinator1 == Combinator.followingSibling && - combinator2 == Combinator.nextSibling) || - (combinator1 == Combinator.nextSibling && - combinator2 == Combinator.followingSibling)) { + } else if ((combinator1.value == Combinator.followingSibling && + combinator2.value == Combinator.nextSibling) || + (combinator1.value == Combinator.nextSibling && + combinator2.value == Combinator.followingSibling)) { var followingSiblingComponent = - combinator1 == Combinator.followingSibling ? component1 : component2; + combinator1.value == Combinator.followingSibling + ? component1 + : component2; var nextSiblingComponent = - combinator1 == Combinator.followingSibling ? component2 : component1; + combinator1.value == Combinator.followingSibling + ? component2 + : component1; if (followingSiblingComponent.selector .isSuperselector(nextSiblingComponent.selector)) { @@ -406,46 +437,45 @@ List>>? _mergeTrailingCombinators( [nextSiblingComponent] ]); } else { - var unified = unifyCompound( - component1.selector.components, component2.selector.components); + var unified = unifyCompound(component1.selector, component2.selector); result.addFirst([ [followingSiblingComponent, nextSiblingComponent], if (unified != null) [ - ComplexSelectorComponent(unified, const [Combinator.nextSibling]) + ComplexSelectorComponent( + unified, nextSiblingComponent.combinators, span) ] ]); } - } else if (combinator1 == Combinator.child && - (combinator2 == Combinator.nextSibling || - combinator2 == Combinator.followingSibling)) { + } else if (combinator1.value == Combinator.child && + (combinator2.value == Combinator.nextSibling || + combinator2.value == Combinator.followingSibling)) { result.addFirst([ [component2] ]); components1.add(component1); - } else if (combinator2 == Combinator.child && - (combinator1 == Combinator.nextSibling || - combinator1 == Combinator.followingSibling)) { + } else if (combinator2.value == Combinator.child && + (combinator1.value == Combinator.nextSibling || + combinator1.value == Combinator.followingSibling)) { result.addFirst([ [component1] ]); components2.add(component2); } else if (combinator1 == combinator2) { - var unified = unifyCompound( - component1.selector.components, component2.selector.components); + var unified = unifyCompound(component1.selector, component2.selector); if (unified == null) return null; result.addFirst([ [ - ComplexSelectorComponent(unified, [combinator1]) + ComplexSelectorComponent(unified, [combinator1], span) ] ]); } else { return null; } - return _mergeTrailingCombinators(components1, components2, result); + return _mergeTrailingCombinators(components1, components2, span, result); } else if (combinator1 != null) { - if (combinator1 == Combinator.child && + if (combinator1.value == Combinator.child && components2.isNotEmpty && components2.last.selector.isSuperselector(components1.last.selector)) { components2.removeLast(); @@ -453,9 +483,9 @@ List>>? _mergeTrailingCombinators( result.addFirst([ [components1.removeLast()] ]); - return _mergeTrailingCombinators(components1, components2, result); + return _mergeTrailingCombinators(components1, components2, span, result); } else { - if (combinator2 == Combinator.child && + if (combinator2?.value == Combinator.child && components1.isNotEmpty && components1.last.selector.isSuperselector(components2.last.selector)) { components1.removeLast(); @@ -463,7 +493,7 @@ List>>? _mergeTrailingCombinators( result.addFirst([ [components2.removeLast()] ]); - return _mergeTrailingCombinators(components1, components2, result); + return _mergeTrailingCombinators(components1, components2, span, result); } } @@ -581,7 +611,9 @@ bool _complexIsParentSuperselector(List complex1, // TODO(nweiz): There's got to be a way to do this without a bunch of extra // allocations... var base = ComplexSelectorComponent( - CompoundSelector([PlaceholderSelector('')]), const []); + CompoundSelector([PlaceholderSelector('', bogusSpan)], bogusSpan), + const [], + bogusSpan); return complexIsSuperselector([...complex1, base], [...complex2, base]); } @@ -598,7 +630,7 @@ bool complexIsSuperselector(List complex1, var i1 = 0; var i2 = 0; - Combinator? previousCombinator; + CssValue? previousCombinator; while (true) { var remaining1 = complex1.length - i1; var remaining2 = complex2.length - i2; @@ -661,7 +693,7 @@ bool complexIsSuperselector(List complex1, previousCombinator = combinator1; if (complex1.length - i1 == 1) { - if (combinator1 == Combinator.followingSibling) { + if (combinator1?.value == Combinator.followingSibling) { // 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) => @@ -682,29 +714,30 @@ bool complexIsSuperselector(List complex1, /// complex superselector and another, given that the earlier complex /// superselector had the combinator [previous]. bool _compatibleWithPreviousCombinator( - Combinator? previous, List parents) { + CssValue? previous, List parents) { if (parents.isEmpty) return true; if (previous == null) return true; // The child and next sibling combinators require that the *immediate* // following component be a superslector. - if (previous != Combinator.followingSibling) return false; + if (previous.value != Combinator.followingSibling) return false; // The following sibling combinator does allow intermediate components, but // only if they're all siblings. return parents.every((component) => - component.combinators.firstOrNull == Combinator.followingSibling || - component.combinators.firstOrNull == Combinator.nextSibling); + component.combinators.firstOrNull?.value == Combinator.followingSibling || + component.combinators.firstOrNull?.value == Combinator.nextSibling); } /// Returns whether [combinator1] is a supercombinator of [combinator2]. /// /// That is, whether `X combinator1 Y` is a superselector of `X combinator2 Y`. -bool _isSupercombinator(Combinator? combinator1, Combinator? combinator2) => +bool _isSupercombinator( + CssValue? combinator1, CssValue? combinator2) => combinator1 == combinator2 || - (combinator1 == null && combinator2 == Combinator.child) || - (combinator1 == Combinator.followingSibling && - combinator2 == Combinator.nextSibling); + (combinator1 == null && combinator2?.value == Combinator.child) || + (combinator1?.value == Combinator.followingSibling && + combinator2?.value == Combinator.nextSibling); /// Returns whether [compound1] is a superselector of [compound2]. /// @@ -776,9 +809,11 @@ bool _compoundComponentsIsSuperselector( Iterable compound1, Iterable compound2, {Iterable? parents}) { if (compound1.isEmpty) return true; - if (compound2.isEmpty) compound2 = [UniversalSelector(namespace: '*')]; - return compoundIsSuperselector( - CompoundSelector(compound1), CompoundSelector(compound2), + if (compound2.isEmpty) { + compound2 = [UniversalSelector(bogusSpan, namespace: '*')]; + } + return compoundIsSuperselector(CompoundSelector(compound1, bogusSpan), + CompoundSelector(compound2, bogusSpan), parents: parents); } @@ -813,7 +848,7 @@ bool _selectorPseudoIsSuperselector( complex1.leadingCombinators.isEmpty && complexIsSuperselector(complex1.components, [ ...?parents, - ComplexSelectorComponent(compound2, const []) + ComplexSelectorComponent(compound2, const [], compound2.span) ])); case 'has': diff --git a/lib/src/extend/merged_extension.dart b/lib/src/extend/merged_extension.dart index ddffad5e1..a0caec7fb 100644 --- a/lib/src/extend/merged_extension.dart +++ b/lib/src/extend/merged_extension.dart @@ -50,8 +50,7 @@ class MergedExtension extends Extension { } MergedExtension._(this.left, this.right) - : super( - left.extender.selector, left.extender.span, left.target, left.span, + : super(left.extender.selector, left.target, left.span, mediaContext: left.mediaContext ?? right.mediaContext, optional: true); diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart index 0be1fd866..282874dac 100644 --- a/lib/src/functions/selector.dart +++ b/lib/src/functions/selector.dart @@ -63,6 +63,7 @@ final _append = _function("append", r"$selectors...", (arguments) { "\$selectors: At least one selector must be passed."); } + var span = EvaluationContext.current.currentCallableSpan; return selectors .map((selector) => selector.assertSelector()) .reduce((parent, child) { @@ -78,10 +79,11 @@ final _append = _function("append", r"$selectors...", (arguments) { } return ComplexSelector(const [], [ - ComplexSelectorComponent(newCompound, component.combinators), + ComplexSelectorComponent(newCompound, component.combinators, span), ...complex.components.skip(1) - ]); - })).resolveParentSelectors(parent); + ], span); + }), span) + .resolveParentSelectors(parent); }).asSassList; }); @@ -151,14 +153,17 @@ final _parse = _function("parse", r"$selector", CompoundSelector? _prependParent(CompoundSelector compound) { var first = compound.components.first; if (first is UniversalSelector) return null; + + var span = EvaluationContext.current.currentCallableSpan; if (first is TypeSelector) { if (first.name.namespace != null) return null; return CompoundSelector([ - ParentSelector(suffix: first.name.name), + ParentSelector(span, suffix: first.name.name), ...compound.components.skip(1) - ]); + ], span); } else { - return CompoundSelector([ParentSelector(), ...compound.components]); + return CompoundSelector( + [ParentSelector(span), ...compound.components], span); } } diff --git a/lib/src/interpolation_map.dart b/lib/src/interpolation_map.dart new file mode 100644 index 000000000..111fbf00b --- /dev/null +++ b/lib/src/interpolation_map.dart @@ -0,0 +1,180 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:math' as math; + +import 'package:charcode/charcode.dart'; +import 'package:source_span/source_span.dart'; +import 'package:string_scanner/string_scanner.dart'; + +import 'ast/sass.dart'; +import 'util/character.dart'; + +/// A class that can map locations in a string generated from an [Interpolation] +/// to the original source code in the interpolation. +class InterpolationMap { + /// The interpolation from which this map was generated. + final Interpolation _interpolation; + + /// Locations in the generated string. + /// + /// Each of these indicates the location in the generated string that + /// corresponds to the end of the component at the same index of + /// [_interpolation.contents]. Its length is always one less than + /// [_interpolation.contents] because the last element always ends the string. + final List _targetLocations; + + /// Creates a new interpolation map that maps the given [targetLocations] in + /// the generated string to the contents of the interpolation. + /// + /// Each target location at index `i` corresponds to the character in the + /// generated string after `interpolation.contents[i]`. + InterpolationMap( + this._interpolation, Iterable targetLocations) + : _targetLocations = List.unmodifiable(targetLocations) { + var expectedLocations = math.max(0, _interpolation.contents.length - 1); + if (_targetLocations.length != expectedLocations) { + throw ArgumentError( + "InterpolationMap must have $expectedLocations targetLocations if the " + "interpolation has ${_interpolation.contents.length} components."); + } + } + + /// Maps [error]'s span in the string generated from this interpolation to its + /// original source. + FormatException mapException(SourceSpanFormatException error) { + var target = error.span; + if (target == null) return error; + + var source = mapSpan(target); + var startIndex = _indexInContents(target.start); + var endIndex = _indexInContents(target.end); + + if (!_interpolation.contents + .skip(startIndex) + .take(endIndex - startIndex + 1) + .any((content) => content is Expression)) { + return SourceSpanFormatException(error.message, source, error.source); + } else { + return MultiSourceSpanFormatException(error.message, source, "", + {target: "error in interpolated output"}, error.source); + } + } + + /// Maps a span in the string generated from this interpolation to its + /// original source. + FileSpan mapSpan(SourceSpan target) { + var start = _mapLocation(target.start); + var end = _mapLocation(target.end); + + if (start is FileSpan) { + if (end is FileSpan) return start.expand(end); + + return _interpolation.span.file.span( + _expandInterpolationSpanLeft(start.start), + (end as FileLocation).offset); + } else if (end is FileSpan) { + return _interpolation.span.file.span((start as FileLocation).offset, + _expandInterpolationSpanRight(end.end)); + } else { + return _interpolation.span.file + .span((start as FileLocation).offset, (end as FileLocation).offset); + } + } + + /// Maps a location in the string generated from this interpolation to its + /// original source. + /// + /// If [source] points to an un-interpolated portion of the original string, + /// this will return the corresponding [FileLocation]. If it points to text + /// generated from interpolation, this will return the full [FileSpan] for + /// that interpolated expression. + Object /* FileLocation|FileSpan */ _mapLocation(SourceLocation target) { + var index = _indexInContents(target); + var chunk = _interpolation.contents[index]; + if (chunk is Expression) return chunk.span; + + var previousLocation = index == 0 + ? _interpolation.span.start + : _interpolation.span.file.location(_expandInterpolationSpanRight( + (_interpolation.contents[index - 1] as Expression).span.end)); + var offsetInString = + target.offset - (index == 0 ? 0 : _targetLocations[index - 1].offset); + + // This produces slightly incorrect mappings if there are _unnecessary_ + // escapes in the source file, but that's unlikely enough that it's probably + // not worth doing a reparse here to fix it. + return previousLocation.file + .location(previousLocation.offset + offsetInString); + } + + /// Return the index in [_interpolation.contents] at which [target] points. + int _indexInContents(SourceLocation target) { + for (var i = 0; i < _targetLocations.length; i++) { + if (target.offset < _targetLocations[i].offset) return i; + } + + return _interpolation.contents.length - 1; + } + + /// Given the start of a [FileSpan] covering an interpolated expression, returns + /// the offset of the interpolation's opening `#`. + /// + /// Note that this can be tricked by a `#{` that appears within a single-line + /// comment before the expression, but since it's only used for error + /// reporting that's probably fine. + int _expandInterpolationSpanLeft(FileLocation start) { + var source = start.file.getText(0, start.offset); + var i = start.offset - 1; + while (true) { + var prev = source.codeUnitAt(i--); + if (prev == $lbrace) { + if (source.codeUnitAt(i) == $hash) break; + } else if (prev == $slash) { + var second = source.codeUnitAt(i--); + if (second == $asterisk) { + while (true) { + var char = source.codeUnitAt(i--); + if (char != $asterisk) continue; + + do { + char = source.codeUnitAt(i--); + } while (char == $asterisk); + if (char == $slash) break; + } + } + } + } + + return i; + } + + /// Given the end of a [FileSpan] covering an interpolated expression, returns + /// the offset of the interpolation's closing `}`. + int _expandInterpolationSpanRight(FileLocation end) { + var scanner = StringScanner(end.file.getText(end.offset)); + while (true) { + var next = scanner.readChar(); + if (next == $rbrace) break; + if (next == $slash) { + var second = scanner.readChar(); + if (second == $slash) { + while (!isNewline(scanner.readChar())) {} + } else if (second == $asterisk) { + while (true) { + var char = scanner.readChar(); + if (char != $asterisk) continue; + + do { + char = scanner.readChar(); + } while (char == $asterisk); + if (char == $slash) break; + } + } + } + } + + return end.offset + scanner.position; + } +} diff --git a/lib/src/parse/at_root_query.dart b/lib/src/parse/at_root_query.dart index f46207675..11eee11f2 100644 --- a/lib/src/parse/at_root_query.dart +++ b/lib/src/parse/at_root_query.dart @@ -5,13 +5,16 @@ import 'package:charcode/charcode.dart'; import '../ast/sass.dart'; +import '../interpolation_map.dart'; import '../logger.dart'; import 'parser.dart'; /// A parser for `@at-root` queries. class AtRootQueryParser extends Parser { - AtRootQueryParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + AtRootQueryParser(String contents, + {Object? url, Logger? logger, InterpolationMap? interpolationMap}) + : super(contents, + url: url, logger: logger, interpolationMap: interpolationMap); AtRootQuery parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart index a301cb486..dcf81cdb3 100644 --- a/lib/src/parse/keyframe_selector.dart +++ b/lib/src/parse/keyframe_selector.dart @@ -4,14 +4,17 @@ import 'package:charcode/charcode.dart'; +import '../interpolation_map.dart'; import '../logger.dart'; import '../util/character.dart'; import 'parser.dart'; /// A parser for `@keyframes` block selectors. class KeyframeSelectorParser extends Parser { - KeyframeSelectorParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + KeyframeSelectorParser(String contents, + {Object? url, Logger? logger, InterpolationMap? interpolationMap}) + : super(contents, + url: url, logger: logger, interpolationMap: interpolationMap); List parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/media_query.dart b/lib/src/parse/media_query.dart index d38a472f5..be86a1994 100644 --- a/lib/src/parse/media_query.dart +++ b/lib/src/parse/media_query.dart @@ -5,14 +5,17 @@ import 'package:charcode/charcode.dart'; import '../ast/css.dart'; +import '../interpolation_map.dart'; import '../logger.dart'; import '../utils.dart'; import 'parser.dart'; /// A parser for `@media` queries. class MediaQueryParser extends Parser { - MediaQueryParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + MediaQueryParser(String contents, + {Object? url, Logger? logger, InterpolationMap? interpolationMap}) + : super(contents, + url: url, logger: logger, interpolationMap: interpolationMap); List parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index dd8f27439..c2c27f56c 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -8,6 +8,7 @@ import 'package:source_span/source_span.dart'; import 'package:string_scanner/string_scanner.dart'; import '../exception.dart'; +import '../interpolation_map.dart'; import '../logger.dart'; import '../util/character.dart'; import '../utils.dart'; @@ -25,6 +26,11 @@ class Parser { @protected final Logger logger; + /// A map used to map source spans in the text being parsed back to their + /// original locations in the source file, if this isn't being parsed directly + /// from source. + final InterpolationMap? _interpolationMap; + /// Parses [text] as a CSS identifier and returns the result. /// /// Throws a [SassFormatException] if parsing fails. @@ -48,9 +54,11 @@ class Parser { Parser(text, logger: logger)._isVariableDeclarationLike(); @protected - Parser(String contents, {Object? url, Logger? logger}) + Parser(String contents, + {Object? url, Logger? logger, InterpolationMap? interpolationMap}) : scanner = SpanScanner(contents, sourceUrl: url), - logger = logger ?? const Logger.stderr(); + logger = logger ?? const Logger.stderr(), + _interpolationMap = interpolationMap; String _parseIdentifier() { return wrapSpanFormatException(() { @@ -662,6 +670,14 @@ class Parser { return scanner.substring(start); } + /// Like [scanner.spanFrom], but passes the span through [_interpolationMap] + /// if it's available. + @protected + FileSpan spanFrom(LineScannerState state) { + var span = scanner.spanFrom(state); + return _interpolationMap?.mapSpan(span) ?? span; + } + /// Prints a warning to standard error, associated with [span]. @protected void warn(String message, FileSpan span) => logger.warn(message, span: span); @@ -710,40 +726,72 @@ class Parser { @protected T wrapSpanFormatException(T callback()) { try { - return callback(); + try { + return callback(); + } on SourceSpanFormatException catch (error, stackTrace) { + var map = _interpolationMap; + if (map == null) rethrow; + + throwWithTrace(map.mapException(error), stackTrace); + } } on SourceSpanFormatException catch (error, stackTrace) { var span = error.span as FileSpan; - if (startsWithIgnoreCase(error.message, "expected") && span.length == 0) { - var startPosition = _firstNewlineBefore(span.start.offset); - if (startPosition != span.start.offset) { - span = span.file.span(startPosition, startPosition); - } + if (startsWithIgnoreCase(error.message, "expected")) { + span = _adjustExceptionSpan(span); } throwWithTrace(SassFormatException(error.message, span), stackTrace); + } on MultiSourceSpanFormatException catch (error, stackTrace) { + var span = error.span as FileSpan; + var secondarySpans = error.secondarySpans.cast(); + if (startsWithIgnoreCase(error.message, "expected")) { + span = _adjustExceptionSpan(span); + secondarySpans = { + for (var entry in secondarySpans.entries) + _adjustExceptionSpan(entry.key): entry.value + }; + } + + throwWithTrace( + MultiSpanSassFormatException( + error.message, span, error.primaryLabel, secondarySpans), + stackTrace); } } - /// If [position] is separated from the previous non-whitespace character in - /// `scanner.string` by one or more newlines, returns the offset of the last + /// Moves span to [_firstNewlineBefore] if necessary. + FileSpan _adjustExceptionSpan(FileSpan span) { + if (span.length > 0) return span; + + var start = _firstNewlineBefore(span.start); + return start == span.start ? span : start.pointSpan(); + } + + /// If [location] is separated from the previous non-whitespace character in + /// `scanner.string` by one or more newlines, returns the location of the last /// separating newline. /// - /// Otherwise returns [position]. + /// Otherwise returns [location]. /// /// This helps avoid missing token errors pointing at the next closing bracket /// rather than the line where the problem actually occurred. - int _firstNewlineBefore(int position) { - var index = position - 1; + FileLocation _firstNewlineBefore(FileLocation location) { + var text = location.file.getText(0, location.offset); + var index = location.offset - 1; int? lastNewline; while (index >= 0) { - var codeUnit = scanner.string.codeUnitAt(index); - if (!isWhitespace(codeUnit)) return lastNewline ?? position; + var codeUnit = text.codeUnitAt(index); + if (!isWhitespace(codeUnit)) { + return lastNewline == null + ? location + : location.file.location(lastNewline); + } if (isNewline(codeUnit)) lastNewline = index; index--; } - // If the document *only* contains whitespace before [position], always - // return [position]. - return position; + // If the document *only* contains whitespace before [location], always + // return [location]. + return location; } } diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 0a270ccf8..e376f76be 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -4,7 +4,9 @@ import 'package:charcode/charcode.dart'; +import '../ast/css/value.dart'; import '../ast/selector.dart'; +import '../interpolation_map.dart'; import '../logger.dart'; import '../util/character.dart'; import '../utils.dart'; @@ -37,11 +39,13 @@ class SelectorParser extends Parser { SelectorParser(String contents, {Object? url, Logger? logger, + InterpolationMap? interpolationMap, bool allowParent = true, bool allowPlaceholder = true}) : _allowParent = allowParent, _allowPlaceholder = allowPlaceholder, - super(contents, url: url, logger: logger); + super(contents, + url: url, logger: logger, interpolationMap: interpolationMap); SelectorList parse() { return wrapSpanFormatException(() { @@ -77,6 +81,7 @@ class SelectorParser extends Parser { /// Consumes a selector list. SelectorList _selectorList() { + var start = scanner.state; var previousLine = scanner.line; var components = [_complexSelector()]; @@ -92,7 +97,7 @@ class SelectorParser extends Parser { components.add(_complexSelector(lineBreak: lineBreak)); } - return SelectorList(components); + return SelectorList(components, spanFrom(start)); } /// Consumes a complex selector. @@ -100,10 +105,13 @@ class SelectorParser extends Parser { /// If [lineBreak] is `true`, that indicates that there was a line break /// before this selector. ComplexSelector _complexSelector({bool lineBreak = false}) { + var start = scanner.state; + + var componentStart = scanner.state; CompoundSelector? lastCompound; - var combinators = []; + var combinators = >[]; - List? initialCombinators; + List>? initialCombinators; var components = []; loop: @@ -113,18 +121,24 @@ class SelectorParser extends Parser { var next = scanner.peekChar(); switch (next) { case $plus: + var combinatorStart = scanner.state; scanner.readChar(); - combinators.add(Combinator.nextSibling); + combinators + .add(CssValue(Combinator.nextSibling, spanFrom(combinatorStart))); break; case $gt: + var combinatorStart = scanner.state; scanner.readChar(); - combinators.add(Combinator.child); + combinators + .add(CssValue(Combinator.child, spanFrom(combinatorStart))); break; case $tilde: + var combinatorStart = scanner.state; scanner.readChar(); - combinators.add(Combinator.followingSibling); + combinators.add( + CssValue(Combinator.followingSibling, spanFrom(combinatorStart))); break; default: @@ -144,10 +158,12 @@ class SelectorParser extends Parser { } if (lastCompound != null) { - components.add(ComplexSelectorComponent(lastCompound, combinators)); + components.add(ComplexSelectorComponent( + lastCompound, combinators, spanFrom(componentStart))); } else if (combinators.isNotEmpty) { assert(initialCombinators == null); initialCombinators = combinators; + componentStart = scanner.state; } lastCompound = _compoundSelector(); @@ -161,26 +177,29 @@ class SelectorParser extends Parser { } if (lastCompound != null) { - components.add(ComplexSelectorComponent(lastCompound, combinators)); + components.add(ComplexSelectorComponent( + lastCompound, combinators, spanFrom(componentStart))); } else if (combinators.isNotEmpty) { initialCombinators = combinators; } else { scanner.error("expected selector."); } - return ComplexSelector(initialCombinators ?? const [], components, + return ComplexSelector( + initialCombinators ?? const [], components, spanFrom(start), lineBreak: lineBreak); } /// Consumes a compound selector. CompoundSelector _compoundSelector() { + var start = scanner.state; var components = [_simpleSelector()]; while (isSimpleSelectorStart(scanner.peekChar())) { components.add(_simpleSelector(allowParent: false)); } - return CompoundSelector(components); + return CompoundSelector(components, spanFrom(start)); } /// Consumes a simple selector. @@ -221,12 +240,15 @@ class SelectorParser extends Parser { /// Consumes an attribute selector. AttributeSelector _attributeSelector() { + var start = scanner.state; scanner.expectChar($lbracket); whitespace(); var name = _attributeName(); whitespace(); - if (scanner.scanChar($rbracket)) return AttributeSelector(name); + if (scanner.scanChar($rbracket)) { + return AttributeSelector(name, spanFrom(start)); + } var operator = _attributeOperator(); whitespace(); @@ -243,7 +265,8 @@ class SelectorParser extends Parser { : null; scanner.expectChar($rbracket); - return AttributeSelector.withOperator(name, operator, value, + return AttributeSelector.withOperator( + name, operator, value, spanFrom(start), modifier: modifier); } @@ -301,40 +324,45 @@ class SelectorParser extends Parser { /// Consumes a class selector. ClassSelector _classSelector() { + var start = scanner.state; scanner.expectChar($dot); var name = identifier(); - return ClassSelector(name); + return ClassSelector(name, spanFrom(start)); } /// Consumes an ID selector. IDSelector _idSelector() { + var start = scanner.state; scanner.expectChar($hash); var name = identifier(); - return IDSelector(name); + return IDSelector(name, spanFrom(start)); } /// Consumes a placeholder selector. PlaceholderSelector _placeholderSelector() { + var start = scanner.state; scanner.expectChar($percent); var name = identifier(); - return PlaceholderSelector(name); + return PlaceholderSelector(name, spanFrom(start)); } /// Consumes a parent selector. ParentSelector _parentSelector() { + var start = scanner.state; scanner.expectChar($ampersand); var suffix = lookingAtIdentifierBody() ? identifierBody() : null; - return ParentSelector(suffix: suffix); + return ParentSelector(spanFrom(start), suffix: suffix); } /// Consumes a pseudo selector. PseudoSelector _pseudoSelector() { + var start = scanner.state; scanner.expectChar($colon); var element = scanner.scanChar($colon); var name = identifier(); if (!scanner.scanChar($lparen)) { - return PseudoSelector(name, element: element); + return PseudoSelector(name, spanFrom(start), element: element); } whitespace(); @@ -364,7 +392,7 @@ class SelectorParser extends Parser { } scanner.expectChar($rparen); - return PseudoSelector(name, + return PseudoSelector(name, spanFrom(start), element: element, argument: argument, selector: selector); } @@ -420,32 +448,36 @@ class SelectorParser extends Parser { /// /// These are combined because either one could start with `*`. SimpleSelector _typeOrUniversalSelector() { + var start = scanner.state; var first = scanner.peekChar(); if (first == $asterisk) { scanner.readChar(); - if (!scanner.scanChar($pipe)) return UniversalSelector(); + if (!scanner.scanChar($pipe)) return UniversalSelector(spanFrom(start)); if (scanner.scanChar($asterisk)) { - return UniversalSelector(namespace: "*"); + return UniversalSelector(spanFrom(start), namespace: "*"); } else { - return TypeSelector(QualifiedName(identifier(), namespace: "*")); + return TypeSelector( + QualifiedName(identifier(), namespace: "*"), spanFrom(start)); } } else if (first == $pipe) { scanner.readChar(); if (scanner.scanChar($asterisk)) { - return UniversalSelector(namespace: ""); + return UniversalSelector(spanFrom(start), namespace: ""); } else { - return TypeSelector(QualifiedName(identifier(), namespace: "")); + return TypeSelector( + QualifiedName(identifier(), namespace: ""), spanFrom(start)); } } var nameOrNamespace = identifier(); if (!scanner.scanChar($pipe)) { - return TypeSelector(QualifiedName(nameOrNamespace)); + return TypeSelector(QualifiedName(nameOrNamespace), spanFrom(start)); } else if (scanner.scanChar($asterisk)) { - return UniversalSelector(namespace: nameOrNamespace); + return UniversalSelector(spanFrom(start), namespace: nameOrNamespace); } else { return TypeSelector( - QualifiedName(identifier(), namespace: nameOrNamespace)); + QualifiedName(identifier(), namespace: nameOrNamespace), + spanFrom(start)); } } } diff --git a/lib/src/util/box.dart b/lib/src/util/box.dart new file mode 100644 index 000000000..cfd076669 --- /dev/null +++ b/lib/src/util/box.dart @@ -0,0 +1,34 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +/// An unmodifiable reference to a value that may be mutated elsewhere. +/// +/// This uses reference equality based on the underlying [ModifiableBox], even +/// when the underlying type uses value equality. +class Box { + final ModifiableBox _inner; + + T get value => _inner.value; + + Box._(this._inner); + + bool operator ==(Object? other) => other is Box && other._inner == _inner; + + int get hashCode => _inner.hashCode; +} + +/// A mutable reference to a (presumably immutable) value. +/// +/// This always uses reference equality, even when the underlying type uses +/// value equality. +class ModifiableBox { + T value; + + ModifiableBox(this.value); + + /// Returns an unmodifiable reference to this box. + /// + /// The underlying modifiable box may still be modified. + Box seal() => Box._(this); +} diff --git a/lib/src/util/span.dart b/lib/src/util/span.dart index d54b4f427..b6840e481 100644 --- a/lib/src/util/span.dart +++ b/lib/src/util/span.dart @@ -9,6 +9,10 @@ import 'package:string_scanner/string_scanner.dart'; import '../utils.dart'; import 'character.dart'; +/// A span that points nowhere, only used for fake AST nodes that will never be +/// presented to the user. +final bogusSpan = SourceFile.decoded([]).span(0); + extension SpanExtensions on FileSpan { /// Returns this span with all whitespace trimmed from both sides. FileSpan trim() => trimLeft().trimRight(); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 21ae174a5..e73423933 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -464,6 +464,16 @@ extension MapExtension on Map { } extension IterableExtension on Iterable { + /// Returns the first `T` returned by [callback] for an element of [iterable], + /// or `null` if it returns `null` for every element. + T? search(T? Function(E element) callback) { + for (var element in this) { + var value = callback(element); + if (value != null) return value; + } + return null; + } + /// Returns a view of this list that covers all elements except the last. /// /// Note this is only efficient for an iterable with a known length. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 11d1356e2..a500e8c4e 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -31,6 +31,7 @@ import '../functions.dart'; import '../functions/meta.dart' as meta; import '../importer.dart'; import '../importer/legacy_node.dart'; +import '../interpolation_map.dart'; import '../io.dart'; import '../logger.dart'; import '../module.dart'; @@ -40,6 +41,7 @@ import '../syntax.dart'; import '../utils.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; +import '../util/span.dart'; import '../value.dart'; import 'expression_to_calc.dart'; import 'interface/css.dart'; @@ -525,7 +527,7 @@ class _EvaluateVisitor if (!(_asNodeSass && url.toString() == 'stdin')) _loadedUrls.add(url); } - var module = await _execute(importer, node); + var module = await _addExceptionTrace(() => _execute(importer, node)); return EvaluateResult(_combineCss(module), _loadedUrls); }); @@ -534,14 +536,14 @@ class _EvaluateVisitor Future runExpression(AsyncImporter? importer, Expression expression) => withEvaluationContext( _EvaluationContext(this, expression), - () => _withFakeStylesheet( - importer, expression, () => expression.accept(this))); + () => _withFakeStylesheet(importer, expression, + () => _addExceptionTrace(() => expression.accept(this)))); Future runStatement(AsyncImporter? importer, Statement statement) => withEvaluationContext( _EvaluationContext(this, statement), - () => _withFakeStylesheet( - importer, statement, () => statement.accept(this))); + () => _withFakeStylesheet(importer, statement, + () => _addExceptionTrace(() => statement.accept(this)))); /// Asserts that [value] is not `null` and returns it. /// @@ -638,29 +640,8 @@ class _EvaluateVisitor _inDependency = oldInDependency; } - try { - await callback(module); - } on SassRuntimeException { - rethrow; - } on MultiSpanSassException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - error.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(error.span)), - stackTrace); - } on SassException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, error.span), stackTrace); - } on MultiSpanSassScriptException catch (error, stackTrace) { - throwWithTrace( - _multiSpanException( - error.message, error.primaryLabel, error.secondarySpans), - stackTrace); - } on SassScriptException catch (error, stackTrace) { - throwWithTrace(_exception(error.message), stackTrace); - } + await _addExceptionSpanAsync(nodeWithSpan, () => callback(module), + addStackFrame: false); }); } @@ -940,10 +921,12 @@ class _EvaluateVisitor var query = AtRootQuery.defaultQuery; var unparsedQuery = node.query; if (unparsedQuery != null) { - var resolved = - await _performInterpolation(unparsedQuery, warnForColor: true); - query = _adjustParseError( - unparsedQuery, () => AtRootQuery.parse(resolved, logger: _logger)); + var tuple = + await _performInterpolationWithMap(unparsedQuery, warnForColor: true); + var resolved = tuple.item1; + var map = tuple.item2; + query = + AtRootQuery.parse(resolved, interpolationMap: map, logger: _logger); } var parent = _parent; @@ -1220,20 +1203,18 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - MultiSpan(styleRule.selector.span, 'invalid selector', + MultiSpan(complex.span.trimRight(), 'invalid selector', {node.span: '@extend rule'}), deprecation: true); } - var targetText = - await _interpolationToValue(node.selector, warnForColor: true); + var tuple = + await _performInterpolationWithMap(node.selector, warnForColor: true); + var targetText = tuple.item1; + var targetMap = tuple.item2; - var list = _adjustParseError( - targetText, - () => SelectorList.parse( - trimAscii(targetText.value, excludeEscape: true), - logger: _logger, - allowParent: false)); + var list = SelectorList.parse(trimAscii(targetText, excludeEscape: true), + interpolationMap: targetMap, logger: _logger, allowParent: false); for (var complex in list.components) { var compound = complex.singleCompound; @@ -1241,7 +1222,7 @@ class _EvaluateVisitor // If the selector was a compound selector but not a simple // selector, emit a more explicit error. throw SassFormatException( - "complex selectors may not be extended.", targetText.span); + "complex selectors may not be extended.", complex.span); } var simple = compound.singleSimple; @@ -1250,7 +1231,7 @@ class _EvaluateVisitor "compound selectors may no longer be extended.\n" "Consider `@extend ${compound.components.join(', ')}` instead.\n" "See https://sass-lang.com/d/extend-compound for details.\n", - targetText.span); + compound.span); } _extensionStore.addExtension( @@ -1651,8 +1632,8 @@ class _EvaluateVisitor } else { throw "Can't find stylesheet to import."; } - } on SassException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, error.span), stackTrace); + } on SassException { + rethrow; } on ArgumentError catch (error, stackTrace) { throwWithTrace(_exception(error.toString()), stackTrace); } catch (error, stackTrace) { @@ -1837,12 +1818,12 @@ class _EvaluateVisitor /// queries. Future> _visitMediaQueries( Interpolation interpolation) async { - var resolved = - await _performInterpolation(interpolation, warnForColor: true); - - // TODO(nweiz): Remove this type argument when sdk#31398 is fixed. - return _adjustParseError>(interpolation, - () => CssMediaQuery.parseList(resolved, logger: _logger)); + var tuple = + await _performInterpolationWithMap(interpolation, warnForColor: true); + var resolved = tuple.item1; + var map = tuple.item2; + return CssMediaQuery.parseList(resolved, + logger: _logger, interpolationMap: map); } /// Returns a list of queries that selects for contexts that match both @@ -1879,16 +1860,18 @@ class _EvaluateVisitor "Style rules may not be used within nested declarations.", node.span); } - var selectorText = await _interpolationToValue(node.selector, - trim: true, warnForColor: true); + var tuple = + await _performInterpolationWithMap(node.selector, warnForColor: true); + var selectorText = tuple.item1; + var selectorMap = tuple.item2; + if (_inKeyframes) { // NOTE: this logic is largely duplicated in [visitCssKeyframeBlock]. Most // changes here should be mirrored there. - var parsedSelector = _adjustParseError( - node.selector, - () => KeyframeSelectorParser(selectorText.value, logger: _logger) - .parse()); + var parsedSelector = KeyframeSelectorParser(selectorText, + logger: _logger, interpolationMap: selectorMap) + .parse(); var rule = ModifiableCssKeyframeBlock( CssValue(List.unmodifiable(parsedSelector), node.selector.span), node.span); @@ -1902,20 +1885,15 @@ class _EvaluateVisitor return null; } - var parsedSelector = _adjustParseError( - node.selector, - () => SelectorList.parse(selectorText.value, + var parsedSelector = SelectorList.parse(selectorText, + interpolationMap: selectorMap, allowParent: !_stylesheet.plainCss, allowPlaceholder: !_stylesheet.plainCss, - logger: _logger)); - parsedSelector = _addExceptionSpan( - node.selector, - () => parsedSelector.resolveParentSelectors( - _styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule)); - - var selector = _extensionStore.addSelector( - parsedSelector, node.selector.span, _mediaQueries); + logger: _logger) + .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule); + + var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: parsedSelector); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; @@ -1942,7 +1920,7 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - node.selector.span, + complex.span.trimRight(), deprecation: true); } else if (complex.leadingCombinators.isNotEmpty) { _warn( @@ -1950,7 +1928,7 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - node.selector.span, + complex.span.trimRight(), deprecation: true); } else { _warn( @@ -1964,7 +1942,7 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - MultiSpan(node.selector.span, 'invalid selector', { + 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)' @@ -2703,27 +2681,10 @@ class _EvaluateVisitor Value result; try { - result = await callback(evaluated.positional); - } on SassRuntimeException { + result = await _addExceptionSpanAsync( + nodeWithSpan, () => callback(evaluated.positional)); + } on SassException { rethrow; - } on MultiSpanSassScriptException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - nodeWithSpan.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(nodeWithSpan.span)), - stackTrace); - } on MultiSpanSassException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - error.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(error.span)), - stackTrace); } catch (error, stackTrace) { String? message; try { @@ -3100,11 +3061,10 @@ class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.value.resolveParentSelectors( + var originalSelector = node.selector.resolveParentSelectors( styleRule?.originalSelector, implicitParent: !_atRootExcludingStyleRule); - var selector = _extensionStore.addSelector( - originalSelector, node.selector.span, _mediaQueries); + var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: originalSelector); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; @@ -3206,10 +3166,42 @@ class _EvaluateVisitor /// values passed into the interpolation. Future _performInterpolation(Interpolation interpolation, {bool warnForColor = false}) async { + var tuple = await _performInterpolationHelper(interpolation, + sourceMap: true, warnForColor: warnForColor); + return tuple.item1; + } + + /// Like [_performInterpolation], but also returns a [InterpolationMap] that + /// can map spans from the resulting string back to the original + /// [interpolation]. + Future> _performInterpolationWithMap( + Interpolation interpolation, + {bool warnForColor = false}) async { + var tuple = await _performInterpolationHelper(interpolation, + sourceMap: true, warnForColor: warnForColor); + return Tuple2(tuple.item1, tuple.item2!); + } + + /// A helper that implements the core logic of both [_performInterpolation] + /// and [_performInterpolationWithMap]. + Future> _performInterpolationHelper( + Interpolation interpolation, + {required bool sourceMap, + bool warnForColor = false}) async { + var targetLocations = sourceMap ? [] : null; var oldInSupportsDeclaration = _inSupportsDeclaration; _inSupportsDeclaration = false; - var result = (await mapAsync(interpolation.contents, (value) async { - if (value is String) return value; + var buffer = StringBuffer(); + var first = true; + for (var value in interpolation.contents) { + if (!first) targetLocations?.add(SourceLocation(buffer.length)); + first = false; + + if (value is String) { + buffer.write(value); + continue; + } + var expression = value as Expression; var result = await expression.accept(this); @@ -3232,11 +3224,15 @@ class _EvaluateVisitor expression.span); } - return _serialize(result, expression, quote: false); - })) - .join(); + buffer.write(_serialize(result, expression, quote: false)); + } _inSupportsDeclaration = oldInSupportsDeclaration; - return result; + + return Tuple2( + buffer.toString(), + targetLocations == null + ? null + : InterpolationMap(interpolation, targetLocations)); } /// Evaluates [expression] and calls `toCssString()` and wraps a @@ -3452,73 +3448,53 @@ class _EvaluateVisitor MultiSpanSassRuntimeException(message, _stack.last.item2.span, primaryLabel, secondaryLabels, _stackTrace()); - /// Runs [callback], and adjusts any [SassFormatException] to be within - /// [nodeWithSpan]'s source span. - /// - /// Specifically, this adjusts format exceptions so that the errors are - /// reported as though the text being parsed were exactly in [span]. This may - /// not be quite accurate if the source text contained interpolation, but - /// it'll still produce a useful error. - /// - /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling - /// [AstNode.span] if the span isn't required, since some nodes need to do - /// real work to manufacture a source span. - T _adjustParseError(AstNode nodeWithSpan, T callback()) { - try { - return callback(); - } on SassFormatException catch (error, stackTrace) { - var errorText = error.span.file.getText(0); - var span = nodeWithSpan.span; - var syntheticFile = span.file - .getText(0) - .replaceRange(span.start.offset, span.end.offset, errorText); - var syntheticSpan = - SourceFile.fromString(syntheticFile, url: span.file.url).span( - span.start.offset + error.span.start.offset, - span.start.offset + error.span.end.offset); - throwWithTrace(_exception(error.message, syntheticSpan), stackTrace); - } - } - /// Runs [callback], and converts any [SassScriptException]s it throws to /// [SassRuntimeException]s with [nodeWithSpan]'s source span. /// /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. - T _addExceptionSpan(AstNode nodeWithSpan, T callback()) { + /// + /// 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(AstNode nodeWithSpan, T callback(), + {bool addStackFrame = true}) { try { return callback(); - } on MultiSpanSassScriptException catch (error, stackTrace) { + } on SassScriptException catch (error, stackTrace) { throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - nodeWithSpan.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(nodeWithSpan.span)), + error + .withSpan(nodeWithSpan.span) + .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)), stackTrace); - } on SassScriptException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, nodeWithSpan.span), stackTrace); } } /// Like [_addExceptionSpan], but for an asynchronous [callback]. Future _addExceptionSpanAsync( - AstNode nodeWithSpan, FutureOr callback()) async { + AstNode nodeWithSpan, FutureOr callback(), + {bool addStackFrame = true}) async { try { return await callback(); - } on MultiSpanSassScriptException catch (error, stackTrace) { + } on SassScriptException catch (error, stackTrace) { throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - nodeWithSpan.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(nodeWithSpan.span)), + error + .withSpan(nodeWithSpan.span) + .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)), stackTrace); - } on SassScriptException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, nodeWithSpan.span), stackTrace); + } + } + + /// Runs [callback], and converts any [SassException]s that aren't already + /// [SassRuntimeException]s to [SassRuntimeException]s with the current stack + /// trace. + Future _addExceptionTrace(FutureOr callback()) async { + try { + return await callback(); + } on SassRuntimeException { + rethrow; + } on SassException catch (error, stackTrace) { + throwWithTrace(error.withTrace(_stackTrace(error.span)), stackTrace); } } diff --git a/lib/src/visitor/clone_css.dart b/lib/src/visitor/clone_css.dart index 73b0f8b76..254f7f49c 100644 --- a/lib/src/visitor/clone_css.dart +++ b/lib/src/visitor/clone_css.dart @@ -8,6 +8,7 @@ import '../ast/css.dart'; import '../ast/css/modifiable.dart'; import '../ast/selector.dart'; import '../extend/extension_store.dart'; +import '../util/box.dart'; import 'interface/css.dart'; /// Returns deep copies of both [stylesheet] and [extender]. @@ -28,8 +29,7 @@ Tuple2 cloneCssStylesheet( class _CloneCssVisitor implements CssVisitor { /// A map from selectors in the original stylesheet to selectors generated for /// the new stylesheet using [ExtensionStore.clone]. - final Map, ModifiableCssValue> - _oldToNewSelectors; + final Map> _oldToNewSelectors; _CloneCssVisitor(this._oldToNewSelectors); diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 83ae6dfd8..e8b54a57f 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: 4cdc21090d758118f0250f6efb2e6bdb0df5f337 +// Checksum: 8945d2e2978c178b096a4bbcd3857572ec5ab1e0 // // ignore_for_file: unused_import @@ -40,6 +40,7 @@ import '../functions.dart'; import '../functions/meta.dart' as meta; import '../importer.dart'; import '../importer/legacy_node.dart'; +import '../interpolation_map.dart'; import '../io.dart'; import '../logger.dart'; import '../module.dart'; @@ -49,6 +50,7 @@ import '../syntax.dart'; import '../utils.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; +import '../util/span.dart'; import '../value.dart'; import 'expression_to_calc.dart'; import 'interface/css.dart'; @@ -530,7 +532,7 @@ class _EvaluateVisitor if (!(_asNodeSass && url.toString() == 'stdin')) _loadedUrls.add(url); } - var module = _execute(importer, node); + var module = _addExceptionTrace(() => _execute(importer, node)); return EvaluateResult(_combineCss(module), _loadedUrls); }); @@ -539,14 +541,14 @@ class _EvaluateVisitor Value runExpression(Importer? importer, Expression expression) => withEvaluationContext( _EvaluationContext(this, expression), - () => _withFakeStylesheet( - importer, expression, () => expression.accept(this))); + () => _withFakeStylesheet(importer, expression, + () => _addExceptionTrace(() => expression.accept(this)))); void runStatement(Importer? importer, Statement statement) => withEvaluationContext( _EvaluationContext(this, statement), - () => _withFakeStylesheet( - importer, statement, () => statement.accept(this))); + () => _withFakeStylesheet(importer, statement, + () => _addExceptionTrace(() => statement.accept(this)))); /// Asserts that [value] is not `null` and returns it. /// @@ -643,29 +645,8 @@ class _EvaluateVisitor _inDependency = oldInDependency; } - try { - callback(module); - } on SassRuntimeException { - rethrow; - } on MultiSpanSassException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - error.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(error.span)), - stackTrace); - } on SassException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, error.span), stackTrace); - } on MultiSpanSassScriptException catch (error, stackTrace) { - throwWithTrace( - _multiSpanException( - error.message, error.primaryLabel, error.secondarySpans), - stackTrace); - } on SassScriptException catch (error, stackTrace) { - throwWithTrace(_exception(error.message), stackTrace); - } + _addExceptionSpan(nodeWithSpan, () => callback(module), + addStackFrame: false); }); } @@ -945,9 +926,12 @@ class _EvaluateVisitor var query = AtRootQuery.defaultQuery; var unparsedQuery = node.query; if (unparsedQuery != null) { - var resolved = _performInterpolation(unparsedQuery, warnForColor: true); - query = _adjustParseError( - unparsedQuery, () => AtRootQuery.parse(resolved, logger: _logger)); + var tuple = + _performInterpolationWithMap(unparsedQuery, warnForColor: true); + var resolved = tuple.item1; + var map = tuple.item2; + query = + AtRootQuery.parse(resolved, interpolationMap: map, logger: _logger); } var parent = _parent; @@ -1223,19 +1207,17 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - MultiSpan(styleRule.selector.span, 'invalid selector', + MultiSpan(complex.span.trimRight(), 'invalid selector', {node.span: '@extend rule'}), deprecation: true); } - var targetText = _interpolationToValue(node.selector, warnForColor: true); + var tuple = _performInterpolationWithMap(node.selector, warnForColor: true); + var targetText = tuple.item1; + var targetMap = tuple.item2; - var list = _adjustParseError( - targetText, - () => SelectorList.parse( - trimAscii(targetText.value, excludeEscape: true), - logger: _logger, - allowParent: false)); + var list = SelectorList.parse(trimAscii(targetText, excludeEscape: true), + interpolationMap: targetMap, logger: _logger, allowParent: false); for (var complex in list.components) { var compound = complex.singleCompound; @@ -1243,7 +1225,7 @@ class _EvaluateVisitor // If the selector was a compound selector but not a simple // selector, emit a more explicit error. throw SassFormatException( - "complex selectors may not be extended.", targetText.span); + "complex selectors may not be extended.", complex.span); } var simple = compound.singleSimple; @@ -1252,7 +1234,7 @@ class _EvaluateVisitor "compound selectors may no longer be extended.\n" "Consider `@extend ${compound.components.join(', ')}` instead.\n" "See https://sass-lang.com/d/extend-compound for details.\n", - targetText.span); + compound.span); } _extensionStore.addExtension( @@ -1649,8 +1631,8 @@ class _EvaluateVisitor } else { throw "Can't find stylesheet to import."; } - } on SassException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, error.span), stackTrace); + } on SassException { + rethrow; } on ArgumentError catch (error, stackTrace) { throwWithTrace(_exception(error.toString()), stackTrace); } catch (error, stackTrace) { @@ -1832,11 +1814,11 @@ class _EvaluateVisitor /// Evaluates [interpolation] and parses the result as a list of media /// queries. List _visitMediaQueries(Interpolation interpolation) { - var resolved = _performInterpolation(interpolation, warnForColor: true); - - // TODO(nweiz): Remove this type argument when sdk#31398 is fixed. - return _adjustParseError>(interpolation, - () => CssMediaQuery.parseList(resolved, logger: _logger)); + var tuple = _performInterpolationWithMap(interpolation, warnForColor: true); + var resolved = tuple.item1; + var map = tuple.item2; + return CssMediaQuery.parseList(resolved, + logger: _logger, interpolationMap: map); } /// Returns a list of queries that selects for contexts that match both @@ -1873,16 +1855,17 @@ class _EvaluateVisitor "Style rules may not be used within nested declarations.", node.span); } - var selectorText = - _interpolationToValue(node.selector, trim: true, warnForColor: true); + var tuple = _performInterpolationWithMap(node.selector, warnForColor: true); + var selectorText = tuple.item1; + var selectorMap = tuple.item2; + if (_inKeyframes) { // NOTE: this logic is largely duplicated in [visitCssKeyframeBlock]. Most // changes here should be mirrored there. - var parsedSelector = _adjustParseError( - node.selector, - () => KeyframeSelectorParser(selectorText.value, logger: _logger) - .parse()); + var parsedSelector = KeyframeSelectorParser(selectorText, + logger: _logger, interpolationMap: selectorMap) + .parse(); var rule = ModifiableCssKeyframeBlock( CssValue(List.unmodifiable(parsedSelector), node.selector.span), node.span); @@ -1896,20 +1879,15 @@ class _EvaluateVisitor return null; } - var parsedSelector = _adjustParseError( - node.selector, - () => SelectorList.parse(selectorText.value, + var parsedSelector = SelectorList.parse(selectorText, + interpolationMap: selectorMap, allowParent: !_stylesheet.plainCss, allowPlaceholder: !_stylesheet.plainCss, - logger: _logger)); - parsedSelector = _addExceptionSpan( - node.selector, - () => parsedSelector.resolveParentSelectors( - _styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule)); - - var selector = _extensionStore.addSelector( - parsedSelector, node.selector.span, _mediaQueries); + logger: _logger) + .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule); + + var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: parsedSelector); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; @@ -1936,7 +1914,7 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - node.selector.span, + complex.span.trimRight(), deprecation: true); } else if (complex.leadingCombinators.isNotEmpty) { _warn( @@ -1944,7 +1922,7 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - node.selector.span, + complex.span.trimRight(), deprecation: true); } else { _warn( @@ -1958,7 +1936,7 @@ class _EvaluateVisitor 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - MultiSpan(node.selector.span, 'invalid selector', { + 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)' @@ -2686,27 +2664,10 @@ class _EvaluateVisitor Value result; try { - result = callback(evaluated.positional); - } on SassRuntimeException { + result = + _addExceptionSpan(nodeWithSpan, () => callback(evaluated.positional)); + } on SassException { rethrow; - } on MultiSpanSassScriptException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - nodeWithSpan.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(nodeWithSpan.span)), - stackTrace); - } on MultiSpanSassException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - error.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(error.span)), - stackTrace); } catch (error, stackTrace) { String? message; try { @@ -3076,11 +3037,10 @@ class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.value.resolveParentSelectors( + var originalSelector = node.selector.resolveParentSelectors( styleRule?.originalSelector, implicitParent: !_atRootExcludingStyleRule); - var selector = _extensionStore.addSelector( - originalSelector, node.selector.span, _mediaQueries); + var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: originalSelector); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; @@ -3179,10 +3139,42 @@ class _EvaluateVisitor /// values passed into the interpolation. String _performInterpolation(Interpolation interpolation, {bool warnForColor = false}) { + var tuple = _performInterpolationHelper(interpolation, + sourceMap: true, warnForColor: warnForColor); + return tuple.item1; + } + + /// Like [_performInterpolation], but also returns a [InterpolationMap] that + /// can map spans from the resulting string back to the original + /// [interpolation]. + Tuple2 _performInterpolationWithMap( + Interpolation interpolation, + {bool warnForColor = false}) { + var tuple = _performInterpolationHelper(interpolation, + sourceMap: true, warnForColor: warnForColor); + return Tuple2(tuple.item1, tuple.item2!); + } + + /// A helper that implements the core logic of both [_performInterpolation] + /// and [_performInterpolationWithMap]. + Tuple2 _performInterpolationHelper( + Interpolation interpolation, + {required bool sourceMap, + bool warnForColor = false}) { + var targetLocations = sourceMap ? [] : null; var oldInSupportsDeclaration = _inSupportsDeclaration; _inSupportsDeclaration = false; - var result = interpolation.contents.map((value) { - if (value is String) return value; + var buffer = StringBuffer(); + var first = true; + for (var value in interpolation.contents) { + if (!first) targetLocations?.add(SourceLocation(buffer.length)); + first = false; + + if (value is String) { + buffer.write(value); + continue; + } + var expression = value as Expression; var result = expression.accept(this); @@ -3205,10 +3197,15 @@ class _EvaluateVisitor expression.span); } - return _serialize(result, expression, quote: false); - }).join(); + buffer.write(_serialize(result, expression, quote: false)); + } _inSupportsDeclaration = oldInSupportsDeclaration; - return result; + + return Tuple2( + buffer.toString(), + targetLocations == null + ? null + : InterpolationMap(interpolation, targetLocations)); } /// Evaluates [expression] and calls `toCssString()` and wraps a @@ -3420,54 +3417,38 @@ class _EvaluateVisitor MultiSpanSassRuntimeException(message, _stack.last.item2.span, primaryLabel, secondaryLabels, _stackTrace()); - /// Runs [callback], and adjusts any [SassFormatException] to be within - /// [nodeWithSpan]'s source span. - /// - /// Specifically, this adjusts format exceptions so that the errors are - /// reported as though the text being parsed were exactly in [span]. This may - /// not be quite accurate if the source text contained interpolation, but - /// it'll still produce a useful error. + /// Runs [callback], and converts any [SassScriptException]s it throws to + /// [SassRuntimeException]s with [nodeWithSpan]'s source span. /// /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. - T _adjustParseError(AstNode nodeWithSpan, T callback()) { + /// + /// 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(AstNode nodeWithSpan, T callback(), + {bool addStackFrame = true}) { try { return callback(); - } on SassFormatException catch (error, stackTrace) { - var errorText = error.span.file.getText(0); - var span = nodeWithSpan.span; - var syntheticFile = span.file - .getText(0) - .replaceRange(span.start.offset, span.end.offset, errorText); - var syntheticSpan = - SourceFile.fromString(syntheticFile, url: span.file.url).span( - span.start.offset + error.span.start.offset, - span.start.offset + error.span.end.offset); - throwWithTrace(_exception(error.message, syntheticSpan), stackTrace); + } on SassScriptException catch (error, stackTrace) { + throwWithTrace( + error + .withSpan(nodeWithSpan.span) + .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)), + stackTrace); } } - /// Runs [callback], and converts any [SassScriptException]s it throws to - /// [SassRuntimeException]s with [nodeWithSpan]'s source span. - /// - /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling - /// [AstNode.span] if the span isn't required, since some nodes need to do - /// real work to manufacture a source span. - T _addExceptionSpan(AstNode nodeWithSpan, T callback()) { + /// Runs [callback], and converts any [SassException]s that aren't already + /// [SassRuntimeException]s to [SassRuntimeException]s with the current stack + /// trace. + T _addExceptionTrace(T callback()) { try { return callback(); - } on MultiSpanSassScriptException catch (error, stackTrace) { - throwWithTrace( - MultiSpanSassRuntimeException( - error.message, - nodeWithSpan.span, - error.primaryLabel, - error.secondarySpans, - _stackTrace(nodeWithSpan.span)), - stackTrace); - } on SassScriptException catch (error, stackTrace) { - throwWithTrace(_exception(error.message, nodeWithSpan.span), stackTrace); + } on SassRuntimeException { + rethrow; + } on SassException catch (error, stackTrace) { + throwWithTrace(error.withTrace(_stackTrace(error.span)), stackTrace); } } diff --git a/lib/src/visitor/selector_search.dart b/lib/src/visitor/selector_search.dart new file mode 100644 index 000000000..f87b38d3b --- /dev/null +++ b/lib/src/visitor/selector_search.dart @@ -0,0 +1,37 @@ +// Copyright 2023 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import '../ast/selector.dart'; +import '../util/nullable.dart'; +import '../utils.dart'; +import 'interface/selector.dart'; + +/// A [SelectorVisitor] whose `visit*` methods default to returning `null`, but +/// which returns the first non-`null` value returned by any method. +/// +/// This can be extended to find the first instance of particular nodes in the +/// AST. +/// +/// {@category Visitor} +mixin SelectorSearchVisitor implements SelectorVisitor { + T? visitAttributeSelector(AttributeSelector attribute) => null; + T? visitClassSelector(ClassSelector klass) => null; + T? visitIDSelector(IDSelector id) => null; + T? visitParentSelector(ParentSelector placeholder) => null; + T? visitPlaceholderSelector(PlaceholderSelector placeholder) => null; + T? visitTypeSelector(TypeSelector type) => null; + T? visitUniversalSelector(UniversalSelector universal) => null; + + T? visitComplexSelector(ComplexSelector complex) => complex.components + .search((component) => visitCompoundSelector(component.selector)); + + T? visitCompoundSelector(CompoundSelector compound) => + compound.components.search((simple) => simple.accept(this)); + + T? visitPseudoSelector(PseudoSelector pseudo) => + pseudo.selector.andThen(visitSelectorList); + + T? visitSelectorList(SelectorList list) => + list.components.search(visitComplexSelector); +} diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 1cd98b593..562945042 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -314,7 +314,7 @@ class _SerializeVisitor void visitCssStyleRule(CssStyleRule node) { _writeIndentation(); - _for(node.selector, () => node.selector.value.accept(this)); + _for(node.selector, () => node.selector.accept(this)); _writeOptionalSpace(); _visitChildren(node); } @@ -1209,7 +1209,7 @@ class _SerializeVisitor /// Writes [combinators] to [_buffer], with spaces in between in expanded /// mode. - void _writeCombinators(List combinators) => + void _writeCombinators(List> combinators) => _writeBetween(combinators, _isCompressed ? '' : ' ', _buffer.write); void visitCompoundSelector(CompoundSelector compound) { diff --git a/lib/src/visitor/statement_search.dart b/lib/src/visitor/statement_search.dart index c1651b80e..791b8689a 100644 --- a/lib/src/visitor/statement_search.dart +++ b/lib/src/visitor/statement_search.dart @@ -6,6 +6,7 @@ import 'package:meta/meta.dart'; import '../ast/sass.dart'; import '../util/nullable.dart'; +import '../utils.dart'; import 'interface/statement.dart'; import 'recursive_statement.dart'; @@ -44,10 +45,10 @@ mixin StatementSearchVisitor implements StatementVisitor { T? visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node); T? visitIfRule(IfRule node) => - node.clauses._search( - (clause) => clause.children._search((child) => child.accept(this))) ?? + node.clauses.search( + (clause) => clause.children.search((child) => child.accept(this))) ?? node.lastClause.andThen((lastClause) => - lastClause.children._search((child) => child.accept(this))); + lastClause.children.search((child) => child.accept(this))); T? visitImportRule(ImportRule node) => null; @@ -92,17 +93,5 @@ mixin StatementSearchVisitor implements StatementVisitor { /// call this. @protected T? visitChildren(List children) => - children._search((child) => child.accept(this)); -} - -extension _IterableExtension on Iterable { - /// Returns the first `T` returned by [callback] for an element of [iterable], - /// or `null` if it returns `null` for every element. - T? _search(T? Function(E element) callback) { - for (var element in this) { - var value = callback(element); - if (value != null) return value; - } - return null; - } + children.search((child) => child.accept(this)); } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 9f4b7f101..1aaf8b7ad 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,23 @@ +## 6.0.0 + +* **Breaking change:** All selector AST node constructors now require a + `FileSpan` and expose a `span` field. + +* **Breaking change:** The `CssStyleRule.selector` field is now a plain + `SelectorList` rather than a `CssValue`. + +* **Breaking change:** The `ModifiableCssValue` class has been removed. + +* Add an `InterpolationMap` class which represents a mapping from an + interpolation's source to the string it generated. + +* Add an `interpolationMap` parameter to `CssMediaQuery.parseList()`, + `AtRootQuery.parse()`, `ComplexSelector.parse`, `CompoundSelector.parse`, + `ListSelector.parse`, and `SimpleSelector.parse`. + +* Add a `SelectorSearchVisitor` mixin, which can be used to return the first + instance of a selector in an AST matching a certain criterion. + ## 5.1.1 * No user-visible changes. diff --git a/pkg/sass_api/lib/sass_api.dart b/pkg/sass_api/lib/sass_api.dart index 21c82fda1..1f4b076e3 100644 --- a/pkg/sass_api/lib/sass_api.dart +++ b/pkg/sass_api/lib/sass_api.dart @@ -17,6 +17,7 @@ export 'package:sass/src/ast/selector.dart'; export 'package:sass/src/async_import_cache.dart'; export 'package:sass/src/exception.dart' show SassFormatException; export 'package:sass/src/import_cache.dart'; +export 'package:sass/src/interpolation_map.dart'; export 'package:sass/src/value.dart' hide ColorFormat, SpanColorFormat; export 'package:sass/src/visitor/find_dependencies.dart'; export 'package:sass/src/visitor/interface/expression.dart'; @@ -26,6 +27,7 @@ export 'package:sass/src/visitor/recursive_ast.dart'; export 'package:sass/src/visitor/recursive_selector.dart'; export 'package:sass/src/visitor/recursive_statement.dart'; export 'package:sass/src/visitor/replace_expression.dart'; +export 'package:sass/src/visitor/selector_search.dart'; export 'package:sass/src/visitor/statement_search.dart'; /// Parses [text] as a CSS identifier and returns the result. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 74aac70d4..d83a4f685 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 5.1.1 +version: 6.0.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass