diff --git a/bin/sass.dart b/bin/sass.dart index 396e4deb9..986ff9bcb 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -4,7 +4,6 @@ import 'dart:isolate'; -import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import 'package:stack_trace/stack_trace.dart'; import 'package:term_glyph/term_glyph.dart' as term_glyph; @@ -19,6 +18,7 @@ import 'package:sass/src/io.dart'; import 'package:sass/src/io.dart' as io; import 'package:sass/src/logger/deprecation_handling.dart'; import 'package:sass/src/stylesheet_graph.dart'; +import 'package:sass/src/util/map.dart'; import 'package:sass/src/utils.dart'; import 'package:sass/src/embedded/executable.dart' // Never load the embedded protocol when compiling to JS. @@ -47,8 +47,8 @@ Future main(List args) async { io.printError(buffer); } - if (args.firstOrNull == '--embedded') { - embedded.main(args.sublist(1)); + if (args case ['--embedded', ...var rest]) { + embedded.main(rest); return; } @@ -84,25 +84,14 @@ Future main(List args) async { return; } - for (var source in options.sourcesToDestinations.keys) { - var destination = options.sourcesToDestinations[source]; + for (var (source, destination) in options.sourcesToDestinations.pairs) { try { await compileStylesheet(options, graph, source, destination, ifModified: options.update); } on SassException catch (error, stackTrace) { - // This is an immediately-invoked function expression to work around - // dart-lang/sdk#33400. - () { - try { - if (destination != null && - // dart-lang/sdk#45348 - !options!.emitErrorCss) { - deleteFile(destination); - } - } on FileSystemException { - // If the file doesn't exist, that's fine. - } - }(); + if (destination != null && !options.emitErrorCss) { + _tryDelete(destination); + } printError(error.toString(color: options.color), options.trace ? getTrace(error) ?? stackTrace : null); @@ -134,9 +123,9 @@ Future main(List args) async { exitCode = 64; } catch (error, stackTrace) { var buffer = StringBuffer(); - if (options != null && options.color) buffer.write('\u001b[31m\u001b[1m'); + if (options?.color ?? false) buffer.write('\u001b[31m\u001b[1m'); buffer.write('Unexpected exception:'); - if (options != null && options.color) buffer.write('\u001b[0m'); + if (options?.color ?? false) buffer.write('\u001b[0m'); buffer.writeln(); buffer.writeln(error); @@ -165,3 +154,14 @@ Future _loadVersion() async { .split(" ") .last; } + +/// Delete [path] if it exists and do nothing otherwise. +/// +/// This is a separate function to work around dart-lang/sdk#53082. +void _tryDelete(String path) { + try { + deleteFile(path); + } on FileSystemException { + // If the file doesn't exist, that's fine. + } +} diff --git a/lib/src/ast/css/at_rule.dart b/lib/src/ast/css/at_rule.dart index 8aff6529e..f11aa253f 100644 --- a/lib/src/ast/css/at_rule.dart +++ b/lib/src/ast/css/at_rule.dart @@ -2,12 +2,11 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import 'node.dart'; import 'value.dart'; /// An unknown plain CSS at-rule. -abstract class CssAtRule extends CssParentNode { +abstract interface class CssAtRule implements CssParentNode { /// The name of this rule. CssValue get name; @@ -19,6 +18,4 @@ abstract class CssAtRule extends CssParentNode { /// This implies `children.isEmpty`, but the reverse is not true—for a rule /// like `@foo {}`, [children] is empty but [isChildless] is `false`. bool get isChildless; - - T accept(CssVisitor visitor) => visitor.visitCssAtRule(this); } diff --git a/lib/src/ast/css/comment.dart b/lib/src/ast/css/comment.dart index 4724a83b2..39d76423c 100644 --- a/lib/src/ast/css/comment.dart +++ b/lib/src/ast/css/comment.dart @@ -2,19 +2,16 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import 'node.dart'; /// A plain CSS comment. /// /// This is always a multi-line comment. -abstract class CssComment extends CssNode { +abstract interface class CssComment implements CssNode { /// The contents of this comment, including `/*` and `*/`. String get text; /// Whether this comment starts with `/*!` and so should be preserved even in /// compressed mode. bool get isPreserved; - - T accept(CssVisitor visitor) => visitor.visitCssComment(this); } diff --git a/lib/src/ast/css/declaration.dart b/lib/src/ast/css/declaration.dart index 1bd7279fc..4d5e906cd 100644 --- a/lib/src/ast/css/declaration.dart +++ b/lib/src/ast/css/declaration.dart @@ -5,12 +5,11 @@ import 'package:source_span/source_span.dart'; import '../../value.dart'; -import '../../visitor/interface/css.dart'; import 'node.dart'; import 'value.dart'; /// A plain CSS declaration (that is, a `name: value` pair). -abstract class CssDeclaration extends CssNode { +abstract interface class CssDeclaration implements CssNode { /// The name of this declaration. CssValue get name; @@ -34,6 +33,4 @@ abstract class CssDeclaration extends CssNode { /// If this is `true`, [isCustomProperty] will also be `true` and [value] will /// contain a [SassString]. bool get parsedAsCustomProperty; - - T accept(CssVisitor visitor); } diff --git a/lib/src/ast/css/import.dart b/lib/src/ast/css/import.dart index b8ad9e9e0..527006b7e 100644 --- a/lib/src/ast/css/import.dart +++ b/lib/src/ast/css/import.dart @@ -2,12 +2,11 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import 'node.dart'; import 'value.dart'; /// A plain CSS `@import`. -abstract class CssImport extends CssNode { +abstract interface class CssImport implements CssNode { /// The URL being imported. /// /// This includes quotes. @@ -15,6 +14,4 @@ abstract class CssImport extends CssNode { /// The modifiers (such as media or supports queries) attached to this import. CssValue? get modifiers; - - T accept(CssVisitor visitor) => visitor.visitCssImport(this); } diff --git a/lib/src/ast/css/keyframe_block.dart b/lib/src/ast/css/keyframe_block.dart index 0b52cd82d..c6255fe45 100644 --- a/lib/src/ast/css/keyframe_block.dart +++ b/lib/src/ast/css/keyframe_block.dart @@ -2,16 +2,13 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import 'node.dart'; import 'value.dart'; /// A block within a `@keyframes` rule. /// /// For example, `10% {opacity: 0.5}`. -abstract class CssKeyframeBlock extends CssParentNode { +abstract interface class CssKeyframeBlock implements CssParentNode { /// The selector for this block. CssValue> get selector; - - T accept(CssVisitor visitor) => visitor.visitCssKeyframeBlock(this); } diff --git a/lib/src/ast/css/media_query.dart b/lib/src/ast/css/media_query.dart index 9f2d49dbc..dc2d5d532 100644 --- a/lib/src/ast/css/media_query.dart +++ b/lib/src/ast/css/media_query.dart @@ -8,7 +8,7 @@ import '../../parse/media_query.dart'; import '../../utils.dart'; /// A plain CSS media query, as used in `@media` and `@import`. -class CssMediaQuery { +final class CssMediaQuery { /// The modifier, probably either "not" or "only". /// /// This may be `null` if no modifier is in use. @@ -197,25 +197,21 @@ class CssMediaQuery { /// /// This is either the singleton values [empty] or [unrepresentable], or an /// instance of [MediaQuerySuccessfulMergeResult]. -abstract class MediaQueryMergeResult { +sealed class MediaQueryMergeResult { /// A singleton value indicating that there are no contexts that match both /// input queries. - static const empty = _SingletonCssMediaQueryMergeResult("empty"); + static const empty = _SingletonCssMediaQueryMergeResult.empty; /// A singleton value indicating that the contexts that match both input /// queries can't be represented by a Level 3 media query. static const unrepresentable = - _SingletonCssMediaQueryMergeResult("unrepresentable"); + _SingletonCssMediaQueryMergeResult.unrepresentable; } /// The subclass [MediaQueryMergeResult] that represents singleton enum values. -class _SingletonCssMediaQueryMergeResult implements MediaQueryMergeResult { - /// The name of the result type. - final String _name; - - const _SingletonCssMediaQueryMergeResult(this._name); - - String toString() => _name; +enum _SingletonCssMediaQueryMergeResult implements MediaQueryMergeResult { + empty, + unrepresentable; } /// A successful result of [CssMediaQuery.merge]. diff --git a/lib/src/ast/css/media_rule.dart b/lib/src/ast/css/media_rule.dart index d44859fb1..8eeba7e01 100644 --- a/lib/src/ast/css/media_rule.dart +++ b/lib/src/ast/css/media_rule.dart @@ -2,16 +2,13 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import 'media_query.dart'; import 'node.dart'; /// A plain CSS `@media` rule. -abstract class CssMediaRule extends CssParentNode { +abstract interface class CssMediaRule implements CssParentNode { /// The queries for this rule. /// /// This is never empty. List get queries; - - T accept(CssVisitor visitor) => visitor.visitCssMediaRule(this); } diff --git a/lib/src/ast/css/modifiable/at_rule.dart b/lib/src/ast/css/modifiable/at_rule.dart index 1abe95285..a63579688 100644 --- a/lib/src/ast/css/modifiable/at_rule.dart +++ b/lib/src/ast/css/modifiable/at_rule.dart @@ -10,7 +10,8 @@ import '../value.dart'; import 'node.dart'; /// A modifiable version of [CssAtRule] for use in the evaluation step. -class ModifiableCssAtRule extends ModifiableCssParentNode implements CssAtRule { +final class ModifiableCssAtRule extends ModifiableCssParentNode + implements CssAtRule { final CssValue name; final CssValue? value; final bool isChildless; diff --git a/lib/src/ast/css/modifiable/comment.dart b/lib/src/ast/css/modifiable/comment.dart index 40e7838c7..a795b1d00 100644 --- a/lib/src/ast/css/modifiable/comment.dart +++ b/lib/src/ast/css/modifiable/comment.dart @@ -10,7 +10,8 @@ import '../comment.dart'; import 'node.dart'; /// A modifiable version of [CssComment] for use in the evaluation step. -class ModifiableCssComment extends ModifiableCssNode implements CssComment { +final class ModifiableCssComment extends ModifiableCssNode + implements CssComment { final String text; final FileSpan span; diff --git a/lib/src/ast/css/modifiable/declaration.dart b/lib/src/ast/css/modifiable/declaration.dart index 1082e1a48..1acb292a7 100644 --- a/lib/src/ast/css/modifiable/declaration.dart +++ b/lib/src/ast/css/modifiable/declaration.dart @@ -11,7 +11,7 @@ import '../value.dart'; import 'node.dart'; /// A modifiable version of [CssDeclaration] for use in the evaluation step. -class ModifiableCssDeclaration extends ModifiableCssNode +final class ModifiableCssDeclaration extends ModifiableCssNode implements CssDeclaration { final CssValue name; final CssValue value; diff --git a/lib/src/ast/css/modifiable/import.dart b/lib/src/ast/css/modifiable/import.dart index 033424e78..24de1fb17 100644 --- a/lib/src/ast/css/modifiable/import.dart +++ b/lib/src/ast/css/modifiable/import.dart @@ -10,7 +10,7 @@ import '../value.dart'; import 'node.dart'; /// A modifiable version of [CssImport] for use in the evaluation step. -class ModifiableCssImport extends ModifiableCssNode implements CssImport { +final class ModifiableCssImport extends ModifiableCssNode implements CssImport { /// The URL being imported. /// /// This includes quotes. diff --git a/lib/src/ast/css/modifiable/keyframe_block.dart b/lib/src/ast/css/modifiable/keyframe_block.dart index fd99536e2..6b7beb9e2 100644 --- a/lib/src/ast/css/modifiable/keyframe_block.dart +++ b/lib/src/ast/css/modifiable/keyframe_block.dart @@ -11,7 +11,7 @@ import '../value.dart'; import 'node.dart'; /// A modifiable version of [CssKeyframeBlock] for use in the evaluation step. -class ModifiableCssKeyframeBlock extends ModifiableCssParentNode +final class ModifiableCssKeyframeBlock extends ModifiableCssParentNode implements CssKeyframeBlock { final CssValue> selector; final FileSpan span; diff --git a/lib/src/ast/css/modifiable/media_rule.dart b/lib/src/ast/css/modifiable/media_rule.dart index a38fce7ec..e30f44b29 100644 --- a/lib/src/ast/css/modifiable/media_rule.dart +++ b/lib/src/ast/css/modifiable/media_rule.dart @@ -11,7 +11,7 @@ import '../media_rule.dart'; import 'node.dart'; /// A modifiable version of [CssMediaRule] for use in the evaluation step. -class ModifiableCssMediaRule extends ModifiableCssParentNode +final class ModifiableCssMediaRule extends ModifiableCssParentNode implements CssMediaRule { final List queries; final FileSpan span; diff --git a/lib/src/ast/css/modifiable/node.dart b/lib/src/ast/css/modifiable/node.dart index 8cfd85579..bef0be821 100644 --- a/lib/src/ast/css/modifiable/node.dart +++ b/lib/src/ast/css/modifiable/node.dart @@ -12,7 +12,7 @@ import '../node.dart'; /// Almost all CSS nodes are the modifiable classes under the covers. However, /// modification should only be done within the evaluation step, so the /// unmodifiable types are used elsewhere to enforce that constraint. -abstract class ModifiableCssNode extends CssNode { +abstract base class ModifiableCssNode extends CssNode { /// The node that contains this, or `null` for the root [CssStylesheet] node. ModifiableCssParentNode? get parent => _parent; ModifiableCssParentNode? _parent; @@ -43,8 +43,7 @@ abstract class ModifiableCssNode extends CssNode { } parent._children.removeAt(_indexInParent!); - for (var i = _indexInParent!; i < parent._children.length; i++) { - var child = parent._children[i]; + for (var child in parent._children.skip(_indexInParent!)) { child._indexInParent = child._indexInParent! - 1; } _parent = null; @@ -52,7 +51,7 @@ abstract class ModifiableCssNode extends CssNode { } /// A modifiable version of [CssParentNode] for use in the evaluation step. -abstract class ModifiableCssParentNode extends ModifiableCssNode +abstract base class ModifiableCssParentNode extends ModifiableCssNode implements CssParentNode { final List children; final List _children; diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart index 9cf017a7c..a5d2b1f0c 100644 --- a/lib/src/ast/css/modifiable/style_rule.dart +++ b/lib/src/ast/css/modifiable/style_rule.dart @@ -11,7 +11,7 @@ import '../style_rule.dart'; import 'node.dart'; /// A modifiable version of [CssStyleRule] for use in the evaluation step. -class ModifiableCssStyleRule extends ModifiableCssParentNode +final class ModifiableCssStyleRule extends ModifiableCssParentNode implements CssStyleRule { SelectorList get selector => _selector.value; diff --git a/lib/src/ast/css/modifiable/stylesheet.dart b/lib/src/ast/css/modifiable/stylesheet.dart index dfb78f616..08f598b35 100644 --- a/lib/src/ast/css/modifiable/stylesheet.dart +++ b/lib/src/ast/css/modifiable/stylesheet.dart @@ -9,7 +9,7 @@ import '../stylesheet.dart'; import 'node.dart'; /// A modifiable version of [CssStylesheet] for use in the evaluation step. -class ModifiableCssStylesheet extends ModifiableCssParentNode +final class ModifiableCssStylesheet extends ModifiableCssParentNode implements CssStylesheet { final FileSpan span; diff --git a/lib/src/ast/css/modifiable/supports_rule.dart b/lib/src/ast/css/modifiable/supports_rule.dart index ad5101cd3..6f3419dff 100644 --- a/lib/src/ast/css/modifiable/supports_rule.dart +++ b/lib/src/ast/css/modifiable/supports_rule.dart @@ -10,7 +10,7 @@ import '../value.dart'; import 'node.dart'; /// A modifiable version of [CssSupportsRule] for use in the evaluation step. -class ModifiableCssSupportsRule extends ModifiableCssParentNode +final class ModifiableCssSupportsRule extends ModifiableCssParentNode implements CssSupportsRule { final CssValue condition; final FileSpan span; diff --git a/lib/src/ast/css/node.dart b/lib/src/ast/css/node.dart index 0e37f5ced..29daba28d 100644 --- a/lib/src/ast/css/node.dart +++ b/lib/src/ast/css/node.dart @@ -13,7 +13,8 @@ import 'comment.dart'; import 'style_rule.dart'; /// A statement in a plain CSS syntax tree. -abstract class CssNode extends AstNode { +@sealed +abstract class CssNode implements AstNode { /// Whether this was generated from the last node in a nested Sass tree that /// got flattened during evaluation. bool get isGroupEnd; @@ -43,12 +44,13 @@ abstract class CssNode extends AstNode { bool get isInvisibleHidingComments => accept( const _IsInvisibleVisitor(includeBogus: true, includeComments: true)); - String toString() => serialize(this, inspect: true).css; + String toString() => serialize(this, inspect: true).$1; } // NOTE: New at-rule implementations should add themselves to [AtRootRule]'s // exclude logic. /// A [CssNode] that can have child statements. +@sealed abstract class CssParentNode extends CssNode { /// The child statements of this node. List get children; diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart index bf19eeaa7..ccce74fdb 100644 --- a/lib/src/ast/css/style_rule.dart +++ b/lib/src/ast/css/style_rule.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import '../selector.dart'; import 'node.dart'; @@ -11,12 +10,10 @@ import 'node.dart'; /// This applies style declarations to elements that match a given selector. /// Note that this isn't *strictly* plain CSS, since [selector] may still /// contain placeholder selectors. -abstract class CssStyleRule extends CssParentNode { +abstract interface class CssStyleRule implements CssParentNode { /// The selector for this rule. SelectorList get selector; /// The selector for this rule, before any extensions were applied. SelectorList get originalSelector; - - T accept(CssVisitor visitor) => visitor.visitCssStyleRule(this); } diff --git a/lib/src/ast/css/supports_rule.dart b/lib/src/ast/css/supports_rule.dart index 091ba1dad..76457bc67 100644 --- a/lib/src/ast/css/supports_rule.dart +++ b/lib/src/ast/css/supports_rule.dart @@ -2,14 +2,11 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '../../visitor/interface/css.dart'; import 'node.dart'; import 'value.dart'; /// A plain CSS `@supports` rule. -abstract class CssSupportsRule extends CssParentNode { +abstract interface class CssSupportsRule implements CssParentNode { /// The supports condition. CssValue get condition; - - T accept(CssVisitor visitor) => visitor.visitCssSupportsRule(this); } diff --git a/lib/src/ast/css/value.dart b/lib/src/ast/css/value.dart index c10d5e665..0cda62a78 100644 --- a/lib/src/ast/css/value.dart +++ b/lib/src/ast/css/value.dart @@ -10,7 +10,7 @@ import '../node.dart'; /// /// This is used to associate a span with a value that doesn't otherwise track /// its span. It has value equality semantics. -class CssValue implements AstNode { +final class CssValue implements AstNode { /// The value. final T value; diff --git a/lib/src/ast/node.dart b/lib/src/ast/node.dart index d061a4787..e63fa9df7 100644 --- a/lib/src/ast/node.dart +++ b/lib/src/ast/node.dart @@ -14,7 +14,7 @@ import 'package:source_span/source_span.dart'; /// /// {@category AST} @sealed -abstract class AstNode { +abstract interface class AstNode { /// The source span associated with the node. /// /// This indicates where in the source Sass or SCSS stylesheet the node was diff --git a/lib/src/ast/sass/argument.dart b/lib/src/ast/sass/argument.dart index 310154337..afd9e337c 100644 --- a/lib/src/ast/sass/argument.dart +++ b/lib/src/ast/sass/argument.dart @@ -2,7 +2,6 @@ // 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 '../../utils.dart'; @@ -14,8 +13,7 @@ import 'node.dart'; /// An argument declared as part of an [ArgumentDeclaration]. /// /// {@category AST} -@sealed -class Argument implements SassNode, SassDeclaration { +final class Argument implements SassNode, SassDeclaration { /// The argument name. final String name; diff --git a/lib/src/ast/sass/argument_declaration.dart b/lib/src/ast/sass/argument_declaration.dart index 5e1ac5932..7ab73c3a5 100644 --- a/lib/src/ast/sass/argument_declaration.dart +++ b/lib/src/ast/sass/argument_declaration.dart @@ -2,15 +2,14 @@ // 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 '../../exception.dart'; import '../../logger.dart'; import '../../parse/scss.dart'; -import '../../utils.dart'; import '../../util/character.dart'; import '../../util/span.dart'; +import '../../utils.dart'; import 'argument.dart'; import 'node.dart'; @@ -18,8 +17,7 @@ import 'node.dart'; /// /// {@category AST} /// {@category Parsing} -@sealed -class ArgumentDeclaration implements SassNode { +final class ArgumentDeclaration implements SassNode { /// The arguments that are taken. final List arguments; @@ -36,19 +34,19 @@ class ArgumentDeclaration implements SassNode { // Move backwards through any whitespace between the name and the arguments. var i = span.start.offset - 1; - while (i > 0 && isWhitespace(text.codeUnitAt(i))) { + while (i > 0 && text.codeUnitAt(i).isWhitespace) { i--; } // Then move backwards through the name itself. - if (!isName(text.codeUnitAt(i))) return span; + if (!text.codeUnitAt(i).isName) return span; i--; - while (i >= 0 && isName(text.codeUnitAt(i))) { + while (i >= 0 && text.codeUnitAt(i).isName) { i--; } // If the name didn't start with [isNameStart], it's not a valid identifier. - if (!isNameStart(text.codeUnitAt(i + 1))) return span; + if (!text.codeUnitAt(i + 1).isNameStart) return span; // Trim because it's possible that this span is empty (for example, a mixin // may be declared without an argument list). diff --git a/lib/src/ast/sass/argument_invocation.dart b/lib/src/ast/sass/argument_invocation.dart index 0514ad8a3..92e7645cf 100644 --- a/lib/src/ast/sass/argument_invocation.dart +++ b/lib/src/ast/sass/argument_invocation.dart @@ -2,10 +2,10 @@ // 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 '../../value/list.dart'; +import '../../util/map.dart'; import 'expression.dart'; import 'expression/list.dart'; import 'node.dart'; @@ -13,8 +13,7 @@ import 'node.dart'; /// A set of arguments passed in to a function or mixin. /// /// {@category AST} -@sealed -class ArgumentInvocation implements SassNode { +final class ArgumentInvocation implements SassNode { /// The arguments passed by position. final List positional; @@ -48,24 +47,25 @@ class ArgumentInvocation implements SassNode { keywordRest = null; String toString() { - var rest = this.rest; - var keywordRest = this.keywordRest; var components = [ for (var argument in positional) _parenthesizeArgument(argument), - for (var entry in named.entries) - "\$${entry.key}: ${_parenthesizeArgument(entry.value)}", - if (rest != null) "${_parenthesizeArgument(rest)}...", - if (keywordRest != null) "${_parenthesizeArgument(keywordRest)}..." + for (var (name, value) in named.pairs) + "\$$name: ${_parenthesizeArgument(value)}", + if (rest case var rest?) "${_parenthesizeArgument(rest)}...", + if (keywordRest case var keywordRest?) + "${_parenthesizeArgument(keywordRest)}..." ]; return "(${components.join(', ')})"; } /// Wraps [argument] in parentheses if necessary. - String _parenthesizeArgument(Expression argument) => - argument is ListExpression && - argument.separator == ListSeparator.comma && - !argument.hasBrackets && - argument.contents.length > 1 - ? "($argument)" - : argument.toString(); + String _parenthesizeArgument(Expression argument) => switch (argument) { + ListExpression( + separator: ListSeparator.comma, + hasBrackets: false, + contents: [_, _, ...] + ) => + "($argument)", + _ => argument.toString() + }; } diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart index 1e3328db4..3bad9cf20 100644 --- a/lib/src/ast/sass/at_root_query.dart +++ b/lib/src/ast/sass/at_root_query.dart @@ -14,8 +14,7 @@ import '../css.dart'; /// A query for the `@at-root` rule. /// /// @nodoc -@internal -class AtRootQuery { +final class AtRootQuery { /// The default at-root query, which excludes only style rules. static const defaultQuery = AtRootQuery._default(); @@ -68,11 +67,13 @@ class AtRootQuery { @internal bool excludes(CssParentNode node) { if (_all) return !include; - if (node is CssStyleRule) return excludesStyleRules; - if (node is CssMediaRule) return excludesName("media"); - if (node is CssSupportsRule) return excludesName("supports"); - if (node is CssAtRule) return excludesName(node.name.value.toLowerCase()); - return false; + return switch (node) { + CssStyleRule() => excludesStyleRules, + CssMediaRule() => excludesName("media"), + CssSupportsRule() => excludesName("supports"), + CssAtRule() => excludesName(node.name.value.toLowerCase()), + _ => false + }; } /// Returns whether [this] excludes an at-rule with the given [name]. diff --git a/lib/src/ast/sass/configured_variable.dart b/lib/src/ast/sass/configured_variable.dart index 59bbc0287..c78066734 100644 --- a/lib/src/ast/sass/configured_variable.dart +++ b/lib/src/ast/sass/configured_variable.dart @@ -2,7 +2,6 @@ // 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 '../../util/span.dart'; @@ -13,8 +12,7 @@ import 'node.dart'; /// A variable configured by a `with` clause in a `@use` or `@forward` rule. /// /// {@category AST} -@sealed -class ConfiguredVariable implements SassNode, SassDeclaration { +final class ConfiguredVariable implements SassNode, SassDeclaration { /// The name of the variable being configured. final String name; diff --git a/lib/src/ast/sass/declaration.dart b/lib/src/ast/sass/declaration.dart index 0307e4994..0f2c78a41 100644 --- a/lib/src/ast/sass/declaration.dart +++ b/lib/src/ast/sass/declaration.dart @@ -11,7 +11,7 @@ import 'node.dart'; /// /// {@category AST} @sealed -abstract class SassDeclaration extends SassNode { +abstract interface class SassDeclaration implements SassNode { /// The name of the declaration, with underscores converted to hyphens. /// /// This does not include the `$` for variables. diff --git a/lib/src/ast/sass/dependency.dart b/lib/src/ast/sass/dependency.dart index d48e27623..54172be7c 100644 --- a/lib/src/ast/sass/dependency.dart +++ b/lib/src/ast/sass/dependency.dart @@ -11,7 +11,7 @@ import 'node.dart'; /// /// {@category AST} @sealed -abstract class SassDependency extends SassNode { +abstract interface class SassDependency implements SassNode { /// The URL of the dependency this rule loads. Uri get url; diff --git a/lib/src/ast/sass/expression.dart b/lib/src/ast/sass/expression.dart index 5a707bf7f..a5682411e 100644 --- a/lib/src/ast/sass/expression.dart +++ b/lib/src/ast/sass/expression.dart @@ -15,7 +15,7 @@ import 'node.dart'; /// {@category AST} /// {@category Parsing} @sealed -abstract class Expression implements SassNode { +abstract interface class Expression implements SassNode { /// Calls the appropriate visit method on [visitor]. T accept(ExpressionVisitor visitor); diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart index b93a8e024..4e87fe8de 100644 --- a/lib/src/ast/sass/expression/binary_operation.dart +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -13,8 +13,7 @@ import 'list.dart'; /// A binary operator, as in `1 + 2` or `$this and $other`. /// /// {@category AST} -@sealed -class BinaryOperationExpression implements Expression { +final class BinaryOperationExpression implements Expression { /// The operator being invoked. final BinaryOperator operator; @@ -64,12 +63,14 @@ class BinaryOperationExpression implements Expression { String toString() { var buffer = StringBuffer(); - var left = this.left; // Hack to make analysis work. - var leftNeedsParens = (left is BinaryOperationExpression && - left.operator.precedence < operator.precedence) || - (left is ListExpression && - !left.hasBrackets && - left.contents.length > 1); + // dart-lang/language#3064 and #3062 track potential ways of making this + // cleaner. + var leftNeedsParens = switch (left) { + BinaryOperationExpression(operator: BinaryOperator(:var precedence)) => + precedence < operator.precedence, + ListExpression(hasBrackets: false, contents: [_, _, ...]) => true, + _ => false + }; if (leftNeedsParens) buffer.writeCharCode($lparen); buffer.write(left); if (leftNeedsParens) buffer.writeCharCode($rparen); @@ -79,12 +80,13 @@ class BinaryOperationExpression implements Expression { buffer.writeCharCode($space); var right = this.right; // Hack to make analysis work. - var rightNeedsParens = (right is BinaryOperationExpression && - right.operator.precedence <= operator.precedence && - !(right.operator == operator && operator.isAssociative)) || - (right is ListExpression && - !right.hasBrackets && - right.contents.length > 1); + var rightNeedsParens = switch (right) { + BinaryOperationExpression(:var operator) => + operator.precedence <= this.operator.precedence && + !(operator == this.operator && operator.isAssociative), + ListExpression(hasBrackets: false, contents: [_, _, ...]) => true, + _ => false + }; if (rightNeedsParens) buffer.writeCharCode($lparen); buffer.write(right); if (rightNeedsParens) buffer.writeCharCode($rparen); diff --git a/lib/src/ast/sass/expression/boolean.dart b/lib/src/ast/sass/expression/boolean.dart index 0686d0bd0..23474a3f6 100644 --- a/lib/src/ast/sass/expression/boolean.dart +++ b/lib/src/ast/sass/expression/boolean.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -11,8 +10,7 @@ import '../expression.dart'; /// A boolean literal, `true` or `false`. /// /// {@category AST} -@sealed -class BooleanExpression implements Expression { +final class BooleanExpression implements Expression { /// The value of this expression. final bool value; diff --git a/lib/src/ast/sass/expression/calculation.dart b/lib/src/ast/sass/expression/calculation.dart index ac17dc0aa..38c25ed14 100644 --- a/lib/src/ast/sass/expression/calculation.dart +++ b/lib/src/ast/sass/expression/calculation.dart @@ -18,8 +18,7 @@ import 'variable.dart'; /// A calculation literal. /// /// {@category AST} -@sealed -class CalculationExpression implements Expression { +final class CalculationExpression implements Expression { /// This calculation's name. final String name; @@ -74,29 +73,31 @@ class CalculationExpression implements Expression { /// Throws an [ArgumentError] if [expression] isn't a valid calculation /// argument. static void _verify(Expression expression) { - if (expression is NumberExpression) return; - if (expression is CalculationExpression) return; - if (expression is VariableExpression) return; - if (expression is FunctionExpression) return; - if (expression is IfExpression) return; - - if (expression is StringExpression) { - if (expression.hasQuotes) { + switch (expression) { + case NumberExpression() || + CalculationExpression() || + VariableExpression() || + FunctionExpression() || + IfExpression() || + StringExpression(hasQuotes: false): + break; + + case ParenthesizedExpression(:var expression): + _verify(expression); + + case BinaryOperationExpression( + :var left, + :var right, + operator: BinaryOperator.plus || + BinaryOperator.minus || + BinaryOperator.times || + BinaryOperator.dividedBy + ): + _verify(left); + _verify(right); + + case _: throw ArgumentError("Invalid calculation argument $expression."); - } - } else if (expression is ParenthesizedExpression) { - _verify(expression.expression); - } else if (expression is BinaryOperationExpression) { - _verify(expression.left); - _verify(expression.right); - if (expression.operator == BinaryOperator.plus) return; - if (expression.operator == BinaryOperator.minus) return; - if (expression.operator == BinaryOperator.times) return; - if (expression.operator == BinaryOperator.dividedBy) return; - - throw ArgumentError("Invalid calculation argument $expression."); - } else { - throw ArgumentError("Invalid calculation argument $expression."); } } diff --git a/lib/src/ast/sass/expression/color.dart b/lib/src/ast/sass/expression/color.dart index eef0e97f4..e81a7f8b8 100644 --- a/lib/src/ast/sass/expression/color.dart +++ b/lib/src/ast/sass/expression/color.dart @@ -2,7 +2,6 @@ // 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 '../../../value.dart'; @@ -12,8 +11,7 @@ import '../expression.dart'; /// A color literal. /// /// {@category AST} -@sealed -class ColorExpression implements Expression { +final class ColorExpression implements Expression { /// The value of this color. final SassColor value; diff --git a/lib/src/ast/sass/expression/function.dart b/lib/src/ast/sass/expression/function.dart index 4d823ca2e..398a2ff03 100644 --- a/lib/src/ast/sass/expression/function.dart +++ b/lib/src/ast/sass/expression/function.dart @@ -2,7 +2,6 @@ // 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 '../../../util/span.dart'; @@ -18,8 +17,7 @@ import '../reference.dart'; /// interpolation. /// /// {@category AST} -@sealed -class FunctionExpression +final class FunctionExpression implements Expression, CallableInvocation, SassReference { /// The namespace of the function being invoked, or `null` if it's invoked /// without a namespace. diff --git a/lib/src/ast/sass/expression/if.dart b/lib/src/ast/sass/expression/if.dart index f41c4f8e9..8805d4bff 100644 --- a/lib/src/ast/sass/expression/if.dart +++ b/lib/src/ast/sass/expression/if.dart @@ -2,7 +2,6 @@ // 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 '../../../ast/sass.dart'; @@ -15,8 +14,7 @@ import '../../../visitor/interface/expression.dart'; /// evaluated. /// /// {@category AST} -@sealed -class IfExpression implements Expression, CallableInvocation { +final class IfExpression implements Expression, CallableInvocation { /// The declaration of `if()`, as though it were a normal function. static final declaration = ArgumentDeclaration.parse( r"@function if($condition, $if-true, $if-false) {"); diff --git a/lib/src/ast/sass/expression/interpolated_function.dart b/lib/src/ast/sass/expression/interpolated_function.dart index ec1941f64..3c97b0c9f 100644 --- a/lib/src/ast/sass/expression/interpolated_function.dart +++ b/lib/src/ast/sass/expression/interpolated_function.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -16,8 +15,8 @@ import '../interpolation.dart'; /// This is always a plain CSS function. /// /// {@category AST} -@sealed -class InterpolatedFunctionExpression implements Expression, CallableInvocation { +final class InterpolatedFunctionExpression + implements Expression, CallableInvocation { /// The name of the function being invoked. final Interpolation name; diff --git a/lib/src/ast/sass/expression/list.dart b/lib/src/ast/sass/expression/list.dart index 5361f68d1..01416afa4 100644 --- a/lib/src/ast/sass/expression/list.dart +++ b/lib/src/ast/sass/expression/list.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:charcode/charcode.dart'; -import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../../../value.dart'; @@ -14,8 +13,7 @@ import 'unary_operation.dart'; /// A list literal. /// /// {@category AST} -@sealed -class ListExpression implements Expression { +final class ListExpression implements Expression { /// The elements of this list. final List contents; @@ -62,22 +60,19 @@ class ListExpression implements Expression { /// Returns whether [expression], contained in [this], needs parentheses when /// printed as Sass source. - bool _elementNeedsParens(Expression expression) { - if (expression is ListExpression) { - if (expression.contents.length < 2) return false; - if (expression.hasBrackets) return false; - return separator == ListSeparator.comma - ? expression.separator == ListSeparator.comma - : expression.separator != ListSeparator.undecided; - } - - if (separator != ListSeparator.space) return false; - - if (expression is UnaryOperationExpression) { - return expression.operator == UnaryOperator.plus || - expression.operator == UnaryOperator.minus; - } - - return false; - } + bool _elementNeedsParens(Expression expression) => switch (expression) { + ListExpression( + contents: [_, _, ...], + hasBrackets: false, + separator: var childSeparator + ) => + separator == ListSeparator.comma + ? childSeparator == ListSeparator.comma + : childSeparator != ListSeparator.undecided, + UnaryOperationExpression( + operator: UnaryOperator.plus || UnaryOperator.minus + ) => + separator == ListSeparator.space, + _ => false + }; } diff --git a/lib/src/ast/sass/expression/map.dart b/lib/src/ast/sass/expression/map.dart index cabc4994f..9bc234780 100644 --- a/lib/src/ast/sass/expression/map.dart +++ b/lib/src/ast/sass/expression/map.dart @@ -2,9 +2,7 @@ // 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:tuple/tuple.dart'; import '../../../visitor/interface/expression.dart'; import '../expression.dart'; @@ -12,21 +10,20 @@ import '../expression.dart'; /// A map literal. /// /// {@category AST} -@sealed -class MapExpression implements Expression { +final class MapExpression implements Expression { /// The pairs in this map. /// /// This is a list of pairs rather than a map because a map may have two keys /// with the same expression (e.g. `(unique-id(): 1, unique-id(): 2)`). - final List> pairs; + final List<(Expression, Expression)> pairs; final FileSpan span; - MapExpression(Iterable> pairs, this.span) + MapExpression(Iterable<(Expression, Expression)> pairs, this.span) : pairs = List.unmodifiable(pairs); T accept(ExpressionVisitor visitor) => visitor.visitMapExpression(this); String toString() => - '(${pairs.map((pair) => '${pair.item1}: ${pair.item2}').join(', ')})'; + '(${[for (var (key, value) in pairs) '$key: $value'].join(', ')})'; } diff --git a/lib/src/ast/sass/expression/null.dart b/lib/src/ast/sass/expression/null.dart index 0e236753e..4155c00b0 100644 --- a/lib/src/ast/sass/expression/null.dart +++ b/lib/src/ast/sass/expression/null.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -11,8 +10,7 @@ import '../expression.dart'; /// A null literal. /// /// {@category AST} -@sealed -class NullExpression implements Expression { +final class NullExpression implements Expression { final FileSpan span; NullExpression(this.span); diff --git a/lib/src/ast/sass/expression/number.dart b/lib/src/ast/sass/expression/number.dart index ad1f1ed1e..7eb2b6fd9 100644 --- a/lib/src/ast/sass/expression/number.dart +++ b/lib/src/ast/sass/expression/number.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -12,8 +11,7 @@ import '../expression.dart'; /// A number literal. /// /// {@category AST} -@sealed -class NumberExpression implements Expression { +final class NumberExpression implements Expression { /// The numeric value. final double value; diff --git a/lib/src/ast/sass/expression/parenthesized.dart b/lib/src/ast/sass/expression/parenthesized.dart index 9b89731d3..3788645e3 100644 --- a/lib/src/ast/sass/expression/parenthesized.dart +++ b/lib/src/ast/sass/expression/parenthesized.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -11,8 +10,7 @@ import '../expression.dart'; /// An expression wrapped in parentheses. /// /// {@category AST} -@sealed -class ParenthesizedExpression implements Expression { +final class ParenthesizedExpression implements Expression { /// The internal expression. final Expression expression; diff --git a/lib/src/ast/sass/expression/selector.dart b/lib/src/ast/sass/expression/selector.dart index c5209a88f..81356690b 100644 --- a/lib/src/ast/sass/expression/selector.dart +++ b/lib/src/ast/sass/expression/selector.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -11,8 +10,7 @@ import '../expression.dart'; /// A parent selector reference, `&`. /// /// {@category AST} -@sealed -class SelectorExpression implements Expression { +final class SelectorExpression implements Expression { final FileSpan span; SelectorExpression(this.span); diff --git a/lib/src/ast/sass/expression/string.dart b/lib/src/ast/sass/expression/string.dart index 010907ff7..2e7824345 100644 --- a/lib/src/ast/sass/expression/string.dart +++ b/lib/src/ast/sass/expression/string.dart @@ -3,10 +3,11 @@ // https://opensource.org/licenses/MIT. import 'package:charcode/charcode.dart'; -import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../../../interpolation_buffer.dart'; +// dart-lang/sdk#52535 +// ignore: unused_import import '../../../util/character.dart'; import '../../../visitor/interface/expression.dart'; import '../expression.dart'; @@ -15,8 +16,7 @@ import '../interpolation.dart'; /// A string literal. /// /// {@category AST} -@sealed -class StringExpression implements Expression { +final class StringExpression implements Expression { /// Interpolation that, when evaluated, produces the contents of this string. /// /// Unlike [asInterpolation], escapes are resolved and quotes are not @@ -65,10 +65,11 @@ class StringExpression implements Expression { buffer.writeCharCode(quote); for (var value in text.contents) { assert(value is Expression || value is String); - if (value is Expression) { - buffer.add(value); - } else if (value is String) { - _quoteInnerText(value, quote, buffer, static: static); + switch (value) { + case Expression(): + buffer.add(value); + case String(): + _quoteInnerText(value, quote, buffer, static: static); } } buffer.writeCharCode(quote); @@ -84,27 +85,28 @@ class StringExpression implements Expression { static void _quoteInnerText(String text, int quote, StringSink buffer, {bool static = false}) { for (var i = 0; i < text.length; i++) { - var codeUnit = text.codeUnitAt(i); - - if (isNewline(codeUnit)) { - buffer.writeCharCode($backslash); - buffer.writeCharCode($a); - if (i != text.length - 1) { - var next = text.codeUnitAt(i + 1); - if (isWhitespace(next) || isHex(next)) { - buffer.writeCharCode($space); + switch (text.codeUnitAt(i)) { + case int(isNewline: true): + buffer.writeCharCode($backslash); + buffer.writeCharCode($a); + if (i != text.length - 1) { + if (text.codeUnitAt(i + 1) + case int(isWhitespace: true) || int(isHex: true)) { + buffer.writeCharCode($space); + } } - } - } else { - if (codeUnit == quote || - codeUnit == $backslash || - (static && - codeUnit == $hash && + + case $backslash && var codeUnit: + case var codeUnit when codeUnit == quote: + case $hash && var codeUnit + when static && i < text.length - 1 && - text.codeUnitAt(i + 1) == $lbrace)) { + text.codeUnitAt(i + 1) == $lbrace: buffer.writeCharCode($backslash); - } - buffer.writeCharCode(codeUnit); + buffer.writeCharCode(codeUnit); + + case var codeUnit: + buffer.writeCharCode(codeUnit); } } } @@ -114,8 +116,7 @@ class StringExpression implements Expression { static int _bestQuote(Iterable strings) { var containsDoubleQuote = false; for (var value in strings) { - for (var i = 0; i < value.length; i++) { - var codeUnit = value.codeUnitAt(i); + for (var codeUnit in value.codeUnits) { if (codeUnit == $single_quote) return $double_quote; if (codeUnit == $double_quote) containsDoubleQuote = true; } diff --git a/lib/src/ast/sass/expression/supports.dart b/lib/src/ast/sass/expression/supports.dart index 3aa28222c..d5de09a75 100644 --- a/lib/src/ast/sass/expression/supports.dart +++ b/lib/src/ast/sass/expression/supports.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -15,8 +14,7 @@ import '../supports_condition.dart'; /// doesn't include the function name wrapping the condition. /// /// {@category AST} -@sealed -class SupportsExpression implements Expression { +final class SupportsExpression implements Expression { /// The condition itself. final SupportsCondition condition; diff --git a/lib/src/ast/sass/expression/unary_operation.dart b/lib/src/ast/sass/expression/unary_operation.dart index 3a3df897f..d437fafc2 100644 --- a/lib/src/ast/sass/expression/unary_operation.dart +++ b/lib/src/ast/sass/expression/unary_operation.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:charcode/charcode.dart'; -import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../../../visitor/interface/expression.dart'; @@ -14,8 +13,7 @@ import 'list.dart'; /// A unary operator, as in `+$var` or `not fn()`. /// /// {@category AST} -@sealed -class UnaryOperationExpression implements Expression { +final class UnaryOperationExpression implements Expression { /// The operator being invoked. final UnaryOperator operator; @@ -33,11 +31,13 @@ class UnaryOperationExpression implements Expression { var buffer = StringBuffer(operator.operator); if (operator == UnaryOperator.not) buffer.writeCharCode($space); var operand = this.operand; - var needsParens = operand is BinaryOperationExpression || - operand is UnaryOperationExpression || - (operand is ListExpression && - !operand.hasBrackets && - operand.contents.length > 1); + var needsParens = switch (operand) { + BinaryOperationExpression() || + UnaryOperationExpression() || + ListExpression(hasBrackets: false, contents: [_, _, ...]) => + true, + _ => false + }; if (needsParens) buffer.write($lparen); buffer.write(operand); if (needsParens) buffer.write($rparen); diff --git a/lib/src/ast/sass/expression/value.dart b/lib/src/ast/sass/expression/value.dart index 55fd52390..75b01212e 100644 --- a/lib/src/ast/sass/expression/value.dart +++ b/lib/src/ast/sass/expression/value.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/expression.dart'; @@ -15,8 +14,7 @@ import '../expression.dart'; /// constructed dynamically, as for the `call()` function. /// /// {@category AST} -@sealed -class ValueExpression implements Expression { +final class ValueExpression implements Expression { /// The embedded value. final Value value; diff --git a/lib/src/ast/sass/expression/variable.dart b/lib/src/ast/sass/expression/variable.dart index 2c5eace35..c07ffbc5a 100644 --- a/lib/src/ast/sass/expression/variable.dart +++ b/lib/src/ast/sass/expression/variable.dart @@ -2,7 +2,6 @@ // 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 '../../../util/span.dart'; @@ -13,8 +12,7 @@ import '../reference.dart'; /// A Sass variable. /// /// {@category AST} -@sealed -class VariableExpression implements Expression, SassReference { +final class VariableExpression implements Expression, SassReference { /// The namespace of the variable being referenced, or `null` if it's /// referenced without a namespace. final String? namespace; diff --git a/lib/src/ast/sass/import.dart b/lib/src/ast/sass/import.dart index 3747a1a3b..7022f3b73 100644 --- a/lib/src/ast/sass/import.dart +++ b/lib/src/ast/sass/import.dart @@ -2,12 +2,9 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:meta/meta.dart'; - import 'node.dart'; /// An abstract superclass for different types of import. /// /// {@category AST} -@sealed -abstract class Import implements SassNode {} +abstract interface class Import implements SassNode {} diff --git a/lib/src/ast/sass/import/dynamic.dart b/lib/src/ast/sass/import/dynamic.dart index bb45cc6b0..38bb6c7f0 100644 --- a/lib/src/ast/sass/import/dynamic.dart +++ b/lib/src/ast/sass/import/dynamic.dart @@ -12,8 +12,7 @@ import '../import.dart'; /// An import that will load a Sass file at runtime. /// /// {@category AST} -@sealed -class DynamicImport implements Import, SassDependency { +final class DynamicImport implements Import, SassDependency { /// The URL of the file to import. /// /// If this is relative, it's relative to the containing file. diff --git a/lib/src/ast/sass/import/static.dart b/lib/src/ast/sass/import/static.dart index 69b1e3bb3..e20ee0d3f 100644 --- a/lib/src/ast/sass/import/static.dart +++ b/lib/src/ast/sass/import/static.dart @@ -2,7 +2,6 @@ // 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 '../import.dart'; @@ -11,8 +10,7 @@ import '../interpolation.dart'; /// An import that produces a plain CSS `@import` rule. /// /// {@category AST} -@sealed -class StaticImport implements Import { +final class StaticImport implements Import { /// The URL for this import. /// /// This already contains quotes. diff --git a/lib/src/ast/sass/interpolation.dart b/lib/src/ast/sass/interpolation.dart index d3a55971a..578394e83 100644 --- a/lib/src/ast/sass/interpolation.dart +++ b/lib/src/ast/sass/interpolation.dart @@ -12,8 +12,7 @@ import 'node.dart'; /// Plain text interpolated with Sass expressions. /// /// {@category AST} -@sealed -class Interpolation implements SassNode { +final class Interpolation implements SassNode { /// The contents of this interpolation. /// /// This contains [String]s and [Expression]s. It never contains two adjacent @@ -25,21 +24,15 @@ class Interpolation implements SassNode { /// If this contains no interpolated expressions, returns its text contents. /// /// Otherwise, returns `null`. - String? get asPlain { - if (contents.isEmpty) return ''; - if (contents.length > 1) return null; - var first = contents.first; - return first is String ? first : null; - } + String? get asPlain => + switch (contents) { [] => '', [String first] => first, _ => null }; /// Returns the plain text before the interpolation, or the empty string. /// /// @nodoc @internal - String get initialPlain { - var first = contents.first; - return first is String ? first : ''; - } + String get initialPlain => + switch (contents) { [String first, ...] => first, _ => '' }; /// Creates a new [Interpolation] by concatenating a sequence of [String]s, /// [Expression]s, or nested [Interpolation]s. @@ -48,15 +41,16 @@ class Interpolation implements SassNode { FileSpan span) { var buffer = InterpolationBuffer(); for (var element in contents) { - if (element is String) { - buffer.write(element); - } else if (element is Expression) { - buffer.add(element); - } else if (element is Interpolation) { - buffer.addInterpolation(element); - } else { - throw ArgumentError.value(contents, "contents", - "May only contains Strings, Expressions, or Interpolations."); + switch (element) { + case String(): + buffer.write(element); + case Expression(): + buffer.add(element); + case Interpolation(): + buffer.addInterpolation(element); + case _: + throw ArgumentError.value(contents, "contents", + "May only contains Strings, Expressions, or Interpolations."); } } diff --git a/lib/src/ast/sass/node.dart b/lib/src/ast/sass/node.dart index 4b8d28b08..ee5211a31 100644 --- a/lib/src/ast/sass/node.dart +++ b/lib/src/ast/sass/node.dart @@ -10,4 +10,4 @@ import '../node.dart'; /// /// {@category AST} @sealed -abstract class SassNode extends AstNode {} +abstract interface class SassNode implements AstNode {} diff --git a/lib/src/ast/sass/reference.dart b/lib/src/ast/sass/reference.dart index ffa838a00..06ed05b73 100644 --- a/lib/src/ast/sass/reference.dart +++ b/lib/src/ast/sass/reference.dart @@ -2,7 +2,6 @@ // 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 'node.dart'; @@ -10,8 +9,7 @@ import 'node.dart'; /// A common interface for any node that references a Sass member. /// /// {@category AST} -@sealed -abstract class SassReference extends SassNode { +abstract interface class SassReference implements SassNode { /// The namespace of the member being referenced, or `null` if it's referenced /// without a namespace. String? get namespace; diff --git a/lib/src/ast/sass/statement.dart b/lib/src/ast/sass/statement.dart index 57a2861b5..123cf3362 100644 --- a/lib/src/ast/sass/statement.dart +++ b/lib/src/ast/sass/statement.dart @@ -2,16 +2,13 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:meta/meta.dart'; - import '../../visitor/interface/statement.dart'; import 'node.dart'; /// A statement in a Sass syntax tree. /// /// {@category AST} -@sealed -abstract class Statement implements SassNode { +abstract interface class Statement implements SassNode { /// Calls the appropriate visit method on [visitor]. T accept(StatementVisitor visitor); } diff --git a/lib/src/ast/sass/statement/at_root_rule.dart b/lib/src/ast/sass/statement/at_root_rule.dart index 51b4f7c88..a354d4794 100644 --- a/lib/src/ast/sass/statement/at_root_rule.dart +++ b/lib/src/ast/sass/statement/at_root_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -15,8 +14,7 @@ import 'parent.dart'; /// This moves it contents "up" the tree through parent nodes. /// /// {@category AST} -@sealed -class AtRootRule extends ParentStatement> { +final class AtRootRule extends ParentStatement> { /// The query specifying which statements this should move its contents /// through. final Interpolation? query; diff --git a/lib/src/ast/sass/statement/at_rule.dart b/lib/src/ast/sass/statement/at_rule.dart index e896c82b6..48c1629f9 100644 --- a/lib/src/ast/sass/statement/at_rule.dart +++ b/lib/src/ast/sass/statement/at_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -13,8 +12,7 @@ import 'parent.dart'; /// An unknown at-rule. /// /// {@category AST} -@sealed -class AtRule extends ParentStatement { +final class AtRule extends ParentStatement { /// The name of this rule. final Interpolation name; diff --git a/lib/src/ast/sass/statement/callable_declaration.dart b/lib/src/ast/sass/statement/callable_declaration.dart index 7f7fec48a..e39b9c035 100644 --- a/lib/src/ast/sass/statement/callable_declaration.dart +++ b/lib/src/ast/sass/statement/callable_declaration.dart @@ -2,7 +2,6 @@ // 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 '../argument_declaration.dart'; @@ -14,8 +13,8 @@ import 'silent_comment.dart'; /// user code. /// /// {@category AST} -@sealed -abstract class CallableDeclaration extends ParentStatement> { +abstract base class CallableDeclaration + extends ParentStatement> { /// The name of this callable, with underscores converted to hyphens. final String name; diff --git a/lib/src/ast/sass/statement/content_block.dart b/lib/src/ast/sass/statement/content_block.dart index 99af7746f..618a49ea5 100644 --- a/lib/src/ast/sass/statement/content_block.dart +++ b/lib/src/ast/sass/statement/content_block.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -13,8 +12,7 @@ import 'callable_declaration.dart'; /// An anonymous block of code that's invoked for a [ContentRule]. /// /// {@category AST} -@sealed -class ContentBlock extends CallableDeclaration { +final class ContentBlock extends CallableDeclaration { ContentBlock(ArgumentDeclaration arguments, Iterable children, FileSpan span) : super("@content", arguments, children, span); diff --git a/lib/src/ast/sass/statement/content_rule.dart b/lib/src/ast/sass/statement/content_rule.dart index d2dcb6914..a05066ef0 100644 --- a/lib/src/ast/sass/statement/content_rule.dart +++ b/lib/src/ast/sass/statement/content_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -15,8 +14,7 @@ import '../statement.dart'; /// caller. /// /// {@category AST} -@sealed -class ContentRule implements Statement { +final class ContentRule implements Statement { /// The arguments pass to this `@content` rule. /// /// This will be an empty invocation if `@content` has no arguments. diff --git a/lib/src/ast/sass/statement/debug_rule.dart b/lib/src/ast/sass/statement/debug_rule.dart index 63601cf44..47c2d452d 100644 --- a/lib/src/ast/sass/statement/debug_rule.dart +++ b/lib/src/ast/sass/statement/debug_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -14,8 +13,7 @@ import '../statement.dart'; /// This prints a Sass value for debugging purposes. /// /// {@category AST} -@sealed -class DebugRule implements Statement { +final class DebugRule implements Statement { /// The expression to print. final Expression expression; diff --git a/lib/src/ast/sass/statement/declaration.dart b/lib/src/ast/sass/statement/declaration.dart index ad7505664..852bc3145 100644 --- a/lib/src/ast/sass/statement/declaration.dart +++ b/lib/src/ast/sass/statement/declaration.dart @@ -16,8 +16,7 @@ import 'parent.dart'; /// A declaration (that is, a `name: value` pair). /// /// {@category AST} -@sealed -class Declaration extends ParentStatement { +final class Declaration extends ParentStatement { /// The name of this declaration. final Interpolation name; @@ -59,13 +58,14 @@ class Declaration extends ParentStatement { buffer.writeCharCode($colon); if (value != null) { - if (!isCustomProperty) { - buffer.writeCharCode($space); - } + if (!isCustomProperty) buffer.writeCharCode($space); buffer.write("$value"); } - var children = this.children; - return children == null ? "$buffer;" : "$buffer {${children.join(" ")}}"; + if (children case var children?) { + return "$buffer {${children.join(" ")}}"; + } else { + return "$buffer;"; + } } } diff --git a/lib/src/ast/sass/statement/each_rule.dart b/lib/src/ast/sass/statement/each_rule.dart index bcdd37c07..68500ef56 100644 --- a/lib/src/ast/sass/statement/each_rule.dart +++ b/lib/src/ast/sass/statement/each_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -15,8 +14,7 @@ import 'parent.dart'; /// This iterates over values in a list or map. /// /// {@category AST} -@sealed -class EachRule extends ParentStatement> { +final class EachRule extends ParentStatement> { /// The variables assigned for each iteration. final List variables; diff --git a/lib/src/ast/sass/statement/error_rule.dart b/lib/src/ast/sass/statement/error_rule.dart index a82e404f4..977567cbd 100644 --- a/lib/src/ast/sass/statement/error_rule.dart +++ b/lib/src/ast/sass/statement/error_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -14,8 +13,7 @@ import '../statement.dart'; /// This emits an error and stops execution. /// /// {@category AST} -@sealed -class ErrorRule implements Statement { +final class ErrorRule implements Statement { /// The expression to evaluate for the error message. final Expression expression; diff --git a/lib/src/ast/sass/statement/extend_rule.dart b/lib/src/ast/sass/statement/extend_rule.dart index 7ba9f0c8c..8aa4e4e33 100644 --- a/lib/src/ast/sass/statement/extend_rule.dart +++ b/lib/src/ast/sass/statement/extend_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -14,8 +13,7 @@ import '../statement.dart'; /// This gives one selector all the styling of another. /// /// {@category AST} -@sealed -class ExtendRule implements Statement { +final class ExtendRule implements Statement { /// The interpolation for the selector that will be extended. final Interpolation selector; diff --git a/lib/src/ast/sass/statement/for_rule.dart b/lib/src/ast/sass/statement/for_rule.dart index 8aef52e51..008f4d1f2 100644 --- a/lib/src/ast/sass/statement/for_rule.dart +++ b/lib/src/ast/sass/statement/for_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -15,8 +14,7 @@ import 'parent.dart'; /// This iterates a set number of times. /// /// {@category AST} -@sealed -class ForRule extends ParentStatement> { +final class ForRule extends ParentStatement> { /// The name of the variable that will contain the index value. final String variable; diff --git a/lib/src/ast/sass/statement/forward_rule.dart b/lib/src/ast/sass/statement/forward_rule.dart index 499f502b7..eea2a226d 100644 --- a/lib/src/ast/sass/statement/forward_rule.dart +++ b/lib/src/ast/sass/statement/forward_rule.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../../../util/span.dart'; @@ -16,8 +15,7 @@ import '../statement.dart'; /// A `@forward` rule. /// /// {@category AST} -@sealed -class ForwardRule implements Statement, SassDependency { +final class ForwardRule implements Statement, SassDependency { /// The URI of the module to forward. /// /// If this is relative, it's relative to the containing file. diff --git a/lib/src/ast/sass/statement/function_rule.dart b/lib/src/ast/sass/statement/function_rule.dart index 0b2327e52..9242bf858 100644 --- a/lib/src/ast/sass/statement/function_rule.dart +++ b/lib/src/ast/sass/statement/function_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../util/span.dart'; @@ -18,8 +17,8 @@ import 'silent_comment.dart'; /// This declares a function that's invoked using normal CSS function syntax. /// /// {@category AST} -@sealed -class FunctionRule extends CallableDeclaration implements SassDeclaration { +final class FunctionRule extends CallableDeclaration + implements SassDeclaration { FileSpan get nameSpan => span.withoutInitialAtRule().initialIdentifier(); FunctionRule(String name, ArgumentDeclaration arguments, diff --git a/lib/src/ast/sass/statement/if_rule.dart b/lib/src/ast/sass/statement/if_rule.dart index 056cbd9f8..2a92ac28c 100644 --- a/lib/src/ast/sass/statement/if_rule.dart +++ b/lib/src/ast/sass/statement/if_rule.dart @@ -20,8 +20,7 @@ import 'variable_declaration.dart'; /// This conditionally executes a block of code. /// /// {@category AST} -@sealed -class IfRule implements Statement { +final class IfRule implements Statement { /// The `@if` and `@else if` clauses. /// /// The first clause whose expression evaluates to `true` will have its @@ -44,7 +43,8 @@ class IfRule implements Statement { String toString() { var result = clauses .mapIndexed((index, clause) => - "@${index == 0 ? 'if' : 'else if'} ${clause.expression} {${clause.children.join(' ')}}") + "@${index == 0 ? 'if' : 'else if'} ${clause.expression} " + "{${clause.children.join(' ')}}") .join(' '); var lastClause = this.lastClause; @@ -56,8 +56,7 @@ class IfRule implements Statement { /// The superclass of `@if` and `@else` clauses. /// /// {@category AST} -@sealed -abstract class IfRuleClause { +sealed class IfRuleClause { /// The statements to evaluate if this clause matches. final List children; @@ -71,19 +70,18 @@ abstract class IfRuleClause { : this._(List.unmodifiable(children)); IfRuleClause._(this.children) - : hasDeclarations = children.any((child) => - child is VariableDeclaration || - child is FunctionRule || - child is MixinRule || - (child is ImportRule && - child.imports.any((import) => import is DynamicImport))); + : hasDeclarations = children.any((child) => switch (child) { + VariableDeclaration() || FunctionRule() || MixinRule() => true, + ImportRule(:var imports) => + imports.any((import) => import is DynamicImport), + _ => false + }); } /// An `@if` or `@else if` clause in an `@if` rule. /// /// {@category AST} -@sealed -class IfClause extends IfRuleClause { +final class IfClause extends IfRuleClause { /// The expression to evaluate to determine whether to run this rule. final Expression expression; @@ -95,8 +93,7 @@ class IfClause extends IfRuleClause { /// An `@else` clause in an `@if` rule. /// /// {@category AST} -@sealed -class ElseClause extends IfRuleClause { +final class ElseClause extends IfRuleClause { ElseClause(Iterable children) : super(children); String toString() => "@else {${children.join(' ')}}"; diff --git a/lib/src/ast/sass/statement/import_rule.dart b/lib/src/ast/sass/statement/import_rule.dart index c18f8e12e..425c3ac42 100644 --- a/lib/src/ast/sass/statement/import_rule.dart +++ b/lib/src/ast/sass/statement/import_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -12,8 +11,7 @@ import '../statement.dart'; /// An `@import` rule. /// /// {@category AST} -@sealed -class ImportRule implements Statement { +final class ImportRule implements Statement { /// The imports imported by this statement. final List imports; diff --git a/lib/src/ast/sass/statement/include_rule.dart b/lib/src/ast/sass/statement/include_rule.dart index 263675284..d3c9ceba6 100644 --- a/lib/src/ast/sass/statement/include_rule.dart +++ b/lib/src/ast/sass/statement/include_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../util/span.dart'; @@ -16,8 +15,8 @@ import 'content_block.dart'; /// A mixin invocation. /// /// {@category AST} -@sealed -class IncludeRule implements Statement, CallableInvocation, SassReference { +final class IncludeRule + implements Statement, CallableInvocation, SassReference { /// The namespace of the mixin being invoked, or `null` if it's invoked /// without a namespace. final String? namespace; diff --git a/lib/src/ast/sass/statement/loud_comment.dart b/lib/src/ast/sass/statement/loud_comment.dart index 2876f1ad1..0c48e09fc 100644 --- a/lib/src/ast/sass/statement/loud_comment.dart +++ b/lib/src/ast/sass/statement/loud_comment.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -12,8 +11,7 @@ import '../statement.dart'; /// A loud CSS-style comment. /// /// {@category AST} -@sealed -class LoudComment implements Statement { +final class LoudComment implements Statement { /// The interpolated text of this comment, including comment characters. final Interpolation text; diff --git a/lib/src/ast/sass/statement/media_rule.dart b/lib/src/ast/sass/statement/media_rule.dart index ec63111c7..d219ca007 100644 --- a/lib/src/ast/sass/statement/media_rule.dart +++ b/lib/src/ast/sass/statement/media_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -13,8 +12,7 @@ import 'parent.dart'; /// A `@media` rule. /// /// {@category AST} -@sealed -class MediaRule extends ParentStatement> { +final class MediaRule extends ParentStatement> { /// The query that determines on which platforms the styles will be in effect. /// /// This is only parsed after the interpolation has been resolved. diff --git a/lib/src/ast/sass/statement/mixin_rule.dart b/lib/src/ast/sass/statement/mixin_rule.dart index 20e2ac254..624eff53e 100644 --- a/lib/src/ast/sass/statement/mixin_rule.dart +++ b/lib/src/ast/sass/statement/mixin_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../util/span.dart'; @@ -20,8 +19,7 @@ import 'silent_comment.dart'; /// This declares a mixin that's invoked using `@include`. /// /// {@category AST} -@sealed -class MixinRule extends CallableDeclaration implements SassDeclaration { +final class MixinRule extends CallableDeclaration implements SassDeclaration { /// Whether the mixin contains a `@content` rule. late final bool hasContent = const _HasContentVisitor().visitMixinRule(this) == true; diff --git a/lib/src/ast/sass/statement/parent.dart b/lib/src/ast/sass/statement/parent.dart index 329e45ba7..21293019d 100644 --- a/lib/src/ast/sass/statement/parent.dart +++ b/lib/src/ast/sass/statement/parent.dart @@ -17,8 +17,7 @@ import 'variable_declaration.dart'; /// not their children lists are nullable. /// /// {@category AST} -@sealed -abstract class ParentStatement?> +abstract base class ParentStatement?> implements Statement { /// The child statements of this statement. final T children; @@ -31,11 +30,14 @@ abstract class ParentStatement?> final bool hasDeclarations; ParentStatement(this.children) - : hasDeclarations = children?.any((child) => - child is VariableDeclaration || - child is FunctionRule || - child is MixinRule || - (child is ImportRule && - child.imports.any((import) => import is DynamicImport))) ?? + : hasDeclarations = children?.any((child) => switch (child) { + VariableDeclaration() || + FunctionRule() || + MixinRule() => + true, + ImportRule(:var imports) => + imports.any((import) => import is DynamicImport), + _ => false, + }) ?? false; } diff --git a/lib/src/ast/sass/statement/return_rule.dart b/lib/src/ast/sass/statement/return_rule.dart index bbf7fd370..dc1efc65c 100644 --- a/lib/src/ast/sass/statement/return_rule.dart +++ b/lib/src/ast/sass/statement/return_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -14,8 +13,7 @@ import '../statement.dart'; /// This exits from the current function body with a return value. /// /// {@category AST} -@sealed -class ReturnRule implements Statement { +final class ReturnRule implements Statement { /// The value to return from this function. final Expression expression; diff --git a/lib/src/ast/sass/statement/silent_comment.dart b/lib/src/ast/sass/statement/silent_comment.dart index 189c5d79f..384cd09fb 100644 --- a/lib/src/ast/sass/statement/silent_comment.dart +++ b/lib/src/ast/sass/statement/silent_comment.dart @@ -2,7 +2,6 @@ // 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:string_scanner/string_scanner.dart'; @@ -12,8 +11,7 @@ import '../statement.dart'; /// A silent Sass-style comment. /// /// {@category AST} -@sealed -class SilentComment implements Statement { +final class SilentComment implements Statement { /// The text of this comment, including comment characters. final String text; diff --git a/lib/src/ast/sass/statement/style_rule.dart b/lib/src/ast/sass/statement/style_rule.dart index 02b6c8ea9..32031c762 100644 --- a/lib/src/ast/sass/statement/style_rule.dart +++ b/lib/src/ast/sass/statement/style_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -15,8 +14,7 @@ import 'parent.dart'; /// This applies style declarations to elements that match a given selector. /// /// {@category AST} -@sealed -class StyleRule extends ParentStatement> { +final class StyleRule extends ParentStatement> { /// The selector to which the declaration will be applied. /// /// This is only parsed after the interpolation has been resolved. diff --git a/lib/src/ast/sass/statement/stylesheet.dart b/lib/src/ast/sass/statement/stylesheet.dart index 709509e0d..b90cddc52 100644 --- a/lib/src/ast/sass/statement/stylesheet.dart +++ b/lib/src/ast/sass/statement/stylesheet.dart @@ -13,6 +13,7 @@ import '../../../parse/css.dart'; import '../../../parse/sass.dart'; import '../../../parse/scss.dart'; import '../../../syntax.dart'; +import '../../../utils.dart'; import '../../../visitor/interface/statement.dart'; import '../statement.dart'; import 'forward_rule.dart'; @@ -28,8 +29,7 @@ import 'variable_declaration.dart'; /// /// {@category AST} /// {@category Parsing} -@sealed -class Stylesheet extends ParentStatement> { +final class Stylesheet extends ParentStatement> { final FileSpan span; /// Whether this was parsed from a plain CSS stylesheet. @@ -56,15 +56,22 @@ class Stylesheet extends ParentStatement> { Stylesheet.internal(Iterable children, this.span, {this.plainCss = false}) : super(List.unmodifiable(children)) { + loop: for (var child in this.children) { - if (child is UseRule) { - _uses.add(child); - } else if (child is ForwardRule) { - _forwards.add(child); - } else if (child is! SilentComment && - child is! LoudComment && - child is! VariableDeclaration) { - break; + switch (child) { + case UseRule(): + _uses.add(child); + + case ForwardRule(): + _forwards.add(child); + + case SilentComment() || LoudComment() || VariableDeclaration(): + // These are allowed between `@use` and `@forward` rules. + break; + + case _: + break loop; + // Once we reach anything else, we know we're done with loads. } } } @@ -87,11 +94,12 @@ class Stylesheet extends ParentStatement> { default: throw ArgumentError("Unknown syntax $syntax."); } - } on SassException catch (error) { + } on SassException catch (error, stackTrace) { var url = error.span.sourceUrl; if (url == null || url.toString() == 'stdin') rethrow; - throw error.withLoadedUrls(Set.unmodifiable({url})); + throw throwWithTrace( + error.withLoadedUrls(Set.unmodifiable({url})), error, stackTrace); } } diff --git a/lib/src/ast/sass/statement/supports_rule.dart b/lib/src/ast/sass/statement/supports_rule.dart index fe23d97ad..13bba084f 100644 --- a/lib/src/ast/sass/statement/supports_rule.dart +++ b/lib/src/ast/sass/statement/supports_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -13,8 +12,7 @@ import 'parent.dart'; /// A `@supports` rule. /// /// {@category AST} -@sealed -class SupportsRule extends ParentStatement> { +final class SupportsRule extends ParentStatement> { /// The condition that selects what browsers this rule targets. final SupportsCondition condition; diff --git a/lib/src/ast/sass/statement/use_rule.dart b/lib/src/ast/sass/statement/use_rule.dart index 0e84759ad..244613abc 100644 --- a/lib/src/ast/sass/statement/use_rule.dart +++ b/lib/src/ast/sass/statement/use_rule.dart @@ -18,8 +18,7 @@ import '../statement.dart'; /// A `@use` rule. /// /// {@category AST} -@sealed -class UseRule implements Statement, SassDependency { +final class UseRule implements Statement, SassDependency { /// The URI of the module to use. /// /// If this is relative, it's relative to the containing file. diff --git a/lib/src/ast/sass/statement/variable_declaration.dart b/lib/src/ast/sass/statement/variable_declaration.dart index a82401b0a..235a41648 100644 --- a/lib/src/ast/sass/statement/variable_declaration.dart +++ b/lib/src/ast/sass/statement/variable_declaration.dart @@ -21,8 +21,7 @@ import 'silent_comment.dart'; /// This defines or sets a variable. /// /// {@category AST} -@sealed -class VariableDeclaration implements Statement, SassDeclaration { +final class VariableDeclaration implements Statement, SassDeclaration { /// The namespace of the variable being set, or `null` if it's defined or set /// without a namespace. final String? namespace; diff --git a/lib/src/ast/sass/statement/warn_rule.dart b/lib/src/ast/sass/statement/warn_rule.dart index ae3ccbef7..026f4ca34 100644 --- a/lib/src/ast/sass/statement/warn_rule.dart +++ b/lib/src/ast/sass/statement/warn_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -14,8 +13,7 @@ import '../statement.dart'; /// This prints a Sass value—usually a string—to warn the user of something. /// /// {@category AST} -@sealed -class WarnRule implements Statement { +final class WarnRule implements Statement { /// The expression to print. final Expression expression; diff --git a/lib/src/ast/sass/statement/while_rule.dart b/lib/src/ast/sass/statement/while_rule.dart index 18e8f6f94..34b39d52a 100644 --- a/lib/src/ast/sass/statement/while_rule.dart +++ b/lib/src/ast/sass/statement/while_rule.dart @@ -2,7 +2,6 @@ // 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 '../../../visitor/interface/statement.dart'; @@ -16,8 +15,7 @@ import 'parent.dart'; /// `true`. /// /// {@category AST} -@sealed -class WhileRule extends ParentStatement> { +final class WhileRule extends ParentStatement> { /// The condition that determines whether the block will be executed. final Expression condition; diff --git a/lib/src/ast/sass/supports_condition.dart b/lib/src/ast/sass/supports_condition.dart index 53f96bb38..4b38d304e 100644 --- a/lib/src/ast/sass/supports_condition.dart +++ b/lib/src/ast/sass/supports_condition.dart @@ -2,12 +2,9 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:meta/meta.dart'; - import 'node.dart'; /// An abstract class for defining the condition a `@supports` rule selects. /// /// {@category AST} -@sealed -abstract class SupportsCondition extends SassNode {} +abstract interface class SupportsCondition implements SassNode {} diff --git a/lib/src/ast/sass/supports_condition/anything.dart b/lib/src/ast/sass/supports_condition/anything.dart index 4dbece4b2..91d90024a 100644 --- a/lib/src/ast/sass/supports_condition/anything.dart +++ b/lib/src/ast/sass/supports_condition/anything.dart @@ -2,7 +2,6 @@ // 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 '../interpolation.dart'; @@ -12,8 +11,7 @@ import '../supports_condition.dart'; /// `` production. /// /// {@category AST} -@sealed -class SupportsAnything implements SupportsCondition { +final class SupportsAnything implements SupportsCondition { /// The contents of the condition. final Interpolation contents; diff --git a/lib/src/ast/sass/supports_condition/declaration.dart b/lib/src/ast/sass/supports_condition/declaration.dart index d29d717c9..322731018 100644 --- a/lib/src/ast/sass/supports_condition/declaration.dart +++ b/lib/src/ast/sass/supports_condition/declaration.dart @@ -13,8 +13,7 @@ import '../supports_condition.dart'; /// supported. /// /// {@category AST} -@sealed -class SupportsDeclaration implements SupportsCondition { +final class SupportsDeclaration implements SupportsCondition { /// The name of the declaration being tested. final Expression name; @@ -33,12 +32,11 @@ class SupportsDeclaration implements SupportsCondition { /// /// @nodoc @internal - bool get isCustomProperty { - var name = this.name; - return name is StringExpression && - !name.hasQuotes && - name.text.initialPlain.startsWith('--'); - } + bool get isCustomProperty => switch (name) { + StringExpression(hasQuotes: false, :var text) => + text.initialPlain.startsWith('--'), + _ => false + }; SupportsDeclaration(this.name, this.value, this.span); diff --git a/lib/src/ast/sass/supports_condition/function.dart b/lib/src/ast/sass/supports_condition/function.dart index 73bdb9bda..dd9ac5b29 100644 --- a/lib/src/ast/sass/supports_condition/function.dart +++ b/lib/src/ast/sass/supports_condition/function.dart @@ -2,7 +2,6 @@ // 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 '../interpolation.dart'; @@ -11,8 +10,7 @@ import '../supports_condition.dart'; /// A function-syntax condition. /// /// {@category AST} -@sealed -class SupportsFunction implements SupportsCondition { +final class SupportsFunction implements SupportsCondition { /// The name of the function. final Interpolation name; diff --git a/lib/src/ast/sass/supports_condition/interpolation.dart b/lib/src/ast/sass/supports_condition/interpolation.dart index 9fbd85829..839fccf9f 100644 --- a/lib/src/ast/sass/supports_condition/interpolation.dart +++ b/lib/src/ast/sass/supports_condition/interpolation.dart @@ -2,7 +2,6 @@ // 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 '../expression.dart'; @@ -11,8 +10,7 @@ import '../supports_condition.dart'; /// An interpolated condition. /// /// {@category AST} -@sealed -class SupportsInterpolation implements SupportsCondition { +final class SupportsInterpolation implements SupportsCondition { /// The expression in the interpolation. final Expression expression; diff --git a/lib/src/ast/sass/supports_condition/negation.dart b/lib/src/ast/sass/supports_condition/negation.dart index 4187f3793..23cd7193e 100644 --- a/lib/src/ast/sass/supports_condition/negation.dart +++ b/lib/src/ast/sass/supports_condition/negation.dart @@ -2,7 +2,6 @@ // 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 '../supports_condition.dart'; @@ -11,8 +10,7 @@ import 'operation.dart'; /// A negated condition. /// /// {@category AST} -@sealed -class SupportsNegation implements SupportsCondition { +final class SupportsNegation implements SupportsCondition { /// The condition that's been negated. final SupportsCondition condition; diff --git a/lib/src/ast/sass/supports_condition/operation.dart b/lib/src/ast/sass/supports_condition/operation.dart index 3e4fc5113..f072fc2e3 100644 --- a/lib/src/ast/sass/supports_condition/operation.dart +++ b/lib/src/ast/sass/supports_condition/operation.dart @@ -2,7 +2,6 @@ // 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 '../supports_condition.dart'; @@ -11,8 +10,7 @@ import 'negation.dart'; /// An operation defining the relationship between two conditions. /// /// {@category AST} -@sealed -class SupportsOperation implements SupportsCondition { +final class SupportsOperation implements SupportsCondition { /// The left-hand operand. final SupportsCondition left; diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart index 35c5a2f54..953ccf7aa 100644 --- a/lib/src/ast/selector.dart +++ b/lib/src/ast/selector.dart @@ -41,7 +41,7 @@ export 'selector/universal.dart'; /// Selectors have structural equality semantics. /// /// {@category AST} -abstract class Selector implements AstNode { +abstract base class Selector implements AstNode { /// Whether this selector, and complex selectors containing it, should not be /// emitted. /// @@ -121,16 +121,17 @@ class _IsInvisibleVisitor with AnySelectorVisitor { bool visitPlaceholderSelector(PlaceholderSelector placeholder) => true; bool visitPseudoSelector(PseudoSelector pseudo) { - var selector = pseudo.selector; - if (selector == null) return false; - - // We don't consider `:not(%foo)` to be invisible because, semantically, it - // means "doesn't match this selector that matches nothing", so it's - // equivalent to *. If the entire compound selector is composed of `:not`s - // with invisible lists, the serializer emits it as `*`. - return pseudo.name == 'not' - ? (includeBogus && selector.isBogus) - : selector.accept(this); + if (pseudo.selector case var selector?) { + // We don't consider `:not(%foo)` to be invisible because, semantically, + // it means "doesn't match this selector that matches nothing", so it's + // equivalent to *. If the entire compound selector is composed of `:not`s + // with invisible lists, the serializer emits it as `*`. + return pseudo.name == 'not' + ? (includeBogus && selector.isBogus) + : selector.accept(this); + } else { + return false; + } } } diff --git a/lib/src/ast/selector/attribute.dart b/lib/src/ast/selector/attribute.dart index 3254ab20c..dcdedf54a 100644 --- a/lib/src/ast/selector/attribute.dart +++ b/lib/src/ast/selector/attribute.dart @@ -2,7 +2,6 @@ // 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 '../../visitor/interface/selector.dart'; @@ -14,8 +13,7 @@ import '../selector.dart'; /// value matching certain conditions as well. /// /// {@category AST} -@sealed -class AttributeSelector extends SimpleSelector { +final class AttributeSelector extends SimpleSelector { /// The name of the attribute being selected for. final QualifiedName name; diff --git a/lib/src/ast/selector/class.dart b/lib/src/ast/selector/class.dart index 60124c5c4..b01ce9da5 100644 --- a/lib/src/ast/selector/class.dart +++ b/lib/src/ast/selector/class.dart @@ -14,8 +14,7 @@ import '../selector.dart'; /// the given name. /// /// {@category AST} -@sealed -class ClassSelector extends SimpleSelector { +final class ClassSelector extends SimpleSelector { /// The class name this selects for. final String name; diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart index 708785fe3..3d97729ca 100644 --- a/lib/src/ast/selector/complex.dart +++ b/lib/src/ast/selector/complex.dart @@ -20,8 +20,7 @@ import '../selector.dart'; /// /// {@category AST} /// {@category Parsing} -@sealed -class ComplexSelector extends Selector { +final class ComplexSelector extends Selector { /// This selector's leading combinators. /// /// If this is empty, that indicates that it has no leading combinator. If @@ -62,11 +61,13 @@ class ComplexSelector extends Selector { /// /// @nodoc @internal - CompoundSelector? get singleCompound => leadingCombinators.isEmpty && - components.length == 1 && - components.first.combinators.isEmpty - ? components.first.selector - : null; + CompoundSelector? get singleCompound { + if (leadingCombinators.isNotEmpty) return null; + return switch (components) { + [ComplexSelectorComponent(:var selector, combinators: [])] => selector, + _ => null + }; + } ComplexSelector(Iterable> leadingCombinators, Iterable components, FileSpan span, @@ -115,22 +116,15 @@ class ComplexSelector extends Selector { ComplexSelector withAdditionalCombinators( List> combinators, {bool forceLineBreak = false}) { - if (combinators.isEmpty) { - return this; - } else if (components.isEmpty) { - return ComplexSelector( + if (combinators.isEmpty) return this; + return switch (components) { + [...var initial, var last] => ComplexSelector(leadingCombinators, + [...initial, last.withAdditionalCombinators(combinators)], span, + lineBreak: lineBreak || forceLineBreak), + [] => ComplexSelector( [...leadingCombinators, ...combinators], const [], span, - lineBreak: lineBreak || forceLineBreak); - } else { - return ComplexSelector( - leadingCombinators, - [ - ...components.exceptLast, - components.last.withAdditionalCombinators(combinators) - ], - span, - lineBreak: lineBreak || forceLineBreak); - } + lineBreak: lineBreak || forceLineBreak) + }; } /// Returns a copy of `this` with an additional [component] added to the end. @@ -166,22 +160,22 @@ class ComplexSelector extends Selector { return ComplexSelector( 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 { + } else if (components case [...var initial, var last]) { return ComplexSelector( leadingCombinators, [ - ...components.exceptLast, - components.last.withAdditionalCombinators(child.leadingCombinators), + ...initial, + last.withAdditionalCombinators(child.leadingCombinators), ...child.components ], span, lineBreak: lineBreak || child.lineBreak || forceLineBreak); + } else { + return ComplexSelector( + [...leadingCombinators, ...child.leadingCombinators], + child.components, + span, + lineBreak: lineBreak || child.lineBreak || forceLineBreak); } } diff --git a/lib/src/ast/selector/complex_component.dart b/lib/src/ast/selector/complex_component.dart index a3142f6eb..70f6d8e42 100644 --- a/lib/src/ast/selector/complex_component.dart +++ b/lib/src/ast/selector/complex_component.dart @@ -14,8 +14,7 @@ import '../selector.dart'; /// This a [CompoundSelector] with one or more trailing [Combinator]s. /// /// {@category AST} -@sealed -class ComplexSelectorComponent { +final class ComplexSelectorComponent { /// This component's compound selector. final CompoundSelector selector; diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index 19e5d7ade..c36662cb0 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.dart @@ -19,8 +19,7 @@ import '../selector.dart'; /// /// {@category AST} /// {@category Parsing} -@sealed -class CompoundSelector extends Selector { +final class CompoundSelector extends Selector { /// The components of this selector. /// /// This is never empty. diff --git a/lib/src/ast/selector/id.dart b/lib/src/ast/selector/id.dart index 9d9442c71..dc820fba3 100644 --- a/lib/src/ast/selector/id.dart +++ b/lib/src/ast/selector/id.dart @@ -15,8 +15,7 @@ import '../selector.dart'; /// This selects elements whose `id` attribute exactly matches the given name. /// /// {@category AST} -@sealed -class IDSelector extends SimpleSelector { +final class IDSelector extends SimpleSelector { /// The ID name this selects for. final String name; diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index a45c0c07a..d432bbfaa 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -11,6 +11,7 @@ import '../../interpolation_map.dart'; import '../../logger.dart'; import '../../parse/selector.dart'; import '../../utils.dart'; +import '../../util/iterable.dart'; import '../../util/span.dart'; import '../../value.dart'; import '../../visitor/interface/selector.dart'; @@ -25,8 +26,7 @@ import '../selector.dart'; /// /// {@category AST} /// {@category Parsing} -@sealed -class SelectorList extends Selector { +final class SelectorList extends Selector { /// The components of this selector. /// /// This is never empty. @@ -180,14 +180,13 @@ class SelectorList extends Selector { } var resolvedSimples = containsSelectorPseudo - ? simples.map((simple) { - if (simple is! PseudoSelector) return simple; - var selector = simple.selector; - if (selector == null) return simple; - if (!_containsParentSelector(selector)) return simple; - return simple.withSelector( - selector.resolveParentSelectors(parent, implicitParent: false)); - }) + ? simples.map((simple) => switch (simple) { + PseudoSelector(:var selector?) + when _containsParentSelector(selector) => + simple.withSelector(selector.resolveParentSelectors(parent, + implicitParent: false)), + _ => simple + }) : simples; var parentSelector = simples.first; @@ -209,6 +208,7 @@ class SelectorList extends Selector { } on SassException catch (error, stackTrace) { throwWithTrace( error.withAdditionalSpan(parentSelector.span, "parent selector"), + error, stackTrace); } @@ -248,6 +248,7 @@ class SelectorList extends Selector { } on SassException catch (error, stackTrace) { throwWithTrace( error.withAdditionalSpan(parentSelector.span, "parent selector"), + error, stackTrace); } }); diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart index 1cde57f9d..18e898652 100644 --- a/lib/src/ast/selector/parent.dart +++ b/lib/src/ast/selector/parent.dart @@ -14,8 +14,7 @@ import '../selector.dart'; /// document. /// /// {@category AST} -@sealed -class ParentSelector extends SimpleSelector { +final class ParentSelector extends SimpleSelector { /// The suffix that will be added to the parent selector after it's been /// resolved. /// diff --git a/lib/src/ast/selector/placeholder.dart b/lib/src/ast/selector/placeholder.dart index 97ef14e4f..a99005d21 100644 --- a/lib/src/ast/selector/placeholder.dart +++ b/lib/src/ast/selector/placeholder.dart @@ -16,8 +16,7 @@ import '../selector.dart'; /// emitting a CSS document. /// /// {@category AST} -@sealed -class PlaceholderSelector extends SimpleSelector { +final class PlaceholderSelector extends SimpleSelector { /// The name of the placeholder. final String name; diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index 2b3078b24..44a263d15 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -20,8 +20,7 @@ import '../selector.dart'; /// ensure that extension and other selector operations work properly. /// /// {@category AST} -@sealed -class PseudoSelector extends SimpleSelector { +final class PseudoSelector extends SimpleSelector { /// The name of this selector. final String name; @@ -157,12 +156,11 @@ class PseudoSelector extends SimpleSelector { (simple.isHost || simple.selector != null))) { return null; } - } else if (compound.length == 1) { - var other = compound.first; - if (other is UniversalSelector || - (other is PseudoSelector && (other.isHost || other.isHostContext))) { - return other.unify([this]); - } + } else if (compound case [var other] + when other is UniversalSelector || + (other is PseudoSelector && + (other.isHost || other.isHostContext))) { + return other.unify([this]); } if (compound.contains(this)) return compound; @@ -170,7 +168,7 @@ class PseudoSelector extends SimpleSelector { var result = []; var addedThis = false; for (var simple in compound) { - if (simple is PseudoSelector && simple.isElement) { + if (simple case PseudoSelector(isElement: true)) { // A given compound selector may only contain one pseudo element. If // [compound] has a different one than [this], unification fails. if (isElement) return null; diff --git a/lib/src/ast/selector/qualified_name.dart b/lib/src/ast/selector/qualified_name.dart index 05bb4a084..6a594a2c7 100644 --- a/lib/src/ast/selector/qualified_name.dart +++ b/lib/src/ast/selector/qualified_name.dart @@ -2,15 +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'; - /// A [qualified name]. /// /// [qualified name]: https://www.w3.org/TR/css3-namespace/#css-qnames /// /// {@category AST} -@sealed -class QualifiedName { +final class QualifiedName { /// The identifier name. final String name; diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index d2da89fdc..0526eed72 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -27,7 +27,7 @@ final _subselectorPseudos = { /// /// {@category AST} /// {@category Parsing} -abstract class SimpleSelector extends Selector { +abstract base class SimpleSelector extends Selector { /// This selector's specificity. /// /// Specificity is represented in base 1000. The spec says this should be @@ -74,12 +74,11 @@ abstract class SimpleSelector extends Selector { /// @nodoc @internal List? unify(List compound) { - if (compound.length == 1) { - var other = compound.first; - if (other is UniversalSelector || - (other is PseudoSelector && (other.isHost || other.isHostContext))) { - return other.unify([this]); - } + if (compound case [var other] + when other is UniversalSelector || + (other is PseudoSelector && + (other.isHost || other.isHostContext))) { + return other.unify([this]); } if (compound.contains(this)) return compound; diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart index b021a6383..d65f94a0c 100644 --- a/lib/src/ast/selector/type.dart +++ b/lib/src/ast/selector/type.dart @@ -14,8 +14,7 @@ import '../selector.dart'; /// This selects elements whose name equals the given name. /// /// {@category AST} -@sealed -class TypeSelector extends SimpleSelector { +final class TypeSelector extends SimpleSelector { /// The element name being selected. final QualifiedName name; @@ -33,7 +32,7 @@ class TypeSelector extends SimpleSelector { /// @nodoc @internal List? unify(List compound) { - if (compound.first is UniversalSelector || compound.first is TypeSelector) { + if (compound.first case UniversalSelector() || TypeSelector()) { var unified = unifyUniversalAndElement(this, compound.first); if (unified == null) return null; return [unified, ...compound.skip(1)]; diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index d3d8fa2a5..d714dcb6a 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -12,8 +12,7 @@ import '../selector.dart'; /// Matches any element in the given namespace. /// /// {@category AST} -@sealed -class UniversalSelector extends SimpleSelector { +final class UniversalSelector extends SimpleSelector { /// The selector namespace. /// /// If this is `null`, this matches all elements in the default namespace. If @@ -32,20 +31,23 @@ class UniversalSelector extends SimpleSelector { /// @nodoc @internal List? unify(List compound) { - var first = compound.first; - if (first is UniversalSelector || first is TypeSelector) { - var unified = unifyUniversalAndElement(this, first); - if (unified == null) return null; - return [unified, ...compound.skip(1)]; - } else if (compound.length == 1 && - first is PseudoSelector && - (first.isHost || first.isHostContext)) { - return null; - } + switch (compound) { + case [UniversalSelector() || TypeSelector(), ...var rest]: + var unified = unifyUniversalAndElement(this, compound.first); + if (unified == null) return null; + return [unified, ...rest]; + + case [PseudoSelector first] when first.isHost || first.isHostContext: + return null; - if (namespace != null && namespace != "*") return [this, ...compound]; - if (compound.isNotEmpty) return compound; - return [this]; + case []: + return [this]; + + case _: + return namespace == null || namespace == "*" + ? compound + : [this, ...compound]; + } } bool isSuperselector(SimpleSelector other) { diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index f1c6ed4bb..0d95a5dd7 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -170,9 +170,7 @@ Future _compileStylesheet( var resultSourceMap = serializeResult.sourceMap; if (resultSourceMap != null && importCache != null) { - // TODO(nweiz): Don't explicitly use a type parameter when dart-lang/sdk#25490 - // is fixed. - mapInPlace( + mapInPlace( resultSourceMap.urls, (url) => url == '' ? Uri.dataFromString(stylesheet.span.file.getText(0), diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index 3df9de9ff..96cbecc18 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -17,6 +17,7 @@ import 'extend/extension_store.dart'; import 'module.dart'; import 'module/forwarded_view.dart'; import 'module/shadowed_view.dart'; +import 'util/map.dart'; import 'util/merged_map_view.dart'; import 'util/nullable.dart'; import 'util/public_member_map_view.dart'; @@ -33,7 +34,7 @@ import 'visitor/clone_css.dart'; /// /// This tracks lexically-scoped information, such as variables, functions, and /// mixins. -class AsyncEnvironment { +final class AsyncEnvironment { /// The modules used in the current scope, indexed by their namespaces. Map get modules => UnmodifiableMapView(_modules); final Map _modules; @@ -235,12 +236,11 @@ class AsyncEnvironment { _globalModules[module] = nodeWithSpan; _allModules.add(module); - for (var name in _variables.first.keys) { - if (module.variables.containsKey(name)) { - throw SassScriptException( - 'This module and the new module both define a variable named ' - '"\$$name".'); - } + if (_variables.first.keys.firstWhereOrNull(module.variables.containsKey) + case var name?) { + throw SassScriptException( + 'This module and the new module both define a variable named ' + '"\$$name".'); } } else { if (_modules.containsKey(namespace)) { @@ -299,11 +299,12 @@ class AsyncEnvironment { larger = newMembers; } - for (var name in smaller.keys) { - if (!larger.containsKey(name)) continue; + for (var (name, small) in smaller.pairs) { + var large = larger[name]; + if (large == null) continue; if (type == "variable" ? newModule.variableIdentity(name) == oldModule.variableIdentity(name) - : larger[name] == smaller[name]) { + : large == small) { continue; } @@ -321,82 +322,82 @@ class AsyncEnvironment { /// /// This is called when [module] is `@import`ed. void importForwards(Module module) { - if (module is _EnvironmentModule) { - var forwarded = module._environment._forwardedModules; - if (forwarded == null) return; - - // Omit modules from [forwarded] that are already globally available and - // forwarded in this module. - var forwardedModules = _forwardedModules; - if (forwardedModules != null) { - forwarded = { - for (var entry in forwarded.entries) - if (!forwardedModules.containsKey(entry.key) || - !_globalModules.containsKey(entry.key)) - entry.key: entry.value, - }; - } else { - forwardedModules = _forwardedModules ??= {}; - } + if (module is! _EnvironmentModule) return; + var forwarded = module._environment._forwardedModules; + if (forwarded == null) return; + + // Omit modules from [forwarded] that are already globally available and + // forwarded in this module. + var forwardedModules = _forwardedModules; + if (forwardedModules != null) { + forwarded = { + for (var (module, node) in forwarded.pairs) + if (!forwardedModules.containsKey(module) || + !_globalModules.containsKey(module)) + module: node, + }; + } else { + forwardedModules = _forwardedModules ??= {}; + } - var forwardedVariableNames = - forwarded.keys.expand((module) => module.variables.keys).toSet(); - var forwardedFunctionNames = - forwarded.keys.expand((module) => module.functions.keys).toSet(); - var forwardedMixinNames = - forwarded.keys.expand((module) => module.mixins.keys).toSet(); - - if (atRoot) { - // Hide members from modules that have already been imported or - // forwarded that would otherwise conflict with the @imported members. - for (var entry in _importedModules.entries.toList()) { - var module = entry.key; - var shadowed = ShadowedModuleView.ifNecessary(module, - variables: forwardedVariableNames, - mixins: forwardedMixinNames, - functions: forwardedFunctionNames); - if (shadowed != null) { - _importedModules.remove(module); - if (!shadowed.isEmpty) _importedModules[shadowed] = entry.value; - } + var forwardedVariableNames = { + for (var module in forwarded.keys) ...module.variables.keys + }; + var forwardedFunctionNames = { + for (var module in forwarded.keys) ...module.functions.keys + }; + var forwardedMixinNames = { + for (var module in forwarded.keys) ...module.mixins.keys + }; + + if (atRoot) { + // Hide members from modules that have already been imported or + // forwarded that would otherwise conflict with the @imported members. + for (var (module, node) in _importedModules.pairs.toList()) { + var shadowed = ShadowedModuleView.ifNecessary(module, + variables: forwardedVariableNames, + mixins: forwardedMixinNames, + functions: forwardedFunctionNames); + if (shadowed != null) { + _importedModules.remove(module); + if (!shadowed.isEmpty) _importedModules[shadowed] = node; } + } - for (var entry in forwardedModules.entries.toList()) { - var module = entry.key; - var shadowed = ShadowedModuleView.ifNecessary(module, - variables: forwardedVariableNames, - mixins: forwardedMixinNames, - functions: forwardedFunctionNames); - if (shadowed != null) { - forwardedModules.remove(module); - if (!shadowed.isEmpty) forwardedModules[shadowed] = entry.value; - } + for (var (module, node) in forwardedModules.pairs.toList()) { + var shadowed = ShadowedModuleView.ifNecessary(module, + variables: forwardedVariableNames, + mixins: forwardedMixinNames, + functions: forwardedFunctionNames); + if (shadowed != null) { + forwardedModules.remove(module); + if (!shadowed.isEmpty) forwardedModules[shadowed] = node; } - - _importedModules.addAll(forwarded); - forwardedModules.addAll(forwarded); - } else { - (_nestedForwardedModules ??= - List.generate(_variables.length - 1, (_) => [])) - .last - .addAll(forwarded.keys); } - // Remove existing member definitions that are now shadowed by the - // forwarded modules. - for (var variable in forwardedVariableNames) { - _variableIndices.remove(variable); - _variables.last.remove(variable); - _variableNodes.last.remove(variable); - } - for (var function in forwardedFunctionNames) { - _functionIndices.remove(function); - _functions.last.remove(function); - } - for (var mixin in forwardedMixinNames) { - _mixinIndices.remove(mixin); - _mixins.last.remove(mixin); - } + _importedModules.addAll(forwarded); + forwardedModules.addAll(forwarded); + } else { + (_nestedForwardedModules ??= + List.generate(_variables.length - 1, (_) => [])) + .last + .addAll(forwarded.keys); + } + + // Remove existing member definitions that are now shadowed by the + // forwarded modules. + for (var variable in forwardedVariableNames) { + _variableIndices.remove(variable); + _variables.last.remove(variable); + _variableNodes.last.remove(variable); + } + for (var function in forwardedFunctionNames) { + _functionIndices.remove(function); + _functions.last.remove(function); + } + for (var mixin in forwardedMixinNames) { + _mixinIndices.remove(mixin); + _mixins.last.remove(mixin); } } @@ -413,25 +414,21 @@ class AsyncEnvironment { _getVariableFromGlobalModule(name); } - var index = _variableIndices[name]; - if (index != null) { + if (_variableIndices[name] case var index?) { _lastVariableName = name; _lastVariableIndex = index; return _variables[index][name] ?? _getVariableFromGlobalModule(name); - } - - index = _variableIndex(name); - if (index == null) { + } else if (_variableIndex(name) case var index?) { + _lastVariableName = name; + _lastVariableIndex = index; + _variableIndices[name] = index; + return _variables[index][name] ?? _getVariableFromGlobalModule(name); + } else { // There isn't a real variable defined as this index, but it will cause // [getVariable] to short-circuit and get to this function faster next // time the variable is accessed. return _getVariableFromGlobalModule(name); } - - _lastVariableName = name; - _lastVariableIndex = index; - _variableIndices[name] = index; - return _variables[index][name] ?? _getVariableFromGlobalModule(name); } /// Returns the value of the variable named [name] from a namespaceless @@ -456,22 +453,20 @@ class AsyncEnvironment { _getVariableNodeFromGlobalModule(name); } - var index = _variableIndices[name]; - if (index != null) { + if (_variableIndices[name] case var index?) { _lastVariableName = name; _lastVariableIndex = index; return _variableNodes[index][name] ?? _getVariableNodeFromGlobalModule(name); + } else if (_variableIndex(name) case var index?) { + _lastVariableName = name; + _lastVariableIndex = index; + _variableIndices[name] = index; + return _variableNodes[index][name] ?? + _getVariableNodeFromGlobalModule(name); + } else { + return _getVariableNodeFromGlobalModule(name); } - - index = _variableIndex(name); - if (index == null) return _getVariableNodeFromGlobalModule(name); - - _lastVariableName = name; - _lastVariableIndex = index; - _variableIndices[name] = index; - return _variableNodes[index][name] ?? - _getVariableNodeFromGlobalModule(name); } /// Returns the node for the variable named [name] from a namespaceless @@ -486,8 +481,7 @@ class AsyncEnvironment { // We don't need to worry about multiple modules defining the same variable, // because that's already been checked by [getVariable]. for (var module in _importedModules.keys.followedBy(_globalModules.keys)) { - var value = module.variableNodes[name]; - if (value != null) return value; + if (module.variableNodes[name] case var value?) return value; } return null; } @@ -621,16 +615,14 @@ class AsyncEnvironment { AsyncCallable? getFunction(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).functions[name]; - var index = _functionIndices[name]; - if (index != null) { + if (_functionIndices[name] case var index?) { + return _functions[index][name] ?? _getFunctionFromGlobalModule(name); + } else if (_functionIndex(name) case var index?) { + _functionIndices[name] = index; return _functions[index][name] ?? _getFunctionFromGlobalModule(name); + } else { + return _getFunctionFromGlobalModule(name); } - - index = _functionIndex(name); - if (index == null) return _getFunctionFromGlobalModule(name); - - _functionIndices[name] = index; - return _functions[index][name] ?? _getFunctionFromGlobalModule(name); } /// Returns the value of the function named [name] from a namespaceless @@ -670,16 +662,14 @@ class AsyncEnvironment { AsyncCallable? getMixin(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).mixins[name]; - var index = _mixinIndices[name]; - if (index != null) { + if (_mixinIndices[name] case var index?) { + return _mixins[index][name] ?? _getMixinFromGlobalModule(name); + } else if (_mixinIndex(name) case var index?) { + _mixinIndices[name] = index; return _mixins[index][name] ?? _getMixinFromGlobalModule(name); + } else { + return _getMixinFromGlobalModule(name); } - - index = _mixinIndex(name); - if (index == null) return _getMixinFromGlobalModule(name); - - _mixinIndices[name] = index; - return _mixins[index][name] ?? _getMixinFromGlobalModule(name); } /// Returns the value of the mixin named [name] from a namespaceless @@ -791,11 +781,10 @@ class AsyncEnvironment { for (var i = 0; i < _variables.length; i++) { var values = _variables[i]; var nodes = _variableNodes[i]; - for (var entry in values.entries) { + for (var (name, value) in values.pairs) { // Implicit configurations are never invalid, making [configurationSpan] // unnecessary, so we pass null here to avoid having to compute it. - configuration[entry.key] = - ConfiguredValue.implicit(entry.value, nodes[entry.key]!); + configuration[name] = ConfiguredValue.implicit(value, nodes[name]!); } } return Configuration.implicit(configuration); @@ -819,22 +808,18 @@ class AsyncEnvironment { /// This is used when resolving imports, since they need to inject forwarded /// members into the current scope. It's the only situation in which a nested /// environment can become a module. - Module toDummyModule() { - return _EnvironmentModule( - this, - CssStylesheet(const [], - SourceFile.decoded(const [], url: "").span(0)), - const {}, - ExtensionStore.empty, - forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules))); - } + Module toDummyModule() => _EnvironmentModule( + this, + CssStylesheet(const [], + SourceFile.decoded(const [], url: "").span(0)), + const {}, + ExtensionStore.empty, + forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules))); /// Returns the module with the given [namespace], or throws a /// [SassScriptException] if none exists. Module _getModule(String namespace) { - var module = _modules[namespace]; - if (module != null) return module; - + if (_modules[namespace] case var module?) return module; throw SassScriptException( 'There is no module with the namespace "$namespace".'); } @@ -851,18 +836,15 @@ class AsyncEnvironment { /// The [type] should be the singular name of the value type being returned. /// It's used to format an appropriate error message. T? _fromOneModule(String name, String type, T? callback(Module module)) { - var nestedForwardedModules = _nestedForwardedModules; - if (nestedForwardedModules != null) { + if (_nestedForwardedModules case var nestedForwardedModules?) { for (var modules in nestedForwardedModules.reversed) { for (var module in modules.reversed) { - var value = callback(module); - if (value != null) return value; + if (callback(module) case var value?) return value; } } } for (var module in _importedModules.keys) { - var value = callback(module); - if (value != null) return value; + if (callback(module) case var value?) return value; } T? value; @@ -877,14 +859,11 @@ class AsyncEnvironment { if (identityFromModule == identity) continue; if (value != null) { - var spans = _globalModules.entries.map( - (entry) => callback(entry.key).andThen((_) => entry.value.span)); - throw MultiSpanSassScriptException( 'This $type is available from multiple global modules.', '$type use', { - for (var span in spans) - if (span != null) span: 'includes $type' + for (var (module, node) in _globalModules.pairs) + if (callback(module) != null) node.span: 'includes $type' }); } @@ -896,7 +875,7 @@ class AsyncEnvironment { } /// A module that represents the top-level members defined in an [Environment]. -class _EnvironmentModule implements Module { +final class _EnvironmentModule implements Module { Uri? get url => css.span.sourceUrl; final List upstream; @@ -932,8 +911,8 @@ class _EnvironmentModule implements Module { environment, css, Map.unmodifiable({ - for (var entry in preModuleComments.entries) - entry.key: List.unmodifiable(entry.value) + for (var (module, comments) in preModuleComments.pairs) + module: List.unmodifiable(comments) }), extensionStore, _makeModulesByVariable(forwarded), @@ -1006,8 +985,7 @@ class _EnvironmentModule implements Module { : upstream = _environment._allModules; void setVariable(String name, Value value, AstNode nodeWithSpan) { - var module = _modulesByVariable[name]; - if (module != null) { + if (_modulesByVariable[name] case var module?) { module.setVariable(name, value, nodeWithSpan); return; } @@ -1030,12 +1008,13 @@ class _EnvironmentModule implements Module { Module cloneCss() { if (!transitivelyContainsCss) return this; - var newCssAndExtensionStore = cloneCssStylesheet(css, extensionStore); + var (newStylesheet, newExtensionStore) = + cloneCssStylesheet(css, extensionStore); return _EnvironmentModule._( _environment, - newCssAndExtensionStore.item1, + newStylesheet, preModuleComments, - newCssAndExtensionStore.item2, + newExtensionStore, _modulesByVariable, variables, variableNodes, diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 0366c36b9..c67f77081 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -6,7 +6,6 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:package_config/package_config_types.dart'; import 'package:path/path.dart' as p; -import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; import 'deprecation.dart'; @@ -15,13 +14,23 @@ import 'importer/no_op.dart'; import 'importer/utils.dart'; import 'io.dart'; import 'logger.dart'; +import 'util/nullable.dart'; import 'utils.dart'; +/// A canonicalized URL and the importer that canonicalized it. +/// +/// This also includes the URL that was originally passed to the importer, which +/// may be resolved relative to a base URL. +typedef AsyncCanonicalizeResult = ( + AsyncImporter, + Uri canonicalUrl, { + Uri originalUrl +}); + /// An in-memory cache of parsed stylesheets that have been imported by Sass. /// /// {@category Dependencies} -@sealed -class AsyncImportCache { +final class AsyncImportCache { /// The importers to use when loading new Sass files. final List _importers; @@ -30,16 +39,14 @@ class AsyncImportCache { /// The canonicalized URLs for each non-canonical URL. /// - /// The second item in each key's tuple is true when this canonicalization is - /// for an `@import` rule. Otherwise, it's for a `@use` or `@forward` rule. - /// - /// This map's values are the same as the return value of [canonicalize]. + /// The `forImport` in each key is true when this canonicalization is for an + /// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule. /// /// This cache isn't used for relative imports, because they depend on the /// specific base importer. That's stored separately in /// [_relativeCanonicalizeCache]. final _canonicalizeCache = - , Tuple3?>{}; + <(Uri, {bool forImport}), AsyncCanonicalizeResult?>{}; /// The canonicalized URLs for each non-canonical URL that's resolved using a /// relative importer. @@ -52,8 +59,13 @@ class AsyncImportCache { /// 4. The `baseUrl` passed to [canonicalize]. /// /// The map's values are the same as the return value of [canonicalize]. - final _relativeCanonicalizeCache = , - Tuple3?>{}; + final _relativeCanonicalizeCache = <( + Uri, { + bool forImport, + AsyncImporter baseImporter, + Uri? baseUrl + }), + AsyncCanonicalizeResult?>{}; /// The parsed stylesheets for each canonicalized import URL. final _importCache = {}; @@ -120,7 +132,7 @@ class AsyncImportCache { /// If any importers understand [url], returns that importer as well as the /// canonicalized URL and the original URL (resolved relative to [baseUrl] if /// applicable). Otherwise, returns `null`. - Future?> canonicalize(Uri url, + Future canonicalize(Uri url, {AsyncImporter? baseImporter, Uri? baseUrl, bool forImport = false}) async { @@ -131,23 +143,29 @@ class AsyncImportCache { } if (baseImporter != null) { - var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, - Tuple4(url, forImport, baseImporter, baseUrl), () async { + var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, ( + url, + forImport: forImport, + baseImporter: baseImporter, + baseUrl: baseUrl + ), () async { var resolvedUrl = baseUrl?.resolveUri(url) ?? url; - var canonicalUrl = - await _canonicalize(baseImporter, resolvedUrl, forImport); - if (canonicalUrl == null) return null; - return Tuple3(baseImporter, canonicalUrl, resolvedUrl); + if (await _canonicalize(baseImporter, resolvedUrl, forImport) + case var canonicalUrl?) { + return (baseImporter, canonicalUrl, originalUrl: resolvedUrl); + } else { + return null; + } }); if (relativeResult != null) return relativeResult; } - return await putIfAbsentAsync(_canonicalizeCache, Tuple2(url, forImport), - () async { + return await putIfAbsentAsync( + _canonicalizeCache, (url, forImport: forImport), () async { for (var importer in _importers) { - var canonicalUrl = await _canonicalize(importer, url, forImport); - if (canonicalUrl != null) { - return Tuple3(importer, canonicalUrl, url); + if (await _canonicalize(importer, url, forImport) + case var canonicalUrl?) { + return (importer, canonicalUrl, originalUrl: url); } } @@ -180,17 +198,19 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// parsed stylesheet. Otherwise, returns `null`. /// /// Caches the result of the import and uses cached results if possible. - Future?> import(Uri url, + Future<(AsyncImporter, Stylesheet)?> import(Uri url, {AsyncImporter? baseImporter, Uri? baseUrl, bool forImport = false}) async { - var tuple = await canonicalize(url, - baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport); - if (tuple == null) return null; - var stylesheet = await importCanonical(tuple.item1, tuple.item2, - originalUrl: tuple.item3); - if (stylesheet == null) return null; - return Tuple2(tuple.item1, stylesheet); + if (await canonicalize(url, + baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport) + case (var importer, var canonicalUrl, :var originalUrl)) { + return (await importCanonical(importer, canonicalUrl, + originalUrl: originalUrl)) + .andThen((stylesheet) => (importer, stylesheet)); + } else { + return null; + } } /// Tries to load the canonicalized [canonicalUrl] using [importer]. @@ -226,21 +246,22 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// Return a human-friendly URL for [canonicalUrl] to use in a stack trace. /// /// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache. - Uri humanize(Uri canonicalUrl) { - // Display the URL with the shortest path length. - var url = minBy( - _canonicalizeCache.values - .whereNotNull() - .where((tuple) => tuple.item2 == canonicalUrl) - .map((tuple) => tuple.item3), - (url) => url.path.length); - if (url == null) return canonicalUrl; - - // Use the canonicalized basename so that we display e.g. - // package:example/_example.scss rather than package:example/example in - // stack traces. - return url.resolve(p.url.basename(canonicalUrl.path)); - } + Uri humanize(Uri canonicalUrl) => + // If multiple original URLs canonicalize to the same thing, choose the + // shortest one. + minBy( + _canonicalizeCache.values + .whereNotNull() + .where((result) => result.$2 == canonicalUrl) + .map((result) => result.originalUrl), + (url) => url.path.length) + // Use the canonicalized basename so that we display e.g. + // package:example/_example.scss rather than package:example/example + // in stack traces. + .andThen((url) => url.resolve(p.url.basename(canonicalUrl.path))) ?? + // If we don't have an original URL cached, display the canonical URL + // as-is. + canonicalUrl; /// Returns the URL to use in the source map to refer to [canonicalUrl]. /// @@ -255,16 +276,9 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// @nodoc @internal void clearCanonicalize(Uri url) { - _canonicalizeCache.remove(Tuple2(url, false)); - _canonicalizeCache.remove(Tuple2(url, true)); - - var relativeKeysToClear = [ - for (var key in _relativeCanonicalizeCache.keys) - if (key.item1 == url) key - ]; - for (var key in relativeKeysToClear) { - _relativeCanonicalizeCache.remove(key); - } + _canonicalizeCache.remove((url, forImport: false)); + _canonicalizeCache.remove((url, forImport: true)); + _relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url); } /// Clears the cached parse tree for the stylesheet with the given diff --git a/lib/src/callable.dart b/lib/src/callable.dart index 28f65c2b1..2d2ed1e26 100644 --- a/lib/src/callable.dart +++ b/lib/src/callable.dart @@ -3,9 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; -import 'ast/sass.dart'; import 'callable/async.dart'; import 'callable/built_in.dart'; import 'exception.dart'; @@ -69,7 +67,7 @@ export 'callable/user_defined.dart'; /// /// {@category Compile} @sealed -abstract class Callable extends AsyncCallable { +abstract interface class Callable implements AsyncCallable { @Deprecated('Use `Callable.function` instead.') factory Callable(String name, String arguments, Value callback(List arguments)) => @@ -127,8 +125,8 @@ abstract class Callable extends AsyncCallable { factory Callable.fromSignature( String signature, Value callback(List arguments), {bool requireParens = true}) { - Tuple2 tuple = + var (name, declaration) = parseSignature(signature, requireParens: requireParens); - return BuiltInCallable.parsed(tuple.item1, tuple.item2, callback); + return BuiltInCallable.parsed(name, declaration, callback); } } diff --git a/lib/src/callable/async.dart b/lib/src/callable/async.dart index 7a77d243a..433ca98b6 100644 --- a/lib/src/callable/async.dart +++ b/lib/src/callable/async.dart @@ -5,9 +5,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; -import '../ast/sass.dart'; import '../exception.dart'; import '../utils.dart'; import '../value.dart'; @@ -24,7 +22,7 @@ import 'async_built_in.dart'; /// /// {@category Compile} @sealed -abstract class AsyncCallable { +abstract interface class AsyncCallable { /// The callable's name. String get name; @@ -50,8 +48,8 @@ abstract class AsyncCallable { factory AsyncCallable.fromSignature( String signature, FutureOr callback(List arguments), {bool requireParens = true}) { - Tuple2 tuple = + var (name, declaration) = parseSignature(signature, requireParens: requireParens); - return AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2, callback); + return AsyncBuiltInCallable.parsed(name, declaration, callback); } } diff --git a/lib/src/callable/async_built_in.dart b/lib/src/callable/async_built_in.dart index ff4513f25..0132b787f 100644 --- a/lib/src/callable/async_built_in.dart +++ b/lib/src/callable/async_built_in.dart @@ -4,8 +4,6 @@ import 'dart:async'; -import 'package:tuple/tuple.dart'; - import '../ast/sass.dart'; import '../value.dart'; import 'async.dart'; @@ -76,7 +74,7 @@ class AsyncBuiltInCallable implements AsyncCallable { /// If no exact match is found, finds the closest approximation. Note that this /// doesn't guarantee that [positional] and [names] are valid for the returned /// [ArgumentDeclaration]. - Tuple2 callbackFor( + (ArgumentDeclaration, Callback) callbackFor( int positional, Set names) => - Tuple2(_arguments, _callback); + (_arguments, _callback); } diff --git a/lib/src/callable/built_in.dart b/lib/src/callable/built_in.dart index a6bad3414..905d11e56 100644 --- a/lib/src/callable/built_in.dart +++ b/lib/src/callable/built_in.dart @@ -2,10 +2,9 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:tuple/tuple.dart'; - import '../ast/sass.dart'; import '../callable.dart'; +import '../util/map.dart'; import '../value.dart'; typedef Callback = Value Function(List arguments); @@ -16,11 +15,11 @@ typedef Callback = Value Function(List arguments); /// may declare multiple different callbacks with multiple different sets of /// arguments. When the callable is invoked, the first callback with matching /// arguments is invoked. -class BuiltInCallable implements Callable, AsyncBuiltInCallable { +final class BuiltInCallable implements Callable, AsyncBuiltInCallable { final String name; /// The overloads declared for this callable. - final List> _overloads; + final List<(ArgumentDeclaration, Callback)> _overloads; /// Creates a function with a single [arguments] declaration and a single /// [callback]. @@ -61,7 +60,7 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// [callback]. BuiltInCallable.parsed(this.name, ArgumentDeclaration arguments, Value callback(List arguments)) - : _overloads = [Tuple2(arguments, callback)]; + : _overloads = [(arguments, callback)]; /// Creates a function with multiple implementations. /// @@ -75,11 +74,11 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { BuiltInCallable.overloadedFunction(this.name, Map overloads, {Object? url}) : _overloads = [ - for (var entry in overloads.entries) - Tuple2( - ArgumentDeclaration.parse('@function $name(${entry.key}) {', - url: url), - entry.value) + for (var (args, callback) in overloads.pairs) + ( + ArgumentDeclaration.parse('@function $name($args) {', url: url), + callback + ) ]; BuiltInCallable._(this.name, this._overloads); @@ -90,16 +89,16 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// If no exact match is found, finds the closest approximation. Note that this /// doesn't guarantee that [positional] and [names] are valid for the returned /// [ArgumentDeclaration]. - Tuple2 callbackFor( + (ArgumentDeclaration, Callback) callbackFor( int positional, Set names) { - Tuple2? fuzzyMatch; + (ArgumentDeclaration, Callback)? fuzzyMatch; int? minMismatchDistance; for (var overload in _overloads) { // Ideally, find an exact match. - if (overload.item1.matches(positional, names)) return overload; + if (overload.$1.matches(positional, names)) return overload; - var mismatchDistance = overload.item1.arguments.length - positional; + var mismatchDistance = overload.$1.arguments.length - positional; if (minMismatchDistance != null) { if (mismatchDistance.abs() > minMismatchDistance.abs()) continue; diff --git a/lib/src/callable/plain_css.dart b/lib/src/callable/plain_css.dart index 9a74ed604..cd46e1f5c 100644 --- a/lib/src/callable/plain_css.dart +++ b/lib/src/callable/plain_css.dart @@ -7,7 +7,7 @@ import '../callable.dart'; /// A callable that emits a plain CSS function. /// /// This can't be used for mixins. -class PlainCssCallable implements Callable { +final class PlainCssCallable implements Callable { final String name; PlainCssCallable(this.name); diff --git a/lib/src/callable/user_defined.dart b/lib/src/callable/user_defined.dart index a0a2af72c..6e0ecfacc 100644 --- a/lib/src/callable/user_defined.dart +++ b/lib/src/callable/user_defined.dart @@ -8,7 +8,7 @@ import '../callable.dart'; /// A callback defined in the user's Sass stylesheet. /// /// The type parameter [E] should either be `Environment` or `AsyncEnvironment`. -class UserDefinedCallable implements Callable { +final class UserDefinedCallable implements Callable { /// The declaration. final CallableDeclaration declaration; diff --git a/lib/src/color_names.dart b/lib/src/color_names.dart index 6bc4575b3..ae315663d 100644 --- a/lib/src/color_names.dart +++ b/lib/src/color_names.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'value.dart'; +import 'util/map.dart'; /// A map from (lowercase) color names to their color values. final colorsByName = { @@ -161,5 +162,5 @@ final colorsByName = { /// A map from Sass colors to (lowercase) color names. final namesByColor = { - for (var entry in colorsByName.entries) entry.value: entry.key + for (var (name, color) in colorsByName.pairs) color: name }; diff --git a/lib/src/compile.dart b/lib/src/compile.dart index af96a3bd0..2b24def1b 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: d6fc59fc731ebbba7d3a8cabc259e09fdc871764 +// Checksum: c2982db43bcd56f81cab3f51b5669e0edd3cfafb // // ignore_for_file: unused_import @@ -179,9 +179,7 @@ CompileResult _compileStylesheet( var resultSourceMap = serializeResult.sourceMap; if (resultSourceMap != null && importCache != null) { - // TODO(nweiz): Don't explicitly use a type parameter when dart-lang/sdk#25490 - // is fixed. - mapInPlace( + mapInPlace( resultSourceMap.urls, (url) => url == '' ? Uri.dataFromString(stylesheet.span.file.getText(0), diff --git a/lib/src/compile_result.dart b/lib/src/compile_result.dart index 459c899dc..ad3e60c0d 100644 --- a/lib/src/compile_result.dart +++ b/lib/src/compile_result.dart @@ -21,7 +21,7 @@ class CompileResult { final SerializeResult _serialize; /// The compiled CSS. - String get css => _serialize.css; + String get css => _serialize.$1; /// The source map indicating how the source files map to [css]. /// diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index b8f10a5bb..1a65d236a 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -7,6 +7,7 @@ import 'ast/node.dart'; import 'ast/sass.dart'; import 'configured_value.dart'; import 'util/limited_map_view.dart'; +import 'util/map.dart'; import 'util/unprefixed_map_view.dart'; /// A set of variables meant to configure a module by overriding its @@ -17,7 +18,7 @@ import 'util/unprefixed_map_view.dart'; /// meaning that it's created by passing a `with` clause to a `@use` rule. /// Explicit configurations have spans associated with them and are represented /// by the [ExplicitConfiguration] subclass. -class Configuration { +final class Configuration { /// A map from variable names (without `$`) to values. /// /// This map may not be modified directly. To remove a value from this @@ -76,14 +77,14 @@ class Configuration { // configured. These views support [Map.remove] so we can mark when a // configuration variable is used by removing it even when the underlying // map is wrapped. - var prefix = forward.prefix; - if (prefix != null) newValues = UnprefixedMapView(newValues, prefix); + if (forward.prefix case var prefix?) { + newValues = UnprefixedMapView(newValues, prefix); + } - var shownVariables = forward.shownVariables; - var hiddenVariables = forward.hiddenVariables; - if (shownVariables != null) { + if (forward.shownVariables case var shownVariables?) { newValues = LimitedMapView.safelist(newValues, shownVariables); - } else if (hiddenVariables != null && hiddenVariables.isNotEmpty) { + } else if (forward.hiddenVariables case var hiddenVariables? + when hiddenVariables.isNotEmpty) { newValues = LimitedMapView.blocklist(newValues, hiddenVariables); } return _withValues(newValues); @@ -101,9 +102,7 @@ class Configuration { String toString() => "(" + - values.entries - .map((entry) => "\$${entry.key}: ${entry.value}") - .join(", ") + + [for (var (name, value) in values.pairs) "\$$name: $value"].join(",") + ")"; } @@ -114,7 +113,7 @@ class Configuration { /// configurations will cause an error if attempting to use them on a module /// that has already been loaded, while implicit configurations will be /// silently ignored in this case. -class ExplicitConfiguration extends Configuration { +final class ExplicitConfiguration extends Configuration { /// The node whose span indicates where the configuration was declared. final AstNode nodeWithSpan; diff --git a/lib/src/configured_value.dart b/lib/src/configured_value.dart index c373b1f8e..faf969cad 100644 --- a/lib/src/configured_value.dart +++ b/lib/src/configured_value.dart @@ -8,7 +8,7 @@ import 'ast/node.dart'; import 'value.dart'; /// A variable value that's been configured for a [Configuration]. -class ConfiguredValue { +final class ConfiguredValue { /// The value of the variable. final Value value; diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 0d913424e..92afcfcc8 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -81,7 +81,7 @@ enum Deprecation { /// 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); + Version? get deprecatedIn => _deprecatedIn.andThen(Version.parse); /// A description of this deprecation that will be displayed in the CLI usage. /// @@ -116,8 +116,7 @@ enum Deprecation { var range = VersionRange(max: version, includeMax: true); return { for (var deprecation in Deprecation.values) - if (deprecation.deprecatedIn?.andThen(range.allows) ?? false) - deprecation + if (deprecation.deprecatedIn.andThen(range.allows) ?? false) deprecation }; } } diff --git a/lib/src/embedded/dispatcher.dart b/lib/src/embedded/dispatcher.dart index f6d6a3e0f..fae22b458 100644 --- a/lib/src/embedded/dispatcher.dart +++ b/lib/src/embedded/dispatcher.dart @@ -29,7 +29,7 @@ final _outboundRequestId = 0; /// A class that dispatches messages to and from the host for a single /// compilation. -class Dispatcher { +final class Dispatcher { /// The channel of encoded protocol buffers, connected to the host. final StreamChannel _channel; diff --git a/lib/src/embedded/executable.dart b/lib/src/embedded/executable.dart index 11dec3454..9248713ae 100644 --- a/lib/src/embedded/executable.dart +++ b/lib/src/embedded/executable.dart @@ -11,23 +11,23 @@ import 'isolate_dispatcher.dart'; import 'util/length_delimited_transformer.dart'; void main(List args) { - if (args.isNotEmpty) { - if (args.first == "--version") { + switch (args) { + case ["--version", ...]: var response = IsolateDispatcher.versionResponse(); response.id = 0; stdout.writeln( JsonEncoder.withIndent(" ").convert(response.toProto3Json())); return; - } - stderr.writeln( - "sass --embedded is not intended to be executed with additional " - "arguments.\n" - "See https://github.com/sass/dart-sass#embedded-dart-sass for " - "details."); - // USAGE error from https://bit.ly/2poTt90 - exitCode = 64; - return; + case [_, ...]: + stderr.writeln( + "sass --embedded is not intended to be executed with additional " + "arguments.\n" + "See https://github.com/sass/dart-sass#embedded-dart-sass for " + "details."); + // USAGE error from https://bit.ly/2poTt90 + exitCode = 64; + return; } IsolateDispatcher( diff --git a/lib/src/embedded/function_registry.dart b/lib/src/embedded/function_registry.dart index 98cd2f6e0..b288fbd8d 100644 --- a/lib/src/embedded/function_registry.dart +++ b/lib/src/embedded/function_registry.dart @@ -7,7 +7,7 @@ import 'embedded_sass.pb.dart'; /// A registry of [SassFunction]s indexed by ID so that the host can invoke /// them. -class FunctionRegistry { +final class FunctionRegistry { /// First-class functions that have been sent to the host. /// /// The functions are located at indexes in the list matching their IDs. diff --git a/lib/src/embedded/importer/base.dart b/lib/src/embedded/importer/base.dart index 9fad62360..1de597da5 100644 --- a/lib/src/embedded/importer/base.dart +++ b/lib/src/embedded/importer/base.dart @@ -9,8 +9,8 @@ import '../dispatcher.dart'; /// An abstract base class for importers that communicate with the host in some /// way. -abstract class ImporterBase extends Importer { - /// The [CompileDispatcher] to which to send requests. +abstract base class ImporterBase extends Importer { + /// The [Dispatcher] to which to send requests. @protected final Dispatcher dispatcher; diff --git a/lib/src/embedded/importer/file.dart b/lib/src/embedded/importer/file.dart index e2b2bad40..b945cba2e 100644 --- a/lib/src/embedded/importer/file.dart +++ b/lib/src/embedded/importer/file.dart @@ -18,7 +18,7 @@ final _filesystemImporter = FilesystemImporter('.'); /// An importer that asks the host to resolve imports in a simplified, /// file-system-centric way. -class FileImporter extends ImporterBase { +final class FileImporter extends ImporterBase { /// The host-provided ID of the importer to invoke. final int _importerId; diff --git a/lib/src/embedded/importer/host.dart b/lib/src/embedded/importer/host.dart index 743cdda95..e4a952100 100644 --- a/lib/src/embedded/importer/host.dart +++ b/lib/src/embedded/importer/host.dart @@ -12,7 +12,7 @@ import '../utils.dart'; import 'base.dart'; /// An importer that asks the host to resolve imports. -class HostImporter extends ImporterBase { +final class HostImporter extends ImporterBase { /// The host-provided ID of the importer to invoke. final int _importerId; @@ -27,16 +27,13 @@ class HostImporter extends ImporterBase { ..url = url.toString() ..fromImport = fromImport); - switch (response.whichResult()) { - case InboundMessage_CanonicalizeResponse_Result.url: - return parseAbsoluteUrl("The importer", response.url); - - case InboundMessage_CanonicalizeResponse_Result.error: - throw response.error; - - case InboundMessage_CanonicalizeResponse_Result.notSet: - return null; - } + return switch (response.whichResult()) { + InboundMessage_CanonicalizeResponse_Result.url => + parseAbsoluteUrl("The importer", response.url), + InboundMessage_CanonicalizeResponse_Result.error => + throw response.error, + InboundMessage_CanonicalizeResponse_Result.notSet => null + }; }()); } @@ -48,21 +45,17 @@ class HostImporter extends ImporterBase { ..importerId = _importerId ..url = url.toString()); - switch (response.whichResult()) { - case InboundMessage_ImportResponse_Result.success: - return ImporterResult(response.success.contents, - sourceMapUrl: response.success.sourceMapUrl.isEmpty - ? null - : parseAbsoluteUrl( - "The importer", response.success.sourceMapUrl), - syntax: syntaxToSyntax(response.success.syntax)); - - case InboundMessage_ImportResponse_Result.error: - throw response.error; - - case InboundMessage_ImportResponse_Result.notSet: - return null; - } + return switch (response.whichResult()) { + InboundMessage_ImportResponse_Result.success => ImporterResult( + response.success.contents, + sourceMapUrl: response.success.sourceMapUrl.isEmpty + ? null + : parseAbsoluteUrl( + "The importer", response.success.sourceMapUrl), + syntax: syntaxToSyntax(response.success.syntax)), + InboundMessage_ImportResponse_Result.error => throw response.error, + InboundMessage_ImportResponse_Result.notSet => null + }; }()); } diff --git a/lib/src/embedded/logger.dart b/lib/src/embedded/logger.dart index c94e52dc1..8da614cc5 100644 --- a/lib/src/embedded/logger.dart +++ b/lib/src/embedded/logger.dart @@ -7,13 +7,14 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import '../logger.dart'; +import '../util/nullable.dart'; import '../utils.dart'; import 'dispatcher.dart'; import 'embedded_sass.pb.dart' hide SourceSpan; import 'utils.dart'; /// A Sass logger that sends log messages as `LogEvent`s. -class EmbeddedLogger implements Logger { +final class EmbeddedLogger implements Logger { /// The [Dispatcher] to which to send events. final Dispatcher _dispatcher; @@ -28,18 +29,14 @@ class EmbeddedLogger implements Logger { _ascii = ascii; void debug(String message, SourceSpan span) { - var url = - span.start.sourceUrl == null ? '-' : p.prettyUri(span.start.sourceUrl); - var buffer = StringBuffer() - ..write('$url:${span.start.line + 1} ') - ..write(_color ? '\u001b[1mDebug\u001b[0m' : 'DEBUG') - ..writeln(': $message'); - _dispatcher.sendLog(OutboundMessage_LogEvent() ..type = LogEventType.DEBUG ..message = message ..span = protofySpan(span) - ..formatted = buffer.toString()); + ..formatted = (span.start.sourceUrl.andThen(p.prettyUri) ?? '-') + + ':${span.start.line + 1} ' + + (_color ? '\u001b[1mDebug\u001b[0m' : 'DEBUG') + + ': $message\n'); } void warn(String message, diff --git a/lib/src/embedded/protofier.dart b/lib/src/embedded/protofier.dart index 3f7c499a1..3a1a792b0 100644 --- a/lib/src/embedded/protofier.dart +++ b/lib/src/embedded/protofier.dart @@ -2,6 +2,8 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import '../util/map.dart'; +import '../util/nullable.dart'; import '../value.dart'; import 'dispatcher.dart'; import 'embedded_sass.pb.dart' as proto; @@ -14,7 +16,7 @@ import 'utils.dart'; /// /// A given [Protofier] instance is valid only within the scope of a single /// custom function call. -class Protofier { +final class Protofier { /// The dispatcher, for invoking deprotofied [Value_HostFunction]s. final Dispatcher _dispatcher; @@ -38,138 +40,117 @@ class Protofier { /// Converts [value] to its protocol buffer representation. proto.Value protofy(Value value) { var result = proto.Value(); - if (value is SassString) { - result.string = Value_String() - ..text = value.text - ..quoted = value.hasQuotes; - } else if (value is SassNumber) { - result.number = _protofyNumber(value); - } else if (value is SassColor) { - if (value.hasCalculatedHsl) { + switch (value) { + case SassString(): + result.string = Value_String() + ..text = value.text + ..quoted = value.hasQuotes; + case SassNumber(): + result.number = _protofyNumber(value); + case SassColor(hasCalculatedHsl: true): result.hslColor = Value_HslColor() ..hue = value.hue * 1.0 ..saturation = value.saturation * 1.0 ..lightness = value.lightness * 1.0 ..alpha = value.alpha * 1.0; - } else { + case SassColor(): result.rgbColor = Value_RgbColor() ..red = value.red ..green = value.green ..blue = value.blue ..alpha = value.alpha * 1.0; - } - } else if (value is SassArgumentList) { - _argumentLists.add(value); - var argList = Value_ArgumentList() - ..id = _argumentLists.length - ..separator = _protofySeparator(value.separator) - ..contents.addAll([for (var element in value.asList) protofy(element)]); - value.keywordsWithoutMarking.forEach((key, value) { - argList.keywords[key] = protofy(value); - }); - - result.argumentList = argList; - } else if (value is SassList) { - result.list = Value_List() - ..separator = _protofySeparator(value.separator) - ..hasBrackets = value.hasBrackets - ..contents.addAll([for (var element in value.asList) protofy(element)]); - } else if (value is SassMap) { - var map = Value_Map(); - value.contents.forEach((key, value) { - map.entries.add(Value_Map_Entry() - ..key = protofy(key) - ..value = protofy(value)); - }); - result.map = map; - } else if (value is SassCalculation) { - result.calculation = _protofyCalculation(value); - } else if (value is SassFunction) { - result.compilerFunction = _functions.protofy(value); - } else if (value == sassTrue) { - result.singleton = SingletonValue.TRUE; - } else if (value == sassFalse) { - result.singleton = SingletonValue.FALSE; - } else if (value == sassNull) { - result.singleton = SingletonValue.NULL; - } else { - throw "Unknown Value $value"; + case SassArgumentList(): + _argumentLists.add(value); + result.argumentList = Value_ArgumentList() + ..id = _argumentLists.length + ..separator = _protofySeparator(value.separator) + ..keywords.addAll({ + for (var (key, value) in value.keywordsWithoutMarking.pairs) + key: protofy(value) + }) + ..contents.addAll(value.asList.map(protofy)); + case SassList(): + result.list = Value_List() + ..separator = _protofySeparator(value.separator) + ..hasBrackets = value.hasBrackets + ..contents.addAll(value.asList.map(protofy)); + case SassMap(): + result.map = Value_Map(); + for (var (key, value) in value.contents.pairs) { + result.map.entries.add(Value_Map_Entry() + ..key = protofy(key) + ..value = protofy(value)); + } + case SassCalculation(): + result.calculation = _protofyCalculation(value); + case SassFunction(): + result.compilerFunction = _functions.protofy(value); + case sassTrue: + result.singleton = SingletonValue.TRUE; + case sassFalse: + result.singleton = SingletonValue.FALSE; + case sassNull: + result.singleton = SingletonValue.NULL; + case _: + throw "Unknown Value $value"; } return result; } /// Converts [number] to its protocol buffer representation. - Value_Number _protofyNumber(SassNumber number) { - var value = Value_Number()..value = number.value * 1.0; - value.numerators.addAll(number.numeratorUnits); - value.denominators.addAll(number.denominatorUnits); - return value; - } + Value_Number _protofyNumber(SassNumber number) => Value_Number() + ..value = number.value * 1.0 + ..numerators.addAll(number.numeratorUnits) + ..denominators.addAll(number.denominatorUnits); /// Converts [separator] to its protocol buffer representation. - proto.ListSeparator _protofySeparator(ListSeparator separator) { - switch (separator) { - case ListSeparator.comma: - return proto.ListSeparator.COMMA; - case ListSeparator.space: - return proto.ListSeparator.SPACE; - case ListSeparator.slash: - return proto.ListSeparator.SLASH; - case ListSeparator.undecided: - return proto.ListSeparator.UNDECIDED; - default: - throw "Unknown ListSeparator $separator"; - } - } + proto.ListSeparator _protofySeparator(ListSeparator separator) => + switch (separator) { + ListSeparator.comma => proto.ListSeparator.COMMA, + ListSeparator.space => proto.ListSeparator.SPACE, + ListSeparator.slash => proto.ListSeparator.SLASH, + ListSeparator.undecided => proto.ListSeparator.UNDECIDED + }; /// Converts [calculation] to its protocol buffer representation. Value_Calculation _protofyCalculation(SassCalculation calculation) => Value_Calculation() ..name = calculation.name - ..arguments.addAll([ - for (var argument in calculation.arguments) - _protofyCalculationValue(argument) - ]); + ..arguments.addAll(calculation.arguments.map(_protofyCalculationValue)); /// Converts a calculation value that appears within a `SassCalculation` to /// its protocol buffer representation. Value_Calculation_CalculationValue _protofyCalculationValue(Object value) { var result = Value_Calculation_CalculationValue(); - if (value is SassNumber) { - result.number = _protofyNumber(value); - } else if (value is SassCalculation) { - result.calculation = _protofyCalculation(value); - } else if (value is SassString) { - result.string = value.text; - } else if (value is CalculationOperation) { - result.operation = Value_Calculation_CalculationOperation() - ..operator = _protofyCalculationOperator(value.operator) - ..left = _protofyCalculationValue(value.left) - ..right = _protofyCalculationValue(value.right); - } else if (value is CalculationInterpolation) { - result.interpolation = value.value; - } else { - throw "Unknown calculation value $value"; + switch (value) { + case SassNumber(): + result.number = _protofyNumber(value); + case SassCalculation(): + result.calculation = _protofyCalculation(value); + case SassString(): + result.string = value.text; + case CalculationOperation(): + result.operation = Value_Calculation_CalculationOperation() + ..operator = _protofyCalculationOperator(value.operator) + ..left = _protofyCalculationValue(value.left) + ..right = _protofyCalculationValue(value.right); + case CalculationInterpolation(): + result.interpolation = value.value; + case _: + throw "Unknown calculation value $value"; } return result; } /// Converts [operator] to its protocol buffer representation. proto.CalculationOperator _protofyCalculationOperator( - CalculationOperator operator) { - switch (operator) { - case CalculationOperator.plus: - return proto.CalculationOperator.PLUS; - case CalculationOperator.minus: - return proto.CalculationOperator.MINUS; - case CalculationOperator.times: - return proto.CalculationOperator.TIMES; - case CalculationOperator.dividedBy: - return proto.CalculationOperator.DIVIDE; - default: - throw "Unknown CalculationOperator $operator"; - } - } + CalculationOperator operator) => + switch (operator) { + CalculationOperator.plus => proto.CalculationOperator.PLUS, + CalculationOperator.minus => proto.CalculationOperator.MINUS, + CalculationOperator.times => proto.CalculationOperator.TIMES, + CalculationOperator.dividedBy => proto.CalculationOperator.DIVIDE + }; /// Converts [response]'s return value to its Sass representation. Value deprotofyResponse(InboundMessage_FunctionCallResponse response) { @@ -218,12 +199,13 @@ class Protofier { "$length elements"); } - return SassArgumentList([ - for (var element in value.argumentList.contents) _deprotofy(element) - ], { - for (var entry in value.argumentList.keywords.entries) - entry.key: _deprotofy(entry.value) - }, separator); + return SassArgumentList( + value.argumentList.contents.map(_deprotofy), + { + for (var (name, value) in value.argumentList.keywords.pairs) + name: _deprotofy(value) + }, + separator); case Value_Value.list: var separator = _deprotofySeparator(value.list.separator); @@ -239,27 +221,22 @@ class Protofier { "$length elements"); } - return SassList([ - for (var element in value.list.contents) _deprotofy(element) - ], separator, brackets: value.list.hasBrackets); + return SassList(value.list.contents.map(_deprotofy), separator, + brackets: value.list.hasBrackets); case Value_Value.map: return value.map.entries.isEmpty ? const SassMap.empty() : SassMap({ - for (var entry in value.map.entries) - _deprotofy(entry.key): _deprotofy(entry.value) + for (var Value_Map_Entry(:key, :value) in value.map.entries) + _deprotofy(key): _deprotofy(value) }); case Value_Value.compilerFunction: var id = value.compilerFunction.id; - var function = _functions[id]; - if (function == null) { - throw paramsError( - "CompilerFunction.id $id doesn't match any known functions"); - } - - return function; + if (_functions[id] case var function?) return function; + throw paramsError( + "CompilerFunction.id $id doesn't match any known functions"); case Value_Value.hostFunction: return SassFunction(hostCallable( @@ -270,16 +247,12 @@ class Protofier { return _deprotofyCalculation(value.calculation); case Value_Value.singleton: - switch (value.singleton) { - case SingletonValue.TRUE: - return sassTrue; - case SingletonValue.FALSE: - return sassFalse; - case SingletonValue.NULL: - return sassNull; - default: - throw "Unknown Value.singleton ${value.singleton}"; - } + return switch (value.singleton) { + SingletonValue.TRUE => sassTrue, + SingletonValue.FALSE => sassFalse, + SingletonValue.NULL => sassNull, + _ => throw "Unknown Value.singleton ${value.singleton}" + }; case Value_Value.notSet: throw mandatoryError("Value.value"); @@ -323,112 +296,75 @@ class Protofier { } /// Converts [separator] to its Sass representation. - ListSeparator _deprotofySeparator(proto.ListSeparator separator) { - switch (separator) { - case proto.ListSeparator.COMMA: - return ListSeparator.comma; - case proto.ListSeparator.SPACE: - return ListSeparator.space; - case proto.ListSeparator.SLASH: - return ListSeparator.slash; - case proto.ListSeparator.UNDECIDED: - return ListSeparator.undecided; - default: - throw "Unknown separator $separator"; - } - } + ListSeparator _deprotofySeparator(proto.ListSeparator separator) => + switch (separator) { + proto.ListSeparator.COMMA => ListSeparator.comma, + proto.ListSeparator.SPACE => ListSeparator.space, + proto.ListSeparator.SLASH => ListSeparator.slash, + proto.ListSeparator.UNDECIDED => ListSeparator.undecided, + _ => throw "Unknown ListSeparator $separator", + }; /// Converts [calculation] to its Sass representation. - Value _deprotofyCalculation(Value_Calculation calculation) { - if (calculation.name == "calc") { - if (calculation.arguments.length != 1) { - throw paramsError( + Value _deprotofyCalculation(Value_Calculation calculation) => + switch (calculation) { + Value_Calculation(name: "calc", arguments: [var arg]) => + SassCalculation.calc(_deprotofyCalculationValue(arg)), + Value_Calculation(name: "calc") => throw paramsError( "Value.Calculation.arguments must have exactly one argument for " - "calc()."); - } - - return SassCalculation.calc( - _deprotofyCalculationValue(calculation.arguments[0])); - } else if (calculation.name == "clamp") { - if (calculation.arguments.isEmpty || calculation.arguments.length > 3) { - throw paramsError( + "calc()."), + Value_Calculation( + name: "clamp", + arguments: [var arg1, ...var rest] && List(length: < 4) + ) => + SassCalculation.clamp( + _deprotofyCalculationValue(arg1), + rest.elementAtOrNull(0).andThen(_deprotofyCalculationValue), + rest.elementAtOrNull(1).andThen(_deprotofyCalculationValue)), + Value_Calculation(name: "clamp") => throw paramsError( "Value.Calculation.arguments must have 1 to 3 arguments for " - "clamp()."); - } - - return SassCalculation.clamp( - _deprotofyCalculationValue(calculation.arguments[0]), - calculation.arguments.length > 1 - ? _deprotofyCalculationValue(calculation.arguments[1]) - : null, - calculation.arguments.length > 2 - ? _deprotofyCalculationValue(calculation.arguments[2]) - : null); - } else if (calculation.name == "min") { - if (calculation.arguments.isEmpty) { - throw paramsError( - "Value.Calculation.arguments must have at least 1 argument for " - "min()."); - } - - return SassCalculation.min( - calculation.arguments.map(_deprotofyCalculationValue)); - } else if (calculation.name == "max") { - if (calculation.arguments.isEmpty) { - throw paramsError( - "Value.Calculation.arguments must have at least 1 argument for " - "max()."); - } - - return SassCalculation.max( - calculation.arguments.map(_deprotofyCalculationValue)); - } else { - throw paramsError( - 'Value.Calculation.name "${calculation.name}" is not a recognized ' - 'calculation type.'); - } - } + "clamp()."), + Value_Calculation(name: "min" || "max", arguments: []) => + throw paramsError( + "Value.Calculation.arguments must have at least 1 argument for " + "${calculation.name}()."), + Value_Calculation(name: "min", :var arguments) => + SassCalculation.min(arguments.map(_deprotofyCalculationValue)), + Value_Calculation(name: "max", :var arguments) => + SassCalculation.max(arguments.map(_deprotofyCalculationValue)), + _ => throw paramsError( + 'Value.Calculation.name "${calculation.name}" is not a recognized ' + 'calculation type.') + }; /// Converts [value] to its Sass representation. - Object _deprotofyCalculationValue(Value_Calculation_CalculationValue value) { - switch (value.whichValue()) { - case Value_Calculation_CalculationValue_Value.number: - return _deprotofyNumber(value.number); - - case Value_Calculation_CalculationValue_Value.calculation: - return _deprotofyCalculation(value.calculation); - - case Value_Calculation_CalculationValue_Value.string: - return SassString(value.string, quotes: false); - - case Value_Calculation_CalculationValue_Value.operation: - return SassCalculation.operate( - _deprotofyCalculationOperator(value.operation.operator), - _deprotofyCalculationValue(value.operation.left), - _deprotofyCalculationValue(value.operation.right)); - - case Value_Calculation_CalculationValue_Value.interpolation: - return CalculationInterpolation(value.interpolation); - - case Value_Calculation_CalculationValue_Value.notSet: - throw mandatoryError("Value.Calculation.value"); - } - } + Object _deprotofyCalculationValue(Value_Calculation_CalculationValue value) => + switch (value.whichValue()) { + Value_Calculation_CalculationValue_Value.number => + _deprotofyNumber(value.number), + Value_Calculation_CalculationValue_Value.calculation => + _deprotofyCalculation(value.calculation), + Value_Calculation_CalculationValue_Value.string => + SassString(value.string, quotes: false), + Value_Calculation_CalculationValue_Value.operation => + SassCalculation.operate( + _deprotofyCalculationOperator(value.operation.operator), + _deprotofyCalculationValue(value.operation.left), + _deprotofyCalculationValue(value.operation.right)), + Value_Calculation_CalculationValue_Value.interpolation => + CalculationInterpolation(value.interpolation), + Value_Calculation_CalculationValue_Value.notSet => + throw mandatoryError("Value.Calculation.value") + }; /// Converts [operator] to its Sass representation. CalculationOperator _deprotofyCalculationOperator( - proto.CalculationOperator operator) { - switch (operator) { - case proto.CalculationOperator.PLUS: - return CalculationOperator.plus; - case proto.CalculationOperator.MINUS: - return CalculationOperator.minus; - case proto.CalculationOperator.TIMES: - return CalculationOperator.times; - case proto.CalculationOperator.DIVIDE: - return CalculationOperator.dividedBy; - default: - throw "Unknown CalculationOperator $operator"; - } - } + proto.CalculationOperator operator) => + switch (operator) { + proto.CalculationOperator.PLUS => CalculationOperator.plus, + proto.CalculationOperator.MINUS => CalculationOperator.minus, + proto.CalculationOperator.TIMES => CalculationOperator.times, + proto.CalculationOperator.DIVIDE => CalculationOperator.dividedBy, + _ => throw "Unknown CalculationOperator $operator" + }; } diff --git a/lib/src/embedded/utils.dart b/lib/src/embedded/utils.dart index 1998196b8..ff987cf2f 100644 --- a/lib/src/embedded/utils.dart +++ b/lib/src/embedded/utils.dart @@ -57,18 +57,12 @@ SourceSpan_SourceLocation _protofyLocation(SourceLocation location) => ..column = location.column; /// Converts a protocol buffer syntax enum into a Sass API syntax enum. -Syntax syntaxToSyntax(proto.Syntax syntax) { - switch (syntax) { - case proto.Syntax.SCSS: - return Syntax.scss; - case proto.Syntax.INDENTED: - return Syntax.sass; - case proto.Syntax.CSS: - return Syntax.css; - default: - throw "Unknown syntax $syntax."; - } -} +Syntax syntaxToSyntax(proto.Syntax syntax) => switch (syntax) { + proto.Syntax.SCSS => Syntax.scss, + proto.Syntax.INDENTED => Syntax.sass, + proto.Syntax.CSS => Syntax.css, + _ => throw "Unknown syntax $syntax." + }; /// Returns the result of running [callback] with the global ASCII config set /// to [ascii]. diff --git a/lib/src/embedded/value.dart b/lib/src/embedded/value.dart deleted file mode 100644 index 1b08f7b83..000000000 --- a/lib/src/embedded/value.dart +++ /dev/null @@ -1,201 +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 '../value.dart'; -import 'dispatcher.dart'; -import 'embedded_sass.pb.dart' as proto; -import 'embedded_sass.pb.dart' hide Value, ListSeparator; -import 'function_registry.dart'; -import 'host_callable.dart'; -import 'utils.dart'; - -/// Converts [value] to its protocol buffer representation. -/// -/// The [functions] tracks the IDs of first-class functions so that the host can -/// pass them back to the compiler. -proto.Value protofyValue(FunctionRegistry functions, Value value) { - var result = proto.Value(); - if (value is SassString) { - result.string = Value_String() - ..text = value.text - ..quoted = value.hasQuotes; - } else if (value is SassNumber) { - var number = Value_Number()..value = value.value * 1.0; - number.numerators.addAll(value.numeratorUnits); - number.denominators.addAll(value.denominatorUnits); - result.number = number; - } else if (value is SassColor) { - // TODO(nweiz): If the color is represented as HSL internally, this coerces - // it to RGB. Is it worth providing some visibility into its internal - // representation so we can serialize without converting? - result.rgbColor = Value_RgbColor() - ..red = value.red - ..green = value.green - ..blue = value.blue - ..alpha = value.alpha * 1.0; - } else if (value is SassList) { - var list = Value_List() - ..separator = _protofySeparator(value.separator) - ..hasBrackets = value.hasBrackets - ..contents.addAll( - [for (var element in value.asList) protofyValue(functions, element)]); - result.list = list; - } else if (value is SassMap) { - var map = Value_Map(); - value.contents.forEach((key, value) { - map.entries.add(Value_Map_Entry() - ..key = protofyValue(functions, key) - ..value = protofyValue(functions, value)); - }); - result.map = map; - } else if (value is SassFunction) { - result.compilerFunction = functions.protofy(value); - } else if (value == sassTrue) { - result.singleton = SingletonValue.TRUE; - } else if (value == sassFalse) { - result.singleton = SingletonValue.FALSE; - } else if (value == sassNull) { - result.singleton = SingletonValue.NULL; - } else { - throw "Unknown Value $value"; - } - return result; -} - -/// Converts [separator] to its protocol buffer representation. -proto.ListSeparator _protofySeparator(ListSeparator separator) { - switch (separator) { - case ListSeparator.comma: - return proto.ListSeparator.COMMA; - case ListSeparator.space: - return proto.ListSeparator.SPACE; - case ListSeparator.slash: - return proto.ListSeparator.SLASH; - case ListSeparator.undecided: - return proto.ListSeparator.UNDECIDED; - default: - throw "Unknown ListSeparator $separator"; - } -} - -/// Converts [value] to its Sass representation. -/// -/// The [functions] tracks the IDs of first-class functions so that they can be -/// deserialized to their original references. -Value deprotofyValue( - Dispatcher dispatcher, FunctionRegistry functions, proto.Value value) { - // Curry recursive calls to this function so we don't have to keep repeating - // ourselves. - deprotofy(proto.Value value) => deprotofyValue(dispatcher, functions, value); - - try { - switch (value.whichValue()) { - case Value_Value.string: - return value.string.text.isEmpty - ? SassString.empty(quotes: value.string.quoted) - : SassString(value.string.text, quotes: value.string.quoted); - - case Value_Value.number: - return SassNumber.withUnits(value.number.value, - numeratorUnits: value.number.numerators, - denominatorUnits: value.number.denominators); - - case Value_Value.rgbColor: - return SassColor.rgb(value.rgbColor.red, value.rgbColor.green, - value.rgbColor.blue, value.rgbColor.alpha); - - case Value_Value.hslColor: - return SassColor.hsl(value.hslColor.hue, value.hslColor.saturation, - value.hslColor.lightness, value.hslColor.alpha); - - case Value_Value.list: - var separator = _deprotofySeparator(value.list.separator); - if (value.list.contents.isEmpty) { - return SassList.empty( - separator: separator, brackets: value.list.hasBrackets); - } - - var length = value.list.contents.length; - if (separator == ListSeparator.undecided && length > 1) { - throw paramsError( - "List $value can't have an undecided separator because it has " - "$length elements"); - } - - return SassList([ - for (var element in value.list.contents) deprotofy(element) - ], separator, brackets: value.list.hasBrackets); - - case Value_Value.map: - return value.map.entries.isEmpty - ? const SassMap.empty() - : SassMap({ - for (var entry in value.map.entries) - deprotofy(entry.key): deprotofy(entry.value) - }); - - case Value_Value.compilerFunction: - var id = value.compilerFunction.id; - var function = functions[id]; - if (function == null) { - throw paramsError( - "CompilerFunction.id $id doesn't match any known functions"); - } - - return function; - - case Value_Value.hostFunction: - return SassFunction(hostCallable( - dispatcher, functions, value.hostFunction.signature, - id: value.hostFunction.id)); - - case Value_Value.singleton: - switch (value.singleton) { - case SingletonValue.TRUE: - return sassTrue; - case SingletonValue.FALSE: - return sassFalse; - case SingletonValue.NULL: - return sassNull; - default: - throw "Unknown Value.singleton ${value.singleton}"; - } - - case Value_Value.notSet: - default: - throw mandatoryError("Value.value"); - } - } on RangeError catch (error) { - var name = error.name; - if (name == null || error.start == null || error.end == null) { - throw paramsError(error.toString()); - } - - if (value.whichValue() == Value_Value.rgbColor) { - name = 'RgbColor.$name'; - } else if (value.whichValue() == Value_Value.hslColor) { - name = 'HslColor.$name'; - } - - throw paramsError( - '$name must be between ${error.start} and ${error.end}, was ' - '${error.invalidValue}'); - } -} - -/// Converts [separator] to its Sass representation. -ListSeparator _deprotofySeparator(proto.ListSeparator separator) { - switch (separator) { - case proto.ListSeparator.COMMA: - return ListSeparator.comma; - case proto.ListSeparator.SPACE: - return ListSeparator.space; - case proto.ListSeparator.SLASH: - return ListSeparator.slash; - case proto.ListSeparator.UNDECIDED: - return ListSeparator.undecided; - default: - throw "Unknown separator $separator"; - } -} diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 83cd9122c..623f67828 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 487c34d7387b6f368ed60ff2db6a748483aba2a1 +// Checksum: f7172be68e0a19c4dc2d2ad04fc32a843a98a6bd // // ignore_for_file: unused_import @@ -24,6 +24,7 @@ import 'extend/extension_store.dart'; import 'module.dart'; import 'module/forwarded_view.dart'; import 'module/shadowed_view.dart'; +import 'util/map.dart'; import 'util/merged_map_view.dart'; import 'util/nullable.dart'; import 'util/public_member_map_view.dart'; @@ -40,7 +41,7 @@ import 'visitor/clone_css.dart'; /// /// This tracks lexically-scoped information, such as variables, functions, and /// mixins. -class Environment { +final class Environment { /// The modules used in the current scope, indexed by their namespaces. Map> get modules => UnmodifiableMapView(_modules); final Map> _modules; @@ -243,12 +244,11 @@ class Environment { _globalModules[module] = nodeWithSpan; _allModules.add(module); - for (var name in _variables.first.keys) { - if (module.variables.containsKey(name)) { - throw SassScriptException( - 'This module and the new module both define a variable named ' - '"\$$name".'); - } + if (_variables.first.keys.firstWhereOrNull(module.variables.containsKey) + case var name?) { + throw SassScriptException( + 'This module and the new module both define a variable named ' + '"\$$name".'); } } else { if (_modules.containsKey(namespace)) { @@ -307,11 +307,12 @@ class Environment { larger = newMembers; } - for (var name in smaller.keys) { - if (!larger.containsKey(name)) continue; + for (var (name, small) in smaller.pairs) { + var large = larger[name]; + if (large == null) continue; if (type == "variable" ? newModule.variableIdentity(name) == oldModule.variableIdentity(name) - : larger[name] == smaller[name]) { + : large == small) { continue; } @@ -329,82 +330,82 @@ class Environment { /// /// This is called when [module] is `@import`ed. void importForwards(Module module) { - if (module is _EnvironmentModule) { - var forwarded = module._environment._forwardedModules; - if (forwarded == null) return; - - // Omit modules from [forwarded] that are already globally available and - // forwarded in this module. - var forwardedModules = _forwardedModules; - if (forwardedModules != null) { - forwarded = { - for (var entry in forwarded.entries) - if (!forwardedModules.containsKey(entry.key) || - !_globalModules.containsKey(entry.key)) - entry.key: entry.value, - }; - } else { - forwardedModules = _forwardedModules ??= {}; - } + if (module is! _EnvironmentModule) return; + var forwarded = module._environment._forwardedModules; + if (forwarded == null) return; + + // Omit modules from [forwarded] that are already globally available and + // forwarded in this module. + var forwardedModules = _forwardedModules; + if (forwardedModules != null) { + forwarded = { + for (var (module, node) in forwarded.pairs) + if (!forwardedModules.containsKey(module) || + !_globalModules.containsKey(module)) + module: node, + }; + } else { + forwardedModules = _forwardedModules ??= {}; + } - var forwardedVariableNames = - forwarded.keys.expand((module) => module.variables.keys).toSet(); - var forwardedFunctionNames = - forwarded.keys.expand((module) => module.functions.keys).toSet(); - var forwardedMixinNames = - forwarded.keys.expand((module) => module.mixins.keys).toSet(); - - if (atRoot) { - // Hide members from modules that have already been imported or - // forwarded that would otherwise conflict with the @imported members. - for (var entry in _importedModules.entries.toList()) { - var module = entry.key; - var shadowed = ShadowedModuleView.ifNecessary(module, - variables: forwardedVariableNames, - mixins: forwardedMixinNames, - functions: forwardedFunctionNames); - if (shadowed != null) { - _importedModules.remove(module); - if (!shadowed.isEmpty) _importedModules[shadowed] = entry.value; - } + var forwardedVariableNames = { + for (var module in forwarded.keys) ...module.variables.keys + }; + var forwardedFunctionNames = { + for (var module in forwarded.keys) ...module.functions.keys + }; + var forwardedMixinNames = { + for (var module in forwarded.keys) ...module.mixins.keys + }; + + if (atRoot) { + // Hide members from modules that have already been imported or + // forwarded that would otherwise conflict with the @imported members. + for (var (module, node) in _importedModules.pairs.toList()) { + var shadowed = ShadowedModuleView.ifNecessary(module, + variables: forwardedVariableNames, + mixins: forwardedMixinNames, + functions: forwardedFunctionNames); + if (shadowed != null) { + _importedModules.remove(module); + if (!shadowed.isEmpty) _importedModules[shadowed] = node; } + } - for (var entry in forwardedModules.entries.toList()) { - var module = entry.key; - var shadowed = ShadowedModuleView.ifNecessary(module, - variables: forwardedVariableNames, - mixins: forwardedMixinNames, - functions: forwardedFunctionNames); - if (shadowed != null) { - forwardedModules.remove(module); - if (!shadowed.isEmpty) forwardedModules[shadowed] = entry.value; - } + for (var (module, node) in forwardedModules.pairs.toList()) { + var shadowed = ShadowedModuleView.ifNecessary(module, + variables: forwardedVariableNames, + mixins: forwardedMixinNames, + functions: forwardedFunctionNames); + if (shadowed != null) { + forwardedModules.remove(module); + if (!shadowed.isEmpty) forwardedModules[shadowed] = node; } - - _importedModules.addAll(forwarded); - forwardedModules.addAll(forwarded); - } else { - (_nestedForwardedModules ??= - List.generate(_variables.length - 1, (_) => [])) - .last - .addAll(forwarded.keys); } - // Remove existing member definitions that are now shadowed by the - // forwarded modules. - for (var variable in forwardedVariableNames) { - _variableIndices.remove(variable); - _variables.last.remove(variable); - _variableNodes.last.remove(variable); - } - for (var function in forwardedFunctionNames) { - _functionIndices.remove(function); - _functions.last.remove(function); - } - for (var mixin in forwardedMixinNames) { - _mixinIndices.remove(mixin); - _mixins.last.remove(mixin); - } + _importedModules.addAll(forwarded); + forwardedModules.addAll(forwarded); + } else { + (_nestedForwardedModules ??= + List.generate(_variables.length - 1, (_) => [])) + .last + .addAll(forwarded.keys); + } + + // Remove existing member definitions that are now shadowed by the + // forwarded modules. + for (var variable in forwardedVariableNames) { + _variableIndices.remove(variable); + _variables.last.remove(variable); + _variableNodes.last.remove(variable); + } + for (var function in forwardedFunctionNames) { + _functionIndices.remove(function); + _functions.last.remove(function); + } + for (var mixin in forwardedMixinNames) { + _mixinIndices.remove(mixin); + _mixins.last.remove(mixin); } } @@ -421,25 +422,21 @@ class Environment { _getVariableFromGlobalModule(name); } - var index = _variableIndices[name]; - if (index != null) { + if (_variableIndices[name] case var index?) { _lastVariableName = name; _lastVariableIndex = index; return _variables[index][name] ?? _getVariableFromGlobalModule(name); - } - - index = _variableIndex(name); - if (index == null) { + } else if (_variableIndex(name) case var index?) { + _lastVariableName = name; + _lastVariableIndex = index; + _variableIndices[name] = index; + return _variables[index][name] ?? _getVariableFromGlobalModule(name); + } else { // There isn't a real variable defined as this index, but it will cause // [getVariable] to short-circuit and get to this function faster next // time the variable is accessed. return _getVariableFromGlobalModule(name); } - - _lastVariableName = name; - _lastVariableIndex = index; - _variableIndices[name] = index; - return _variables[index][name] ?? _getVariableFromGlobalModule(name); } /// Returns the value of the variable named [name] from a namespaceless @@ -464,22 +461,20 @@ class Environment { _getVariableNodeFromGlobalModule(name); } - var index = _variableIndices[name]; - if (index != null) { + if (_variableIndices[name] case var index?) { _lastVariableName = name; _lastVariableIndex = index; return _variableNodes[index][name] ?? _getVariableNodeFromGlobalModule(name); + } else if (_variableIndex(name) case var index?) { + _lastVariableName = name; + _lastVariableIndex = index; + _variableIndices[name] = index; + return _variableNodes[index][name] ?? + _getVariableNodeFromGlobalModule(name); + } else { + return _getVariableNodeFromGlobalModule(name); } - - index = _variableIndex(name); - if (index == null) return _getVariableNodeFromGlobalModule(name); - - _lastVariableName = name; - _lastVariableIndex = index; - _variableIndices[name] = index; - return _variableNodes[index][name] ?? - _getVariableNodeFromGlobalModule(name); } /// Returns the node for the variable named [name] from a namespaceless @@ -494,8 +489,7 @@ class Environment { // We don't need to worry about multiple modules defining the same variable, // because that's already been checked by [getVariable]. for (var module in _importedModules.keys.followedBy(_globalModules.keys)) { - var value = module.variableNodes[name]; - if (value != null) return value; + if (module.variableNodes[name] case var value?) return value; } return null; } @@ -629,16 +623,14 @@ class Environment { Callable? getFunction(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).functions[name]; - var index = _functionIndices[name]; - if (index != null) { + if (_functionIndices[name] case var index?) { + return _functions[index][name] ?? _getFunctionFromGlobalModule(name); + } else if (_functionIndex(name) case var index?) { + _functionIndices[name] = index; return _functions[index][name] ?? _getFunctionFromGlobalModule(name); + } else { + return _getFunctionFromGlobalModule(name); } - - index = _functionIndex(name); - if (index == null) return _getFunctionFromGlobalModule(name); - - _functionIndices[name] = index; - return _functions[index][name] ?? _getFunctionFromGlobalModule(name); } /// Returns the value of the function named [name] from a namespaceless @@ -678,16 +670,14 @@ class Environment { Callable? getMixin(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).mixins[name]; - var index = _mixinIndices[name]; - if (index != null) { + if (_mixinIndices[name] case var index?) { + return _mixins[index][name] ?? _getMixinFromGlobalModule(name); + } else if (_mixinIndex(name) case var index?) { + _mixinIndices[name] = index; return _mixins[index][name] ?? _getMixinFromGlobalModule(name); + } else { + return _getMixinFromGlobalModule(name); } - - index = _mixinIndex(name); - if (index == null) return _getMixinFromGlobalModule(name); - - _mixinIndices[name] = index; - return _mixins[index][name] ?? _getMixinFromGlobalModule(name); } /// Returns the value of the mixin named [name] from a namespaceless @@ -797,11 +787,10 @@ class Environment { for (var i = 0; i < _variables.length; i++) { var values = _variables[i]; var nodes = _variableNodes[i]; - for (var entry in values.entries) { + for (var (name, value) in values.pairs) { // Implicit configurations are never invalid, making [configurationSpan] // unnecessary, so we pass null here to avoid having to compute it. - configuration[entry.key] = - ConfiguredValue.implicit(entry.value, nodes[entry.key]!); + configuration[name] = ConfiguredValue.implicit(value, nodes[name]!); } } return Configuration.implicit(configuration); @@ -825,22 +814,18 @@ class Environment { /// This is used when resolving imports, since they need to inject forwarded /// members into the current scope. It's the only situation in which a nested /// environment can become a module. - Module toDummyModule() { - return _EnvironmentModule( - this, - CssStylesheet(const [], - SourceFile.decoded(const [], url: "").span(0)), - const {}, - ExtensionStore.empty, - forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules))); - } + Module toDummyModule() => _EnvironmentModule( + this, + CssStylesheet(const [], + SourceFile.decoded(const [], url: "").span(0)), + const {}, + ExtensionStore.empty, + forwarded: _forwardedModules.andThen((modules) => MapKeySet(modules))); /// Returns the module with the given [namespace], or throws a /// [SassScriptException] if none exists. Module _getModule(String namespace) { - var module = _modules[namespace]; - if (module != null) return module; - + if (_modules[namespace] case var module?) return module; throw SassScriptException( 'There is no module with the namespace "$namespace".'); } @@ -858,18 +843,15 @@ class Environment { /// It's used to format an appropriate error message. T? _fromOneModule( String name, String type, T? callback(Module module)) { - var nestedForwardedModules = _nestedForwardedModules; - if (nestedForwardedModules != null) { + if (_nestedForwardedModules case var nestedForwardedModules?) { for (var modules in nestedForwardedModules.reversed) { for (var module in modules.reversed) { - var value = callback(module); - if (value != null) return value; + if (callback(module) case var value?) return value; } } } for (var module in _importedModules.keys) { - var value = callback(module); - if (value != null) return value; + if (callback(module) case var value?) return value; } T? value; @@ -884,14 +866,11 @@ class Environment { if (identityFromModule == identity) continue; if (value != null) { - var spans = _globalModules.entries.map( - (entry) => callback(entry.key).andThen((_) => entry.value.span)); - throw MultiSpanSassScriptException( 'This $type is available from multiple global modules.', '$type use', { - for (var span in spans) - if (span != null) span: 'includes $type' + for (var (module, node) in _globalModules.pairs) + if (callback(module) != null) node.span: 'includes $type' }); } @@ -903,7 +882,7 @@ class Environment { } /// A module that represents the top-level members defined in an [Environment]. -class _EnvironmentModule implements Module { +final class _EnvironmentModule implements Module { Uri? get url => css.span.sourceUrl; final List> upstream; @@ -939,8 +918,8 @@ class _EnvironmentModule implements Module { environment, css, Map.unmodifiable({ - for (var entry in preModuleComments.entries) - entry.key: List.unmodifiable(entry.value) + for (var (module, comments) in preModuleComments.pairs) + module: List.unmodifiable(comments) }), extensionStore, _makeModulesByVariable(forwarded), @@ -1014,8 +993,7 @@ class _EnvironmentModule implements Module { : upstream = _environment._allModules; void setVariable(String name, Value value, AstNode nodeWithSpan) { - var module = _modulesByVariable[name]; - if (module != null) { + if (_modulesByVariable[name] case var module?) { module.setVariable(name, value, nodeWithSpan); return; } @@ -1038,12 +1016,13 @@ class _EnvironmentModule implements Module { Module cloneCss() { if (!transitivelyContainsCss) return this; - var newCssAndExtensionStore = cloneCssStylesheet(css, extensionStore); + var (newStylesheet, newExtensionStore) = + cloneCssStylesheet(css, extensionStore); return _EnvironmentModule._( _environment, - newCssAndExtensionStore.item1, + newStylesheet, preModuleComments, - newCssAndExtensionStore.item2, + newExtensionStore, _modulesByVariable, variables, variableNodes, diff --git a/lib/src/evaluation_context.dart b/lib/src/evaluation_context.dart index a18ccb471..5c1b074a9 100644 --- a/lib/src/evaluation_context.dart +++ b/lib/src/evaluation_context.dart @@ -12,15 +12,17 @@ import 'deprecation.dart'; /// /// This allows us to expose zone-scoped information without having to create a /// new zone variable for each piece of information. -abstract class EvaluationContext { +abstract interface class EvaluationContext { /// The current evaluation context. /// /// Throws [StateError] if there isn't a Sass stylesheet currently being /// evaluated. static EvaluationContext get current { - var context = Zone.current[#_evaluationContext]; - if (context is EvaluationContext) return context; - throw StateError("No Sass stylesheet is currently being evaluated."); + if (Zone.current[#_evaluationContext] case EvaluationContext context) { + return context; + } else { + throw StateError("No Sass stylesheet is currently being evaluated."); + } } /// Returns the span for the currently executing callable. diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index dee1c3eb1..504999c49 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -2,15 +2,12 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:collection'; - 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'; import '../../sass.dart'; import '../io.dart'; @@ -20,7 +17,7 @@ import '../util/character.dart'; /// /// The constructor and any members may throw [UsageException]s indicating that /// invalid arguments were passed. -class ExecutableOptions { +final class ExecutableOptions { /// The bar character to use in help separators. static final _separatorBar = isWindows ? '=' : '━'; @@ -94,17 +91,25 @@ class ExecutableOptions { 'a complete list.', allowedHelp: { for (var deprecation in Deprecation.values) - if (deprecation.deprecatedIn != null && - deprecation.description != null) - deprecation.id: deprecation.description!, + if (deprecation + case Deprecation( + deprecatedIn: _?, + :var id, + :var description? + )) + id: 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!, + if (deprecation + case Deprecation( + deprecatedIn: null, + :var id, + :var description? + )) + id: description }); parser @@ -167,10 +172,8 @@ class ExecutableOptions { 'stdin', 'indented', 'style', 'source-map', 'source-map-urls', // 'embed-sources', 'embed-source-map', 'update', 'watch' ]; - for (var option in invalidOptions) { - if (_options.wasParsed(option)) { - throw UsageException("--$option isn't allowed with --interactive."); - } + if (invalidOptions.firstWhereOrNull(_options.wasParsed) case var option?) { + throw UsageException("--$option isn't allowed with --interactive."); } return true; }(); @@ -359,10 +362,7 @@ class ExecutableOptions { continue; } - var sourceAndDestination = _splitSourceAndDestination(argument); - var source = sourceAndDestination.item1; - var destination = sourceAndDestination.item2; - + var (source, destination) = _splitSourceAndDestination(argument); if (!seen.add(source)) _fail('Duplicate source "$source".'); if (source == '-') { @@ -381,7 +381,7 @@ class ExecutableOptions { /// Splits an argument that contains a colon and returns its source and its /// destination component. - Tuple2 _splitSourceAndDestination(String argument) { + (String, String) _splitSourceAndDestination(String argument) { for (var i = 0; i < argument.length; i++) { // A colon at position 1 may be a Windows drive letter and not a // separator. @@ -396,7 +396,7 @@ class ExecutableOptions { } if (nextColon != -1) _fail('"$argument" may only contain one ":".'); - return Tuple2(argument.substring(0, i), argument.substring(i + 1)); + return (argument.substring(0, i), argument.substring(i + 1)); } } @@ -406,7 +406,7 @@ class ExecutableOptions { /// Returns whether [string] contains an absolute Windows path at [index]. bool _isWindowsPath(String string, int index) => string.length > index + 2 && - isAlphabetic(string.codeUnitAt(index)) && + string.codeUnitAt(index).isAlphabetic && string.codeUnitAt(index + 1) == $colon; /// Returns the sub-map of [sourcesToDestinations] for the given [source] and @@ -512,28 +512,27 @@ class ExecutableOptions { Set get fatalDeprecations => _fatalDeprecations ??= () { var deprecations = {}; for (var id in _options['fatal-deprecation'] as List) { - var deprecation = Deprecation.fromId(id); - if (deprecation != null) { + if (Deprecation.fromId(id) case var deprecation?) { 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".'); + continue; + } + + 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; diff --git a/lib/src/executable/watch.dart b/lib/src/executable/watch.dart index 47d809e8d..1fc8d9f37 100644 --- a/lib/src/executable/watch.dart +++ b/lib/src/executable/watch.dart @@ -13,6 +13,7 @@ import '../exception.dart'; import '../importer/filesystem.dart'; import '../io.dart'; import '../stylesheet_graph.dart'; +import '../util/map.dart'; import '../util/multi_dir_watcher.dart'; import '../utils.dart'; import 'compile_stylesheet.dart'; @@ -40,12 +41,11 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async { // they currently exist. This ensures that changes that come in update a // known-good state. var watcher = _Watcher(options, graph); - for (var entry in _sourcesToDestinations(options).entries) { - graph.addCanonical(FilesystemImporter('.'), - p.toUri(canonicalize(entry.key)), p.toUri(entry.key), + for (var (source, destination) in _sourcesToDestinations(options).pairs) { + graph.addCanonical( + FilesystemImporter('.'), p.toUri(canonicalize(source)), p.toUri(source), recanonicalize: false); - var success = - await watcher.compile(entry.key, entry.value, ifModified: true); + var success = await watcher.compile(source, destination, ifModified: true); if (!success && options.stopOnError) { dirWatcher.events.listen(null).cancel(); return; @@ -58,7 +58,7 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async { /// Holds state that's shared across functions that react to changes on the /// filesystem. -class _Watcher { +final class _Watcher { /// The options for the Sass executable. final ExecutableOptions _options; @@ -138,17 +138,14 @@ class _Watcher { case ChangeType.MODIFY: var success = await _handleModify(event.path); if (!success && _options.stopOnError) return; - break; case ChangeType.ADD: var success = await _handleAdd(event.path); if (!success && _options.stopOnError) return; - break; case ChangeType.REMOVE: var success = await _handleRemove(event.path); if (!success && _options.stopOnError) return; - break; } } } @@ -162,11 +159,12 @@ class _Watcher { // It's important to access the node ahead-of-time because it's possible // that `_graph.reload()` notices the file has been deleted and removes it // from the graph. - var node = _graph.nodes[url]; - if (node == null) return _handleAdd(path); - - _graph.reload(url); - return await _recompileDownstream([node]); + if (_graph.nodes[url] case var node?) { + _graph.reload(url); + return await _recompileDownstream([node]); + } else { + return _handleAdd(path); + } } /// Handles an add event for the stylesheet at [url]. @@ -188,8 +186,7 @@ class _Watcher { var url = _canonicalize(path); if (_graph.nodes.containsKey(url)) { - var destination = _destinationFor(path); - if (destination != null) _delete(destination); + if (_destinationFor(path) case var destination?) _delete(destination); } var downstream = _graph.remove(FilesystemImporter('.'), url); @@ -208,19 +205,17 @@ class _Watcher { var typeForPath = p.PathMap(); for (var event in buffer) { var oldType = typeForPath[event.path]; - if (oldType == null) { - typeForPath[event.path] = event.type; - } else if (event.type == ChangeType.REMOVE) { - typeForPath[event.path] = ChangeType.REMOVE; - } else if (oldType != ChangeType.ADD) { - typeForPath[event.path] = ChangeType.MODIFY; - } + typeForPath[event.path] = switch ((oldType, event.type)) { + (null, var newType) => newType, + (_, ChangeType.REMOVE) => ChangeType.REMOVE, + (ChangeType.ADD, _) => ChangeType.ADD, + (_, _) => ChangeType.MODIFY + }; } return [ - for (var entry in typeForPath.entries) - // PathMap always has nullable keys - WatchEvent(entry.value, entry.key!) + // PathMap always has nullable keys + for (var (path!, type) in typeForPath.pairs) WatchEvent(type, path) ]; }); } @@ -255,10 +250,10 @@ class _Watcher { if (url.scheme != 'file') return true; var source = p.fromUri(url); - var destination = _destinationFor(source); - if (destination == null) return true; - - return await compile(source, destination); + return switch (_destinationFor(source)) { + var destination? => await compile(source, destination), + _ => true + }; } /// If a Sass file at [source] should be compiled to CSS, returns the path to @@ -266,15 +261,17 @@ class _Watcher { /// /// Otherwise, returns `null`. String? _destinationFor(String source) { - var destination = _sourcesToDestinations(_options)[source]; - if (destination != null) return destination; + if (_sourcesToDestinations(_options)[source] case var destination?) { + return destination; + } if (p.basename(source).startsWith('_')) return null; - for (var entry in _sourceDirectoriesToDestinations(_options).entries) { - if (!p.isWithin(entry.key, source)) continue; + for (var (sourceDir, destinationDir) + in _sourceDirectoriesToDestinations(_options).pairs) { + if (!p.isWithin(sourceDir, source)) continue; - var destination = p.join(entry.value, - p.setExtension(p.relative(source, from: entry.key), '.css')); + var destination = p.join(destinationDir, + p.setExtension(p.relative(source, from: sourceDir), '.css')); // Don't compile ".css" files to their own locations. if (!p.equals(destination, source)) return destination; diff --git a/lib/src/extend/empty_extension_store.dart b/lib/src/extend/empty_extension_store.dart index 46aef4d93..a00c85d86 100644 --- a/lib/src/extend/empty_extension_store.dart +++ b/lib/src/extend/empty_extension_store.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; -import 'package:tuple/tuple.dart'; import '../ast/css.dart'; import '../ast/selector.dart'; @@ -14,7 +13,7 @@ import 'extension.dart'; /// An [ExtensionStore] that contains no extensions and can have no extensions /// added. -class EmptyExtensionStore implements ExtensionStore { +final class EmptyExtensionStore implements ExtensionStore { bool get isEmpty => true; Set get simpleSelectors => const UnmodifiableSetView.empty(); @@ -43,6 +42,6 @@ class EmptyExtensionStore implements ExtensionStore { "addExtensions() can't be called for a const ExtensionStore."); } - Tuple2>> clone() => - const Tuple2(EmptyExtensionStore(), {}); + (ExtensionStore, Map>) clone() => + const (EmptyExtensionStore(), {}); } diff --git a/lib/src/extend/extension.dart b/lib/src/extend/extension.dart index 3323dabf1..87f0995d7 100644 --- a/lib/src/extend/extension.dart +++ b/lib/src/extend/extension.dart @@ -51,7 +51,7 @@ class Extension { /// A selector that's extending another selector, such as `A` in `A {@extend /// B}`. -class Extender { +final class Extender { /// The selector in which the `@extend` appeared. final ComplexSelector selector; diff --git a/lib/src/extend/extension_store.dart b/lib/src/extend/extension_store.dart index 24bdc43ad..a3f276ee9 100644 --- a/lib/src/extend/extension_store.dart +++ b/lib/src/extend/extension_store.dart @@ -6,15 +6,15 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:source_span/source_span.dart'; -import 'package:tuple/tuple.dart'; import '../ast/css.dart'; import '../ast/selector.dart'; import '../ast/sass.dart'; import '../exception.dart'; -import '../utils.dart'; import '../util/box.dart'; +import '../util/map.dart'; import '../util/nullable.dart'; +import '../utils.dart'; import 'empty_extension_store.dart'; import 'extension.dart'; import 'merged_extension.dart'; @@ -96,9 +96,7 @@ class ExtensionStore { ExtendMode mode, FileSpan span) { var extender = ExtensionStore._mode(mode); - if (!selector.isInvisible) { - extender._originals.addAll(selector.components); - } + if (!selector.isInvisible) extender._originals.addAll(selector.components); for (var complex in targets.components) { var compound = complex.singleCompound; @@ -150,9 +148,9 @@ class ExtensionStore { /// returned. Iterable extensionsWhereTarget( bool callback(SimpleSelector target)) sync* { - for (var entry in _extensions.entries) { - if (!callback(entry.key)) continue; - for (var extension in entry.value.values) { + for (var (simple, sources) in _extensions.pairs) { + if (!callback(simple)) continue; + for (var extension in sources.values) { if (extension is MergedExtension) { yield* extension .unmerge() @@ -176,9 +174,7 @@ class ExtensionStore { [List? mediaContext]) { var originalSelector = selector; if (!originalSelector.isInvisible) { - for (var complex in originalSelector.components) { - _originals.add(complex); - } + _originals.addAll(originalSelector.components); } if (_extensions.isNotEmpty) { @@ -190,6 +186,7 @@ class ExtensionStore { "From ${error.span.message('')}\n" "${error.message}", error.span), + error, stackTrace); } } @@ -209,10 +206,7 @@ class ExtensionStore { for (var component in complex.components) { for (var simple in component.selector.components) { _selectors.putIfAbsent(simple, () => {}).add(selector); - if (simple is! PseudoSelector) continue; - - var selectorInPseudo = simple.selector; - if (selectorInPseudo != null) { + if (simple case PseudoSelector(selector: var selectorInPseudo?)) { _registerSelector(selectorInPseudo, selector); } } @@ -243,15 +237,13 @@ class ExtensionStore { var extension = Extension(complex, target, extend.span, mediaContext: mediaContext, optional: extend.isOptional); - var existingExtension = sources[complex]; - if (existingExtension != null) { + if (sources[complex] case var existingExtension?) { // If there's already an extend from [extender] to [target], we don't need // to re-run the extension. We may need to mark the extension as // mandatory, though. sources[complex] = MergedExtension.merge(existingExtension, extension); continue; } - sources[complex] = extension; for (var simple in _simpleSelectors(complex)) { @@ -289,11 +281,10 @@ class ExtensionStore { for (var simple in component.selector.components) { yield simple; - if (simple is! PseudoSelector) continue; - var selector = simple.selector; - if (selector == null) continue; - for (var complex in selector.components) { - yield* _simpleSelectors(complex); + if (simple case PseudoSelector(:var selector?)) { + for (var complex in selector.components) { + yield* _simpleSelectors(complex); + } } } } @@ -322,7 +313,7 @@ class ExtensionStore { for (var extension in extensions.toList()) { var sources = _extensions[extension.target]!; - List? selectors; + Iterable? selectors; try { selectors = _extendComplex( extension.extender.selector, newExtensions, extension.mediaContext); @@ -331,22 +322,18 @@ class ExtensionStore { throwWithTrace( error.withAdditionalSpan( extension.extender.selector.span, "target selector"), + error, stackTrace); } + // If the output contains the original complex selector, there's no need + // to recreate it. var containsExtension = selectors.first == extension.extender.selector; - var first = true; - for (var complex in selectors) { - // If the output contains the original complex selector, there's no - // need to recreate it. - if (containsExtension && first) { - first = false; - continue; - } + if (containsExtension) selectors = selectors.skip(1); + for (var complex in selectors) { var withExtender = extension.withExtender(complex); - var existingExtension = sources[complex]; - if (existingExtension != null) { + if (sources[complex] case var existingExtension?) { sources[complex] = MergedExtension.merge(existingExtension, withExtender); } else { @@ -393,6 +380,7 @@ class ExtensionStore { "From ${selector.value.span.message('')}\n" "${error.message}", error.span), + error, stackTrace); } @@ -423,9 +411,9 @@ class ExtensionStore { for (var extensionStore in extensionStores) { if (extensionStore.isEmpty) continue; _sourceSpecificity.addAll(extensionStore._sourceSpecificity); - extensionStore._extensions.forEach((target, newSources) { + for (var (target, newSources) in extensionStore._extensions.pairs) { // Private selectors can't be extended across module boundaries. - if (target is PlaceholderSelector && target.isPrivate) return; + if (target case PlaceholderSelector(isPrivate: true)) continue; // Find existing extensions to extend. var extensionsForTarget = _extensionsByExtender[target]; @@ -440,14 +428,8 @@ class ExtensionStore { } // Add [newSources] to [_extensions]. - var existingSources = _extensions[target]; - if (existingSources == null) { - _extensions[target] = Map.of(newSources); - if (extensionsForTarget != null || selectorsForTarget != null) { - (newExtensions ??= {})[target] = Map.of(newSources); - } - } else { - newSources.forEach((extender, extension) { + if (_extensions[target] case var existingSources?) { + for (var (extender, extension) in newSources.pairs) { extension = existingSources.putOrMerge( extender, extension, MergedExtension.merge); @@ -455,21 +437,27 @@ class ExtensionStore { (newExtensions ??= {}).putIfAbsent(target, () => {})[extender] = extension; } - }); + } + } else { + _extensions[target] = Map.of(newSources); + if (extensionsForTarget != null || selectorsForTarget != null) { + (newExtensions ??= {})[target] = Map.of(newSources); + } } - }); + } } - // We can't just naively check for `null` here due to dart-lang/sdk#45348. - newExtensions.andThen((newExtensions) { + if (newExtensions != null) { // We can ignore the return value here because it's only useful for extend // loops, which can't exist across module boundaries. - extensionsToExtend.andThen((extensionsToExtend) => - _extendExistingExtensions(extensionsToExtend, newExtensions)); + if (extensionsToExtend != null) { + _extendExistingExtensions(extensionsToExtend, newExtensions); + } - selectorsToExtend.andThen((selectorsToExtend) => - _extendExistingSelectors(selectorsToExtend, newExtensions)); - }); + if (selectorsToExtend != null) { + _extendExistingSelectors(selectorsToExtend, newExtensions); + } + } } /// Extends [list] using [extensions]. @@ -487,7 +475,7 @@ class ExtensionStore { '_extendComplex($complex) should return null rather than [] if ' 'extension fails'); if (result == null) { - if (extended != null) extended.add(complex); + extended?.add(complex); } else { extended ??= i == 0 ? [] : list.components.sublist(0, i).toList(); extended.addAll(result); @@ -639,9 +627,9 @@ class ExtensionStore { // Optimize for the simple case of a single simple selector that doesn't // need any unification. - if (options.length == 1) { + if (options case [var extenders]) { List? result; - for (var extender in options.first) { + for (var extender in extenders) { extender.assertCompatibleMediaContext(mediaQueryContext); var complex = extender.selector.withAdditionalCombinators(component.combinators); @@ -788,9 +776,9 @@ class ExtensionStore { ]; } - if (simple is PseudoSelector && simple.selector != null) { - var extended = _extendPseudo(simple, extensions, mediaQueryContext); - if (extended != null) { + if (simple case PseudoSelector(selector: _?)) { + if (_extendPseudo(simple, extensions, mediaQueryContext) + case var extended?) { return extended.map( (pseudo) => withoutPseudo(pseudo) ?? [_extenderForSimple(pseudo)]); } @@ -993,7 +981,7 @@ class ExtensionStore { /// 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() { + (ExtensionStore, Map>) clone() { var newSelectors = >>{}; var newMediaContexts = , List>{}; var oldToNewSelectors = Map>.identity(); @@ -1007,19 +995,21 @@ class ExtensionStore { newSelectorSet.add(newSelector); oldToNewSelectors[selector.value] = newSelector.seal(); - var mediaContext = _mediaContexts[selector]; - if (mediaContext != null) newMediaContexts[newSelector] = mediaContext; + if (_mediaContexts[selector] case var mediaContext?) { + newMediaContexts[newSelector] = mediaContext; + } } }); - return Tuple2( - ExtensionStore._( - newSelectors, - copyMapOfMap(_extensions), - copyMapOfList(_extensionsByExtender), - newMediaContexts, - Map.identity()..addAll(_sourceSpecificity), - Set.identity()..addAll(_originals)), - oldToNewSelectors); + return ( + ExtensionStore._( + newSelectors, + copyMapOfMap(_extensions), + copyMapOfList(_extensionsByExtender), + newMediaContexts, + Map.identity()..addAll(_sourceSpecificity), + Set.identity()..addAll(_originals)), + oldToNewSelectors + ); } } diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index 047e48174..6299c5fcf 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -14,10 +14,10 @@ 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/iterable.dart'; import '../util/span.dart'; import '../utils.dart'; @@ -41,19 +41,23 @@ List? unifyComplex( for (var complex in complexes) { if (complex.isUseless) return null; - if (complex.components.length == 1 && - complex.leadingCombinators.isNotEmpty) { - var newLeadingCombinator = complex.leadingCombinators.single; - if (leadingCombinator != null && - leadingCombinator != newLeadingCombinator) { + if (complex + case ComplexSelector( + components: [_], + leadingCombinators: [var newLeadingCombinator] + )) { + if (leadingCombinator == null) { + leadingCombinator = newLeadingCombinator; + } else if (leadingCombinator != newLeadingCombinator) { return null; } - leadingCombinator = newLeadingCombinator; } var base = complex.components.last; - if (base.combinators.isNotEmpty) { - var newTrailingCombinator = base.combinators.single; + if (base + case ComplexSelectorComponent( + combinators: [var newTrailingCombinator] + )) { if (trailingCombinator != null && trailingCombinator != newTrailingCombinator) { return null; @@ -125,29 +129,8 @@ CompoundSelector? unifyCompound( /// If no such selector can be produced, returns `null`. SimpleSelector? unifyUniversalAndElement( SimpleSelector selector1, SimpleSelector selector2) { - String? namespace1; - String? name1; - if (selector1 is UniversalSelector) { - namespace1 = selector1.namespace; - } else if (selector1 is TypeSelector) { - namespace1 = selector1.name.namespace; - name1 = selector1.name.name; - } else { - throw ArgumentError.value(selector1, 'selector1', - 'must be a UniversalSelector or a TypeSelector'); - } - - String? namespace2; - String? name2; - if (selector2 is UniversalSelector) { - namespace2 = selector2.namespace; - } else if (selector2 is TypeSelector) { - namespace2 = selector2.name.namespace; - name2 = selector2.name.name; - } else { - throw ArgumentError.value(selector2, 'selector2', - 'must be a UniversalSelector or a TypeSelector'); - } + var (namespace1, name1) = _namespaceAndName(selector1, 'selector1'); + var (namespace2, name2) = _namespaceAndName(selector2, 'selector2'); String? namespace; if (namespace1 == namespace2 || namespace2 == '*') { @@ -172,6 +155,22 @@ SimpleSelector? unifyUniversalAndElement( : TypeSelector(QualifiedName(name, namespace: namespace), selector1.span); } +/// Returns the namespace and name for [selector], which must be a +/// [UniversalSelector] or a [TypeSelector]. +/// +/// The [name] parameter is used for error reporting. +(String? namespace, String? name) _namespaceAndName( + SimpleSelector selector, String name) => + switch (selector) { + UniversalSelector(:var namespace) => (namespace, null), + TypeSelector(name: QualifiedName(:var name, :var namespace)) => ( + namespace, + name + ), + _ => throw ArgumentError.value( + selector, name, 'must be a UniversalSelector or a TypeSelector') + }; + /// Expands "parenthesized selectors" in [complexes]. /// /// That is, if we have `.A .B {@extend .C}` and `.D .C {...}`, this @@ -188,8 +187,7 @@ SimpleSelector? unifyUniversalAndElement( /// as having line breaks. List weave(List complexes, FileSpan span, {bool forceLineBreak = false}) { - if (complexes.length == 1) { - var complex = complexes.first; + if (complexes case [var complex]) { if (!forceLineBreak || complex.lineBreak) return complexes; return [ ComplexSelector( @@ -199,7 +197,6 @@ List weave(List complexes, FileSpan span, } var prefixes = [complexes.first]; - for (var complex in complexes.skip(1)) { if (complex.components.length == 1) { for (var i = 0; i < prefixes.length; i++) { @@ -249,30 +246,30 @@ Iterable? _weaveParents( // Make queues of _only_ the parent selectors. The prefix only contains // parents, but the complex selector has a target that we don't want to weave // in. - var queue1 = Queue.of(prefix.components); - var queue2 = Queue.of(base.components.exceptLast); + var queue1 = QueueList.from(prefix.components); + var queue2 = QueueList.from(base.components.exceptLast); 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 // with one another. - var rootish1 = _firstIfRootish(queue1); - var rootish2 = _firstIfRootish(queue2); - if (rootish1 != null && rootish2 != null) { - var rootish = unifyCompound(rootish1.selector, rootish2.selector); - if (rootish == null) return null; - 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 - // it to the beginning of _both_ queues. - var rootish = (rootish1 ?? rootish2)!; - queue1.addFirst(rootish); - queue2.addFirst(rootish); + switch ((_firstIfRootish(queue1), _firstIfRootish(queue2))) { + case (var rootish1?, var rootish2?): + var rootish = unifyCompound(rootish1.selector, rootish2.selector); + if (rootish == null) return null; + queue1.addFirst(ComplexSelectorComponent( + rootish, rootish1.combinators, rootish1.span)); + queue2.addFirst(ComplexSelectorComponent( + rootish, rootish2.combinators, rootish1.span)); + + case (var rootish?, null): + case (null, var rootish?): + // 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 + // it to the beginning of _both_ queues. + queue1.addFirst(rootish); + queue2.addFirst(rootish); } var groups1 = _groupSelectors(queue1); @@ -288,9 +285,7 @@ Iterable? _weaveParents( ComplexSelector(const [], group1, span), ComplexSelector(const [], group2, span) ], span); - if (unified == null) return null; - if (unified.length > 1) return null; - return unified.first.components; + return unified?.singleOrNull?.components; }); var choices = >>[]; @@ -324,17 +319,17 @@ Iterable? _weaveParents( /// appear in a complex selector's first component, removes and returns that /// element. ComplexSelectorComponent? _firstIfRootish( - Queue queue) { - if (queue.isEmpty) return null; - var first = queue.first; - for (var simple in first.selector.components) { - if (simple is PseudoSelector && - simple.isClass && - _rootishPseudoClasses.contains(simple.normalizedName)) { - queue.removeFirst(); - return first; + QueueList queue) { + if (queue case [var first, ...]) { + for (var simple in first.selector.components) { + if (simple case PseudoSelector(isClass: true, :var normalizedName) + when _rootishPseudoClasses.contains(normalizedName)) { + queue.removeFirst(); + return first; + } } } + return null; } @@ -343,17 +338,15 @@ ComplexSelectorComponent? _firstIfRootish( /// /// Returns `null` if the combinator lists can't be unified. 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; - if (combinators1.length > 1) return null; - if (combinators2.length > 1) return null; - if (combinators1.isEmpty) return combinators2; - if (combinators2.isEmpty) return combinators1; - return listEquals(combinators1, combinators2) ? combinators1 : null; -} + List>? combinators1, + List>? combinators2) => + // Allow null arguments just to make calls to `Iterable.reduce()` easier. + switch ((combinators1, combinators2)) { + (null, _) || (_, null) => null, + (List(length: > 1), _) || (_, List(length: > 1)) => null, + ([], var combinators) || (var combinators, []) => combinators, + _ => listEquals(combinators1, combinators2) ? combinators1 : null + }; /// Extracts trailing [ComplexSelectorComponent]s with trailing combinators from /// [components1] and [components2] and merges them together into a single list. @@ -369,32 +362,36 @@ List>? _mergeLeadingCombinators( /// /// The [span] will be used for any new combined selectors. List>>? _mergeTrailingCombinators( - Queue components1, - Queue components2, + QueueList components1, + QueueList 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 = switch (components1) { + [..., var last] => last.combinators, + _ => const >[] + }; + var combinators2 = switch (components2) { + [..., var last] => last.combinators, + _ => const >[] + }; if (combinators1.isEmpty && combinators2.isEmpty) return result; - if (combinators1.length > 1 || combinators2.length > 1) return null; // This code looks complicated, but it's actually just a bunch of special // cases for interactions between different combinators. - var combinator1 = combinators1.isEmpty ? null : combinators1.first; - var combinator2 = combinators2.isEmpty ? null : combinators2.first; - if (combinator1 != null && combinator2 != null) { - var component1 = components1.removeLast(); - var component2 = components2.removeLast(); - - if (combinator1.value == Combinator.followingSibling && - combinator2.value == Combinator.followingSibling) { + switch (( + combinators1.firstOrNull?.value, + combinators2.firstOrNull?.value, + // Include the component lists in the pattern match so we can easily + // generalize cases across different orderings of the two combinators. + components1, + components2 + )) { + case (Combinator.followingSibling, Combinator.followingSibling, _, _): + var component1 = components1.removeLast(); + var component2 = components2.removeLast(); if (component1.selector.isSuperselector(component2.selector)) { result.addFirst([ [component2] @@ -409,92 +406,97 @@ List>>? _mergeTrailingCombinators( [component2, component1] ]; - var unified = unifyCompound(component1.selector, component2.selector); - if (unified != null) { + if (unifyCompound(component1.selector, component2.selector) + case var unified?) { choices.add([ - ComplexSelectorComponent(unified, [combinator1], span) + ComplexSelectorComponent(unified, [combinators1.first], span) ]); } result.addFirst(choices); } - } else if ((combinator1.value == Combinator.followingSibling && - combinator2.value == Combinator.nextSibling) || - (combinator1.value == Combinator.nextSibling && - combinator2.value == Combinator.followingSibling)) { - var followingSiblingComponent = - combinator1.value == Combinator.followingSibling - ? component1 - : component2; - var nextSiblingComponent = - combinator1.value == Combinator.followingSibling - ? component2 - : component1; - - if (followingSiblingComponent.selector - .isSuperselector(nextSiblingComponent.selector)) { + + case ( + Combinator.followingSibling, + Combinator.nextSibling, + var followingComponents, + var nextComponents + ) || + ( + Combinator.nextSibling, + Combinator.followingSibling, + var nextComponents, + var followingComponents + ): + var next = nextComponents.removeLast(); + var following = followingComponents.removeLast(); + if (following.selector.isSuperselector(next.selector)) { result.addFirst([ - [nextSiblingComponent] + [next] ]); } else { - var unified = unifyCompound(component1.selector, component2.selector); result.addFirst([ - [followingSiblingComponent, nextSiblingComponent], - if (unified != null) - [ - ComplexSelectorComponent( - unified, nextSiblingComponent.combinators, span) - ] + [following, next], + if (unifyCompound(following.selector, next.selector) + case var unified?) + [ComplexSelectorComponent(unified, next.combinators, span)] ]); } - } else if (combinator1.value == Combinator.child && - (combinator2.value == Combinator.nextSibling || - combinator2.value == Combinator.followingSibling)) { - result.addFirst([ - [component2] - ]); - components1.add(component1); - } else if (combinator2.value == Combinator.child && - (combinator1.value == Combinator.nextSibling || - combinator1.value == Combinator.followingSibling)) { + + case ( + Combinator.child, + Combinator.nextSibling || Combinator.followingSibling, + _, + var siblingComponents + ): + case ( + Combinator.nextSibling || Combinator.followingSibling, + Combinator.child, + var siblingComponents, + _ + ): result.addFirst([ - [component1] + [siblingComponents.removeLast()] ]); - components2.add(component2); - } else if (combinator1 == combinator2) { - var unified = unifyCompound(component1.selector, component2.selector); + + case (var combinator1?, var combinator2?, _, _) + when combinator1 == combinator2: + var unified = unifyCompound( + components1.removeLast().selector, components2.removeLast().selector); if (unified == null) return null; result.addFirst([ [ - ComplexSelectorComponent(unified, [combinator1], span) + ComplexSelectorComponent(unified, [combinators1.first], span) ] ]); - } else { - return null; - } - return _mergeTrailingCombinators(components1, components2, span, result); - } else if (combinator1 != null) { - if (combinator1.value == Combinator.child && - components2.isNotEmpty && - components2.last.selector.isSuperselector(components1.last.selector)) { - components2.removeLast(); - } - result.addFirst([ - [components1.removeLast()] - ]); - return _mergeTrailingCombinators(components1, components2, span, result); - } else { - if (combinator2?.value == Combinator.child && - components1.isNotEmpty && - components1.last.selector.isSuperselector(components2.last.selector)) { - components1.removeLast(); - } - result.addFirst([ - [components2.removeLast()] - ]); - return _mergeTrailingCombinators(components1, components2, span, result); + case ( + var combinator?, + null, + var combinatorComponents, + var descendantComponents + ): + case ( + null, + var combinator?, + var descendantComponents, + var combinatorComponents + ): + if (combinator == Combinator.child && + (descendantComponents.lastOrNull?.selector + .isSuperselector(combinatorComponents.last.selector) ?? + false)) { + descendantComponents.removeLast(); + } + result.addFirst([ + [combinatorComponents.removeLast()] + ]); + + case _: + return null; } + + return _mergeTrailingCombinators(components1, components2, span, result); } /// Returns whether [complex1] and [complex2] need to be unified to produce a @@ -542,13 +544,14 @@ List> _chunks( chunk2.add(queue2.removeFirst()); } - if (chunk1.isEmpty && chunk2.isEmpty) return []; - if (chunk1.isEmpty) return [chunk2]; - if (chunk2.isEmpty) return [chunk1]; - return [ - [...chunk1, ...chunk2], - [...chunk2, ...chunk1] - ]; + return switch ((chunk1, chunk2)) { + ([], []) => [], + ([], var chunk) || (var chunk, []) => [chunk], + _ => [ + [...chunk1, ...chunk2], + [...chunk2, ...chunk1] + ] + }; } /// Returns a list of all possible paths through the given lists. @@ -757,26 +760,27 @@ bool compoundIsSuperselector( // // In addition, order matters when pseudo-elements are involved. The selectors // before them must - var tuple1 = _findPseudoElementIndexed(compound1); - var tuple2 = _findPseudoElementIndexed(compound2); - if (tuple1 != null && tuple2 != null) { - return tuple1.item1.isSuperselector(tuple2.item1) && - _compoundComponentsIsSuperselector( - compound1.components.take(tuple1.item2), - compound2.components.take(tuple2.item2), - parents: parents) && - _compoundComponentsIsSuperselector( - compound1.components.skip(tuple1.item2 + 1), - compound2.components.skip(tuple2.item2 + 1), - parents: parents); - } else if (tuple1 != null || tuple2 != null) { - return false; + switch (( + _findPseudoElementIndexed(compound1), + _findPseudoElementIndexed(compound2) + )) { + case ((var pseudo1, var index1), (var pseudo2, var index2)): + return pseudo1.isSuperselector(pseudo2) && + _compoundComponentsIsSuperselector(compound1.components.take(index1), + compound2.components.take(index2), parents: parents) && + _compoundComponentsIsSuperselector( + compound1.components.skip(index1 + 1), + compound2.components.skip(index2 + 1), + parents: parents); + + case (_?, _) || (_, _?): + return false; } // Every selector in [compound1.components] must have a matching selector in // [compound2.components]. for (var simple1 in compound1.components) { - if (simple1 is PseudoSelector && simple1.selector != null) { + if (simple1 case PseudoSelector(selector: _?)) { if (!_selectorPseudoIsSuperselector(simple1, compound2, parents: parents)) { return false; @@ -791,11 +795,10 @@ bool compoundIsSuperselector( /// If [compound] contains a pseudo-element, returns it and its index in /// [compound.components]. -Tuple2? _findPseudoElementIndexed( - CompoundSelector compound) { +(PseudoSelector, int)? _findPseudoElementIndexed(CompoundSelector compound) { for (var i = 0; i < compound.components.length; i++) { var simple = compound.components[i]; - if (simple is PseudoSelector && simple.isElement) return Tuple2(simple, i); + if (simple case PseudoSelector(isElement: true)) return (simple, i); } return null; } @@ -830,11 +833,10 @@ bool _compoundComponentsIsSuperselector( bool _selectorPseudoIsSuperselector( PseudoSelector pseudo1, CompoundSelector compound2, {Iterable? parents}) { - var selector1_ = pseudo1.selector; - if (selector1_ == null) { + var selector1 = pseudo1.selector; + if (selector1 == null) { throw ArgumentError("Selector $pseudo1 must have a selector argument."); } - var selector1 = selector1_; // dart-lang/sdk#45348 switch (pseudo1.normalizedName) { case 'is': @@ -865,22 +867,16 @@ bool _selectorPseudoIsSuperselector( return selector1.components.every((complex) { if (complex.isBogus) return false; - return compound2.components.any((simple2) { - if (simple2 is TypeSelector) { - return complex.components.last.selector.components.any( - (simple1) => simple1 is TypeSelector && simple1 != simple2); - } else if (simple2 is IDSelector) { - return complex.components.last.selector.components - .any((simple1) => simple1 is IDSelector && simple1 != simple2); - } else if (simple2 is PseudoSelector && - simple2.name == pseudo1.name) { - var selector2 = simple2.selector; - if (selector2 == null) return false; - return listIsSuperselector(selector2.components, [complex]); - } else { - return false; - } - }); + return compound2.components.any((simple2) => switch (simple2) { + TypeSelector() => complex.components.last.selector.components.any( + (simple1) => simple1 is TypeSelector && simple1 != simple2), + IDSelector() => complex.components.last.selector.components.any( + (simple1) => simple1 is IDSelector && simple1 != simple2), + PseudoSelector(selector: var selector2?) + when simple2.name == pseudo1.name => + listIsSuperselector(selector2.components, [complex]), + _ => false + }); }); case 'current': diff --git a/lib/src/extend/merged_extension.dart b/lib/src/extend/merged_extension.dart index a0caec7fb..b089379b1 100644 --- a/lib/src/extend/merged_extension.dart +++ b/lib/src/extend/merged_extension.dart @@ -11,7 +11,7 @@ import 'extension.dart'; /// /// This is used when multiple mandatory extensions exist to ensure that both of /// them are marked as resolved. -class MergedExtension extends Extension { +final class MergedExtension extends Extension { /// One of the merged extensions. final Extension left; diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index 3b9e0b294..14b82b2ea 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -607,18 +607,9 @@ Value _rgb(String name, List arguments) { Value _rgbTwoArg(String name, List arguments) { // rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789` // and functions are parsed after variable substitution. - if (arguments[0].isVar) { + if (arguments[0].isVar || + (arguments[0] is! SassColor && arguments[1].isVar)) { return _functionString(name, arguments); - } else if (arguments[1].isVar) { - var first = arguments[0]; - if (first is SassColor) { - return SassString( - "$name(${first.red}, ${first.green}, ${first.blue}, " - "${arguments[1].toCssString()})", - quotes: false); - } else { - return _functionString(name, arguments); - } } else if (arguments[1].isSpecialNumber) { var color = arguments[0].assertColor("color"); return SassString( @@ -757,18 +748,17 @@ Object /* SassString | List */ _parseChannels( if (alphaFromSlashList != null) return [...list, alphaFromSlashList]; - var maybeSlashSeparated = list[2]; - if (maybeSlashSeparated is SassNumber) { - var slash = maybeSlashSeparated.asSlash; - if (slash == null) return list; - return [list[0], list[1], slash.item1, slash.item2]; - } else if (maybeSlashSeparated is SassString && - !maybeSlashSeparated.hasQuotes && - maybeSlashSeparated.text.contains("/")) { - return _functionString(name, [channels]); - } else { - return list; - } + return switch (list[2]) { + SassNumber(asSlash: (var channel3, var alpha)) => [ + list[0], + list[1], + channel3, + alpha + ], + SassString(hasQuotes: false, :var text) when text.contains("/") => + _functionString(name, [channels]), + _ => list + }; } /// Returns whether [value] is an unquoted string that start with `var(` and diff --git a/lib/src/functions/list.dart b/lib/src/functions/list.dart index 533be29ad..1d911a88d 100644 --- a/lib/src/functions/list.dart +++ b/lib/src/functions/list.dart @@ -38,7 +38,7 @@ final _setNth = _function("set-nth", r"$list, $n, $value", (arguments) { var value = arguments[2]; var newList = list.asList.toList(); newList[list.sassIndexToListIndex(index, "n")] = value; - return arguments[0].withListContents(newList); + return list.withListContents(newList); }); final _join = _function( @@ -48,25 +48,20 @@ final _join = _function( var separatorParam = arguments[2].assertString("separator"); var bracketedParam = arguments[3]; - ListSeparator separator; - if (separatorParam.text == "auto") { - if (list1.separator != ListSeparator.undecided) { - separator = list1.separator; - } else if (list2.separator != ListSeparator.undecided) { - separator = list2.separator; - } else { - separator = ListSeparator.space; - } - } else if (separatorParam.text == "space") { - separator = ListSeparator.space; - } else if (separatorParam.text == "comma") { - separator = ListSeparator.comma; - } else if (separatorParam.text == "slash") { - separator = ListSeparator.slash; - } else { - throw SassScriptException( - '\$separator: Must be "space", "comma", "slash", or "auto".'); - } + var separator = switch (separatorParam.text) { + "auto" => switch ((list1.separator, list2.separator)) { + (ListSeparator.undecided, ListSeparator.undecided) => + ListSeparator.space, + (ListSeparator.undecided, var separator) || + (var separator, _) => + separator + }, + "space" => ListSeparator.space, + "comma" => ListSeparator.comma, + "slash" => ListSeparator.slash, + _ => throw SassScriptException( + '\$separator: Must be "space", "comma", "slash", or "auto".') + }; var bracketed = bracketedParam is SassString && bracketedParam.text == 'auto' ? list1.hasBrackets @@ -82,21 +77,16 @@ final _append = var value = arguments[1]; var separatorParam = arguments[2].assertString("separator"); - ListSeparator separator; - if (separatorParam.text == "auto") { - separator = list.separator == ListSeparator.undecided + var separator = switch (separatorParam.text) { + "auto" => list.separator == ListSeparator.undecided ? ListSeparator.space - : list.separator; - } else if (separatorParam.text == "space") { - separator = ListSeparator.space; - } else if (separatorParam.text == "comma") { - separator = ListSeparator.comma; - } else if (separatorParam.text == "slash") { - separator = ListSeparator.slash; - } else { - throw SassScriptException( - '\$separator: Must be "space", "comma", "slash", or "auto".'); - } + : list.separator, + "space" => ListSeparator.space, + "comma" => ListSeparator.comma, + "slash" => ListSeparator.slash, + _ => throw SassScriptException( + '\$separator: Must be "space", "comma", "slash", or "auto".') + }; var newList = [...list.asList, value]; return list.withListContents(newList, separator: separator); @@ -125,16 +115,14 @@ final _index = _function("index", r"$list, $value", (arguments) { return index == -1 ? sassNull : SassNumber(index + 1); }); -final _separator = _function("separator", r"$list", (arguments) { - switch (arguments[0].separator) { - case ListSeparator.comma: - return SassString("comma", quotes: false); - case ListSeparator.slash: - return SassString("slash", quotes: false); - default: - return SassString("space", quotes: false); - } -}); +final _separator = _function( + "separator", + r"$list", + (arguments) => switch (arguments[0].separator) { + ListSeparator.comma => SassString("comma", quotes: false), + ListSeparator.slash => SassString("slash", quotes: false), + _ => SassString("space", quotes: false) + }); final _isBracketed = _function("is-bracketed", r"$list", (arguments) => SassBoolean(arguments[0].hasBrackets)); diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index 232e70997..97628c1fd 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -9,7 +9,8 @@ import 'package:collection/collection.dart'; import '../callable.dart'; import '../exception.dart'; import '../module/built_in.dart'; -import '../utils.dart'; +import '../util/iterable.dart'; +import '../util/map.dart'; import '../value.dart'; /// The global definitions of Sass map functions. @@ -40,11 +41,8 @@ final _get = _function("get", r"$map, $key, $keys...", (arguments) { var keys = [arguments[1], ...arguments[2].asList]; for (var key in keys.exceptLast) { var value = map.contents[key]; - if (value is SassMap) { - map = value; - } else { - return sassNull; - } + if (value is! SassMap) return sassNull; + map = value; } return map.contents[keys.last] ?? sassNull; }); @@ -56,13 +54,16 @@ final _set = BuiltInCallable.overloadedFunction("set", { }, r"$map, $args...": (arguments) { var map = arguments[0].assertMap("map"); - var args = arguments[1].asList; - if (args.isEmpty) { - throw SassScriptException("Expected \$args to contain a key."); - } else if (args.length == 1) { - throw SassScriptException("Expected \$args to contain a value."); + switch (arguments[1].asList) { + case []: + throw SassScriptException("Expected \$args to contain a key."); + case [_]: + throw SassScriptException("Expected \$args to contain a value."); + case [...var keys, var value]: + return _modify(map, keys, (_) => value); + default: + throw '[BUG] Unreachable code'; } - return _modify(map, args.sublist(0, args.length - 1), (_) => args.last); }, }); @@ -74,18 +75,21 @@ final _merge = BuiltInCallable.overloadedFunction("merge", { }, r"$map1, $args...": (arguments) { var map1 = arguments[0].assertMap("map1"); - var args = arguments[1].asList; - if (args.isEmpty) { - throw SassScriptException("Expected \$args to contain a key."); - } else if (args.length == 1) { - throw SassScriptException("Expected \$args to contain a map."); + switch (arguments[1].asList) { + case []: + throw SassScriptException("Expected \$args to contain a key."); + case [_]: + throw SassScriptException("Expected \$args to contain a map."); + case [...var keys, var last]: + var map2 = last.assertMap("map2"); + return _modify(map1, keys, (oldValue) { + var nestedMap = oldValue.tryMap(); + if (nestedMap == null) return map2; + return SassMap({...nestedMap.contents, ...map2.contents}); + }); + default: + throw '[BUG] Unreachable code'; } - var map2 = args.last.assertMap("map2"); - return _modify(map1, args.exceptLast, (oldValue) { - var nestedMap = oldValue.tryMap(); - if (nestedMap == null) return map2; - return SassMap({...nestedMap.contents, ...map2.contents}); - }); }, }); @@ -100,8 +104,8 @@ final _deepRemove = var map = arguments[0].assertMap("map"); var keys = [arguments[1], ...arguments[2].asList]; return _modify(map, keys.exceptLast, (value) { - var nestedMap = value.tryMap(); - if (nestedMap != null && nestedMap.contents.containsKey(keys.last)) { + if (value.tryMap() case var nestedMap? + when nestedMap.contents.containsKey(keys.last)) { return SassMap(Map.of(nestedMap.contents)..remove(keys.last)); } return value; @@ -144,11 +148,8 @@ final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) { var keys = [arguments[1], ...arguments[2].asList]; for (var key in keys.exceptLast) { var value = map.contents[key]; - if (value is SassMap) { - map = value; - } else { - return sassFalse; - } + if (value is! SassMap) return sassFalse; + map = value; } return SassBoolean(map.contents.containsKey(keys.last)); }); @@ -198,22 +199,16 @@ SassMap _deepMergeImpl(SassMap map1, SassMap map2) { if (map2.contents.isEmpty) return map1; var result = Map.of(map1.contents); - - map2.contents.forEach((key, value) { - var resultMap = result[key]?.tryMap(); - if (resultMap == null) { - result[key] = value; + for (var (key, value) in map2.contents.pairs) { + if ((result[key]?.tryMap(), value.tryMap()) + case (var resultMap?, var valueMap?)) { + var merged = _deepMergeImpl(resultMap, valueMap); + if (identical(merged, resultMap)) continue; + result[key] = merged; } else { - var valueMap = value.tryMap(); - if (valueMap != null) { - var merged = _deepMergeImpl(resultMap, valueMap); - if (identical(merged, resultMap)) return; - result[key] = merged; - } else { - result[key] = value; - } + result[key] = value; } - }); + } return SassMap(result); } diff --git a/lib/src/functions/meta.dart b/lib/src/functions/meta.dart index 4ed71f8bf..41537d55d 100644 --- a/lib/src/functions/meta.dart +++ b/lib/src/functions/meta.dart @@ -7,6 +7,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import '../callable.dart'; +import '../util/map.dart'; import '../value.dart'; /// Feature names supported by Dart sass. @@ -31,34 +32,33 @@ final global = UnmodifiableListView([ _function("inspect", r"$value", (arguments) => SassString(arguments.first.toString(), quotes: false)), - _function("type-of", r"$value", (arguments) { - var value = arguments[0]; - if (value is SassArgumentList) { - return SassString("arglist", quotes: false); - } - if (value is SassBoolean) return SassString("bool", quotes: false); - if (value is SassColor) return SassString("color", quotes: false); - if (value is SassList) return SassString("list", quotes: false); - if (value is SassMap) return SassString("map", quotes: false); - if (value == sassNull) return SassString("null", quotes: false); - if (value is SassNumber) return SassString("number", quotes: false); - if (value is SassFunction) return SassString("function", quotes: false); - if (value is SassCalculation) { - return SassString("calculation", quotes: false); - } - assert(value is SassString); - return SassString("string", quotes: false); - }), + _function( + "type-of", + r"$value", + (arguments) => SassString( + switch (arguments[0]) { + SassArgumentList() => "arglist", + SassBoolean() => "bool", + SassColor() => "color", + SassList() => "list", + SassMap() => "map", + sassNull => "null", + SassNumber() => "number", + SassFunction() => "function", + SassCalculation() => "calculation", + SassString() => "string", + _ => throw "[BUG] Unknown value type ${arguments[0]}" + }, + quotes: false)), _function("keywords", r"$args", (arguments) { - var argumentList = arguments[0]; - if (argumentList is SassArgumentList) { + if (arguments[0] case SassArgumentList(:var keywords)) { return SassMap({ - for (var entry in argumentList.keywords.entries) - SassString(entry.key, quotes: false): entry.value + for (var (key, value) in keywords.pairs) + SassString(key, quotes: false): value }); } else { - throw "\$args: $argumentList is not an argument list."; + throw "\$args: ${arguments[0]} is not an argument list."; } }) ]); @@ -72,10 +72,11 @@ final local = UnmodifiableListView([ }), _function("calc-args", r"$calc", (arguments) { var calculation = arguments[0].assertCalculation("calc"); - return SassList(calculation.arguments.map((argument) { - if (argument is Value) return argument; - return SassString(argument.toString(), quotes: false); - }), ListSeparator.comma); + return SassList( + calculation.arguments.map((argument) => argument is Value + ? argument + : SassString(argument.toString(), quotes: false)), + ListSeparator.comma); }) ]); diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart index 282874dac..d6fddf9de 100644 --- a/lib/src/functions/selector.dart +++ b/lib/src/functions/selector.dart @@ -72,7 +72,7 @@ final _append = _function("append", r"$selectors...", (arguments) { throw SassScriptException("Can't append $complex to $parent."); } - var component = complex.components.first; + var [component, ...rest] = complex.components; var newCompound = _prependParent(component.selector); if (newCompound == null) { throw SassScriptException("Can't append $complex to $parent."); @@ -80,7 +80,7 @@ final _append = _function("append", r"$selectors...", (arguments) { return ComplexSelector(const [], [ ComplexSelectorComponent(newCompound, component.combinators, span), - ...complex.components.skip(1) + ...rest ], span); }), span) .resolveParentSelectors(parent); @@ -121,8 +121,7 @@ final _unify = _function("unify", r"$selector1, $selector2", (arguments) { var selector2 = arguments[1].assertSelector(name: "selector2") ..assertNotBogus(name: "selector2"); - var result = selector1.unify(selector2); - return result == null ? sassNull : result.asSassList; + return selector1.unify(selector2)?.asSassList ?? sassNull; }); final _isSuperselector = @@ -151,20 +150,15 @@ final _parse = _function("parse", r"$selector", /// Adds a [ParentSelector] to the beginning of [compound], or returns `null` if /// that wouldn't produce a valid 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(span, suffix: first.name.name), - ...compound.components.skip(1) - ], span); - } else { - return CompoundSelector( - [ParentSelector(span), ...compound.components], span); - } + return switch (compound.components) { + [UniversalSelector(), ...] => null, + [TypeSelector type, ...] when type.name.namespace != null => null, + [TypeSelector type, ...var rest] => CompoundSelector( + [ParentSelector(span, suffix: type.name.name), ...rest], span), + var components => + CompoundSelector([ParentSelector(span), ...components], span) + }; } /// Like [BuiltInCallable.function], but always sets the URL to diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 86b9198b2..b85c78d07 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: 96e085628560f348a79b8f99b96f7352f450868c +// Checksum: 1b6289e0dd362fcb02f331a16a30fe94050b4e17 // // ignore_for_file: unused_import @@ -13,7 +13,6 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:package_config/package_config_types.dart'; import 'package:path/path.dart' as p; -import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; import 'deprecation.dart'; @@ -22,13 +21,19 @@ import 'importer/no_op.dart'; import 'importer/utils.dart'; import 'io.dart'; import 'logger.dart'; +import 'util/nullable.dart'; import 'utils.dart'; +/// A canonicalized URL and the importer that canonicalized it. +/// +/// This also includes the URL that was originally passed to the importer, which +/// may be resolved relative to a base URL. +typedef CanonicalizeResult = (Importer, Uri canonicalUrl, {Uri originalUrl}); + /// An in-memory cache of parsed stylesheets that have been imported by Sass. /// /// {@category Dependencies} -@sealed -class ImportCache { +final class ImportCache { /// The importers to use when loading new Sass files. final List _importers; @@ -37,15 +42,13 @@ class ImportCache { /// The canonicalized URLs for each non-canonical URL. /// - /// The second item in each key's tuple is true when this canonicalization is - /// for an `@import` rule. Otherwise, it's for a `@use` or `@forward` rule. - /// - /// This map's values are the same as the return value of [canonicalize]. + /// The `forImport` in each key is true when this canonicalization is for an + /// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule. /// /// This cache isn't used for relative imports, because they depend on the /// specific base importer. That's stored separately in /// [_relativeCanonicalizeCache]. - final _canonicalizeCache = , Tuple3?>{}; + final _canonicalizeCache = <(Uri, {bool forImport}), CanonicalizeResult?>{}; /// The canonicalized URLs for each non-canonical URL that's resolved using a /// relative importer. @@ -58,8 +61,13 @@ class ImportCache { /// 4. The `baseUrl` passed to [canonicalize]. /// /// The map's values are the same as the return value of [canonicalize]. - final _relativeCanonicalizeCache = - , Tuple3?>{}; + final _relativeCanonicalizeCache = <( + Uri, { + bool forImport, + Importer baseImporter, + Uri? baseUrl + }), + CanonicalizeResult?>{}; /// The parsed stylesheets for each canonicalized import URL. final _importCache = {}; @@ -126,7 +134,7 @@ class ImportCache { /// If any importers understand [url], returns that importer as well as the /// canonicalized URL and the original URL (resolved relative to [baseUrl] if /// applicable). Otherwise, returns `null`. - Tuple3? canonicalize(Uri url, + CanonicalizeResult? canonicalize(Uri url, {Importer? baseImporter, Uri? baseUrl, bool forImport = false}) { if (isBrowser && (baseImporter == null || baseImporter is NoOpImporter) && @@ -135,21 +143,27 @@ class ImportCache { } if (baseImporter != null) { - var relativeResult = _relativeCanonicalizeCache - .putIfAbsent(Tuple4(url, forImport, baseImporter, baseUrl), () { + var relativeResult = _relativeCanonicalizeCache.putIfAbsent(( + url, + forImport: forImport, + baseImporter: baseImporter, + baseUrl: baseUrl + ), () { var resolvedUrl = baseUrl?.resolveUri(url) ?? url; - var canonicalUrl = _canonicalize(baseImporter, resolvedUrl, forImport); - if (canonicalUrl == null) return null; - return Tuple3(baseImporter, canonicalUrl, resolvedUrl); + if (_canonicalize(baseImporter, resolvedUrl, forImport) + case var canonicalUrl?) { + return (baseImporter, canonicalUrl, originalUrl: resolvedUrl); + } else { + return null; + } }); if (relativeResult != null) return relativeResult; } - return _canonicalizeCache.putIfAbsent(Tuple2(url, forImport), () { + return _canonicalizeCache.putIfAbsent((url, forImport: forImport), () { for (var importer in _importers) { - var canonicalUrl = _canonicalize(importer, url, forImport); - if (canonicalUrl != null) { - return Tuple3(importer, canonicalUrl, url); + if (_canonicalize(importer, url, forImport) case var canonicalUrl?) { + return (importer, canonicalUrl, originalUrl: url); } } @@ -181,15 +195,16 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// parsed stylesheet. Otherwise, returns `null`. /// /// Caches the result of the import and uses cached results if possible. - Tuple2? import(Uri url, + (Importer, Stylesheet)? import(Uri url, {Importer? baseImporter, Uri? baseUrl, bool forImport = false}) { - var tuple = canonicalize(url, - baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport); - if (tuple == null) return null; - var stylesheet = - importCanonical(tuple.item1, tuple.item2, originalUrl: tuple.item3); - if (stylesheet == null) return null; - return Tuple2(tuple.item1, stylesheet); + if (canonicalize(url, + baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport) + case (var importer, var canonicalUrl, :var originalUrl)) { + return importCanonical(importer, canonicalUrl, originalUrl: originalUrl) + .andThen((stylesheet) => (importer, stylesheet)); + } else { + return null; + } } /// Tries to load the canonicalized [canonicalUrl] using [importer]. @@ -225,21 +240,22 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// Return a human-friendly URL for [canonicalUrl] to use in a stack trace. /// /// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache. - Uri humanize(Uri canonicalUrl) { - // Display the URL with the shortest path length. - var url = minBy( - _canonicalizeCache.values - .whereNotNull() - .where((tuple) => tuple.item2 == canonicalUrl) - .map((tuple) => tuple.item3), - (url) => url.path.length); - if (url == null) return canonicalUrl; - - // Use the canonicalized basename so that we display e.g. - // package:example/_example.scss rather than package:example/example in - // stack traces. - return url.resolve(p.url.basename(canonicalUrl.path)); - } + Uri humanize(Uri canonicalUrl) => + // If multiple original URLs canonicalize to the same thing, choose the + // shortest one. + minBy( + _canonicalizeCache.values + .whereNotNull() + .where((result) => result.$2 == canonicalUrl) + .map((result) => result.originalUrl), + (url) => url.path.length) + // Use the canonicalized basename so that we display e.g. + // package:example/_example.scss rather than package:example/example + // in stack traces. + .andThen((url) => url.resolve(p.url.basename(canonicalUrl.path))) ?? + // If we don't have an original URL cached, display the canonical URL + // as-is. + canonicalUrl; /// Returns the URL to use in the source map to refer to [canonicalUrl]. /// @@ -254,16 +270,9 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// @nodoc @internal void clearCanonicalize(Uri url) { - _canonicalizeCache.remove(Tuple2(url, false)); - _canonicalizeCache.remove(Tuple2(url, true)); - - var relativeKeysToClear = [ - for (var key in _relativeCanonicalizeCache.keys) - if (key.item1 == url) key - ]; - for (var key in relativeKeysToClear) { - _relativeCanonicalizeCache.remove(key); - } + _canonicalizeCache.remove((url, forImport: false)); + _canonicalizeCache.remove((url, forImport: true)); + _relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url); } /// Clears the cached parse tree for the stylesheet with the given diff --git a/lib/src/importer/js_to_dart/async.dart b/lib/src/importer/js_to_dart/async.dart index d11552885..22eb763a5 100644 --- a/lib/src/importer/js_to_dart/async.dart +++ b/lib/src/importer/js_to_dart/async.dart @@ -16,7 +16,7 @@ import '../result.dart'; /// A wrapper for a potentially-asynchronous JS API importer that exposes it as /// a Dart [AsyncImporter]. -class JSToDartAsyncImporter extends AsyncImporter { +final class JSToDartAsyncImporter extends AsyncImporter { /// The wrapped canonicalize function. final Object? Function(String, CanonicalizeOptions) _canonicalize; diff --git a/lib/src/importer/js_to_dart/async_file.dart b/lib/src/importer/js_to_dart/async_file.dart index f7dadee18..b54db415d 100644 --- a/lib/src/importer/js_to_dart/async_file.dart +++ b/lib/src/importer/js_to_dart/async_file.dart @@ -23,7 +23,7 @@ final _filesystemImporter = FilesystemImporter('.'); /// A wrapper for a potentially-asynchronous JS API file importer that exposes /// it as a Dart [AsyncImporter]. -class JSToDartAsyncFileImporter extends AsyncImporter { +final class JSToDartAsyncFileImporter extends AsyncImporter { /// The wrapped `findFileUrl` function. final Object? Function(String, CanonicalizeOptions) _findFileUrl; diff --git a/lib/src/importer/js_to_dart/file.dart b/lib/src/importer/js_to_dart/file.dart index 78b65395f..346a43d8c 100644 --- a/lib/src/importer/js_to_dart/file.dart +++ b/lib/src/importer/js_to_dart/file.dart @@ -18,7 +18,7 @@ final _filesystemImporter = FilesystemImporter('.'); /// A wrapper for a potentially-asynchronous JS API file importer that exposes /// it as a Dart [AsyncImporter]. -class JSToDartFileImporter extends Importer { +final class JSToDartFileImporter extends Importer { /// The wrapped `findFileUrl` function. final Object? Function(String, CanonicalizeOptions) _findFileUrl; diff --git a/lib/src/importer/js_to_dart/sync.dart b/lib/src/importer/js_to_dart/sync.dart index 7f2339d6e..4608db75a 100644 --- a/lib/src/importer/js_to_dart/sync.dart +++ b/lib/src/importer/js_to_dart/sync.dart @@ -12,7 +12,7 @@ import '../../util/nullable.dart'; /// A wrapper for a synchronous JS API importer that exposes it as a Dart /// [Importer]. -class JSToDartImporter extends Importer { +final class JSToDartImporter extends Importer { /// The wrapped canonicalize function. final Object? Function(String, CanonicalizeOptions) _canonicalize; diff --git a/lib/src/importer/legacy_node/implementation.dart b/lib/src/importer/legacy_node/implementation.dart index 3175648b7..a51d16c07 100644 --- a/lib/src/importer/legacy_node/implementation.dart +++ b/lib/src/importer/legacy_node/implementation.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:js/js.dart'; import 'package:path/path.dart' as p; -import 'package:tuple/tuple.dart'; import '../../io.dart'; import '../../js/function.dart'; @@ -39,7 +38,7 @@ import '../utils.dart'; /// 3. Filesystem imports relative to the working directory. /// 4. Filesystem imports relative to an `includePaths` path. /// 5. Filesystem imports relative to a `SASS_PATH` path. -class NodeImporter { +final class NodeImporter { /// The options for the `this` context in which importer functions are /// invoked. /// @@ -74,7 +73,7 @@ class NodeImporter { /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2? loadRelative( + (String contents, String url)? loadRelative( String url, Uri? previous, bool forImport) { if (p.url.isAbsolute(url)) { if (!url.startsWith('/') && !url.startsWith('file:')) return null; @@ -93,13 +92,13 @@ class NodeImporter { /// The [previous] URL is the URL of the stylesheet in which the import /// appeared. Returns the contents of the stylesheet and the URL to use as /// [previous] for imports within the loaded stylesheet. - Tuple2? load(String url, Uri? previous, bool forImport) { + (String contents, String url)? load( + String url, Uri? previous, bool forImport) { // The previous URL is always an absolute file path for filesystem imports. var previousString = _previousToString(previous); for (var importer in _importers) { - var value = - call2(importer, _renderContext(forImport), url, previousString); - if (value != null) { + if (call2(importer, _renderContext(forImport), url, previousString) + case var value?) { return _handleImportResult(url, previous, value, forImport); } } @@ -113,14 +112,13 @@ class NodeImporter { /// The [previous] URL is the URL of the stylesheet in which the import /// appeared. Returns the contents of the stylesheet and the URL to use as /// [previous] for imports within the loaded stylesheet. - Future?> loadAsync( + Future<(String contents, String url)?> loadAsync( String url, Uri? previous, bool forImport) async { // The previous URL is always an absolute file path for filesystem imports. var previousString = _previousToString(previous); for (var importer in _importers) { - var value = - await _callImporterAsync(importer, url, previousString, forImport); - if (value != null) { + if (await _callImporterAsync(importer, url, previousString, forImport) + case var value?) { return _handleImportResult(url, previous, value, forImport); } } @@ -129,18 +127,18 @@ class NodeImporter { } /// Converts [previous] to a string to pass to the importer function. - String _previousToString(Uri? previous) { - if (previous == null) return 'stdin'; - if (previous.scheme == 'file') return p.fromUri(previous); - return previous.toString(); - } + String _previousToString(Uri? previous) => switch (previous) { + null => 'stdin', + Uri(scheme: 'file') => p.fromUri(previous), + _ => previous.toString() + }; /// Tries to load a stylesheet at the given [url] from a load path (including /// the working directory), if that URL refers to the filesystem. /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2? _resolveLoadPathFromUrl(Uri url, bool forImport) => + (String, String)? _resolveLoadPathFromUrl(Uri url, bool forImport) => url.scheme == '' || url.scheme == 'file' ? _resolveLoadPath(p.fromUri(url), forImport) : null; @@ -150,15 +148,16 @@ class NodeImporter { /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2? _resolveLoadPath(String path, bool forImport) { + (String, String)? _resolveLoadPath(String path, bool forImport) { // 2: Filesystem imports relative to the working directory. - var cwdResult = _tryPath(p.absolute(path), forImport); - if (cwdResult != null) return cwdResult; + if (_tryPath(p.absolute(path), forImport) case var result?) return result; // 3: Filesystem imports relative to [_includePaths]. for (var includePath in _includePaths) { - var result = _tryPath(p.absolute(p.join(includePath, path)), forImport); - if (result != null) return result; + if (_tryPath(p.absolute(p.join(includePath, path)), forImport) + case var result?) { + return result; + } } return null; @@ -168,15 +167,15 @@ class NodeImporter { /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2? _tryPath(String path, bool forImport) => (forImport + (String, String)? _tryPath(String path, bool forImport) => (forImport ? inImportRule(() => resolveImportPath(path)) : resolveImportPath(path)) - .andThen((resolved) => - Tuple2(readFile(resolved), p.toUri(resolved).toString())); + .andThen( + (resolved) => (readFile(resolved), p.toUri(resolved).toString())); - /// Converts an importer's return [value] to a tuple that can be returned by - /// [load]. - Tuple2? _handleImportResult( + /// Converts an importer's return [value] to a (contents, url) pair that can + /// be returned by [load]. + (String, String)? _handleImportResult( String url, Uri? previous, Object value, bool forImport) { if (isJSError(value)) throw value; if (value is! NodeImporterResult) return null; @@ -189,9 +188,9 @@ class NodeImporter { } if (file == null) { - return Tuple2(contents ?? '', url); + return (contents ?? '', url); } else if (contents != null) { - return Tuple2(contents, p.toUri(file).toString()); + return (contents, p.toUri(file).toString()); } else { var resolved = loadRelative(p.toUri(file).toString(), previous, forImport) ?? diff --git a/lib/src/importer/legacy_node/interface.dart b/lib/src/importer/legacy_node/interface.dart index 5cef8cd30..0b6f5cb29 100644 --- a/lib/src/importer/legacy_node/interface.dart +++ b/lib/src/importer/legacy_node/interface.dart @@ -2,20 +2,19 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:tuple/tuple.dart'; - -class NodeImporter { +final class NodeImporter { NodeImporter(Object options, Iterable includePaths, Iterable importers); - Tuple2? loadRelative( + (String contents, String url)? loadRelative( String url, Uri? previous, bool forImport) => throw ''; - Tuple2? load(String url, Uri? previous, bool forImport) => + (String contents, String url)? load( + String url, Uri? previous, bool forImport) => throw ''; - Future?> loadAsync( + Future<(String contents, String url)?> loadAsync( String url, Uri? previous, bool forImport) => throw ''; } diff --git a/lib/src/importer/no_op.dart b/lib/src/importer/no_op.dart index e6261499d..e0c8b61cb 100644 --- a/lib/src/importer/no_op.dart +++ b/lib/src/importer/no_op.dart @@ -8,7 +8,7 @@ import '../importer.dart'; /// /// This is used for stylesheets which don't support relative imports, such as /// those created from Dart code with plain strings. -class NoOpImporter extends Importer { +final class NoOpImporter extends Importer { Uri? canonicalize(Uri url) => null; ImporterResult? load(Uri url) => null; bool couldCanonicalize(Uri url, Uri canonicalUrl) => false; diff --git a/lib/src/importer/utils.dart b/lib/src/importer/utils.dart index f9459bfd2..464a050f8 100644 --- a/lib/src/importer/utils.dart +++ b/lib/src/importer/utils.dart @@ -71,13 +71,12 @@ String? _tryPathAsDirectory(String path) { /// /// If it contains no paths, returns `null`. If it contains more than one, /// throws an exception. -String? _exactlyOne(List paths) { - if (paths.isEmpty) return null; - if (paths.length == 1) return paths.first; - - throw "It's not clear which file to import. Found:\n" + - paths.map((path) => " " + p.prettyUri(p.toUri(path))).join("\n"); -} +String? _exactlyOne(List paths) => switch (paths) { + [] => null, + [var path] => path, + _ => throw "It's not clear which file to import. Found:\n" + + paths.map((path) => " " + p.prettyUri(p.toUri(path))).join("\n") + }; /// If [fromImport] is `true`, invokes callback and returns the result. /// diff --git a/lib/src/interpolation_buffer.dart b/lib/src/interpolation_buffer.dart index 68cd37701..014c31fec 100644 --- a/lib/src/interpolation_buffer.dart +++ b/lib/src/interpolation_buffer.dart @@ -11,7 +11,7 @@ import 'ast/sass.dart'; /// /// Add text using [write] and related methods, and [Expression]s using [add]. /// Once that's done, call [interpolation] to build the result. -class InterpolationBuffer implements StringSink { +final class InterpolationBuffer implements StringSink { /// The buffer that accumulates plain text. final _text = StringBuffer(); @@ -49,10 +49,9 @@ class InterpolationBuffer implements StringSink { if (interpolation.contents.isEmpty) return; Iterable toAdd = interpolation.contents; - var first = interpolation.contents.first; - if (first is String) { + if (interpolation.contents case [String first, ...var rest]) { _text.write(first); - toAdd = interpolation.contents.skip(1); + toAdd = rest; } _flushText(); diff --git a/lib/src/interpolation_map.dart b/lib/src/interpolation_map.dart index 8cb81e09b..9f6788f53 100644 --- a/lib/src/interpolation_map.dart +++ b/lib/src/interpolation_map.dart @@ -10,9 +10,9 @@ import 'package:source_span/source_span.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 { +/// A map from locations in a string generated from an [Interpolation] to the +/// original source code in the interpolation. +final class InterpolationMap { /// The interpolation from which this map was generated. final Interpolation _interpolation; @@ -63,24 +63,17 @@ class InterpolationMap { /// 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); - } - } + FileSpan mapSpan(SourceSpan target) => + switch ((_mapLocation(target.start), _mapLocation(target.end))) { + (FileSpan start, FileSpan end) => start.expand(end), + (FileSpan start, FileLocation end) => _interpolation.span.file + .span(_expandInterpolationSpanLeft(start.start), end.offset), + (FileLocation start, FileSpan end) => _interpolation.span.file + .span(start.offset, _expandInterpolationSpanRight(end.end)), + (FileLocation start, FileLocation end) => + _interpolation.span.file.span(start.offset, end.offset), + _ => throw '[BUG] Unreachable' + }; /// Maps a location in the string generated from this interpolation to its /// original source. @@ -91,8 +84,9 @@ class InterpolationMap { /// 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; + if (_interpolation.contents[index] case Expression chunk) { + return chunk.span; + } var previousLocation = index == 0 ? _interpolation.span.start @@ -160,7 +154,7 @@ class InterpolationMap { if (next == $slash) { var second = source[i++]; if (second == $slash) { - while (!isNewline(source[i++])) {} + while (!source[i++].isNewline) {} } else if (second == $asterisk) { while (true) { var char = source[i++]; diff --git a/lib/src/io.dart b/lib/src/io.dart index f8e299e86..029f45aec 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart @@ -42,7 +42,7 @@ String _realCasePath(String path) { if (isWindows) { // Drive names are *always* case-insensitive, so convert them to uppercase. var prefix = p.rootPrefix(path); - if (prefix.isNotEmpty && isAlphabetic(prefix.codeUnitAt(0))) { + if (prefix.isNotEmpty && prefix.codeUnitAt(0).isAlphabetic) { path = prefix.toUpperCase() + path.substring(prefix.length); } } @@ -61,12 +61,13 @@ String _realCasePath(String path) { (realPath) => equalsIgnoreCase(p.basename(realPath), basename)) .toList(); - return matches.length != 1 - // If the file doesn't exist, or if there are multiple options (meaning - // the filesystem isn't actually case-insensitive), use `basename` - // as-is. - ? p.join(realDirname, basename) - : matches[0]; + return switch (matches) { + [var match] => match, + // If the file doesn't exist, or if there are multiple options + // (meaning the filesystem isn't actually case-insensitive), use + // `basename` as-is. + _ => p.join(realDirname, basename) + }; } on FileSystemException catch (_) { // If there's an error listing a directory, it's likely because we're // trying to reach too far out of the current directory into something diff --git a/lib/src/io/js.dart b/lib/src/io/js.dart index 5fd3db3e3..043567ad6 100644 --- a/lib/src/io/js.dart +++ b/lib/src/io/js.dart @@ -29,9 +29,8 @@ class FileSystemException { } void printError(Object? message) { - var process_ = process; - if (process_ != null) { - process_.stderr.write("${message ?? ''}\n"); + if (process case var process?) { + process.stderr.write("${message ?? ''}\n"); } else { console.error(message ?? ''); } diff --git a/lib/src/io/vm.dart b/lib/src/io/vm.dart index adffcbb32..14afd60e6 100644 --- a/lib/src/io/vm.dart +++ b/lib/src/io/vm.dart @@ -57,6 +57,7 @@ String readFile(String path) { throwWithTrace( SassException( "Invalid UTF-8.", sourceFile.location(stringOffset).pointSpan()), + error, stackTrace); } } diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index fb863161d..3858c733f 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -172,11 +172,11 @@ Promise _wrapAsyncSassExceptions(Promise promise, : jsThrow(error as Object))); /// Converts an output style string to an instance of [OutputStyle]. -OutputStyle _parseOutputStyle(String? style) { - if (style == null || style == 'expanded') return OutputStyle.expanded; - if (style == 'compressed') return OutputStyle.compressed; - jsThrow(JsError('Unknown output style "$style".')); -} +OutputStyle _parseOutputStyle(String? style) => switch (style) { + null || 'expanded' => OutputStyle.expanded, + 'compressed' => OutputStyle.compressed, + _ => jsThrow(JsError('Unknown output style "$style".')) + }; /// Converts [importer] into an [AsyncImporter] that can be used with /// [compileAsync] or [compileStringAsync]. @@ -184,21 +184,22 @@ AsyncImporter _parseAsyncImporter(Object? importer) { if (importer == null) jsThrow(JsError("Importers may not be null.")); importer as NodeImporter; - var findFileUrl = importer.findFileUrl; var canonicalize = importer.canonicalize; var load = importer.load; - if (findFileUrl == null) { - if (canonicalize == null || load == null) { - jsThrow(JsError( - "An importer must have either canonicalize and load methods, or a " - "findFileUrl method.")); + if (importer.findFileUrl case var findFileUrl?) { + if (canonicalize != null || load != null) { + jsThrow( + JsError("An importer may not have a findFileUrl method as well as " + "canonicalize and load methods.")); + } else { + return JSToDartAsyncFileImporter(findFileUrl); } - return JSToDartAsyncImporter(canonicalize, load); - } else if (canonicalize != null || load != null) { - jsThrow(JsError("An importer may not have a findFileUrl method as well as " - "canonicalize and load methods.")); + } else if (canonicalize == null || load == null) { + jsThrow(JsError( + "An importer must have either canonicalize and load methods, or a " + "findFileUrl method.")); } else { - return JSToDartAsyncFileImporter(findFileUrl); + return JSToDartAsyncImporter(canonicalize, load); } } @@ -207,21 +208,22 @@ Importer _parseImporter(Object? importer) { if (importer == null) jsThrow(JsError("Importers may not be null.")); importer as NodeImporter; - var findFileUrl = importer.findFileUrl; var canonicalize = importer.canonicalize; var load = importer.load; - if (findFileUrl == null) { - if (canonicalize == null || load == null) { - jsThrow(JsError( - "An importer must have either canonicalize and load methods, or a " - "findFileUrl method.")); + if (importer.findFileUrl case var findFileUrl?) { + if (canonicalize != null || load != null) { + jsThrow( + JsError("An importer may not have a findFileUrl method as well as " + "canonicalize and load methods.")); + } else { + return JSToDartFileImporter(findFileUrl); } - return JSToDartImporter(canonicalize, load); - } else if (canonicalize != null || load != null) { - jsThrow(JsError("An importer may not have a findFileUrl method as well as " - "canonicalize and load methods.")); + } else if (canonicalize == null || load == null) { + jsThrow(JsError( + "An importer must have either canonicalize and load methods, or a " + "findFileUrl method.")); } else { - return JSToDartFileImporter(findFileUrl); + return JSToDartImporter(canonicalize, load); } } diff --git a/lib/src/js/immutable.dart b/lib/src/js/immutable.dart index 354d9c306..b9dfc6f28 100644 --- a/lib/src/js/immutable.dart +++ b/lib/src/js/immutable.dart @@ -4,6 +4,8 @@ import 'package:js/js.dart'; +import '../util/map.dart'; + @JS('immutable.List') class ImmutableList { external factory ImmutableList([List? contents]); @@ -36,8 +38,8 @@ List jsToDartList(Object? list) => /// Converts a Dart map into an equivalent [ImmutableMap]. ImmutableMap dartMapToImmutableMap(Map dartMap) { var immutableMap = ImmutableMap().asMutable(); - for (var entry in dartMap.entries) { - immutableMap = immutableMap.set(entry.key, entry.value); + for (var (key, value) in dartMap.pairs) { + immutableMap = immutableMap.set(key, value); } return immutableMap.asImmutable(); } diff --git a/lib/src/js/legacy.dart b/lib/src/js/legacy.dart index 5f948650a..2aaaf9830 100644 --- a/lib/src/js/legacy.dart +++ b/lib/src/js/legacy.dart @@ -41,8 +41,7 @@ void render( if (!isNode) { jsThrow(JsError("The render() method is only available in Node.js.")); } - var fiber = options.fiber; - if (fiber != null) { + if (options.fiber case var fiber?) { fiber.call(allowInterop(() { try { callback(null, renderSync(options)); @@ -72,9 +71,8 @@ Future _renderAsync(RenderOptions options) async { var start = DateTime.now(); CompileResult result; - var data = options.data; var file = options.file.andThen(p.absolute); - if (data != null) { + if (options.data case var data?) { result = await compileStringAsync(data, nodeImporter: _parseImporter(options, start), functions: _parseFunctions(options, start, asynch: true), @@ -126,9 +124,8 @@ RenderResult renderSync(RenderOptions options) { var start = DateTime.now(); CompileResult result; - var data = options.data; var file = options.file.andThen(p.absolute); - if (data != null) { + if (options.data case var data?) { result = compileString(data, nodeImporter: _parseImporter(options, start), functions: _parseFunctions(options, start).cast(), @@ -175,15 +172,11 @@ RenderResult renderSync(RenderOptions options) { /// Converts an exception to a [JsError]. JsError _wrapException(Object exception, StackTrace stackTrace) { if (exception is SassException) { - String file; - var url = exception.span.sourceUrl; - if (url == null) { - file = 'stdin'; - } else if (url.scheme == 'file') { - file = p.fromUri(url); - } else { - file = url.toString(); - } + var file = switch (exception.span.sourceUrl) { + null => 'stdin', + Uri(scheme: 'file') && var url => p.fromUri(url), + var url => url.toString() + }; return _newRenderError(exception.toString().replaceFirst("Error: ", ""), getTrace(exception) ?? stackTrace, @@ -213,8 +206,7 @@ List _parseFunctions(RenderOptions options, DateTime start, var context = RenderContext(options: _contextOptions(options, start)); context.options.context = context; - var fiber = options.fiber; - if (fiber != null) { + if (options.fiber case var fiber?) { result.add(Callable.fromSignature(signature.trimLeft(), (arguments) { var currentFiber = fiber.current; var jsArguments = [ @@ -259,20 +251,16 @@ List _parseFunctions(RenderOptions options, DateTime start, /// Parses [importer] and [includePaths] from [RenderOptions] into a /// [NodeImporter]. NodeImporter _parseImporter(RenderOptions options, DateTime start) { - List importers; - if (options.importer == null) { - importers = []; - } else if (options.importer is List) { - importers = (options.importer as List).cast(); - } else { - importers = [options.importer as JSFunction]; - } + var importers = switch (options.importer) { + null => [], + List importers => importers.cast(), + var importer => [importer as JSFunction], + }; var contextOptions = importers.isNotEmpty ? _contextOptions(options, start) : Object(); - var fiber = options.fiber; - if (fiber != null) { + if (options.fiber case var fiber?) { importers = importers.map((importer) { return allowInteropCaptureThis( (Object thisArg, String url, String previous, [Object? _]) { @@ -317,31 +305,26 @@ RenderContextOptions _contextOptions(RenderOptions options, DateTime start) { } /// Parse [style] into an [OutputStyle]. -OutputStyle _parseOutputStyle(String? style) { - if (style == null || style == 'expanded') return OutputStyle.expanded; - if (style == 'compressed') return OutputStyle.compressed; - throw ArgumentError('Unsupported output style "$style".'); -} +OutputStyle _parseOutputStyle(String? style) => switch (style) { + null || 'expanded' => OutputStyle.expanded, + 'compressed' => OutputStyle.compressed, + _ => jsThrow(JsError('Unknown output style "$style".')) + }; /// Parses the indentation width into an [int]. -int? _parseIndentWidth(Object? width) { - if (width == null) return null; - return width is int ? width : int.parse(width.toString()); -} +int? _parseIndentWidth(Object? width) => switch (width) { + null => null, + int() => width, + _ => int.parse(width.toString()) + }; /// Parses the name of a line feed type into a [LineFeed]. -LineFeed _parseLineFeed(String? str) { - switch (str) { - case 'cr': - return LineFeed.cr; - case 'crlf': - return LineFeed.crlf; - case 'lfcr': - return LineFeed.lfcr; - default: - return LineFeed.lf; - } -} +LineFeed _parseLineFeed(String? str) => switch (str) { + 'cr' => LineFeed.cr, + 'crlf' => LineFeed.crlf, + 'lfcr' => LineFeed.lfcr, + _ => LineFeed.lf + }; /// Creates a [RenderResult] that exposes [result] in the Node Sass API format. RenderResult _newRenderResult( @@ -363,12 +346,10 @@ RenderResult _newRenderResult( sourceMap.sourceRoot = options.sourceMapRoot; var outFile = options.outFile; if (outFile == null) { - var file = options.file; - if (file == null) { - sourceMap.targetUrl = 'stdin.css'; - } else { - sourceMap.targetUrl = p.toUri(p.setExtension(file, '.css')).toString(); - } + sourceMap.targetUrl = switch (options.file) { + var file? => p.toUri(p.setExtension(file, '.css')).toString(), + _ => sourceMap.targetUrl = 'stdin.css' + }; } else { sourceMap.targetUrl = p.toUri(p.relative(outFile, from: sourceMapDir)).toString(); @@ -412,7 +393,7 @@ RenderResult _newRenderResult( duration: end.difference(start).inMilliseconds, includedFiles: [ for (var url in result.loadedUrls) - if (url.scheme == 'file') p.fromUri(url) else url.toString() + url.scheme == 'file' ? p.fromUri(url) : url.toString() ])); } diff --git a/lib/src/js/legacy/value.dart b/lib/src/js/legacy/value.dart index 2c45f3c2c..0087aa1ae 100644 --- a/lib/src/js/legacy/value.dart +++ b/lib/src/js/legacy/value.dart @@ -39,11 +39,11 @@ Value unwrapValue(Object? object) { } /// Wraps a [Value] in a wrapper that exposes the Node Sass API for that value. -Object wrapValue(Value value) { - if (value is SassColor) return newNodeSassColor(value); - if (value is SassList) return newNodeSassList(value); - if (value is SassMap) return newNodeSassMap(value); - if (value is SassNumber) return newNodeSassNumber(value); - if (value is SassString) return newNodeSassString(value); - return value; -} +Object wrapValue(Value value) => switch (value) { + SassColor() => newNodeSassColor(value), + SassList() => newNodeSassList(value), + SassMap() => newNodeSassMap(value), + SassNumber() => newNodeSassNumber(value), + SassString() => newNodeSassString(value), + _ => value + }; diff --git a/lib/src/js/legacy/value/map.dart b/lib/src/js/legacy/value/map.dart index 2830ce47c..44b59c618 100644 --- a/lib/src/js/legacy/value/map.dart +++ b/lib/src/js/legacy/value/map.dart @@ -4,6 +4,7 @@ import 'package:js/js.dart'; +import '../../../util/map.dart'; import '../../../value.dart'; import '../../reflection.dart'; import '../value.dart'; @@ -39,14 +40,14 @@ final JSClass legacyMapClass = createJSClass('sass.types.Map', var newKey = unwrapValue(key); var newMap = {}; var i = 0; - for (var oldEntry in thisArg.dartValue.contents.entries) { + for (var (oldKey, oldValue) in thisArg.dartValue.contents.pairs) { if (i == index) { - newMap[newKey] = oldEntry.value; + newMap[newKey] = oldValue; } else { - if (newKey == oldEntry.key) { + if (newKey == oldKey) { throw ArgumentError.value(key, 'key', "is already in the map"); } - newMap[oldEntry.key] = oldEntry.value; + newMap[oldKey] = oldValue; } i++; } diff --git a/lib/src/js/utils.dart b/lib/src/js/utils.dart index 168993098..687484c9a 100644 --- a/lib/src/js/utils.dart +++ b/lib/src/js/utils.dart @@ -218,25 +218,18 @@ Map objectToMap(Object object) { } /// Converts a JavaScript separator string into a [ListSeparator]. -ListSeparator jsToDartSeparator(String? separator) { - switch (separator) { - case ' ': - return ListSeparator.space; - case ',': - return ListSeparator.comma; - case '/': - return ListSeparator.slash; - case null: - return ListSeparator.undecided; - default: - jsThrow(JsError('Unknown separator "$separator".')); - } -} +ListSeparator jsToDartSeparator(String? separator) => switch (separator) { + ' ' => ListSeparator.space, + ',' => ListSeparator.comma, + '/' => ListSeparator.slash, + null => ListSeparator.undecided, + _ => jsThrow(JsError('Unknown separator "$separator".')) + }; /// Converts a syntax string to an instance of [Syntax]. -Syntax parseSyntax(String? syntax) { - if (syntax == null || syntax == 'scss') return Syntax.scss; - if (syntax == 'indented') return Syntax.sass; - if (syntax == 'css') return Syntax.css; - jsThrow(JsError('Unknown syntax "$syntax".')); -} +Syntax parseSyntax(String? syntax) => switch (syntax) { + null || 'scss' => Syntax.scss, + 'indented' => Syntax.sass, + 'css' => Syntax.css, + _ => jsThrow(JsError('Unknown syntax "$syntax".')) + }; diff --git a/lib/src/js/value/map.dart b/lib/src/js/value/map.dart index d1235846f..3e13373d6 100644 --- a/lib/src/js/value/map.dart +++ b/lib/src/js/value/map.dart @@ -4,6 +4,7 @@ import 'package:node_interop/js.dart'; +import '../../util/map.dart'; import '../../value.dart'; import '../immutable.dart'; import '../reflection.dart'; @@ -25,8 +26,8 @@ final JSClass mapClass = () { if (index < 0) index = self.lengthAsList + index; if (index < 0 || index >= self.lengthAsList) return undefined; - var entry = self.contents.entries.elementAt(index); - return SassList([entry.key, entry.value], ListSeparator.space); + var (key, value) = self.contents.pairs.elementAt(index); + return SassList([key, value], ListSeparator.space); } else { return self.contents[indexOrKey] ?? undefined; } diff --git a/lib/src/logger.dart b/lib/src/logger.dart index e5f738a14..a329b3b79 100644 --- a/lib/src/logger.dart +++ b/lib/src/logger.dart @@ -44,8 +44,7 @@ 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) { + if (this case DeprecationHandlingLogger self) { self.warnForDeprecation(deprecation, message, span: span, trace: trace); } else if (!deprecation.isFuture) { warn(message, span: span, trace: trace, deprecation: true); @@ -54,7 +53,7 @@ extension WarnForDeprecation on Logger { } /// A logger that emits no messages. -class _QuietLogger implements Logger { +final class _QuietLogger implements Logger { void warn(String message, {FileSpan? span, Trace? trace, bool deprecation = false}) {} void debug(String message, SourceSpan span) {} diff --git a/lib/src/logger/deprecation_handling.dart b/lib/src/logger/deprecation_handling.dart index b477b07b1..4b185651b 100644 --- a/lib/src/logger/deprecation_handling.dart +++ b/lib/src/logger/deprecation_handling.dart @@ -16,7 +16,7 @@ const _maxRepetitions = 5; /// A logger that wraps an inner logger to have special handling for /// deprecation warnings. -class DeprecationHandlingLogger implements Logger { +final class DeprecationHandlingLogger implements Logger { /// A map of how many times each deprecation has been emitted by this logger. final _warningCounts = {}; @@ -61,11 +61,11 @@ class DeprecationHandlingLogger implements Logger { 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); + throw switch ((span, trace)) { + (var span?, var trace?) => SassRuntimeException(message, span, trace), + (var span?, null) => SassException(message, span), + _ => SassScriptException(message) + }; } if (deprecation.isFuture && !futureDeprecations.contains(deprecation)) { diff --git a/lib/src/logger/js_to_dart.dart b/lib/src/logger/js_to_dart.dart index ef9f3d5a5..aa58a243d 100644 --- a/lib/src/logger/js_to_dart.dart +++ b/lib/src/logger/js_to_dart.dart @@ -11,7 +11,7 @@ import '../logger.dart'; import '../js/logger.dart'; /// A wrapper around a [JSLogger] that exposes it as a Dart [Logger]. -class JSToDartLogger implements Logger { +final class JSToDartLogger implements Logger { /// The wrapped logger object. final JSLogger? _node; @@ -29,28 +29,26 @@ class JSToDartLogger implements Logger { void warn(String message, {FileSpan? span, Trace? trace, bool deprecation = false}) { - var warn = _node?.warn; - if (warn == null) { - _withAscii(() { - _fallback.warn(message, - span: span, trace: trace, deprecation: deprecation); - }); - } else { + if (_node?.warn case var warn?) { warn( message, WarnOptions( span: span ?? (undefined as SourceSpan?), stack: trace.toString(), deprecation: deprecation)); + } else { + _withAscii(() { + _fallback.warn(message, + span: span, trace: trace, deprecation: deprecation); + }); } } void debug(String message, SourceSpan span) { - var debug = _node?.debug; - if (debug == null) { - _withAscii(() => _fallback.debug(message, span)); - } else { + if (_node?.debug case var debug?) { debug(message, DebugOptions(span: span)); + } else { + _withAscii(() => _fallback.debug(message, span)); } } diff --git a/lib/src/logger/stderr.dart b/lib/src/logger/stderr.dart index 64237db58..fc001008f 100644 --- a/lib/src/logger/stderr.dart +++ b/lib/src/logger/stderr.dart @@ -11,7 +11,7 @@ import '../logger.dart'; import '../utils.dart'; /// A logger that prints warnings to standard error or browser console. -class StderrLogger implements Logger { +final class StderrLogger implements Logger { /// Whether to use terminal colors in messages. final bool color; diff --git a/lib/src/logger/tracking.dart b/lib/src/logger/tracking.dart index 75522c7d3..efa463787 100644 --- a/lib/src/logger/tracking.dart +++ b/lib/src/logger/tracking.dart @@ -8,7 +8,7 @@ import 'package:stack_trace/stack_trace.dart'; import '../logger.dart'; /// An logger that wraps another logger and keeps track of when it is used. -class TrackingLogger implements Logger { +final class TrackingLogger implements Logger { final Logger _logger; /// Whether [warn] has been called on this logger. diff --git a/lib/src/module.dart b/lib/src/module.dart index b3f6baa16..b545b2751 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -11,7 +11,7 @@ import 'extend/extension_store.dart'; import 'value.dart'; /// The interface for a Sass module. -abstract class Module { +abstract interface class Module { /// The canonical URL for this module's source file. /// /// This may be `null` if the module was loaded from a string without a URL diff --git a/lib/src/module/built_in.dart b/lib/src/module/built_in.dart index 82821ad81..e19d9bdb0 100644 --- a/lib/src/module/built_in.dart +++ b/lib/src/module/built_in.dart @@ -13,7 +13,7 @@ import '../module.dart'; import '../value.dart'; /// A module provided by Sass, available under the special `sass:` URL space. -class BuiltInModule implements Module { +final class BuiltInModule implements Module { final Uri url; final Map functions; final Map mixins; diff --git a/lib/src/module/forwarded_view.dart b/lib/src/module/forwarded_view.dart index 90bc80532..c6cb42647 100644 --- a/lib/src/module/forwarded_view.dart +++ b/lib/src/module/forwarded_view.dart @@ -88,16 +88,15 @@ class ForwardedModuleView implements Module { } void setVariable(String name, Value value, AstNode nodeWithSpan) { - var shownVariables = _rule.shownVariables; - var hiddenVariables = _rule.hiddenVariables; - if (shownVariables != null && !shownVariables.contains(name)) { + if (_rule.shownVariables case var shownVariables? + when !shownVariables.contains(name)) { throw SassScriptException("Undefined variable."); - } else if (hiddenVariables != null && hiddenVariables.contains(name)) { + } else if (_rule.hiddenVariables case var hiddenVariables? + when hiddenVariables.contains(name)) { throw SassScriptException("Undefined variable."); } - var prefix = _rule.prefix; - if (prefix != null) { + if (_rule.prefix case var prefix?) { if (!name.startsWith(prefix)) { throw SassScriptException("Undefined variable."); } @@ -111,8 +110,7 @@ class ForwardedModuleView implements Module { Object variableIdentity(String name) { assert(variables.containsKey(name)); - var prefix = _rule.prefix; - if (prefix != null) { + if (_rule.prefix case var prefix?) { assert(name.startsWith(prefix)); name = name.substring(prefix.length); } diff --git a/lib/src/module/shadowed_view.dart b/lib/src/module/shadowed_view.dart index bb9bb2d0e..b355bc2b8 100644 --- a/lib/src/module/shadowed_view.dart +++ b/lib/src/module/shadowed_view.dart @@ -14,7 +14,7 @@ import '../value.dart'; /// A [Module] that only exposes members that aren't shadowed by a given /// blocklist of member names. -class ShadowedModuleView implements Module { +final class ShadowedModuleView implements Module { /// The wrapped module. final Module _inner; @@ -81,7 +81,7 @@ class ShadowedModuleView implements Module { if (!variables.containsKey(name)) { throw SassScriptException("Undefined variable."); } else { - return _inner.setVariable(name, value, nodeWithSpan); + _inner.setVariable(name, value, nodeWithSpan); } } diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index bc56d5b5d..acbb51fe7 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -46,36 +46,34 @@ class CssParser extends ScssParser { var name = interpolatedIdentifier(); whitespace(); - switch (name.asPlain) { - case "at-root": - case "content": - case "debug": - case "each": - case "error": - case "extend": - case "for": - case "function": - case "if": - case "include": - case "mixin": - case "return": - case "warn": - case "while": - almostAnyValue(); - error("This at-rule isn't allowed in plain CSS.", - scanner.spanFrom(start)); - - case "import": - return _cssImportRule(start); - case "media": - return mediaRule(start); - case "-moz-document": - return mozDocumentRule(start, name); - case "supports": - return supportsRule(start); - default: - return unknownAtRule(start, name); - } + return switch (name.asPlain) { + "at-root" || + "content" || + "debug" || + "each" || + "error" || + "extend" || + "for" || + "function" || + "if" || + "include" || + "mixin" || + "return" || + "warn" || + "while" => + _forbiddenAtRoot(start), + "import" => _cssImportRule(start), + "media" => mediaRule(start), + "-moz-document" => mozDocumentRule(start, name), + "supports" => supportsRule(start), + _ => unknownAtRule(start, name) + }; + } + + /// Throws an error for a forbidden at-rule. + Never _forbiddenAtRoot(LineScannerState start) { + almostAnyValue(); + error("This at-rule isn't allowed in plain CSS.", scanner.spanFrom(start)); } /// Consumes a plain-CSS `@import` rule that disallows interpolation. @@ -83,14 +81,10 @@ class CssParser extends ScssParser { /// [start] should point before the `@`. ImportRule _cssImportRule(LineScannerState start) { var urlStart = scanner.state; - var next = scanner.peekChar(); - Expression url; - if (next == $u || next == $U) { - url = dynamicUrl(); - } else { - url = - StringExpression(interpolatedString().asInterpolation(static: true)); - } + var url = switch (scanner.peekChar()) { + $u || $U => dynamicUrl(), + _ => StringExpression(interpolatedString().asInterpolation(static: true)) + }; var urlSpan = scanner.spanFrom(urlStart); whitespace(); @@ -108,8 +102,9 @@ class CssParser extends ScssParser { var plain = identifier.asPlain!; // CSS doesn't allow non-plain identifiers var lower = plain.toLowerCase(); - var specialFunction = trySpecialFunction(lower, start); - if (specialFunction != null) return specialFunction; + if (trySpecialFunction(lower, start) case var specialFunction?) { + return specialFunction; + } var beforeArguments = scanner.state; if (!scanner.scanChar($lparen)) return StringExpression(identifier); diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart index dcf81cdb3..71908c3e3 100644 --- a/lib/src/parse/keyframe_selector.dart +++ b/lib/src/parse/keyframe_selector.dart @@ -44,33 +44,32 @@ class KeyframeSelectorParser extends Parser { if (scanner.scanChar($plus)) buffer.writeCharCode($plus); var second = scanner.peekChar(); - if (!isDigit(second) && second != $dot) { + if (!second.isDigit && second != $dot) { scanner.error("Expected number."); } - while (isDigit(scanner.peekChar())) { + while (scanner.peekChar().isDigit) { buffer.writeCharCode(scanner.readChar()); } if (scanner.peekChar() == $dot) { buffer.writeCharCode(scanner.readChar()); - while (isDigit(scanner.peekChar())) { + while (scanner.peekChar().isDigit) { buffer.writeCharCode(scanner.readChar()); } } if (scanIdentChar($e)) { buffer.writeCharCode($e); - var next = scanner.peekChar(); - if (next == $plus || next == $minus) { + if (scanner.peekChar() case $plus || $minus) { buffer.writeCharCode(scanner.readChar()); } - if (!isDigit(scanner.peekChar())) scanner.error("Expected digit."); + if (!scanner.peekChar().isDigit) scanner.error("Expected digit."); - while (isDigit(scanner.peekChar())) { + do { buffer.writeCharCode(scanner.readChar()); - } + } while (scanner.peekChar().isDigit); } scanner.expectChar($percent); diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 044285331..dba0bef99 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -12,6 +12,7 @@ import '../interpolation_map.dart'; import '../logger.dart'; import '../util/character.dart'; import '../util/lazy_file_span.dart'; +import '../util/map.dart'; import '../utils.dart'; /// The abstract base class for all parsers. @@ -90,7 +91,7 @@ class Parser { /// Consumes whitespace, but not comments. @protected void whitespaceWithoutComments() { - while (!scanner.isDone && isWhitespace(scanner.peekChar())) { + while (!scanner.isDone && scanner.peekChar().isWhitespace) { scanner.readChar(); } } @@ -98,7 +99,7 @@ class Parser { /// Consumes spaces and tabs. @protected void spaces() { - while (!scanner.isDone && isSpaceOrTab(scanner.peekChar())) { + while (!scanner.isDone && scanner.peekChar().isSpaceOrTab) { scanner.readChar(); } } @@ -110,23 +111,22 @@ class Parser { bool scanComment() { if (scanner.peekChar() != $slash) return false; - var next = scanner.peekChar(1); - if (next == $slash) { - silentComment(); - return true; - } else if (next == $asterisk) { - loudComment(); - return true; - } else { - return false; + switch (scanner.peekChar(1)) { + case $slash: + silentComment(); + return true; + case $asterisk: + loudComment(); + return true; + case _: + return false; } } /// Like [whitespace], but throws an error if no whitespace is consumed. @protected void expectWhitespace() { - if (scanner.isDone || - !(isWhitespace(scanner.peekChar()) || scanComment())) { + if (scanner.isDone || !(scanner.peekChar().isWhitespace || scanComment())) { scanner.error("Expected whitespace."); } @@ -137,7 +137,7 @@ class Parser { @protected void silentComment() { scanner.expect("//"); - while (!scanner.isDone && !isNewline(scanner.peekChar())) { + while (!scanner.isDone && !scanner.peekChar().isNewline) { scanner.readChar(); } } @@ -181,18 +181,18 @@ class Parser { } } - var first = scanner.peekChar(); - if (first == null) { - scanner.error("Expected identifier."); - } else if (normalize && first == $underscore) { - scanner.readChar(); - text.writeCharCode($dash); - } else if (isNameStart(first)) { - text.writeCharCode(scanner.readChar()); - } else if (first == $backslash) { - text.write(escape(identifierStart: true)); - } else { - scanner.error("Expected identifier."); + switch (scanner.peekChar()) { + case null: + scanner.error("Expected identifier."); + case $underscore when normalize: + scanner.readChar(); + text.writeCharCode($dash); + case int(isNameStart: true): + text.writeCharCode(scanner.readChar()); + case $backslash: + text.write(escape(identifierStart: true)); + case _: + scanner.error("Expected identifier."); } _identifierBody(text, normalize: normalize, unit: unit); @@ -211,24 +211,24 @@ class Parser { /// Like [_identifierBody], but parses the body into the [text] buffer. void _identifierBody(StringBuffer text, {bool normalize = false, bool unit = false}) { + loop: while (true) { - var next = scanner.peekChar(); - if (next == null) { - break; - } else if (unit && next == $dash) { - // Disallow `-` followed by a dot or a digit digit in units. - var second = scanner.peekChar(1); - if (second != null && (second == $dot || isDigit(second))) break; - text.writeCharCode(scanner.readChar()); - } else if (normalize && next == $underscore) { - scanner.readChar(); - text.writeCharCode($dash); - } else if (isName(next)) { - text.writeCharCode(scanner.readChar()); - } else if (next == $backslash) { - text.write(escape()); - } else { - break; + switch (scanner.peekChar()) { + case null: + break loop; + case $dash when unit: + // Disallow `-` followed by a dot or a digit digit in units. + if (scanner.peekChar(1) case $dot || int(isDigit: true)) break loop; + text.writeCharCode(scanner.readChar()); + case $underscore when normalize: + scanner.readChar(); + text.writeCharCode($dash); + case int(isName: true): + text.writeCharCode(scanner.readChar()); + case $backslash: + text.write(escape()); + case _: + break loop; } } } @@ -239,8 +239,9 @@ class Parser { /// quotes and its escapes are resolved. @protected String string() { - // NOTE: this logic is largely duplicated in ScssParser._interpolatedString. - // Most changes here should be mirrored there. + // NOTE: this logic is largely duplicated in + // StylesheetParser.interpolatedString. Most changes here should be mirrored + // there. var quote = scanner.readChar(); if (quote != $single_quote && quote != $double_quote) { @@ -248,22 +249,23 @@ class Parser { } var buffer = StringBuffer(); + loop: while (true) { - var next = scanner.peekChar(); - if (next == quote) { - scanner.readChar(); - break; - } else if (next == null || isNewline(next)) { - scanner.error("Expected ${String.fromCharCode(quote)}."); - } else if (next == $backslash) { - if (isNewline(scanner.peekChar(1))) { - scanner.readChar(); + switch (scanner.peekChar()) { + case var next when next == quote: scanner.readChar(); - } else { - buffer.writeCharCode(escapeCharacter()); - } - } else { - buffer.writeCharCode(scanner.readChar()); + break loop; + case null || int(isNewline: true): + scanner.error("Expected ${String.fromCharCode(quote)}."); + case $backslash: + if (scanner.peekChar(1).isNewline) { + scanner.readChar(); + scanner.readChar(); + } else { + buffer.writeCharCode(escapeCharacter()); + } + case _: + buffer.writeCharCode(scanner.readChar()); } } @@ -277,12 +279,12 @@ class Parser { @protected double naturalNumber() { var first = scanner.readChar(); - if (!isDigit(first)) { + if (!first.isDigit) { scanner.error("Expected digit.", position: scanner.position - 1); } var number = asDecimal(first).toDouble(); - while (isDigit(scanner.peekChar())) { + while (scanner.peekChar().isDigit) { number *= 10; number += asDecimal(scanner.readChar()); } @@ -306,16 +308,16 @@ class Parser { while (true) { var next = scanner.peekChar(); switch (next) { + case null: + break loop; + case $backslash: buffer.write(escape(identifierStart: true)); wroteNewline = false; - break; - case $double_quote: - case $single_quote: + case $double_quote || $single_quote: buffer.write(rawText(string)); wroteNewline = false; - break; case $slash: if (scanner.peekChar(1) == $asterisk) { @@ -324,67 +326,48 @@ class Parser { buffer.writeCharCode(scanner.readChar()); } wroteNewline = false; - break; - case $space: - case $tab: - if (wroteNewline || !isWhitespace(scanner.peekChar(1))) { + case $space || $tab: + if (wroteNewline || !scanner.peekChar(1).isWhitespace) { buffer.writeCharCode($space); } scanner.readChar(); - break; - case $lf: - case $cr: - case $ff: - if (!isNewline(scanner.peekChar(-1))) buffer.writeln(); + case $lf || $cr || $ff: + if (!scanner.peekChar(-1).isNewline) buffer.writeln(); scanner.readChar(); wroteNewline = true; - break; - case $lparen: - case $lbrace: - case $lbracket: - buffer.writeCharCode(next!); // dart-lang/sdk#45357 + case $lparen || $lbrace || $lbracket: + buffer.writeCharCode(next); brackets.add(opposite(scanner.readChar())); wroteNewline = false; - break; - case $rparen: - case $rbrace: - case $rbracket: + case $rparen || $rbrace || $rbracket: if (brackets.isEmpty) break loop; - buffer.writeCharCode(next!); // dart-lang/sdk#45357 + buffer.writeCharCode(next); scanner.expectChar(brackets.removeLast()); wroteNewline = false; - break; case $semicolon: if (brackets.isEmpty) break loop; buffer.writeCharCode(scanner.readChar()); - break; - case $u: - case $U: - var url = tryUrl(); - if (url != null) { + case $u || $U: + if (tryUrl() case var url?) { buffer.write(url); } else { buffer.writeCharCode(scanner.readChar()); } wroteNewline = false; - break; default: - if (next == null) break loop; - if (lookingAtIdentifier()) { buffer.write(identifier()); } else { buffer.writeCharCode(scanner.readChar()); } wroteNewline = false; - break; } } @@ -412,26 +395,29 @@ class Parser { // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not // backtrack and re-parse as a function expression. var buffer = StringBuffer()..write("url("); + loop: while (true) { - var next = scanner.peekChar(); - if (next == null) { - break; - } else if (next == $backslash) { - buffer.write(escape()); - } else if (next == $percent || - next == $ampersand || - next == $hash || - (next >= $asterisk && next <= $tilde) || - next >= 0x0080) { - buffer.writeCharCode(scanner.readChar()); - } else if (isWhitespace(next)) { - whitespace(); - if (scanner.peekChar() != $rparen) break; - } else if (next == $rparen) { - buffer.writeCharCode(scanner.readChar()); - return buffer.toString(); - } else { - break; + switch (scanner.peekChar()) { + case null: + break loop; + case $backslash: + buffer.write(escape()); + case $percent || + $ampersand || + $hash || + // dart-lang/sdk#52740 + // ignore: non_constant_relational_pattern_expression + (>= $asterisk && <= $tilde) || + >= 0x0080: + buffer.writeCharCode(scanner.readChar()); + case int(isWhitespace: true): + whitespace(); + if (scanner.peekChar() != $rparen) break loop; + case $rparen: + buffer.writeCharCode(scanner.readChar()); + return buffer.toString(); + case _: + break loop; } } @@ -460,25 +446,25 @@ class Parser { var start = scanner.position; scanner.expectChar($backslash); var value = 0; - var first = scanner.peekChar(); - if (first == null) { - scanner.error("Expected escape sequence."); - } else if (isNewline(first)) { - scanner.error("Expected escape sequence."); - } else if (isHex(first)) { - for (var i = 0; i < 6; i++) { - var next = scanner.peekChar(); - if (next == null || !isHex(next)) break; - value *= 16; - value += asHex(scanner.readChar()); - } + switch (scanner.peekChar()) { + case null: + scanner.error("Expected escape sequence."); + case int(isNewline: true): + scanner.error("Expected escape sequence."); + case int(isHex: true): + for (var i = 0; i < 6; i++) { + var next = scanner.peekChar(); + if (next == null || !next.isHex) break; + value *= 16; + value += asHex(scanner.readChar()); + } - scanCharIf(isWhitespace); - } else { - value = scanner.readChar(); + scanCharIf((char) => char.isWhitespace); + case _: + value = scanner.readChar(); } - if (identifierStart ? isNameStart(value) : isName(value)) { + if (identifierStart ? value.isNameStart : value.isName) { try { return String.fromCharCode(value); } on RangeError { @@ -487,7 +473,7 @@ class Parser { } } else if (value <= 0x1F || value == 0x7F || - (identifierStart && isDigit(value))) { + (identifierStart && value.isDigit)) { var buffer = StringBuffer()..writeCharCode($backslash); if (value > 0xF) buffer.writeCharCode(hexCharFor(value >> 4)); buffer.writeCharCode(hexCharFor(value & 0xF)); @@ -522,14 +508,15 @@ class Parser { ? actual == char : characterEqualsIgnoreCase(char, actual); - var next = scanner.peekChar(); - if (next != null && matches(next)) { - scanner.readChar(); - return true; - } else if (next == $backslash) { - var start = scanner.state; - if (matches(escapeCharacter())) return true; - scanner.state = start; + switch (scanner.peekChar()) { + case var next? when matches(next): + scanner.readChar(); + return true; + + case $backslash: + var start = scanner.state; + if (matches(escapeCharacter())) return true; + scanner.state = start; } return false; } @@ -554,26 +541,16 @@ class Parser { /// /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#starts-with-a-number @protected - bool lookingAtNumber() { - var first = scanner.peekChar(); - if (first == null) return false; - if (isDigit(first)) return true; - - if (first == $dot) { - var second = scanner.peekChar(1); - return second != null && isDigit(second); - } else if (first == $plus || first == $minus) { - var second = scanner.peekChar(1); - if (second == null) return false; - if (isDigit(second)) return true; - if (second != $dot) return false; - - var third = scanner.peekChar(2); - return third != null && isDigit(third); - } else { - return false; - } - } + bool lookingAtNumber() => switch (scanner.peekChar()) { + int(isDigit: true) => true, + $dot => scanner.peekChar(1)?.isDigit ?? false, + $plus || $minus => switch (scanner.peekChar(1)) { + int(isDigit: true) => true, + $dot => scanner.peekChar(2)?.isDigit ?? false, + _ => false + }, + _ => false + }; /// Returns whether the scanner is immediately before a plain CSS identifier. /// @@ -588,14 +565,14 @@ class Parser { // See also [ScssParser._lookingAtInterpolatedIdentifier]. forward ??= 0; - var first = scanner.peekChar(forward); - if (first == null) return false; - if (isNameStart(first) || first == $backslash) return true; - if (first != $dash) return false; - - var second = scanner.peekChar(forward + 1); - if (second == null) return false; - return isNameStart(second) || second == $backslash || second == $dash; + return switch (scanner.peekChar(forward)) { + int(isNameStart: true) || $backslash => true, + $dash => switch (scanner.peekChar(forward + 1)) { + int(isNameStart: true) || $backslash || $dash => true, + _ => false + }, + _ => false + }; } /// Returns whether the scanner is immediately before a sequence of characters @@ -603,7 +580,7 @@ class Parser { @protected bool lookingAtIdentifierBody() { var next = scanner.peekChar(); - return next != null && (isName(next) || next == $backslash); + return next != null && (next.isName || next == $backslash); } /// Consumes an identifier if its name exactly matches [text]. @@ -676,10 +653,9 @@ class Parser { @protected FileSpan spanFrom(LineScannerState state) { var span = scanner.spanFrom(state); - if (_interpolationMap != null) { - return LazyFileSpan(() => _interpolationMap!.mapSpan(span)); - } - return span; + return _interpolationMap == null + ? span + : LazyFileSpan(() => _interpolationMap!.mapSpan(span)); } /// Prints a warning to standard error, associated with [span]. @@ -695,7 +671,7 @@ class Parser { if (trace == null) { throw exception; } else { - throwWithTrace(exception, trace); + throwWithTrace(exception, error, trace); } } @@ -708,6 +684,7 @@ class Parser { } on SourceSpanFormatException catch (error, stackTrace) { throwWithTrace( SourceSpanFormatException(message, error.span, error.source), + error, stackTrace); } } @@ -736,7 +713,7 @@ class Parser { var map = _interpolationMap; if (map == null) rethrow; - throwWithTrace(map.mapException(error), stackTrace); + throwWithTrace(map.mapException(error), error, stackTrace); } } on SourceSpanFormatException catch (error, stackTrace) { var span = error.span as FileSpan; @@ -744,21 +721,23 @@ class Parser { span = _adjustExceptionSpan(span); } - throwWithTrace(SassFormatException(error.message, span), stackTrace); + throwWithTrace( + SassFormatException(error.message, span), error, 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 + for (var (span, description) in secondarySpans.pairs) + _adjustExceptionSpan(span): description }; } throwWithTrace( MultiSpanSassFormatException( error.message, span, error.primaryLabel, secondarySpans), + error, stackTrace); } } @@ -785,12 +764,12 @@ class Parser { int? lastNewline; while (index >= 0) { var codeUnit = text.codeUnitAt(index); - if (!isWhitespace(codeUnit)) { + if (!codeUnit.isWhitespace) { return lastNewline == null ? location : location.file.location(lastNewline); } - if (isNewline(codeUnit)) lastNewline = index; + if (codeUnit.isNewline) lastNewline = index; index--; } diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index a5b3343d6..95fd054c5 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -49,7 +49,7 @@ class SassParser extends StylesheetParser { buffer.addInterpolation(almostAnyValue(omitComments: true)); buffer.writeCharCode($lf); } while (buffer.trailingString.trimRight().endsWith(',') && - scanCharIf(isNewline)); + scanCharIf((char) => char.isNewline)); return buffer.interpolation(scanner.spanFrom(start)); } @@ -62,18 +62,14 @@ class SassParser extends StylesheetParser { position: _nextIndentationEnd!.position); } - bool atEndOfStatement() { - var next = scanner.peekChar(); - return next == null || isNewline(next); - } + bool atEndOfStatement() => scanner.peekChar()?.isNewline ?? true; bool lookingAtChildren() => atEndOfStatement() && _peekIndentation() > currentIndentation; Import importArgument() { switch (scanner.peekChar()) { - case $u: - case $U: + case $u || $U: var start = scanner.state; if (scanIdentifier("url")) { if (scanner.scanChar($lparen)) { @@ -83,10 +79,8 @@ class SassParser extends StylesheetParser { scanner.state = start; } } - break; - case $single_quote: - case $double_quote: + case $single_quote || $double_quote: return super.importArgument(); } @@ -95,7 +89,7 @@ class SassParser extends StylesheetParser { while (next != null && next != $comma && next != $semicolon && - !isNewline(next)) { + !next.isNewline) { scanner.readChar(); next = scanner.peekChar(); } @@ -136,23 +130,20 @@ class SassParser extends StylesheetParser { List children(Statement child()) { var children = []; _whileIndentedLower(() { - var parsedChild = _child(child); - if (parsedChild != null) children.add(parsedChild); + if (_child(child) case var parsedChild?) children.add(parsedChild); }); return children; } List statements(Statement? statement()) { - var first = scanner.peekChar(); - if (first == $tab || first == $space) { + if (scanner.peekChar() case $tab || $space) { scanner.error("Indenting at the beginning of the document is illegal.", position: 0, length: scanner.position); } var statements = []; while (!scanner.isDone) { - var child = _child(statement); - if (child != null) statements.add(child); + if (_child(statement) case var child?) statements.add(child); var indentation = _readIndentation(); assert(indentation == 0); } @@ -164,31 +155,17 @@ class SassParser extends StylesheetParser { /// This consumes children that are allowed at all levels of the document; the /// [child] parameter is called to consume any children that are specifically /// allowed in the caller's context. - Statement? _child(Statement? child()) { - switch (scanner.peekChar()) { - // Ignore empty lines. - case $cr: - case $lf: - case $ff: - return null; - - case $dollar: - return variableDeclarationWithoutNamespace(); - - case $slash: - switch (scanner.peekChar(1)) { - case $slash: - return _silentComment(); - case $asterisk: - return _loudComment(); - default: - return child(); - } - - default: - return child(); - } - } + Statement? _child(Statement? child()) => switch (scanner.peekChar()) { + // Ignore empty lines. + $cr || $lf || $ff => null, + $dollar => variableDeclarationWithoutNamespace(), + $slash => switch (scanner.peekChar(1)) { + $slash => _silentComment(), + $asterisk => _loudComment(), + _ => child() + }, + _ => child() + }; /// Consumes an indented-style silent comment. SilentComment _silentComment() { @@ -212,7 +189,7 @@ class SassParser extends StylesheetParser { buffer.writeCharCode($space); } - while (!scanner.isDone && !isNewline(scanner.peekChar())) { + while (!scanner.isDone && !scanner.peekChar().isNewline) { buffer.writeCharCode(scanner.readChar()); } buffer.writeln(); @@ -248,7 +225,7 @@ class SassParser extends StylesheetParser { // If the first line is empty, ignore it. var beginningOfComment = scanner.position; spaces(); - if (isNewline(scanner.peekChar())) { + if (scanner.peekChar().isNewline) { _readIndentation(); buffer.writeCharCode($space); } else { @@ -266,11 +243,8 @@ class SassParser extends StylesheetParser { loop: while (!scanner.isDone) { - var next = scanner.peekChar(); - switch (next) { - case $lf: - case $cr: - case $ff: + switch (scanner.peekChar()) { + case $lf || $cr || $ff: break loop; case $hash: @@ -279,11 +253,9 @@ class SassParser extends StylesheetParser { } else { buffer.writeCharCode(scanner.readChar()); } - break; - default: + case _: buffer.writeCharCode(scanner.readChar()); - break; } } @@ -319,7 +291,7 @@ class SassParser extends StylesheetParser { scanner.expect("/*"); while (true) { var next = scanner.readChar(); - if (isNewline(next)) scanner.error("expected */."); + if (next.isNewline) scanner.error("expected */."); if (next != $asterisk) continue; do { @@ -338,8 +310,7 @@ class SassParser extends StylesheetParser { scanner.readChar(); if (scanner.peekChar() == $lf) scanner.readChar(); return; - case $lf: - case $ff: + case $lf || $ff: scanner.readChar(); return; default: @@ -348,19 +319,15 @@ class SassParser extends StylesheetParser { } /// Returns whether the scanner is immediately before *two* newlines. - bool _lookingAtDoubleNewline() { - switch (scanner.peekChar()) { - case $cr: - var nextChar = scanner.peekChar(1); - if (nextChar == $lf) return isNewline(scanner.peekChar(2)); - return nextChar == $cr || nextChar == $ff; - case $lf: - case $ff: - return isNewline(scanner.peekChar(1)); - default: - return false; - } - } + bool _lookingAtDoubleNewline() => switch (scanner.peekChar()) { + $cr => switch (scanner.peekChar(1)) { + $lf => scanner.peekChar(2).isNewline, + $cr || $ff => true, + _ => false + }, + $lf || $ff => scanner.peekChar(1).isNewline, + _ => false + }; /// As long as the scanner's position is indented beneath the starting line, /// runs [body] to consume the next statement. @@ -394,8 +361,7 @@ class SassParser extends StylesheetParser { /// Returns the indentation level of the next line. int _peekIndentation() { - var cached = _nextIndentation; - if (cached != null) return cached; + if (_nextIndentation case var cached?) return cached; if (scanner.isDone) { _nextIndentation = 0; @@ -404,7 +370,7 @@ class SassParser extends StylesheetParser { } var start = scanner.state; - if (!scanCharIf(isNewline)) { + if (!scanCharIf((char) => char.isNewline)) { scanner.error("Expected newline.", position: scanner.position); } @@ -416,14 +382,15 @@ class SassParser extends StylesheetParser { containsSpace = false; nextIndentation = 0; + loop: while (true) { - var next = scanner.peekChar(); - if (next == $space) { - containsSpace = true; - } else if (next == $tab) { - containsTab = true; - } else { - break; + switch (scanner.peekChar()) { + case $space: + containsSpace = true; + case $tab: + containsTab = true; + case _: + break loop; } nextIndentation++; scanner.readChar(); @@ -435,7 +402,7 @@ class SassParser extends StylesheetParser { scanner.state = start; return 0; } - } while (scanCharIf(isNewline)); + } while (scanCharIf((char) => char.isNewline)); _checkIndentationConsistency(containsTab, containsSpace); diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index bbb61a9e4..cc432e9c8 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -24,8 +24,7 @@ class ScssParser extends StylesheetParser { void expectStatementSeparator([String? name]) { whitespaceWithoutComments(); if (scanner.isDone) return; - var next = scanner.peekChar(); - if (next == $semicolon || next == $rbrace) return; + if (scanner.peekChar() case $semicolon || $rbrace) return; scanner.expectChar($semicolon); } @@ -69,28 +68,22 @@ class ScssParser extends StylesheetParser { switch (scanner.peekChar()) { case $dollar: children.add(variableDeclarationWithoutNamespace()); - break; case $slash: switch (scanner.peekChar(1)) { case $slash: children.add(_silentComment()); whitespaceWithoutComments(); - break; case $asterisk: children.add(_loudComment()); whitespaceWithoutComments(); - break; default: children.add(child()); - break; } - break; case $semicolon: scanner.readChar(); whitespaceWithoutComments(); - break; case $rbrace: scanner.expectChar($rbrace); @@ -98,7 +91,6 @@ class ScssParser extends StylesheetParser { default: children.add(child()); - break; } } } @@ -110,34 +102,25 @@ class ScssParser extends StylesheetParser { switch (scanner.peekChar()) { case $dollar: statements.add(variableDeclarationWithoutNamespace()); - break; case $slash: switch (scanner.peekChar(1)) { case $slash: statements.add(_silentComment()); whitespaceWithoutComments(); - break; case $asterisk: statements.add(_loudComment()); whitespaceWithoutComments(); - break; default: - var child = statement(); - if (child != null) statements.add(child); - break; + if (statement() case var child?) statements.add(child); } - break; case $semicolon: scanner.readChar(); whitespaceWithoutComments(); - break; default: - var child = statement(); - if (child != null) statements.add(child); - break; + if (statement() case var child?) statements.add(child); } } return statements; @@ -149,7 +132,7 @@ class ScssParser extends StylesheetParser { scanner.expect("//"); do { - while (!scanner.isDone && !isNewline(scanner.readChar())) {} + while (!scanner.isDone && !scanner.readChar().isNewline) {} if (scanner.isDone) break; spaces(); } while (scanner.scan("//")); @@ -168,6 +151,7 @@ class ScssParser extends StylesheetParser { var start = scanner.state; scanner.expect("/*"); var buffer = InterpolationBuffer()..write("/*"); + loop: while (true) { switch (scanner.peekChar()) { case $hash: @@ -180,7 +164,7 @@ class ScssParser extends StylesheetParser { case $asterisk: buffer.writeCharCode(scanner.readChar()); - if (scanner.peekChar() != $slash) break; + if (scanner.peekChar() != $slash) continue loop; buffer.writeCharCode(scanner.readChar()); return LoudComment(buffer.interpolation(scanner.spanFrom(start))); @@ -188,16 +172,13 @@ class ScssParser extends StylesheetParser { case $cr: scanner.readChar(); if (scanner.peekChar() != $lf) buffer.writeCharCode($lf); - break; case $ff: scanner.readChar(); buffer.writeCharCode($lf); - break; default: buffer.writeCharCode(scanner.readChar()); - break; } } } diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index e376f76be..75df8b205 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -88,8 +88,7 @@ class SelectorParser extends Parser { whitespace(); while (scanner.scanChar($comma)) { whitespace(); - var next = scanner.peekChar(); - if (next == $comma) continue; + if (scanner.peekChar() == $comma) continue; if (scanner.isDone) break; var lineBreak = scanner.line != previousLine; @@ -118,45 +117,37 @@ class SelectorParser extends Parser { while (true) { whitespace(); - var next = scanner.peekChar(); - switch (next) { + switch (scanner.peekChar()) { case $plus: var combinatorStart = scanner.state; scanner.readChar(); combinators .add(CssValue(Combinator.nextSibling, spanFrom(combinatorStart))); - break; case $gt: var combinatorStart = scanner.state; scanner.readChar(); combinators .add(CssValue(Combinator.child, spanFrom(combinatorStart))); - break; case $tilde: var combinatorStart = scanner.state; scanner.readChar(); combinators.add( CssValue(Combinator.followingSibling, spanFrom(combinatorStart))); - break; - - default: - if (next == null || - (!const { - $lbracket, - $dot, - $hash, - $percent, - $colon, - $ampersand, - $asterisk, - $pipe - }.contains(next) && - !lookingAtIdentifier())) { - break loop; - } + case null: + break loop; + + case $lbracket || + $dot || + $hash || + $percent || + $colon || + $ampersand || + $asterisk || + $pipe: + case _ when lookingAtIdentifier(): if (lastCompound != null) { components.add(ComplexSelectorComponent( lastCompound, combinators, spanFrom(componentStart))); @@ -172,7 +163,9 @@ class SelectorParser extends Parser { scanner.error( '"&" may only used at the beginning of a compound selector.'); } - break; + + case _: + break loop; } } @@ -260,7 +253,7 @@ class SelectorParser extends Parser { whitespace(); next = scanner.peekChar(); - var modifier = next != null && isAlphabetic(next) + var modifier = next != null && next.isAlphabetic ? String.fromCharCode(scanner.readChar()) : null; @@ -380,7 +373,7 @@ class SelectorParser extends Parser { } else if (unvendored == "nth-child" || unvendored == "nth-last-child") { argument = _aNPlusB(); whitespace(); - if (isWhitespace(scanner.peekChar(-1)) && scanner.peekChar() != $rparen) { + if (scanner.peekChar(-1).isWhitespace && scanner.peekChar() != $rparen) { expectIdentifier("of"); argument += " of"; whitespace(); @@ -402,27 +395,23 @@ class SelectorParser extends Parser { String _aNPlusB() { var buffer = StringBuffer(); switch (scanner.peekChar()) { - case $e: - case $E: + case $e || $E: expectIdentifier("even"); return "even"; - case $o: - case $O: + case $o || $O: expectIdentifier("odd"); return "odd"; - case $plus: - case $minus: + case $plus || $minus: buffer.writeCharCode(scanner.readChar()); break; } - var first = scanner.peekChar(); - if (first != null && isDigit(first)) { - while (isDigit(scanner.peekChar())) { + if (scanner.peekChar().isDigit) { + do { buffer.writeCharCode(scanner.readChar()); - } + } while (scanner.peekChar().isDigit); whitespace(); if (!scanIdentChar($n)) return buffer.toString(); } else { @@ -436,11 +425,10 @@ class SelectorParser extends Parser { buffer.writeCharCode(scanner.readChar()); whitespace(); - var last = scanner.peekChar(); - if (last == null || !isDigit(last)) scanner.error("Expected a number."); - while (isDigit(scanner.peekChar())) { + if (!scanner.peekChar().isDigit) scanner.error("Expected a number."); + do { buffer.writeCharCode(scanner.readChar()); - } + } while (scanner.peekChar().isDigit); return buffer.toString(); } @@ -449,24 +437,17 @@ 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($asterisk)) { if (!scanner.scanChar($pipe)) return UniversalSelector(spanFrom(start)); - if (scanner.scanChar($asterisk)) { - return UniversalSelector(spanFrom(start), namespace: "*"); - } else { - return TypeSelector( - QualifiedName(identifier(), namespace: "*"), spanFrom(start)); - } - } else if (first == $pipe) { - scanner.readChar(); - if (scanner.scanChar($asterisk)) { - return UniversalSelector(spanFrom(start), namespace: ""); - } else { - return TypeSelector( - QualifiedName(identifier(), namespace: ""), spanFrom(start)); - } + return scanner.scanChar($asterisk) + ? UniversalSelector(spanFrom(start), namespace: "*") + : TypeSelector( + QualifiedName(identifier(), namespace: "*"), spanFrom(start)); + } else if (scanner.scanChar($pipe)) { + return scanner.scanChar($asterisk) + ? UniversalSelector(spanFrom(start), namespace: "") + : TypeSelector( + QualifiedName(identifier(), namespace: ""), spanFrom(start)); } var nameOrNamespace = identifier(); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 0a4286687..b81f2d999 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -7,7 +7,6 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:string_scanner/string_scanner.dart'; -import 'package:tuple/tuple.dart'; import '../ast/sass.dart'; import '../color_names.dart'; @@ -144,7 +143,7 @@ abstract class StylesheetParser extends Parser { /// option and returns its name and declaration. /// /// If [requireParens] is `false`, this allows parentheses to be omitted. - Tuple2 parseSignature( + (String name, ArgumentDeclaration) parseSignature( {bool requireParens = true}) { return wrapSpanFormatException(() { var name = identifier(); @@ -152,7 +151,7 @@ abstract class StylesheetParser extends Parser { ? _argumentDeclaration() : ArgumentDeclaration.empty(scanner.emptySpan); scanner.expectDone(); - return Tuple2(name, arguments); + return (name, arguments); }); } @@ -184,7 +183,7 @@ abstract class StylesheetParser extends Parser { case $rbrace: scanner.error('unmatched "}".', length: 1); - default: + case _: return _inStyleRule || _inUnknownAtRule || _inMixin || _inContentBlock ? _declarationOrStyleRule() : _variableDeclarationOrStyleRule(); @@ -228,32 +227,32 @@ abstract class StylesheetParser extends Parser { var global = false; var flagStart = scanner.state; while (scanner.scanChar($exclamation)) { - var flag = identifier(); - if (flag == 'default') { - if (guarded) { - logger.warnForDeprecation( - Deprecation.duplicateVariableFlags, - '!default should only be written once for each variable.\n' - 'This will be an error in Dart Sass 2.0.0.', - span: scanner.spanFrom(flagStart)); - } + switch (identifier()) { + case 'default': + if (guarded) { + logger.warnForDeprecation( + Deprecation.duplicateVariableFlags, + '!default should only be written once for each variable.\n' + 'This will be an error in Dart Sass 2.0.0.', + span: scanner.spanFrom(flagStart)); + } + guarded = true; - guarded = true; - } else if (flag == 'global') { - if (namespace != null) { - error("!global isn't allowed for variables in other modules.", - scanner.spanFrom(flagStart)); - } else if (global) { - logger.warnForDeprecation( - Deprecation.duplicateVariableFlags, - '!global should only be written once for each variable.\n' - 'This will be an error in Dart Sass 2.0.0.', - span: scanner.spanFrom(flagStart)); - } + case 'global': + if (namespace != null) { + error("!global isn't allowed for variables in other modules.", + scanner.spanFrom(flagStart)); + } else if (global) { + logger.warnForDeprecation( + Deprecation.duplicateVariableFlags, + '!global should only be written once for each variable.\n' + 'This will be an error in Dart Sass 2.0.0.', + span: scanner.spanFrom(flagStart)); + } + global = true; - global = true; - } else { - error("Invalid flag name.", scanner.spanFrom(flagStart)); + case _: + error("Invalid flag name.", scanner.spanFrom(flagStart)); } whitespace(); @@ -283,14 +282,12 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; var variableOrInterpolation = _variableDeclarationOrInterpolation(); - if (variableOrInterpolation is VariableDeclaration) { - return variableOrInterpolation; - } else { - return _styleRule( - InterpolationBuffer() - ..addInterpolation(variableOrInterpolation as Interpolation), - start); - } + return variableOrInterpolation is VariableDeclaration + ? variableOrInterpolation + : _styleRule( + InterpolationBuffer() + ..addInterpolation(variableOrInterpolation as Interpolation), + start); } /// Consumes a [VariableDeclaration], a [Declaration], or a [StyleRule]. @@ -350,14 +347,8 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; var nameBuffer = InterpolationBuffer(); - // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" - // hacks. - var first = scanner.peekChar(); var startsWithPunctuation = false; - if (first == $colon || - first == $asterisk || - first == $dot || - (first == $hash && scanner.peekChar(1) != $lbrace)) { + if (_lookingAtPotentialPropertyHack()) { startsWithPunctuation = true; nameBuffer.writeCharCode(scanner.readChar()); nameBuffer.write(rawText(whitespace)); @@ -527,13 +518,7 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; Interpolation name; - // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" - // hacks. - var first = scanner.peekChar(); - if (first == $colon || - first == $asterisk || - first == $dot || - (first == $hash && scanner.peekChar(1) != $lbrace)) { + if (_lookingAtPotentialPropertyHack()) { var nameBuffer = InterpolationBuffer(); nameBuffer.writeCharCode(scanner.readChar()); nameBuffer.write(rawText(whitespace)); @@ -586,10 +571,9 @@ abstract class StylesheetParser extends Parser { } /// Consumes a statement that's allowed within a declaration. - Statement _declarationChild() { - if (scanner.peekChar() == $at) return _declarationAtRule(); - return _propertyOrVariableDeclaration(parseCustomProperties: false); - } + Statement _declarationChild() => scanner.peekChar() == $at + ? _declarationAtRule() + : _propertyOrVariableDeclaration(parseCustomProperties: false); // ## At Rules @@ -673,32 +657,19 @@ abstract class StylesheetParser extends Parser { /// Consumes an at-rule allowed within a property declaration. Statement _declarationAtRule() { var start = scanner.state; - var name = _plainAtRuleName(); - - switch (name) { - case "content": - return _contentRule(start); - case "debug": - return _debugRule(start); - case "each": - return _eachRule(start, _declarationChild); - case "else": - return _disallowedAtRule(start); - case "error": - return _errorRule(start); - case "for": - return _forRule(start, _declarationChild); - case "if": - return _ifRule(start, _declarationChild); - case "include": - return _includeRule(start); - case "warn": - return _warnRule(start); - case "while": - return _whileRule(start, _declarationChild); - default: - return _disallowedAtRule(start); - } + return switch (_plainAtRuleName()) { + "content" => _contentRule(start), + "debug" => _debugRule(start), + "each" => _eachRule(start, _declarationChild), + "else" => _disallowedAtRule(start), + "error" => _errorRule(start), + "for" => _forRule(start, _declarationChild), + "if" => _ifRule(start, _declarationChild), + "include" => _includeRule(start), + "warn" => _warnRule(start), + "while" => _whileRule(start, _declarationChild), + _ => _disallowedAtRule(start) + }; } /// Consumes a statement allowed within a function. @@ -729,28 +700,18 @@ abstract class StylesheetParser extends Parser { } var start = scanner.state; - switch (_plainAtRuleName()) { - case "debug": - return _debugRule(start); - case "each": - return _eachRule(start, _functionChild); - case "else": - return _disallowedAtRule(start); - case "error": - return _errorRule(start); - case "for": - return _forRule(start, _functionChild); - case "if": - return _ifRule(start, _functionChild); - case "return": - return _returnRule(start); - case "warn": - return _warnRule(start); - case "while": - return _whileRule(start, _functionChild); - default: - return _disallowedAtRule(start); - } + return switch (_plainAtRuleName()) { + "debug" => _debugRule(start), + "each" => _eachRule(start, _functionChild), + "else" => _disallowedAtRule(start), + "error" => _errorRule(start), + "for" => _forRule(start, _functionChild), + "if" => _ifRule(start, _functionChild), + "return" => _returnRule(start), + "warn" => _warnRule(start), + "while" => _whileRule(start, _functionChild), + _ => _disallowedAtRule(start), + }; } /// Consumes an at-rule's name, with interpolation disallowed. @@ -899,16 +860,16 @@ abstract class StylesheetParser extends Parser { scanner.spanFrom(start)); } - switch (unvendor(name)) { - case "calc": - case "element": - case "expression": - case "url": - case "and": - case "or": - case "not": - case "clamp": - error("Invalid function name.", scanner.spanFrom(start)); + if (unvendor(name) + case "calc" || + "element" || + "expression" || + "url" || + "and" || + "or" || + "not" || + "clamp") { + error("Invalid function name.", scanner.spanFrom(start)); } whitespace(); @@ -977,13 +938,9 @@ abstract class StylesheetParser extends Parser { Set? hiddenMixinsAndFunctions; Set? hiddenVariables; if (scanIdentifier("show")) { - var members = _memberList(); - shownMixinsAndFunctions = members.item1; - shownVariables = members.item2; + (shownMixinsAndFunctions, shownVariables) = _memberList(); } else if (scanIdentifier("hide")) { - var members = _memberList(); - hiddenMixinsAndFunctions = members.item1; - hiddenVariables = members.item2; + (hiddenMixinsAndFunctions, hiddenVariables) = _memberList(); } var configuration = _configuration(allowGuarded: true); @@ -1013,7 +970,7 @@ abstract class StylesheetParser extends Parser { /// /// The plain identifiers are returned in the first set, and the variable /// names in the second. - Tuple2, Set> _memberList() { + (Set, Set) _memberList() { var identifiers = {}; var variables = {}; do { @@ -1028,7 +985,7 @@ abstract class StylesheetParser extends Parser { whitespace(); } while (scanner.scanChar($comma)); - return Tuple2(identifiers, variables); + return (identifiers, variables); } /// Consumes an `@if` rule. @@ -1097,8 +1054,7 @@ abstract class StylesheetParser extends Parser { @protected Import importArgument() { var start = scanner.state; - var next = scanner.peekChar(); - if (next == $u || next == $U) { + if (scanner.peekChar() case $u || $U) { var url = dynamicUrl(); whitespace(); var modifiers = tryImportModifiers(); @@ -1144,10 +1100,11 @@ abstract class StylesheetParser extends Parser { if (url.length < 5) return false; if (url.endsWith(".css")) return true; - var first = url.codeUnitAt(0); - if (first == $slash) return url.codeUnitAt(1) == $slash; - if (first != $h) return false; - return url.startsWith("http://") || url.startsWith("https://"); + return switch (url.codeUnitAt(0)) { + $slash => url.codeUnitAt(1) == $slash, + $h => url.startsWith("http://") || url.startsWith("https://"), + _ => false + }; } /// Consumes a sequence of modifiers (such as media or supports queries) @@ -1215,8 +1172,7 @@ abstract class StylesheetParser extends Parser { } else if (scanner.peekChar() == $lparen) { return _supportsCondition(); } else { - var function = _tryImportSupportsFunction(); - if (function != null) return function; + if (_tryImportSupportsFunction() case var function?) return function; var start = scanner.state; var name = _expression(); @@ -1351,11 +1307,9 @@ abstract class StylesheetParser extends Parser { var identifierStart = scanner.state; var identifier = this.identifier(); switch (identifier) { - case "url": - case "url-prefix": - case "domain": - var contents = _tryUrlContents(identifierStart, name: identifier); - if (contents != null) { + case "url" || "url-prefix" || "domain": + if (_tryUrlContents(identifierStart, name: identifier) + case var contents?) { buffer.addInterpolation(contents); } else { scanner.expectChar($lparen); @@ -1378,7 +1332,6 @@ abstract class StylesheetParser extends Parser { !trailing.endsWith('url-prefix("")')) { needsDeprecationWarning = true; } - break; case "regexp": buffer.write("regexp("); @@ -1387,7 +1340,6 @@ abstract class StylesheetParser extends Parser { scanner.expectChar($rparen); buffer.writeCharCode($rparen); needsDeprecationWarning = true; - break; default: error("Invalid function name.", scanner.spanFrom(identifierStart)); @@ -1512,8 +1464,7 @@ abstract class StylesheetParser extends Parser { var guarded = false; var flagStart = scanner.state; if (allowGuarded && scanner.scanChar($exclamation)) { - var flag = identifier(); - if (flag == 'default') { + if (identifier() == 'default') { guarded = true; whitespace(); } else { @@ -1570,8 +1521,9 @@ abstract class StylesheetParser extends Parser { _inUnknownAtRule = true; Interpolation? value; - var next = scanner.peekChar(); - if (next != $exclamation && !atEndOfStatement()) value = almostAnyValue(); + if (scanner.peekChar() != $exclamation && !atEndOfStatement()) { + value = almostAnyValue(); + } AtRule rule; if (lookingAtChildren()) { @@ -1804,12 +1756,11 @@ abstract class StylesheetParser extends Parser { singleExpression_ = BinaryOperationExpression(operator, left, right); allowSlash = false; - if (operator == BinaryOperator.plus || - operator == BinaryOperator.minus) { + if (operator case BinaryOperator.plus || BinaryOperator.minus) { if (scanner.string.substring( right.span.start.offset - 1, right.span.start.offset) == operator.operator && - isWhitespace(scanner.string.codeUnitAt(left.span.end.offset))) { + scanner.string.codeUnitAt(left.span.end.offset).isWhitespace) { logger.warnForDeprecation( Deprecation.strictUnary, "This operation is parsed as:\n" @@ -1903,17 +1854,15 @@ abstract class StylesheetParser extends Parser { resolveOperations(); var spaceExpressions = spaceExpressions_; - if (spaceExpressions != null) { - var singleExpression = singleExpression_; - if (singleExpression == null) scanner.error("Expected expression."); - - spaceExpressions.add(singleExpression); - singleExpression_ = ListExpression( - spaceExpressions, - ListSeparator.space, - spaceExpressions.first.span.expand(singleExpression.span)); - spaceExpressions_ = null; - } + if (spaceExpressions == null) return; + + var singleExpression = singleExpression_; + if (singleExpression == null) scanner.error("Expected expression."); + + spaceExpressions.add(singleExpression); + singleExpression_ = ListExpression(spaceExpressions, ListSeparator.space, + spaceExpressions.first.span.expand(singleExpression.span)); + spaceExpressions_ = null; } loop: @@ -1921,33 +1870,28 @@ abstract class StylesheetParser extends Parser { whitespace(); if (until != null && until()) break; - var first = scanner.peekChar(); - switch (first) { + switch (scanner.peekChar()) { + case null: + break loop; + case $lparen: // Parenthesized numbers can't be slash-separated. addSingleExpression(_parentheses()); - break; case $lbracket: addSingleExpression(_expression(bracketList: true)); - break; case $dollar: addSingleExpression(_variable()); - break; case $ampersand: addSingleExpression(_selector()); - break; - case $single_quote: - case $double_quote: + case $single_quote || $double_quote: addSingleExpression(interpolatedString()); - break; case $hash: addSingleExpression(_hashExpression()); - break; case $equal: scanner.readChar(); @@ -1957,57 +1901,47 @@ abstract class StylesheetParser extends Parser { scanner.expectChar($equal); addOperator(BinaryOperator.equals); } - break; case $exclamation: - var next = scanner.peekChar(1); - if (next == $equal) { - scanner.readChar(); - scanner.readChar(); - addOperator(BinaryOperator.notEquals); - } else if (next == null || - equalsLetterIgnoreCase($i, next) || - isWhitespace(next)) { - addSingleExpression(_importantExpression()); - } else { - break loop; + switch (scanner.peekChar(1)) { + case $equal: + scanner.readChar(); + scanner.readChar(); + addOperator(BinaryOperator.notEquals); + case null || $i || $I || int(isWhitespace: true): + addSingleExpression(_importantExpression()); + case _: + break loop; } - break; case $langle: scanner.readChar(); addOperator(scanner.scanChar($equal) ? BinaryOperator.lessThanOrEquals : BinaryOperator.lessThan); - break; case $rangle: scanner.readChar(); addOperator(scanner.scanChar($equal) ? BinaryOperator.greaterThanOrEquals : BinaryOperator.greaterThan); - break; case $asterisk: scanner.readChar(); addOperator(BinaryOperator.times); - break; + + case $plus when singleExpression_ == null: + addSingleExpression(_unaryOperation()); case $plus: - if (singleExpression_ == null) { - addSingleExpression(_unaryOperation()); - } else { - scanner.readChar(); - addOperator(BinaryOperator.plus); - } - break; + scanner.readChar(); + addOperator(BinaryOperator.plus); case $minus: - var next = scanner.peekChar(1); - if ((isDigit(next) || next == $dot) && + if (scanner.peekChar(1) case int(isDigit: true) || $dot // Make sure `1-2` parses as `1 - 2`, not `1 (-2)`. - (singleExpression_ == null || - isWhitespace(scanner.peekChar(-1)))) { + when singleExpression_ == null || + scanner.peekChar(-1).isWhitespace) { addSingleExpression(_number()); } else if (_lookingAtInterpolatedIdentifier()) { addSingleExpression(identifierLike()); @@ -2017,117 +1951,48 @@ abstract class StylesheetParser extends Parser { scanner.readChar(); addOperator(BinaryOperator.minus); } - break; + + case $slash when singleExpression_ == null: + addSingleExpression(_unaryOperation()); case $slash: - if (singleExpression_ == null) { - addSingleExpression(_unaryOperation()); - } else { - scanner.readChar(); - addOperator(BinaryOperator.dividedBy); - } - break; + scanner.readChar(); + addOperator(BinaryOperator.dividedBy); case $percent: scanner.readChar(); addOperator(BinaryOperator.modulo); - break; - case $0: - case $1: - case $2: - case $3: - case $4: - case $5: - case $6: - case $7: - case $8: - case $9: + // dart-lang/sdk#52740 + // ignore: non_constant_relational_pattern_expression + case >= $0 && <= $9: addSingleExpression(_number()); - break; + + case $dot when scanner.peekChar(1) == $dot: + break loop; case $dot: - if (scanner.peekChar(1) == $dot) break loop; addSingleExpression(_number()); - break; - case $a: - if (!plainCss && scanIdentifier("and")) { - addOperator(BinaryOperator.and); - } else { - addSingleExpression(identifierLike()); - } - break; + case $a when !plainCss && scanIdentifier("and"): + addOperator(BinaryOperator.and); - case $o: - if (!plainCss && scanIdentifier("or")) { - addOperator(BinaryOperator.or); - } else { - addSingleExpression(identifierLike()); - } - break; + case $o when !plainCss && scanIdentifier("or"): + addOperator(BinaryOperator.or); - case $u: - case $U: - if (scanner.peekChar(1) == $plus) { - addSingleExpression(_unicodeRange()); - } else { - addSingleExpression(identifierLike()); - } - break; + // dart-lang/sdk#52740 + // ignore: non_constant_relational_pattern_expression + case $u || $U when scanner.peekChar(1) == $plus: + addSingleExpression(_unicodeRange()); - case $b: - case $c: - case $d: - case $e: - case $f: - case $g: - case $h: - case $i: - case $j: - case $k: - case $l: - case $m: - case $n: - case $p: - case $q: - case $r: - case $s: - case $t: - case $v: - case $w: - case $x: - case $y: - case $z: - case $A: - case $B: - case $C: - case $D: - case $E: - case $F: - case $G: - case $H: - case $I: - case $J: - case $K: - case $L: - case $M: - case $N: - case $O: - case $P: - case $Q: - case $R: - case $S: - case $T: - case $V: - case $W: - case $X: - case $Y: - case $Z: - case $_: - case $backslash: + // ignore: non_constant_relational_pattern_expression + case (>= $a && <= $z) || + // ignore: non_constant_relational_pattern_expression + (>= $A && <= $Z) || + $_ || + $backslash || + >= 0x80: addSingleExpression(identifierLike()); - break; case $comma: // If we discover we're parsing a list whose first element is a @@ -2154,20 +2019,15 @@ abstract class StylesheetParser extends Parser { scanner.readChar(); allowSlash = true; singleExpression_ = null; - break; - default: - if (first != null && first >= 0x80) { - addSingleExpression(identifierLike()); - break; - } else { - break loop; - } + case _: + break loop; } } if (bracketList) scanner.expectChar($rbracket); + // TODO(dart-lang/sdk#52756): Use patterns to null-check these values. var commaExpressions = commaExpressions_; var spaceExpressions = spaceExpressions_; if (commaExpressions != null) { @@ -2209,119 +2069,36 @@ abstract class StylesheetParser extends Parser { (expression is BinaryOperationExpression && expression.allowsSlash); /// Consumes an expression that doesn't contain any top-level whitespace. - Expression _singleExpression() { - var first = scanner.peekChar(); - switch (first) { - // Note: when adding a new case, make sure it's reflected in - // [_lookingAtExpression] and [_expression]. - case $lparen: - return _parentheses(); - case $slash: - return _unaryOperation(); - case $dot: - return _number(); - case $lbracket: - return _expression(bracketList: true); - case $dollar: - return _variable(); - case $ampersand: - return _selector(); - - case $single_quote: - case $double_quote: - return interpolatedString(); - - case $hash: - return _hashExpression(); - - case $plus: - return _plusExpression(); - - case $minus: - return _minusExpression(); - - case $exclamation: - return _importantExpression(); - - case $u: - case $U: - if (scanner.peekChar(1) == $plus) { - return _unicodeRange(); - } else { - return identifierLike(); - } - - case $0: - case $1: - case $2: - case $3: - case $4: - case $5: - case $6: - case $7: - case $8: - case $9: - return _number(); - - case $a: - case $b: - case $c: - case $d: - case $e: - case $f: - case $g: - case $h: - case $i: - case $j: - case $k: - case $l: - case $m: - case $n: - case $o: - case $p: - case $q: - case $r: - case $s: - case $t: - case $v: - case $w: - case $x: - case $y: - case $z: - case $A: - case $B: - case $C: - case $D: - case $E: - case $F: - case $G: - case $H: - case $I: - case $J: - case $K: - case $L: - case $M: - case $N: - case $O: - case $P: - case $Q: - case $R: - case $S: - case $T: - case $V: - case $W: - case $X: - case $Y: - case $Z: - case $_: - case $backslash: - return identifierLike(); - - default: - if (first != null && first >= 0x80) return identifierLike(); - scanner.error("Expected expression."); - } - } + Expression _singleExpression() => switch (scanner.peekChar()) { + // Note: when adding a new case, make sure it's reflected in + // [_lookingAtExpression] and [_expression]. + null => scanner.error("Expected expression."), + $lparen => _parentheses(), + $slash => _unaryOperation(), + $dot => _number(), + $lbracket => _expression(bracketList: true), + $dollar => _variable(), + $ampersand => _selector(), + $single_quote || $double_quote => interpolatedString(), + $hash => _hashExpression(), + $plus => _plusExpression(), + $minus => _minusExpression(), + $exclamation => _importantExpression(), + // dart-lang/sdk#52740 + // ignore: non_constant_relational_pattern_expression + $u || $U when scanner.peekChar(1) == $plus => _unicodeRange(), + // ignore: non_constant_relational_pattern_expression + >= $0 && <= $9 => _number(), + // ignore: non_constant_relational_pattern_expression + (>= $a && <= $z) || + // ignore: non_constant_relational_pattern_expression + (>= $A && <= $Z) || + $_ || + $backslash || + >= 0x80 => + identifierLike(), + _ => scanner.error("Expected expression.") + }; /// Consumes a parenthesized expression. Expression _parentheses() { @@ -2375,7 +2152,7 @@ abstract class StylesheetParser extends Parser { /// as the expression before the colon and [start] the point before the /// opening parenthesis. MapExpression _map(Expression first, LineScannerState start) { - var pairs = [Tuple2(first, expressionUntilComma())]; + var pairs = [(first, expressionUntilComma())]; while (scanner.scanChar($comma)) { whitespace(); @@ -2385,7 +2162,7 @@ abstract class StylesheetParser extends Parser { scanner.expectChar($colon); whitespace(); var value = expressionUntilComma(); - pairs.add(Tuple2(key, value)); + pairs.add((key, value)); } scanner.expectChar($rparen); @@ -2400,8 +2177,7 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; scanner.expectChar($hash); - var first = scanner.peekChar(); - if (first != null && isDigit(first)) { + if (scanner.peekChar()?.isDigit ?? false) { return ColorExpression(_hexColorContents(start), scanner.spanFrom(start)); } @@ -2428,14 +2204,14 @@ abstract class StylesheetParser extends Parser { int green; int blue; double? alpha; - if (!isHex(scanner.peekChar())) { + if (!scanner.peekChar().isHex) { // #abc red = (digit1 << 4) + digit1; green = (digit2 << 4) + digit2; blue = (digit3 << 4) + digit3; } else { var digit4 = _hexDigit(); - if (!isHex(scanner.peekChar())) { + if (!scanner.peekChar().isHex) { // #abcd red = (digit1 << 4) + digit1; green = (digit2 << 4) + digit2; @@ -2446,7 +2222,7 @@ abstract class StylesheetParser extends Parser { green = (digit3 << 4) + digit4; blue = (_hexDigit() << 4) + _hexDigit(); - if (isHex(scanner.peekChar())) { + if (scanner.peekChar().isHex) { alpha = ((_hexDigit() << 4) + _hexDigit()) / 0xff; } } @@ -2466,35 +2242,29 @@ abstract class StylesheetParser extends Parser { /// hex color. bool _isHexColor(Interpolation interpolation) { var plain = interpolation.asPlain; - if (plain == null) return false; - if (plain.length != 3 && - plain.length != 4 && - plain.length != 6 && - plain.length != 8) { + if (plain case String(length: 3 || 4 || 6 || 8)) { + return plain.codeUnits.every((char) => char.isHex); + } else { return false; } - return plain.codeUnits.every(isHex); } // Consumes a single hexadecimal digit. - int _hexDigit() { - var char = scanner.peekChar(); - if (char == null || !isHex(char)) scanner.error("Expected hex digit."); - return asHex(scanner.readChar()); - } + int _hexDigit() => (scanner.peekChar()?.isHex ?? false) + ? asHex(scanner.readChar()) + : scanner.error("Expected hex digit."); /// Consumes an expression that starts with a `+`. Expression _plusExpression() { assert(scanner.peekChar() == $plus); var next = scanner.peekChar(1); - return isDigit(next) || next == $dot ? _number() : _unaryOperation(); + return next.isDigit || next == $dot ? _number() : _unaryOperation(); } /// Consumes an expression that starts with a `-`. Expression _minusExpression() { assert(scanner.peekChar() == $minus); - var next = scanner.peekChar(1); - if (isDigit(next) || next == $dot) return _number(); + if (scanner.peekChar(1) case int(isDigit: true) || $dot) return _number(); if (_lookingAtInterpolatedIdentifier()) return identifierLike(); return _unaryOperation(); } @@ -2528,18 +2298,12 @@ abstract class StylesheetParser extends Parser { /// Returns the unary operator corresponding to [character], or `null` if /// the character is not a unary operator. - UnaryOperator? _unaryOperatorFor(int character) { - switch (character) { - case $plus: - return UnaryOperator.plus; - case $minus: - return UnaryOperator.minus; - case $slash: - return UnaryOperator.divide; - default: - return null; - } - } + UnaryOperator? _unaryOperatorFor(int character) => switch (character) { + $plus => UnaryOperator.plus, + $minus => UnaryOperator.minus, + $slash => UnaryOperator.divide, + _ => null + }; /// Consumes a number expression. NumberExpression _number() { @@ -2578,11 +2342,11 @@ abstract class StylesheetParser extends Parser { /// /// Doesn't support scientific notation. void _consumeNaturalNumber() { - if (!isDigit(scanner.readChar())) { + if (!scanner.readChar().isDigit) { scanner.error("Expected digit.", position: scanner.position - 1); } - while (isDigit(scanner.peekChar())) { + while (scanner.peekChar().isDigit) { scanner.readChar(); } } @@ -2595,13 +2359,13 @@ abstract class StylesheetParser extends Parser { void _tryDecimal({bool allowTrailingDot = false}) { if (scanner.peekChar() != $dot) return; - if (!isDigit(scanner.peekChar(1))) { + if (!scanner.peekChar(1).isDigit) { if (allowTrailingDot) return; scanner.error("Expected digit.", position: scanner.position + 1); } scanner.readChar(); - while (isDigit(scanner.peekChar())) { + while (scanner.peekChar().isDigit) { scanner.readChar(); } } @@ -2612,13 +2376,13 @@ abstract class StylesheetParser extends Parser { if (first != $e && first != $E) return; var next = scanner.peekChar(1); - if (!isDigit(next) && next != $minus && next != $plus) return; + if (!next.isDigit && next != $minus && next != $plus) return; scanner.readChar(); - if (next == $plus || next == $minus) scanner.readChar(); - if (!isDigit(scanner.peekChar())) scanner.error("Expected digit."); + if (next case $plus || $minus) scanner.readChar(); + if (!scanner.peekChar().isDigit) scanner.error("Expected digit."); - while (isDigit(scanner.peekChar())) { + while (scanner.peekChar().isDigit) { scanner.readChar(); } } @@ -2630,7 +2394,7 @@ abstract class StylesheetParser extends Parser { scanner.expectChar($plus); var firstRangeLength = 0; - while (scanCharIf((char) => char != null && isHex(char))) { + while (scanCharIf((char) => char != null && char.isHex)) { firstRangeLength++; } @@ -2652,7 +2416,7 @@ abstract class StylesheetParser extends Parser { if (scanner.scanChar($minus)) { var secondRangeStart = scanner.state; var secondRangeLength = 0; - while (scanCharIf((char) => char != null && isHex(char))) { + while (scanCharIf((char) => char != null && char.isHex)) { secondRangeLength++; } @@ -2707,8 +2471,8 @@ abstract class StylesheetParser extends Parser { /// Consumes a quoted string expression. StringExpression interpolatedString() { - // NOTE: this logic is largely duplicated in ScssParser.interpolatedString. - // Most changes here should be mirrored there. + // NOTE: this logic is largely duplicated in Parser.string. Most changes + // here should be mirrored there. var start = scanner.state; var quote = scanner.readChar(); @@ -2718,30 +2482,27 @@ abstract class StylesheetParser extends Parser { } var buffer = InterpolationBuffer(); + loop: while (true) { - var next = scanner.peekChar(); - if (next == quote) { - scanner.readChar(); - break; - } else if (next == null || isNewline(next)) { - scanner.error("Expected ${String.fromCharCode(quote)}."); - } else if (next == $backslash) { - var second = scanner.peekChar(1); - if (isNewline(second)) { + switch (scanner.peekChar()) { + case var next when next == quote: scanner.readChar(); - scanner.readChar(); - if (second == $cr) scanner.scanChar($lf); - } else { - buffer.writeCharCode(escapeCharacter()); - } - } else if (next == $hash) { - if (scanner.peekChar(1) == $lbrace) { + break loop; + case null || int(isNewline: true): + scanner.error("Expected ${String.fromCharCode(quote)}."); + case $backslash: + var second = scanner.peekChar(1); + if (second.isNewline) { + scanner.readChar(); + scanner.readChar(); + if (second == $cr) scanner.scanChar($lf); + } else { + buffer.writeCharCode(escapeCharacter()); + } + case $hash when scanner.peekChar(1) == $lbrace: buffer.add(singleInterpolation()); - } else { + case _: buffer.writeCharCode(scanner.readChar()); - } - } else { - buffer.writeCharCode(scanner.readChar()); } } @@ -2779,38 +2540,39 @@ abstract class StylesheetParser extends Parser { return BooleanExpression(true, identifier.span); } - var color = colorsByName[lower]; - if (color != null) { + if (colorsByName[lower] case var color?) { color = SassColor.rgbInternal(color.red, color.green, color.blue, color.alpha, SpanColorFormat(identifier.span)); return ColorExpression(color, identifier.span); } } - var specialFunction = trySpecialFunction(lower, start); - if (specialFunction != null) return specialFunction; + if (trySpecialFunction(lower, start) case var specialFunction?) { + return specialFunction; + } } switch (scanner.peekChar()) { + case $dot when scanner.peekChar(1) == $dot: + return StringExpression(identifier); + case $dot: - if (scanner.peekChar(1) == $dot) return StringExpression(identifier); scanner.readChar(); - + // TODO(dart-lang/sdk#52757): Make this a separate case. if (plain != null) return namespacedExpression(plain, start); error("Interpolation isn't allowed in namespaces.", identifier.span); + case $lparen when plain != null: + return FunctionExpression( + plain, + _argumentInvocation(allowEmptySecondArg: lower == 'var'), + scanner.spanFrom(start)); + case $lparen: - if (plain == null) { - return InterpolatedFunctionExpression( - identifier, _argumentInvocation(), scanner.spanFrom(start)); - } else { - return FunctionExpression( - plain, - _argumentInvocation(allowEmptySecondArg: lower == 'var'), - scanner.spanFrom(start)); - } + return InterpolatedFunctionExpression( + identifier, _argumentInvocation(), scanner.spanFrom(start)); - default: + case _: return StringExpression(identifier); } } @@ -2839,42 +2601,38 @@ abstract class StylesheetParser extends Parser { /// [name]. @protected Expression? trySpecialFunction(String name, LineScannerState start) { - var calculation = - scanner.peekChar() == $lparen ? _tryCalculation(name, start) : null; - if (calculation != null) return calculation; + if (scanner.peekChar() == $lparen) { + if (_tryCalculation(name, start) case var calculation?) { + return calculation; + } + } var normalized = unvendor(name); InterpolationBuffer buffer; switch (normalized) { - case "calc": - case "element": - case "expression": - if (!scanner.scanChar($lparen)) return null; + case "calc" || "element" || "expression" when scanner.scanChar($lparen): buffer = InterpolationBuffer() ..write(name) ..writeCharCode($lparen); - break; - case "progid": - if (!scanner.scanChar($colon)) return null; + case "progid" when scanner.scanChar($colon): buffer = InterpolationBuffer() ..write(name) ..writeCharCode($colon); var next = scanner.peekChar(); - while (next != null && (isAlphabetic(next) || next == $dot)) { + while (next != null && (next.isAlphabetic || next == $dot)) { buffer.writeCharCode(scanner.readChar()); next = scanner.peekChar(); } scanner.expectChar($lparen); buffer.writeCharCode($lparen); - break; case "url": return _tryUrlContents(start) .andThen((contents) => StringExpression(contents)); - default: + case _: return null; } @@ -2897,8 +2655,7 @@ abstract class StylesheetParser extends Parser { var arguments = _calculationArguments(1); return CalculationExpression(name, arguments, scanner.spanFrom(start)); - case "min": - case "max": + case "min" || "max": // min() and max() are parsed as calculations if possible, and otherwise // are parsed as normal Sass functions. var beforeArguments = scanner.state; @@ -2916,7 +2673,7 @@ abstract class StylesheetParser extends Parser { var arguments = _calculationArguments(3); return CalculationExpression(name, arguments, scanner.spanFrom(start)); - default: + case _: return null; } } @@ -2928,8 +2685,7 @@ abstract class StylesheetParser extends Parser { /// Otherwise, any number greater than zero are consumed. List _calculationArguments([int? maxArgs]) { scanner.expectChar($lparen); - var interpolation = _tryCalculationInterpolation(); - if (interpolation != null) { + if (_tryCalculationInterpolation() case var interpolation?) { scanner.expectChar($rparen); return [interpolation]; } @@ -2956,22 +2712,20 @@ abstract class StylesheetParser extends Parser { while (true) { var next = scanner.peekChar(); - if (next == $plus || next == $minus) { - if (!isWhitespace(scanner.peekChar(-1)) || - !isWhitespace(scanner.peekChar(1))) { - scanner.error( - '"+" and "-" must be surrounded by whitespace in calculations.'); - } + if (next != $plus && next != $minus) return sum; - scanner.readChar(); - whitespace(); - sum = BinaryOperationExpression( - next == $plus ? BinaryOperator.plus : BinaryOperator.minus, - sum, - _calculationProduct()); - } else { - return sum; + if (!scanner.peekChar(-1).isWhitespace || + !scanner.peekChar(1).isWhitespace) { + scanner.error( + '"+" and "-" must be surrounded by whitespace in calculations.'); } + + scanner.readChar(); + whitespace(); + sum = BinaryOperationExpression( + next == $plus ? BinaryOperator.plus : BinaryOperator.minus, + sum, + _calculationProduct()); } } @@ -2982,64 +2736,64 @@ abstract class StylesheetParser extends Parser { while (true) { whitespace(); var next = scanner.peekChar(); - if (next == $asterisk || next == $slash) { - scanner.readChar(); - whitespace(); - product = BinaryOperationExpression( - next == $asterisk ? BinaryOperator.times : BinaryOperator.dividedBy, - product, - _calculationValue()); - } else { - return product; - } + if (next != $asterisk && next != $slash) return product; + + scanner.readChar(); + whitespace(); + product = BinaryOperationExpression( + next == $asterisk ? BinaryOperator.times : BinaryOperator.dividedBy, + product, + _calculationValue()); } } /// Parses a single calculation value. Expression _calculationValue() { - var next = scanner.peekChar(); - if (next == $plus || next == $dot || isDigit(next)) { - return _number(); - } else if (next == $dollar) { - return _variable(); - } else if (next == $lparen) { - var start = scanner.state; - scanner.readChar(); + switch (scanner.peekChar()) { + case $plus || $dot || int(isDigit: true): + return _number(); + case $dollar: + return _variable(); + case $lparen: + var start = scanner.state; + scanner.readChar(); + + Expression? value = _tryCalculationInterpolation(); + if (value == null) { + whitespace(); + value = _calculationSum(); + } - Expression? value = _tryCalculationInterpolation(); - if (value == null) { whitespace(); - value = _calculationSum(); - } + scanner.expectChar($rparen); + return ParenthesizedExpression(value, scanner.spanFrom(start)); + case _ when lookingAtIdentifier(): + var start = scanner.state; + var ident = identifier(); + if (scanner.scanChar($dot)) return namespacedExpression(ident, start); + if (scanner.peekChar() != $lparen) { + return StringExpression( + Interpolation([ident], scanner.spanFrom(start)), + quotes: false); + } - whitespace(); - scanner.expectChar($rparen); - return ParenthesizedExpression(value, scanner.spanFrom(start)); - } else if (lookingAtIdentifier()) { - var start = scanner.state; - var ident = identifier(); - if (scanner.scanChar($dot)) return namespacedExpression(ident, start); - if (scanner.peekChar() != $lparen) { - return StringExpression(Interpolation([ident], scanner.spanFrom(start)), - quotes: false); - } + var lowerCase = ident.toLowerCase(); + if (_tryCalculation(lowerCase, start) case var calculation?) { + return calculation; + } else if (lowerCase == "if") { + return IfExpression(_argumentInvocation(), scanner.spanFrom(start)); + } else { + return FunctionExpression( + ident, _argumentInvocation(), scanner.spanFrom(start)); + } - var lowerCase = ident.toLowerCase(); - var calculation = _tryCalculation(lowerCase, start); - if (calculation != null) { - return calculation; - } else if (lowerCase == "if") { - return IfExpression(_argumentInvocation(), scanner.spanFrom(start)); - } else { - return FunctionExpression( - ident, _argumentInvocation(), scanner.spanFrom(start)); - } - } else if (next == $minus) { // This has to go after [lookingAtIdentifier] because a hyphen can start // an identifier as well as a number. - return _number(); - } else { - scanner.error("Expected number, variable, function, or calculation."); + case $minus: + return _number(); + + case _: + scanner.error("Expected number, variable, function, or calculation."); } } @@ -3064,16 +2818,12 @@ abstract class StylesheetParser extends Parser { case $backslash: scanner.readChar(); scanner.readChar(); - break; case $slash: if (!scanComment()) scanner.readChar(); - break; - case $single_quote: - case $double_quote: + case $single_quote || $double_quote: interpolatedString(); - break; case $hash: if (parens == 0 && scanner.peekChar(1) == $lbrace) { @@ -3081,7 +2831,6 @@ abstract class StylesheetParser extends Parser { return true; } scanner.readChar(); - break; case $lparen: parens++; @@ -3093,7 +2842,6 @@ abstract class StylesheetParser extends Parser { // dart-lang/sdk#45357 brackets.add(opposite(next!)); scanner.readChar(); - break; case $rparen: parens--; @@ -3107,9 +2855,8 @@ abstract class StylesheetParser extends Parser { return false; } scanner.readChar(); - break; - default: + case _: scanner.readChar(); } } @@ -3135,32 +2882,32 @@ abstract class StylesheetParser extends Parser { var buffer = InterpolationBuffer() ..write(name ?? 'url') ..writeCharCode($lparen); + loop: while (true) { - var next = scanner.peekChar(); - if (next == null) { - break; - } else if (next == $backslash) { - buffer.write(escape()); - } else if (next == $exclamation || - next == $percent || - next == $ampersand || - (next >= $asterisk && next <= $tilde) || - next >= 0x0080) { - buffer.writeCharCode(scanner.readChar()); - } else if (next == $hash) { - if (scanner.peekChar(1) == $lbrace) { + switch (scanner.peekChar()) { + case null: + break loop; + case $backslash: + buffer.write(escape()); + case $hash when scanner.peekChar(1) == $lbrace: buffer.add(singleInterpolation()); - } else { + case $exclamation || + $percent || + $ampersand || + $hash || + // dart-lang/sdk#52740 + // ignore: non_constant_relational_pattern_expression + (>= $asterisk && <= $tilde) || + >= 0x80: buffer.writeCharCode(scanner.readChar()); - } - } else if (isWhitespace(next)) { - whitespaceWithoutComments(); - if (scanner.peekChar() != $rparen) break; - } else if (next == $rparen) { - buffer.writeCharCode(scanner.readChar()); - return buffer.interpolation(scanner.spanFrom(start)); - } else { - break; + case int(isWhitespace: true): + whitespaceWithoutComments(); + if (scanner.peekChar() != $rparen) break loop; + case $rparen: + buffer.writeCharCode(scanner.readChar()); + return buffer.interpolation(scanner.spanFrom(start)); + case _: + break loop; } } @@ -3173,8 +2920,9 @@ abstract class StylesheetParser extends Parser { Expression dynamicUrl() { var start = scanner.state; expectIdentifier("url"); - var contents = _tryUrlContents(start); - if (contents != null) return StringExpression(contents); + if (_tryUrlContents(start) case var contents?) { + return StringExpression(contents); + } return InterpolatedFunctionExpression( Interpolation(["url"], scanner.spanFrom(start)), @@ -3207,18 +2955,14 @@ abstract class StylesheetParser extends Parser { loop: while (true) { - var next = scanner.peekChar(); - switch (next) { + switch (scanner.peekChar()) { case $backslash: // Write a literal backslash because this text will be re-parsed. buffer.writeCharCode(scanner.readChar()); buffer.writeCharCode(scanner.readChar()); - break; - case $double_quote: - case $single_quote: + case $double_quote || $single_quote: buffer.addInterpolation(interpolatedString().asInterpolation()); - break; case $slash: var commentStart = scanner.position; @@ -3227,57 +2971,41 @@ abstract class StylesheetParser extends Parser { } else { buffer.writeCharCode(scanner.readChar()); } - break; - case $hash: - if (scanner.peekChar(1) == $lbrace) { - // Add a full interpolated identifier to handle cases like - // "#{...}--1", since "--1" isn't a valid identifier on its own. - buffer.addInterpolation(interpolatedIdentifier()); - } else { - buffer.writeCharCode(scanner.readChar()); - } - break; + case $hash when scanner.peekChar(1) == $lbrace: + // Add a full interpolated identifier to handle cases like + // "#{...}--1", since "--1" isn't a valid identifier on its own. + buffer.addInterpolation(interpolatedIdentifier()); - case $cr: - case $lf: - case $ff: + case $cr || $lf || $ff: if (indented) break loop; buffer.writeCharCode(scanner.readChar()); - break; - case $exclamation: - case $semicolon: - case $lbrace: - case $rbrace: + case $exclamation || $semicolon || $lbrace || $rbrace: break loop; - case $u: - case $U: + case $u || $U: var beforeUrl = scanner.state; if (!scanIdentifier("url")) { buffer.writeCharCode(scanner.readChar()); - break; + continue loop; } - var contents = _tryUrlContents(beforeUrl); - if (contents == null) { + if (_tryUrlContents(beforeUrl) case var contents?) { + buffer.addInterpolation(contents); + } else { scanner.state = beforeUrl; buffer.writeCharCode(scanner.readChar()); - } else { - buffer.addInterpolation(contents); } - break; - default: - if (next == null) break loop; + case null: + break loop; + + case _ when lookingAtIdentifier(): + buffer.write(identifier()); - if (lookingAtIdentifier()) { - buffer.write(identifier()); - } else { - buffer.writeCharCode(scanner.readChar()); - } - break; + case _: + buffer.writeCharCode(scanner.readChar()); } } @@ -3309,115 +3037,92 @@ abstract class StylesheetParser extends Parser { var wroteNewline = false; loop: while (true) { - var next = scanner.peekChar(); - switch (next) { + switch (scanner.peekChar()) { case $backslash: buffer.write(escape(identifierStart: true)); wroteNewline = false; - break; - case $double_quote: - case $single_quote: + case $double_quote || $single_quote: buffer.addInterpolation(interpolatedString().asInterpolation()); wroteNewline = false; - break; - case $slash: - if (scanner.peekChar(1) == $asterisk) { - buffer.write(rawText(loudComment)); - } else { - buffer.writeCharCode(scanner.readChar()); - } + case $slash when scanner.peekChar(1) == $asterisk: + buffer.write(rawText(loudComment)); wroteNewline = false; - break; - case $hash: - if (scanner.peekChar(1) == $lbrace) { - // Add a full interpolated identifier to handle cases like - // "#{...}--1", since "--1" isn't a valid identifier on its own. - buffer.addInterpolation(interpolatedIdentifier()); - } else { - buffer.writeCharCode(scanner.readChar()); - } + // Add a full interpolated identifier to handle cases like "#{...}--1", + // since "--1" isn't a valid identifier on its own. + case $hash when scanner.peekChar(1) == $lbrace: + buffer.addInterpolation(interpolatedIdentifier()); wroteNewline = false; - break; - case $space: - case $tab: - if (wroteNewline || !isWhitespace(scanner.peekChar(1))) { - buffer.writeCharCode(scanner.readChar()); - } else { - scanner.readChar(); - } - break; + case $space || $tab + when !wroteNewline && scanner.peekChar(1).isWhitespace: + // Collapse whitespace into a single character unless it's following a + // newline, in which case we assume it's indentation. + scanner.readChar(); - case $lf: - case $cr: - case $ff: - if (indented) break loop; - if (!isNewline(scanner.peekChar(-1))) buffer.writeln(); + case $space || $tab: + buffer.writeCharCode(scanner.readChar()); + + case $lf || $cr || $ff when indented: + break loop; + + case $lf || $cr || $ff: + // Collapse multiple newlines into one. + if (!scanner.peekChar(-1).isNewline) buffer.writeln(); scanner.readChar(); wroteNewline = true; - break; - case $lparen: - case $lbrace: - case $lbracket: - buffer.writeCharCode(next!); // dart-lang/sdk#45357 - brackets.add(opposite(scanner.readChar())); + case $lparen || $lbrace || $lbracket: + var bracket = scanner.readChar(); + buffer.writeCharCode(bracket); + brackets.add(opposite(bracket)); wroteNewline = false; - break; - case $rparen: - case $rbrace: - case $rbracket: + case $rparen || $rbrace || $rbracket: if (brackets.isEmpty) break loop; - buffer.writeCharCode(next!); // dart-lang/sdk#45357 - scanner.expectChar(brackets.removeLast()); + var bracket = brackets.removeLast(); + scanner.expectChar(bracket); + buffer.writeCharCode(bracket); wroteNewline = false; - break; case $semicolon: if (!allowSemicolon && brackets.isEmpty) break loop; buffer.writeCharCode(scanner.readChar()); wroteNewline = false; - break; case $colon: if (!allowColon && brackets.isEmpty) break loop; buffer.writeCharCode(scanner.readChar()); wroteNewline = false; - break; - case $u: - case $U: + case $u || $U: var beforeUrl = scanner.state; if (!scanIdentifier("url")) { buffer.writeCharCode(scanner.readChar()); wroteNewline = false; - break; + continue loop; } - var contents = _tryUrlContents(beforeUrl); - if (contents == null) { + if (_tryUrlContents(beforeUrl) case var contents?) { + buffer.addInterpolation(contents); + } else { scanner.state = beforeUrl; buffer.writeCharCode(scanner.readChar()); - } else { - buffer.addInterpolation(contents); } wroteNewline = false; - break; - default: - if (next == null) break loop; + case null: + break loop; - if (lookingAtIdentifier()) { - buffer.write(identifier()); - } else { - buffer.writeCharCode(scanner.readChar()); - } + case _ when lookingAtIdentifier(): + buffer.write(identifier()); + wroteNewline = false; + + case _: + buffer.writeCharCode(scanner.readChar()); wroteNewline = false; - break; } } @@ -3442,17 +3147,17 @@ abstract class StylesheetParser extends Parser { } } - var first = scanner.peekChar(); - if (first == null) { - scanner.error("Expected identifier."); - } else if (isNameStart(first)) { - buffer.writeCharCode(scanner.readChar()); - } else if (first == $backslash) { - buffer.write(escape(identifierStart: true)); - } else if (first == $hash && scanner.peekChar(1) == $lbrace) { - buffer.add(singleInterpolation()); - } else { - scanner.error("Expected identifier."); + switch (scanner.peekChar()) { + case null: + scanner.error("Expected identifier."); + case int(isNameStart: true): + buffer.writeCharCode(scanner.readChar()); + case $backslash: + buffer.write(escape(identifierStart: true)); + case $hash when scanner.peekChar(1) == $lbrace: + buffer.add(singleInterpolation()); + case _: + scanner.error("Expected identifier."); } _interpolatedIdentifierBody(buffer); @@ -3462,21 +3167,19 @@ abstract class StylesheetParser extends Parser { /// Consumes a chunk of a possibly-interpolated CSS identifier after the name /// start, and adds the contents to the [buffer] buffer. void _interpolatedIdentifierBody(InterpolationBuffer buffer) { + loop: while (true) { - var next = scanner.peekChar(); - if (next == null) { - break; - } else if (next == $underscore || - next == $dash || - isAlphanumeric(next) || - next >= 0x0080) { - buffer.writeCharCode(scanner.readChar()); - } else if (next == $backslash) { - buffer.write(escape()); - } else if (next == $hash && scanner.peekChar(1) == $lbrace) { - buffer.add(singleInterpolation()); - } else { - break; + switch (scanner.peekChar()) { + case null: + break loop; + case $underscore || $dash || int(isAlphanumeric: true) || >= 0x80: + buffer.writeCharCode(scanner.readChar()); + case $backslash: + buffer.write(escape()); + case $hash when scanner.peekChar(1) == $lbrace: + buffer.add(singleInterpolation()); + case _: + break loop; } } } @@ -3646,11 +3349,10 @@ abstract class StylesheetParser extends Parser { buffer.add(_expression()); } else { var next = scanner.peekChar(); - if (next == $langle || next == $rangle || next == $equal) { + if (next case $langle || $rangle || $equal) { buffer.writeCharCode($space); buffer.writeCharCode(scanner.readChar()); - if ((next == $langle || next == $rangle) && - scanner.scanChar($equal)) { + if (next case $langle || $rangle when scanner.scanChar($equal)) { buffer.writeCharCode($equal); } buffer.writeCharCode($space); @@ -3658,9 +3360,8 @@ abstract class StylesheetParser extends Parser { whitespace(); buffer.add(_expressionUntilComparison()); - if ((next == $langle || next == $rangle) && - // dart-lang/sdk#45356 - scanner.scanChar(next!)) { + // dart-lang/sdk#45356 + if (next case $langle || $rangle when scanner.scanChar(next!)) { buffer.writeCharCode($space); buffer.writeCharCode(next); if (scanner.scanChar($equal)) buffer.writeCharCode($equal); @@ -3680,11 +3381,12 @@ abstract class StylesheetParser extends Parser { /// Consumes an expression until it reaches a top-level `<`, `>`, or a `=` /// that's not `==`. - Expression _expressionUntilComparison() => _expression(until: () { - var next = scanner.peekChar(); - if (next == $equal) return scanner.peekChar(1) != $equal; - return next == $langle || next == $rangle; - }); + Expression _expressionUntilComparison() => _expression( + until: () => switch (scanner.peekChar()) { + $equal => scanner.peekChar(1) != $equal, + $langle || $rangle => true, + _ => false + }); // ## Supports Conditions @@ -3734,12 +3436,10 @@ abstract class StylesheetParser extends Parser { allowEmpty: true, allowSemicolon: true); scanner.expectChar($rparen); return SupportsFunction(identifier, arguments, scanner.spanFrom(start)); - } else if (identifier.contents.length != 1 || - identifier.contents.first is! Expression) { - error("Expected @supports condition.", identifier.span); + } else if (identifier.contents case [Expression expression]) { + return SupportsInterpolation(expression, scanner.spanFrom(start)); } else { - return SupportsInterpolation( - identifier.contents.first as Expression, scanner.spanFrom(start)); + error("Expected @supports condition.", identifier.span); } } @@ -3781,8 +3481,7 @@ abstract class StylesheetParser extends Parser { _inParentheses = wasInParentheses; var identifier = interpolatedIdentifier(); - var operation = _trySupportsOperation(identifier, nameStart); - if (operation != null) { + if (_trySupportsOperation(identifier, nameStart) case var operation?) { scanner.expectChar($rparen); return operation; } @@ -3812,9 +3511,8 @@ abstract class StylesheetParser extends Parser { SupportsDeclaration _supportsDeclarationValue( Expression name, LineScannerState start) { Expression value; - if (name is StringExpression && - !name.hasQuotes && - name.text.initialPlain.startsWith("--")) { + if (name case StringExpression(hasQuotes: false, :var text) + when text.initialPlain.startsWith("--")) { value = StringExpression(_interpolatedDeclarationValue()); } else { whitespace(); @@ -3871,58 +3569,65 @@ abstract class StylesheetParser extends Parser { /// start escapes and it considers interpolation to be valid in an identifier. /// /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier - bool _lookingAtInterpolatedIdentifier() { - // See also [ScssParser._lookingAtIdentifier]. - - var first = scanner.peekChar(); - if (first == null) return false; - if (isNameStart(first) || first == $backslash) return true; - if (first == $hash) return scanner.peekChar(1) == $lbrace; - - if (first != $dash) return false; - var second = scanner.peekChar(1); - if (second == null) return false; - if (second == $hash) return scanner.peekChar(2) == $lbrace; - return isNameStart(second) || second == $backslash || second == $dash; - } + bool _lookingAtInterpolatedIdentifier() => + // See also [ScssParser._lookingAtIdentifier]. + + switch (scanner.peekChar()) { + null => false, + int(isNameStart: true) || $backslash => true, + $hash => scanner.peekChar(1) == $lbrace, + $dash => switch (scanner.peekChar(1)) { + null => false, + $hash => scanner.peekChar(2) == $lbrace, + int(isNameStart: true) || $backslash || $dash => true, + _ => false + }, + _ => false + }; + + /// Returns whether the scanner is immediately before a character that could + /// start a `*prop: val`, `:prop: val`, `#prop: val`, or `.prop: val` hack. + bool _lookingAtPotentialPropertyHack() => switch (scanner.peekChar()) { + $colon || $asterisk || $dot => true, + $hash => scanner.peekChar(1) != $lbrace, + _ => false + }; /// Returns whether the scanner is immediately before a sequence of characters /// that could be part of an CSS identifier body. /// /// The identifier body may include interpolation. - bool _lookingAtInterpolatedIdentifierBody() { - var first = scanner.peekChar(); - if (first == null) return false; - if (isName(first) || first == $backslash) return true; - return first == $hash && scanner.peekChar(1) == $lbrace; - } + bool _lookingAtInterpolatedIdentifierBody() => switch (scanner.peekChar()) { + null => false, + int(isName: true) || $backslash => true, + $hash => scanner.peekChar(1) == $lbrace, + _ => false + }; /// Returns whether the scanner is immediately before a SassScript expression. - bool _lookingAtExpression() { - var character = scanner.peekChar(); - if (character == null) return false; - if (character == $dot) return scanner.peekChar(1) != $dot; - if (character == $exclamation) { - var next = scanner.peekChar(1); - return next == null || - equalsLetterIgnoreCase($i, next) || - isWhitespace(next); - } - - return character == $lparen || - character == $slash || - character == $lbracket || - character == $single_quote || - character == $double_quote || - character == $hash || - character == $plus || - character == $minus || - character == $backslash || - character == $dollar || - character == $ampersand || - isNameStart(character) || - isDigit(character); - } + bool _lookingAtExpression() => switch (scanner.peekChar()) { + null => false, + $dot => scanner.peekChar(1) != $dot, + $exclamation => switch (scanner.peekChar(1)) { + null || $i || $I || int(isWhitespace: true) => true, + _ => false + }, + $lparen || + $slash || + $lbracket || + $single_quote || + $double_quote || + $hash || + $plus || + $minus || + $backslash || + $dollar || + $ampersand || + int(isNameStart: true) || + int(isDigit: true) => + true, + _ => false + }; // ## Utilities diff --git a/lib/src/stylesheet_graph.dart b/lib/src/stylesheet_graph.dart index 1cd1823a0..3109fc5f0 100644 --- a/lib/src/stylesheet_graph.dart +++ b/lib/src/stylesheet_graph.dart @@ -4,14 +4,24 @@ import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; -import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; import 'import_cache.dart'; import 'importer.dart'; +import 'util/map.dart'; import 'util/nullable.dart'; import 'visitor/find_dependencies.dart'; +/// Maps from non-canonicalized imported URLs in [stylesheet] to nodes, which +/// appears within [baseUrl] imported by [baseImporter]. +/// +/// [modules] contains stylesheets depended on by module loads, while [imports] +/// contains those depended on via `@import`. +typedef _UpstreamNodes = ({ + Map modules, + Map imports +}); + /// A graph of the import relationships between stylesheets, available via /// [nodes]. class StylesheetGraph { @@ -66,12 +76,14 @@ class StylesheetGraph { /// /// Returns `null` if the import cache can't find a stylesheet at [url]. StylesheetNode? _add(Uri url, [Importer? baseImporter, Uri? baseUrl]) { - var tuple = _ignoreErrors(() => importCache.canonicalize(url, + var result = _ignoreErrors(() => importCache.canonicalize(url, baseImporter: baseImporter, baseUrl: baseUrl)); - if (tuple == null) return null; - - addCanonical(tuple.item1, tuple.item2, tuple.item3); - return nodes[tuple.item2]; + if (result case (var importer, var canonicalUrl, :var originalUrl)) { + addCanonical(importer, canonicalUrl, originalUrl); + return nodes[canonicalUrl]; + } else { + return null; + } } /// Adds the stylesheet at the canonicalized [canonicalUrl] and all the @@ -92,14 +104,13 @@ class StylesheetGraph { Set addCanonical( Importer importer, Uri canonicalUrl, Uri originalUrl, {bool recanonicalize = true}) { - var node = _nodes[canonicalUrl]; - if (node != null) return const {}; + if (_nodes[canonicalUrl] != null) return const {}; var stylesheet = _ignoreErrors(() => importCache .importCanonical(importer, canonicalUrl, originalUrl: originalUrl)); if (stylesheet == null) return const {}; - node = StylesheetNode._(stylesheet, importer, canonicalUrl, + var node = StylesheetNode._(stylesheet, importer, canonicalUrl, _upstreamNodes(stylesheet, importer, canonicalUrl)); _nodes[canonicalUrl] = node; @@ -113,17 +124,20 @@ class StylesheetGraph { /// /// The first map contains stylesheets depended on via module loads while the /// second map contains those depended on via `@import`. - Tuple2, Map> _upstreamNodes( + _UpstreamNodes _upstreamNodes( Stylesheet stylesheet, Importer baseImporter, Uri baseUrl) { var active = {baseUrl}; var dependencies = findDependencies(stylesheet); - return Tuple2({ - for (var url in dependencies.modules) - url: _nodeFor(url, baseImporter, baseUrl, active) - }, { - for (var url in dependencies.imports) - url: _nodeFor(url, baseImporter, baseUrl, active, forImport: true) - }); + return ( + modules: { + for (var url in dependencies.modules) + url: _nodeFor(url, baseImporter, baseUrl, active) + }, + imports: { + for (var url in dependencies.imports) + url: _nodeFor(url, baseImporter, baseUrl, active, forImport: true) + } + ); } /// Re-parses the stylesheet at [canonicalUrl] and updates the dependency graph @@ -151,7 +165,7 @@ class StylesheetGraph { node._stylesheet = stylesheet; var upstream = _upstreamNodes(stylesheet, node.importer, canonicalUrl); - node._replaceUpstream(upstream.item1, upstream.item2); + node._replaceUpstream(upstream.modules, upstream.imports); return true; } @@ -226,13 +240,13 @@ class StylesheetGraph { {required bool forImport}) { var map = forImport ? node.upstreamImports : node.upstream; var newMap = {}; - map.forEach((url, upstream) { - if (!importer.couldCanonicalize(url, canonicalUrl)) return; + for (var (url, upstream) in map.pairs) { + if (!importer.couldCanonicalize(url, canonicalUrl)) continue; importCache.clearCanonicalize(url); // If the import produces a different canonicalized URL than it did // before, it changed and the stylesheet needs to be recompiled. - Tuple3? result; + CanonicalizeResult? result; try { result = importCache.canonicalize(url, baseImporter: node.importer, @@ -244,11 +258,11 @@ class StylesheetGraph { // recompiled. } - var newCanonicalUrl = result?.item2; - if (newCanonicalUrl == upstream?.canonicalUrl) return; + var newCanonicalUrl = result?.$2; + if (newCanonicalUrl == upstream?.canonicalUrl) continue; - newMap[url] = result == null ? null : nodes[result.item2]; - }); + newMap[url] = result == null ? null : nodes[newCanonicalUrl]; + } return newMap; } @@ -260,26 +274,24 @@ class StylesheetGraph { StylesheetNode? _nodeFor( Uri url, Importer baseImporter, Uri baseUrl, Set active, {bool forImport = false}) { - var tuple = _ignoreErrors(() => importCache.canonicalize(url, + var result = _ignoreErrors(() => importCache.canonicalize(url, baseImporter: baseImporter, baseUrl: baseUrl, forImport: forImport)); // If an import fails, let the evaluator surface that error rather than // surfacing it here. - if (tuple == null) return null; - var importer = tuple.item1; - var canonicalUrl = tuple.item2; - var resolvedUrl = tuple.item3; + if (result == null) return null; + var (importer, canonicalUrl, :originalUrl) = result; // Don't use [putIfAbsent] here because we want to avoid adding an entry if // the import fails. - if (_nodes.containsKey(canonicalUrl)) return _nodes[canonicalUrl]; + if (_nodes[canonicalUrl] case var node?) return node; /// If we detect a circular import, act as though it doesn't exist. A better /// error will be produced during compilation. if (active.contains(canonicalUrl)) return null; var stylesheet = _ignoreErrors(() => importCache - .importCanonical(importer, canonicalUrl, originalUrl: resolvedUrl)); + .importCanonical(importer, canonicalUrl, originalUrl: originalUrl)); if (stylesheet == null) return null; active.add(canonicalUrl); @@ -341,11 +353,11 @@ class StylesheetNode { final _downstream = {}; StylesheetNode._(this._stylesheet, this.importer, this.canonicalUrl, - Tuple2, Map> allUpstream) - : _upstream = allUpstream.item1, - _upstreamImports = allUpstream.item2 { + _UpstreamNodes allUpstream) + : _upstream = allUpstream.modules, + _upstreamImports = allUpstream.imports { for (var node in upstream.values.followedBy(upstreamImports.values)) { - if (node != null) node._downstream.add(this); + node?._downstream.add(this); } } diff --git a/lib/src/syntax.dart b/lib/src/syntax.dart index 9c737c957..ab9fc9557 100644 --- a/lib/src/syntax.dart +++ b/lib/src/syntax.dart @@ -18,16 +18,11 @@ enum Syntax { css('CSS'); /// Returns the default syntax to use for a file loaded from [path]. - static Syntax forPath(String path) { - switch (p.extension(path)) { - case '.sass': - return Syntax.sass; - case '.css': - return Syntax.css; - default: - return Syntax.scss; - } - } + static Syntax forPath(String path) => switch (p.extension(path)) { + '.sass' => Syntax.sass, + '.css' => Syntax.css, + _ => Syntax.scss + }; /// The name of the syntax. final String _name; diff --git a/lib/src/util/character.dart b/lib/src/util/character.dart index 0282fda43..ea4085d29 100644 --- a/lib/src/util/character.dart +++ b/lib/src/util/character.dart @@ -10,69 +10,79 @@ import 'package:charcode/charcode.dart'; /// lowercase equivalents. const _asciiCaseBit = 0x20; -/// Returns whether [character] is an ASCII whitespace character. -bool isWhitespace(int? character) => - isSpaceOrTab(character) || isNewline(character); - -/// Returns whether [character] is an ASCII newline. -bool isNewline(int? character) => - character == $lf || character == $cr || character == $ff; - -/// Returns whether [character] is a space or a tab character. -bool isSpaceOrTab(int? character) => character == $space || character == $tab; - -/// Returns whether [character] is a letter or number. -bool isAlphanumeric(int character) => - isAlphabetic(character) || isDigit(character); - -/// Returns whether [character] is a letter. -bool isAlphabetic(int character) => - (character >= $a && character <= $z) || - (character >= $A && character <= $Z); - -/// Returns whether [character] is a number. -bool isDigit(int? character) => - character != null && character >= $0 && character <= $9; - -/// Returns whether [character] is legal as the start of a Sass identifier. -bool isNameStart(int character) => - character == $_ || isAlphabetic(character) || character >= 0x0080; - -/// Returns whether [character] is legal in the body of a Sass identifier. -bool isName(int character) => - isNameStart(character) || isDigit(character) || character == $minus; - -/// Returns whether [character] is a hexadecimal digit. -bool isHex(int? character) { - if (character == null) return false; - if (isDigit(character)) return true; - if (character >= $a && character <= $f) return true; - if (character >= $A && character <= $F) return true; - return false; +// Define these checks as extension getters so they can be used in pattern +// matches. +extension CharacterExtension on int { + /// Returns whether [character] is a letter or number. + bool get isAlphanumeric => isAlphabetic || isDigit; + + /// Returns whether [character] is a letter. + bool get isAlphabetic => + (this >= $a && this <= $z) || (this >= $A && this <= $Z); + + /// Returns whether [character] is a number. + bool get isDigit => this >= $0 && this <= $9; + + /// Returns whether [character] is legal as the start of a Sass identifier. + bool get isNameStart => this == $_ || isAlphabetic || this >= 0x0080; + + /// Returns whether [character] is legal in the body of a Sass identifier. + bool get isName => isNameStart || isDigit || this == $minus; + + /// Returns whether [character] is the beginning of a UTF-16 surrogate pair. + bool get isHighSurrogate => + // A character is a high surrogate exactly if it matches 0b110110XXXXXXXXXX. + // 0x36 == 0b110110. + this >> 10 == 0x36; + + /// Returns whether [character] is a Unicode private-use code point in the Basic + /// Multilingual Plane. + /// + /// See https://en.wikipedia.org/wiki/Private_Use_Areas for details. + bool get isPrivateUseBMP => this >= 0xE000 && this <= 0xF8FF; + + /// Returns whether [character] is the high surrogate for a code point in a + /// Unicode private-use supplementary plane. + /// + /// See https://en.wikipedia.org/wiki/Private_Use_Areas for details. + bool get isPrivateUseHighSurrogate => + // Supplementary Private Use Area-A's and B's high surrogates range from + // 0xDB80 to 0xDBFF, which covers exactly the range 0b110110111XXXXXXX. + // 0b110110111 == 0x1B7. + this >> 7 == 0x1B7; + + /// Returns whether [character] is a hexadecimal digit. + bool get isHex => + isDigit || (this >= $a && this <= $f) || (this >= $A && this <= $F); } -/// Returns whether [character] is the beginning of a UTF-16 surrogate pair. -bool isHighSurrogate(int character) => - // A character is a high surrogate exactly if it matches 0b110110XXXXXXXXXX. - // 0x36 == 0b110110. - character >> 10 == 0x36; - -/// Returns whether [character] is a Unicode private-use code point in the Basic -/// Multilingual Plane. -/// -/// See https://en.wikipedia.org/wiki/Private_Use_Areas for details. -bool isPrivateUseBMP(int character) => - character >= 0xE000 && character <= 0xF8FF; +// Like [CharacterExtension], but these are defined on nullable ints because +// they only use equality comparisons. +// +// This also extends a few [CharacterExtension] getters to return `false` for +// null values. +extension NullableCharacterExtension on int? { + /// Returns whether [character] is an ASCII whitespace character. + bool get isWhitespace => isSpaceOrTab || isNewline; + + /// Returns whether [character] is an ASCII newline. + bool get isNewline => this == $lf || this == $cr || this == $ff; + + /// Returns whether [character] is a space or a tab character. + bool get isSpaceOrTab => this == $space || this == $tab; + + /// Returns whether [character] is a number. + bool get isDigit { + var self = this; + return self != null && self.isDigit; + } -/// Returns whether [character] is the high surrogate for a code point in a -/// Unicode private-use supplementary plane. -/// -/// See https://en.wikipedia.org/wiki/Private_Use_Areas for details. -bool isPrivateUseHighSurrogate(int character) => - // Supplementary Private Use Area-A's and B's high surrogates range from - // 0xDB80 to 0xDBFF, which covers exactly the range 0b110110111XXXXXXX. - // 0b110110111 == 0x1B7. - character >> 7 == 0x1B7; + /// Returns whether [character] is a hexadecimal digit. + bool get isHex { + var self = this; + return self != null && self.isHex; + } +} /// Combines a UTF-16 high and low surrogate pair into a single code unit. /// @@ -104,10 +114,15 @@ bool isPrivate(String identifier) { /// /// Assumes that [character] is a hex digit. int asHex(int character) { - assert(isHex(character)); - if (character <= $9) return character - $0; - if (character <= $F) return 10 + character - $A; - return 10 + character - $a; + assert(character.isHex); + return switch (character) { + // dart-lang/sdk#52740 + // ignore: non_constant_relational_pattern_expression + <= $9 => character - $0, + // ignore: non_constant_relational_pattern_expression + <= $F => 10 + character - $A, + _ => 10 + character - $a + }; } /// Returns the hexadecimal digit for [number]. @@ -136,19 +151,13 @@ int decimalCharFor(int number) { /// Assumes that [character] is a left-hand brace-like character, and returns /// the right-hand version. -int opposite(int character) { - switch (character) { - case $lparen: - return $rparen; - case $lbrace: - return $rbrace; - case $lbracket: - return $rbracket; - default: - throw ArgumentError( - '"${String.fromCharCode(character)}" isn\'t a brace-like character.'); - } -} +int opposite(int character) => switch (character) { + $lparen => $rparen, + $lbrace => $rbrace, + $lbracket => $rbracket, + _ => throw ArgumentError( + '"${String.fromCharCode(character)}" isn\'t a brace-like character.') + }; /// Returns [character], converted to upper case if it's an ASCII lowercase /// letter. diff --git a/lib/src/util/iterable.dart b/lib/src/util/iterable.dart new file mode 100644 index 000000000..c5d7e16a2 --- /dev/null +++ b/lib/src/util/iterable.dart @@ -0,0 +1,23 @@ +// 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. + +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) { + if (callback(element) case var value?) 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. + Iterable get exceptLast { + var size = length - 1; + if (size < 0) throw StateError('Iterable may not be empty'); + return take(size); + } +} diff --git a/lib/src/util/map.dart b/lib/src/util/map.dart new file mode 100644 index 000000000..70037fd64 --- /dev/null +++ b/lib/src/util/map.dart @@ -0,0 +1,19 @@ +// 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. + +extension MapExtensions on Map { + /// If [this] doesn't contain the given [key], sets that key to [value] and + /// returns it. + /// + /// Otherwise, calls [merge] with the existing value and [value] and sets + /// [key] to the result. + V putOrMerge(K key, V value, V Function(V oldValue, V newValue) merge) => + containsKey(key) + ? this[key] = merge(this[key] as V, value) + : this[key] = value; + + // TODO(nweiz): Remove this once dart-lang/collection#289 is released. + /// Like [Map.entries], but returns each entry as a record. + Iterable<(K, V)> get pairs => entries.map((e) => (e.key, e.value)); +} diff --git a/lib/src/util/merged_map_view.dart b/lib/src/util/merged_map_view.dart index df749871f..cb0b05458 100644 --- a/lib/src/util/merged_map_view.dart +++ b/lib/src/util/merged_map_view.dart @@ -47,12 +47,11 @@ class MergedMapView extends MapBase { V? operator [](Object? key) => _mapsByKey[key as K]?[key]; operator []=(K key, V value) { - var child = _mapsByKey[key]; - if (child == null) { + if (_mapsByKey[key] case var child?) { + child[key] = value; + } else { throw UnsupportedError("New entries may not be added to MergedMapView."); } - - child[key] = value; } V? remove(Object? key) { diff --git a/lib/src/util/multi_dir_watcher.dart b/lib/src/util/multi_dir_watcher.dart index 9543a1c08..77c1d12c5 100644 --- a/lib/src/util/multi_dir_watcher.dart +++ b/lib/src/util/multi_dir_watcher.dart @@ -7,6 +7,7 @@ import 'package:path/path.dart' as p; import 'package:watcher/watcher.dart'; import '../io.dart'; +import 'map.dart'; /// Watches multiple directories which may change over time recursively for changes. /// @@ -37,10 +38,8 @@ class MultiDirWatcher { /// from [directory]. Future watch(String directory) { var isParentOfExistingDir = false; - for (var entry in _watchers.entries.toList()) { - var existingDir = entry.key!; // dart-lang/path#100 - var existingWatcher = entry.value; - + // dart-lang/path#100 + for (var (existingDir!, existingWatcher) in _watchers.pairs.toList()) { if (!isParentOfExistingDir && (p.equals(existingDir, directory) || p.isWithin(existingDir, directory))) { diff --git a/lib/src/util/span.dart b/lib/src/util/span.dart index b6840e481..46866f60d 100644 --- a/lib/src/util/span.dart +++ b/lib/src/util/span.dart @@ -20,7 +20,7 @@ extension SpanExtensions on FileSpan { /// Returns this span with all leading whitespace trimmed. FileSpan trimLeft() { var start = 0; - while (isWhitespace(text.codeUnitAt(start))) { + while (text.codeUnitAt(start).isWhitespace) { start++; } return subspan(start); @@ -29,7 +29,7 @@ extension SpanExtensions on FileSpan { /// Returns this span with all trailing whitespace trimmed. FileSpan trimRight() { var end = text.length - 1; - while (isWhitespace(text.codeUnitAt(end))) { + while (text.codeUnitAt(end).isWhitespace) { end--; } return subspan(0, end + 1); @@ -94,14 +94,15 @@ extension SpanExtensions on FileSpan { /// Consumes an identifier from [scanner]. void _scanIdentifier(StringScanner scanner) { + loop: while (!scanner.isDone) { - var char = scanner.peekChar()!; - if (char == $backslash) { - consumeEscapedCharacter(scanner); - } else if (isName(char)) { - scanner.readChar(); - } else { - break; + switch (scanner.peekChar()) { + case $backslash: + consumeEscapedCharacter(scanner); + case int(isName: true): + scanner.readChar(); + case _: + break loop; } } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index e73423933..abd7834cc 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -10,12 +10,13 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:string_scanner/string_scanner.dart'; import 'package:term_glyph/term_glyph.dart' as glyph; -import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; import 'exception.dart'; import 'parse/scss.dart'; import 'util/character.dart'; +import 'util/iterable.dart'; +import 'util/map.dart'; /// The URL used in stack traces when no source URL is available. final _noSourceUrl = Uri.parse("-"); @@ -50,13 +51,14 @@ String a(String word) => [$a, $e, $i, $o, $u].contains(word.codeUnitAt(0)) ? "an $word" : "a $word"; /// Returns a bulleted list of items in [bullets]. -String bulletedList(Iterable bullets) { - return bullets.map((element) { - var lines = element.split("\n"); - return "${glyph.bullet} ${lines.first}" + - (lines.length > 1 ? "\n" + indent(lines.skip(1).join("\n"), 2) : ""); - }).join("\n"); -} +String bulletedList(Iterable bullets) => bullets.map((element) { + var lines = element.split("\n"); + return "${glyph.bullet} ${lines.first}" + + switch (lines) { + [_, ...var rest] => "\n" + indent(rest.join("\n"), 2), + _ => "" + }; + }).join("\n"); /// Returns the number of times [codeUnit] appears in [string]. int countOccurrences(String string, int codeUnit) { @@ -98,7 +100,7 @@ String trimAsciiRight(String string, {bool excludeEscape = false}) { /// whitespace, or [null] if [string] is entirely spaces. int? _firstNonWhitespace(String string) { for (var i = 0; i < string.length; i++) { - if (!isWhitespace(string.codeUnitAt(i))) return i; + if (!string.codeUnitAt(i).isWhitespace) return i; } return null; } @@ -111,7 +113,7 @@ int? _firstNonWhitespace(String string) { int? _lastNonWhitespace(String string, {bool excludeEscape = false}) { for (var i = string.length - 1; i >= 0; i--) { var codeUnit = string.codeUnitAt(i); - if (!isWhitespace(codeUnit)) { + if (!codeUnit.isWhitespace) { if (excludeEscape && i != 0 && i != string.length - 1 && @@ -153,13 +155,6 @@ List flattenVertically(Iterable> iterable) { return result; } -/// Returns the first element of [iterable], or `null` if the iterable is empty. -// TODO(nweiz): Use package:collection -T? firstOrNull(Iterable iterable) { - var iterator = iterable.iterator; - return iterator.moveNext() ? iterator.current : null; -} - /// Converts [codepointIndex] to a code unit index, relative to [string]. /// /// A codepoint index is the index in pure Unicode codepoints; a code unit index @@ -167,7 +162,7 @@ T? firstOrNull(Iterable iterable) { int codepointIndexToCodeUnitIndex(String string, int codepointIndex) { var codeUnitIndex = 0; for (var i = 0; i < codepointIndex; i++) { - if (isHighSurrogate(string.codeUnitAt(codeUnitIndex++))) codeUnitIndex++; + if (string.codeUnitAt(codeUnitIndex++).isHighSurrogate) codeUnitIndex++; } return codeUnitIndex; } @@ -180,7 +175,7 @@ int codeUnitIndexToCodepointIndex(String string, int codeUnitIndex) { var codepointIndex = 0; for (var i = 0; i < codeUnitIndex; i++) { codepointIndex++; - if (isHighSurrogate(string.codeUnitAt(i))) i++; + if (string.codeUnitAt(i).isHighSurrogate) i++; } return codepointIndex; } @@ -339,8 +334,7 @@ void removeFirstWhere(List list, bool test(T value), {void orElse()?}) { void mapAddAll2( Map> destination, Map> source) { source.forEach((key, inner) { - var innerDestination = destination[key]; - if (innerDestination != null) { + if (destination[key] case var innerDestination?) { innerDestination.addAll(inner); } else { destination[key] = inner; @@ -385,11 +379,11 @@ Future putIfAbsentAsync( /// Returns a deep copy of a map that contains maps. Map> copyMapOfMap(Map> map) => - {for (var entry in map.entries) entry.key: Map.of(entry.value)}; + {for (var (key, child) in map.pairs) key: Map.of(child)}; /// Returns a deep copy of a map that contains lists. Map> copyMapOfList(Map> map) => - {for (var entry in map.entries) entry.key: entry.value.toList()}; + {for (var (key, list) in map.pairs) key: list.toList()}; /// Consumes an escape sequence from [scanner] and returns the character it /// represents. @@ -397,39 +391,37 @@ int consumeEscapedCharacter(StringScanner scanner) { // See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point. scanner.expectChar($backslash); - var first = scanner.peekChar(); - if (first == null) { - return 0xFFFD; - } else if (isNewline(first)) { - scanner.error("Expected escape sequence."); - } else if (isHex(first)) { - var value = 0; - for (var i = 0; i < 6; i++) { - var next = scanner.peekChar(); - if (next == null || !isHex(next)) break; - value = (value << 4) + asHex(scanner.readChar()); - } - if (isWhitespace(scanner.peekChar())) scanner.readChar(); - - if (value == 0 || - (value >= 0xD800 && value <= 0xDFFF) || - value >= 0x10FFFF) { + switch (scanner.peekChar()) { + case null: return 0xFFFD; - } else { - return value; - } - } else { - return scanner.readChar(); + case int(isNewline: true): + scanner.error("Expected escape sequence."); + case int(isHex: true): + var value = 0; + for (var i = 0; i < 6; i++) { + var next = scanner.peekChar(); + if (next == null || !next.isHex) break; + value = (value << 4) + asHex(scanner.readChar()); + } + if (scanner.peekChar().isWhitespace) scanner.readChar(); + + return switch (value) { + 0 || (>= 0xD800 && <= 0xDFFF) || >= 0x10FFFF => 0xFFFD, + _ => value + }; + case _: + return scanner.readChar(); } } // TODO(nweiz): Use a built-in solution for this when dart-lang/sdk#10297 is // fixed. -/// Throws [error] with [trace] stored as its stack trace. +/// Throws [error] with [originalError]'s stack trace (which defaults to +/// [trace]) stored as its stack trace. /// /// Note that [trace] is only accessible via [getTrace]. -Never throwWithTrace(Object error, StackTrace trace) { - attachTrace(error, trace); +Never throwWithTrace(Object error, Object originalError, StackTrace trace) { + attachTrace(error, getTrace(originalError) ?? trace); throw error; } @@ -437,7 +429,7 @@ Never throwWithTrace(Object error, StackTrace trace) { /// /// In most cases, [throwWithTrace] should be used instead of this. void attachTrace(Object error, StackTrace trace) { - if (error is String || error is num || error is bool) return; + if (error case String() || num() || bool()) return; // Non-`Error` objects thrown in Node will have empty stack traces. We don't // want to store these because they don't have any useful information. @@ -451,46 +443,13 @@ void attachTrace(Object error, StackTrace trace) { StackTrace? getTrace(Object error) => error is String || error is num || error is bool ? null : _traces[error]; -extension MapExtension on Map { - /// If [this] doesn't contain the given [key], sets that key to [value] and - /// returns it. - /// - /// Otherwise, calls [merge] with the existing value and [value] and sets - /// [key] to the result. - V putOrMerge(K key, V value, V Function(V oldValue, V newValue) merge) => - containsKey(key) - ? this[key] = merge(this[key] as V, value) - : this[key] = value; -} - -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. - Iterable get exceptLast { - var size = length - 1; - if (size < 0) throw StateError('Iterable may not be empty'); - return take(size); - } -} - /// Parses a function signature of the format allowed by Node Sass's functions /// option and returns its name and declaration. /// /// If [requireParens] is `false`, this allows parentheses to be omitted. /// /// Throws a [SassFormatException] if parsing fails. -Tuple2 parseSignature(String signature, +(String name, ArgumentDeclaration) parseSignature(String signature, {bool requireParens = true}) { try { return ScssParser(signature).parseSignature(requireParens: requireParens); @@ -498,6 +457,7 @@ Tuple2 parseSignature(String signature, throwWithTrace( SassFormatException( 'Invalid signature "$signature": ${error.message}', error.span), + error, stackTrace); } } diff --git a/lib/src/value.dart b/lib/src/value.dart index ed90bc918..4b21e2434 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -208,8 +208,7 @@ abstract class Value { /// Throws a [SassScriptException] if [this] isn't a type or a structure that /// can be parsed as a selector. String _selectorString([String? name]) { - var string = _selectorStringOrNull(); - if (string != null) return string; + if (_selectorStringOrNull() case var string?) return string; throw SassScriptException( "$this is not a valid selector: it must be a string,\n" @@ -223,40 +222,35 @@ abstract class Value { /// Returns `null` if [this] isn't a type or a structure that can be parsed as /// a selector. String? _selectorStringOrNull() { - if (this is SassString) return (this as SassString).text; - if (this is! SassList) return null; - var list = this as SassList; - if (list.asList.isEmpty) return null; + var self = this; + if (self is SassString) return self.text; + if (self is! SassList) return null; + if (self.asList.isEmpty) return null; var result = []; - switch (list.separator) { + switch (self.separator) { case ListSeparator.comma: - for (var complex in list.asList) { - if (complex is SassString) { - result.add(complex.text); - } else if (complex is SassList && - complex.separator == ListSeparator.space) { - var string = complex._selectorStringOrNull(); - if (string == null) return null; - result.add(string); - } else { - return null; + for (var complex in self.asList) { + switch (complex) { + case SassString(): + result.add(complex.text); + case SassList(separator: ListSeparator.space): + var string = complex._selectorStringOrNull(); + if (string == null) return null; + result.add(string); + case _: + return null; } } - break; case ListSeparator.slash: return null; - default: - for (var compound in list.asList) { - if (compound is SassString) { - result.add(compound.text); - } else { - return null; - } + case _: + for (var compound in self.asList) { + if (compound is! SassString) return null; + result.add(compound.text); } - break; } - return result.join(list.separator == ListSeparator.comma ? ', ' : ' '); + return result.join(self.separator == ListSeparator.comma ? ', ' : ' '); } /// Returns a new list containing [contents] that defaults to this value's @@ -320,28 +314,21 @@ abstract class Value { /// /// @nodoc @internal - Value plus(Value other) { - if (other is SassString) { - return SassString(toCssString() + other.text, quotes: other.hasQuotes); - } else if (other is SassCalculation) { - throw SassScriptException('Undefined operation "$this + $other".'); - } else { - return SassString(toCssString() + other.toCssString(), quotes: false); - } - } + Value plus(Value other) => switch (other) { + SassString() => + SassString(toCssString() + other.text, quotes: other.hasQuotes), + SassCalculation() => + throw SassScriptException('Undefined operation "$this + $other".'), + _ => SassString(toCssString() + other.toCssString(), quotes: false) + }; /// The SassScript `-` operation. /// /// @nodoc @internal - Value minus(Value other) { - if (other is SassCalculation) { - throw SassScriptException('Undefined operation "$this - $other".'); - } else { - return SassString("${toCssString()}-${other.toCssString()}", - quotes: false); - } - } + Value minus(Value other) => other is SassCalculation + ? throw SassScriptException('Undefined operation "$this - $other".') + : SassString("${toCssString()}-${other.toCssString()}", quotes: false); /// The SassScript `/` operation. /// @@ -427,6 +414,7 @@ extension SassApiValue on Value { throwWithTrace( SassScriptException( error.toString().replaceFirst("Error: ", ""), name), + error, stackTrace); } } @@ -451,6 +439,7 @@ extension SassApiValue on Value { throwWithTrace( SassScriptException( error.toString().replaceFirst("Error: ", ""), name), + error, stackTrace); } } @@ -475,6 +464,7 @@ extension SassApiValue on Value { throwWithTrace( SassScriptException( error.toString().replaceFirst("Error: ", ""), name), + error, stackTrace); } } @@ -499,6 +489,7 @@ extension SassApiValue on Value { throwWithTrace( SassScriptException( error.toString().replaceFirst("Error: ", ""), name), + error, stackTrace); } } diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index 12a53e947..b39011bf0 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -20,8 +20,7 @@ import '../visitor/serialize.dart'; /// works with are always fully simplified. /// /// {@category Value} -@sealed -class SassCalculation extends Value { +final class SassCalculation extends Value { /// The calculation's name, such as `"calc"`. final String name; @@ -51,12 +50,12 @@ class SassCalculation extends Value { /// This automatically simplifies the calculation, so it may return a /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it /// can determine that the calculation will definitely produce invalid CSS. - static Value calc(Object argument) { - argument = _simplify(argument); - if (argument is SassNumber) return argument; - if (argument is SassCalculation) return argument; - return SassCalculation._("calc", List.unmodifiable([argument])); - } + static Value calc(Object argument) => switch (_simplify(argument)) { + SassNumber value => value, + SassCalculation value => value, + var simplified => + SassCalculation._("calc", List.unmodifiable([simplified])) + }; /// Creates a `min()` calculation with the given [arguments]. /// @@ -182,14 +181,11 @@ class SassCalculation extends Value { static Object operateInternal( CalculationOperator operator, Object left, Object right, {required bool inMinMax, required bool simplify}) { - if (!simplify) { - return CalculationOperation._(operator, left, right); - } + if (!simplify) return CalculationOperation._(operator, left, right); left = _simplify(left); right = _simplify(right); - if (operator == CalculationOperator.plus || - operator == CalculationOperator.minus) { + if (operator case CalculationOperator.plus || CalculationOperator.minus) { if (left is SassNumber && right is SassNumber && (inMinMax @@ -228,23 +224,20 @@ class SassCalculation extends Value { List.unmodifiable(args.map(_simplify)); /// Simplifies a calculation argument. - static Object _simplify(Object arg) { - if (arg is SassNumber || - arg is CalculationInterpolation || - arg is CalculationOperation) { - return arg; - } else if (arg is SassString) { - if (!arg.hasQuotes) return arg; - throw SassScriptException( - "Quoted string $arg can't be used in a calculation."); - } else if (arg is SassCalculation) { - return arg.name == 'calc' ? arg.arguments[0] : arg; - } else if (arg is Value) { - throw SassScriptException("Value $arg can't be used in a calculation."); - } else { - throw ArgumentError("Unexpected calculation argument $arg."); - } - } + static Object _simplify(Object arg) => switch (arg) { + SassNumber() || + CalculationInterpolation() || + CalculationOperation() => + arg, + SassString(hasQuotes: false) => arg, + SassString() => throw SassScriptException( + "Quoted string $arg can't be used in a calculation."), + SassCalculation(name: 'calc', arguments: [var value]) => value, + SassCalculation() => arg, + Value() => throw SassScriptException( + "Value $arg can't be used in a calculation."), + _ => throw ArgumentError("Unexpected calculation argument $arg.") + }; /// Verifies that all the numbers in [args] aren't known to be incompatible /// with one another, and that they don't have units that are too complex for @@ -254,8 +247,7 @@ class SassCalculation extends Value { // _EvaluateVisitor._verifyCompatibleNumbers and most changes here should // also be reflected there. for (var arg in args) { - if (arg is! SassNumber) continue; - if (arg.numeratorUnits.length > 1 || arg.denominatorUnits.isNotEmpty) { + if (arg case SassNumber(hasComplexUnits: true)) { throw SassScriptException( "Number $arg isn't compatible with CSS calculations."); } @@ -326,8 +318,7 @@ class SassCalculation extends Value { /// A binary operation that can appear in a [SassCalculation]. /// /// {@category Value} -@sealed -class CalculationOperation { +final class CalculationOperation { /// We use a getters to allow overriding the logic in the JS API /// implementation. diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index d3845559e..a1aae468b 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -298,15 +298,12 @@ class SassColor extends Value { if (hue < 0) hue += 1; if (hue > 1) hue -= 1; - if (hue < 1 / 6) { - return m1 + (m2 - m1) * hue * 6; - } else if (hue < 1 / 2) { - return m2; - } else if (hue < 2 / 3) { - return m1 + (m2 - m1) * (2 / 3 - hue) * 6; - } else { - return m1; - } + return switch (hue) { + < 1 / 6 => m1 + (m2 - m1) * hue * 6, + < 1 / 2 => m2, + < 2 / 3 => m1 + (m2 - m1) * (2 / 3 - hue) * 6, + _ => m1 + }; } /// Returns an `rgb()` or `rgba()` function call that will evaluate to this diff --git a/lib/src/value/map.dart b/lib/src/value/map.dart index bafbc4e12..b9895eda4 100644 --- a/lib/src/value/map.dart +++ b/lib/src/value/map.dart @@ -7,6 +7,7 @@ import 'package:meta/meta.dart'; import '../visitor/interface/value.dart'; import '../value.dart'; import '../utils.dart'; +import '../util/map.dart'; /// A SassScript map. /// @@ -29,13 +30,10 @@ class SassMap extends Value { ListSeparator get separator => contents.isEmpty ? ListSeparator.undecided : ListSeparator.comma; - List get asList { - var result = []; - contents.forEach((key, value) { - result.add(SassList([key, value], ListSeparator.space)); - }); - return result; - } + List get asList => [ + for (var (key, value) in contents.pairs) + SassList([key, value], ListSeparator.space) + ]; /// @nodoc @internal diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 9a5821bfd..410bc8465 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -5,9 +5,9 @@ import 'dart:math'; import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; import '../exception.dart'; +import '../util/map.dart'; import '../util/number.dart'; import '../utils.dart'; import '../value.dart'; @@ -155,8 +155,8 @@ const _unitsByType = { /// A map from units to the human-readable names of those unit types. final _typesByUnit = { - for (var entry in _unitsByType.entries) - for (var unit in entry.value) unit: entry.key + for (var (type, units) in _unitsByType.pairs) + for (var unit in units) unit: type }; /// Returns the number of [unit1]s per [unit2]. @@ -167,9 +167,8 @@ final _typesByUnit = { @internal double? conversionFactor(String unit1, String unit2) { if (unit1 == unit2) return 1; - var innerMap = _conversions[unit1]; - if (innerMap == null) return null; - return innerMap[unit2]; + if (_conversions[unit1] case var innerMap?) return innerMap[unit2]; + return null; } /// A SassScript number. @@ -217,12 +216,18 @@ abstract class SassNumber extends Value { /// should use [assertUnit]. bool get hasUnits; + /// Whether [this] has more than one numerator unit, or any denominator units. + /// + /// This is `true` for numbers whose units make them unrepresentable as CSS + /// lengths. + bool get hasComplexUnits; + /// The representation of this number as two slash-separated numbers, if it /// has one. /// /// @nodoc @internal - final Tuple2? asSlash; + final (SassNumber, SassNumber)? asSlash; /// Whether [this] is an integer, according to [fuzzyEquals]. /// @@ -253,47 +258,44 @@ abstract class SassNumber extends Value { factory SassNumber.withUnits(num value, {List? numeratorUnits, List? denominatorUnits}) { var valueDouble = value.toDouble(); - if (denominatorUnits == null || denominatorUnits.isEmpty) { - if (numeratorUnits == null || numeratorUnits.isEmpty) { + switch ((numeratorUnits, denominatorUnits)) { + case (null || [], null || []): return UnitlessSassNumber(valueDouble); - } else if (numeratorUnits.length == 1) { - return SingleUnitSassNumber(valueDouble, numeratorUnits[0]); - } else { + case ([var unit], null || []): + return SingleUnitSassNumber(valueDouble, unit); + // TODO(dart-lang/language#3160): Remove extra null checks + case (var numerators?, null || []): return ComplexSassNumber( - valueDouble, List.unmodifiable(numeratorUnits), const []); - } - } else if (numeratorUnits == null || numeratorUnits.isEmpty) { - return ComplexSassNumber( - valueDouble, const [], List.unmodifiable(denominatorUnits)); - } else { - var numerators = numeratorUnits.toList(); - var unsimplifiedDenominators = denominatorUnits.toList(); - - var denominators = []; - for (var denominator in unsimplifiedDenominators) { - var simplifiedAway = false; - for (var i = 0; i < numerators.length; i++) { - var factor = conversionFactor(denominator, numerators[i]); - if (factor == null) continue; - valueDouble *= factor; - numerators.removeAt(i); - simplifiedAway = true; - break; - } - if (!simplifiedAway) denominators.add(denominator); - } + valueDouble, List.unmodifiable(numerators), const []); + case (null || [], var denominators?): + return ComplexSassNumber( + valueDouble, const [], List.unmodifiable(denominators)); + } - if (denominatorUnits.isEmpty) { - if (numeratorUnits.isEmpty) { - return UnitlessSassNumber(valueDouble); - } else if (numeratorUnits.length == 1) { - return SingleUnitSassNumber(valueDouble, numeratorUnits.single); - } + // dart-lang/language#3160 as well + var numerators = numeratorUnits!.toList(); + var unsimplifiedDenominators = denominatorUnits!.toList(); + + var denominators = []; + for (var denominator in unsimplifiedDenominators) { + var simplifiedAway = false; + for (var i = 0; i < numerators.length; i++) { + var factor = conversionFactor(denominator, numerators[i]); + if (factor == null) continue; + valueDouble *= factor; + numerators.removeAt(i); + simplifiedAway = true; + break; } - - return ComplexSassNumber(valueDouble, List.unmodifiable(numerators), - List.unmodifiable(denominators)); + if (!simplifiedAway) denominators.add(denominator); } + + return switch ((numerators, denominators)) { + ([], []) => UnitlessSassNumber(valueDouble), + ([var unit], []) => SingleUnitSassNumber(valueDouble, unit), + _ => ComplexSassNumber(valueDouble, List.unmodifiable(numerators), + List.unmodifiable(denominators)) + }; } /// @nodoc @@ -315,7 +317,7 @@ abstract class SassNumber extends Value { @internal SassNumber withoutSlash() => asSlash == null ? this : withValue(value); - /// Returns a copy of [this] with [asSlash] set to a tuple containing + /// Returns a copy of [this] with [asSlash] set to a pair containing /// [numerator] and [denominator]. /// /// @nodoc @@ -331,8 +333,7 @@ abstract class SassNumber extends Value { /// from a function argument, [name] is the argument name (without the `$`). /// It's used for error reporting. int assertInt([String? name]) { - var integer = fuzzyAsInt(value); - if (integer != null) return integer; + if (fuzzyAsInt(value) case var integer?) return integer; throw SassScriptException("$this is not an int.", name); } @@ -343,8 +344,7 @@ abstract class SassNumber extends Value { /// came from a function argument, [name] is the argument name (without the /// `$`). It's used for error reporting. double valueInRange(num min, num max, [String? name]) { - var result = fuzzyCheckRange(value, min, max); - if (result != null) return result; + if (fuzzyCheckRange(value, min, max) case var result?) return result; throw SassScriptException( "Expected $this to be within $min$unitString and $max$unitString.", name); @@ -360,8 +360,7 @@ abstract class SassNumber extends Value { /// @nodoc @internal double valueInRangeWithUnit(num min, num max, String name, String unit) { - var result = fuzzyCheckRange(value, min, max); - if (result != null) return result; + if (fuzzyCheckRange(value, min, max) case var result?) return result; throw SassScriptException( "Expected $this to be within $min$unit and $max$unit.", name); } @@ -792,28 +791,19 @@ abstract class SassNumber extends Value { SassNumber multiplyUnits(double value, List otherNumerators, List otherDenominators) { // Short-circuit without allocating any new unit lists if possible. - if (numeratorUnits.isEmpty) { - if (otherDenominators.isEmpty && - !_areAnyConvertible(denominatorUnits, otherNumerators)) { - return SassNumber.withUnits(value, - numeratorUnits: otherNumerators, - denominatorUnits: denominatorUnits); - } else if (denominatorUnits.isEmpty) { - return SassNumber.withUnits(value, - numeratorUnits: otherNumerators, - denominatorUnits: otherDenominators); - } - } else if (otherNumerators.isEmpty) { - if (otherDenominators.isEmpty) { + switch (( + numeratorUnits, + denominatorUnits, + otherNumerators, + otherDenominators + )) { + case (var numerators, var denominators, [], []) || + ([], [], var numerators, var denominators): + case ([], var denominators, var numerators, []) || + (var numerators, [], [], var denominators) + when !_areAnyConvertible(numerators, denominators): return SassNumber.withUnits(value, - numeratorUnits: numeratorUnits, - denominatorUnits: otherDenominators); - } else if (denominatorUnits.isEmpty && - !_areAnyConvertible(numeratorUnits, otherDenominators)) { - return SassNumber.withUnits(value, - numeratorUnits: numeratorUnits, - denominatorUnits: otherDenominators); - } + numeratorUnits: numerators, denominatorUnits: denominators); } var newNumerators = []; @@ -845,53 +835,45 @@ abstract class SassNumber extends Value { /// Returns whether there exists a unit in [units1] that can be converted to a /// unit in [units2]. - bool _areAnyConvertible(List units1, List units2) { - return units1.any((unit1) { - var innerMap = _conversions[unit1]; - if (innerMap == null) return units2.contains(unit1); - return units2.any(innerMap.containsKey); - }); - } + bool _areAnyConvertible(List units1, List units2) => + units1.any((unit1) => switch (_conversions[unit1]) { + var innerMap? => units2.any(innerMap.containsKey), + _ => units2.contains(unit1) + }); /// Returns a human-readable string representation of [numerators] and /// [denominators]. - String _unitString(List numerators, List denominators) { - if (numerators.isEmpty) { - if (denominators.isEmpty) return "no units"; - if (denominators.length == 1) return denominators.single + "^-1"; - return "(${denominators.join('*')})^-1"; - } - - if (denominators.isEmpty) return numerators.join("*"); - - return "${numerators.join("*")}/${denominators.join("*")}"; - } + String _unitString(List numerators, List denominators) => + switch ((numerators, denominators)) { + ([], []) => "no units", + ([], [var denominator]) => "$denominator^-1", + ([], _) => "(${denominators.join('*')})^-1", + (_, []) => numerators.join("*"), + _ => "${numerators.join("*")}/${denominators.join("*")}" + }; bool operator ==(Object other) { - if (other is SassNumber) { - if (numeratorUnits.length != other.numeratorUnits.length || - denominatorUnits.length != other.denominatorUnits.length) { - return false; - } - if (!hasUnits) return fuzzyEquals(value, other.value); - - if (!listEquals(_canonicalizeUnitList(numeratorUnits), - _canonicalizeUnitList(other.numeratorUnits)) || - !listEquals(_canonicalizeUnitList(denominatorUnits), - _canonicalizeUnitList(other.denominatorUnits))) { - return false; - } + if (other is! SassNumber) return false; + if (numeratorUnits.length != other.numeratorUnits.length || + denominatorUnits.length != other.denominatorUnits.length) { + return false; + } + if (!hasUnits) return fuzzyEquals(value, other.value); - return fuzzyEquals( - value * - _canonicalMultiplier(numeratorUnits) / - _canonicalMultiplier(denominatorUnits), - other.value * - _canonicalMultiplier(other.numeratorUnits) / - _canonicalMultiplier(other.denominatorUnits)); - } else { + if (!listEquals(_canonicalizeUnitList(numeratorUnits), + _canonicalizeUnitList(other.numeratorUnits)) || + !listEquals(_canonicalizeUnitList(denominatorUnits), + _canonicalizeUnitList(other.denominatorUnits))) { return false; } + + return fuzzyEquals( + value * + _canonicalMultiplier(numeratorUnits) / + _canonicalMultiplier(denominatorUnits), + other.value * + _canonicalMultiplier(other.numeratorUnits) / + _canonicalMultiplier(other.denominatorUnits)); } int get hashCode => hashCache ??= fuzzyHashCode(value * diff --git a/lib/src/value/number/complex.dart b/lib/src/value/number/complex.dart index adbc362e4..71c143b0d 100644 --- a/lib/src/value/number/complex.dart +++ b/lib/src/value/number/complex.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; import '../../value.dart'; import '../number.dart'; @@ -24,6 +23,7 @@ class ComplexSassNumber extends SassNumber { final List _denominatorUnits; bool get hasUnits => true; + bool get hasComplexUnits => true; ComplexSassNumber( double value, List numeratorUnits, List denominatorUnits) @@ -31,7 +31,7 @@ class ComplexSassNumber extends SassNumber { ComplexSassNumber._( double value, this._numeratorUnits, this._denominatorUnits, - [Tuple2? asSlash]) + [(SassNumber, SassNumber)? asSlash]) : super.protected(value, asSlash) { assert(numeratorUnits.length > 1 || denominatorUnits.isNotEmpty); } @@ -52,6 +52,6 @@ class ComplexSassNumber extends SassNumber { ComplexSassNumber._(value.toDouble(), numeratorUnits, denominatorUnits); SassNumber withSlash(SassNumber numerator, SassNumber denominator) => - ComplexSassNumber._(value, numeratorUnits, denominatorUnits, - Tuple2(numerator, denominator)); + ComplexSassNumber._( + value, numeratorUnits, denominatorUnits, (numerator, denominator)); } diff --git a/lib/src/value/number/single_unit.dart b/lib/src/value/number/single_unit.dart index 7a02e4dfd..9ccea8488 100644 --- a/lib/src/value/number/single_unit.dart +++ b/lib/src/value/number/single_unit.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; import '../../util/number.dart'; import '../../utils.dart'; @@ -49,16 +48,17 @@ class SingleUnitSassNumber extends SassNumber { List get denominatorUnits => const []; bool get hasUnits => true; + bool get hasComplexUnits => false; SingleUnitSassNumber(double value, this._unit, - [Tuple2? asSlash]) + [(SassNumber, SassNumber)? asSlash]) : super.protected(value, asSlash); SassNumber withValue(num value) => SingleUnitSassNumber(value.toDouble(), _unit); SassNumber withSlash(SassNumber numerator, SassNumber denominator) => - SingleUnitSassNumber(value, _unit, Tuple2(numerator, denominator)); + SingleUnitSassNumber(value, _unit, (numerator, denominator)); bool hasUnit(String unit) => unit == _unit; diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index 506ef6d5b..06b54d39b 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; import '../../util/number.dart'; import '../../value.dart'; @@ -18,14 +17,15 @@ class UnitlessSassNumber extends SassNumber { List get denominatorUnits => const []; bool get hasUnits => false; + bool get hasComplexUnits => false; - UnitlessSassNumber(double value, [Tuple2? asSlash]) + UnitlessSassNumber(double value, [(SassNumber, SassNumber)? asSlash]) : super.protected(value, asSlash); SassNumber withValue(num value) => UnitlessSassNumber(value.toDouble()); SassNumber withSlash(SassNumber numerator, SassNumber denominator) => - UnitlessSassNumber(value, Tuple2(numerator, denominator)); + UnitlessSassNumber(value, (numerator, denominator)); bool hasUnit(String unit) => false; diff --git a/lib/src/value/string.dart b/lib/src/value/string.dart index 2ded47bd7..a6965bbf1 100644 --- a/lib/src/value/string.dart +++ b/lib/src/value/string.dart @@ -70,43 +70,32 @@ class SassString extends Value { if (hasQuotes) return false; if (text.length < "min(_)".length) return false; - var first = text.codeUnitAt(0); - if (equalsLetterIgnoreCase($c, first)) { - var second = text.codeUnitAt(1); - if (equalsLetterIgnoreCase($l, second)) { - if (!equalsLetterIgnoreCase($a, text.codeUnitAt(2))) return false; - if (!equalsLetterIgnoreCase($m, text.codeUnitAt(3))) return false; - if (!equalsLetterIgnoreCase($p, text.codeUnitAt(4))) return false; - return text.codeUnitAt(5) == $lparen; - } else if (equalsLetterIgnoreCase($a, second)) { - if (!equalsLetterIgnoreCase($l, text.codeUnitAt(2))) return false; - if (!equalsLetterIgnoreCase($c, text.codeUnitAt(3))) return false; - return text.codeUnitAt(4) == $lparen; - } else { - return false; - } - } else if (equalsLetterIgnoreCase($v, first)) { - if (!equalsLetterIgnoreCase($a, text.codeUnitAt(1))) return false; - if (!equalsLetterIgnoreCase($r, text.codeUnitAt(2))) return false; - return text.codeUnitAt(3) == $lparen; - } else if (equalsLetterIgnoreCase($e, first)) { - if (!equalsLetterIgnoreCase($n, text.codeUnitAt(1))) return false; - if (!equalsLetterIgnoreCase($v, text.codeUnitAt(2))) return false; - return text.codeUnitAt(3) == $lparen; - } else if (equalsLetterIgnoreCase($m, first)) { - var second = text.codeUnitAt(1); - if (equalsLetterIgnoreCase($a, second)) { - if (!equalsLetterIgnoreCase($x, text.codeUnitAt(2))) return false; - return text.codeUnitAt(3) == $lparen; - } else if (equalsLetterIgnoreCase($i, second)) { - if (!equalsLetterIgnoreCase($n, text.codeUnitAt(2))) return false; - return text.codeUnitAt(3) == $lparen; - } else { - return false; - } - } else { - return false; - } + return switch (text.codeUnitAt(0)) { + $c || $C => switch (text.codeUnitAt(1)) { + $l || $L => equalsLetterIgnoreCase($a, text.codeUnitAt(2)) && + equalsLetterIgnoreCase($m, text.codeUnitAt(3)) && + equalsLetterIgnoreCase($p, text.codeUnitAt(4)) && + text.codeUnitAt(5) == $lparen, + $a || $A => equalsLetterIgnoreCase($l, text.codeUnitAt(2)) && + equalsLetterIgnoreCase($c, text.codeUnitAt(3)) && + text.codeUnitAt(4) == $lparen, + _ => false + }, + $v || $V => equalsLetterIgnoreCase($a, text.codeUnitAt(1)) && + equalsLetterIgnoreCase($r, text.codeUnitAt(2)) && + text.codeUnitAt(3) == $lparen, + $e || $E => equalsLetterIgnoreCase($n, text.codeUnitAt(1)) && + equalsLetterIgnoreCase($v, text.codeUnitAt(2)) && + text.codeUnitAt(3) == $lparen, + $m || $M => switch (text.codeUnitAt(1)) { + $a || $A => equalsLetterIgnoreCase($x, text.codeUnitAt(2)) && + text.codeUnitAt(3) == $lparen, + $i || $I => equalsLetterIgnoreCase($n, text.codeUnitAt(2)) && + text.codeUnitAt(3) == $lparen, + _ => false + }, + _ => false + }; } /// @nodoc @@ -189,13 +178,9 @@ class SassString extends Value { /// @nodoc @internal - Value plus(Value other) { - if (other is SassString) { - return SassString(text + other.text, quotes: hasQuotes); - } else { - return SassString(text + other.toCssString(), quotes: hasQuotes); - } - } + Value plus(Value other) => other is SassString + ? SassString(text + other.text, quotes: hasQuotes) + : SassString(text + other.toCssString(), quotes: hasQuotes); bool operator ==(Object other) => other is SassString && text == other.text; diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 802987eba..3e8dabcd3 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -11,7 +11,6 @@ import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; -import 'package:tuple/tuple.dart'; import '../ast/css.dart'; import '../ast/css/modifiable.dart'; @@ -41,6 +40,7 @@ import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; import '../syntax.dart'; import '../utils.dart'; +import '../util/map.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; import '../util/span.dart'; @@ -95,7 +95,7 @@ Future evaluateAsync(Stylesheet stylesheet, /// A class that can evaluate multiple independent statements and expressions /// in the context of a single module. -class AsyncEvaluator { +final class AsyncEvaluator { /// The visitor that evaluates each expression and statement. final _EvaluateVisitor _visitor; @@ -124,7 +124,7 @@ class AsyncEvaluator { } /// A visitor that executes Sass code to produce a CSS tree. -class _EvaluateVisitor +final class _EvaluateVisitor implements StatementVisitor>, ExpressionVisitor>, @@ -164,7 +164,7 @@ class _EvaluateVisitor /// /// We only want to emit one warning per location, to avoid blowing up users' /// consoles with redundant warnings. - final _warningsEmitted = >{}; + final _warningsEmitted = <(String, SourceSpan)>{}; /// Whether to avoid emitting warnings for files loaded from dependencies. final bool _quietDeps; @@ -257,13 +257,13 @@ class _EvaluateVisitor /// The dynamic call stack representing function invocations, mixin /// invocations, and imports surrounding the current context. /// - /// Each member is a tuple of the span where the stack trace starts and the + /// Each member is a pair of the span where the stack trace starts and the /// name of the member being invoked. /// /// This stores [AstNode]s rather than [FileSpan]s 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. - final _stack = >[]; + final _stack = <(String, AstNode)>[]; /// Whether we're running in Node Sass-compatibility mode. bool get _asNodeSass => _nodeImporter != null; @@ -399,8 +399,8 @@ class _EvaluateVisitor } return SassMap({ - for (var entry in module.variables.entries) - SassString(entry.key): entry.value + for (var (name, value) in module.variables.pairs) + SassString(name): value }); }, url: "sass:meta"), @@ -412,8 +412,8 @@ class _EvaluateVisitor } return SassMap({ - for (var entry in module.functions.entries) - SassString(entry.key): SassFunction(entry.value) + for (var (name, value) in module.functions.pairs) + SassString(name): SassFunction(value) }); }, url: "sass:meta"), @@ -423,19 +423,20 @@ class _EvaluateVisitor var css = arguments[1].isTruthy; var module = arguments[2].realNull?.assertString("module"); - if (css && module != null) { - throw r"$css and $module may not both be passed at once."; + if (css) { + if (module != null) { + throw r"$css and $module may not both be passed at once."; + } + return SassFunction(PlainCssCallable(name.text)); } - var callable = css - ? PlainCssCallable(name.text) - : _addExceptionSpan( - _callableNode!, - () => _getFunction(name.text.replaceAll("_", "-"), - namespace: module?.text)); - if (callable != null) return SassFunction(callable); + var callable = _addExceptionSpan( + _callableNode!, + () => _getFunction(name.text.replaceAll("_", "-"), + namespace: module?.text)); + if (callable == null) throw "Function not found: $name"; - throw "Function not found: $name"; + return SassFunction(callable); }, url: "sass:meta"), AsyncBuiltInCallable.function("call", r"$function, $args...", @@ -450,8 +451,8 @@ class _EvaluateVisitor ? null : ValueExpression( SassMap({ - for (var entry in args.keywords.entries) - SassString(entry.key, quotes: false): entry.value + for (var (name, value) in args.keywords.pairs) + SassString(name, quotes: false): value }), callableNode.span)); @@ -532,18 +533,16 @@ class _EvaluateVisitor try { return await withEvaluationContext(_EvaluationContext(this, node), () async { - var url = node.span.sourceUrl; - if (url != null) { + if (node.span.sourceUrl case var url?) { _activeModules[url] = null; if (!(_asNodeSass && url.toString() == 'stdin')) _loadedUrls.add(url); } var module = await _addExceptionTrace(() => _execute(importer, node)); - - return EvaluateResult(_combineCss(module), _loadedUrls); + return (stylesheet: _combineCss(module), loadedUrls: _loadedUrls); }); } on SassException catch (error, stackTrace) { - throwWithTrace(error.withLoadedUrls(_loadedUrls), stackTrace); + throwWithTrace(error.withLoadedUrls(_loadedUrls), error, stackTrace); } } @@ -610,8 +609,7 @@ class _EvaluateVisitor {Uri? baseUrl, Configuration? configuration, bool namesInErrors = false}) async { - var builtInModule = _builtInModules[url]; - if (builtInModule != null) { + if (_builtInModules[url] case var builtInModule?) { if (configuration is ExplicitConfiguration) { throw _exception( namesInErrors @@ -628,30 +626,33 @@ class _EvaluateVisitor } await _withStackFrame(stackFrame, nodeWithSpan, () async { - var result = await _loadStylesheet(url.toString(), nodeWithSpan.span, + var (stylesheet, :importer, :isDependency) = await _loadStylesheet( + url.toString(), nodeWithSpan.span, baseUrl: baseUrl); - var stylesheet = result.stylesheet; var canonicalUrl = stylesheet.span.sourceUrl; - if (canonicalUrl != null && _activeModules.containsKey(canonicalUrl)) { - var message = namesInErrors - ? "Module loop: ${p.prettyUri(canonicalUrl)} is already being " - "loaded." - : "Module loop: this module is already being loaded."; - - throw _activeModules[canonicalUrl].andThen((previousLoad) => - _multiSpanException(message, "new load", - {previousLoad.span: "original load"})) ?? - _exception(message); + if (canonicalUrl != null) { + if (_activeModules.containsKey(canonicalUrl)) { + var message = namesInErrors + ? "Module loop: ${p.prettyUri(canonicalUrl)} is already being " + "loaded." + : "Module loop: this module is already being loaded."; + + throw _activeModules[canonicalUrl].andThen((previousLoad) => + _multiSpanException(message, "new load", + {previousLoad.span: "original load"})) ?? + _exception(message); + } else { + _activeModules[canonicalUrl] = nodeWithSpan; + } } - if (canonicalUrl != null) _activeModules[canonicalUrl] = nodeWithSpan; var firstLoad = !_modules.containsKey(canonicalUrl); var oldInDependency = _inDependency; - _inDependency = result.isDependency; + _inDependency = isDependency; Module module; try { - module = await _execute(result.importer, stylesheet, + module = await _execute(importer, stylesheet, configuration: configuration, nodeWithSpan: nodeWithSpan, namesInErrors: namesInErrors); @@ -680,8 +681,7 @@ class _EvaluateVisitor bool namesInErrors = false}) async { var url = stylesheet.span.sourceUrl; - var alreadyLoaded = _modules[url]; - if (alreadyLoaded != null) { + if (_modules[url] case var alreadyLoaded?) { var currentConfiguration = configuration ?? _configuration; if (!_moduleConfigurations[url]!.sameOriginal(currentConfiguration) && currentConfiguration is ExplicitConfiguration) { @@ -779,16 +779,15 @@ class _EvaluateVisitor /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted /// after [_endOfImports], if necessary. - List _addOutOfOrderImports() { - var outOfOrderImports = _outOfOrderImports; - if (outOfOrderImports == null) return _root.children; - - return [ - ..._root.children.take(_endOfImports), - ...outOfOrderImports, - ..._root.children.skip(_endOfImports) - ]; - } + List _addOutOfOrderImports() => + switch (_outOfOrderImports) { + null => _root.children, + var outOfOrderImports => [ + ..._root.children.take(_endOfImports), + ...outOfOrderImports, + ..._root.children.skip(_endOfImports) + ] + }; /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all /// modules transitively used by [root]. @@ -800,9 +799,10 @@ class _EvaluateVisitor CssStylesheet _combineCss(Module root, {bool clone = false}) { if (!root.upstream.any((module) => module.transitivelyContainsCss)) { var selectors = root.extensionStore.simpleSelectors; - var unsatisfiedExtension = firstOrNull(root.extensionStore - .extensionsWhereTarget((target) => !selectors.contains(target))); - if (unsatisfiedExtension != null) { + if (root.extensionStore + .extensionsWhereTarget((target) => !selectors.contains(target)) + .firstOrNull + case var unsatisfiedExtension?) { _throwForUnsatisfiedExtension(unsatisfiedExtension); } @@ -829,8 +829,7 @@ class _EvaluateVisitor for (var upstream in module.upstream) { if (upstream.transitivelyContainsCss) { - var comments = module.preModuleComments[upstream]; - if (comments != null) { + if (module.preModuleComments[upstream] case var comments?) { // Intermix the top-level comments with plain CSS `@import`s until we // start to have actual CSS defined, at which point start treating it as // normal CSS. @@ -887,11 +886,11 @@ class _EvaluateVisitor if (module.extensionStore.isEmpty) continue; for (var upstream in module.upstream) { - var url = upstream.url; - if (url == null) continue; - downstreamExtensionStores - .putIfAbsent(url, () => []) - .add(module.extensionStore); + if (upstream.url case var url?) { + downstreamExtensionStores + .putIfAbsent(url, () => []) + .add(module.extensionStore); + } } // Remove all extensions that are now satisfied after adding downstream @@ -918,12 +917,15 @@ class _EvaluateVisitor /// static imports. int _indexAfterImports(List statements) { var lastImport = -1; + loop: for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement is CssImport) { - lastImport = i; - } else if (statement is! CssComment) { - break; + switch (statements[i]) { + case CssImport(): + lastImport = i; + case CssComment(): + continue loop; + case _: + break loop; } } return lastImport + 1; @@ -940,12 +942,9 @@ class _EvaluateVisitor Future visitAtRootRule(AtRootRule node) async { var query = AtRootQuery.defaultQuery; - var unparsedQuery = node.query; - if (unparsedQuery != null) { - var tuple = + if (node.query case var unparsedQuery?) { + var (resolved, map) = await _performInterpolationWithMap(unparsedQuery, warnForColor: true); - var resolved = tuple.item1; - var map = tuple.item2; query = AtRootQuery.parse(resolved, interpolationMap: map, logger: _logger); } @@ -955,13 +954,12 @@ class _EvaluateVisitor while (parent is! CssStylesheet) { if (!query.excludes(parent)) included.add(parent); - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw StateError( "CssNodes must have a CssStylesheet transitive parent node."); } - - parent = grandparent; } var root = _trimIncluded(included); @@ -977,10 +975,10 @@ class _EvaluateVisitor } var innerCopy = root; - if (included.isNotEmpty) { - innerCopy = included.first.copyWithoutChildren(); + if (included case [var first, ...var rest]) { + innerCopy = first.copyWithoutChildren(); var outerCopy = innerCopy; - for (var node in included.skip(1)) { + for (var node in rest) { var copy = node.copyWithoutChildren(); copy.addChild(outerCopy); outerCopy = copy; @@ -1017,21 +1015,20 @@ class _EvaluateVisitor while (parent != nodes[i]) { innermostContiguous = null; - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw ArgumentError( "Expected ${nodes[i]} to be an ancestor of $this."); } - - parent = grandparent; } innermostContiguous ??= i; - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw ArgumentError("Expected ${nodes[i]} to be an ancestor of $this."); } - parent = grandparent; } if (parent != _root) return _root; @@ -1134,27 +1131,27 @@ class _EvaluateVisitor } var name = await _interpolationToValue(node.name, warnForColor: true); - if (_declarationName != null) { - name = CssValue("$_declarationName-${name.value}", name.span); - } - var cssValue = await node.value.andThen( - (value) async => CssValue(await value.accept(this), value.span)); - - // If the value is an empty list, preserve it, because converting it to CSS - // will throw an error that we want the user to see. - if (cssValue != null && - (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { - _parent.addChild(ModifiableCssDeclaration(name, cssValue, node.span, - parsedAsCustomProperty: node.isCustomProperty, - valueSpanForMap: - _sourceMap ? node.value.andThen(_expressionNode)?.span : null)); - } else if (name.value.startsWith('--') && cssValue != null) { - throw _exception( - "Custom property values may not be empty.", cssValue.span); + if (_declarationName case var declarationName?) { + name = CssValue("$declarationName-${name.value}", name.span); + } + + if (node.value case var expression?) { + var value = await expression.accept(this); + // If the value is an empty list, preserve it, because converting it to CSS + // will throw an error that we want the user to see. + if (!value.isBlank || _isEmptyList(value)) { + _parent.addChild(ModifiableCssDeclaration( + name, CssValue(value, expression.span), node.span, + parsedAsCustomProperty: node.isCustomProperty, + valueSpanForMap: + _sourceMap ? node.value.andThen(_expressionNode)?.span : null)); + } else if (name.value.startsWith('--')) { + throw _exception( + "Custom property values may not be empty.", expression.span); + } } - var children = node.children; - if (children != null) { + if (node.children case var children?) { var oldDeclarationName = _declarationName; _declarationName = name.value; await _environment.scope(() async { @@ -1174,11 +1171,12 @@ class _EvaluateVisitor Future visitEachRule(EachRule node) async { var list = await node.list.accept(this); var nodeWithSpan = _expressionNode(node.list); - var setVariables = node.variables.length == 1 - ? (Value value) => _environment.setLocalVariable(node.variables.first, - _withoutSlash(value, nodeWithSpan), nodeWithSpan) - : (Value value) => - _setMultipleVariables(node.variables, value, nodeWithSpan); + var setVariables = switch (node.variables) { + [var variable] => (Value value) => _environment.setLocalVariable( + variable, _withoutSlash(value, nodeWithSpan), nodeWithSpan), + var variables => (Value value) => + _setMultipleVariables(variables, value, nodeWithSpan) + }; return _environment.scope(() { return _handleReturn(list.asList, (element) { setVariables(element); @@ -1229,10 +1227,8 @@ class _EvaluateVisitor Deprecation.bogusCombinators); } - var tuple = + var (targetText, targetMap) = await _performInterpolationWithMap(node.selector, warnForColor: true); - var targetText = tuple.item1; - var targetMap = tuple.item2; var list = SelectorList.parse(trimAscii(targetText, excludeEscape: true), interpolationMap: targetMap, logger: _logger, allowParent: false); @@ -1346,9 +1342,11 @@ class _EvaluateVisitor numeratorUnits: fromNumber.numeratorUnits, denominatorUnits: fromNumber.denominatorUnits), nodeWithSpan); - var result = await _handleReturn( - node.children, (child) => child.accept(this)); - if (result != null) return result; + if (await _handleReturn( + node.children, (child) => child.accept(this)) + case var result?) { + return result; + } } return null; }, semiGlobal: true); @@ -1404,8 +1402,8 @@ class _EvaluateVisitor var newValues = Map.of(configuration.values); for (var variable in node.configuration) { if (variable.isGuarded) { - var oldValue = configuration.remove(variable.name); - if (oldValue != null && oldValue.value != sassNull) { + if (configuration.remove(variable.name) case var oldValue? + when oldValue.value != sassNull) { newValues[variable.name] = oldValue; continue; } @@ -1466,14 +1464,14 @@ class _EvaluateVisitor if (configuration is! ExplicitConfiguration) return; if (configuration.isEmpty) return; - var entry = configuration.values.entries.first; + var (name, value) = configuration.values.pairs.first; throw _exception( nameInError - ? "\$${entry.key} was not declared with !default in the @used " + ? "\$$name was not declared with !default in the @used " "module." : "This variable was not declared with !default in the @used " "module.", - entry.value.configurationSpan); + value.configurationSpan); } Future visitFunctionRule(FunctionRule node) async { @@ -1490,14 +1488,12 @@ class _EvaluateVisitor break; } } - if (clause == null) return null; - return await _environment.scope( + return await clause.andThen>((clause) => _environment.scope( () => _handleReturn( - clause!.children, // dart-lang/sdk#45348 - (child) => child.accept(this)), + clause.children, (child) => child.accept(this)), semiGlobal: true, - when: clause.hasDeclarations); + when: clause.hasDeclarations)); } Future visitImportRule(ImportRule node) async { @@ -1514,9 +1510,8 @@ class _EvaluateVisitor /// Adds the stylesheet imported by [import] to the current document. Future _visitDynamicImport(DynamicImport import) { return _withStackFrame("@import", import, () async { - var result = + var (stylesheet, :importer, :isDependency) = await _loadStylesheet(import.urlString, import.span, forImport: true); - var stylesheet = result.stylesheet; var url = stylesheet.span.sourceUrl; if (url != null) { @@ -1537,9 +1532,9 @@ class _EvaluateVisitor var oldImporter = _importer; var oldStylesheet = _stylesheet; var oldInDependency = _inDependency; - _importer = result.importer; + _importer = importer; _stylesheet = stylesheet; - _inDependency = result.isDependency; + _inDependency = isDependency; await visitStylesheet(stylesheet); _importer = oldImporter; _stylesheet = oldStylesheet; @@ -1568,7 +1563,7 @@ class _EvaluateVisitor var oldOutOfOrderImports = _outOfOrderImports; var oldConfiguration = _configuration; var oldInDependency = _inDependency; - _importer = result.importer; + _importer = importer; _stylesheet = stylesheet; if (loadsUserDefinedModules) { _root = ModifiableCssStylesheet(stylesheet.span); @@ -1576,7 +1571,7 @@ class _EvaluateVisitor _endOfImports = 0; _outOfOrderImports = null; } - _inDependency = result.isDependency; + _inDependency = isDependency; // This configuration is only used if it passes through a `@forward` // rule, so we avoid creating unnecessary ones for performance reasons. @@ -1636,33 +1631,28 @@ class _EvaluateVisitor assert(_importSpan == null); _importSpan = span; - var importCache = _importCache; - if (importCache != null) { - var parsedUrl = Uri.parse(url); + if (_importCache case var importCache?) { baseUrl ??= _stylesheet.span.sourceUrl; - var tuple = await importCache.canonicalize(parsedUrl, - baseImporter: _importer, baseUrl: baseUrl, forImport: forImport); - - if (tuple != null) { + if (await importCache.canonicalize(Uri.parse(url), + baseImporter: _importer, baseUrl: baseUrl, forImport: forImport) + case (var importer, var canonicalUrl, :var originalUrl)) { // Make sure we record the canonical URL as "loaded" even if the // actual load fails, because watchers should watch it to see if it // changes in a way that allows the load to succeed. - _loadedUrls.add(tuple.item2); - - var isDependency = _inDependency || tuple.item1 != _importer; - var stylesheet = await importCache.importCanonical( - tuple.item1, tuple.item2, - originalUrl: tuple.item3, quiet: _quietDeps && isDependency); - if (stylesheet != null) { - return _LoadedStylesheet(stylesheet, - importer: tuple.item1, isDependency: isDependency); + _loadedUrls.add(canonicalUrl); + + var isDependency = _inDependency || importer != _importer; + if (await importCache.importCanonical(importer, canonicalUrl, + originalUrl: originalUrl, quiet: _quietDeps && isDependency) + case var stylesheet?) { + return (stylesheet, importer: importer, isDependency: isDependency); } } } else { - var result = await _importLikeNode( - url, baseUrl ?? _stylesheet.span.sourceUrl, forImport); - if (result != null) { - result.stylesheet.span.sourceUrl.andThen(_loadedUrls.add); + if (await _importLikeNode( + url, baseUrl ?? _stylesheet.span.sourceUrl, forImport) + case var result?) { + result.$1.span.sourceUrl.andThen(_loadedUrls.add); return result; } } @@ -1677,7 +1667,7 @@ class _EvaluateVisitor } on SassException { rethrow; } on ArgumentError catch (error, stackTrace) { - throwWithTrace(_exception(error.toString()), stackTrace); + throwWithTrace(_exception(error.toString()), error, stackTrace); } catch (error, stackTrace) { String? message; try { @@ -1685,7 +1675,7 @@ class _EvaluateVisitor } catch (_) { message = error.toString(); } - throwWithTrace(_exception(message), stackTrace); + throwWithTrace(_exception(message), error, stackTrace); } finally { _importSpan = null; } @@ -1707,15 +1697,15 @@ class _EvaluateVisitor isDependency = true; } - var contents = result.item1; - var url = result.item2; - - return _LoadedStylesheet( - Stylesheet.parse(contents, - url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, - url: url, - logger: _quietDeps && isDependency ? Logger.quiet : _logger), - isDependency: isDependency); + var (contents, url) = result; + return ( + Stylesheet.parse( + contents, url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, + url: url, + logger: _quietDeps && isDependency ? Logger.quiet : _logger), + importer: null, + isDependency: isDependency + ); } /// Adds a CSS import for [import]. @@ -1739,45 +1729,47 @@ class _EvaluateVisitor } Future visitIncludeRule(IncludeRule node) async { + var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent); var mixin = _addExceptionSpan(node, () => _environment.getMixin(node.name, namespace: node.namespace)); - if (mixin == null) { - throw _exception("Undefined mixin.", node.span); - } + switch (mixin) { + case null: + throw _exception("Undefined mixin.", node.span); - var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent); - if (mixin is AsyncBuiltInCallable) { - if (node.content != null) { + case AsyncBuiltInCallable() when node.content != null: throw _exception("Mixin doesn't accept a content block.", node.span); - } - await _runBuiltInCallable(node.arguments, mixin, nodeWithSpan); - } else if (mixin is UserDefinedCallable) { - if (node.content != null && - !(mixin.declaration as MixinRule).hasContent) { + case AsyncBuiltInCallable(): + await _runBuiltInCallable(node.arguments, mixin, nodeWithSpan); + + case UserDefinedCallable( + declaration: MixinRule(hasContent: false) + ) + when node.content != null: throw MultiSpanSassRuntimeException( "Mixin doesn't accept a content block.", node.spanWithoutContent, "invocation", {mixin.declaration.arguments.spanWithName: "declaration"}, _stackTrace(node.spanWithoutContent)); - } - var contentCallable = node.content.andThen((content) => - UserDefinedCallable(content, _environment.closure(), - inDependency: _inDependency)); - await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, - () async { - await _environment.withContent(contentCallable, () async { - await _environment.asMixin(() async { - for (var statement in mixin.declaration.children) { - await _addErrorSpan(nodeWithSpan, () => statement.accept(this)); - } + case UserDefinedCallable(): + var contentCallable = node.content.andThen((content) => + UserDefinedCallable(content, _environment.closure(), + inDependency: _inDependency)); + await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, + () async { + await _environment.withContent(contentCallable, () async { + await _environment.asMixin(() async { + for (var statement in mixin.declaration.children) { + await _addErrorSpan(nodeWithSpan, () => statement.accept(this)); + } + }); }); }); - }); - } else { - throw UnsupportedError("Unknown callable type $mixin."); + + case _: + throw UnsupportedError("Unknown callable type $mixin."); } return null; @@ -1827,12 +1819,7 @@ class _EvaluateVisitor ModifiableCssMediaRule(mergedQueries ?? queries, node.span), () async { await _withMediaQueries(mergedQueries ?? queries, mergedSources, () async { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - await child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the media query so that // declarations immediately inside @media have somewhere to go. // @@ -1843,6 +1830,10 @@ class _EvaluateVisitor await child.accept(this); } }, scopeWhen: false); + } else { + for (var child in node.children) { + await child.accept(this); + } } }); }, @@ -1860,10 +1851,8 @@ class _EvaluateVisitor /// queries. Future> _visitMediaQueries( Interpolation interpolation) async { - var tuple = + var (resolved, map) = await _performInterpolationWithMap(interpolation, warnForColor: true); - var resolved = tuple.item1; - var map = tuple.item2; return CssMediaQuery.parseList(resolved, logger: _logger, interpolationMap: map); } @@ -1878,11 +1867,16 @@ class _EvaluateVisitor Iterable queries1, Iterable queries2) { var queries = []; for (var query1 in queries1) { + inner: for (var query2 in queries2) { - var result = query1.merge(query2); - if (result == MediaQueryMergeResult.empty) continue; - if (result == MediaQueryMergeResult.unrepresentable) return null; - queries.add((result as MediaQuerySuccessfulMergeResult).query); + switch (query1.merge(query2)) { + case MediaQueryMergeResult.empty: + continue inner; + case MediaQueryMergeResult.unrepresentable: + return null; + case MediaQuerySuccessfulMergeResult result: + queries.add(result.query); + } } } return queries; @@ -1902,10 +1896,8 @@ class _EvaluateVisitor "Style rules may not be used within nested declarations.", node.span); } - var tuple = + var (selectorText, selectorMap) = 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 @@ -2017,12 +2009,7 @@ class _EvaluateVisitor await _visitSupportsCondition(node.condition), node.condition.span); await _withParent(ModifiableCssSupportsRule(condition, node.span), () async { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - await child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the supports rule so that // declarations immediately inside @supports have somewhere to go. // @@ -2033,6 +2020,10 @@ class _EvaluateVisitor await child.accept(this); } }); + } else { + for (var child in node.children) { + await child.accept(this); + } } }, through: (node) => node is CssStyleRule, @@ -2042,31 +2033,37 @@ class _EvaluateVisitor } /// Evaluates [condition] and converts it to a plain CSS string. - Future _visitSupportsCondition(SupportsCondition condition) async { - if (condition is SupportsOperation) { - return "${await _parenthesize(condition.left, condition.operator)} " - "${condition.operator} " - "${await _parenthesize(condition.right, condition.operator)}"; - } else if (condition is SupportsNegation) { - return "not ${await _parenthesize(condition.condition)}"; - } else if (condition is SupportsInterpolation) { - return await _evaluateToCss(condition.expression, quote: false); - } else if (condition is SupportsDeclaration) { - var oldInSupportsDeclaration = _inSupportsDeclaration; - _inSupportsDeclaration = true; - var result = "(${await _evaluateToCss(condition.name)}:" - "${condition.isCustomProperty ? '' : ' '}" - "${await _evaluateToCss(condition.value)})"; + Future _visitSupportsCondition(SupportsCondition condition) async => + switch (condition) { + SupportsOperation operation => + "${await _parenthesize(operation.left, operation.operator)} " + "${operation.operator} " + "${await _parenthesize(operation.right, operation.operator)}", + SupportsNegation negation => + "not ${await _parenthesize(negation.condition)}", + SupportsInterpolation interpolation => + await _evaluateToCss(interpolation.expression, quote: false), + SupportsDeclaration declaration => await _withSupportsDeclaration( + () async => "(${await _evaluateToCss(declaration.name)}:" + "${declaration.isCustomProperty ? '' : ' '}" + "${await _evaluateToCss(declaration.value)})"), + SupportsFunction function => + "${await _performInterpolation(function.name)}(" + "${await _performInterpolation(function.arguments)})", + SupportsAnything anything => + "(${await _performInterpolation(anything.contents)})", + var condition => throw ArgumentError( + "Unknown supports condition type ${condition.runtimeType}.") + }; + + /// Runs [callback] in a context where [_inSupportsDeclaration] is true. + Future _withSupportsDeclaration(FutureOr callback()) async { + var oldInSupportsDeclaration = _inSupportsDeclaration; + _inSupportsDeclaration = true; + try { + return await callback(); + } finally { _inSupportsDeclaration = oldInSupportsDeclaration; - return result; - } else if (condition is SupportsFunction) { - return "${await _performInterpolation(condition.name)}(" - "${await _performInterpolation(condition.arguments)})"; - } else if (condition is SupportsAnything) { - return "(${await _performInterpolation(condition.contents)})"; - } else { - throw ArgumentError( - "Unknown supports condition type ${condition.runtimeType}."); } } @@ -2078,20 +2075,22 @@ class _EvaluateVisitor /// necessary if [condition] is also a [SupportsOperation]. Future _parenthesize(SupportsCondition condition, [String? operator]) async { - if ((condition is SupportsNegation) || - (condition is SupportsOperation && - (operator == null || operator != condition.operator))) { - return "(${await _visitSupportsCondition(condition)})"; - } else { - return await _visitSupportsCondition(condition); + switch (condition) { + case SupportsNegation(): + case SupportsOperation() + when operator == null || operator != condition.operator: + return "(${await _visitSupportsCondition(condition)})"; + + case _: + return await _visitSupportsCondition(condition); } } Future visitVariableDeclaration(VariableDeclaration node) async { if (node.isGuarded) { if (node.namespace == null && _environment.atRoot) { - var override = _configuration.remove(node.name); - if (override != null && override.value != sassNull) { + if (_configuration.remove(node.name) case var override? + when override.value != sassNull) { _addExceptionSpan(node, () { _environment.setVariable( node.name, override.value, override.assignmentNode, @@ -2170,9 +2169,11 @@ class _EvaluateVisitor Future visitWhileRule(WhileRule node) { return _environment.scope(() async { while ((await node.condition.accept(this)).isTruthy) { - var result = await _handleReturn( - node.children, (child) => child.accept(this)); - if (result != null) return result; + if (await _handleReturn( + node.children, (child) => child.accept(this)) + case var result?) { + return result; + } } return null; }, semiGlobal: true, when: node.hasDeclarations); @@ -2183,96 +2184,70 @@ class _EvaluateVisitor Future visitBinaryOperationExpression(BinaryOperationExpression node) { return _addExceptionSpanAsync(node, () async { var left = await node.left.accept(this); - switch (node.operator) { - case BinaryOperator.singleEquals: - var right = await node.right.accept(this); - return left.singleEquals(right); - - case BinaryOperator.or: - return left.isTruthy ? left : await node.right.accept(this); - - case BinaryOperator.and: - return left.isTruthy ? await node.right.accept(this) : left; - - case BinaryOperator.equals: - var right = await node.right.accept(this); - return SassBoolean(left == right); - - case BinaryOperator.notEquals: - var right = await node.right.accept(this); - return SassBoolean(left != right); - - case BinaryOperator.greaterThan: - var right = await node.right.accept(this); - return left.greaterThan(right); - - case BinaryOperator.greaterThanOrEquals: - var right = await node.right.accept(this); - return left.greaterThanOrEquals(right); - - case BinaryOperator.lessThan: - var right = await node.right.accept(this); - return left.lessThan(right); - - case BinaryOperator.lessThanOrEquals: - var right = await node.right.accept(this); - return left.lessThanOrEquals(right); - - case BinaryOperator.plus: - var right = await node.right.accept(this); - return left.plus(right); - - case BinaryOperator.minus: - var right = await node.right.accept(this); - return left.minus(right); - - case BinaryOperator.times: - var right = await node.right.accept(this); - return left.times(right); - - case BinaryOperator.dividedBy: - var right = await node.right.accept(this); - var result = left.dividedBy(right); - if (node.allowsSlash && left is SassNumber && right is SassNumber) { - return (result as SassNumber).withSlash(left, right); - } else { - if (left is SassNumber && right is SassNumber) { - String recommendation(Expression expression) { - if (expression is BinaryOperationExpression && - expression.operator == BinaryOperator.dividedBy) { - return "math.div(${recommendation(expression.left)}, " - "${recommendation(expression.right)})"; - } else if (expression is ParenthesizedExpression) { - return expression.expression.toString(); - } else { - return expression.toString(); - } - } - - _warn( - "Using / for division outside of calc() is deprecated " - "and will be removed in Dart Sass 2.0.0.\n" - "\n" - "Recommendation: ${recommendation(node)} or " - "${expressionToCalc(node)}\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/slash-div", - node.span, - Deprecation.slashDiv); - } + return switch (node.operator) { + BinaryOperator.singleEquals => + left.singleEquals(await node.right.accept(this)), + BinaryOperator.or => + left.isTruthy ? left : await node.right.accept(this), + BinaryOperator.and => + left.isTruthy ? await node.right.accept(this) : left, + BinaryOperator.equals => + SassBoolean(left == await node.right.accept(this)), + BinaryOperator.notEquals => + SassBoolean(left != await node.right.accept(this)), + BinaryOperator.greaterThan => + left.greaterThan(await node.right.accept(this)), + BinaryOperator.greaterThanOrEquals => + left.greaterThanOrEquals(await node.right.accept(this)), + BinaryOperator.lessThan => left.lessThan(await node.right.accept(this)), + BinaryOperator.lessThanOrEquals => + left.lessThanOrEquals(await node.right.accept(this)), + BinaryOperator.plus => left.plus(await node.right.accept(this)), + BinaryOperator.minus => left.minus(await node.right.accept(this)), + BinaryOperator.times => left.times(await node.right.accept(this)), + BinaryOperator.dividedBy => + _slash(left, await node.right.accept(this), node), + BinaryOperator.modulo => left.modulo(await node.right.accept(this)) + }; + }); + } - return result; - } + /// Returns the result of the SassScript `/` operation between [left] and + /// [right] in [node]. + Value _slash(Value left, Value right, BinaryOperationExpression node) { + var result = left.dividedBy(right); + switch ((left, right)) { + case (SassNumber left, SassNumber right) when node.allowsSlash: + return (result as SassNumber).withSlash(left, right); + + case (SassNumber(), SassNumber()): + String recommendation(Expression expression) => switch (expression) { + BinaryOperationExpression( + operator: BinaryOperator.dividedBy, + :var left, + :var right + ) => + "math.div(${recommendation(left)}, ${recommendation(right)})", + ParenthesizedExpression() => expression.expression.toString(), + _ => expression.toString() + }; - case BinaryOperator.modulo: - var right = await node.right.accept(this); - return left.modulo(right); + _warn( + "Using / for division outside of calc() is deprecated " + "and will be removed in Dart Sass 2.0.0.\n" + "\n" + "Recommendation: ${recommendation(node)} or " + "${expressionToCalc(node)}\n" + "\n" + "More info and automated migrator: " + "https://sass-lang.com/d/slash-div", + node.span, + Deprecation.slashDiv); + return result; - default: - throw ArgumentError("Unknown binary operator ${node.operator}."); - } - }); + case _: + return result; + } } Future visitValueExpression(ValueExpression node) async => node.value; @@ -2288,18 +2263,12 @@ class _EvaluateVisitor UnaryOperationExpression node) async { var operand = await node.operand.accept(this); return _addExceptionSpan(node, () { - switch (node.operator) { - case UnaryOperator.plus: - return operand.unaryPlus(); - case UnaryOperator.minus: - return operand.unaryMinus(); - case UnaryOperator.divide: - return operand.unaryDivide(); - case UnaryOperator.not: - return operand.unaryNot(); - default: - throw StateError("Unknown unary operator ${node.operator}."); - } + return switch (node.operator) { + UnaryOperator.plus => operand.unaryPlus(), + UnaryOperator.minus => operand.unaryMinus(), + UnaryOperator.divide => operand.unaryDivide(), + UnaryOperator.not => operand.unaryNot() + }; }); } @@ -2307,16 +2276,13 @@ class _EvaluateVisitor SassBoolean(node.value); Future visitIfExpression(IfExpression node) async { - var pair = await _evaluateMacroArguments(node); - var positional = pair.item1; - var named = pair.item2; - + var (positional, named) = await _evaluateMacroArguments(node); _verifyArguments(positional.length, named, IfExpression.declaration, node); // ignore: prefer_is_empty - var condition = positional.length > 0 ? positional[0] : named["condition"]!; - var ifTrue = positional.length > 1 ? positional[1] : named["if-true"]!; - var ifFalse = positional.length > 2 ? positional[2] : named["if-false"]!; + var condition = positional.elementAtOrNull(0) ?? named["condition"]!; + var ifTrue = positional.elementAtOrNull(1) ?? named["if-true"]!; + var ifFalse = positional.elementAtOrNull(2) ?? named["if-false"]!; var result = (await condition.accept(this)).isTruthy ? ifTrue : ifFalse; return _withoutSlash(await result.accept(this), _expressionNode(result)); @@ -2341,28 +2307,20 @@ class _EvaluateVisitor } try { - switch (node.name) { - case "calc": - assert(arguments.length == 1); - return SassCalculation.calc(arguments[0]); - case "min": - return SassCalculation.min(arguments); - case "max": - return SassCalculation.max(arguments); - case "clamp": - return SassCalculation.clamp( - arguments[0], - arguments.length > 1 ? arguments[1] : null, - arguments.length > 2 ? arguments[2] : null); - default: - throw UnsupportedError('Unknown calculation name "${node.name}".'); - } + return switch (node.name) { + "calc" => SassCalculation.calc(arguments[0]), + "min" => SassCalculation.min(arguments), + "max" => SassCalculation.max(arguments), + "clamp" => SassCalculation.clamp(arguments[0], + arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), + _ => throw UnsupportedError('Unknown calculation name "${node.name}".') + }; } on SassScriptException catch (error, stackTrace) { // The simplification logic in the [SassCalculation] static methods will // throw an error if the arguments aren't compatible, but we have access // to the original spans so we can throw a more informative error. _verifyCompatibleNumbers(arguments, node.arguments); - throwWithTrace(_exception(error.message, node.span), stackTrace); + throwWithTrace(_exception(error.message, node.span), error, stackTrace); } } @@ -2376,9 +2334,7 @@ class _EvaluateVisitor // SassCalculation._verifyCompatibleNumbers and most changes here should // also be reflected there. for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg is! SassNumber) continue; - if (arg.numeratorUnits.length > 1 || arg.denominatorUnits.isNotEmpty) { + if (args[i] case SassNumber arg when arg.hasComplexUnits) { throw _exception("Number $arg isn't compatible with CSS calculations.", nodesWithSpans[i].span); } @@ -2410,78 +2366,70 @@ class _EvaluateVisitor /// old global `min()` and `max()` functions. Future _visitCalculationValue(Expression node, {required bool inMinMax}) async { - if (node is ParenthesizedExpression) { - var inner = node.expression; - var result = await _visitCalculationValue(inner, inMinMax: inMinMax); - return inner is FunctionExpression && - inner.name.toLowerCase() == 'var' && - result is SassString && - !result.hasQuotes - ? SassString('(${result.text})', quotes: false) - : result; - } else if (node is StringExpression) { - assert(!node.hasQuotes); - var text = node.text.asPlain; + switch (node) { + case ParenthesizedExpression(expression: var inner): + var result = await _visitCalculationValue(inner, inMinMax: inMinMax); + return inner is FunctionExpression && + inner.name.toLowerCase() == 'var' && + result is SassString && + !result.hasQuotes + ? SassString('(${result.text})', quotes: false) + : result; + + case StringExpression(text: Interpolation(asPlain: var text?)): + assert(!node.hasQuotes); + return switch (text.toLowerCase()) { + 'pi' => SassNumber(math.pi), + 'e' => SassNumber(math.e), + 'infinity' => SassNumber(double.infinity), + '-infinity' => SassNumber(double.negativeInfinity), + 'nan' => SassNumber(double.nan), + _ => SassString(text, quotes: false) + }; + // If there's actual interpolation, create a CalculationInterpolation. // Otherwise, create an UnquotedString. The main difference is that // UnquotedStrings don't get extra defensive parentheses. - if (text == null) { + case StringExpression(): + assert(!node.hasQuotes); return CalculationInterpolation(await _performInterpolation(node.text)); - } - switch (text.toLowerCase()) { - case 'pi': - return SassNumber(math.pi); - case 'e': - return SassNumber(math.e); - case 'infinity': - return SassNumber(double.infinity); - case '-infinity': - return SassNumber(double.negativeInfinity); - case 'nan': - return SassNumber(double.nan); - default: - return SassString(text, quotes: false); - } - } else if (node is BinaryOperationExpression) { - return await _addExceptionSpanAsync( - node, - () async => SassCalculation.operateInternal( - _binaryOperatorToCalculationOperator(node.operator), - await _visitCalculationValue(node.left, inMinMax: inMinMax), - await _visitCalculationValue(node.right, inMinMax: inMinMax), - inMinMax: inMinMax, - simplify: !_inSupportsDeclaration)); - } else { - assert(node is NumberExpression || - node is CalculationExpression || - node is VariableExpression || - node is FunctionExpression || - node is IfExpression); - var result = await node.accept(this); - if (result is SassNumber || result is SassCalculation) return result; - if (result is SassString && !result.hasQuotes) return result; - throw _exception( - "Value $result can't be used in a calculation.", node.span); + case BinaryOperationExpression(:var operator, :var left, :var right): + return await _addExceptionSpanAsync( + node, + () async => SassCalculation.operateInternal( + _binaryOperatorToCalculationOperator(operator), + await _visitCalculationValue(left, inMinMax: inMinMax), + await _visitCalculationValue(right, inMinMax: inMinMax), + inMinMax: inMinMax, + simplify: !_inSupportsDeclaration)); + + case _: + assert(node is NumberExpression || + node is CalculationExpression || + node is VariableExpression || + node is FunctionExpression || + node is IfExpression); + return switch (await node.accept(this)) { + SassNumber result => result, + SassCalculation result => result, + SassString result when !result.hasQuotes => result, + var result => throw _exception( + "Value $result can't be used in a calculation.", node.span) + }; } } /// Returns the [CalculationOperator] that corresponds to [operator]. CalculationOperator _binaryOperatorToCalculationOperator( - BinaryOperator operator) { - switch (operator) { - case BinaryOperator.plus: - return CalculationOperator.plus; - case BinaryOperator.minus: - return CalculationOperator.minus; - case BinaryOperator.times: - return CalculationOperator.times; - case BinaryOperator.dividedBy: - return CalculationOperator.dividedBy; - default: - throw UnsupportedError("Invalid calculation operator $operator."); - } - } + BinaryOperator operator) => + switch (operator) { + BinaryOperator.plus => CalculationOperator.plus, + BinaryOperator.minus => CalculationOperator.minus, + BinaryOperator.times => CalculationOperator.times, + BinaryOperator.dividedBy => CalculationOperator.dividedBy, + _ => throw UnsupportedError("Invalid calculation operator $operator.") + }; Future visitColorExpression(ColorExpression node) async => node.value; @@ -2495,22 +2443,22 @@ class _EvaluateVisitor Future visitMapExpression(MapExpression node) async { var map = {}; var keyNodes = {}; - for (var pair in node.pairs) { - var keyValue = await pair.item1.accept(this); - var valueValue = await pair.item2.accept(this); + for (var (key, value) in node.pairs) { + var keyValue = await key.accept(this); + var valueValue = await value.accept(this); var oldValue = map[keyValue]; if (oldValue != null) { var oldValueSpan = keyNodes[keyValue]?.span; throw MultiSpanSassRuntimeException( 'Duplicate key.', - pair.item1.span, + key.span, 'second key', {if (oldValueSpan != null) oldValueSpan: 'first key'}, - _stackTrace(pair.item1.span)); + _stackTrace(key.span)); } map[keyValue] = valueValue; - keyNodes[keyValue] = pair.item1; + keyNodes[keyValue] = key; } return SassMap(map); } @@ -2709,9 +2657,8 @@ class _EvaluateVisitor _callableNode = nodeWithSpan; var namedSet = MapKeySet(evaluated.named); - var tuple = callable.callbackFor(evaluated.positional.length, namedSet); - var overload = tuple.item1; - var callback = tuple.item2; + var (overload, callback) = + callable.callbackFor(evaluated.positional.length, namedSet); _addExceptionSpan(nodeWithSpan, () => overload.verify(evaluated.positional.length, namedSet)); @@ -2756,7 +2703,7 @@ class _EvaluateVisitor } catch (_) { message = error.toString(); } - throwWithTrace(_exception(message, nodeWithSpan.span), stackTrace); + throwWithTrace(_exception(message, nodeWithSpan.span), error, stackTrace); } _callableNode = oldCallableNode; @@ -2792,17 +2739,21 @@ class _EvaluateVisitor var named = {}; var namedNodes = {}; - for (var entry in arguments.named.entries) { - var nodeForSpan = _expressionNode(entry.value); - named[entry.key] = - _withoutSlash(await entry.value.accept(this), nodeForSpan); - namedNodes[entry.key] = nodeForSpan; + for (var (name, value) in arguments.named.pairs) { + var nodeForSpan = _expressionNode(value); + named[name] = _withoutSlash(await value.accept(this), nodeForSpan); + namedNodes[name] = nodeForSpan; } var restArgs = arguments.rest; if (restArgs == null) { - return _ArgumentResults(positional, positionalNodes, named, namedNodes, - ListSeparator.undecided); + return ( + positional: positional, + positionalNodes: positionalNodes, + named: named, + namedNodes: namedNodes, + separator: ListSeparator.undecided + ); } var rest = await restArgs.accept(this); @@ -2833,8 +2784,13 @@ class _EvaluateVisitor var keywordRestArgs = arguments.keywordRest; if (keywordRestArgs == null) { - return _ArgumentResults( - positional, positionalNodes, named, namedNodes, separator); + return ( + positional: positional, + positionalNodes: positionalNodes, + named: named, + namedNodes: namedNodes, + separator: separator + ); } var keywordRest = await keywordRestArgs.accept(this); @@ -2845,8 +2801,13 @@ class _EvaluateVisitor for (var key in keywordRest.contents.keys) (key as SassString).text: keywordRestNodeForSpan }); - return _ArgumentResults( - positional, positionalNodes, named, namedNodes, separator); + return ( + positional: positional, + positionalNodes: positionalNodes, + named: named, + namedNodes: namedNodes, + separator: separator + ); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", @@ -2859,12 +2820,11 @@ class _EvaluateVisitor /// /// Returns the arguments as expressions so that they can be lazily evaluated /// for macros such as `if()`. - Future, Map>> + Future<(List positional, Map named)> _evaluateMacroArguments(CallableInvocation invocation) async { var restArgs_ = invocation.arguments.rest; if (restArgs_ == null) { - return Tuple2( - invocation.arguments.positional, invocation.arguments.named); + return (invocation.arguments.positional, invocation.arguments.named); } var restArgs = restArgs_; // dart-lang/sdk#45348 @@ -2890,7 +2850,7 @@ class _EvaluateVisitor } var keywordRestArgs_ = invocation.arguments.keywordRest; - if (keywordRestArgs_ == null) return Tuple2(positional, named); + if (keywordRestArgs_ == null) return (positional, named); var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348 var keywordRest = await keywordRestArgs.accept(this); @@ -2903,7 +2863,7 @@ class _EvaluateVisitor (value) => ValueExpression( _withoutSlash(value, keywordRestNodeForSpan), keywordRestArgs.span)); - return Tuple2(positional, named); + return (positional, named); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", @@ -2953,15 +2913,17 @@ class _EvaluateVisitor var oldInSupportsDeclaration = _inSupportsDeclaration; _inSupportsDeclaration = false; var result = SassString( - (await mapAsync(node.text.contents, (value) async { - if (value is String) return value; - var expression = value as Expression; - var result = await expression.accept(this); - return result is SassString - ? result.text - : _serialize(result, expression, quote: false); - })) - .join(), + [ + for (var value in node.text.contents) + switch (value) { + String() => value, + Expression() => switch (await value.accept(this)) { + SassString(:var text) => text, + var result => _serialize(result, value, quote: false) + }, + _ => throw UnsupportedError("Unknown interpolation value $value") + } + ].join(), quotes: node.hasQuotes); _inSupportsDeclaration = oldInSupportsDeclaration; return result; @@ -3088,12 +3050,7 @@ class _EvaluateVisitor () async { await _withMediaQueries(mergedQueries ?? node.queries, mergedSources, () async { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - await child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the media query so that // declarations immediately inside @media have somewhere to go. // @@ -3104,6 +3061,10 @@ class _EvaluateVisitor await child.accept(this); } }, scopeWhen: false); + } else { + for (var child in node.children) { + await child.accept(this); + } } }); }, @@ -3142,8 +3103,7 @@ class _EvaluateVisitor }, through: (node) => node is CssStyleRule, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - if (styleRule == null && _parent.children.isNotEmpty) { - var lastChild = _parent.children.last; + if (_parent.children case [..., var lastChild] when styleRule == null) { lastChild.isGroupEnd = true; } } @@ -3166,12 +3126,7 @@ class _EvaluateVisitor await _withParent(ModifiableCssSupportsRule(node.condition, node.span), () async { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - await child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the supports rule so that // declarations immediately inside @supports have somewhere to go. // @@ -3182,6 +3137,10 @@ class _EvaluateVisitor await child.accept(this); } }); + } else { + for (var child in node.children) { + await child.accept(this); + } } }, through: (node) => node is CssStyleRule, scopeWhen: false); } @@ -3195,8 +3154,7 @@ class _EvaluateVisitor Future _handleReturn( List list, Future callback(T value)) async { for (var value in list) { - var result = await callback(value); - if (result != null) return result; + if (await callback(value) case var result?) return result; } return null; } @@ -3230,25 +3188,25 @@ class _EvaluateVisitor /// values passed into the interpolation. Future _performInterpolation(Interpolation interpolation, {bool warnForColor = false}) async { - var tuple = await _performInterpolationHelper(interpolation, + var (result, _) = await _performInterpolationHelper(interpolation, sourceMap: true, warnForColor: warnForColor); - return tuple.item1; + return result; } /// Like [_performInterpolation], but also returns a [InterpolationMap] that /// can map spans from the resulting string back to the original /// [interpolation]. - Future> _performInterpolationWithMap( + Future<(String, InterpolationMap)> _performInterpolationWithMap( Interpolation interpolation, {bool warnForColor = false}) async { - var tuple = await _performInterpolationHelper(interpolation, + var (result, map) = await _performInterpolationHelper(interpolation, sourceMap: true, warnForColor: warnForColor); - return Tuple2(tuple.item1, tuple.item2!); + return (result, map!); } /// A helper that implements the core logic of both [_performInterpolation] /// and [_performInterpolationWithMap]. - Future> _performInterpolationHelper( + Future<(String, InterpolationMap?)> _performInterpolationHelper( Interpolation interpolation, {required bool sourceMap, bool warnForColor = false}) async { @@ -3269,9 +3227,7 @@ class _EvaluateVisitor var expression = value as Expression; var result = await expression.accept(this); - if (warnForColor && - result is SassColor && - namesByColor.containsKey(result)) { + if (warnForColor && namesByColor.containsKey(result)) { var alternative = BinaryOperationExpression( BinaryOperator.plus, StringExpression(Interpolation([""], interpolation.span), @@ -3292,11 +3248,11 @@ class _EvaluateVisitor } _inSupportsDeclaration = oldInSupportsDeclaration; - return Tuple2( - buffer.toString(), - targetLocations == null - ? null - : InterpolationMap(interpolation, targetLocations)); + return ( + buffer.toString(), + targetLocations.andThen( + (targetLocations) => InterpolationMap(interpolation, targetLocations)) + ); } /// Evaluates [expression] and calls `toCssString()` and wraps a @@ -3371,12 +3327,12 @@ class _EvaluateVisitor var parent = _parent; if (through != null) { while (through(parent)) { - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw ArgumentError( "through() must return false for at least one parent of $node."); } - parent = grandparent; } // If the parent has a (visible) following sibling, we shouldn't add to @@ -3436,7 +3392,7 @@ class _EvaluateVisitor /// real work to manufacture a source span. Future _withStackFrame( String member, AstNode nodeWithSpan, Future callback()) async { - _stack.add(Tuple2(_member, nodeWithSpan)); + _stack.add((_member, nodeWithSpan)); var oldMember = _member; _member = member; var result = await callback(); @@ -3448,16 +3404,12 @@ class _EvaluateVisitor /// Like [Value.withoutSlash], but produces a deprecation warning if [value] /// was a slash-separated number. Value _withoutSlash(Value value, AstNode nodeForSpan) { - if (value is SassNumber && value.asSlash != null) { - String recommendation(SassNumber number) { - var asSlash = number.asSlash; - if (asSlash != null) { - return "math.div(${recommendation(asSlash.item1)}, " - "${recommendation(asSlash.item2)})"; - } else { - return number.toString(); - } - } + if (value case SassNumber(asSlash: _?)) { + String recommendation(SassNumber number) => switch (number.asSlash) { + (var before, var after) => + "math.div(${recommendation(before)}, ${recommendation(after)})", + _ => number.toString() + }; _warn( "Using / for division is deprecated and will be removed in Dart Sass " @@ -3484,7 +3436,8 @@ class _EvaluateVisitor /// If [span] is passed, it's used for the innermost stack frame. Trace _stackTrace([FileSpan? span]) { var frames = [ - ..._stack.map((tuple) => _stackFrame(tuple.item1, tuple.item2.span)), + for (var (member, nodeWithSpan) in _stack) + _stackFrame(member, nodeWithSpan.span), if (span != null) _stackFrame(_member, span) ]; return Trace(frames.reversed); @@ -3497,7 +3450,7 @@ class _EvaluateVisitor return; } - if (!_warningsEmitted.add(Tuple2(message, span))) return; + if (!_warningsEmitted.add((message, span))) return; var trace = _stackTrace(span); if (deprecation == null) { _logger.warn(message, span: span, trace: trace); @@ -3512,7 +3465,7 @@ class _EvaluateVisitor /// If [span] is passed, it's used for the innermost stack frame. SassRuntimeException _exception(String message, [FileSpan? span]) => SassRuntimeException( - message, span ?? _stack.last.item2.span, _stackTrace(span)); + message, span ?? _stack.last.$2.span, _stackTrace(span)); /// Returns a [MultiSpanSassRuntimeException] with the given [message], /// [primaryLabel], and [secondaryLabels]. @@ -3520,8 +3473,8 @@ class _EvaluateVisitor /// The primary span is taken from the current stack trace span. SassRuntimeException _multiSpanException(String message, String primaryLabel, Map secondaryLabels) => - MultiSpanSassRuntimeException(message, _stack.last.item2.span, - primaryLabel, secondaryLabels, _stackTrace()); + MultiSpanSassRuntimeException(message, _stack.last.$2.span, primaryLabel, + secondaryLabels, _stackTrace()); /// Runs [callback], and converts any [SassScriptException]s it throws to /// [SassRuntimeException]s with [nodeWithSpan]'s source span. @@ -3541,6 +3494,7 @@ class _EvaluateVisitor error .withSpan(nodeWithSpan.span) .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)), + error, stackTrace); } } @@ -3556,6 +3510,7 @@ class _EvaluateVisitor error .withSpan(nodeWithSpan.span) .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)), + error, stackTrace); } } @@ -3569,7 +3524,8 @@ class _EvaluateVisitor } on SassRuntimeException { rethrow; } on SassException catch (error, stackTrace) { - throwWithTrace(error.withTrace(_stackTrace(error.span)), stackTrace); + throwWithTrace( + error.withTrace(_stackTrace(error.span)), error, stackTrace); } } @@ -3583,6 +3539,7 @@ class _EvaluateVisitor if (!error.span.text.startsWith("@error")) rethrow; throwWithTrace( SassRuntimeException(error.message, nodeWithSpan.span, _stackTrace()), + error, stackTrace); } } @@ -3599,7 +3556,7 @@ class _EvaluateVisitor /// because it will add the parent selector to the CSS if the `@import` appeared /// in a nested context, but the parent selector was already added when the /// imported stylesheet was evaluated. -class _ImportedCssVisitor implements ModifiableCssVisitor { +final class _ImportedCssVisitor implements ModifiableCssVisitor { /// The visitor in whose context this was created. final _EvaluateVisitor _visitor; @@ -3659,19 +3616,17 @@ class _ImportedCssVisitor implements ModifiableCssVisitor { /// The result of compiling a Sass document to a CSS tree, along with metadata /// about the compilation process. -class EvaluateResult { +typedef EvaluateResult = ({ /// The CSS syntax tree. - final CssStylesheet stylesheet; + CssStylesheet stylesheet, /// The canonical URLs of all stylesheets loaded during compilation. - final Set loadedUrls; - - EvaluateResult(this.stylesheet, this.loadedUrls); -} + Set loadedUrls +}); /// An implementation of [EvaluationContext] using the information available in /// [_EvaluateVisitor]. -class _EvaluationContext implements EvaluationContext { +final class _EvaluationContext implements EvaluationContext { /// The visitor backing this context. final _EvaluateVisitor _visitor; @@ -3682,8 +3637,7 @@ class _EvaluationContext implements EvaluationContext { _EvaluationContext(this._visitor, this._defaultWarnNodeWithSpan); FileSpan get currentCallableSpan { - var callableNode = _visitor._callableNode; - if (callableNode != null) return callableNode.span; + if (_visitor._callableNode case var callableNode?) return callableNode.span; throw StateError("No Sass callable is currently being evaluated."); } @@ -3698,50 +3652,43 @@ class _EvaluationContext implements EvaluationContext { } /// The result of evaluating arguments to a function or mixin. -class _ArgumentResults { +typedef _ArgumentResults = ({ /// Arguments passed by position. - final List positional; + List positional, /// The [AstNode]s that hold the spans for each [positional] argument. /// /// This stores [AstNode]s rather than [FileSpan]s 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. - final List positionalNodes; + List positionalNodes, /// Arguments passed by name. - final Map named; + Map named, /// The [AstNode]s that hold the spans for each [named] argument. /// /// This stores [AstNode]s rather than [FileSpan]s 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. - final Map namedNodes; + Map namedNodes, /// The separator used for the rest argument list, if any. - final ListSeparator separator; - - _ArgumentResults(this.positional, this.positionalNodes, this.named, - this.namedNodes, this.separator); -} + ListSeparator separator +}); /// The result of loading a stylesheet via [AsyncEvaluator._loadStylesheet]. -class _LoadedStylesheet { +typedef _LoadedStylesheet = ( /// The stylesheet itself. - final Stylesheet stylesheet; - + Stylesheet stylesheet, { /// The importer that was used to load the stylesheet. /// /// This is `null` when running in Node Sass compatibility mode. - final AsyncImporter? importer; + AsyncImporter? importer, /// Whether this load counts as a dependency. /// /// That is, whether this was (transitively) loaded through a load path or /// importer rather than relative to the entrypoint. - final bool isDependency; - - _LoadedStylesheet(this.stylesheet, - {this.importer, required this.isDependency}); -} + bool isDependency +}); diff --git a/lib/src/visitor/clone_css.dart b/lib/src/visitor/clone_css.dart index 254f7f49c..d011c986a 100644 --- a/lib/src/visitor/clone_css.dart +++ b/lib/src/visitor/clone_css.dart @@ -2,8 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'package:tuple/tuple.dart'; - import '../ast/css.dart'; import '../ast/css/modifiable.dart'; import '../ast/selector.dart'; @@ -14,19 +12,18 @@ import 'interface/css.dart'; /// Returns deep copies of both [stylesheet] and [extender]. /// /// The [extender] must be associated with [stylesheet]. -Tuple2 cloneCssStylesheet( +(ModifiableCssStylesheet, ExtensionStore) cloneCssStylesheet( CssStylesheet stylesheet, ExtensionStore extensionStore) { - var result = extensionStore.clone(); - var newExtensionStore = result.item1; - var oldToNewSelectors = result.item2; + var (newExtensionStore, oldToNewSelectors) = extensionStore.clone(); - return Tuple2( - _CloneCssVisitor(oldToNewSelectors).visitCssStylesheet(stylesheet), - newExtensionStore); + return ( + _CloneCssVisitor(oldToNewSelectors).visitCssStylesheet(stylesheet), + newExtensionStore + ); } /// A visitor that creates a deep (and mutable) copy of a [CssStylesheet]. -class _CloneCssVisitor implements CssVisitor { +final class _CloneCssVisitor implements CssVisitor { /// A map from selectors in the original stylesheet to selectors generated for /// the new stylesheet using [ExtensionStore.clone]. final Map> _oldToNewSelectors; @@ -58,17 +55,16 @@ class _CloneCssVisitor implements CssVisitor { _visitChildren(ModifiableCssMediaRule(node.queries, node.span), node); ModifiableCssStyleRule visitCssStyleRule(CssStyleRule node) { - var newSelector = _oldToNewSelectors[node.selector]; - if (newSelector == null) { + if (_oldToNewSelectors[node.selector] case var newSelector?) { + return _visitChildren( + ModifiableCssStyleRule(newSelector, node.span, + originalSelector: node.originalSelector), + node); + } else { throw StateError( "The ExtensionStore and CssStylesheet passed to cloneCssStylesheet() " "must come from the same compilation."); } - - return _visitChildren( - ModifiableCssStyleRule(newSelector, node.span, - originalSelector: node.originalSelector), - node); } ModifiableCssStylesheet visitCssStylesheet(CssStylesheet node) => diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index aaaaa3dfc..a8639f4e6 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: 81aac8d1ac5bea43a019307cc5eb754610b0c6be +// Checksum: 6eb7f76735562eba91e9460af796b269b3b0aaf7 // // ignore_for_file: unused_import @@ -20,7 +20,6 @@ import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; -import 'package:tuple/tuple.dart'; import '../ast/css.dart'; import '../ast/css/modifiable.dart'; @@ -50,6 +49,7 @@ import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; import '../syntax.dart'; import '../utils.dart'; +import '../util/map.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; import '../util/span.dart'; @@ -103,7 +103,7 @@ EvaluateResult evaluate(Stylesheet stylesheet, /// A class that can evaluate multiple independent statements and expressions /// in the context of a single module. -class Evaluator { +final class Evaluator { /// The visitor that evaluates each expression and statement. final _EvaluateVisitor _visitor; @@ -132,7 +132,7 @@ class Evaluator { } /// A visitor that executes Sass code to produce a CSS tree. -class _EvaluateVisitor +final class _EvaluateVisitor implements StatementVisitor, ExpressionVisitor, @@ -172,7 +172,7 @@ class _EvaluateVisitor /// /// We only want to emit one warning per location, to avoid blowing up users' /// consoles with redundant warnings. - final _warningsEmitted = >{}; + final _warningsEmitted = <(String, SourceSpan)>{}; /// Whether to avoid emitting warnings for files loaded from dependencies. final bool _quietDeps; @@ -265,13 +265,13 @@ class _EvaluateVisitor /// The dynamic call stack representing function invocations, mixin /// invocations, and imports surrounding the current context. /// - /// Each member is a tuple of the span where the stack trace starts and the + /// Each member is a pair of the span where the stack trace starts and the /// name of the member being invoked. /// /// This stores [AstNode]s rather than [FileSpan]s 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. - final _stack = >[]; + final _stack = <(String, AstNode)>[]; /// Whether we're running in Node Sass-compatibility mode. bool get _asNodeSass => _nodeImporter != null; @@ -407,8 +407,8 @@ class _EvaluateVisitor } return SassMap({ - for (var entry in module.variables.entries) - SassString(entry.key): entry.value + for (var (name, value) in module.variables.pairs) + SassString(name): value }); }, url: "sass:meta"), @@ -420,8 +420,8 @@ class _EvaluateVisitor } return SassMap({ - for (var entry in module.functions.entries) - SassString(entry.key): SassFunction(entry.value) + for (var (name, value) in module.functions.pairs) + SassString(name): SassFunction(value) }); }, url: "sass:meta"), @@ -431,19 +431,20 @@ class _EvaluateVisitor var css = arguments[1].isTruthy; var module = arguments[2].realNull?.assertString("module"); - if (css && module != null) { - throw r"$css and $module may not both be passed at once."; + if (css) { + if (module != null) { + throw r"$css and $module may not both be passed at once."; + } + return SassFunction(PlainCssCallable(name.text)); } - var callable = css - ? PlainCssCallable(name.text) - : _addExceptionSpan( - _callableNode!, - () => _getFunction(name.text.replaceAll("_", "-"), - namespace: module?.text)); - if (callable != null) return SassFunction(callable); + var callable = _addExceptionSpan( + _callableNode!, + () => _getFunction(name.text.replaceAll("_", "-"), + namespace: module?.text)); + if (callable == null) throw "Function not found: $name"; - throw "Function not found: $name"; + return SassFunction(callable); }, url: "sass:meta"), BuiltInCallable.function("call", r"$function, $args...", (arguments) { @@ -457,8 +458,8 @@ class _EvaluateVisitor ? null : ValueExpression( SassMap({ - for (var entry in args.keywords.entries) - SassString(entry.key, quotes: false): entry.value + for (var (name, value) in args.keywords.pairs) + SassString(name, quotes: false): value }), callableNode.span)); @@ -536,18 +537,16 @@ class _EvaluateVisitor EvaluateResult run(Importer? importer, Stylesheet node) { try { return withEvaluationContext(_EvaluationContext(this, node), () { - var url = node.span.sourceUrl; - if (url != null) { + if (node.span.sourceUrl case var url?) { _activeModules[url] = null; if (!(_asNodeSass && url.toString() == 'stdin')) _loadedUrls.add(url); } var module = _addExceptionTrace(() => _execute(importer, node)); - - return EvaluateResult(_combineCss(module), _loadedUrls); + return (stylesheet: _combineCss(module), loadedUrls: _loadedUrls); }); } on SassException catch (error, stackTrace) { - throwWithTrace(error.withLoadedUrls(_loadedUrls), stackTrace); + throwWithTrace(error.withLoadedUrls(_loadedUrls), error, stackTrace); } } @@ -614,8 +613,7 @@ class _EvaluateVisitor {Uri? baseUrl, Configuration? configuration, bool namesInErrors = false}) { - var builtInModule = _builtInModules[url]; - if (builtInModule != null) { + if (_builtInModules[url] case var builtInModule?) { if (configuration is ExplicitConfiguration) { throw _exception( namesInErrors @@ -631,30 +629,32 @@ class _EvaluateVisitor } _withStackFrame(stackFrame, nodeWithSpan, () { - var result = + var (stylesheet, :importer, :isDependency) = _loadStylesheet(url.toString(), nodeWithSpan.span, baseUrl: baseUrl); - var stylesheet = result.stylesheet; var canonicalUrl = stylesheet.span.sourceUrl; - if (canonicalUrl != null && _activeModules.containsKey(canonicalUrl)) { - var message = namesInErrors - ? "Module loop: ${p.prettyUri(canonicalUrl)} is already being " - "loaded." - : "Module loop: this module is already being loaded."; - - throw _activeModules[canonicalUrl].andThen((previousLoad) => - _multiSpanException(message, "new load", - {previousLoad.span: "original load"})) ?? - _exception(message); + if (canonicalUrl != null) { + if (_activeModules.containsKey(canonicalUrl)) { + var message = namesInErrors + ? "Module loop: ${p.prettyUri(canonicalUrl)} is already being " + "loaded." + : "Module loop: this module is already being loaded."; + + throw _activeModules[canonicalUrl].andThen((previousLoad) => + _multiSpanException(message, "new load", + {previousLoad.span: "original load"})) ?? + _exception(message); + } else { + _activeModules[canonicalUrl] = nodeWithSpan; + } } - if (canonicalUrl != null) _activeModules[canonicalUrl] = nodeWithSpan; var firstLoad = !_modules.containsKey(canonicalUrl); var oldInDependency = _inDependency; - _inDependency = result.isDependency; + _inDependency = isDependency; Module module; try { - module = _execute(result.importer, stylesheet, + module = _execute(importer, stylesheet, configuration: configuration, nodeWithSpan: nodeWithSpan, namesInErrors: namesInErrors); @@ -682,8 +682,7 @@ class _EvaluateVisitor bool namesInErrors = false}) { var url = stylesheet.span.sourceUrl; - var alreadyLoaded = _modules[url]; - if (alreadyLoaded != null) { + if (_modules[url] case var alreadyLoaded?) { var currentConfiguration = configuration ?? _configuration; if (!_moduleConfigurations[url]!.sameOriginal(currentConfiguration) && currentConfiguration is ExplicitConfiguration) { @@ -781,16 +780,15 @@ class _EvaluateVisitor /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted /// after [_endOfImports], if necessary. - List _addOutOfOrderImports() { - var outOfOrderImports = _outOfOrderImports; - if (outOfOrderImports == null) return _root.children; - - return [ - ..._root.children.take(_endOfImports), - ...outOfOrderImports, - ..._root.children.skip(_endOfImports) - ]; - } + List _addOutOfOrderImports() => + switch (_outOfOrderImports) { + null => _root.children, + var outOfOrderImports => [ + ..._root.children.take(_endOfImports), + ...outOfOrderImports, + ..._root.children.skip(_endOfImports) + ] + }; /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all /// modules transitively used by [root]. @@ -802,9 +800,10 @@ class _EvaluateVisitor CssStylesheet _combineCss(Module root, {bool clone = false}) { if (!root.upstream.any((module) => module.transitivelyContainsCss)) { var selectors = root.extensionStore.simpleSelectors; - var unsatisfiedExtension = firstOrNull(root.extensionStore - .extensionsWhereTarget((target) => !selectors.contains(target))); - if (unsatisfiedExtension != null) { + if (root.extensionStore + .extensionsWhereTarget((target) => !selectors.contains(target)) + .firstOrNull + case var unsatisfiedExtension?) { _throwForUnsatisfiedExtension(unsatisfiedExtension); } @@ -831,8 +830,7 @@ class _EvaluateVisitor for (var upstream in module.upstream) { if (upstream.transitivelyContainsCss) { - var comments = module.preModuleComments[upstream]; - if (comments != null) { + if (module.preModuleComments[upstream] case var comments?) { // Intermix the top-level comments with plain CSS `@import`s until we // start to have actual CSS defined, at which point start treating it as // normal CSS. @@ -889,11 +887,11 @@ class _EvaluateVisitor if (module.extensionStore.isEmpty) continue; for (var upstream in module.upstream) { - var url = upstream.url; - if (url == null) continue; - downstreamExtensionStores - .putIfAbsent(url, () => []) - .add(module.extensionStore); + if (upstream.url case var url?) { + downstreamExtensionStores + .putIfAbsent(url, () => []) + .add(module.extensionStore); + } } // Remove all extensions that are now satisfied after adding downstream @@ -920,12 +918,15 @@ class _EvaluateVisitor /// static imports. int _indexAfterImports(List statements) { var lastImport = -1; + loop: for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement is CssImport) { - lastImport = i; - } else if (statement is! CssComment) { - break; + switch (statements[i]) { + case CssImport(): + lastImport = i; + case CssComment(): + continue loop; + case _: + break loop; } } return lastImport + 1; @@ -942,12 +943,9 @@ class _EvaluateVisitor Value? visitAtRootRule(AtRootRule node) { var query = AtRootQuery.defaultQuery; - var unparsedQuery = node.query; - if (unparsedQuery != null) { - var tuple = + if (node.query case var unparsedQuery?) { + var (resolved, map) = _performInterpolationWithMap(unparsedQuery, warnForColor: true); - var resolved = tuple.item1; - var map = tuple.item2; query = AtRootQuery.parse(resolved, interpolationMap: map, logger: _logger); } @@ -957,13 +955,12 @@ class _EvaluateVisitor while (parent is! CssStylesheet) { if (!query.excludes(parent)) included.add(parent); - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw StateError( "CssNodes must have a CssStylesheet transitive parent node."); } - - parent = grandparent; } var root = _trimIncluded(included); @@ -979,10 +976,10 @@ class _EvaluateVisitor } var innerCopy = root; - if (included.isNotEmpty) { - innerCopy = included.first.copyWithoutChildren(); + if (included case [var first, ...var rest]) { + innerCopy = first.copyWithoutChildren(); var outerCopy = innerCopy; - for (var node in included.skip(1)) { + for (var node in rest) { var copy = node.copyWithoutChildren(); copy.addChild(outerCopy); outerCopy = copy; @@ -1019,21 +1016,20 @@ class _EvaluateVisitor while (parent != nodes[i]) { innermostContiguous = null; - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw ArgumentError( "Expected ${nodes[i]} to be an ancestor of $this."); } - - parent = grandparent; } innermostContiguous ??= i; - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw ArgumentError("Expected ${nodes[i]} to be an ancestor of $this."); } - parent = grandparent; } if (parent != _root) return _root; @@ -1136,27 +1132,27 @@ class _EvaluateVisitor } var name = _interpolationToValue(node.name, warnForColor: true); - if (_declarationName != null) { - name = CssValue("$_declarationName-${name.value}", name.span); - } - var cssValue = - node.value.andThen((value) => CssValue(value.accept(this), value.span)); - - // If the value is an empty list, preserve it, because converting it to CSS - // will throw an error that we want the user to see. - if (cssValue != null && - (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { - _parent.addChild(ModifiableCssDeclaration(name, cssValue, node.span, - parsedAsCustomProperty: node.isCustomProperty, - valueSpanForMap: - _sourceMap ? node.value.andThen(_expressionNode)?.span : null)); - } else if (name.value.startsWith('--') && cssValue != null) { - throw _exception( - "Custom property values may not be empty.", cssValue.span); + if (_declarationName case var declarationName?) { + name = CssValue("$declarationName-${name.value}", name.span); + } + + if (node.value case var expression?) { + var value = expression.accept(this); + // If the value is an empty list, preserve it, because converting it to CSS + // will throw an error that we want the user to see. + if (!value.isBlank || _isEmptyList(value)) { + _parent.addChild(ModifiableCssDeclaration( + name, CssValue(value, expression.span), node.span, + parsedAsCustomProperty: node.isCustomProperty, + valueSpanForMap: + _sourceMap ? node.value.andThen(_expressionNode)?.span : null)); + } else if (name.value.startsWith('--')) { + throw _exception( + "Custom property values may not be empty.", expression.span); + } } - var children = node.children; - if (children != null) { + if (node.children case var children?) { var oldDeclarationName = _declarationName; _declarationName = name.value; _environment.scope(() { @@ -1176,11 +1172,12 @@ class _EvaluateVisitor Value? visitEachRule(EachRule node) { var list = node.list.accept(this); var nodeWithSpan = _expressionNode(node.list); - var setVariables = node.variables.length == 1 - ? (Value value) => _environment.setLocalVariable(node.variables.first, - _withoutSlash(value, nodeWithSpan), nodeWithSpan) - : (Value value) => - _setMultipleVariables(node.variables, value, nodeWithSpan); + var setVariables = switch (node.variables) { + [var variable] => (Value value) => _environment.setLocalVariable( + variable, _withoutSlash(value, nodeWithSpan), nodeWithSpan), + var variables => (Value value) => + _setMultipleVariables(variables, value, nodeWithSpan) + }; return _environment.scope(() { return _handleReturn(list.asList, (element) { setVariables(element); @@ -1230,9 +1227,8 @@ class _EvaluateVisitor Deprecation.bogusCombinators); } - var tuple = _performInterpolationWithMap(node.selector, warnForColor: true); - var targetText = tuple.item1; - var targetMap = tuple.item2; + var (targetText, targetMap) = + _performInterpolationWithMap(node.selector, warnForColor: true); var list = SelectorList.parse(trimAscii(targetText, excludeEscape: true), interpolationMap: targetMap, logger: _logger, allowParent: false); @@ -1345,9 +1341,11 @@ class _EvaluateVisitor numeratorUnits: fromNumber.numeratorUnits, denominatorUnits: fromNumber.denominatorUnits), nodeWithSpan); - var result = _handleReturn( - node.children, (child) => child.accept(this)); - if (result != null) return result; + if (_handleReturn( + node.children, (child) => child.accept(this)) + case var result?) { + return result; + } } return null; }, semiGlobal: true); @@ -1403,8 +1401,8 @@ class _EvaluateVisitor var newValues = Map.of(configuration.values); for (var variable in node.configuration) { if (variable.isGuarded) { - var oldValue = configuration.remove(variable.name); - if (oldValue != null && oldValue.value != sassNull) { + if (configuration.remove(variable.name) case var oldValue? + when oldValue.value != sassNull) { newValues[variable.name] = oldValue; continue; } @@ -1464,14 +1462,14 @@ class _EvaluateVisitor if (configuration is! ExplicitConfiguration) return; if (configuration.isEmpty) return; - var entry = configuration.values.entries.first; + var (name, value) = configuration.values.pairs.first; throw _exception( nameInError - ? "\$${entry.key} was not declared with !default in the @used " + ? "\$$name was not declared with !default in the @used " "module." : "This variable was not declared with !default in the @used " "module.", - entry.value.configurationSpan); + value.configurationSpan); } Value? visitFunctionRule(FunctionRule node) { @@ -1488,14 +1486,12 @@ class _EvaluateVisitor break; } } - if (clause == null) return null; - return _environment.scope( + return clause.andThen((clause) => _environment.scope( () => _handleReturn( - clause!.children, // dart-lang/sdk#45348 - (child) => child.accept(this)), + clause.children, (child) => child.accept(this)), semiGlobal: true, - when: clause.hasDeclarations); + when: clause.hasDeclarations)); } Value? visitImportRule(ImportRule node) { @@ -1512,9 +1508,8 @@ class _EvaluateVisitor /// Adds the stylesheet imported by [import] to the current document. void _visitDynamicImport(DynamicImport import) { return _withStackFrame("@import", import, () { - var result = + var (stylesheet, :importer, :isDependency) = _loadStylesheet(import.urlString, import.span, forImport: true); - var stylesheet = result.stylesheet; var url = stylesheet.span.sourceUrl; if (url != null) { @@ -1535,9 +1530,9 @@ class _EvaluateVisitor var oldImporter = _importer; var oldStylesheet = _stylesheet; var oldInDependency = _inDependency; - _importer = result.importer; + _importer = importer; _stylesheet = stylesheet; - _inDependency = result.isDependency; + _inDependency = isDependency; visitStylesheet(stylesheet); _importer = oldImporter; _stylesheet = oldStylesheet; @@ -1566,7 +1561,7 @@ class _EvaluateVisitor var oldOutOfOrderImports = _outOfOrderImports; var oldConfiguration = _configuration; var oldInDependency = _inDependency; - _importer = result.importer; + _importer = importer; _stylesheet = stylesheet; if (loadsUserDefinedModules) { _root = ModifiableCssStylesheet(stylesheet.span); @@ -1574,7 +1569,7 @@ class _EvaluateVisitor _endOfImports = 0; _outOfOrderImports = null; } - _inDependency = result.isDependency; + _inDependency = isDependency; // This configuration is only used if it passes through a `@forward` // rule, so we avoid creating unnecessary ones for performance reasons. @@ -1633,32 +1628,28 @@ class _EvaluateVisitor assert(_importSpan == null); _importSpan = span; - var importCache = _importCache; - if (importCache != null) { - var parsedUrl = Uri.parse(url); + if (_importCache case var importCache?) { baseUrl ??= _stylesheet.span.sourceUrl; - var tuple = importCache.canonicalize(parsedUrl, - baseImporter: _importer, baseUrl: baseUrl, forImport: forImport); - - if (tuple != null) { + if (importCache.canonicalize(Uri.parse(url), + baseImporter: _importer, baseUrl: baseUrl, forImport: forImport) + case (var importer, var canonicalUrl, :var originalUrl)) { // Make sure we record the canonical URL as "loaded" even if the // actual load fails, because watchers should watch it to see if it // changes in a way that allows the load to succeed. - _loadedUrls.add(tuple.item2); - - var isDependency = _inDependency || tuple.item1 != _importer; - var stylesheet = importCache.importCanonical(tuple.item1, tuple.item2, - originalUrl: tuple.item3, quiet: _quietDeps && isDependency); - if (stylesheet != null) { - return _LoadedStylesheet(stylesheet, - importer: tuple.item1, isDependency: isDependency); + _loadedUrls.add(canonicalUrl); + + var isDependency = _inDependency || importer != _importer; + if (importCache.importCanonical(importer, canonicalUrl, + originalUrl: originalUrl, quiet: _quietDeps && isDependency) + case var stylesheet?) { + return (stylesheet, importer: importer, isDependency: isDependency); } } } else { - var result = _importLikeNode( - url, baseUrl ?? _stylesheet.span.sourceUrl, forImport); - if (result != null) { - result.stylesheet.span.sourceUrl.andThen(_loadedUrls.add); + if (_importLikeNode( + url, baseUrl ?? _stylesheet.span.sourceUrl, forImport) + case var result?) { + result.$1.span.sourceUrl.andThen(_loadedUrls.add); return result; } } @@ -1673,7 +1664,7 @@ class _EvaluateVisitor } on SassException { rethrow; } on ArgumentError catch (error, stackTrace) { - throwWithTrace(_exception(error.toString()), stackTrace); + throwWithTrace(_exception(error.toString()), error, stackTrace); } catch (error, stackTrace) { String? message; try { @@ -1681,7 +1672,7 @@ class _EvaluateVisitor } catch (_) { message = error.toString(); } - throwWithTrace(_exception(message), stackTrace); + throwWithTrace(_exception(message), error, stackTrace); } finally { _importSpan = null; } @@ -1703,15 +1694,15 @@ class _EvaluateVisitor isDependency = true; } - var contents = result.item1; - var url = result.item2; - - return _LoadedStylesheet( - Stylesheet.parse(contents, - url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, - url: url, - logger: _quietDeps && isDependency ? Logger.quiet : _logger), - isDependency: isDependency); + var (contents, url) = result; + return ( + Stylesheet.parse( + contents, url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, + url: url, + logger: _quietDeps && isDependency ? Logger.quiet : _logger), + importer: null, + isDependency: isDependency + ); } /// Adds a CSS import for [import]. @@ -1735,44 +1726,46 @@ class _EvaluateVisitor } Value? visitIncludeRule(IncludeRule node) { + var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent); var mixin = _addExceptionSpan(node, () => _environment.getMixin(node.name, namespace: node.namespace)); - if (mixin == null) { - throw _exception("Undefined mixin.", node.span); - } + switch (mixin) { + case null: + throw _exception("Undefined mixin.", node.span); - var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent); - if (mixin is BuiltInCallable) { - if (node.content != null) { + case BuiltInCallable() when node.content != null: throw _exception("Mixin doesn't accept a content block.", node.span); - } - _runBuiltInCallable(node.arguments, mixin, nodeWithSpan); - } else if (mixin is UserDefinedCallable) { - if (node.content != null && - !(mixin.declaration as MixinRule).hasContent) { + case BuiltInCallable(): + _runBuiltInCallable(node.arguments, mixin, nodeWithSpan); + + case UserDefinedCallable( + declaration: MixinRule(hasContent: false) + ) + when node.content != null: throw MultiSpanSassRuntimeException( "Mixin doesn't accept a content block.", node.spanWithoutContent, "invocation", {mixin.declaration.arguments.spanWithName: "declaration"}, _stackTrace(node.spanWithoutContent)); - } - var contentCallable = node.content.andThen((content) => - UserDefinedCallable(content, _environment.closure(), - inDependency: _inDependency)); - _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () { - _environment.withContent(contentCallable, () { - _environment.asMixin(() { - for (var statement in mixin.declaration.children) { - _addErrorSpan(nodeWithSpan, () => statement.accept(this)); - } + case UserDefinedCallable(): + var contentCallable = node.content.andThen((content) => + UserDefinedCallable(content, _environment.closure(), + inDependency: _inDependency)); + _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () { + _environment.withContent(contentCallable, () { + _environment.asMixin(() { + for (var statement in mixin.declaration.children) { + _addErrorSpan(nodeWithSpan, () => statement.accept(this)); + } + }); }); }); - }); - } else { - throw UnsupportedError("Unknown callable type $mixin."); + + case _: + throw UnsupportedError("Unknown callable type $mixin."); } return null; @@ -1821,12 +1814,7 @@ class _EvaluateVisitor _withParent(ModifiableCssMediaRule(mergedQueries ?? queries, node.span), () { _withMediaQueries(mergedQueries ?? queries, mergedSources, () { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the media query so that // declarations immediately inside @media have somewhere to go. // @@ -1837,6 +1825,10 @@ class _EvaluateVisitor child.accept(this); } }, scopeWhen: false); + } else { + for (var child in node.children) { + child.accept(this); + } } }); }, @@ -1853,9 +1845,8 @@ class _EvaluateVisitor /// Evaluates [interpolation] and parses the result as a list of media /// queries. List _visitMediaQueries(Interpolation interpolation) { - var tuple = _performInterpolationWithMap(interpolation, warnForColor: true); - var resolved = tuple.item1; - var map = tuple.item2; + var (resolved, map) = + _performInterpolationWithMap(interpolation, warnForColor: true); return CssMediaQuery.parseList(resolved, logger: _logger, interpolationMap: map); } @@ -1870,11 +1861,16 @@ class _EvaluateVisitor Iterable queries1, Iterable queries2) { var queries = []; for (var query1 in queries1) { + inner: for (var query2 in queries2) { - var result = query1.merge(query2); - if (result == MediaQueryMergeResult.empty) continue; - if (result == MediaQueryMergeResult.unrepresentable) return null; - queries.add((result as MediaQuerySuccessfulMergeResult).query); + switch (query1.merge(query2)) { + case MediaQueryMergeResult.empty: + continue inner; + case MediaQueryMergeResult.unrepresentable: + return null; + case MediaQuerySuccessfulMergeResult result: + queries.add(result.query); + } } } return queries; @@ -1894,9 +1890,8 @@ class _EvaluateVisitor "Style rules may not be used within nested declarations.", node.span); } - var tuple = _performInterpolationWithMap(node.selector, warnForColor: true); - var selectorText = tuple.item1; - var selectorMap = tuple.item2; + var (selectorText, selectorMap) = + _performInterpolationWithMap(node.selector, warnForColor: true); if (_inKeyframes) { // NOTE: this logic is largely duplicated in [visitCssKeyframeBlock]. Most @@ -2007,12 +2002,7 @@ class _EvaluateVisitor var condition = CssValue(_visitSupportsCondition(node.condition), node.condition.span); _withParent(ModifiableCssSupportsRule(condition, node.span), () { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the supports rule so that // declarations immediately inside @supports have somewhere to go. // @@ -2023,6 +2013,10 @@ class _EvaluateVisitor child.accept(this); } }); + } else { + for (var child in node.children) { + child.accept(this); + } } }, through: (node) => node is CssStyleRule, @@ -2032,31 +2026,35 @@ class _EvaluateVisitor } /// Evaluates [condition] and converts it to a plain CSS string. - String _visitSupportsCondition(SupportsCondition condition) { - if (condition is SupportsOperation) { - return "${_parenthesize(condition.left, condition.operator)} " - "${condition.operator} " - "${_parenthesize(condition.right, condition.operator)}"; - } else if (condition is SupportsNegation) { - return "not ${_parenthesize(condition.condition)}"; - } else if (condition is SupportsInterpolation) { - return _evaluateToCss(condition.expression, quote: false); - } else if (condition is SupportsDeclaration) { - var oldInSupportsDeclaration = _inSupportsDeclaration; - _inSupportsDeclaration = true; - var result = "(${_evaluateToCss(condition.name)}:" - "${condition.isCustomProperty ? '' : ' '}" - "${_evaluateToCss(condition.value)})"; + String _visitSupportsCondition(SupportsCondition condition) => + switch (condition) { + SupportsOperation operation => + "${_parenthesize(operation.left, operation.operator)} " + "${operation.operator} " + "${_parenthesize(operation.right, operation.operator)}", + SupportsNegation negation => "not ${_parenthesize(negation.condition)}", + SupportsInterpolation interpolation => + _evaluateToCss(interpolation.expression, quote: false), + SupportsDeclaration declaration => + _withSupportsDeclaration(() => "(${_evaluateToCss(declaration.name)}:" + "${declaration.isCustomProperty ? '' : ' '}" + "${_evaluateToCss(declaration.value)})"), + SupportsFunction function => "${_performInterpolation(function.name)}(" + "${_performInterpolation(function.arguments)})", + SupportsAnything anything => + "(${_performInterpolation(anything.contents)})", + var condition => throw ArgumentError( + "Unknown supports condition type ${condition.runtimeType}.") + }; + + /// Runs [callback] in a context where [_inSupportsDeclaration] is true. + T _withSupportsDeclaration(T callback()) { + var oldInSupportsDeclaration = _inSupportsDeclaration; + _inSupportsDeclaration = true; + try { + return callback(); + } finally { _inSupportsDeclaration = oldInSupportsDeclaration; - return result; - } else if (condition is SupportsFunction) { - return "${_performInterpolation(condition.name)}(" - "${_performInterpolation(condition.arguments)})"; - } else if (condition is SupportsAnything) { - return "(${_performInterpolation(condition.contents)})"; - } else { - throw ArgumentError( - "Unknown supports condition type ${condition.runtimeType}."); } } @@ -2067,20 +2065,22 @@ class _EvaluateVisitor /// [SupportsOperation], and is used to determine whether parentheses are /// necessary if [condition] is also a [SupportsOperation]. String _parenthesize(SupportsCondition condition, [String? operator]) { - if ((condition is SupportsNegation) || - (condition is SupportsOperation && - (operator == null || operator != condition.operator))) { - return "(${_visitSupportsCondition(condition)})"; - } else { - return _visitSupportsCondition(condition); + switch (condition) { + case SupportsNegation(): + case SupportsOperation() + when operator == null || operator != condition.operator: + return "(${_visitSupportsCondition(condition)})"; + + case _: + return _visitSupportsCondition(condition); } } Value? visitVariableDeclaration(VariableDeclaration node) { if (node.isGuarded) { if (node.namespace == null && _environment.atRoot) { - var override = _configuration.remove(node.name); - if (override != null && override.value != sassNull) { + if (_configuration.remove(node.name) case var override? + when override.value != sassNull) { _addExceptionSpan(node, () { _environment.setVariable( node.name, override.value, override.assignmentNode, @@ -2157,9 +2157,11 @@ class _EvaluateVisitor Value? visitWhileRule(WhileRule node) { return _environment.scope(() { while (node.condition.accept(this).isTruthy) { - var result = _handleReturn( - node.children, (child) => child.accept(this)); - if (result != null) return result; + if (_handleReturn( + node.children, (child) => child.accept(this)) + case var result?) { + return result; + } } return null; }, semiGlobal: true, when: node.hasDeclarations); @@ -2170,96 +2172,65 @@ class _EvaluateVisitor Value visitBinaryOperationExpression(BinaryOperationExpression node) { return _addExceptionSpan(node, () { var left = node.left.accept(this); - switch (node.operator) { - case BinaryOperator.singleEquals: - var right = node.right.accept(this); - return left.singleEquals(right); - - case BinaryOperator.or: - return left.isTruthy ? left : node.right.accept(this); - - case BinaryOperator.and: - return left.isTruthy ? node.right.accept(this) : left; - - case BinaryOperator.equals: - var right = node.right.accept(this); - return SassBoolean(left == right); - - case BinaryOperator.notEquals: - var right = node.right.accept(this); - return SassBoolean(left != right); - - case BinaryOperator.greaterThan: - var right = node.right.accept(this); - return left.greaterThan(right); - - case BinaryOperator.greaterThanOrEquals: - var right = node.right.accept(this); - return left.greaterThanOrEquals(right); - - case BinaryOperator.lessThan: - var right = node.right.accept(this); - return left.lessThan(right); - - case BinaryOperator.lessThanOrEquals: - var right = node.right.accept(this); - return left.lessThanOrEquals(right); - - case BinaryOperator.plus: - var right = node.right.accept(this); - return left.plus(right); - - case BinaryOperator.minus: - var right = node.right.accept(this); - return left.minus(right); - - case BinaryOperator.times: - var right = node.right.accept(this); - return left.times(right); - - case BinaryOperator.dividedBy: - var right = node.right.accept(this); - var result = left.dividedBy(right); - if (node.allowsSlash && left is SassNumber && right is SassNumber) { - return (result as SassNumber).withSlash(left, right); - } else { - if (left is SassNumber && right is SassNumber) { - String recommendation(Expression expression) { - if (expression is BinaryOperationExpression && - expression.operator == BinaryOperator.dividedBy) { - return "math.div(${recommendation(expression.left)}, " - "${recommendation(expression.right)})"; - } else if (expression is ParenthesizedExpression) { - return expression.expression.toString(); - } else { - return expression.toString(); - } - } - - _warn( - "Using / for division outside of calc() is deprecated " - "and will be removed in Dart Sass 2.0.0.\n" - "\n" - "Recommendation: ${recommendation(node)} or " - "${expressionToCalc(node)}\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/slash-div", - node.span, - Deprecation.slashDiv); - } + return switch (node.operator) { + BinaryOperator.singleEquals => + left.singleEquals(node.right.accept(this)), + BinaryOperator.or => left.isTruthy ? left : node.right.accept(this), + BinaryOperator.and => left.isTruthy ? node.right.accept(this) : left, + BinaryOperator.equals => SassBoolean(left == node.right.accept(this)), + BinaryOperator.notEquals => + SassBoolean(left != node.right.accept(this)), + BinaryOperator.greaterThan => left.greaterThan(node.right.accept(this)), + BinaryOperator.greaterThanOrEquals => + left.greaterThanOrEquals(node.right.accept(this)), + BinaryOperator.lessThan => left.lessThan(node.right.accept(this)), + BinaryOperator.lessThanOrEquals => + left.lessThanOrEquals(node.right.accept(this)), + BinaryOperator.plus => left.plus(node.right.accept(this)), + BinaryOperator.minus => left.minus(node.right.accept(this)), + BinaryOperator.times => left.times(node.right.accept(this)), + BinaryOperator.dividedBy => _slash(left, node.right.accept(this), node), + BinaryOperator.modulo => left.modulo(node.right.accept(this)) + }; + }); + } - return result; - } + /// Returns the result of the SassScript `/` operation between [left] and + /// [right] in [node]. + Value _slash(Value left, Value right, BinaryOperationExpression node) { + var result = left.dividedBy(right); + switch ((left, right)) { + case (SassNumber left, SassNumber right) when node.allowsSlash: + return (result as SassNumber).withSlash(left, right); + + case (SassNumber(), SassNumber()): + String recommendation(Expression expression) => switch (expression) { + BinaryOperationExpression( + operator: BinaryOperator.dividedBy, + :var left, + :var right + ) => + "math.div(${recommendation(left)}, ${recommendation(right)})", + ParenthesizedExpression() => expression.expression.toString(), + _ => expression.toString() + }; - case BinaryOperator.modulo: - var right = node.right.accept(this); - return left.modulo(right); + _warn( + "Using / for division outside of calc() is deprecated " + "and will be removed in Dart Sass 2.0.0.\n" + "\n" + "Recommendation: ${recommendation(node)} or " + "${expressionToCalc(node)}\n" + "\n" + "More info and automated migrator: " + "https://sass-lang.com/d/slash-div", + node.span, + Deprecation.slashDiv); + return result; - default: - throw ArgumentError("Unknown binary operator ${node.operator}."); - } - }); + case _: + return result; + } } Value visitValueExpression(ValueExpression node) => node.value; @@ -2274,18 +2245,12 @@ class _EvaluateVisitor Value visitUnaryOperationExpression(UnaryOperationExpression node) { var operand = node.operand.accept(this); return _addExceptionSpan(node, () { - switch (node.operator) { - case UnaryOperator.plus: - return operand.unaryPlus(); - case UnaryOperator.minus: - return operand.unaryMinus(); - case UnaryOperator.divide: - return operand.unaryDivide(); - case UnaryOperator.not: - return operand.unaryNot(); - default: - throw StateError("Unknown unary operator ${node.operator}."); - } + return switch (node.operator) { + UnaryOperator.plus => operand.unaryPlus(), + UnaryOperator.minus => operand.unaryMinus(), + UnaryOperator.divide => operand.unaryDivide(), + UnaryOperator.not => operand.unaryNot() + }; }); } @@ -2293,16 +2258,13 @@ class _EvaluateVisitor SassBoolean(node.value); Value visitIfExpression(IfExpression node) { - var pair = _evaluateMacroArguments(node); - var positional = pair.item1; - var named = pair.item2; - + var (positional, named) = _evaluateMacroArguments(node); _verifyArguments(positional.length, named, IfExpression.declaration, node); // ignore: prefer_is_empty - var condition = positional.length > 0 ? positional[0] : named["condition"]!; - var ifTrue = positional.length > 1 ? positional[1] : named["if-true"]!; - var ifFalse = positional.length > 2 ? positional[2] : named["if-false"]!; + var condition = positional.elementAtOrNull(0) ?? named["condition"]!; + var ifTrue = positional.elementAtOrNull(1) ?? named["if-true"]!; + var ifFalse = positional.elementAtOrNull(2) ?? named["if-false"]!; var result = condition.accept(this).isTruthy ? ifTrue : ifFalse; return _withoutSlash(result.accept(this), _expressionNode(result)); @@ -2327,28 +2289,20 @@ class _EvaluateVisitor } try { - switch (node.name) { - case "calc": - assert(arguments.length == 1); - return SassCalculation.calc(arguments[0]); - case "min": - return SassCalculation.min(arguments); - case "max": - return SassCalculation.max(arguments); - case "clamp": - return SassCalculation.clamp( - arguments[0], - arguments.length > 1 ? arguments[1] : null, - arguments.length > 2 ? arguments[2] : null); - default: - throw UnsupportedError('Unknown calculation name "${node.name}".'); - } + return switch (node.name) { + "calc" => SassCalculation.calc(arguments[0]), + "min" => SassCalculation.min(arguments), + "max" => SassCalculation.max(arguments), + "clamp" => SassCalculation.clamp(arguments[0], + arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), + _ => throw UnsupportedError('Unknown calculation name "${node.name}".') + }; } on SassScriptException catch (error, stackTrace) { // The simplification logic in the [SassCalculation] static methods will // throw an error if the arguments aren't compatible, but we have access // to the original spans so we can throw a more informative error. _verifyCompatibleNumbers(arguments, node.arguments); - throwWithTrace(_exception(error.message, node.span), stackTrace); + throwWithTrace(_exception(error.message, node.span), error, stackTrace); } } @@ -2362,9 +2316,7 @@ class _EvaluateVisitor // SassCalculation._verifyCompatibleNumbers and most changes here should // also be reflected there. for (var i = 0; i < args.length; i++) { - var arg = args[i]; - if (arg is! SassNumber) continue; - if (arg.numeratorUnits.length > 1 || arg.denominatorUnits.isNotEmpty) { + if (args[i] case SassNumber arg when arg.hasComplexUnits) { throw _exception("Number $arg isn't compatible with CSS calculations.", nodesWithSpans[i].span); } @@ -2395,78 +2347,70 @@ class _EvaluateVisitor /// subtracted with numbers with units, for backwards-compatibility with the /// old global `min()` and `max()` functions. Object _visitCalculationValue(Expression node, {required bool inMinMax}) { - if (node is ParenthesizedExpression) { - var inner = node.expression; - var result = _visitCalculationValue(inner, inMinMax: inMinMax); - return inner is FunctionExpression && - inner.name.toLowerCase() == 'var' && - result is SassString && - !result.hasQuotes - ? SassString('(${result.text})', quotes: false) - : result; - } else if (node is StringExpression) { - assert(!node.hasQuotes); - var text = node.text.asPlain; + switch (node) { + case ParenthesizedExpression(expression: var inner): + var result = _visitCalculationValue(inner, inMinMax: inMinMax); + return inner is FunctionExpression && + inner.name.toLowerCase() == 'var' && + result is SassString && + !result.hasQuotes + ? SassString('(${result.text})', quotes: false) + : result; + + case StringExpression(text: Interpolation(asPlain: var text?)): + assert(!node.hasQuotes); + return switch (text.toLowerCase()) { + 'pi' => SassNumber(math.pi), + 'e' => SassNumber(math.e), + 'infinity' => SassNumber(double.infinity), + '-infinity' => SassNumber(double.negativeInfinity), + 'nan' => SassNumber(double.nan), + _ => SassString(text, quotes: false) + }; + // If there's actual interpolation, create a CalculationInterpolation. // Otherwise, create an UnquotedString. The main difference is that // UnquotedStrings don't get extra defensive parentheses. - if (text == null) { + case StringExpression(): + assert(!node.hasQuotes); return CalculationInterpolation(_performInterpolation(node.text)); - } - switch (text.toLowerCase()) { - case 'pi': - return SassNumber(math.pi); - case 'e': - return SassNumber(math.e); - case 'infinity': - return SassNumber(double.infinity); - case '-infinity': - return SassNumber(double.negativeInfinity); - case 'nan': - return SassNumber(double.nan); - default: - return SassString(text, quotes: false); - } - } else if (node is BinaryOperationExpression) { - return _addExceptionSpan( - node, - () => SassCalculation.operateInternal( - _binaryOperatorToCalculationOperator(node.operator), - _visitCalculationValue(node.left, inMinMax: inMinMax), - _visitCalculationValue(node.right, inMinMax: inMinMax), - inMinMax: inMinMax, - simplify: !_inSupportsDeclaration)); - } else { - assert(node is NumberExpression || - node is CalculationExpression || - node is VariableExpression || - node is FunctionExpression || - node is IfExpression); - var result = node.accept(this); - if (result is SassNumber || result is SassCalculation) return result; - if (result is SassString && !result.hasQuotes) return result; - throw _exception( - "Value $result can't be used in a calculation.", node.span); + case BinaryOperationExpression(:var operator, :var left, :var right): + return _addExceptionSpan( + node, + () => SassCalculation.operateInternal( + _binaryOperatorToCalculationOperator(operator), + _visitCalculationValue(left, inMinMax: inMinMax), + _visitCalculationValue(right, inMinMax: inMinMax), + inMinMax: inMinMax, + simplify: !_inSupportsDeclaration)); + + case _: + assert(node is NumberExpression || + node is CalculationExpression || + node is VariableExpression || + node is FunctionExpression || + node is IfExpression); + return switch (node.accept(this)) { + SassNumber result => result, + SassCalculation result => result, + SassString result when !result.hasQuotes => result, + var result => throw _exception( + "Value $result can't be used in a calculation.", node.span) + }; } } /// Returns the [CalculationOperator] that corresponds to [operator]. CalculationOperator _binaryOperatorToCalculationOperator( - BinaryOperator operator) { - switch (operator) { - case BinaryOperator.plus: - return CalculationOperator.plus; - case BinaryOperator.minus: - return CalculationOperator.minus; - case BinaryOperator.times: - return CalculationOperator.times; - case BinaryOperator.dividedBy: - return CalculationOperator.dividedBy; - default: - throw UnsupportedError("Invalid calculation operator $operator."); - } - } + BinaryOperator operator) => + switch (operator) { + BinaryOperator.plus => CalculationOperator.plus, + BinaryOperator.minus => CalculationOperator.minus, + BinaryOperator.times => CalculationOperator.times, + BinaryOperator.dividedBy => CalculationOperator.dividedBy, + _ => throw UnsupportedError("Invalid calculation operator $operator.") + }; SassColor visitColorExpression(ColorExpression node) => node.value; @@ -2478,22 +2422,22 @@ class _EvaluateVisitor SassMap visitMapExpression(MapExpression node) { var map = {}; var keyNodes = {}; - for (var pair in node.pairs) { - var keyValue = pair.item1.accept(this); - var valueValue = pair.item2.accept(this); + for (var (key, value) in node.pairs) { + var keyValue = key.accept(this); + var valueValue = value.accept(this); var oldValue = map[keyValue]; if (oldValue != null) { var oldValueSpan = keyNodes[keyValue]?.span; throw MultiSpanSassRuntimeException( 'Duplicate key.', - pair.item1.span, + key.span, 'second key', {if (oldValueSpan != null) oldValueSpan: 'first key'}, - _stackTrace(pair.item1.span)); + _stackTrace(key.span)); } map[keyValue] = valueValue; - keyNodes[keyValue] = pair.item1; + keyNodes[keyValue] = key; } return SassMap(map); } @@ -2689,9 +2633,8 @@ class _EvaluateVisitor _callableNode = nodeWithSpan; var namedSet = MapKeySet(evaluated.named); - var tuple = callable.callbackFor(evaluated.positional.length, namedSet); - var overload = tuple.item1; - var callback = tuple.item2; + var (overload, callback) = + callable.callbackFor(evaluated.positional.length, namedSet); _addExceptionSpan(nodeWithSpan, () => overload.verify(evaluated.positional.length, namedSet)); @@ -2736,7 +2679,7 @@ class _EvaluateVisitor } catch (_) { message = error.toString(); } - throwWithTrace(_exception(message, nodeWithSpan.span), stackTrace); + throwWithTrace(_exception(message, nodeWithSpan.span), error, stackTrace); } _callableNode = oldCallableNode; @@ -2771,16 +2714,21 @@ class _EvaluateVisitor var named = {}; var namedNodes = {}; - for (var entry in arguments.named.entries) { - var nodeForSpan = _expressionNode(entry.value); - named[entry.key] = _withoutSlash(entry.value.accept(this), nodeForSpan); - namedNodes[entry.key] = nodeForSpan; + for (var (name, value) in arguments.named.pairs) { + var nodeForSpan = _expressionNode(value); + named[name] = _withoutSlash(value.accept(this), nodeForSpan); + namedNodes[name] = nodeForSpan; } var restArgs = arguments.rest; if (restArgs == null) { - return _ArgumentResults(positional, positionalNodes, named, namedNodes, - ListSeparator.undecided); + return ( + positional: positional, + positionalNodes: positionalNodes, + named: named, + namedNodes: namedNodes, + separator: ListSeparator.undecided + ); } var rest = restArgs.accept(this); @@ -2811,8 +2759,13 @@ class _EvaluateVisitor var keywordRestArgs = arguments.keywordRest; if (keywordRestArgs == null) { - return _ArgumentResults( - positional, positionalNodes, named, namedNodes, separator); + return ( + positional: positional, + positionalNodes: positionalNodes, + named: named, + namedNodes: namedNodes, + separator: separator + ); } var keywordRest = keywordRestArgs.accept(this); @@ -2823,8 +2776,13 @@ class _EvaluateVisitor for (var key in keywordRest.contents.keys) (key as SassString).text: keywordRestNodeForSpan }); - return _ArgumentResults( - positional, positionalNodes, named, namedNodes, separator); + return ( + positional: positional, + positionalNodes: positionalNodes, + named: named, + namedNodes: namedNodes, + separator: separator + ); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", @@ -2837,12 +2795,11 @@ class _EvaluateVisitor /// /// Returns the arguments as expressions so that they can be lazily evaluated /// for macros such as `if()`. - Tuple2, Map> _evaluateMacroArguments( - CallableInvocation invocation) { + (List positional, Map named) + _evaluateMacroArguments(CallableInvocation invocation) { var restArgs_ = invocation.arguments.rest; if (restArgs_ == null) { - return Tuple2( - invocation.arguments.positional, invocation.arguments.named); + return (invocation.arguments.positional, invocation.arguments.named); } var restArgs = restArgs_; // dart-lang/sdk#45348 @@ -2868,7 +2825,7 @@ class _EvaluateVisitor } var keywordRestArgs_ = invocation.arguments.keywordRest; - if (keywordRestArgs_ == null) return Tuple2(positional, named); + if (keywordRestArgs_ == null) return (positional, named); var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348 var keywordRest = keywordRestArgs.accept(this); @@ -2881,7 +2838,7 @@ class _EvaluateVisitor (value) => ValueExpression( _withoutSlash(value, keywordRestNodeForSpan), keywordRestArgs.span)); - return Tuple2(positional, named); + return (positional, named); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", @@ -2931,14 +2888,17 @@ class _EvaluateVisitor var oldInSupportsDeclaration = _inSupportsDeclaration; _inSupportsDeclaration = false; var result = SassString( - node.text.contents.map((value) { - if (value is String) return value; - var expression = value as Expression; - var result = expression.accept(this); - return result is SassString - ? result.text - : _serialize(result, expression, quote: false); - }).join(), + [ + for (var value in node.text.contents) + switch (value) { + String() => value, + Expression() => switch (value.accept(this)) { + SassString(:var text) => text, + var result => _serialize(result, value, quote: false) + }, + _ => throw UnsupportedError("Unknown interpolation value $value") + } + ].join(), quotes: node.hasQuotes); _inSupportsDeclaration = oldInSupportsDeclaration; return result; @@ -3061,12 +3021,7 @@ class _EvaluateVisitor _withParent( ModifiableCssMediaRule(mergedQueries ?? node.queries, node.span), () { _withMediaQueries(mergedQueries ?? node.queries, mergedSources, () { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the media query so that // declarations immediately inside @media have somewhere to go. // @@ -3077,6 +3032,10 @@ class _EvaluateVisitor child.accept(this); } }, scopeWhen: false); + } else { + for (var child in node.children) { + child.accept(this); + } } }); }, @@ -3115,8 +3074,7 @@ class _EvaluateVisitor }, through: (node) => node is CssStyleRule, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - if (styleRule == null && _parent.children.isNotEmpty) { - var lastChild = _parent.children.last; + if (_parent.children case [..., var lastChild] when styleRule == null) { lastChild.isGroupEnd = true; } } @@ -3138,12 +3096,7 @@ class _EvaluateVisitor } _withParent(ModifiableCssSupportsRule(node.condition, node.span), () { - var styleRule = _styleRule; - if (styleRule == null) { - for (var child in node.children) { - child.accept(this); - } - } else { + if (_styleRule case var styleRule?) { // If we're in a style rule, copy it into the supports rule so that // declarations immediately inside @supports have somewhere to go. // @@ -3154,6 +3107,10 @@ class _EvaluateVisitor child.accept(this); } }); + } else { + for (var child in node.children) { + child.accept(this); + } } }, through: (node) => node is CssStyleRule, scopeWhen: false); } @@ -3166,8 +3123,7 @@ class _EvaluateVisitor /// returned `null`. Value? _handleReturn(List list, Value? callback(T value)) { for (var value in list) { - var result = callback(value); - if (result != null) return result; + if (callback(value) case var result?) return result; } return null; } @@ -3200,25 +3156,25 @@ class _EvaluateVisitor /// values passed into the interpolation. String _performInterpolation(Interpolation interpolation, {bool warnForColor = false}) { - var tuple = _performInterpolationHelper(interpolation, + var (result, _) = _performInterpolationHelper(interpolation, sourceMap: true, warnForColor: warnForColor); - return tuple.item1; + return result; } /// Like [_performInterpolation], but also returns a [InterpolationMap] that /// can map spans from the resulting string back to the original /// [interpolation]. - Tuple2 _performInterpolationWithMap( + (String, InterpolationMap) _performInterpolationWithMap( Interpolation interpolation, {bool warnForColor = false}) { - var tuple = _performInterpolationHelper(interpolation, + var (result, map) = _performInterpolationHelper(interpolation, sourceMap: true, warnForColor: warnForColor); - return Tuple2(tuple.item1, tuple.item2!); + return (result, map!); } /// A helper that implements the core logic of both [_performInterpolation] /// and [_performInterpolationWithMap]. - Tuple2 _performInterpolationHelper( + (String, InterpolationMap?) _performInterpolationHelper( Interpolation interpolation, {required bool sourceMap, bool warnForColor = false}) { @@ -3239,9 +3195,7 @@ class _EvaluateVisitor var expression = value as Expression; var result = expression.accept(this); - if (warnForColor && - result is SassColor && - namesByColor.containsKey(result)) { + if (warnForColor && namesByColor.containsKey(result)) { var alternative = BinaryOperationExpression( BinaryOperator.plus, StringExpression(Interpolation([""], interpolation.span), @@ -3262,11 +3216,11 @@ class _EvaluateVisitor } _inSupportsDeclaration = oldInSupportsDeclaration; - return Tuple2( - buffer.toString(), - targetLocations == null - ? null - : InterpolationMap(interpolation, targetLocations)); + return ( + buffer.toString(), + targetLocations.andThen( + (targetLocations) => InterpolationMap(interpolation, targetLocations)) + ); } /// Evaluates [expression] and calls `toCssString()` and wraps a @@ -3339,12 +3293,12 @@ class _EvaluateVisitor var parent = _parent; if (through != null) { while (through(parent)) { - var grandparent = parent.parent; - if (grandparent == null) { + if (parent.parent case var grandparent?) { + parent = grandparent; + } else { throw ArgumentError( "through() must return false for at least one parent of $node."); } - parent = grandparent; } // If the parent has a (visible) following sibling, we shouldn't add to @@ -3402,7 +3356,7 @@ class _EvaluateVisitor /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. T _withStackFrame(String member, AstNode nodeWithSpan, T callback()) { - _stack.add(Tuple2(_member, nodeWithSpan)); + _stack.add((_member, nodeWithSpan)); var oldMember = _member; _member = member; var result = callback(); @@ -3414,16 +3368,12 @@ class _EvaluateVisitor /// Like [Value.withoutSlash], but produces a deprecation warning if [value] /// was a slash-separated number. Value _withoutSlash(Value value, AstNode nodeForSpan) { - if (value is SassNumber && value.asSlash != null) { - String recommendation(SassNumber number) { - var asSlash = number.asSlash; - if (asSlash != null) { - return "math.div(${recommendation(asSlash.item1)}, " - "${recommendation(asSlash.item2)})"; - } else { - return number.toString(); - } - } + if (value case SassNumber(asSlash: _?)) { + String recommendation(SassNumber number) => switch (number.asSlash) { + (var before, var after) => + "math.div(${recommendation(before)}, ${recommendation(after)})", + _ => number.toString() + }; _warn( "Using / for division is deprecated and will be removed in Dart Sass " @@ -3450,7 +3400,8 @@ class _EvaluateVisitor /// If [span] is passed, it's used for the innermost stack frame. Trace _stackTrace([FileSpan? span]) { var frames = [ - ..._stack.map((tuple) => _stackFrame(tuple.item1, tuple.item2.span)), + for (var (member, nodeWithSpan) in _stack) + _stackFrame(member, nodeWithSpan.span), if (span != null) _stackFrame(_member, span) ]; return Trace(frames.reversed); @@ -3463,7 +3414,7 @@ class _EvaluateVisitor return; } - if (!_warningsEmitted.add(Tuple2(message, span))) return; + if (!_warningsEmitted.add((message, span))) return; var trace = _stackTrace(span); if (deprecation == null) { _logger.warn(message, span: span, trace: trace); @@ -3478,7 +3429,7 @@ class _EvaluateVisitor /// If [span] is passed, it's used for the innermost stack frame. SassRuntimeException _exception(String message, [FileSpan? span]) => SassRuntimeException( - message, span ?? _stack.last.item2.span, _stackTrace(span)); + message, span ?? _stack.last.$2.span, _stackTrace(span)); /// Returns a [MultiSpanSassRuntimeException] with the given [message], /// [primaryLabel], and [secondaryLabels]. @@ -3486,8 +3437,8 @@ class _EvaluateVisitor /// The primary span is taken from the current stack trace span. SassRuntimeException _multiSpanException(String message, String primaryLabel, Map secondaryLabels) => - MultiSpanSassRuntimeException(message, _stack.last.item2.span, - primaryLabel, secondaryLabels, _stackTrace()); + MultiSpanSassRuntimeException(message, _stack.last.$2.span, primaryLabel, + secondaryLabels, _stackTrace()); /// Runs [callback], and converts any [SassScriptException]s it throws to /// [SassRuntimeException]s with [nodeWithSpan]'s source span. @@ -3507,6 +3458,7 @@ class _EvaluateVisitor error .withSpan(nodeWithSpan.span) .withTrace(_stackTrace(addStackFrame ? nodeWithSpan.span : null)), + error, stackTrace); } } @@ -3520,7 +3472,8 @@ class _EvaluateVisitor } on SassRuntimeException { rethrow; } on SassException catch (error, stackTrace) { - throwWithTrace(error.withTrace(_stackTrace(error.span)), stackTrace); + throwWithTrace( + error.withTrace(_stackTrace(error.span)), error, stackTrace); } } @@ -3534,6 +3487,7 @@ class _EvaluateVisitor if (!error.span.text.startsWith("@error")) rethrow; throwWithTrace( SassRuntimeException(error.message, nodeWithSpan.span, _stackTrace()), + error, stackTrace); } } @@ -3550,7 +3504,7 @@ class _EvaluateVisitor /// because it will add the parent selector to the CSS if the `@import` appeared /// in a nested context, but the parent selector was already added when the /// imported stylesheet was evaluated. -class _ImportedCssVisitor implements ModifiableCssVisitor { +final class _ImportedCssVisitor implements ModifiableCssVisitor { /// The visitor in whose context this was created. final _EvaluateVisitor _visitor; @@ -3610,7 +3564,7 @@ class _ImportedCssVisitor implements ModifiableCssVisitor { /// An implementation of [EvaluationContext] using the information available in /// [_EvaluateVisitor]. -class _EvaluationContext implements EvaluationContext { +final class _EvaluationContext implements EvaluationContext { /// The visitor backing this context. final _EvaluateVisitor _visitor; @@ -3621,8 +3575,7 @@ class _EvaluationContext implements EvaluationContext { _EvaluationContext(this._visitor, this._defaultWarnNodeWithSpan); FileSpan get currentCallableSpan { - var callableNode = _visitor._callableNode; - if (callableNode != null) return callableNode.span; + if (_visitor._callableNode case var callableNode?) return callableNode.span; throw StateError("No Sass callable is currently being evaluated."); } @@ -3637,50 +3590,43 @@ class _EvaluationContext implements EvaluationContext { } /// The result of evaluating arguments to a function or mixin. -class _ArgumentResults { +typedef _ArgumentResults = ({ /// Arguments passed by position. - final List positional; + List positional, /// The [AstNode]s that hold the spans for each [positional] argument. /// /// This stores [AstNode]s rather than [FileSpan]s 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. - final List positionalNodes; + List positionalNodes, /// Arguments passed by name. - final Map named; + Map named, /// The [AstNode]s that hold the spans for each [named] argument. /// /// This stores [AstNode]s rather than [FileSpan]s 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. - final Map namedNodes; + Map namedNodes, /// The separator used for the rest argument list, if any. - final ListSeparator separator; - - _ArgumentResults(this.positional, this.positionalNodes, this.named, - this.namedNodes, this.separator); -} + ListSeparator separator +}); /// The result of loading a stylesheet via [Evaluator._loadStylesheet]. -class _LoadedStylesheet { +typedef _LoadedStylesheet = ( /// The stylesheet itself. - final Stylesheet stylesheet; - + Stylesheet stylesheet, { /// The importer that was used to load the stylesheet. /// /// This is `null` when running in Node Sass compatibility mode. - final Importer? importer; + Importer? importer, /// Whether this load counts as a dependency. /// /// That is, whether this was (transitively) loaded through a load path or /// importer rather than relative to the entrypoint. - final bool isDependency; - - _LoadedStylesheet(this.stylesheet, - {this.importer, required this.isDependency}); -} + bool isDependency +}); diff --git a/lib/src/visitor/expression_to_calc.dart b/lib/src/visitor/expression_to_calc.dart index c434046fe..961735655 100644 --- a/lib/src/visitor/expression_to_calc.dart +++ b/lib/src/visitor/expression_to_calc.dart @@ -37,17 +37,15 @@ class _MakeExpressionCalculationSafe with ReplaceExpressionVisitor { InterpolatedFunctionExpression node) => node; - Expression visitUnaryOperationExpression(UnaryOperationExpression node) { - // `calc()` doesn't support unary operations. - if (node.operator == UnaryOperator.plus) { - return node.operand; - } else if (node.operator == UnaryOperator.minus) { - return BinaryOperationExpression( - BinaryOperator.times, NumberExpression(-1, node.span), node.operand); - } else { - // Other unary operations don't produce numbers, so keep them as-is to - // give the user a more useful syntax error after serialization. - return super.visitUnaryOperationExpression(node); - } - } + Expression visitUnaryOperationExpression(UnaryOperationExpression node) => + switch (node.operator) { + // `calc()` doesn't support unary operations. + UnaryOperator.plus => node.operand, + UnaryOperator.minus => BinaryOperationExpression(BinaryOperator.times, + NumberExpression(-1, node.span), node.operand), + _ => + // Other unary operations don't produce numbers, so keep them as-is to + // give the user a more useful syntax error after serialization. + super.visitUnaryOperationExpression(node) + }; } diff --git a/lib/src/visitor/find_dependencies.dart b/lib/src/visitor/find_dependencies.dart index ca4dfb934..94607e952 100644 --- a/lib/src/visitor/find_dependencies.dart +++ b/lib/src/visitor/find_dependencies.dart @@ -66,20 +66,20 @@ class _FindDependenciesVisitor with RecursiveStatementVisitor { void visitIncludeRule(IncludeRule node) { if (node.name != 'load-css') return; if (!_metaNamespaces.contains(node.namespace)) return; - if (node.arguments.positional.isEmpty) return; - var argument = node.arguments.positional.first; - if (argument is! StringExpression) return; - var url = argument.text.asPlain; - try { - if (url != null) _metaLoadCss.add(Uri.parse(url)); - } on FormatException { - // Ignore invalid URLs. + + if (node.arguments.positional + case [StringExpression(text: Interpolation(asPlain: var url?))]) { + try { + _metaLoadCss.add(Uri.parse(url)); + } on FormatException { + // Ignore invalid URLs. + } } } } /// A struct of different types of dependencies a Sass stylesheet can contain. -class DependencyReport { +final class DependencyReport { /// An unmodifiable set of all `@use`d URLs in the stylesheet (excluding /// built-in modules). final Set uses; diff --git a/lib/src/visitor/interface/css.dart b/lib/src/visitor/interface/css.dart index 06485582a..032149b02 100644 --- a/lib/src/visitor/interface/css.dart +++ b/lib/src/visitor/interface/css.dart @@ -8,7 +8,7 @@ import 'modifiable_css.dart'; /// An interface for [visitors][] that traverse CSS statements. /// /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern -abstract class CssVisitor implements ModifiableCssVisitor { +abstract interface class CssVisitor implements ModifiableCssVisitor { T visitCssAtRule(CssAtRule node); T visitCssComment(CssComment node); T visitCssDeclaration(CssDeclaration node); diff --git a/lib/src/visitor/interface/expression.dart b/lib/src/visitor/interface/expression.dart index 0a642ec64..db5f70f32 100644 --- a/lib/src/visitor/interface/expression.dart +++ b/lib/src/visitor/interface/expression.dart @@ -9,7 +9,7 @@ import '../../ast/sass.dart'; /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern /// /// {@category Visitor} -abstract class ExpressionVisitor { +abstract interface class ExpressionVisitor { T visitBinaryOperationExpression(BinaryOperationExpression node); T visitBooleanExpression(BooleanExpression node); T visitCalculationExpression(CalculationExpression node); diff --git a/lib/src/visitor/interface/modifiable_css.dart b/lib/src/visitor/interface/modifiable_css.dart index 643dc9b0a..2683f2f15 100644 --- a/lib/src/visitor/interface/modifiable_css.dart +++ b/lib/src/visitor/interface/modifiable_css.dart @@ -7,7 +7,7 @@ import '../../ast/css/modifiable.dart'; /// An interface for [visitors][] that traverse CSS statements. /// /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern -abstract class ModifiableCssVisitor { +abstract interface class ModifiableCssVisitor { T visitCssAtRule(ModifiableCssAtRule node); T visitCssComment(ModifiableCssComment node); T visitCssDeclaration(ModifiableCssDeclaration node); diff --git a/lib/src/visitor/interface/selector.dart b/lib/src/visitor/interface/selector.dart index 91b68913c..a680e1aed 100644 --- a/lib/src/visitor/interface/selector.dart +++ b/lib/src/visitor/interface/selector.dart @@ -9,7 +9,7 @@ import '../../ast/selector.dart'; /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern /// /// {@category Visitor} -abstract class SelectorVisitor { +abstract interface class SelectorVisitor { T visitAttributeSelector(AttributeSelector attribute); T visitClassSelector(ClassSelector klass); T visitComplexSelector(ComplexSelector complex); diff --git a/lib/src/visitor/interface/statement.dart b/lib/src/visitor/interface/statement.dart index c1bf6e470..610c488bf 100644 --- a/lib/src/visitor/interface/statement.dart +++ b/lib/src/visitor/interface/statement.dart @@ -9,7 +9,7 @@ import '../../ast/sass.dart'; /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern /// /// {@category Visitor} -abstract class StatementVisitor { +abstract interface class StatementVisitor { T visitAtRootRule(AtRootRule node); T visitAtRule(AtRule node); T visitContentBlock(ContentBlock node); diff --git a/lib/src/visitor/interface/value.dart b/lib/src/visitor/interface/value.dart index 5b98de42e..db25c86d5 100644 --- a/lib/src/visitor/interface/value.dart +++ b/lib/src/visitor/interface/value.dart @@ -7,7 +7,7 @@ import '../../value.dart'; /// An interface for [visitors][] that traverse SassScript values. /// /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern -abstract class ValueVisitor { +abstract interface class ValueVisitor { T visitBoolean(SassBoolean value); T visitCalculation(SassCalculation value); T visitColor(SassColor value); diff --git a/lib/src/visitor/recursive_ast.dart b/lib/src/visitor/recursive_ast.dart index aee77c869..0b31aafe2 100644 --- a/lib/src/visitor/recursive_ast.dart +++ b/lib/src/visitor/recursive_ast.dart @@ -80,11 +80,11 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor } } - node.lastClause.andThen((lastClause) { + if (node.lastClause case var lastClause?) { for (var child in lastClause.children) { child.accept(this); } - }); + } } void visitImportRule(ImportRule node) { @@ -185,9 +185,9 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor } void visitMapExpression(MapExpression node) { - for (var pair in node.pairs) { - pair.item1.accept(this); - pair.item2.accept(this); + for (var (key, value) in node.pairs) { + key.accept(this); + value.accept(this); } } @@ -247,16 +247,17 @@ mixin RecursiveAstVisitor on RecursiveStatementVisitor /// [SupportsCondition] they encounter. @protected void visitSupportsCondition(SupportsCondition condition) { - if (condition is SupportsOperation) { - visitSupportsCondition(condition.left); - visitSupportsCondition(condition.right); - } else if (condition is SupportsNegation) { - visitSupportsCondition(condition.condition); - } else if (condition is SupportsInterpolation) { - visitExpression(condition.expression); - } else if (condition is SupportsDeclaration) { - visitExpression(condition.name); - visitExpression(condition.value); + switch (condition) { + case SupportsOperation(): + visitSupportsCondition(condition.left); + visitSupportsCondition(condition.right); + case SupportsNegation(): + visitSupportsCondition(condition.condition); + case SupportsInterpolation(): + visitExpression(condition.expression); + case SupportsDeclaration(): + visitExpression(condition.name); + visitExpression(condition.value); } } diff --git a/lib/src/visitor/recursive_statement.dart b/lib/src/visitor/recursive_statement.dart index 408bb5874..3d89a84d9 100644 --- a/lib/src/visitor/recursive_statement.dart +++ b/lib/src/visitor/recursive_statement.dart @@ -53,11 +53,11 @@ mixin RecursiveStatementVisitor implements StatementVisitor { } } - node.lastClause.andThen((lastClause) { + if (node.lastClause case var lastClause?) { for (var child in lastClause.children) { child.accept(this); } - }); + } } void visitImportRule(ImportRule node) {} diff --git a/lib/src/visitor/replace_expression.dart b/lib/src/visitor/replace_expression.dart index d34775c28..b330cfbbf 100644 --- a/lib/src/visitor/replace_expression.dart +++ b/lib/src/visitor/replace_expression.dart @@ -3,10 +3,10 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:tuple/tuple.dart'; import '../ast/sass.dart'; import '../exception.dart'; +import '../util/map.dart'; import 'interface/expression.dart'; /// A visitor that recursively traverses each expression in a SassScript AST and @@ -52,10 +52,10 @@ mixin ReplaceExpressionVisitor implements ExpressionVisitor { node.contents.map((item) => item.accept(this)), node.separator, node.span, brackets: node.hasBrackets); - Expression visitMapExpression(MapExpression node) => MapExpression( - node.pairs.map( - (pair) => Tuple2(pair.item1.accept(this), pair.item2.accept(this))), - node.span); + Expression visitMapExpression(MapExpression node) => MapExpression([ + for (var (key, value) in node.pairs) + (key.accept(this), value.accept(this)) + ], node.span); Expression visitNullExpression(NullExpression node) => node; @@ -89,8 +89,8 @@ mixin ReplaceExpressionVisitor implements ExpressionVisitor { ArgumentInvocation( invocation.positional.map((expression) => expression.accept(this)), { - for (var entry in invocation.named.entries) - entry.key: entry.value.accept(this) + for (var (name, value) in invocation.named.pairs) + name: value.accept(this) }, invocation.span, rest: invocation.rest?.accept(this), diff --git a/lib/src/visitor/selector_search.dart b/lib/src/visitor/selector_search.dart index f87b38d3b..8029ad8c7 100644 --- a/lib/src/visitor/selector_search.dart +++ b/lib/src/visitor/selector_search.dart @@ -3,8 +3,8 @@ // https://opensource.org/licenses/MIT. import '../ast/selector.dart'; +import '../util/iterable.dart'; import '../util/nullable.dart'; -import '../utils.dart'; import 'interface/selector.dart'; /// A [SelectorVisitor] whose `visit*` methods default to returning `null`, but diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 36d92e716..c0c071155 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -19,6 +19,7 @@ import '../parse/parser.dart'; import '../utils.dart'; import '../util/character.dart'; import '../util/no_source_map_buffer.dart'; +import '../util/nullable.dart'; import '../util/number.dart'; import '../util/source_map_buffer.dart'; import '../util/span.dart'; @@ -62,18 +63,15 @@ SerializeResult serialize(CssNode node, var css = visitor._buffer.toString(); String prefix; if (charset && css.codeUnits.any((codeUnit) => codeUnit > 0x7F)) { - if (style == OutputStyle.compressed) { - prefix = '\uFEFF'; - } else { - prefix = '@charset "UTF-8";\n'; - } + prefix = style == OutputStyle.compressed ? '\uFEFF' : '@charset "UTF-8";\n'; } else { prefix = ''; } - return SerializeResult(prefix + css, - sourceMap: - sourceMap ? visitor._buffer.buildSourceMap(prefix: prefix) : null); + return ( + prefix + css, + sourceMap: sourceMap ? visitor._buffer.buildSourceMap(prefix: prefix) : null + ); } /// Converts [value] to a CSS string. @@ -104,7 +102,7 @@ String serializeSelector(Selector selector, {bool inspect = false}) { } /// A visitor that converts CSS syntax trees to plain strings. -class _SerializeVisitor +final class _SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor { /// A buffer that contains the CSS produced so far. final SourceMapBuffer _buffer; @@ -183,18 +181,17 @@ class _SerializeVisitor // Ignore sourceMappingURL and sourceURL comments. if (node.text.startsWith(RegExp(r"/\*# source(Mapping)?URL="))) return; - var minimumIndentation = _minimumIndentation(node.text); - assert(minimumIndentation != -1); - if (minimumIndentation == null) { + if (_minimumIndentation(node.text) case var minimumIndentation?) { + assert(minimumIndentation != -1); + minimumIndentation = + math.min(minimumIndentation, node.span.start.column); + + _writeIndentation(); + _writeWithIndent(node.text, minimumIndentation); + } else { _writeIndentation(); _buffer.write(node.text); - return; } - - minimumIndentation = math.min(minimumIndentation, node.span.start.column); - - _writeIndentation(); - _writeWithIndent(node.text, minimumIndentation); }); } @@ -205,8 +202,7 @@ class _SerializeVisitor _buffer.writeCharCode($at); _write(node.name); - var value = node.value; - if (value != null) { + if (node.value case var value?) { _buffer.writeCharCode($space); _write(value); } @@ -248,8 +244,7 @@ class _SerializeVisitor _writeOptionalSpace(); _for(node.url, () => _writeImportUrl(node.url.value)); - var modifiers = node.modifiers; - if (modifiers != null) { + if (node.modifiers case var modifiers?) { _writeOptionalSpace(); _buffer.write(modifiers); } @@ -288,20 +283,17 @@ class _SerializeVisitor } void _visitMediaQuery(CssMediaQuery query) { - if (query.modifier != null) { - _buffer.write(query.modifier); + if (query.modifier case var modifier?) { + _buffer.write(modifier); _buffer.writeCharCode($space); } - if (query.type != null) { - _buffer.write(query.type); - if (query.conditions.isNotEmpty) { - _buffer.write(" and "); - } + if (query.type case var type?) { + _buffer.write(type); + if (query.conditions.isNotEmpty) _buffer.write(" and "); } - if (query.conditions.length == 1 && - query.conditions.first.startsWith("(not ")) { + if (query.conditions case [var first] when first.startsWith("(not ")) { _buffer.write("not "); var condition = query.conditions.first; _buffer.write(condition.substring("(not ".length, condition.length - 1)); @@ -363,10 +355,11 @@ class _SerializeVisitor throwWithTrace( MultiSpanSassException(error.message, node.value.span, error.primaryLabel, error.secondarySpans), + error, stackTrace); } on SassScriptException catch (error, stackTrace) { throwWithTrace( - SassException(error.message, node.value.span), stackTrace); + SassException(error.message, node.value.span), error, stackTrace); } } } @@ -382,7 +375,7 @@ class _SerializeVisitor } _buffer.writeCharCode($space); - while (isWhitespace(scanner.peekChar())) { + while (scanner.peekChar().isWhitespace) { scanner.readChar(); } } @@ -392,19 +385,16 @@ class _SerializeVisitor void _writeReindentedValue(CssDeclaration node) { var value = (node.value.value as SassString).text; - var minimumIndentation = _minimumIndentation(value); - if (minimumIndentation == null) { - _buffer.write(value); - return; - } else if (minimumIndentation == -1) { - _buffer.write(trimAsciiRight(value, excludeEscape: true)); - _buffer.writeCharCode($space); - return; + switch (_minimumIndentation(value)) { + case null: + _buffer.write(value); + case -1: + _buffer.write(trimAsciiRight(value, excludeEscape: true)); + _buffer.writeCharCode($space); + case var minimumIndentation: + _writeWithIndent( + value, math.min(minimumIndentation, node.name.span.start.column)); } - - minimumIndentation = - math.min(minimumIndentation, node.name.span.start.column); - _writeWithIndent(value, minimumIndentation); } /// Returns the indentation level of the least-indented non-empty line in @@ -447,11 +437,12 @@ class _SerializeVisitor } while (true) { - assert(isWhitespace(scanner.peekChar(-1))); + assert(scanner.peekChar(-1).isWhitespace); // Scan forward until we hit non-whitespace or the end of [text]. var lineStart = scanner.position; var newlines = 1; + inner: while (true) { // If we hit the end of [text], we still need to preserve the fact that // whitespace exists because it could matter for custom properties. @@ -460,11 +451,15 @@ class _SerializeVisitor return; } - var next = scanner.readChar(); - if (next == $space || next == $tab) continue; - if (next != $lf) break; - lineStart = scanner.position; - newlines++; + switch (scanner.readChar()) { + case $space || $tab: + continue inner; + case $lf: + lineStart = scanner.position; + newlines++; + case _: + break inner; + } } _writeTimes($lf, newlines); @@ -493,63 +488,62 @@ class _SerializeVisitor } void _writeCalculationValue(Object value) { - if (value is SassNumber && !value.value.isFinite) { - if (value.numeratorUnits.length > 1 || - value.denominatorUnits.isNotEmpty) { + switch (value) { + case SassNumber(value: double(isFinite: false), hasComplexUnits: true): if (!_inspect) { throw SassScriptException("$value isn't a valid CSS value."); } _writeNumber(value.value); _buffer.write(value.unitString); - return; - } - if (value.value == double.infinity) { - _buffer.write('infinity'); - } else if (value.value == double.negativeInfinity) { - _buffer.write('-infinity'); - } else if (value.value.isNaN) { - _buffer.write('NaN'); - } + case SassNumber(value: double(isFinite: false)): + switch (value.value) { + case double.infinity: + _buffer.write('infinity'); + case double.negativeInfinity: + _buffer.write('-infinity'); + case double(isNaN: true): + _buffer.write('NaN'); + } - var unit = value.numeratorUnits.firstOrNull; - if (unit != null) { - _writeOptionalSpace(); - _buffer.writeCharCode($asterisk); - _writeOptionalSpace(); - _buffer.writeCharCode($1); - _buffer.write(unit); - } - } else if (value is Value) { - value.accept(this); - } else if (value is CalculationInterpolation) { - _buffer.write(value.value); - } else if (value is CalculationOperation) { - var left = value.left; - var parenthesizeLeft = left is CalculationInterpolation || - (left is CalculationOperation && - left.operator.precedence < value.operator.precedence); - if (parenthesizeLeft) _buffer.writeCharCode($lparen); - _writeCalculationValue(left); - if (parenthesizeLeft) _buffer.writeCharCode($rparen); - - var operatorWhitespace = !_isCompressed || value.operator.precedence == 1; - if (operatorWhitespace) _buffer.writeCharCode($space); - _buffer.write(value.operator.operator); - if (operatorWhitespace) _buffer.writeCharCode($space); - - var right = value.right; - var parenthesizeRight = right is CalculationInterpolation || - (right is CalculationOperation && - _parenthesizeCalculationRhs(value.operator, right.operator)) || - (value.operator == CalculationOperator.dividedBy && - right is SassNumber && - !right.value.isFinite && - right.hasUnits); - if (parenthesizeRight) _buffer.writeCharCode($lparen); - _writeCalculationValue(right); - if (parenthesizeRight) _buffer.writeCharCode($rparen); + if (value.numeratorUnits.firstOrNull case var unit?) { + _writeOptionalSpace(); + _buffer.writeCharCode($asterisk); + _writeOptionalSpace(); + _buffer.writeCharCode($1); + _buffer.write(unit); + } + + case Value(): + value.accept(this); + + case CalculationInterpolation(): + _buffer.write(value.value); + + case CalculationOperation(:var operator, :var left, :var right): + var parenthesizeLeft = left is CalculationInterpolation || + (left is CalculationOperation && + left.operator.precedence < operator.precedence); + if (parenthesizeLeft) _buffer.writeCharCode($lparen); + _writeCalculationValue(left); + if (parenthesizeLeft) _buffer.writeCharCode($rparen); + + var operatorWhitespace = !_isCompressed || operator.precedence == 1; + if (operatorWhitespace) _buffer.writeCharCode($space); + _buffer.write(operator.operator); + if (operatorWhitespace) _buffer.writeCharCode($space); + + var parenthesizeRight = right is CalculationInterpolation || + (right is CalculationOperation && + _parenthesizeCalculationRhs(operator, right.operator)) || + (operator == CalculationOperator.dividedBy && + right is SassNumber && + !right.value.isFinite && + right.hasUnits); + if (parenthesizeRight) _buffer.writeCharCode($lparen); + _writeCalculationValue(right); + if (parenthesizeRight) _buffer.writeCharCode($rparen); } } @@ -558,12 +552,13 @@ class _SerializeVisitor /// /// In `a ? (b # c)`, `outer` is `?` and `right` is `#`. bool _parenthesizeCalculationRhs( - CalculationOperator outer, CalculationOperator right) { - if (outer == CalculationOperator.dividedBy) return true; - if (outer == CalculationOperator.plus) return false; - return right == CalculationOperator.plus || - right == CalculationOperator.minus; - } + CalculationOperator outer, CalculationOperator right) => + switch (outer) { + CalculationOperator.dividedBy => true, + CalculationOperator.plus => false, + _ => right == CalculationOperator.plus || + right == CalculationOperator.minus + }; void visitColor(SassColor value) { // In compressed mode, emit colors in the shortest representation possible. @@ -571,9 +566,8 @@ class _SerializeVisitor if (!fuzzyEquals(value.alpha, 1)) { _writeRgb(value); } else { - var name = namesByColor[value]; var hexLength = _canUseShortHex(value) ? 4 : 7; - if (name != null && name.length <= hexLength) { + if (namesByColor[value] case var name? when name.length <= hexLength) { _buffer.write(name); } else if (_canUseShortHex(value)) { _buffer.writeCharCode($hash); @@ -588,20 +582,22 @@ class _SerializeVisitor } } } else { - var format = value.format; - if (format != null) { - if (format == ColorFormat.rgbFunction) { - _writeRgb(value); - } else if (format == ColorFormat.hslFunction) { - _writeHsl(value); - } else { - _buffer.write((format as SpanColorFormat).original); + if (value.format case var format?) { + switch (format) { + case ColorFormat.rgbFunction: + _writeRgb(value); + case ColorFormat.hslFunction: + _writeHsl(value); + case SpanColorFormat(): + _buffer.write(format.original); + case _: + assert(false, "unknown format"); } - } else if (namesByColor.containsKey(value) && + } else if (namesByColor[value] case var name? // Always emit generated transparent colors in rgba format. This works // around an IE bug. See sass/sass#1782. - !fuzzyEquals(value.alpha, 0)) { - _buffer.write(namesByColor[value]); + when !fuzzyEquals(value.alpha, 0)) { + _buffer.write(name); } else if (fuzzyEquals(value.alpha, 1)) { _buffer.writeCharCode($hash); _writeHexComponent(value.red); @@ -722,40 +718,29 @@ class _SerializeVisitor } /// Returns the string to use to separate list items for lists with the given [separator]. - String _separatorString(ListSeparator separator) { - switch (separator) { - case ListSeparator.comma: - return _commaSeparator; - case ListSeparator.slash: - return _isCompressed ? "/" : " / "; - case ListSeparator.space: - return " "; - default: + String _separatorString(ListSeparator separator) => switch (separator) { + ListSeparator.comma => _commaSeparator, + ListSeparator.slash => _isCompressed ? "/" : " / ", + ListSeparator.space => " ", // This should never be used, but it may still be returned since // [_separatorString] is invoked eagerly by [writeList] even for lists // with only one elements. - return ""; - } - } + _ => "" + }; /// Returns whether [value] needs parentheses as an element in a list with the /// given [separator]. - bool _elementNeedsParens(ListSeparator separator, Value value) { - if (value is SassList) { - if (value.asList.length < 2) return false; - if (value.hasBrackets) return false; - switch (separator) { - case ListSeparator.comma: - return value.separator == ListSeparator.comma; - case ListSeparator.slash: - return value.separator == ListSeparator.comma || - value.separator == ListSeparator.slash; - default: - return value.separator != ListSeparator.undecided; - } - } - return false; - } + bool _elementNeedsParens(ListSeparator separator, Value value) => + switch (value) { + SassList(asList: List(length: > 1), hasBrackets: false) => switch ( + separator) { + ListSeparator.comma => value.separator == ListSeparator.comma, + ListSeparator.slash => value.separator == ListSeparator.comma || + value.separator == ListSeparator.slash, + _ => value.separator != ListSeparator.undecided, + }, + _ => false + }; void visitMap(SassMap map) { if (!_inspect) { @@ -785,11 +770,10 @@ class _SerializeVisitor } void visitNumber(SassNumber value) { - var asSlash = value.asSlash; - if (asSlash != null) { - visitNumber(asSlash.item1); + if (value.asSlash case (var before, var after)) { + visitNumber(before); _buffer.writeCharCode($slash); - visitNumber(asSlash.item2); + visitNumber(after); return; } @@ -801,14 +785,11 @@ class _SerializeVisitor _writeNumber(value.value); if (!_inspect) { - if (value.numeratorUnits.length > 1 || - value.denominatorUnits.isNotEmpty) { + if (value.hasComplexUnits) { throw SassScriptException("$value isn't a valid CSS value."); } - if (value.numeratorUnits.isNotEmpty) { - _buffer.write(value.numeratorUnits.first); - } + if (value.numeratorUnits case [var first]) _buffer.write(first); } else { _buffer.write(value.unitString); } @@ -819,8 +800,7 @@ class _SerializeVisitor void _writeNumber(double number) { // Dart always converts integers to strings in the obvious way, so all we // have to do is clamp doubles that are close to being integers. - var integer = fuzzyAsInt(number); - if (integer != null) { + if (fuzzyAsInt(number) case var integer?) { // JS still uses exponential notation for integers, so we have to handle // it here. _buffer.write(_removeExponent(integer.toString())); @@ -1033,80 +1013,74 @@ class _SerializeVisitor for (var i = 0; i < string.length; i++) { var char = string.codeUnitAt(i); switch (char) { + case $single_quote when forceDoubleQuote: + buffer.writeCharCode($single_quote); + + case $single_quote when includesDoubleQuote: + _visitQuotedString(string, forceDoubleQuote: true); + return; + case $single_quote: - if (forceDoubleQuote) { - buffer.writeCharCode($single_quote); - } else if (includesDoubleQuote) { - _visitQuotedString(string, forceDoubleQuote: true); - return; - } else { - includesSingleQuote = true; - buffer.writeCharCode($single_quote); - } - break; + includesSingleQuote = true; + buffer.writeCharCode($single_quote); + + case $double_quote when forceDoubleQuote: + buffer.writeCharCode($backslash); + buffer.writeCharCode($double_quote); + + case $double_quote when includesSingleQuote: + _visitQuotedString(string, forceDoubleQuote: true); + return; case $double_quote: - if (forceDoubleQuote) { - buffer.writeCharCode($backslash); - buffer.writeCharCode($double_quote); - } else if (includesSingleQuote) { - _visitQuotedString(string, forceDoubleQuote: true); - return; - } else { - includesDoubleQuote = true; - buffer.writeCharCode($double_quote); - } - break; + includesDoubleQuote = true; + buffer.writeCharCode($double_quote); // Write newline characters and unprintable ASCII characters as escapes. - case $nul: - case $soh: - case $stx: - case $etx: - case $eot: - case $enq: - case $ack: - case $bel: - case $bs: - case $lf: - case $vt: - case $ff: - case $cr: - case $so: - case $si: - case $dle: - case $dc1: - case $dc2: - case $dc3: - case $dc4: - case $nak: - case $syn: - case $etb: - case $can: - case $em: - case $sub: - case $esc: - case $fs: - case $gs: - case $rs: - case $us: + case $nul || + $soh || + $stx || + $etx || + $eot || + $enq || + $ack || + $bel || + $bs || + $lf || + $vt || + $ff || + $cr || + $so || + $si || + $dle || + $dc1 || + $dc2 || + $dc3 || + $dc4 || + $nak || + $syn || + $etb || + $can || + $em || + $sub || + $esc || + $fs || + $gs || + $rs || + $us: _writeEscape(buffer, char, string, i); - break; case $backslash: buffer.writeCharCode($backslash); buffer.writeCharCode($backslash); - break; - default: - var newIndex = _tryPrivateUseCharacter(buffer, char, string, i); - if (newIndex != null) { + case _: + if (_tryPrivateUseCharacter(buffer, char, string, i) + case var newIndex?) { i = newIndex; - break; + } else { + buffer.writeCharCode(char); } - - buffer.writeCharCode(char); - break; } } @@ -1124,27 +1098,22 @@ class _SerializeVisitor void _visitUnquotedString(String string) { var afterNewline = false; for (var i = 0; i < string.length; i++) { - var char = string.codeUnitAt(i); - switch (char) { + switch (string.codeUnitAt(i)) { case $lf: _buffer.writeCharCode($space); afterNewline = true; - break; case $space: if (!afterNewline) _buffer.writeCharCode($space); - break; - default: + case var char: afterNewline = false; - var newIndex = _tryPrivateUseCharacter(_buffer, char, string, i); - if (newIndex != null) { + if (_tryPrivateUseCharacter(_buffer, char, string, i) + case var newIndex?) { i = newIndex; - break; + } else { + _buffer.writeCharCode(char); } - - _buffer.writeCharCode(char); - break; } } } @@ -1165,12 +1134,12 @@ class _SerializeVisitor StringBuffer buffer, int codeUnit, String string, int i) { if (_isCompressed) return null; - if (isPrivateUseBMP(codeUnit)) { + if (codeUnit.isPrivateUseBMP) { _writeEscape(buffer, codeUnit, string, i); return i; } - if (isPrivateUseHighSurrogate(codeUnit) && string.length > i + 1) { + if (codeUnit.isPrivateUseHighSurrogate && string.length > i + 1) { _writeEscape(buffer, combineSurrogates(codeUnit, string.codeUnitAt(i + 1)), string, i + 1); return i + 1; @@ -1191,7 +1160,7 @@ class _SerializeVisitor if (string.length == i + 1) return; var next = string.codeUnitAt(i + 1); - if (isHex(next) || next == $space || next == $tab) { + if (next case int(isHex: true) || $space || $tab) { buffer.writeCharCode($space); } } @@ -1202,8 +1171,7 @@ class _SerializeVisitor _buffer.writeCharCode($lbracket); _buffer.write(attribute.name); - var value = attribute.value; - if (value != null) { + if (attribute.value case var value?) { _buffer.write(attribute.op); if (Parser.isIdentifier(value) && // Emit identifiers that start with `--` with quotes, because IE11 @@ -1216,7 +1184,7 @@ class _SerializeVisitor _visitQuotedString(value); if (attribute.modifier != null) _writeOptionalSpace(); } - if (attribute.modifier != null) _buffer.write(attribute.modifier); + attribute.modifier.andThen(_buffer.write); } _buffer.writeCharCode($rbracket); } @@ -1228,8 +1196,11 @@ class _SerializeVisitor void visitComplexSelector(ComplexSelector complex) { _writeCombinators(complex.leadingCombinators); - if (complex.leadingCombinators.isNotEmpty && - complex.components.isNotEmpty) { + if (complex + case ComplexSelector( + leadingCombinators: [_, ...], + components: [_, ...] + )) { _writeOptionalSpace(); } @@ -1291,7 +1262,7 @@ class _SerializeVisitor void visitParentSelector(ParentSelector parent) { _buffer.writeCharCode($ampersand); - if (parent.suffix != null) _buffer.write(parent.suffix); + parent.suffix.andThen(_buffer.write); } void visitPlaceholderSelector(PlaceholderSelector placeholder) { @@ -1300,11 +1271,12 @@ class _SerializeVisitor } void visitPseudoSelector(PseudoSelector pseudo) { - var innerSelector = pseudo.selector; // `:not(%a)` is semantically identical to `*`. - if (innerSelector != null && - pseudo.name == 'not' && - innerSelector.isInvisible) { + if (pseudo + case PseudoSelector( + name: 'not', + selector: SelectorList(isInvisible: true) + )) { return; } @@ -1318,7 +1290,7 @@ class _SerializeVisitor _buffer.write(pseudo.argument); if (pseudo.selector != null) _buffer.writeCharCode($space); } - if (innerSelector != null) visitSelectorList(innerSelector); + pseudo.selector.andThen(visitSelectorList); _buffer.writeCharCode($rparen); } @@ -1353,7 +1325,7 @@ class _SerializeVisitor for (var child in parent.children) { if (_isInvisible(child)) continue; - if (previous != null && _requiresSemicolon(previous)) { + if (previous.andThen(_requiresSemicolon) ?? false) { _buffer.writeCharCode($semicolon); } @@ -1533,14 +1505,13 @@ enum LineFeed { } /// The result of converting a CSS AST to CSS text. -class SerializeResult { +typedef SerializeResult = ( /// The serialized CSS. - final String css; + String css, /// The source map indicating how the source files map to [css]. /// /// This is `null` if source mapping was disabled for this compilation. - final SingleMapping? sourceMap; - - SerializeResult(this.css, {this.sourceMap}); -} + { + SingleMapping? sourceMap +}); diff --git a/lib/src/visitor/statement_search.dart b/lib/src/visitor/statement_search.dart index 791b8689a..6abd7fe9f 100644 --- a/lib/src/visitor/statement_search.dart +++ b/lib/src/visitor/statement_search.dart @@ -5,8 +5,8 @@ import 'package:meta/meta.dart'; import '../ast/sass.dart'; +import '../util/iterable.dart'; import '../util/nullable.dart'; -import '../utils.dart'; import 'interface/statement.dart'; import 'recursive_statement.dart'; diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index af651fb26..e2d5fb23b 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,6 +1,12 @@ -## 7.2.2 +## 8.0.0 -* No user-visible changes. +* Various classes now use Dart 3 [class modifiers] to more specifically restrict + their usage to the intended patterns. + + [class modifiers]: https://dart.dev/language/class-modifiers + +* All uses of classes from the `tuple` package have been replaced by record + types. ## 7.2.1 diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index daf988e02..f6f3687f3 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: 7.2.2 +version: 8.0.0-dev description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass diff --git a/pubspec.yaml b/pubspec.yaml index db49b8fde..22c0f9e93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,7 +32,6 @@ dependencies: stream_transform: ^2.0.0 string_scanner: ^1.1.0 term_glyph: ^1.2.0 - tuple: ^2.0.0 typed_data: ^1.1.0 watcher: ^1.0.0 diff --git a/test/source_map_test.dart b/test/source_map_test.dart index e7558e1c4..61b91120f 100644 --- a/test/source_map_test.dart +++ b/test/source_map_test.dart @@ -9,7 +9,6 @@ import 'package:source_maps/source_maps.dart'; import 'package:source_span/source_span.dart'; import 'package:string_scanner/string_scanner.dart'; import 'package:test/test.dart'; -import 'package:tuple/tuple.dart'; import 'package:sass/sass.dart'; import 'package:sass/src/utils.dart'; @@ -720,31 +719,22 @@ void _expectSourceMap(String sass, String scss, String css, /// Like [_expectSourceMap], but with only SCSS source. void _expectScssSourceMap(String scss, String css, {Importer? importer, OutputStyle? style}) { - var scssTuple = _extractLocations(_reindent(scss)); - var scssText = scssTuple.item1; - var scssLocations = _tuplesToMap(scssTuple.item2); - - var cssTuple = _extractLocations(_reindent(css)); - var cssText = cssTuple.item1; - var cssLocations = cssTuple.item2; + var (scssText, scssLocations) = _extractLocations(_reindent(scss)); + var (cssText, cssLocations) = _extractLocations(_reindent(css)); late SingleMapping scssMap; var scssOutput = compileString(scssText, sourceMap: (map) => scssMap = map, importer: importer, style: style); expect(scssOutput, equals(cssText)); - _expectMapMatches(scssMap, scssText, cssText, scssLocations, cssLocations); + _expectMapMatches( + scssMap, scssText, cssText, _pairsToMap(scssLocations), cssLocations); } /// Like [_expectSourceMap], but with only indented source. void _expectSassSourceMap(String sass, String css, {Importer? importer, OutputStyle? style}) { - var sassTuple = _extractLocations(_reindent(sass)); - var sassText = sassTuple.item1; - var sassLocations = _tuplesToMap(sassTuple.item2); - - var cssTuple = _extractLocations(_reindent(css)); - var cssText = cssTuple.item1; - var cssLocations = cssTuple.item2; + var (sassText, sassLocations) = _extractLocations(_reindent(sass)); + var (cssText, cssLocations) = _extractLocations(_reindent(css)); late SingleMapping sassMap; var sassOutput = compileString(sassText, @@ -753,7 +743,8 @@ void _expectSassSourceMap(String sass, String css, importer: importer, style: style); expect(sassOutput, equals(cssText)); - _expectMapMatches(sassMap, sassText, cssText, sassLocations, cssLocations); + _expectMapMatches( + sassMap, sassText, cssText, _pairsToMap(sassLocations), cssLocations); } /// Returns [string] with leading whitespace stripped from each line so that the @@ -770,11 +761,10 @@ String _reindent(String string) { } /// Parses and removes the location annotations from [text]. -Tuple2>> _extractLocations( - String text) { +(String, List<(String, SourceLocation)>) _extractLocations(String text) { var scanner = StringScanner(text); var buffer = StringBuffer(); - var locations = >[]; + var locations = <(String, SourceLocation)>[]; var offset = 0; var line = 0; @@ -786,8 +776,10 @@ Tuple2>> _extractLocations( while (!scanner.scan("}}")) { scanner.readChar(); } - locations.add(Tuple2(scanner.substring(start, scanner.position - 2), - SourceLocation(offset, line: line, column: column))); + locations.add(( + scanner.substring(start, scanner.position - 2), + SourceLocation(offset, line: line, column: column) + )); } else if (scanner.scanChar($lf)) { offset++; line++; @@ -800,16 +792,16 @@ Tuple2>> _extractLocations( } } - return Tuple2(buffer.toString(), locations); + return (buffer.toString(), locations); } -/// Converts a list of tuples to a map, asserting that each key appears only +/// Converts a list of pairs to a map, asserting that each key appears only /// once. -Map _tuplesToMap(Iterable> tuples) { +Map _pairsToMap(Iterable<(K, V)> pairs) { var map = {}; - for (var tuple in tuples) { - expect(map, isNot(contains(tuple.item1))); - map[tuple.item1] = tuple.item2; + for (var (key, value) in pairs) { + expect(map, isNot(contains(key))); + map[key] = value; } return map; } @@ -821,17 +813,15 @@ void _expectMapMatches( String sourceText, String targetText, Map sourceLocations, - List> targetLocations) { + List<(String, SourceLocation)> targetLocations) { expect(sourceLocations.keys, - equals({for (var tuple in targetLocations) tuple.item1})); + equals({for (var (name, _) in targetLocations) name})); String actualMap() => "\nActual map:\n\n" + _mapToString(map, sourceText, targetText) + "\n"; var entryIter = _entriesForMap(map).iterator; - for (var tuple in targetLocations) { - var name = tuple.item1; - var expectedTarget = tuple.item2; + for (var (name, expectedTarget) in targetLocations) { var expectedSource = sourceLocations[name]!; if (!entryIter.moveNext()) { @@ -885,17 +875,17 @@ String _mapToString(SingleMapping map, String sourceText, String targetText) { // A map from lines and columns in [sourceText] to the names of the entries // with those source locations. - var entryNames = , String>{}; + var entryNames = <(int, int), String>{}; var i = 0; for (var entry in entriesInSourceOrder) { entryNames.putIfAbsent( - Tuple2(entry.source.line, entry.source.column), () => (++i).toString()); + (entry.source.line, entry.source.column), () => (++i).toString()); } var sourceScanner = LineScanner(sourceText); var sourceBuffer = StringBuffer(); while (!sourceScanner.isDone) { - var name = entryNames[Tuple2(sourceScanner.line, sourceScanner.column)]; + var name = entryNames[(sourceScanner.line, sourceScanner.column)]; if (name != null) sourceBuffer.write("{{$name}}"); sourceBuffer.writeCharCode(sourceScanner.readChar()); } @@ -907,7 +897,7 @@ String _mapToString(SingleMapping map, String sourceText, String targetText) { var entry = entryIter.current; if (targetScanner.line == entry.target.line && targetScanner.column == entry.target.column) { - var name = entryNames[Tuple2(entry.source.line, entry.source.column)]; + var name = entryNames[(entry.source.line, entry.source.column)]; targetBuffer.write("{{$name}}"); if (!entryIter.moveNext()) { targetBuffer.write(targetScanner.rest); diff --git a/tool/grind.dart b/tool/grind.dart index 5992988f2..ce65138df 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -121,8 +121,7 @@ void all() {} @Task('Run the Dart formatter.') void format() { - run('dart', - arguments: ['run', 'dart_style:format', '--overwrite', '--fix', '.']); + run('dart', arguments: ['format', '--fix', '.']); } @Task('Installs dependencies from npm.') diff --git a/tool/grind/benchmark.dart b/tool/grind/benchmark.dart index a94cf7c8f..82ef95d3f 100644 --- a/tool/grind/benchmark.dart +++ b/tool/grind/benchmark.dart @@ -8,6 +8,7 @@ import 'dart:math' as math; import 'package:grinder/grinder.dart'; import 'package:path/path.dart' as p; +import 'package:sass/src/util/nullable.dart'; import 'utils.dart'; @@ -86,11 +87,11 @@ Future _writeNTimes(String path, String text, num times, log("Generating $path..."); var sink = file.openWrite(); - if (header != null) sink.writeln(header); + header.andThen(sink.writeln); for (var i = 0; i < times; i++) { sink.writeln(text); } - if (footer != null) sink.writeln(footer); + footer.andThen(sink.writeln); await sink.close(); } diff --git a/tool/grind/frameworks.dart b/tool/grind/frameworks.dart index 038c954f0..014bfbdc2 100644 --- a/tool/grind/frameworks.dart +++ b/tool/grind/frameworks.dart @@ -46,8 +46,7 @@ Future _findLatestRelease(String slug, {Pattern? pattern}) async { var page = 1; while (releases.isNotEmpty) { - for (var release in releases) { - var tagName = release['tag_name'] as String; + for (var {'tag_name': String tagName} in releases) { if (pattern.allMatches(tagName).isNotEmpty) return tagName; } diff --git a/tool/grind/synchronize.dart b/tool/grind/synchronize.dart index e35155256..87ab7af5d 100644 --- a/tool/grind/synchronize.dart +++ b/tool/grind/synchronize.dart @@ -160,6 +160,19 @@ class _Visitor extends RecursiveAstVisitor { } } + void visitGenericTypeAlias(GenericTypeAlias node) { + if (_sharedClasses.contains(node.name.lexeme)) { + _skipNode(node); + } else { + for (var child in node.sortedCommentAndAnnotations) { + child.accept(this); + } + _rename(node.name); + node.typeParameters?.accept(this); + node.type.accept(this); + } + } + void visitExpressionFunctionBody(ExpressionFunctionBody node) { _skip(node.keyword); node.visitChildren(this); @@ -194,8 +207,11 @@ class _Visitor extends RecursiveAstVisitor { void visitMethodInvocation(MethodInvocation node) { // Convert async utility methods to their synchronous equivalents. - if (node.target == null && - ["mapAsync", "putIfAbsentAsync"].contains(node.methodName.name)) { + if (node + case MethodInvocation( + target: null, + methodName: SimpleIdentifier(name: "mapAsync" || "putIfAbsentAsync") + )) { _writeTo(node); var arguments = node.argumentList.arguments; _write(arguments.first); @@ -219,10 +235,9 @@ class _Visitor extends RecursiveAstVisitor { } void visitNamedType(NamedType node) { - if (["Future", "FutureOr"].contains(node.name2.lexeme)) { + if (node.name2.lexeme case "Future" || "FutureOr") { _skip(node.name2); - var typeArguments = node.typeArguments; - if (typeArguments != null) { + if (node.typeArguments case var typeArguments?) { _skip(typeArguments.leftBracket); typeArguments.arguments.first.accept(this); _skip(typeArguments.rightBracket); diff --git a/tool/grind/utils.dart b/tool/grind/utils.dart index 56b9b7458..12fd8ed1e 100644 --- a/tool/grind/utils.dart +++ b/tool/grind/utils.dart @@ -40,9 +40,8 @@ void ensureBuild() { /// Returns the environment variable named [name], or throws an exception if it /// can't be found. String environment(String name) { - var value = Platform.environment[name]; - if (value == null) fail("Required environment variable $name not found."); - return value; + if (Platform.environment[name] case var value?) return value; + fail("Required environment variable $name not found."); } /// Ensure that the repository at [url] is cloned into the build directory and