From 664a670ab84eae0e59845bfd3cacbc208b6046b3 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 28 Feb 2023 13:38:42 -0800 Subject: [PATCH 1/8] Add a changelog entry for sass/embedded-host-node#207 (#1898) --- CHANGELOG.md | 6 ++++++ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cae5bed9..11998e4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.58.4 + +### Embedded Sass + +* Improve the performance of starting up a compilation. + ## 1.58.3 * No user-visible changes. diff --git a/pubspec.yaml b/pubspec.yaml index c6b6118de..9687577c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.58.3 +version: 1.58.4-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From eb1ced15d6cd66078526e7989576a704f55e71b2 Mon Sep 17 00:00:00 2001 From: Goodwine <2022649+Goodwine@users.noreply.github.com> Date: Wed, 1 Mar 2023 18:12:11 -0800 Subject: [PATCH 2/8] Pull @font-face out to root (#1899) Closes sass/sass#1251 --- CHANGELOG.md | 3 +++ lib/src/visitor/async_evaluate.dart | 4 +++- lib/src/visitor/evaluate.dart | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11998e4ee..81814ec23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.58.4 +* Pull `@font-face` to the root rather than bubbling the style rule selector + inwards. + ### Embedded Sass * Improve the performance of starting up a compilation. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 7eb5f4f7f..38c1c1b23 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1292,7 +1292,9 @@ class _EvaluateVisitor await _withParent(ModifiableCssAtRule(name, node.span, value: value), () async { var styleRule = _styleRule; - if (styleRule == null || _inKeyframes) { + if (styleRule == null || _inKeyframes || name.value == 'font-face') { + // Special-cased at-rules within style blocks are pulled out to the + // root. Equivalent to prepending "@at-root" on them. for (var child in children) { await child.accept(this); } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 45d5986aa..d9e6dbf21 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: d84fe267879d0fb034853a0a8a5105b2919916ec +// Checksum: 73b7fb0f310d090dee2b3383f7b08c095e5fb1c0 // // ignore_for_file: unused_import @@ -1293,7 +1293,9 @@ class _EvaluateVisitor _withParent(ModifiableCssAtRule(name, node.span, value: value), () { var styleRule = _styleRule; - if (styleRule == null || _inKeyframes) { + if (styleRule == null || _inKeyframes || name.value == 'font-face') { + // Special-cased at-rules within style blocks are pulled out to the + // root. Equivalent to prepending "@at-root" on them. for (var child in children) { child.accept(this); } From f022e02bc5dfcd918a8c4dde7b2ac086bbd6557b Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 3 Mar 2023 15:07:15 -0800 Subject: [PATCH 3/8] Improve errors for invalid CSS values passed to CSS functions (#1901) Closes #1769 --- CHANGELOG.md | 2 ++ lib/src/visitor/async_evaluate.dart | 38 +++++++++++++++++---------- lib/src/visitor/evaluate.dart | 40 ++++++++++++++++++----------- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81814ec23..bc6ef13e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * Pull `@font-face` to the root rather than bubbling the style rule selector inwards. +* Improve error messages for invalid CSS values passed to plain CSS functions. + ### Embedded Sass * Improve the performance of starting up a compilation. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 38c1c1b23..11d1356e2 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2622,22 +2622,32 @@ class _EvaluateVisitor } var buffer = StringBuffer("${callable.name}("); - var first = true; - for (var argument in arguments.positional) { - if (first) { - first = false; - } else { - buffer.write(", "); - } + try { + var first = true; + for (var argument in arguments.positional) { + if (first) { + first = false; + } else { + buffer.write(", "); + } - buffer.write(await _evaluateToCss(argument)); - } + buffer.write(await _evaluateToCss(argument)); + } - var restArg = arguments.rest; - if (restArg != null) { - var rest = await restArg.accept(this); - if (!first) buffer.write(", "); - buffer.write(_serialize(rest, restArg)); + var restArg = arguments.rest; + if (restArg != null) { + var rest = await restArg.accept(this); + if (!first) buffer.write(", "); + buffer.write(_serialize(rest, restArg)); + } + } on SassRuntimeException catch (error) { + if (!error.message.endsWith("isn't a valid CSS value.")) rethrow; + throw MultiSpanSassRuntimeException( + error.message, + error.span, + "value", + {nodeWithSpan.span: "unknown function treated as plain CSS"}, + error.trace); } buffer.writeCharCode($rparen); diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index d9e6dbf21..83ae6dfd8 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: 73b7fb0f310d090dee2b3383f7b08c095e5fb1c0 +// Checksum: 4cdc21090d758118f0250f6efb2e6bdb0df5f337 // // ignore_for_file: unused_import @@ -2605,22 +2605,32 @@ class _EvaluateVisitor } var buffer = StringBuffer("${callable.name}("); - var first = true; - for (var argument in arguments.positional) { - if (first) { - first = false; - } else { - buffer.write(", "); - } + try { + var first = true; + for (var argument in arguments.positional) { + if (first) { + first = false; + } else { + buffer.write(", "); + } - buffer.write(_evaluateToCss(argument)); - } + buffer.write(_evaluateToCss(argument)); + } - var restArg = arguments.rest; - if (restArg != null) { - var rest = restArg.accept(this); - if (!first) buffer.write(", "); - buffer.write(_serialize(rest, restArg)); + var restArg = arguments.rest; + if (restArg != null) { + var rest = restArg.accept(this); + if (!first) buffer.write(", "); + buffer.write(_serialize(rest, restArg)); + } + } on SassRuntimeException catch (error) { + if (!error.message.endsWith("isn't a valid CSS value.")) rethrow; + throw MultiSpanSassRuntimeException( + error.message, + error.span, + "value", + {nodeWithSpan.span: "unknown function treated as plain CSS"}, + error.trace); } buffer.writeCharCode($rparen); From 434f2b99f154c14dc5754ed1566d1b788a3e126a Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 6 Mar 2023 13:18:25 -0800 Subject: [PATCH 4/8] Remove workaround for dart-lang/setup-dart#59 (#1904) --- .github/workflows/ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdb81411d..76c69496d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -332,20 +332,14 @@ jobs: include: - runner: macos-latest platform: macos-x64 - architecture: x64 - runner: self-hosted platform: macos-arm64 - architecture: arm64 - runner: windows-latest platform: windows - architecture: x64 steps: - uses: actions/checkout@v3 - uses: dart-lang/setup-dart@v1 - # Workaround for dart-lang/setup-dart#59 - with: - architecture: ${{ matrix.architecture }} - run: dart pub get - name: Deploy run: dart run grinder pkg-github-${{ matrix.platform }} From 9417b6e8d8c4fdd0453bbbef3ac259cca65a0f36 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 8 Mar 2023 14:59:12 -0800 Subject: [PATCH 5/8] Track original source spans for selectors (#1903) Closes #1783 --- CHANGELOG.md | 2 + lib/src/ast/css/media_query.dart | 7 +- lib/src/ast/css/modifiable.dart | 1 - lib/src/ast/css/modifiable/style_rule.dart | 17 +- lib/src/ast/css/modifiable/value.dart | 17 -- lib/src/ast/css/node.dart | 4 +- lib/src/ast/css/style_rule.dart | 3 +- lib/src/ast/css/value.dart | 7 +- lib/src/ast/sass/at_root_query.dart | 7 +- lib/src/ast/selector.dart | 10 +- lib/src/ast/selector/attribute.dart | 11 +- lib/src/ast/selector/class.dart | 5 +- lib/src/ast/selector/complex.dart | 33 ++- lib/src/ast/selector/complex_component.dart | 13 +- lib/src/ast/selector/compound.dart | 6 +- lib/src/ast/selector/id.dart | 5 +- lib/src/ast/selector/list.dart | 169 +++++++----- lib/src/ast/selector/parent.dart | 3 +- lib/src/ast/selector/placeholder.dart | 5 +- lib/src/ast/selector/pseudo.dart | 16 +- lib/src/ast/selector/simple.dart | 7 +- lib/src/ast/selector/type.dart | 5 +- lib/src/ast/selector/universal.dart | 3 +- lib/src/exception.dart | 67 +++++ lib/src/extend/empty_extension_store.dart | 15 +- lib/src/extend/extender.dart | 60 ----- lib/src/extend/extension.dart | 12 +- lib/src/extend/extension_store.dart | 176 ++++++------- lib/src/extend/functions.dart | 203 +++++++++------ lib/src/extend/merged_extension.dart | 3 +- lib/src/functions/selector.dart | 17 +- lib/src/interpolation_map.dart | 180 +++++++++++++ lib/src/parse/at_root_query.dart | 7 +- lib/src/parse/keyframe_selector.dart | 7 +- lib/src/parse/media_query.dart | 7 +- lib/src/parse/parser.dart | 84 ++++-- lib/src/parse/selector.dart | 86 ++++-- lib/src/util/box.dart | 34 +++ lib/src/util/span.dart | 4 + lib/src/utils.dart | 10 + lib/src/visitor/async_evaluate.dart | 274 +++++++++----------- lib/src/visitor/clone_css.dart | 4 +- lib/src/visitor/evaluate.dart | 251 +++++++++--------- lib/src/visitor/selector_search.dart | 37 +++ lib/src/visitor/serialize.dart | 4 +- lib/src/visitor/statement_search.dart | 21 +- pkg/sass_api/CHANGELOG.md | 20 ++ pkg/sass_api/lib/sass_api.dart | 2 + pkg/sass_api/pubspec.yaml | 2 +- 49 files changed, 1187 insertions(+), 756 deletions(-) delete mode 100644 lib/src/ast/css/modifiable/value.dart delete mode 100644 lib/src/extend/extender.dart create mode 100644 lib/src/interpolation_map.dart create mode 100644 lib/src/util/box.dart create mode 100644 lib/src/visitor/selector_search.dart 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 From 8f8138dfabbf48437d736b9bba7f481fac71ed3c Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Fri, 10 Mar 2023 14:24:33 -0800 Subject: [PATCH 6/8] Add --fatal-deprecations and --future-deprecations (#1820) * Add fatal/futureDeprecations to Dart API This adds a new `Deprecation` class that specifies an ID for each deprecated feature along with what Dart Sass version deprecated it. The compile functions allow you to pass a set of `fatalDeprecations` that will cause an error instead of a warning. You can also pass a set of `futureDeprecations`, which let you opt-in to deprecations (like `@import`) early. * Add future deprecation for `@import` * Add flags * Merge colorUnits and randomWithUnits * Update changelogs and pubspecs * Add tests * Use isFuture instead of deprecatedIn == null * Split warnForDeprecation from warn * Add missing word to error message --- CHANGELOG.md | 22 ++++ bin/sass.dart | 14 ++- lib/sass.dart | 32 +++-- lib/src/ast/selector.dart | 5 +- lib/src/async_compile.dart | 29 +++-- lib/src/async_import_cache.dart | 5 +- lib/src/compile.dart | 31 +++-- lib/src/deprecation.dart | 118 ++++++++++++++++++ lib/src/evaluation_context.dart | 17 ++- lib/src/executable/compile_stylesheet.dart | 16 ++- lib/src/executable/options.dart | 73 ++++++++++- lib/src/functions/color.dart | 33 ++--- lib/src/functions/math.dart | 5 +- lib/src/import_cache.dart | 7 +- lib/src/interpolation_map.dart | 6 +- lib/src/logger.dart | 19 +++ lib/src/logger/deprecation_handling.dart | 102 +++++++++++++++ lib/src/logger/terse.dart | 58 --------- lib/src/parse/scss.dart | 7 +- lib/src/parse/stylesheet.dart | 21 +++- lib/src/value.dart | 5 +- lib/src/visitor/async_evaluate.dart | 34 ++--- lib/src/visitor/evaluate.dart | 36 +++--- pkg/sass_api/pubspec.yaml | 2 +- pubspec.yaml | 4 +- test/cli/shared.dart | 66 ++++++++++ test/deprecations_test.dart | 137 +++++++++++++++++++++ 27 files changed, 731 insertions(+), 173 deletions(-) create mode 100644 lib/src/deprecation.dart create mode 100644 lib/src/logger/deprecation_handling.dart delete mode 100644 lib/src/logger/terse.dart create mode 100644 test/deprecations_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e116bd4..9bb9bca8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## 1.59.0 + +### Command Line Interface + +* Added a new `--fatal-deprecation` flag that lets you treat a deprecation + warning as an error. You can pass an individual deprecation ID + (e.g. `slash-div`) or you can pass a Dart Sass version to treat all + deprecations initially emitted in that version or earlier as errors. + +* New `--future-deprecation` flag that lets you opt into warning for use of + certain features that will be deprecated in the future. At the moment, the + only option is `--future-deprecation=import`, which will emit warnings for + Sass `@import` rules, which are not yet deprecated, but will be in the future. + +### Dart API + +* New `Deprecation` enum, which contains the different current and future + deprecations used by the new CLI flags. + +* The `compile` methods now take in `fatalDeprecations` and `futureDeprecations` + parameters, which work similarly to the CLI flags. + ## 1.58.4 * Pull `@font-face` to the root rather than bubbling the style rule selector diff --git a/bin/sass.dart b/bin/sass.dart index 9114fcf5c..9ca390a96 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -15,6 +15,7 @@ import 'package:sass/src/executable/repl.dart'; import 'package:sass/src/executable/watch.dart'; import 'package:sass/src/import_cache.dart'; import 'package:sass/src/io.dart'; +import 'package:sass/src/logger/deprecation_handling.dart'; import 'package:sass/src/stylesheet_graph.dart'; import 'package:sass/src/utils.dart'; @@ -52,8 +53,17 @@ Future main(List args) async { return; } - var graph = StylesheetGraph( - ImportCache(loadPaths: options.loadPaths, logger: options.logger)); + var graph = StylesheetGraph(ImportCache( + loadPaths: options.loadPaths, + // This logger is only used for handling fatal/future deprecations + // during parsing, and is re-used across parses, so we don't want to + // limit repetition. A separate DeprecationHandlingLogger is created for + // each compilation, which will limit repetition if verbose is not + // passed in addition to handling fatal/future deprecations. + logger: DeprecationHandlingLogger(options.logger, + fatalDeprecations: options.fatalDeprecations, + futureDeprecations: options.futureDeprecations, + limitRepetition: false))); if (options.watch) { await watch(options, graph); return; diff --git a/lib/sass.dart b/lib/sass.dart index d29e53824..1c2acc016 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -13,6 +13,7 @@ import 'src/async_import_cache.dart'; import 'src/callable.dart'; import 'src/compile.dart' as c; import 'src/compile_result.dart'; +import 'src/deprecation.dart'; import 'src/exception.dart'; import 'src/import_cache.dart'; import 'src/importer.dart'; @@ -24,9 +25,10 @@ import 'src/visitor/serialize.dart'; export 'src/callable.dart' show Callable, AsyncCallable; export 'src/compile_result.dart'; +export 'src/deprecation.dart'; export 'src/exception.dart' show SassException; export 'src/importer.dart'; -export 'src/logger.dart'; +export 'src/logger.dart' show Logger; export 'src/syntax.dart'; export 'src/value.dart' hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat; @@ -105,7 +107,9 @@ CompileResult compileToResult(String path, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) => + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) => c.compile(path, logger: logger, importCache: ImportCache( @@ -118,7 +122,9 @@ CompileResult compileToResult(String path, quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, - charset: charset); + charset: charset, + fatalDeprecations: fatalDeprecations, + futureDeprecations: futureDeprecations); /// Compiles [source] to CSS and returns a [CompileResult] containing the CSS /// and additional metadata about the compilation.. @@ -200,7 +206,9 @@ CompileResult compileStringToResult(String source, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) => + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) => c.compileString(source, syntax: syntax, logger: logger, @@ -216,7 +224,9 @@ CompileResult compileStringToResult(String source, quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, - charset: charset); + charset: charset, + fatalDeprecations: fatalDeprecations, + futureDeprecations: futureDeprecations); /// Like [compileToResult], except it runs asynchronously. /// @@ -234,7 +244,9 @@ Future compileToResultAsync(String path, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) => + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) => c.compileAsync(path, logger: logger, importCache: AsyncImportCache( @@ -247,7 +259,9 @@ Future compileToResultAsync(String path, quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, - charset: charset); + charset: charset, + fatalDeprecations: fatalDeprecations, + futureDeprecations: futureDeprecations); /// Like [compileStringToResult], except it runs asynchronously. /// @@ -270,7 +284,9 @@ Future compileStringToResultAsync(String source, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) => + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) => c.compileStringAsync(source, syntax: syntax, logger: logger, diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart index 0af8ac0b3..35c5a2f54 100644 --- a/lib/src/ast/selector.dart +++ b/lib/src/ast/selector.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; +import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../visitor/any_selector.dart'; @@ -88,13 +89,13 @@ abstract class Selector implements AstNode { /// throw a [SassException] in Dart Sass 2.0.0. void assertNotBogus({String? name}) { if (!isBogus) return; - warn( + warnForDeprecation( (name == null ? '' : '\$$name: ') + '$this is not valid CSS.\n' 'This will be an error in Dart Sass 2.0.0.\n' '\n' 'More info: https://sass-lang.com/d/bogus-combinators', - deprecation: true); + Deprecation.bogusCombinators); } /// Calls the appropriate visit method on [visitor]. diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index d01dfcc40..88c32282e 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -10,11 +10,12 @@ import 'ast/sass.dart'; import 'async_import_cache.dart'; import 'callable.dart'; import 'compile_result.dart'; +import 'deprecation.dart'; import 'importer.dart'; import 'importer/legacy_node.dart'; import 'io.dart'; import 'logger.dart'; -import 'logger/terse.dart'; +import 'logger/deprecation_handling.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/async_evaluate.dart'; @@ -37,9 +38,14 @@ Future compileAsync(String path, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) async { - TerseLogger? terseLogger; - if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) async { + DeprecationHandlingLogger deprecationLogger = logger = + DeprecationHandlingLogger(logger ?? Logger.stderr(), + fatalDeprecations: {...?fatalDeprecations}, + futureDeprecations: {...?futureDeprecations}, + limitRepetition: !verbose); // If the syntax is different than the importer would default to, we have to // parse the file manually and we can't store it in the cache. @@ -71,7 +77,7 @@ Future compileAsync(String path, sourceMap, charset); - terseLogger?.summarize(node: nodeImporter != null); + deprecationLogger.summarize(node: nodeImporter != null); return result; } @@ -96,9 +102,14 @@ Future compileStringAsync(String source, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) async { - TerseLogger? terseLogger; - if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) async { + DeprecationHandlingLogger deprecationLogger = logger = + DeprecationHandlingLogger(logger ?? Logger.stderr(), + fatalDeprecations: {...?fatalDeprecations}, + futureDeprecations: {...?futureDeprecations}, + limitRepetition: !verbose); var stylesheet = Stylesheet.parse(source, syntax ?? Syntax.scss, url: url, logger: logger); @@ -118,7 +129,7 @@ Future compileStringAsync(String source, sourceMap, charset); - terseLogger?.summarize(node: nodeImporter != null); + deprecationLogger.summarize(node: nodeImporter != null); return result; } diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 7139b3b75..209212313 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -9,6 +9,7 @@ import 'package:path/path.dart' as p; import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; +import 'deprecation.dart'; import 'importer.dart'; import 'importer/utils.dart'; import 'io.dart'; @@ -154,10 +155,10 @@ class AsyncImportCache { ? inImportRule(() => importer.canonicalize(url)) : importer.canonicalize(url)); if (result?.scheme == '') { - _logger.warn(""" + _logger.warnForDeprecation(Deprecation.relativeCanonical, """ Importer $importer canonicalized $url to $result. Relative canonical URLs are deprecated and will eventually be disallowed. -""", deprecation: true); +"""); } return result; } diff --git a/lib/src/compile.dart b/lib/src/compile.dart index 8e4f650cb..9e7835879 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: f8b5bf7eafbe3523ca4df1a6832e131c5c03986b +// Checksum: 628fbfe8a6717cca332dd646eeda2260dd3e30c6 // // ignore_for_file: unused_import @@ -19,11 +19,12 @@ import 'ast/sass.dart'; import 'import_cache.dart'; import 'callable.dart'; import 'compile_result.dart'; +import 'deprecation.dart'; import 'importer.dart'; import 'importer/legacy_node.dart'; import 'io.dart'; import 'logger.dart'; -import 'logger/terse.dart'; +import 'logger/deprecation_handling.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/evaluate.dart'; @@ -46,9 +47,14 @@ CompileResult compile(String path, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) { - TerseLogger? terseLogger; - if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) { + DeprecationHandlingLogger deprecationLogger = logger = + DeprecationHandlingLogger(logger ?? Logger.stderr(), + fatalDeprecations: {...?fatalDeprecations}, + futureDeprecations: {...?futureDeprecations}, + limitRepetition: !verbose); // If the syntax is different than the importer would default to, we have to // parse the file manually and we can't store it in the cache. @@ -80,7 +86,7 @@ CompileResult compile(String path, sourceMap, charset); - terseLogger?.summarize(node: nodeImporter != null); + deprecationLogger.summarize(node: nodeImporter != null); return result; } @@ -105,9 +111,14 @@ CompileResult compileString(String source, bool quietDeps = false, bool verbose = false, bool sourceMap = false, - bool charset = true}) { - TerseLogger? terseLogger; - if (!verbose) logger = terseLogger = TerseLogger(logger ?? Logger.stderr()); + bool charset = true, + Iterable? fatalDeprecations, + Iterable? futureDeprecations}) { + DeprecationHandlingLogger deprecationLogger = logger = + DeprecationHandlingLogger(logger ?? Logger.stderr(), + fatalDeprecations: {...?fatalDeprecations}, + futureDeprecations: {...?futureDeprecations}, + limitRepetition: !verbose); var stylesheet = Stylesheet.parse(source, syntax ?? Syntax.scss, url: url, logger: logger); @@ -127,7 +138,7 @@ CompileResult compileString(String source, sourceMap, charset); - terseLogger?.summarize(node: nodeImporter != null); + deprecationLogger.summarize(node: nodeImporter != null); return result; } diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart new file mode 100644 index 000000000..25526c510 --- /dev/null +++ b/lib/src/deprecation.dart @@ -0,0 +1,118 @@ +// Copyright 2022 Google LLC. 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:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import 'util/nullable.dart'; + +/// A deprecated feature in the language. +enum Deprecation { + /// Deprecation for passing a string to `call` instead of `get-function`. + callString('call-string', + deprecatedIn: '0.0.0', + description: 'Passing a string directly to meta.call().'), + + /// Deprecation for `@elseif`. + elseif('elseif', deprecatedIn: '1.3.2', description: '@elseif.'), + + /// Deprecation for parsing `@-moz-document`. + mozDocument('moz-document', + deprecatedIn: '1.7.2', description: '@-moz-document.'), + + /// Deprecation for importers using relative canonical URLs. + relativeCanonical('relative-canonical', deprecatedIn: '1.14.2'), + + /// Deprecation for declaring new variables with `!global`. + newGlobal('new-global', + deprecatedIn: '1.17.2', + description: 'Declaring new variables with !global.'), + + /// Deprecation for certain functions in the color module matching the + /// behavior of their global counterparts for compatiblity reasons. + colorModuleCompat('color-module-compat', + deprecatedIn: '1.23.0', + description: + 'Using color module functions in place of plain CSS functions.'), + + /// Deprecation for treating `/` as division. + slashDiv('slash-div', + deprecatedIn: '1.33.0', description: '/ operator for division.'), + + /// Deprecation for leading, trailing, and repeated combinators. + bogusCombinators('bogus-combinators', + deprecatedIn: '1.54.0', + description: 'Leading, trailing, and repeated combinators.'), + + /// Deprecation for ambiguous `+` and `-` operators. + strictUnary('strict-unary', + deprecatedIn: '1.55.0', description: 'Ambiguous + and - operators.'), + + /// Deprecation for passing invalid units to certain built-in functions. + functionUnits('function-units', + deprecatedIn: '1.56.0', + description: 'Passing invalid units to built-in functions.'), + + /// Deprecation for `@import` rules. + import.future('import', description: '@import rules.'), + + /// Used for deprecations coming from user-authored code. + userAuthored('user-authored', deprecatedIn: null); + + /// A unique ID for this deprecation in kebab case. + /// + /// This is used to refer to the deprecation on the command line. + final String id; + + /// Underlying version string used by [deprecatedIn]. + /// + /// This is necessary because [Version] doesn't have a constant constructor, + /// so we can't use it directly as an enum property. + final String? _deprecatedIn; + + /// The Dart Sass version this feature was first deprecated in. + /// + /// For deprecations that have existed in all versions of Dart Sass, this + /// should be 0.0.0. For deprecations not related to a specific Sass version, + /// this should be null. + Version? get deprecatedIn => _deprecatedIn?.andThen(Version.parse); + + /// A description of this deprecation that will be displayed in the CLI usage. + /// + /// If this is null, the given deprecation will not be listed. + final String? description; + + /// Whether this deprecation will occur in the future. + /// + /// If this is true, `deprecatedIn` will be null, since we do not yet know + /// what version of Dart Sass this deprecation will be live in. + final bool isFuture; + + /// Constructs a regular deprecation. + const Deprecation(this.id, {required String? deprecatedIn, this.description}) + : _deprecatedIn = deprecatedIn, + isFuture = false; + + /// Constructs a future deprecation. + const Deprecation.future(this.id, {this.description}) + : _deprecatedIn = null, + isFuture = true; + + @override + String toString() => id; + + /// Returns the deprecation with a given ID, or null if none exists. + static Deprecation? fromId(String id) => Deprecation.values + .firstWhereOrNull((deprecation) => deprecation.id == id); + + /// Returns the set of all deprecations done in or before [version]. + static Set forVersion(Version version) { + var range = VersionRange(max: version, includeMax: true); + return { + for (var deprecation in Deprecation.values) + if (deprecation.deprecatedIn?.andThen(range.allows) ?? false) + deprecation + }; + } +} diff --git a/lib/src/evaluation_context.dart b/lib/src/evaluation_context.dart index 5a347f7f4..a18ccb471 100644 --- a/lib/src/evaluation_context.dart +++ b/lib/src/evaluation_context.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'package:source_span/source_span.dart'; +import 'deprecation.dart'; + /// An interface that exposes information about the current Sass evaluation. /// /// This allows us to expose zone-scoped information without having to create a @@ -33,9 +35,9 @@ abstract class EvaluationContext { /// Prints a warning message associated with the current `@import` or function /// call. /// - /// If [deprecation] is `true`, the warning is emitted as a deprecation - /// warning. - void warn(String message, {bool deprecation = false}); + /// If [deprecation] is non-null, the warning is emitted as a deprecation + /// warning of that type. + void warn(String message, [Deprecation? deprecation]); } /// Prints a warning message associated with the current `@import` or function @@ -44,10 +46,15 @@ abstract class EvaluationContext { /// If [deprecation] is `true`, the warning is emitted as a deprecation warning. /// /// This may only be called within a custom function or importer callback. -/// /// {@category Compile} void warn(String message, {bool deprecation = false}) => - EvaluationContext.current.warn(message, deprecation: deprecation); + EvaluationContext.current + .warn(message, deprecation ? Deprecation.userAuthored : null); + +/// Prints a deprecation warning with [message] of type [deprecation]. +void warnForDeprecation(String message, Deprecation deprecation) { + EvaluationContext.current.warn(message, deprecation); +} /// Runs [callback] with [context] as [EvaluationContext.current]. /// diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index c6d6b5126..8c24ee494 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -72,7 +72,9 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, - charset: options.charset) + charset: options.charset, + fatalDeprecations: options.fatalDeprecations, + futureDeprecations: options.futureDeprecations) : await compileAsync(source, syntax: syntax, logger: options.logger, @@ -81,7 +83,9 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, - charset: options.charset); + charset: options.charset, + fatalDeprecations: options.fatalDeprecations, + futureDeprecations: options.futureDeprecations); } else { result = source == null ? compileString(await readStdin(), @@ -93,7 +97,9 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, - charset: options.charset) + charset: options.charset, + fatalDeprecations: options.fatalDeprecations, + futureDeprecations: options.futureDeprecations) : compile(source, syntax: syntax, logger: options.logger, @@ -102,7 +108,9 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, - charset: options.charset); + charset: options.charset, + fatalDeprecations: options.fatalDeprecations, + futureDeprecations: options.futureDeprecations); } } on SassException catch (error) { if (options.emitErrorCss) { diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 8687c0d7e..dee1c3eb1 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -8,6 +8,7 @@ import 'package:args/args.dart'; import 'package:charcode/charcode.dart'; import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import 'package:term_glyph/term_glyph.dart' as term_glyph; import 'package:tuple/tuple.dart'; @@ -78,6 +79,34 @@ class ExecutableOptions { ..addFlag('embed-source-map', help: 'Embed source map contents in CSS.', defaultsTo: false); + parser + ..addSeparator(_separator('Warnings')) + ..addFlag('quiet', abbr: 'q', help: "Don't print warnings.") + ..addFlag('quiet-deps', + help: "Don't print compiler warnings from dependencies.\n" + "Stylesheets imported through load paths count as dependencies.") + ..addFlag('verbose', + help: "Print all deprecation warnings even when they're repetitive.") + ..addMultiOption('fatal-deprecation', + help: 'Deprecations to treat as errors. You may also pass a Sass\n' + 'version to include any behavior deprecated in or before it.\n' + 'See https://sass-lang.com/documentation/breaking-changes for \n' + 'a complete list.', + allowedHelp: { + for (var deprecation in Deprecation.values) + if (deprecation.deprecatedIn != null && + deprecation.description != null) + deprecation.id: deprecation.description!, + }) + ..addMultiOption('future-deprecation', + help: 'Opt in to a deprecation early.', + allowedHelp: { + for (var deprecation in Deprecation.values) + if (deprecation.deprecatedIn == null && + deprecation.description != null) + deprecation.id: deprecation.description!, + }); + parser ..addSeparator(_separator('Other')) ..addFlag('watch', @@ -98,12 +127,6 @@ class ExecutableOptions { abbr: 'c', help: 'Whether to use terminal colors for messages.') ..addFlag('unicode', help: 'Whether to use Unicode characters for messages.') - ..addFlag('quiet', abbr: 'q', help: "Don't print warnings.") - ..addFlag('quiet-deps', - help: "Don't print compiler warnings from dependencies.\n" - "Stylesheets imported through load paths count as dependencies.") - ..addFlag('verbose', - help: "Print all deprecation warnings even when they're repetitive.") ..addFlag('trace', help: 'Print full Dart stack traces for exceptions.') ..addFlag('help', abbr: 'h', help: 'Print this usage information.', negatable: false) @@ -485,6 +508,44 @@ class ExecutableOptions { : p.absolute(path)); } + /// The set of deprecations that cause errors. + Set get fatalDeprecations => _fatalDeprecations ??= () { + var deprecations = {}; + for (var id in _options['fatal-deprecation'] as List) { + var deprecation = Deprecation.fromId(id); + if (deprecation != null) { + deprecations.add(deprecation); + } else { + try { + var argVersion = Version.parse(id); + // We can't get the version synchronously when running from + // source, so we just ignore this check by using a version higher + // than any that will ever be used. + var sassVersion = Version.parse( + const bool.hasEnvironment('version') + ? const String.fromEnvironment('version') + : '1000.0.0'); + if (argVersion > sassVersion) { + _fail('Invalid version $argVersion. --fatal-deprecation ' + 'requires a version less than or equal to the current ' + 'Dart Sass version.'); + } + deprecations.addAll(Deprecation.forVersion(argVersion)); + } on FormatException { + _fail('Invalid deprecation "$id".'); + } + } + } + return deprecations; + }(); + Set? _fatalDeprecations; + + /// The set of future deprecations that should emit warnings anyway. + Set get futureDeprecations => { + for (var id in _options['future-deprecation'] as List) + Deprecation.fromId(id) ?? _fail('Invalid deprecation "$id".') + }; + /// Returns the value of [name] in [options] if it was explicitly provided by /// the user, and `null` otherwise. Object? _ifParsed(String name) => diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 5ff3bad1b..b5d911df9 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -7,6 +7,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import '../callable.dart'; +import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../module/built_in.dart'; @@ -232,12 +233,12 @@ final module = BuiltInModule("color", functions: [ } var result = _functionString("invert", arguments.take(1)); - warn( + warnForDeprecation( "Passing a number (${arguments[0]}) to color.invert() is " "deprecated.\n" "\n" "Recommendation: $result", - deprecation: true); + Deprecation.colorModuleCompat); return result; } @@ -259,12 +260,12 @@ final module = BuiltInModule("color", functions: [ _function("grayscale", r"$color", (arguments) { if (arguments[0] is SassNumber) { var result = _functionString("grayscale", arguments.take(1)); - warn( + warnForDeprecation( "Passing a number (${arguments[0]}) to color.grayscale() is " "deprecated.\n" "\n" "Recommendation: $result", - deprecation: true); + Deprecation.colorModuleCompat); return result; } @@ -313,11 +314,11 @@ final module = BuiltInModule("color", functions: [ !argument.hasQuotes && argument.text.contains(_microsoftFilterStart)) { var result = _functionString("alpha", arguments); - warn( + warnForDeprecation( "Using color.alpha() for a Microsoft filter is deprecated.\n" "\n" "Recommendation: $result", - deprecation: true); + Deprecation.colorModuleCompat); return result; } @@ -331,11 +332,11 @@ final module = BuiltInModule("color", functions: [ argument.text.contains(_microsoftFilterStart))) { // Support the proprietary Microsoft alpha() function. var result = _functionString("alpha", arguments); - warn( + warnForDeprecation( "Using color.alpha() for a Microsoft filter is deprecated.\n" "\n" "Recommendation: $result", - deprecation: true); + Deprecation.colorModuleCompat); return result; } @@ -348,12 +349,12 @@ final module = BuiltInModule("color", functions: [ _function("opacity", r"$color", (arguments) { if (arguments[0] is SassNumber) { var result = _functionString("opacity", arguments); - warn( + warnForDeprecation( "Passing a number (${arguments[0]} to color.opacity() is " "deprecated.\n" "\n" "Recommendation: $result", - deprecation: true); + Deprecation.colorModuleCompat); return result; } @@ -459,14 +460,14 @@ SassColor _updateComponents(List arguments, if (number == null) return null; if (!scale && checkUnitless) { if (number.hasUnits) { - warn( + warnForDeprecation( "\$$name: Passing a number with unit ${number.unitString} is " "deprecated.\n" "\n" "To preserve current behavior: ${number.unitSuggestion(name)}\n" "\n" "More info: https://sass-lang.com/d/function-units", - deprecation: true); + Deprecation.functionUnits); } } if (!scale && checkPercent) _checkPercent(number, name); @@ -656,13 +657,13 @@ double _angleValue(Value angleValue, String name) { var angle = angleValue.assertNumber(name); if (angle.compatibleWithUnit('deg')) return angle.coerceValueToUnit('deg'); - warn( + warnForDeprecation( "\$$name: Passing a unit other than deg ($angle) is deprecated.\n" "\n" "To preserve current behavior: ${angle.unitSuggestion(name)}\n" "\n" "See https://sass-lang.com/d/function-units", - deprecation: true); + Deprecation.functionUnits); return angle.value; } @@ -670,13 +671,13 @@ double _angleValue(Value angleValue, String name) { void _checkPercent(SassNumber number, String name) { if (number.hasUnit('%')) return; - warn( + warnForDeprecation( "\$$name: Passing a number without unit % ($number) is deprecated.\n" "\n" "To preserve current behavior: ${number.unitSuggestion(name, '%')}\n" "\n" "More info: https://sass-lang.com/d/function-units", - deprecation: true); + Deprecation.functionUnits); } /// Create an HWB color from the given [arguments]. diff --git a/lib/src/functions/math.dart b/lib/src/functions/math.dart index 66860f488..5b7fa15f5 100644 --- a/lib/src/functions/math.dart +++ b/lib/src/functions/math.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import '../callable.dart'; +import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../module/built_in.dart'; @@ -250,7 +251,7 @@ final _randomFunction = _function("random", r"$limit: null", (arguments) { var limit = arguments[0].assertNumber("limit"); if (limit.hasUnits) { - warn( + warnForDeprecation( "math.random() will no longer ignore \$limit units ($limit) in a " "future release.\n" "\n" @@ -261,7 +262,7 @@ final _randomFunction = _function("random", r"$limit: null", (arguments) { "math.random(math.div(\$limit, 1${limit.unitString}))\n" "\n" "More info: https://sass-lang.com/d/function-units", - deprecation: true); + Deprecation.functionUnits); } var limitScalar = limit.assertInt("limit"); diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 185a5a5bd..9ad6be8be 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: cd71f3debc089cd05cd86e2eee32c2f10a05f489 +// Checksum: 92d6816f673ecbabd993aea7b79e27553f896ff4 // // ignore_for_file: unused_import @@ -16,6 +16,7 @@ import 'package:path/path.dart' as p; import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; +import 'deprecation.dart'; import 'importer.dart'; import 'importer/utils.dart'; import 'io.dart'; @@ -155,10 +156,10 @@ class ImportCache { ? inImportRule(() => importer.canonicalize(url)) : importer.canonicalize(url)); if (result?.scheme == '') { - _logger.warn(""" + _logger.warnForDeprecation(Deprecation.relativeCanonical, """ Importer $importer canonicalized $url to $result. Relative canonical URLs are deprecated and will eventually be disallowed. -""", deprecation: true); +"""); } return result; } diff --git a/lib/src/interpolation_map.dart b/lib/src/interpolation_map.dart index 111fbf00b..eb52e4009 100644 --- a/lib/src/interpolation_map.dart +++ b/lib/src/interpolation_map.dart @@ -33,11 +33,11 @@ class InterpolationMap { InterpolationMap( this._interpolation, Iterable targetLocations) : _targetLocations = List.unmodifiable(targetLocations) { - var expectedLocations = math.max(0, _interpolation.contents.length - 1); + 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."); + "InterpolationMap must have $expectedLocations targetLocations if the " + "interpolation has ${_interpolation.contents.length} components."); } } diff --git a/lib/src/logger.dart b/lib/src/logger.dart index d395d8e90..e5f738a14 100644 --- a/lib/src/logger.dart +++ b/lib/src/logger.dart @@ -2,9 +2,12 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; +import 'deprecation.dart'; +import 'logger/deprecation_handling.dart'; import 'logger/stderr.dart'; /// An interface for loggers that print messages produced by Sass stylesheets. @@ -34,6 +37,22 @@ abstract class Logger { void debug(String message, SourceSpan span); } +/// An extension to add a `warnForDeprecation` method to loggers without +/// making a breaking API change. +@internal +extension WarnForDeprecation on Logger { + /// Emits a deprecation warning for [deprecation] with the given [message]. + void warnForDeprecation(Deprecation deprecation, String message, + {FileSpan? span, Trace? trace}) { + var self = this; + if (self is DeprecationHandlingLogger) { + self.warnForDeprecation(deprecation, message, span: span, trace: trace); + } else if (!deprecation.isFuture) { + warn(message, span: span, trace: trace, deprecation: true); + } + } +} + /// A logger that emits no messages. class _QuietLogger implements Logger { void warn(String message, diff --git a/lib/src/logger/deprecation_handling.dart b/lib/src/logger/deprecation_handling.dart new file mode 100644 index 000000000..284602042 --- /dev/null +++ b/lib/src/logger/deprecation_handling.dart @@ -0,0 +1,102 @@ +// Copyright 2018 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:collection/collection.dart'; +import 'package:source_span/source_span.dart'; +import 'package:stack_trace/stack_trace.dart'; + +import '../deprecation.dart'; +import '../exception.dart'; +import '../logger.dart'; + +/// The maximum number of repetitions of the same warning +/// [DeprecationHandlingLogger] will emit before hiding the rest. +const _maxRepetitions = 5; + +/// A logger that wraps an inner logger to have special handling for +/// deprecation warnings. +class DeprecationHandlingLogger implements Logger { + /// A map of how many times each deprecation has been emitted by this logger. + final _warningCounts = {}; + + final Logger _inner; + + /// Deprecation warnings of one of these types will cause an error to be + /// thrown. + /// + /// Future deprecations in this list will still cause an error even if they + /// are not also in [futureDeprecations]. + final Set fatalDeprecations; + + /// Future deprecations that the user has explicitly opted into. + final Set futureDeprecations; + + /// Whether repetitions of the same warning should be limited to no more than + /// [_maxRepetitions]. + final bool limitRepetition; + + DeprecationHandlingLogger(this._inner, + {required this.fatalDeprecations, + required this.futureDeprecations, + this.limitRepetition = true}); + + void warn(String message, + {FileSpan? span, Trace? trace, bool deprecation = false}) { + _inner.warn(message, span: span, trace: trace, deprecation: deprecation); + } + + /// Processes a deprecation warning. + /// + /// If [deprecation] is in [fatalDeprecations], this shows an error. + /// + /// If it's a future deprecation that hasn't been opted into or its a + /// deprecation that's already been warned for [_maxReptitions] times and + /// [limitRepetitions] is true, the warning is dropped. + /// + /// Otherwise, this is passed on to [warn]. + void warnForDeprecation(Deprecation deprecation, String message, + {FileSpan? span, Trace? trace}) { + if (fatalDeprecations.contains(deprecation)) { + message += "\n\nThis is only an error because you've set the " + '$deprecation deprecation to be fatal.\n' + 'Remove this setting if you need to keep using this feature.'; + if (span != null && trace != null) { + throw SassRuntimeException(message, span, trace); + } + if (span == null) throw SassScriptException(message); + throw SassException(message, span); + } + + if (deprecation.isFuture && !futureDeprecations.contains(deprecation)) { + return; + } + + if (limitRepetition) { + var count = + _warningCounts[deprecation] = (_warningCounts[deprecation] ?? 0) + 1; + if (count > _maxRepetitions) return; + } + + warn(message, span: span, trace: trace, deprecation: true); + } + + void debug(String message, SourceSpan span) => _inner.debug(message, span); + + /// Prints a warning indicating the number of deprecation warnings that were + /// omitted due to repetition. + /// + /// The [node] flag indicates whether this is running in Node.js mode, in + /// which case it doesn't mention "verbose mode" because the Node API doesn't + /// support that. + void summarize({required bool node}) { + var total = _warningCounts.values + .where((count) => count > _maxRepetitions) + .map((count) => count - _maxRepetitions) + .sum; + if (total > 0) { + _inner.warn("$total repetitive deprecation warnings omitted." + + (node ? "" : "\nRun in verbose mode to see all warnings.")); + } + } +} diff --git a/lib/src/logger/terse.dart b/lib/src/logger/terse.dart deleted file mode 100644 index 83258da15..000000000 --- a/lib/src/logger/terse.dart +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2018 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:collection/collection.dart'; -import 'package:source_span/source_span.dart'; -import 'package:stack_trace/stack_trace.dart'; - -import '../logger.dart'; - -/// The maximum number of repetitions of the same warning [TerseLogger] will -/// emit before hiding the rest. -const _maxRepetitions = 5; - -/// A logger that wraps an inner logger to omit repeated deprecation warnings. -/// -/// A warning is considered "repeated" if the first paragraph is the same as -/// another warning that's already been emitted. -class TerseLogger implements Logger { - /// A map from the first paragraph of a warning to the number of times this - /// logger has emitted a warning with that line. - final _warningCounts = {}; - - final Logger _inner; - - TerseLogger(this._inner); - - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { - if (deprecation) { - var firstParagraph = message.split("\n\n").first; - var count = _warningCounts[firstParagraph] = - (_warningCounts[firstParagraph] ?? 0) + 1; - if (count > _maxRepetitions) return; - } - - _inner.warn(message, span: span, trace: trace, deprecation: deprecation); - } - - void debug(String message, SourceSpan span) => _inner.debug(message, span); - - /// Prints a warning indicating the number of deprecation warnings that were - /// omitted. - /// - /// The [node] flag indicates whether this is running in Node.js mode, in - /// which case it doesn't mention "verbose mode" because the Node API doesn't - /// support that. - void summarize({required bool node}) { - var total = _warningCounts.values - .where((count) => count > _maxRepetitions) - .map((count) => count - _maxRepetitions) - .sum; - if (total > 0) { - _inner.warn("$total repetitive deprecation warnings omitted." + - (node ? "" : "\nRun in verbose mode to see all warnings.")); - } - } -} diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index 047a52eee..a936ba594 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -5,6 +5,7 @@ import 'package:charcode/charcode.dart'; import '../ast/sass.dart'; +import '../deprecation.dart'; import '../interpolation_buffer.dart'; import '../logger.dart'; import '../util/character.dart'; @@ -45,13 +46,13 @@ class ScssParser extends StylesheetParser { if (scanner.scanChar($at)) { if (scanIdentifier('else', caseSensitive: true)) return true; if (scanIdentifier('elseif', caseSensitive: true)) { - logger.warn( + logger.warnForDeprecation( + Deprecation.elseif, '@elseif is deprecated and will not be supported in future Sass ' 'versions.\n' '\n' 'Recommendation: @else if', - span: scanner.spanFrom(beforeAt), - deprecation: true); + span: scanner.spanFrom(beforeAt)); scanner.position -= 2; return true; } diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index d15801f28..76ac28360 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -11,6 +11,7 @@ import 'package:tuple/tuple.dart'; import '../ast/sass.dart'; import '../color_names.dart'; +import '../deprecation.dart'; import '../exception.dart'; import '../interpolation_buffer.dart'; import '../logger.dart'; @@ -1056,6 +1057,14 @@ abstract class StylesheetParser extends Parser { do { whitespace(); var argument = importArgument(); + if (argument is DynamicImport) { + logger.warnForDeprecation( + Deprecation.import, + 'Sass @import rules will be deprecated in the future.\n' + 'Remove the --future-deprecation=import flag to silence this ' + 'warning for now.', + span: argument.span); + } if ((_inControlDirective || _inMixin) && argument is DynamicImport) { _disallowedAtRule(start); } @@ -1381,13 +1390,13 @@ abstract class StylesheetParser extends Parser { var value = buffer.interpolation(scanner.spanFrom(valueStart)); return _withChildren(_statement, start, (children, span) { if (needsDeprecationWarning) { - logger.warn( + logger.warnForDeprecation( + Deprecation.mozDocument, "@-moz-document is deprecated and support will be removed in Dart " "Sass 2.0.0.\n" "\n" "For details, see https://sass-lang.com/d/moz-document.", - span: span, - deprecation: true); + span: span); } return AtRule(name, span, value: value, children: children); @@ -1787,7 +1796,8 @@ abstract class StylesheetParser extends Parser { right.span.start.offset - 1, right.span.start.offset) == operator.operator && isWhitespace(scanner.string.codeUnitAt(left.span.end.offset))) { - logger.warn( + logger.warnForDeprecation( + Deprecation.strictUnary, "This operation is parsed as:\n" "\n" " $left ${operator.operator} $right\n" @@ -1804,8 +1814,7 @@ abstract class StylesheetParser extends Parser { "\n" "More info and automated migrator: " "https://sass-lang.com/d/strict-unary", - span: singleExpression_!.span, - deprecation: true); + span: singleExpression_!.span); } } } diff --git a/lib/src/value.dart b/lib/src/value.dart index 22105eb2f..ed90bc918 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'ast/selector.dart'; +import 'deprecation.dart'; import 'evaluation_context.dart'; import 'exception.dart'; import 'utils.dart'; @@ -123,7 +124,7 @@ abstract class Value { int sassIndexToListIndex(Value sassIndex, [String? name]) { var indexValue = sassIndex.assertNumber(name); if (indexValue.hasUnits) { - warn( + warnForDeprecation( "\$$name: Passing a number with unit ${indexValue.unitString} is " "deprecated.\n" "\n" @@ -131,7 +132,7 @@ abstract class Value { "${indexValue.unitSuggestion(name ?? 'index')}\n" "\n" "More info: https://sass-lang.com/d/function-units", - deprecation: true); + Deprecation.functionUnits); } var index = indexValue.assertInt(name); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index a500e8c4e..8160fe3a9 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -23,6 +23,7 @@ import '../callable.dart'; import '../color_names.dart'; import '../configuration.dart'; import '../configured_value.dart'; +import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../extend/extension_store.dart'; @@ -447,12 +448,12 @@ class _EvaluateVisitor callableNode.span)); if (function is SassString) { - warn( + warnForDeprecation( "Passing a string to call() is deprecated and will be illegal in " "Dart Sass 2.0.0.\n" "\n" "Recommendation: call(get-function($function))", - deprecation: true); + Deprecation.callString); var callableNode = _callableNode!; var expression = @@ -1205,7 +1206,7 @@ class _EvaluateVisitor 'More info: https://sass-lang.com/d/bogus-combinators', MultiSpan(complex.span.trimRight(), 'invalid selector', {node.span: '@extend rule'}), - deprecation: true); + Deprecation.bogusCombinators); } var tuple = @@ -1921,7 +1922,7 @@ class _EvaluateVisitor '\n' 'More info: https://sass-lang.com/d/bogus-combinators', complex.span.trimRight(), - deprecation: true); + Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { _warn( 'The selector "${complex.toString().trim()}" is invalid CSS.\n' @@ -1929,7 +1930,7 @@ class _EvaluateVisitor '\n' 'More info: https://sass-lang.com/d/bogus-combinators', complex.span.trimRight(), - deprecation: true); + Deprecation.bogusCombinators); } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -1948,7 +1949,7 @@ class _EvaluateVisitor ? '\n(try converting to a //-style comment)' : '') }), - deprecation: true); + Deprecation.bogusCombinators); } } } @@ -2079,7 +2080,7 @@ class _EvaluateVisitor "Recommendation: add `${node.originalName}: null` at the " "stylesheet root.", node.span, - deprecation: true); + Deprecation.newGlobal); } var value = @@ -2216,7 +2217,7 @@ class _EvaluateVisitor "More info and automated migrator: " "https://sass-lang.com/d/slash-div", node.span, - deprecation: true); + Deprecation.slashDiv); } return result; @@ -3398,7 +3399,7 @@ class _EvaluateVisitor "More info and automated migrator: " "https://sass-lang.com/d/slash-div", nodeForSpan.span, - deprecation: true); + Deprecation.slashDiv); } return value.withoutSlash(); @@ -3421,15 +3422,20 @@ class _EvaluateVisitor } /// Emits a warning with the given [message] about the given [span]. - void _warn(String message, FileSpan span, {bool deprecation = false}) { + void _warn(String message, FileSpan span, [Deprecation? deprecation]) { if (_quietDeps && (_inDependency || (_currentCallable?.inDependency ?? false))) { return; } if (!_warningsEmitted.add(Tuple2(message, span))) return; - _logger.warn(message, - span: span, trace: _stackTrace(span), deprecation: deprecation); + var trace = _stackTrace(span); + if (deprecation == null) { + _logger.warn(message, span: span, trace: trace); + } else { + _logger.warnForDeprecation(deprecation, message, + span: span, trace: trace); + } } /// Returns a [SassRuntimeException] with the given [message]. @@ -3612,13 +3618,13 @@ class _EvaluationContext implements EvaluationContext { throw StateError("No Sass callable is currently being evaluated."); } - void warn(String message, {bool deprecation = false}) { + void warn(String message, [Deprecation? deprecation]) { _visitor._warn( message, _visitor._importSpan ?? _visitor._callableNode?.span ?? _defaultWarnNodeWithSpan.span, - deprecation: deprecation); + deprecation); } } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index e8b54a57f..6d9c26bb3 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: 8945d2e2978c178b096a4bbcd3857572ec5ab1e0 +// Checksum: 8a55729a9dc5dafe90954738907880052d930898 // // ignore_for_file: unused_import @@ -32,6 +32,7 @@ import '../callable.dart'; import '../color_names.dart'; import '../configuration.dart'; import '../configured_value.dart'; +import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../extend/extension_store.dart'; @@ -454,12 +455,12 @@ class _EvaluateVisitor callableNode.span)); if (function is SassString) { - warn( + warnForDeprecation( "Passing a string to call() is deprecated and will be illegal in " "Dart Sass 2.0.0.\n" "\n" "Recommendation: call(get-function($function))", - deprecation: true); + Deprecation.callString); var callableNode = _callableNode!; var expression = @@ -1209,7 +1210,7 @@ class _EvaluateVisitor 'More info: https://sass-lang.com/d/bogus-combinators', MultiSpan(complex.span.trimRight(), 'invalid selector', {node.span: '@extend rule'}), - deprecation: true); + Deprecation.bogusCombinators); } var tuple = _performInterpolationWithMap(node.selector, warnForColor: true); @@ -1915,7 +1916,7 @@ class _EvaluateVisitor '\n' 'More info: https://sass-lang.com/d/bogus-combinators', complex.span.trimRight(), - deprecation: true); + Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { _warn( 'The selector "${complex.toString().trim()}" is invalid CSS.\n' @@ -1923,7 +1924,7 @@ class _EvaluateVisitor '\n' 'More info: https://sass-lang.com/d/bogus-combinators', complex.span.trimRight(), - deprecation: true); + Deprecation.bogusCombinators); } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -1942,7 +1943,7 @@ class _EvaluateVisitor ? '\n(try converting to a //-style comment)' : '') }), - deprecation: true); + Deprecation.bogusCombinators); } } } @@ -2071,7 +2072,7 @@ class _EvaluateVisitor "Recommendation: add `${node.originalName}: null` at the " "stylesheet root.", node.span, - deprecation: true); + Deprecation.newGlobal); } var value = _withoutSlash(node.expression.accept(this), node.expression); @@ -2206,7 +2207,7 @@ class _EvaluateVisitor "More info and automated migrator: " "https://sass-lang.com/d/slash-div", node.span, - deprecation: true); + Deprecation.slashDiv); } return result; @@ -3367,7 +3368,7 @@ class _EvaluateVisitor "More info and automated migrator: " "https://sass-lang.com/d/slash-div", nodeForSpan.span, - deprecation: true); + Deprecation.slashDiv); } return value.withoutSlash(); @@ -3390,15 +3391,20 @@ class _EvaluateVisitor } /// Emits a warning with the given [message] about the given [span]. - void _warn(String message, FileSpan span, {bool deprecation = false}) { + void _warn(String message, FileSpan span, [Deprecation? deprecation]) { if (_quietDeps && (_inDependency || (_currentCallable?.inDependency ?? false))) { return; } if (!_warningsEmitted.add(Tuple2(message, span))) return; - _logger.warn(message, - span: span, trace: _stackTrace(span), deprecation: deprecation); + var trace = _stackTrace(span); + if (deprecation == null) { + _logger.warn(message, span: span, trace: trace); + } else { + _logger.warnForDeprecation(deprecation, message, + span: span, trace: trace); + } } /// Returns a [SassRuntimeException] with the given [message]. @@ -3554,13 +3560,13 @@ class _EvaluationContext implements EvaluationContext { throw StateError("No Sass callable is currently being evaluated."); } - void warn(String message, {bool deprecation = false}) { + void warn(String message, [Deprecation? deprecation]) { _visitor._warn( message, _visitor._importSpan ?? _visitor._callableNode?.span ?? _defaultWarnNodeWithSpan.span, - deprecation: deprecation); + deprecation); } } diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index d83a4f685..9791fbc0f 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -10,7 +10,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - sass: 1.58.3 + sass: 1.59.0 dev_dependencies: dartdoc: ^5.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 9687577c8..f5224ae9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.58.4-dev +version: 1.59.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass @@ -21,6 +21,7 @@ dependencies: js: ^0.6.3 package_config: ^2.0.0 path: ^1.8.0 + pub_semver: ^2.0.0 source_maps: ^0.10.10 source_span: ^1.8.1 stack_trace: ^1.10.0 @@ -42,7 +43,6 @@ dev_dependencies: node_preamble: ^2.0.0 lints: ^2.0.0 pub_api_client: ^2.1.1 - pub_semver: ^2.0.0 pubspec_parse: ^1.0.0 stream_channel: ^2.1.0 test: ^1.16.7 diff --git a/test/cli/shared.dart b/test/cli/shared.dart index 2b92179f1..63c990685 100644 --- a/test/cli/shared.dart +++ b/test/cli/shared.dart @@ -813,6 +813,72 @@ void sharedTests( }); }); + group("with --fatal-deprecation", () { + test("set to a specific deprecation, errors as intended", () async { + await d.file("test.scss", "a {b: (4/2)}").create(); + var sass = await runSass(["--fatal-deprecation=slash-div", "test.scss"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(65); + }); + + test("set to version, errors as intended", () async { + await d.file("test.scss", "a {b: (4/2)}").create(); + var sass = await runSass(["--fatal-deprecation=1.33.0", "test.scss"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(65); + }); + + test("set to lower version, only warns", () async { + await d.file("test.scss", "a {b: (4/2)}").create(); + var sass = await runSass(["--fatal-deprecation=1.32.0", "test.scss"]); + expect( + sass.stdout, + emitsInOrder([ + "a {", + " b: 2;", + "}", + ])); + expect(sass.stderr, emitsThrough(contains("DEPRECATION WARNING"))); + await sass.shouldExit(0); + }); + + test("set to future version, usage error", () async { + await d.file("test.scss", "a {b: (4/2)}").create(); + var sass = await runSass(["--fatal-deprecation=1000.0.0", "test.scss"]); + expect(sass.stdout, emitsThrough(contains("Invalid version 1000.0.0"))); + await sass.shouldExit(64); + }); + }); + + group("with --future-deprecation", () { + test("set to a deprecation, warns as intended", () async { + await d.file("_lib.scss", "a{b:c}").create(); + await d.file("test.scss", "@import 'lib'").create(); + var sass = await runSass(["--future-deprecation=import", "test.scss"]); + expect( + sass.stdout, + emitsInOrder([ + "a {", + " b: c;", + "}", + ])); + expect(sass.stderr, emitsThrough(contains("DEPRECATION WARNING"))); + await sass.shouldExit(0); + }); + + test("set alongside --fatal-deprecation, errors as intended", () async { + await d.file("_lib.scss", "a{b:c}").create(); + await d.file("test.scss", "@import 'lib'").create(); + var sass = await runSass([ + "--future-deprecation=import", + "--fatal-deprecation=import", + "test.scss" + ]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(65); + }); + }); + test("doesn't unassign variables", () async { // This is a regression test for one of the strangest errors I've ever // encountered. Every bit of what's going on was necessary to reproduce it, diff --git a/test/deprecations_test.dart b/test/deprecations_test.dart new file mode 100644 index 000000000..84503aa00 --- /dev/null +++ b/test/deprecations_test.dart @@ -0,0 +1,137 @@ +// Copyright 2018 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:test/test.dart'; + +import 'package:sass/sass.dart'; + +void main() { + // Deprecated in all version of Dart Sass + test("callString is violated by passing a string to call", () { + _expectDeprecation("a { b: call(random)}", Deprecation.callString); + }); + + // Deprecated in 1.3.2 + test("elseIf is violated by using @elseif instead of @else if", () { + _expectDeprecation("@if false {} @elseif {}", Deprecation.elseif); + }); + + // Deprecated in 1.7.2 + test("mozDocument is violated by most @-moz-document rules", () { + _expectDeprecation( + "@-moz-document url-prefix(foo) {}", Deprecation.mozDocument); + }); + + // Deprecated in 1.17.2 + test("newGlobal is violated by declaring a new variable with !global", () { + _expectDeprecation(r"a {$foo: bar !global;}", Deprecation.newGlobal); + }); + + // Deprecated in 1.23.0 + group("colorModuleCompat is violated by", () { + var color = "@use 'sass:color'; a { b: color"; + + test("passing a number to color.invert", () { + _expectDeprecation("$color.invert(0)}", Deprecation.colorModuleCompat); + }); + + test("passing a number to color.grayscale", () { + _expectDeprecation("$color.grayscale(0)}", Deprecation.colorModuleCompat); + }); + + test("passing a number to color.opacity", () { + _expectDeprecation("$color.opacity(0)}", Deprecation.colorModuleCompat); + }); + + test("using color.alpha for a microsoft filter", () { + _expectDeprecation( + "$color.alpha(foo=bar)}", Deprecation.colorModuleCompat); + }); + }); + + // Deprecated in 1.33.0 + test("slashDiv is violated by using / for division", () { + _expectDeprecation(r"a {b: (4/2)}", Deprecation.slashDiv); + }); + + // Deprecated in 1.54.0 + group("bogusCombinators is violated by", () { + test("adjacent combinators", () { + _expectDeprecation("a > > a {b: c}", Deprecation.bogusCombinators); + }); + + test("leading combinators", () { + _expectDeprecation("a > {b: c}", Deprecation.bogusCombinators); + }); + + test("trailing combinators", () { + _expectDeprecation("> a {b: c}", Deprecation.bogusCombinators); + }); + }); + + // Deprecated in 1.55.0 + group("strictUnary is violated by", () { + test("an ambiguous + operator", () { + _expectDeprecation(r"a {b: 1 +2}", Deprecation.strictUnary); + }); + + test("an ambiguous - operator", () { + _expectDeprecation(r"a {$x: 2; b: 1 -$x}", Deprecation.strictUnary); + }); + }); + + // Deprecated in various Sass versions <=1.56.0 + group("functionUnits is violated by", () { + test("a hue with a non-angle unit", () { + _expectDeprecation("a {b: hsl(10px, 0%, 0%)}", Deprecation.functionUnits); + }); + + test("a saturation/lightness with a non-percent unit", () { + _expectDeprecation( + "a {b: hsl(10deg, 0px, 0%)}", Deprecation.functionUnits); + }); + + test("a saturation/lightness with no unit", () { + _expectDeprecation("a {b: hsl(10deg, 0%, 0)}", Deprecation.functionUnits); + }); + + test("an alpha value with a percent unit", () { + _expectDeprecation( + r"@use 'sass:color'; a {b: color.change(red, $alpha: 1%)}", + Deprecation.functionUnits); + }); + + test("an alpha value with a non-percent unit", () { + _expectDeprecation( + r"@use 'sass:color'; a {b: color.change(red, $alpha: 1px)}", + Deprecation.functionUnits); + }); + + test("calling math.random with units", () { + _expectDeprecation("@use 'sass:math'; a {b: math.random(100px)}", + Deprecation.functionUnits); + }); + + test("calling list.nth with units", () { + _expectDeprecation("@use 'sass:list'; a {b: list.nth(1 2, 1px)}", + Deprecation.functionUnits); + }); + + test("calling list.set-nth with units", () { + _expectDeprecation("@use 'sass:list'; a {b: list.set-nth(1 2, 1px, 3)}", + Deprecation.functionUnits); + }); + }); +} + +/// Confirms that [source] will error if [deprecation] is fatal. +void _expectDeprecation(String source, Deprecation deprecation) { + try { + compileStringToResult(source, fatalDeprecations: {deprecation}); + } catch (e) { + if (e.toString().contains("$deprecation deprecation to be fatal")) return; + fail('Unexpected error: $e'); + } + fail("No error for violating $deprecation."); +} From d0ca8e0bc9f790a2b411728a07f9ec44b168c10e Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Fri, 10 Mar 2023 15:36:32 -0800 Subject: [PATCH 7/8] Fix macOS arm64 build and release a new version (#1906) * Revert "Remove workaround for dart-lang/setup-dart#59 (#1904)" This reverts commit 434f2b99f154c14dc5754ed1566d1b788a3e126a. * Bump version --- .github/workflows/ci.yml | 6 ++++++ CHANGELOG.md | 4 ++++ pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76c69496d..bdb81411d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -332,14 +332,20 @@ jobs: include: - runner: macos-latest platform: macos-x64 + architecture: x64 - runner: self-hosted platform: macos-arm64 + architecture: arm64 - runner: windows-latest platform: windows + architecture: x64 steps: - uses: actions/checkout@v3 - uses: dart-lang/setup-dart@v1 + # Workaround for dart-lang/setup-dart#59 + with: + architecture: ${{ matrix.architecture }} - run: dart pub get - name: Deploy run: dart run grinder pkg-github-${{ matrix.platform }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb9bca8f..1ae8d52ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.59.1 + +* No user-visible changes. + ## 1.59.0 ### Command Line Interface diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 1aaf8b7ad..6dd0b405b 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.1 + +* No user-visible changes. + ## 6.0.0 * **Breaking change:** All selector AST node constructors now require a diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 9791fbc0f..40976bd8d 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: 6.0.0 +version: 6.0.1 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - sass: 1.59.0 + sass: 1.59.1 dev_dependencies: dartdoc: ^5.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index f5224ae9b..5dd290518 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.59.0 +version: 1.59.1 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From b540d5914e4d6f0d8942c5af6310cf89691eb7ce Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Fri, 10 Mar 2023 17:08:59 -0800 Subject: [PATCH 8/8] Release 1.59.2 (#1908) --- CHANGELOG.md | 4 ++++ pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae8d52ef..ebc7ec3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.59.2 + +* No user-visible changes. + ## 1.59.1 * No user-visible changes. diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 6dd0b405b..d9a0f072a 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.2 + +* No user-visible changes. + ## 6.0.1 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 40976bd8d..6800bf69c 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: 6.0.1 +version: 6.0.2 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - sass: 1.59.1 + sass: 1.59.2 dev_dependencies: dartdoc: ^5.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 5dd290518..521a8d84d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.59.1 +version: 1.59.2 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass