diff --git a/bin/sass.dart b/bin/sass.dart index d74f39c6c..b49cdd4e2 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -24,7 +24,7 @@ Future main(List args) async { // has been printed to stderr. // // If [trace] is passed, its terse representation is printed after the error. - void printError(String error, StackTrace stackTrace) { + void printError(String error, StackTrace? stackTrace) { if (printedError) stderr.writeln(); printedError = true; stderr.writeln(error); @@ -35,7 +35,7 @@ Future main(List args) async { } } - ExecutableOptions options; + ExecutableOptions? options; try { options = ExecutableOptions.parse(args); term_glyph.ascii = !options.unicode; @@ -52,7 +52,7 @@ Future main(List args) async { } var graph = StylesheetGraph( - ImportCache([], loadPaths: options.loadPaths, logger: options.logger)); + ImportCache(loadPaths: options.loadPaths, logger: options.logger)); if (options.watch) { await watch(options, graph); return; @@ -68,7 +68,9 @@ Future main(List args) async { // dart-lang/sdk#33400. () { try { - if (destination != null && !options.emitErrorCss) { + if (destination != null && + // dart-lang/sdk#45348 + !options!.emitErrorCss) { deleteFile(destination); } } on FileSystemException { @@ -86,7 +88,11 @@ Future main(List args) async { if (exitCode != 66) exitCode = 65; if (options.stopOnError) return; } on FileSystemException catch (error, stackTrace) { - printError("Error reading ${p.relative(error.path)}: ${error.message}.", + var path = error.path; + printError( + path == null + ? error.message + : "Error reading ${p.relative(path)}: ${error.message}.", options.trace ? stackTrace : null); // Error 66 indicates no input. @@ -115,12 +121,14 @@ Future main(List args) async { /// Loads and returns the current version of Sass. Future _loadVersion() async { - var version = const String.fromEnvironment('version'); - if (const bool.fromEnvironment('node')) { - version += " compiled with dart2js " - "${const String.fromEnvironment('dart-version')}"; + if (const bool.hasEnvironment('version')) { + var version = const String.fromEnvironment('version'); + if (const bool.fromEnvironment('node')) { + version += " compiled with dart2js " + "${const String.fromEnvironment('dart-version')}"; + } + return version; } - if (version != null) return version; var libDir = p.fromUri(await Isolate.resolvePackageUri(Uri.parse('package:sass/'))); diff --git a/lib/sass.dart b/lib/sass.dart index e45a610dd..4c79dd622 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -5,6 +5,7 @@ /// We strongly recommend importing this library with the prefix `sass`. library sass; +import 'package:package_config/package_config_types.dart'; import 'package:source_maps/source_maps.dart'; import 'src/async_import_cache.dart'; @@ -14,8 +15,8 @@ import 'src/exception.dart'; import 'src/import_cache.dart'; import 'src/importer.dart'; import 'src/logger.dart'; -import 'src/sync_package_resolver.dart'; import 'src/syntax.dart'; +import 'src/util/nullable.dart'; import 'src/visitor/serialize.dart'; export 'src/callable.dart' show Callable, AsyncCallable; @@ -48,11 +49,11 @@ export 'src/warn.dart' show warn; /// * Each load path specified in the `SASS_PATH` environment variable, which /// should be semicolon-separated on Windows and colon-separated elsewhere. /// -/// * `package:` resolution using [packageResolver], which is a -/// [`SyncPackageResolver`][] from the `package_resolver` package. Note that +/// * `package:` resolution using [packageConfig], which is a +/// [`PackageConfig`][] from the `package_resolver` package. Note that /// this is a shorthand for adding a [PackageImporter] to [importers]. /// -/// [`SyncPackageResolver`]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html +/// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html /// /// Dart functions that can be called from Sass may be passed using [functions]. /// Each [Callable] defines a top-level function that will be invoked when the @@ -87,26 +88,27 @@ export 'src/warn.dart' show warn; /// Throws a [SassException] if conversion fails. String compile(String path, {bool color = false, - Logger logger, - Iterable importers, - Iterable loadPaths, - SyncPackageResolver packageResolver, - Iterable functions, - OutputStyle style, - void sourceMap(SingleMapping map), + Logger? logger, + Iterable? importers, + Iterable? loadPaths, + PackageConfig? packageConfig, + Iterable? functions, + OutputStyle? style, + void sourceMap(SingleMapping map)?, bool charset = true}) { logger ??= Logger.stderr(color: color); var result = c.compile(path, logger: logger, - importCache: ImportCache(importers, + importCache: ImportCache( + importers: importers, logger: logger, loadPaths: loadPaths, - packageResolver: packageResolver), + packageConfig: packageConfig), functions: functions, style: style, sourceMap: sourceMap != null, charset: charset); - if (sourceMap != null) sourceMap(result.sourceMap); + result.sourceMap.andThen(sourceMap); return result.css; } @@ -132,11 +134,11 @@ String compile(String path, /// * Each load path specified in the `SASS_PATH` environment variable, which /// should be semicolon-separated on Windows and colon-separated elsewhere. /// -/// * `package:` resolution using [packageResolver], which is a -/// [`SyncPackageResolver`][] from the `package_resolver` package. Note that +/// * `package:` resolution using [packageConfig], which is a +/// [`PackageConfig`][] from the `package_resolver` package. Note that /// this is a shorthand for adding a [PackageImporter] to [importers]. /// -/// [`SyncPackageResolver`]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html +/// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html /// /// Dart functions that can be called from Sass may be passed using [functions]. /// Each [Callable] defines a top-level function that will be invoked when the @@ -174,26 +176,27 @@ String compile(String path, /// /// Throws a [SassException] if conversion fails. String compileString(String source, - {Syntax syntax, + {Syntax? syntax, bool color = false, - Logger logger, - Iterable importers, - SyncPackageResolver packageResolver, - Iterable loadPaths, - Iterable functions, - OutputStyle style, - Importer importer, - Object url, - void sourceMap(SingleMapping map), + Logger? logger, + Iterable? importers, + PackageConfig? packageConfig, + Iterable? loadPaths, + Iterable? functions, + OutputStyle? style, + Importer? importer, + Object? url, + void sourceMap(SingleMapping map)?, bool charset = true, @Deprecated("Use syntax instead.") bool indented = false}) { logger ??= Logger.stderr(color: color); var result = c.compileString(source, syntax: syntax ?? (indented ? Syntax.sass : Syntax.scss), logger: logger, - importCache: ImportCache(importers, + importCache: ImportCache( + importers: importers, logger: logger, - packageResolver: packageResolver, + packageConfig: packageConfig, loadPaths: loadPaths), functions: functions, style: style, @@ -201,7 +204,7 @@ String compileString(String source, url: url, sourceMap: sourceMap != null, charset: charset); - if (sourceMap != null) sourceMap(result.sourceMap); + result.sourceMap.andThen(sourceMap); return result.css; } @@ -212,24 +215,25 @@ String compileString(String source, /// slower, so [compile] should be preferred if possible. Future compileAsync(String path, {bool color = false, - Logger logger, - Iterable importers, - SyncPackageResolver packageResolver, - Iterable loadPaths, - Iterable functions, - OutputStyle style, - void sourceMap(SingleMapping map)}) async { + Logger? logger, + Iterable? importers, + PackageConfig? packageConfig, + Iterable? loadPaths, + Iterable? functions, + OutputStyle? style, + void sourceMap(SingleMapping map)?}) async { logger ??= Logger.stderr(color: color); var result = await c.compileAsync(path, logger: logger, - importCache: AsyncImportCache(importers, + importCache: AsyncImportCache( + importers: importers, logger: logger, loadPaths: loadPaths, - packageResolver: packageResolver), + packageConfig: packageConfig), functions: functions, style: style, sourceMap: sourceMap != null); - if (sourceMap != null) sourceMap(result.sourceMap); + result.sourceMap.andThen(sourceMap); return result.css; } @@ -239,26 +243,27 @@ Future compileAsync(String path, /// synchronous [Importer]s. However, running asynchronously is also somewhat /// slower, so [compileString] should be preferred if possible. Future compileStringAsync(String source, - {Syntax syntax, + {Syntax? syntax, bool color = false, - Logger logger, - Iterable importers, - SyncPackageResolver packageResolver, - Iterable loadPaths, - Iterable functions, - OutputStyle style, - AsyncImporter importer, - Object url, - void sourceMap(SingleMapping map), + Logger? logger, + Iterable? importers, + PackageConfig? packageConfig, + Iterable? loadPaths, + Iterable? functions, + OutputStyle? style, + AsyncImporter? importer, + Object? url, + void sourceMap(SingleMapping map)?, bool charset = true, @Deprecated("Use syntax instead.") bool indented = false}) async { logger ??= Logger.stderr(color: color); var result = await c.compileStringAsync(source, syntax: syntax ?? (indented ? Syntax.sass : Syntax.scss), logger: logger, - importCache: AsyncImportCache(importers, + importCache: AsyncImportCache( + importers: importers, logger: logger, - packageResolver: packageResolver, + packageConfig: packageConfig, loadPaths: loadPaths), functions: functions, style: style, @@ -266,6 +271,6 @@ Future compileStringAsync(String source, url: url, sourceMap: sourceMap != null, charset: charset); - if (sourceMap != null) sourceMap(result.sourceMap); + result.sourceMap.andThen(sourceMap); return result.css; } diff --git a/lib/src/ast/css/at_rule.dart b/lib/src/ast/css/at_rule.dart index 864d8751b..8aff6529e 100644 --- a/lib/src/ast/css/at_rule.dart +++ b/lib/src/ast/css/at_rule.dart @@ -12,7 +12,7 @@ abstract class CssAtRule extends CssParentNode { CssValue get name; /// The value of this rule. - CssValue get value; + CssValue? get value; /// Whether the rule has no children. /// diff --git a/lib/src/ast/css/import.dart b/lib/src/ast/css/import.dart index 065ef076e..33db7c9b9 100644 --- a/lib/src/ast/css/import.dart +++ b/lib/src/ast/css/import.dart @@ -15,10 +15,10 @@ abstract class CssImport extends CssNode { CssValue get url; /// The supports condition attached to this import. - CssValue get supports; + CssValue? get supports; /// The media query attached to this import. - List get media; + List? get media; T accept(CssVisitor visitor) => visitor.visitCssImport(this); } diff --git a/lib/src/ast/css/media_query.dart b/lib/src/ast/css/media_query.dart index 1f2bc1962..54d5d81c2 100644 --- a/lib/src/ast/css/media_query.dart +++ b/lib/src/ast/css/media_query.dart @@ -11,12 +11,12 @@ class CssMediaQuery { /// The modifier, probably either "not" or "only". /// /// This may be `null` if no modifier is in use. - final String modifier; + final String? modifier; /// The media type, for example "screen" or "print". /// /// This may be `null`. If so, [features] will not be empty. - final String type; + final String? type; /// Feature queries, including parentheses. final List features; @@ -33,11 +33,11 @@ class CssMediaQuery { /// /// Throws a [SassFormatException] if parsing fails. static List parseList(String contents, - {Object url, Logger logger}) => + {Object? url, Logger? logger}) => MediaQueryParser(contents, url: url, logger: logger).parse(); /// Creates a media query specifies a type and, optionally, features. - CssMediaQuery(this.type, {this.modifier, Iterable features}) + CssMediaQuery(this.type, {this.modifier, Iterable? features}) : features = features == null ? const [] : List.unmodifiable(features); /// Creates a media query that only specifies features. @@ -59,8 +59,8 @@ class CssMediaQuery { CssMediaQuery.condition([...this.features, ...other.features])); } - String modifier; - String type; + String? modifier; + String? type; List features; if ((ourModifier == 'not') != (theirModifier == 'not')) { if (ourType == theirType) { diff --git a/lib/src/ast/css/modifiable/at_rule.dart b/lib/src/ast/css/modifiable/at_rule.dart index 4a4f3ce6e..e616de5c2 100644 --- a/lib/src/ast/css/modifiable/at_rule.dart +++ b/lib/src/ast/css/modifiable/at_rule.dart @@ -12,7 +12,7 @@ import 'node.dart'; /// A modifiable version of [CssAtRule] for use in the evaluation step. class ModifiableCssAtRule extends ModifiableCssParentNode implements CssAtRule { final CssValue name; - final CssValue value; + final CssValue? value; final bool isChildless; final FileSpan span; diff --git a/lib/src/ast/css/modifiable/declaration.dart b/lib/src/ast/css/modifiable/declaration.dart index e4bb7fc44..5be720b18 100644 --- a/lib/src/ast/css/modifiable/declaration.dart +++ b/lib/src/ast/css/modifiable/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 '../../../value.dart'; @@ -24,9 +23,9 @@ class ModifiableCssDeclaration extends ModifiableCssNode /// Returns a new CSS declaration with the given properties. ModifiableCssDeclaration(this.name, this.value, this.span, - {@required bool parsedAsCustomProperty, FileSpan valueSpanForMap}) + {required bool parsedAsCustomProperty, FileSpan? valueSpanForMap}) : parsedAsCustomProperty = parsedAsCustomProperty, - valueSpanForMap = valueSpanForMap ?? span { + valueSpanForMap = valueSpanForMap ?? value.span { if (parsedAsCustomProperty) { if (!isCustomProperty) { throw ArgumentError( diff --git a/lib/src/ast/css/modifiable/import.dart b/lib/src/ast/css/modifiable/import.dart index cf8127ff8..9d705b72e 100644 --- a/lib/src/ast/css/modifiable/import.dart +++ b/lib/src/ast/css/modifiable/import.dart @@ -18,15 +18,15 @@ class ModifiableCssImport extends ModifiableCssNode implements CssImport { final CssValue url; /// The supports condition attached to this import. - final CssValue supports; + final CssValue? supports; /// The media query attached to this import. - final List media; + final List? media; final FileSpan span; ModifiableCssImport(this.url, this.span, - {this.supports, Iterable media}) + {this.supports, Iterable? media}) : media = media == null ? null : List.unmodifiable(media); T accept(ModifiableCssVisitor visitor) => visitor.visitCssImport(this); diff --git a/lib/src/ast/css/modifiable/node.dart b/lib/src/ast/css/modifiable/node.dart index af737943b..3b493c88a 100644 --- a/lib/src/ast/css/modifiable/node.dart +++ b/lib/src/ast/css/modifiable/node.dart @@ -16,21 +16,22 @@ import '../style_rule.dart'; /// unmodifiable types are used elsewhere to enfore that constraint. abstract class ModifiableCssNode extends CssNode { /// The node that contains this, or `null` for the root [CssStylesheet] node. - ModifiableCssParentNode get parent => _parent; - ModifiableCssParentNode _parent; + ModifiableCssParentNode? get parent => _parent; + ModifiableCssParentNode? _parent; /// The index of [this] in `parent.children`. /// /// This makes [remove] more efficient. - int _indexInParent; + int? _indexInParent; var isGroupEnd = false; /// Whether this node has a visible sibling after it. bool get hasFollowingSibling { - if (_parent == null) return false; - var siblings = _parent.children; - for (var i = _indexInParent + 1; i < siblings.length; i++) { + var parent = _parent; + if (parent == null) return false; + var siblings = parent.children; + for (var i = _indexInParent! + 1; i < siblings.length; i++) { var sibling = siblings[i]; if (!_isInvisible(sibling)) return true; } @@ -63,13 +64,15 @@ abstract class ModifiableCssNode extends CssNode { /// /// Throws a [StateError] if [parent] is `null`. void remove() { - if (_parent == null) { + var parent = _parent; + if (parent == null) { throw StateError("Can't remove a node without a parent."); } - _parent._children.removeAt(_indexInParent); - for (var i = _indexInParent; i < _parent._children.length; i++) { - _parent._children[i]._indexInParent--; + parent._children.removeAt(_indexInParent!); + for (var i = _indexInParent!; i < parent._children.length; i++) { + var child = parent._children[i]; + child._indexInParent = child._indexInParent! - 1; } _parent = null; } diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart index 69875813a..1e9d7e869 100644 --- a/lib/src/ast/css/modifiable/style_rule.dart +++ b/lib/src/ast/css/modifiable/style_rule.dart @@ -21,7 +21,7 @@ class ModifiableCssStyleRule extends ModifiableCssParentNode /// /// If [originalSelector] isn't passed, it defaults to [selector.value]. ModifiableCssStyleRule(ModifiableCssValue selector, this.span, - {SelectorList originalSelector}) + {SelectorList? originalSelector}) : selector = selector, originalSelector = originalSelector ?? selector.value; diff --git a/lib/src/ast/css/modifiable/value.dart b/lib/src/ast/css/modifiable/value.dart index fa042efde..2f29676be 100644 --- a/lib/src/ast/css/modifiable/value.dart +++ b/lib/src/ast/css/modifiable/value.dart @@ -7,7 +7,7 @@ import 'package:source_span/source_span.dart'; import '../value.dart'; /// A modifiable version of [CssValue] for use in the evaluation step. -class ModifiableCssValue implements CssValue { +class ModifiableCssValue implements CssValue { T value; final FileSpan span; diff --git a/lib/src/ast/css/stylesheet.dart b/lib/src/ast/css/stylesheet.dart index 3c92b9c2d..08a671cde 100644 --- a/lib/src/ast/css/stylesheet.dart +++ b/lib/src/ast/css/stylesheet.dart @@ -26,7 +26,7 @@ class CssStylesheet extends CssParentNode { : children = UnmodifiableListView(children); /// Creates an empty stylesheet with the given source URL. - CssStylesheet.empty({Object url}) + CssStylesheet.empty({Object? url}) : children = const [], span = SourceFile.decoded(const [], url: url).span(0, 0); diff --git a/lib/src/ast/css/value.dart b/lib/src/ast/css/value.dart index 90379398d..ce8ee2689 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. -class CssValue implements AstNode { +class CssValue implements AstNode { /// The value. final T value; diff --git a/lib/src/ast/sass/argument.dart b/lib/src/ast/sass/argument.dart index 4fb508ecd..6e725496a 100644 --- a/lib/src/ast/sass/argument.dart +++ b/lib/src/ast/sass/argument.dart @@ -14,7 +14,7 @@ class Argument implements SassNode { final String name; /// The default value of this argument, or `null` if none was declared. - final Expression defaultValue; + final Expression? defaultValue; final FileSpan span; @@ -26,7 +26,7 @@ class Argument implements SassNode { String get originalName => defaultValue == null ? span.text : declarationName(span); - Argument(this.name, {this.defaultValue, this.span}); + Argument(this.name, {this.defaultValue, required this.span}); String toString() => defaultValue == null ? name : "$name: $defaultValue"; } diff --git a/lib/src/ast/sass/argument_declaration.dart b/lib/src/ast/sass/argument_declaration.dart index 9f08ae07f..9b7f71584 100644 --- a/lib/src/ast/sass/argument_declaration.dart +++ b/lib/src/ast/sass/argument_declaration.dart @@ -19,7 +19,7 @@ class ArgumentDeclaration implements SassNode { /// The name of the rest argument (as in `$args...`), or `null` if none was /// declared. - final String restArgument; + final String? restArgument; final FileSpan span; @@ -49,28 +49,15 @@ class ArgumentDeclaration implements SassNode { return span.file.span(i + 1, span.end.offset).trim(); } - /// The name of the rest argument as written in the document, without - /// underscores converted to hyphens and including the leading `$`. - /// - /// This isn't particularly efficient, and should only be used for error - /// messages. - String get originalRestArgument { - if (restArgument == null) return null; - - var text = span.text; - var fromDollar = text.substring(text.lastIndexOf("\$")); - return fromDollar.substring(0, text.indexOf(".")); - } - /// Returns whether this declaration takes no arguments. bool get isEmpty => arguments.isEmpty && restArgument == null; ArgumentDeclaration(Iterable arguments, - {this.restArgument, this.span}) + {this.restArgument, required this.span}) : arguments = List.unmodifiable(arguments); /// Creates a declaration that declares no arguments. - ArgumentDeclaration.empty({this.span}) + ArgumentDeclaration.empty({required this.span}) : arguments = const [], restArgument = null; @@ -81,7 +68,7 @@ class ArgumentDeclaration implements SassNode { /// /// Throws a [SassFormatException] if parsing fails. factory ArgumentDeclaration.parse(String contents, - {Object url, Logger logger}) => + {Object? url, Logger? logger}) => ScssParser(contents, url: url, logger: logger).parseArgumentDeclaration(); /// Throws a [SassScriptException] if [positional] and [names] aren't valid @@ -113,7 +100,7 @@ class ArgumentDeclaration implements SassNode { "Only ${arguments.length} " "${names.isEmpty ? '' : 'positional '}" "${pluralize('argument', arguments.length)} allowed, but " - "${positional} ${pluralize('was', positional, plural: 'were')} " + "$positional ${pluralize('was', positional, plural: 'were')} " "passed.", "invocation", {spanWithName: "declaration"}); @@ -133,7 +120,11 @@ class ArgumentDeclaration implements SassNode { /// Returns the argument named [name] with a leading `$` and its original /// underscores (which are otherwise converted to hyphens). String _originalArgumentName(String name) { - if (name == restArgument) return originalRestArgument; + if (name == restArgument) { + var text = span.text; + var fromDollar = text.substring(text.lastIndexOf("\$")); + return fromDollar.substring(0, text.indexOf(".")); + } for (var argument in arguments) { if (argument.name == name) return argument.originalName; diff --git a/lib/src/ast/sass/argument_invocation.dart b/lib/src/ast/sass/argument_invocation.dart index b18fcb07f..3fb5d7fb9 100644 --- a/lib/src/ast/sass/argument_invocation.dart +++ b/lib/src/ast/sass/argument_invocation.dart @@ -16,10 +16,10 @@ class ArgumentInvocation implements SassNode { final Map named; /// The first rest argument (as in `$args...`). - final Expression rest; + final Expression? rest; /// The second rest argument, which is expected to only contain a keyword map. - final Expression keywordRest; + final Expression? keywordRest; final FileSpan span; diff --git a/lib/src/ast/sass/at_root_query.dart b/lib/src/ast/sass/at_root_query.dart index 3c4e7f73a..6fb57b3ba 100644 --- a/lib/src/ast/sass/at_root_query.dart +++ b/lib/src/ast/sass/at_root_query.dart @@ -50,7 +50,7 @@ class AtRootQuery { /// If passed, [url] is the name of the file from which [contents] comes. /// /// Throws a [SassFormatException] if parsing fails. - factory AtRootQuery.parse(String contents, {Object url, Logger logger}) => + factory AtRootQuery.parse(String contents, {Object? url, Logger? logger}) => AtRootQueryParser(contents, url: url, logger: logger).parse(); /// Returns whether [this] excludes [node]. diff --git a/lib/src/ast/sass/expression.dart b/lib/src/ast/sass/expression.dart index 579c8482b..3e642f41c 100644 --- a/lib/src/ast/sass/expression.dart +++ b/lib/src/ast/sass/expression.dart @@ -17,6 +17,6 @@ abstract class Expression implements SassNode { /// If passed, [url] is the name of the file from which [contents] comes. /// /// Throws a [SassFormatException] if parsing fails. - factory Expression.parse(String contents, {Object url, Logger logger}) => + factory Expression.parse(String contents, {Object? url, Logger? logger}) => ScssParser(contents, url: url, logger: logger).parseExpression(); } diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart index b1608288b..5d7e566c6 100644 --- a/lib/src/ast/sass/expression/binary_operation.dart +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -5,7 +5,6 @@ import 'package:source_span/source_span.dart'; import 'package:charcode/charcode.dart'; -import '../../../utils.dart'; import '../../../visitor/interface/expression.dart'; import '../expression.dart'; @@ -29,14 +28,14 @@ class BinaryOperationExpression implements Expression { // expressions in a row by moving to the left- and right-most expressions. var left = this.left; while (left is BinaryOperationExpression) { - left = (left as BinaryOperationExpression).left; + left = left.left; } var right = this.right; while (right is BinaryOperationExpression) { - right = (right as BinaryOperationExpression).right; + right = right.right; } - return spanForList([left, right]); + return left.span.expand(right.span); } BinaryOperationExpression(this.operator, this.left, this.right) diff --git a/lib/src/ast/sass/expression/color.dart b/lib/src/ast/sass/expression/color.dart index 49f1d62ae..359aef859 100644 --- a/lib/src/ast/sass/expression/color.dart +++ b/lib/src/ast/sass/expression/color.dart @@ -13,9 +13,9 @@ class ColorExpression implements Expression { /// The value of this color. final SassColor value; - FileSpan get span => value.originalSpan; + final FileSpan span; - ColorExpression(this.value); + ColorExpression(this.value) : span = value.originalSpan!; T accept(ExpressionVisitor visitor) => visitor.visitColorExpression(this); diff --git a/lib/src/ast/sass/expression/function.dart b/lib/src/ast/sass/expression/function.dart index 682e5a76a..ca9d3c5a7 100644 --- a/lib/src/ast/sass/expression/function.dart +++ b/lib/src/ast/sass/expression/function.dart @@ -16,7 +16,7 @@ import '../interpolation.dart'; class FunctionExpression implements Expression, CallableInvocation { /// The namespace of the function being invoked, or `null` if it's invoked /// without a namespace. - final String namespace; + final String? namespace; /// The name of the function being invoked. /// diff --git a/lib/src/ast/sass/expression/list.dart b/lib/src/ast/sass/expression/list.dart index ac5a61aa5..661e1bbf5 100644 --- a/lib/src/ast/sass/expression/list.dart +++ b/lib/src/ast/sass/expression/list.dart @@ -5,7 +5,6 @@ import 'package:charcode/charcode.dart'; import 'package:source_span/source_span.dart'; -import '../../../utils.dart'; import '../../../value.dart'; import '../../../visitor/interface/expression.dart'; import '../expression.dart'; @@ -25,13 +24,12 @@ class ListExpression implements Expression { final FileSpan span; ListExpression(Iterable contents, ListSeparator separator, - {bool brackets = false, FileSpan span}) + {bool brackets = false, required FileSpan span}) : this._(List.unmodifiable(contents), separator, brackets, span); - ListExpression._(List contents, this.separator, this.hasBrackets, - FileSpan span) - : contents = contents, - span = span ?? spanForList(contents); + ListExpression._( + List contents, this.separator, this.hasBrackets, this.span) + : contents = contents; T accept(ExpressionVisitor visitor) => visitor.visitListExpression(this); diff --git a/lib/src/ast/sass/expression/number.dart b/lib/src/ast/sass/expression/number.dart index 7868ceff9..ccee50137 100644 --- a/lib/src/ast/sass/expression/number.dart +++ b/lib/src/ast/sass/expression/number.dart @@ -13,7 +13,7 @@ class NumberExpression implements Expression { final num value; /// The number's unit, or `null`. - final String unit; + final String? unit; final FileSpan span; @@ -22,5 +22,5 @@ class NumberExpression implements Expression { T accept(ExpressionVisitor visitor) => visitor.visitNumberExpression(this); - String toString() => "${value}${unit ?? ''}"; + String toString() => "$value${unit ?? ''}"; } diff --git a/lib/src/ast/sass/expression/string.dart b/lib/src/ast/sass/expression/string.dart index 5504cfe2c..b2eb7a542 100644 --- a/lib/src/ast/sass/expression/string.dart +++ b/lib/src/ast/sass/expression/string.dart @@ -26,10 +26,14 @@ class StringExpression implements Expression { /// Returns Sass source for a quoted string that, when evaluated, will have /// [text] as its contents. - static String quoteText(String text) => - StringExpression.plain(text, null, quotes: true) - .asInterpolation(static: true) - .asPlain; + static String quoteText(String text) { + var quote = _bestQuote([text]); + var buffer = StringBuffer(); + buffer.writeCharCode(quote); + _quoteInnerText(text, quote, buffer, static: true); + buffer.writeCharCode(quote); + return buffer.toString(); + } StringExpression(this.text, {bool quotes = false}) : hasQuotes = quotes; @@ -48,59 +52,67 @@ class StringExpression implements Expression { /// If [static] is true, this escapes any `#{` sequences in the string. If /// [quote] is passed, it uses that character as the quote mark; otherwise, it /// determines the best quote to add by looking at the string. - Interpolation asInterpolation({bool static = false, int quote}) { + Interpolation asInterpolation({bool static = false, int? quote}) { if (!hasQuotes) return text; - quote ??= hasQuotes ? _bestQuote() : null; + quote ??= _bestQuote(text.contents.whereType()); var buffer = InterpolationBuffer(); - if (quote != null) buffer.writeCharCode(quote); + 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) { - for (var i = 0; i < value.length; i++) { - var codeUnit = value.codeUnitAt(i); - - if (isNewline(codeUnit)) { - buffer.writeCharCode($backslash); - buffer.writeCharCode($a); - if (i != value.length - 1) { - var next = value.codeUnitAt(i + 1); - if (isWhitespace(next) || isHex(next)) { - buffer.writeCharCode($space); - } - } - } else { - if (codeUnit == quote || - codeUnit == $backslash || - (static && - codeUnit == $hash && - i < value.length - 1 && - value.codeUnitAt(i + 1) == $lbrace)) { - buffer.writeCharCode($backslash); - } - buffer.writeCharCode(codeUnit); - } - } + _quoteInnerText(value, quote, buffer, static: static); } } - if (quote != null) buffer.writeCharCode(quote); + buffer.writeCharCode(quote); return buffer.interpolation(text.span); } - /// Returns the code unit for the best quote to use when converting this - /// string to Sass source. - int _bestQuote() { - var containsDoubleQuote = false; - for (var value in text.contents) { - if (value is String) { - for (var i = 0; i < value.length; i++) { - var codeUnit = value.codeUnitAt(i); - if (codeUnit == $single_quote) return $double_quote; - if (codeUnit == $double_quote) containsDoubleQuote = true; + /// Writes to [buffer] the contents of a string (without quotes) that evalutes + /// to [text] according to Sass's parsing logic. + /// + /// This always adds an escape sequence before [quote]. If [static] is true, + /// it also escapes any `#{` sequences in the string. + 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); + } + } + } else { + if (codeUnit == quote || + codeUnit == $backslash || + (static && + codeUnit == $hash && + i < text.length - 1 && + text.codeUnitAt(i + 1) == $lbrace)) { + buffer.writeCharCode($backslash); } + buffer.writeCharCode(codeUnit); + } + } + } + + /// Returns the code unit for the best quote to use when converting [strings] + /// to Sass source. + 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); + if (codeUnit == $single_quote) return $double_quote; + if (codeUnit == $double_quote) containsDoubleQuote = true; } } return containsDoubleQuote ? $single_quote : $double_quote; diff --git a/lib/src/ast/sass/expression/value.dart b/lib/src/ast/sass/expression/value.dart index 1ef46e52e..e188d0d98 100644 --- a/lib/src/ast/sass/expression/value.dart +++ b/lib/src/ast/sass/expression/value.dart @@ -18,7 +18,7 @@ class ValueExpression implements Expression { final FileSpan span; - ValueExpression(this.value, [this.span]); + ValueExpression(this.value, this.span); T accept(ExpressionVisitor visitor) => visitor.visitValueExpression(this); diff --git a/lib/src/ast/sass/expression/variable.dart b/lib/src/ast/sass/expression/variable.dart index ab01f9440..7cb5357e9 100644 --- a/lib/src/ast/sass/expression/variable.dart +++ b/lib/src/ast/sass/expression/variable.dart @@ -11,7 +11,7 @@ import '../expression.dart'; class VariableExpression implements Expression { /// The namespace of the variable being referenced, or `null` if it's /// referenced without a namespace. - final String namespace; + final String? namespace; /// The name of this variable, with underscores converted to hyphens. final String name; diff --git a/lib/src/ast/sass/import/static.dart b/lib/src/ast/sass/import/static.dart index c3bf69de3..a9bc57aa0 100644 --- a/lib/src/ast/sass/import/static.dart +++ b/lib/src/ast/sass/import/static.dart @@ -18,11 +18,11 @@ class StaticImport implements Import { /// The supports condition attached to this import, or `null` if no condition /// is attached. - final SupportsCondition supports; + final SupportsCondition? supports; /// The media query attached to this import, or `null` if no condition is /// attached. - final Interpolation media; + final Interpolation? media; final FileSpan span; diff --git a/lib/src/ast/sass/interpolation.dart b/lib/src/ast/sass/interpolation.dart index 2fd68f181..5df0b7c14 100644 --- a/lib/src/ast/sass/interpolation.dart +++ b/lib/src/ast/sass/interpolation.dart @@ -20,7 +20,7 @@ class Interpolation implements SassNode { /// If this contains no interpolated expressions, returns its text contents. /// /// Otherwise, returns `null`. - String get asPlain { + String? get asPlain { if (contents.isEmpty) return ''; if (contents.length > 1) return null; var first = contents.first; diff --git a/lib/src/ast/sass/statement/at_root_rule.dart b/lib/src/ast/sass/statement/at_root_rule.dart index dc433a9ae..ec3e6f069 100644 --- a/lib/src/ast/sass/statement/at_root_rule.dart +++ b/lib/src/ast/sass/statement/at_root_rule.dart @@ -12,10 +12,10 @@ import 'parent.dart'; /// An `@at-root` rule. /// /// This moves it contents "up" the tree through parent nodes. -class AtRootRule extends ParentStatement { +class AtRootRule extends ParentStatement> { /// The query specifying which statements this should move its contents /// through. - final Interpolation query; + final Interpolation? query; final FileSpan span; diff --git a/lib/src/ast/sass/statement/at_rule.dart b/lib/src/ast/sass/statement/at_rule.dart index d35a45210..91075e1e3 100644 --- a/lib/src/ast/sass/statement/at_rule.dart +++ b/lib/src/ast/sass/statement/at_rule.dart @@ -15,11 +15,11 @@ class AtRule extends ParentStatement { final Interpolation name; /// The value of this rule. - final Interpolation value; + final Interpolation? value; final FileSpan span; - AtRule(this.name, this.span, {this.value, Iterable children}) + AtRule(this.name, this.span, {this.value, Iterable? children}) : super(children == null ? null : List.unmodifiable(children)); T accept(StatementVisitor visitor) => visitor.visitAtRule(this); @@ -27,6 +27,8 @@ class AtRule extends ParentStatement { String toString() { var buffer = StringBuffer("@$name"); if (value != null) buffer.write(" $value"); + + var children = this.children; return children == null ? "$buffer;" : "$buffer {${children.join(" ")}}"; } } diff --git a/lib/src/ast/sass/statement/callable_declaration.dart b/lib/src/ast/sass/statement/callable_declaration.dart index f26bb8a82..4c67b3670 100644 --- a/lib/src/ast/sass/statement/callable_declaration.dart +++ b/lib/src/ast/sass/statement/callable_declaration.dart @@ -11,14 +11,12 @@ import 'silent_comment.dart'; /// An abstract class for callables (functions or mixins) that are declared in /// user code. -abstract class CallableDeclaration extends ParentStatement { +abstract class CallableDeclaration extends ParentStatement> { /// The name of this callable, with underscores converted to hyphens. - /// - /// This may be `null` for callables without names. final String name; /// The comment immediately preceding this declaration. - final SilentComment comment; + final SilentComment? comment; /// The declared arguments this callable accepts. final ArgumentDeclaration arguments; @@ -27,7 +25,7 @@ abstract class CallableDeclaration extends ParentStatement { CallableDeclaration( this.name, this.arguments, Iterable children, this.span, - {SilentComment comment}) + {SilentComment? comment}) : comment = comment, super(List.unmodifiable(children)); } diff --git a/lib/src/ast/sass/statement/content_block.dart b/lib/src/ast/sass/statement/content_block.dart index 85881d7ed..0b1c85e08 100644 --- a/lib/src/ast/sass/statement/content_block.dart +++ b/lib/src/ast/sass/statement/content_block.dart @@ -13,7 +13,7 @@ import 'callable_declaration.dart'; class ContentBlock extends CallableDeclaration { ContentBlock(ArgumentDeclaration arguments, Iterable children, FileSpan span) - : super(null /* name */, arguments, children, span); + : super("@content", arguments, children, span); T accept(StatementVisitor visitor) => visitor.visitContentBlock(this); diff --git a/lib/src/ast/sass/statement/declaration.dart b/lib/src/ast/sass/statement/declaration.dart index 6d6240ece..f7a889046 100644 --- a/lib/src/ast/sass/statement/declaration.dart +++ b/lib/src/ast/sass/statement/declaration.dart @@ -17,7 +17,10 @@ class Declaration extends ParentStatement { final Interpolation name; /// The value of this declaration. - final Expression value; + /// + /// If [children] is `null`, this is never `null`. Otherwise, it may or may + /// not be `null`. + final Expression? value; final FileSpan span; @@ -30,13 +33,25 @@ class Declaration extends ParentStatement { /// If this is `true`, then `value` will be a [StringExpression]. bool get isCustomProperty => name.initialPlain.startsWith('--'); - Declaration(this.name, this.span, {this.value, Iterable children}) - : super( - children = children == null ? null : List.unmodifiable(children)) { + Declaration(this.name, Expression value, this.span) + : value = value, + super(null) { if (isCustomProperty && value is! StringExpression) { throw ArgumentError( 'Declarations whose names begin with "--" must have StringExpression ' - 'values (was `${value}` of type ${value.runtimeType}).'); + 'values (was `$value` of type ${value.runtimeType}).'); + } + } + + /// Creates a declaration with children. + /// + /// For these declaraions, a value is optional. + Declaration.nested(this.name, Iterable children, this.span, + {this.value}) + : super(List.unmodifiable(children)) { + if (isCustomProperty && value is! StringExpression) { + throw ArgumentError( + 'Declarations whose names begin with "--" may not be nested.'); } } diff --git a/lib/src/ast/sass/statement/each_rule.dart b/lib/src/ast/sass/statement/each_rule.dart index cd1ad7b77..4efa30076 100644 --- a/lib/src/ast/sass/statement/each_rule.dart +++ b/lib/src/ast/sass/statement/each_rule.dart @@ -12,7 +12,7 @@ import 'parent.dart'; /// An `@each` rule. /// /// This iterates over values in a list or map. -class EachRule extends ParentStatement { +class EachRule extends ParentStatement> { /// The variables assigned for each iteration. final List variables; diff --git a/lib/src/ast/sass/statement/for_rule.dart b/lib/src/ast/sass/statement/for_rule.dart index eae380a0e..bde7721b2 100644 --- a/lib/src/ast/sass/statement/for_rule.dart +++ b/lib/src/ast/sass/statement/for_rule.dart @@ -12,7 +12,7 @@ import 'parent.dart'; /// A `@for` rule. /// /// This iterates a set number of times. -class ForRule extends ParentStatement { +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 542d0b1b7..53ca23f31 100644 --- a/lib/src/ast/sass/statement/forward_rule.dart +++ b/lib/src/ast/sass/statement/forward_rule.dart @@ -26,7 +26,7 @@ class ForwardRule implements Statement { /// If this is non-`null`, [hiddenMixinsAndFunctions] and [hiddenVariables] /// are guaranteed to both be `null` and [shownVariables] is guaranteed to be /// non-`null`. - final Set shownMixinsAndFunctions; + final Set? shownMixinsAndFunctions; /// The set of variable names (without `$`) that may be accessed from the /// forwarded module. @@ -37,7 +37,7 @@ class ForwardRule implements Statement { /// If this is non-`null`, [hiddenMixinsAndFunctions] and [hiddenVariables] /// are guaranteed to both be `null` and [shownMixinsAndFunctions] is /// guaranteed to be non-`null`. - final Set shownVariables; + final Set? shownVariables; /// The set of mixin and function names that may not be accessed from the /// forwarded module. @@ -48,7 +48,7 @@ class ForwardRule implements Statement { /// If this is non-`null`, [shownMixinsAndFunctions] and [shownVariables] are /// guaranteed to both be `null` and [hiddenVariables] is guaranteed to be /// non-`null`. - final Set hiddenMixinsAndFunctions; + final Set? hiddenMixinsAndFunctions; /// The set of variable names (without `$`) that may be accessed from the /// forwarded module. @@ -59,11 +59,11 @@ class ForwardRule implements Statement { /// If this is non-`null`, [shownMixinsAndFunctions] and [shownVariables] are /// guaranteed to both be `null` and [hiddenMixinsAndFunctions] is guaranteed /// to be non-`null`. - final Set hiddenVariables; + final Set? hiddenVariables; /// The prefix to add to the beginning of the names of members of the used /// module, or `null` if member names are used as-is. - final String prefix; + final String? prefix; /// A list of variable assignments used to configure the loaded modules. final List configuration; @@ -72,7 +72,7 @@ class ForwardRule implements Statement { /// Creates a `@forward` rule that allows all members to be accessed. ForwardRule(this.url, this.span, - {this.prefix, Iterable configuration}) + {this.prefix, Iterable? configuration}) : shownMixinsAndFunctions = null, shownVariables = null, hiddenMixinsAndFunctions = null, @@ -84,7 +84,7 @@ class ForwardRule implements Statement { /// [shownMixinsAndFunctions] and [shownVariables] to be accessed. ForwardRule.show(this.url, Iterable shownMixinsAndFunctions, Iterable shownVariables, this.span, - {this.prefix, Iterable configuration}) + {this.prefix, Iterable? configuration}) : shownMixinsAndFunctions = UnmodifiableSetView(Set.of(shownMixinsAndFunctions)), shownVariables = UnmodifiableSetView(Set.of(shownVariables)), @@ -97,7 +97,7 @@ class ForwardRule implements Statement { /// [hiddenMixinsAndFunctions] and [hiddenVariables] to be accessed. ForwardRule.hide(this.url, Iterable hiddenMixinsAndFunctions, Iterable hiddenVariables, this.span, - {this.prefix, Iterable configuration}) + {this.prefix, Iterable? configuration}) : shownMixinsAndFunctions = null, shownVariables = null, hiddenMixinsAndFunctions = @@ -112,17 +112,20 @@ class ForwardRule implements Statement { var buffer = StringBuffer("@forward ${StringExpression.quoteText(url.toString())}"); + var shownMixinsAndFunctions = this.shownMixinsAndFunctions; + var hiddenMixinsAndFunctions = this.hiddenMixinsAndFunctions; if (shownMixinsAndFunctions != null) { buffer ..write(" show ") - ..write(_memberList(shownMixinsAndFunctions, shownVariables)); + ..write(_memberList(shownMixinsAndFunctions, shownVariables!)); } else if (hiddenMixinsAndFunctions != null && hiddenMixinsAndFunctions.isNotEmpty) { buffer ..write(" hide ") - ..write(_memberList(hiddenMixinsAndFunctions, hiddenVariables)); + ..write(_memberList(hiddenMixinsAndFunctions, hiddenVariables!)); } + var prefix = this.prefix; if (prefix != null) buffer.write(" as $prefix*"); if (configuration.isNotEmpty) { @@ -136,9 +139,5 @@ class ForwardRule implements Statement { /// Returns a combined list of names of the given members. String _memberList( Iterable mixinsAndFunctions, Iterable variables) => - [ - if (shownMixinsAndFunctions != null) ...shownMixinsAndFunctions, - if (shownVariables != null) - for (var name in shownVariables) "\$$name" - ].join(", "); + [...mixinsAndFunctions, for (var name in variables) "\$$name"].join(", "); } diff --git a/lib/src/ast/sass/statement/function_rule.dart b/lib/src/ast/sass/statement/function_rule.dart index 78bcd4d94..136194ab1 100644 --- a/lib/src/ast/sass/statement/function_rule.dart +++ b/lib/src/ast/sass/statement/function_rule.dart @@ -16,7 +16,7 @@ import 'silent_comment.dart'; class FunctionRule extends CallableDeclaration { FunctionRule(String name, ArgumentDeclaration arguments, Iterable children, FileSpan span, - {SilentComment comment}) + {SilentComment? comment}) : super(name, arguments, children, span, comment: comment); T accept(StatementVisitor visitor) => visitor.visitFunctionRule(this); diff --git a/lib/src/ast/sass/statement/if_rule.dart b/lib/src/ast/sass/statement/if_rule.dart index 84050d58d..d69cd637e 100644 --- a/lib/src/ast/sass/statement/if_rule.dart +++ b/lib/src/ast/sass/statement/if_rule.dart @@ -27,55 +27,61 @@ class IfRule implements Statement { /// The final, unconditional `@else` clause. /// /// This is `null` if there is no unconditional `@else`. - final IfClause lastClause; + final ElseClause? lastClause; final FileSpan span; IfRule(Iterable clauses, this.span, {this.lastClause}) - : clauses = List.unmodifiable(clauses) { - assert(clauses.every((clause) => clause.expression != null)); - assert(lastClause?.expression == null); - } + : clauses = List.unmodifiable(clauses); T accept(StatementVisitor visitor) => visitor.visitIfRule(this); String toString() { var first = true; - return clauses.map((clause) { - var name = first ? 'if' : 'else'; - first = false; - return '@$name ${clause.expression} {${clause.children.join(" ")}}'; - }).join(' '); + var result = clauses + .map((clause) => + "@${first ? 'if' : 'else if'} {${clause.children.join(' ')}}") + .join(' '); + + var lastClause = this.lastClause; + if (lastClause != null) result += " $lastClause"; + return result; } } -/// A single clause in an `@if` rule. -class IfClause { - /// The expression to evaluate to determine whether to run this rule, or - /// `null` if this is the final unconditional `@else` clause. - final Expression expression; - +/// The superclass of `@if` and `@else` clauses. +abstract class IfRuleClause { /// The statements to evaluate if this clause matches. final List children; /// Whether any of [children] is a variable, function, or mixin declaration. final bool hasDeclarations; - IfClause(Expression expression, Iterable children) - : this._(expression, List.unmodifiable(children)); - - IfClause.last(Iterable children) - : this._(null, List.unmodifiable(children)); + IfRuleClause(Iterable children) + : this._(List.unmodifiable(children)); - IfClause._(this.expression, this.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))); +} + +/// An `@if` or `@else if` clause in an `@if` rule. +class IfClause extends IfRuleClause { + /// The expression to evaluate to determine whether to run this rule. + final Expression expression; + + IfClause(this.expression, Iterable children) : super(children); + + String toString() => "@if $expression {${children.join(' ')}}"; +} + +/// An `@else` clause in an `@if` rule. +class ElseClause extends IfRuleClause { + ElseClause(Iterable children) : super(children); - String toString() => - (expression == null ? "@else" : "@if $expression") + - " {${children.join(' ')}}"; + String toString() => "@else {${children.join(' ')}}"; } diff --git a/lib/src/ast/sass/statement/include_rule.dart b/lib/src/ast/sass/statement/include_rule.dart index b7c6840b9..1a51e6cd0 100644 --- a/lib/src/ast/sass/statement/include_rule.dart +++ b/lib/src/ast/sass/statement/include_rule.dart @@ -15,7 +15,7 @@ import 'content_block.dart'; class IncludeRule implements Statement, CallableInvocation { /// The namespace of the mixin being invoked, or `null` if it's invoked /// without a namespace. - final String namespace; + final String? namespace; /// The name of the mixin being invoked, with underscores converted to /// hyphens. @@ -26,7 +26,7 @@ class IncludeRule implements Statement, CallableInvocation { /// The block that will be invoked for [ContentRule]s in the mixin being /// invoked, or `null` if this doesn't pass a content block. - final ContentBlock content; + final ContentBlock? content; final FileSpan span; diff --git a/lib/src/ast/sass/statement/media_rule.dart b/lib/src/ast/sass/statement/media_rule.dart index a73b806cd..04e4911f4 100644 --- a/lib/src/ast/sass/statement/media_rule.dart +++ b/lib/src/ast/sass/statement/media_rule.dart @@ -10,7 +10,7 @@ import '../statement.dart'; import 'parent.dart'; /// A `@media` rule. -class MediaRule extends ParentStatement { +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 7256e92c2..1b3ceb2c3 100644 --- a/lib/src/ast/sass/statement/mixin_rule.dart +++ b/lib/src/ast/sass/statement/mixin_rule.dart @@ -24,7 +24,7 @@ class MixinRule extends CallableDeclaration { /// won't work correctly. MixinRule(String name, ArgumentDeclaration arguments, Iterable children, FileSpan span, - {this.hasContent = false, SilentComment comment}) + {this.hasContent = false, SilentComment? comment}) : super(name, arguments, children, span, comment: comment); T accept(StatementVisitor visitor) => visitor.visitMixinRule(this); diff --git a/lib/src/ast/sass/statement/parent.dart b/lib/src/ast/sass/statement/parent.dart index 06d6560b2..0c0fd383e 100644 --- a/lib/src/ast/sass/statement/parent.dart +++ b/lib/src/ast/sass/statement/parent.dart @@ -10,9 +10,13 @@ import 'mixin_rule.dart'; import 'variable_declaration.dart'; /// A [Statement] that can have child statements. -abstract class ParentStatement implements Statement { +/// +/// This has a generic parameter so that its subclasses can choose whether or +/// not their children lists are nullable. +abstract class ParentStatement?> + implements Statement { /// The child statements of this statement. - final List children; + final T children; /// Whether any of [children] is a variable, function, or mixin declaration, /// or a dynamic import rule. diff --git a/lib/src/ast/sass/statement/silent_comment.dart b/lib/src/ast/sass/statement/silent_comment.dart index 4a001619c..c716ae3c4 100644 --- a/lib/src/ast/sass/statement/silent_comment.dart +++ b/lib/src/ast/sass/statement/silent_comment.dart @@ -18,7 +18,7 @@ class SilentComment implements Statement { /// /// The leading slashes and space on each line is removed. Returns `null` when /// there is no documentation comment. - String get docComment { + String? get docComment { var buffer = StringBuffer(); for (var line in text.split('\n')) { var scanner = StringScanner(line.trim()); diff --git a/lib/src/ast/sass/statement/style_rule.dart b/lib/src/ast/sass/statement/style_rule.dart index 0b207efe9..f6e4f9734 100644 --- a/lib/src/ast/sass/statement/style_rule.dart +++ b/lib/src/ast/sass/statement/style_rule.dart @@ -12,7 +12,7 @@ import 'parent.dart'; /// A style rule. /// /// This applies style declarations to elements that match a given selector. -class StyleRule extends ParentStatement { +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 2cabd259e..e7c440fe2 100644 --- a/lib/src/ast/sass/statement/stylesheet.dart +++ b/lib/src/ast/sass/statement/stylesheet.dart @@ -23,7 +23,7 @@ import 'variable_declaration.dart'; /// A Sass stylesheet. /// /// This is the root Sass node. It contains top-level statements. -class Stylesheet extends ParentStatement { +class Stylesheet extends ParentStatement> { final FileSpan span; /// Whether this was parsed from a plain CSS stylesheet. @@ -58,7 +58,7 @@ class Stylesheet extends ParentStatement { /// /// Throws a [SassFormatException] if parsing fails. factory Stylesheet.parse(String contents, Syntax syntax, - {Object url, Logger logger}) { + {Object? url, Logger? logger}) { switch (syntax) { case Syntax.sass: return Stylesheet.parseSass(contents, url: url, logger: logger); @@ -76,7 +76,8 @@ class Stylesheet extends ParentStatement { /// If passed, [url] is the name of the file from which [contents] comes. /// /// Throws a [SassFormatException] if parsing fails. - factory Stylesheet.parseSass(String contents, {Object url, Logger logger}) => + factory Stylesheet.parseSass(String contents, + {Object? url, Logger? logger}) => SassParser(contents, url: url, logger: logger).parse(); /// Parses an SCSS stylesheet from [contents]. @@ -84,7 +85,8 @@ class Stylesheet extends ParentStatement { /// If passed, [url] is the name of the file from which [contents] comes. /// /// Throws a [SassFormatException] if parsing fails. - factory Stylesheet.parseScss(String contents, {Object url, Logger logger}) => + factory Stylesheet.parseScss(String contents, + {Object? url, Logger? logger}) => ScssParser(contents, url: url, logger: logger).parse(); /// Parses a plain CSS stylesheet from [contents]. @@ -92,7 +94,7 @@ class Stylesheet extends ParentStatement { /// If passed, [url] is the name of the file from which [contents] comes. /// /// Throws a [SassFormatException] if parsing fails. - factory Stylesheet.parseCss(String contents, {Object url, Logger logger}) => + factory Stylesheet.parseCss(String contents, {Object? url, Logger? logger}) => CssParser(contents, url: url, logger: logger).parse(); T accept(StatementVisitor visitor) => visitor.visitStylesheet(this); diff --git a/lib/src/ast/sass/statement/supports_rule.dart b/lib/src/ast/sass/statement/supports_rule.dart index e83957075..8898a8f1f 100644 --- a/lib/src/ast/sass/statement/supports_rule.dart +++ b/lib/src/ast/sass/statement/supports_rule.dart @@ -10,7 +10,7 @@ import '../supports_condition.dart'; import 'parent.dart'; /// A `@supports` rule. -class SupportsRule extends ParentStatement { +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 3a582c089..ffa13a839 100644 --- a/lib/src/ast/sass/statement/use_rule.dart +++ b/lib/src/ast/sass/statement/use_rule.dart @@ -20,7 +20,7 @@ class UseRule implements Statement { /// The namespace for members of the used module, or `null` if the members /// can be accessed without a namespace. - final String namespace; + final String? namespace; /// A list of variable assignments used to configure the loaded modules. final List configuration; @@ -28,10 +28,10 @@ class UseRule implements Statement { final FileSpan span; UseRule(this.url, this.namespace, this.span, - {Iterable configuration}) + {Iterable? configuration}) : configuration = configuration == null ? const [] - : List.unmodifiable(configuration) { + : List.unmodifiable(configuration) { for (var variable in this.configuration) { if (variable.isGuarded) { throw ArgumentError.value(variable, "configured variable", @@ -45,7 +45,7 @@ class UseRule implements Statement { /// If passed, [url] is the name of the file from which [contents] comes. /// /// Throws a [SassFormatException] if parsing fails. - factory UseRule.parse(String contents, {Object url, Logger logger}) => + factory UseRule.parse(String contents, {Object? url, Logger? logger}) => ScssParser(contents, url: url, logger: logger).parseUseRule(); T accept(StatementVisitor visitor) => visitor.visitUseRule(this); diff --git a/lib/src/ast/sass/statement/variable_declaration.dart b/lib/src/ast/sass/statement/variable_declaration.dart index 81f2b1ea9..4f401a2a5 100644 --- a/lib/src/ast/sass/statement/variable_declaration.dart +++ b/lib/src/ast/sass/statement/variable_declaration.dart @@ -18,13 +18,13 @@ import 'silent_comment.dart'; class VariableDeclaration implements Statement { /// The namespace of the variable being set, or `null` if it's defined or set /// without a namespace. - final String namespace; + final String? namespace; /// The name of the variable. final String name; /// The comment immediately preceding this declaration. - SilentComment comment; + SilentComment? comment; /// The value the variable is being assigned to. final Expression expression; @@ -52,7 +52,7 @@ class VariableDeclaration implements Statement { {this.namespace, bool guarded = false, bool global = false, - SilentComment comment}) + SilentComment? comment}) : isGuarded = guarded, isGlobal = global, comment = comment { @@ -68,7 +68,7 @@ class VariableDeclaration implements Statement { /// /// Throws a [SassFormatException] if parsing fails. factory VariableDeclaration.parse(String contents, - {Object url, Logger logger}) => + {Object? url, Logger? logger}) => ScssParser(contents, url: url, logger: logger).parseVariableDeclaration(); T accept(StatementVisitor visitor) => diff --git a/lib/src/ast/sass/statement/while_rule.dart b/lib/src/ast/sass/statement/while_rule.dart index 69cff0889..7aa1b1bd7 100644 --- a/lib/src/ast/sass/statement/while_rule.dart +++ b/lib/src/ast/sass/statement/while_rule.dart @@ -13,14 +13,14 @@ import 'parent.dart'; /// /// This repeatedly executes a block of code as long as a statement evaluates to /// `true`. -class WhileRule extends ParentStatement { +class WhileRule extends ParentStatement> { /// The condition that determines whether the block will be executed. final Expression condition; final FileSpan span; WhileRule(this.condition, Iterable children, this.span) - : super(List.unmodifiable(children)); + : super(List.unmodifiable(children)); T accept(StatementVisitor visitor) => visitor.visitWhileRule(this); diff --git a/lib/src/ast/sass/supports_condition/operation.dart b/lib/src/ast/sass/supports_condition/operation.dart index 0bebbc447..41b5c5218 100644 --- a/lib/src/ast/sass/supports_condition/operation.dart +++ b/lib/src/ast/sass/supports_condition/operation.dart @@ -31,7 +31,7 @@ class SupportsOperation implements SupportsCondition { } String toString() => - "${_parenthesize(left)} ${operator} ${_parenthesize(right)}"; + "${_parenthesize(left)} $operator ${_parenthesize(right)}"; String _parenthesize(SupportsCondition condition) => condition is SupportsNegation || diff --git a/lib/src/ast/selector/attribute.dart b/lib/src/ast/selector/attribute.dart index 9cedbefc2..a3fbdcb18 100644 --- a/lib/src/ast/selector/attribute.dart +++ b/lib/src/ast/selector/attribute.dart @@ -17,7 +17,7 @@ class AttributeSelector extends SimpleSelector { /// /// If this is `null`, this matches any element with the given property, /// regardless of this value. It's `null` if and only if [value] is `null`. - final AttributeOperator op; + final AttributeOperator? op; /// An assertion about the value of [name]. /// @@ -25,7 +25,7 @@ class AttributeSelector extends SimpleSelector { /// /// If this is `null`, this matches any element with the given property, /// regardless of this value. It's `null` if and only if [op] is `null`. - final String value; + final String? value; /// The modifier which indicates how the attribute selector should be /// processed. @@ -35,7 +35,7 @@ class AttributeSelector extends SimpleSelector { /// [case-sensitivity]: https://www.w3.org/TR/selectors-4/#attribute-case /// /// If [op] is `null`, this is always `null` as well. - final String modifier; + final String? modifier; /// Creates an attribute selector that matches any element with a property of /// the given name. diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart index d2503036d..0bf9076aa 100644 --- a/lib/src/ast/selector/complex.dart +++ b/lib/src/ast/selector/complex.dart @@ -33,10 +33,10 @@ class ComplexSelector extends Selector { /// can have a range of possible specificities. int get minSpecificity { if (_minSpecificity == null) _computeSpecificity(); - return _minSpecificity; + return _minSpecificity!; } - int _minSpecificity; + int? _minSpecificity; /// The maximum possible specificity that this selector can have. /// @@ -44,19 +44,13 @@ class ComplexSelector extends Selector { /// can have a range of possible specificities. int get maxSpecificity { if (_maxSpecificity == null) _computeSpecificity(); - return _maxSpecificity; + return _maxSpecificity!; } - int _maxSpecificity; + int? _maxSpecificity; - bool get isInvisible { - if (_isInvisible != null) return _isInvisible; - _isInvisible = components.any( - (component) => component is CompoundSelector && component.isInvisible); - return _isInvisible; - } - - bool _isInvisible; + late final bool isInvisible = components.any( + (component) => component is CompoundSelector && component.isInvisible); ComplexSelector(Iterable components, {this.lineBreak = false}) @@ -77,14 +71,16 @@ class ComplexSelector extends Selector { /// Computes [_minSpecificity] and [_maxSpecificity]. void _computeSpecificity() { - _minSpecificity = 0; - _maxSpecificity = 0; + var minSpecificity = 0; + var maxSpecificity = 0; for (var component in components) { if (component is CompoundSelector) { - _minSpecificity += component.minSpecificity; - _maxSpecificity += component.maxSpecificity; + minSpecificity += component.minSpecificity; + maxSpecificity += component.maxSpecificity; } } + _minSpecificity = minSpecificity; + _maxSpecificity = maxSpecificity; } int get hashCode => listHash(components); diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index e058168e8..f5241250e 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.dart @@ -25,10 +25,10 @@ class CompoundSelector extends Selector implements ComplexSelectorComponent { /// can have a range of possible specificities. int get minSpecificity { if (_minSpecificity == null) _computeSpecificity(); - return _minSpecificity; + return _minSpecificity!; } - int _minSpecificity; + int? _minSpecificity; /// The maximum possible specificity that this selector can have. /// @@ -36,10 +36,10 @@ class CompoundSelector extends Selector implements ComplexSelectorComponent { /// can have a range of possible specificities. int get maxSpecificity { if (_maxSpecificity == null) _computeSpecificity(); - return _maxSpecificity; + return _maxSpecificity!; } - int _maxSpecificity; + int? _maxSpecificity; bool get isInvisible => components.any((component) => component.isInvisible); @@ -58,7 +58,7 @@ class CompoundSelector extends Selector implements ComplexSelectorComponent { /// /// Throws a [SassFormatException] if parsing fails. factory CompoundSelector.parse(String contents, - {Object url, Logger logger, bool allowParent = true}) => + {Object? url, Logger? logger, bool allowParent = true}) => SelectorParser(contents, url: url, logger: logger, allowParent: allowParent) .parseCompoundSelector(); @@ -75,12 +75,14 @@ class CompoundSelector extends Selector implements ComplexSelectorComponent { /// Computes [_minSpecificity] and [_maxSpecificity]. void _computeSpecificity() { - _minSpecificity = 0; - _maxSpecificity = 0; + var minSpecificity = 0; + var maxSpecificity = 0; for (var simple in components) { - _minSpecificity += simple.minSpecificity; - _maxSpecificity += simple.maxSpecificity; + minSpecificity += simple.minSpecificity; + maxSpecificity += simple.maxSpecificity; } + _minSpecificity = minSpecificity; + _maxSpecificity = maxSpecificity; } int get hashCode => listHash(components); diff --git a/lib/src/ast/selector/id.dart b/lib/src/ast/selector/id.dart index 7985d747b..21c3eb1b3 100644 --- a/lib/src/ast/selector/id.dart +++ b/lib/src/ast/selector/id.dart @@ -22,7 +22,7 @@ class IDSelector extends SimpleSelector { IDSelector addSuffix(String suffix) => IDSelector(name + suffix); - List unify(List compound) { + List? unify(List compound) { // A given compound selector may only contain one ID. if (compound.any((simple) => simple is IDSelector && simple != this)) { return null; diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index 73f291735..e8670e3a7 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -54,8 +54,8 @@ class SelectorList extends Selector { /// /// Throws a [SassFormatException] if parsing fails. factory SelectorList.parse(String contents, - {Object url, - Logger logger, + {Object? url, + Logger? logger, bool allowParent = true, bool allowPlaceholder = true}) => SelectorParser(contents, @@ -71,7 +71,7 @@ class SelectorList extends Selector { /// both this and [other]. /// /// If no such list can be produced, returns `null`. - SelectorList unify(SelectorList other) { + SelectorList? unify(SelectorList other) { var contents = components.expand((complex1) { return other.components.expand((complex2) { var unified = unifyComplex([complex1.components, complex2.components]); @@ -91,7 +91,7 @@ class SelectorList extends Selector { /// The given [parent] may be `null`, indicating that this has no parents. If /// so, this list is returned as-is if it doesn't contain any explicit /// [ParentSelector]s. If it does, this throws a [SassScriptException]. - SelectorList resolveParentSelectors(SelectorList parent, + SelectorList resolveParentSelectors(SelectorList? parent, {bool implicitParent = true}) { if (parent == null) { if (!_containsParentSelector) return this; @@ -148,22 +148,24 @@ class SelectorList extends Selector { bool _complexContainsParentSelector(ComplexSelector complex) => complex.components.any((component) => component is CompoundSelector && - component.components.any((simple) => - simple is ParentSelector || - (simple is PseudoSelector && - simple.selector != null && - simple.selector._containsParentSelector))); + component.components.any((simple) { + if (simple is ParentSelector) return true; + if (simple is! PseudoSelector) return false; + var selector = simple.selector; + return selector != null && selector._containsParentSelector; + })); /// Returns a new [CompoundSelector] based on [compound] with all /// [ParentSelector]s replaced with [parent]. /// /// Returns `null` if [compound] doesn't contain any [ParentSelector]s. - Iterable _resolveParentSelectorsCompound( + Iterable? _resolveParentSelectorsCompound( CompoundSelector compound, SelectorList parent) { - var containsSelectorPseudo = compound.components.any((simple) => - simple is PseudoSelector && - simple.selector != null && - simple.selector._containsParentSelector); + var containsSelectorPseudo = compound.components.any((simple) { + if (simple is! PseudoSelector) return false; + var selector = simple.selector; + return selector != null && selector._containsParentSelector; + }); if (!containsSelectorPseudo && compound.components.first is! ParentSelector) { return null; @@ -171,14 +173,12 @@ class SelectorList extends Selector { var resolvedMembers = containsSelectorPseudo ? compound.components.map((simple) { - if (simple is PseudoSelector) { - if (simple.selector == null) return simple; - if (!simple.selector._containsParentSelector) return simple; - return simple.withSelector(simple.selector - .resolveParentSelectors(parent, implicitParent: false)); - } else { - return simple; - } + if (simple is! PseudoSelector) return simple; + var selector = simple.selector; + if (selector == null) return simple; + if (!selector._containsParentSelector) return simple; + return simple.withSelector( + selector.resolveParentSelectors(parent, implicitParent: false)); }) : compound.components; @@ -200,7 +200,7 @@ class SelectorList extends Selector { 'Parent "$complex" is incompatible with this selector.'); } - var last = lastComponent as CompoundSelector; + var last = lastComponent; var suffix = (compound.components.first as ParentSelector).suffix; if (suffix != null) { last = CompoundSelector([ diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart index ec046144e..ed2b77068 100644 --- a/lib/src/ast/selector/parent.dart +++ b/lib/src/ast/selector/parent.dart @@ -15,7 +15,7 @@ class ParentSelector extends SimpleSelector { /// /// This is assumed to be a valid identifier suffix. It may be `null`, /// indicating that the parent selector will not be modified. - final String suffix; + final String? suffix; ParentSelector({this.suffix}); diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index 6b516e11a..51e9fed05 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -51,29 +51,30 @@ class PseudoSelector extends SimpleSelector { /// /// This is `null` if there's no argument. If [argument] and [selector] are /// both non-`null`, the selector follows the argument. - final String argument; + final String? argument; /// The selector argument passed to this selector. /// /// This is `null` if there's no selector. If [argument] and [selector] are /// both non-`null`, the selector follows the argument. - final SelectorList selector; + final SelectorList? selector; int get minSpecificity { if (_minSpecificity == null) _computeSpecificity(); - return _minSpecificity; + return _minSpecificity!; } - int _minSpecificity; + int? _minSpecificity; int get maxSpecificity { if (_maxSpecificity == null) _computeSpecificity(); - return _maxSpecificity; + return _maxSpecificity!; } - int _maxSpecificity; + int? _maxSpecificity; bool get isInvisible { + var selector = this.selector; if (selector == null) return false; // We don't consider `:not(%foo)` to be invisible because, semantically, it @@ -123,7 +124,7 @@ class PseudoSelector extends SimpleSelector { return PseudoSelector(name + suffix, element: isElement); } - List unify(List compound) { + List? unify(List compound) { if (compound.length == 1 && compound.first is UniversalSelector) { return compound.first.unify([this]); } @@ -158,6 +159,7 @@ class PseudoSelector extends SimpleSelector { return; } + var selector = this.selector; if (selector == null) { _minSpecificity = super.minSpecificity; _maxSpecificity = super.maxSpecificity; @@ -165,20 +167,24 @@ class PseudoSelector extends SimpleSelector { } if (name == 'not') { - _minSpecificity = 0; - _maxSpecificity = 0; + var minSpecificity = 0; + var maxSpecificity = 0; for (var complex in selector.components) { - _minSpecificity = math.max(_minSpecificity, complex.minSpecificity); - _maxSpecificity = math.max(_maxSpecificity, complex.maxSpecificity); + minSpecificity = math.max(minSpecificity, complex.minSpecificity); + maxSpecificity = math.max(maxSpecificity, complex.maxSpecificity); } + _minSpecificity = minSpecificity; + _maxSpecificity = maxSpecificity; } else { // This is higher than any selector's specificity can actually be. - _minSpecificity = math.pow(super.minSpecificity, 3) as int; - _maxSpecificity = 0; + var minSpecificity = math.pow(super.minSpecificity, 3) as int; + var maxSpecificity = 0; for (var complex in selector.components) { - _minSpecificity = math.min(_minSpecificity, complex.minSpecificity); - _maxSpecificity = math.max(_maxSpecificity, complex.maxSpecificity); + minSpecificity = math.min(minSpecificity, complex.minSpecificity); + maxSpecificity = math.max(maxSpecificity, complex.maxSpecificity); } + _minSpecificity = minSpecificity; + _maxSpecificity = maxSpecificity; } } diff --git a/lib/src/ast/selector/qualified_name.dart b/lib/src/ast/selector/qualified_name.dart index 24b7a461a..9abcfe0f7 100644 --- a/lib/src/ast/selector/qualified_name.dart +++ b/lib/src/ast/selector/qualified_name.dart @@ -14,7 +14,7 @@ class QualifiedName { /// If this is `null`, [name] belongs to the default namespace. If it's the /// empty string, [name] belongs to no namespace. If it's `*`, [name] belongs /// to any namespace. Otherwise, [name] belongs to the given namespace. - final String namespace; + final String? namespace; QualifiedName(this.name, {this.namespace}); diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index f421db7b8..a32f9082f 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -35,7 +35,7 @@ abstract class SimpleSelector extends Selector { /// /// Throws a [SassFormatException] if parsing fails. factory SimpleSelector.parse(String contents, - {Object url, Logger logger, bool allowParent = true}) => + {Object? url, Logger? logger, bool allowParent = true}) => SelectorParser(contents, url: url, logger: logger, allowParent: allowParent) .parseSimpleSelector(); @@ -57,7 +57,7 @@ abstract class SimpleSelector extends Selector { /// /// Returns `null` if unification is impossible—for example, if there are /// multiple ID selectors. - List unify(List compound) { + List? unify(List compound) { if (compound.length == 1 && compound.first is UniversalSelector) { return compound.first.unify([this]); } diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart index b6116a85b..28d1ba8f4 100644 --- a/lib/src/ast/selector/type.dart +++ b/lib/src/ast/selector/type.dart @@ -21,7 +21,7 @@ class TypeSelector extends SimpleSelector { TypeSelector addSuffix(String suffix) => TypeSelector( QualifiedName(name.name + suffix, namespace: name.namespace)); - List unify(List compound) { + List? unify(List compound) { if (compound.first is UniversalSelector || compound.first is TypeSelector) { var unified = unifyUniversalAndElement(this, compound.first); if (unified == null) return null; diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index 1e07783f9..fcba37594 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -14,7 +14,7 @@ class UniversalSelector extends SimpleSelector { /// it's the empty string, this matches all elements that aren't in any /// namespace. If it's `*`, this matches all elements in any namespace. /// Otherwise, it matches all elements in the given namespace. - final String namespace; + final String? namespace; int get minSpecificity => 0; @@ -23,7 +23,7 @@ class UniversalSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitUniversalSelector(this); - List unify(List compound) { + List? unify(List compound) { if (compound.first is UniversalSelector || compound.first is TypeSelector) { var unified = unifyUniversalAndElement(this, compound.first); if (unified == null) return null; diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index b1996b646..2e90fa50b 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -15,7 +15,6 @@ import 'importer.dart'; import 'importer/node.dart'; import 'io.dart'; import 'logger.dart'; -import 'sync_package_resolver.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/async_evaluate.dart'; @@ -26,25 +25,25 @@ import 'visitor/serialize.dart'; /// /// At most one of `importCache` and `nodeImporter` may be provided at once. Future compileAsync(String path, - {Syntax syntax, - Logger logger, - AsyncImportCache importCache, - NodeImporter nodeImporter, - Iterable functions, - OutputStyle style, + {Syntax? syntax, + Logger? logger, + AsyncImportCache? importCache, + NodeImporter? nodeImporter, + Iterable? functions, + OutputStyle? style, bool useSpaces = true, - int indentWidth, - LineFeed lineFeed, + int? indentWidth, + LineFeed? lineFeed, bool sourceMap = false, bool charset = true}) async { // If the syntax is different than the importer would default to, we have to // parse the file manually and we can't store it in the cache. - Stylesheet stylesheet; + Stylesheet? stylesheet; if (nodeImporter == null && (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= AsyncImportCache.none(logger: logger); - stylesheet = await importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), p.toUri(path)); + stylesheet = (await importCache.importCanonical( + FilesystemImporter('.'), p.toUri(canonicalize(path)), p.toUri(path)))!; } else { stylesheet = Stylesheet.parse( readFile(path), syntax ?? Syntax.forPath(path), @@ -71,20 +70,19 @@ Future compileAsync(String path, /// /// At most one of `importCache` and `nodeImporter` may be provided at once. Future compileStringAsync(String source, - {Syntax syntax, - Logger logger, - AsyncImportCache importCache, - NodeImporter nodeImporter, - Iterable importers, - Iterable loadPaths, - SyncPackageResolver packageResolver, - AsyncImporter importer, - Iterable functions, - OutputStyle style, + {Syntax? syntax, + Logger? logger, + AsyncImportCache? importCache, + NodeImporter? nodeImporter, + Iterable? importers, + Iterable? loadPaths, + AsyncImporter? importer, + Iterable? functions, + OutputStyle? style, bool useSpaces = true, - int indentWidth, - LineFeed lineFeed, - Object url, + int? indentWidth, + LineFeed? lineFeed, + Object? url, bool sourceMap = false, bool charset = true}) async { var stylesheet = @@ -110,15 +108,15 @@ Future compileStringAsync(String source, /// Arguments are handled as for [compileStringAsync]. Future _compileStylesheet( Stylesheet stylesheet, - Logger logger, - AsyncImportCache importCache, - NodeImporter nodeImporter, + Logger? logger, + AsyncImportCache? importCache, + NodeImporter? nodeImporter, AsyncImporter importer, - Iterable functions, - OutputStyle style, + Iterable? functions, + OutputStyle? style, bool useSpaces, - int indentWidth, - LineFeed lineFeed, + int? indentWidth, + LineFeed? lineFeed, bool sourceMap, bool charset) async { var evaluateResult = await evaluateAsync(stylesheet, @@ -137,11 +135,12 @@ Future _compileStylesheet( sourceMap: sourceMap, charset: charset); - if (serializeResult.sourceMap != null && importCache != null) { + 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( - serializeResult.sourceMap.urls, + resultSourceMap.urls, (url) => url == '' ? Uri.dataFromString(stylesheet.span.file.getText(0), encoding: utf8) @@ -167,13 +166,13 @@ class CompileResult { /// The source map indicating how the source files map to [css]. /// /// This is `null` if source mapping was disabled for this compilation. - SingleMapping get sourceMap => _serialize.sourceMap; + SingleMapping? get sourceMap => _serialize.sourceMap; /// A map from source file URLs to the corresponding [SourceFile]s. /// /// This can be passed to [sourceMap]'s [Mapping.spanFor] method. It's `null` /// if source mapping was disabled for this compilation. - Map get sourceFiles => _serialize.sourceFiles; + Map? get sourceFiles => _serialize.sourceFiles; /// The set that will eventually populate the JS API's /// `result.stats.includedFiles` field. diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index c8d73acc7..5c1b2d33b 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -4,7 +4,6 @@ import 'dart:collection'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; @@ -15,11 +14,12 @@ import 'callable.dart'; import 'configuration.dart'; import 'configured_value.dart'; import 'exception.dart'; -import 'extend/extender.dart'; +import 'extend/extension_store.dart'; import 'module.dart'; import 'module/forwarded_view.dart'; import 'module/shadowed_view.dart'; import 'util/merged_map_view.dart'; +import 'util/nullable.dart'; import 'util/public_member_map_view.dart'; import 'utils.dart'; import 'value.dart'; @@ -48,20 +48,20 @@ class AsyncEnvironment { /// The modules forwarded by this module. /// /// This is `null` if there are no forwarded modules. - Set _forwardedModules; + Set? _forwardedModules; /// A map from modules in [_forwardedModules] to the nodes whose spans /// indicate where those modules were originally forwarded. /// /// This is `null` if there are no forwarded modules. - Map _forwardedModuleNodes; + Map? _forwardedModuleNodes; /// Modules forwarded by nested imports at each lexical scope level *beneath /// the global scope*. /// /// This is `null` until it's needed, since most environments won't ever use /// this. - List> _nestedForwardedModules; + List>? _nestedForwardedModules; /// Modules from [_modules], [_globalModules], and [_forwardedModules], in the /// order in which they were `@use`d. @@ -82,7 +82,7 @@ class AsyncEnvironment { /// 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> _variableNodes; + final List>? _variableNodes; /// A map of variable names to their indices in [_variables]. /// @@ -117,8 +117,8 @@ class AsyncEnvironment { /// The content block passed to the lexically-enclosing mixin, or `null` if /// this is not in a mixin, or if no content block was passed. - UserDefinedCallable get content => _content; - UserDefinedCallable _content; + UserDefinedCallable? get content => _content; + UserDefinedCallable? _content; /// Whether the environment is lexically at the root of the document. bool get atRoot => _variables.length == 1; @@ -137,10 +137,10 @@ class AsyncEnvironment { /// /// This is cached to speed up repeated references to the same variable, as /// well as references to the last variable's [FileSpan]. - String _lastVariableName; + String? _lastVariableName; /// The index in [_variables] of the last variable that was accessed. - int _lastVariableIndex; + int? _lastVariableIndex; /// Creates an [AsyncEnvironment]. /// @@ -233,7 +233,7 @@ class AsyncEnvironment { /// Throws a [SassScriptException] if there's already a module with the given /// [namespace], or if [namespace] is `null` and [module] defines a variable /// with the same name as a variable defined in this environment. - void addModule(Module module, AstNode nodeWithSpan, {String namespace}) { + void addModule(Module module, AstNode nodeWithSpan, {String? namespace}) { if (namespace == null) { _globalModules.add(module); _globalModuleNodes[module] = nodeWithSpan; @@ -248,10 +248,11 @@ class AsyncEnvironment { } } else { if (_modules.containsKey(namespace)) { + var span = _namespaceNodes[namespace]?.span; throw MultiSpanSassScriptException( "There's already a module with namespace \"$namespace\".", "new @use", - {_namespaceNodes[namespace].span: "original @use"}); + {if (span != null) span: "original @use"}); } _modules[namespace] = module; @@ -263,16 +264,16 @@ class AsyncEnvironment { /// Exposes the members in [module] to downstream modules as though they were /// defined in this module, according to the modifications defined by [rule]. void forwardModule(Module module, ForwardRule rule) { - _forwardedModules ??= {}; - _forwardedModuleNodes ??= {}; + var forwardedModules = (_forwardedModules ??= {}); + var forwardedModuleNodes = (_forwardedModuleNodes ??= {}); var view = ForwardedModuleView.ifNecessary(module, rule); - for (var other in _forwardedModules) { + for (var other in forwardedModules) { _assertNoConflicts( - view.variables, other.variables, view, other, "variable", rule); + view.variables, other.variables, view, other, "variable"); _assertNoConflicts( - view.functions, other.functions, view, other, "function", rule); - _assertNoConflicts(view.mixins, other.mixins, view, other, "mixin", rule); + view.functions, other.functions, view, other, "function"); + _assertNoConflicts(view.mixins, other.mixins, view, other, "mixin"); } // Add the original module to [_allModules] (rather than the @@ -280,8 +281,8 @@ class AsyncEnvironment { // `==`. This is safe because upstream modules are only used for collating // CSS, not for the members they expose. _allModules.add(module); - _forwardedModules.add(view); - _forwardedModuleNodes[view] = rule; + forwardedModules.add(view); + forwardedModuleNodes[view] = rule; } /// Throws a [SassScriptException] if [newMembers] from [newModule] has any @@ -293,8 +294,7 @@ class AsyncEnvironment { Map oldMembers, Module newModule, Module oldModule, - String type, - AstNode newModuleNodeWithSpan) { + String type) { Map smaller; Map larger; if (newMembers.length < oldMembers.length) { @@ -314,10 +314,11 @@ class AsyncEnvironment { } if (type == "variable") name = "\$$name"; + var span = _forwardedModuleNodes?[oldModule]?.span; throw MultiSpanSassScriptException( 'Two forwarded modules both define a $type named $name.', "new @forward", - {_forwardedModuleNodes[oldModule].span: "original @forward"}); + {if (span != null) span: "original @forward"}); } } @@ -332,17 +333,19 @@ class AsyncEnvironment { // Omit modules from [forwarded] that are already globally available and // forwarded in this module. - if (_forwardedModules != null) { + var forwardedModules = _forwardedModules; + if (forwardedModules != null) { forwarded = { for (var module in forwarded) - if (!_forwardedModules.contains(module) || + if (!forwardedModules.contains(module) || !_globalModules.contains(module)) module }; + } else { + forwardedModules = _forwardedModules ??= {}; } - _forwardedModules ??= {}; - _forwardedModuleNodes ??= {}; + var forwardedModuleNodes = _forwardedModuleNodes ??= {}; var forwardedVariableNames = forwarded.expand((module) => module.variables.keys).toSet(); @@ -364,34 +367,38 @@ class AsyncEnvironment { if (!shadowed.isEmpty) { _globalModules.add(shadowed); - _globalModuleNodes[shadowed] = _globalModuleNodes.remove(module); + _globalModuleNodes[shadowed] = _globalModuleNodes.remove(module)!; } } } - for (var module in _forwardedModules.toList()) { + + for (var module in forwardedModules.toList()) { var shadowed = ShadowedModuleView.ifNecessary(module, variables: forwardedVariableNames, mixins: forwardedMixinNames, functions: forwardedFunctionNames); if (shadowed != null) { - _forwardedModules.remove(module); + forwardedModules.remove(module); if (!shadowed.isEmpty) { - _forwardedModules.add(shadowed); - _forwardedModuleNodes[shadowed] = - _forwardedModuleNodes.remove(module); + forwardedModules.add(shadowed); + forwardedModuleNodes[shadowed] = + forwardedModuleNodes.remove(module)!; } } } _globalModules.addAll(forwarded); - _globalModuleNodes.addAll(module._environment._forwardedModuleNodes); - _forwardedModules.addAll(forwarded); - _forwardedModuleNodes.addAll(module._environment._forwardedModuleNodes); + _globalModuleNodes + .addAll(module._environment._forwardedModuleNodes ?? const {}); + forwardedModules.addAll(forwarded); + forwardedModuleNodes + .addAll(module._environment._forwardedModuleNodes ?? const {}); } else { - _nestedForwardedModules ??= - List.generate(_variables.length - 1, (_) => []); - _nestedForwardedModules.last.addAll(forwarded); + (_nestedForwardedModules ??= + List.generate(_variables.length - 1, (_) => [])) + .last + .addAll(forwarded); } // Remove existing member definitions that are now shadowed by the @@ -399,7 +406,7 @@ class AsyncEnvironment { for (var variable in forwardedVariableNames) { _variableIndices.remove(variable); _variables.last.remove(variable); - if (_variableNodes != null) _variableNodes.last.remove(variable); + _variableNodes?.last.remove(variable); } for (var function in forwardedFunctionNames) { _functionIndices.remove(function); @@ -417,11 +424,11 @@ class AsyncEnvironment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose variables named [name]. - Value getVariable(String name, {String namespace}) { + Value? getVariable(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).variables[name]; if (_lastVariableName == name) { - return _variables[_lastVariableIndex][name] ?? + return _variables[_lastVariableIndex!][name] ?? _getVariableFromGlobalModule(name); } @@ -449,7 +456,7 @@ class AsyncEnvironment { /// Returns the value of the variable named [name] from a namespaceless /// module, or `null` if no such variable is declared in any namespaceless /// module. - Value _getVariableFromGlobalModule(String name) => + Value? _getVariableFromGlobalModule(String name) => _fromOneModule(name, "variable", (module) => module.variables[name]); /// Returns the node for the variable named [name], or `null` if no such @@ -460,11 +467,18 @@ class AsyncEnvironment { /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't /// required, since some nodes need to do real work to manufacture a source /// span. - AstNode getVariableNode(String name, {String namespace}) { - if (namespace != null) return _getModule(namespace).variableNodes[name]; + AstNode? getVariableNode(String name, {String? namespace}) { + var variableNodes = _variableNodes; + if (variableNodes == null) { + throw StateError( + "getVariableNodes() should only be called if sourceMap = true was " + "passed in."); + } + + if (namespace != null) return _getModule(namespace).variableNodes![name]; if (_lastVariableName == name) { - return _variableNodes[_lastVariableIndex][name] ?? + return variableNodes[_lastVariableIndex!][name] ?? _getVariableNodeFromGlobalModule(name); } @@ -472,7 +486,7 @@ class AsyncEnvironment { if (index != null) { _lastVariableName = name; _lastVariableIndex = index; - return _variableNodes[index][name] ?? + return variableNodes[index][name] ?? _getVariableNodeFromGlobalModule(name); } @@ -482,8 +496,7 @@ class AsyncEnvironment { _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; - return _variableNodes[index][name] ?? - _getVariableNodeFromGlobalModule(name); + return variableNodes[index][name] ?? _getVariableNodeFromGlobalModule(name); } /// Returns the node for the variable named [name] from a namespaceless @@ -494,11 +507,11 @@ class AsyncEnvironment { /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't /// required, since some nodes need to do real work to manufacture a source /// span. - AstNode _getVariableNodeFromGlobalModule(String name) { + AstNode? _getVariableNodeFromGlobalModule(String name) { // 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 _globalModules) { - var value = module.variableNodes[name]; + var value = module.variableNodes![name]; if (value != null) return value; } return null; @@ -511,7 +524,7 @@ class AsyncEnvironment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - bool globalVariableExists(String name, {String namespace}) { + bool globalVariableExists(String name, {String? namespace}) { if (namespace != null) { return _getModule(namespace).variables.containsKey(name); } @@ -521,7 +534,7 @@ class AsyncEnvironment { /// Returns the index of the last map in [_variables] that has a [name] key, /// or `null` if none exists. - int _variableIndex(String name) { + int? _variableIndex(String name) { for (var i = _variables.length - 1; i >= 0; i--) { if (_variables[i].containsKey(name)) return i; } @@ -546,8 +559,8 @@ class AsyncEnvironment { /// defined with the given namespace, if no variable with the given [name] is /// defined in module with the given namespace, or if no [namespace] is passed /// and multiple global modules define variables named [name]. - void setVariable(String name, Value value, AstNode nodeWithSpan, - {String namespace, bool global = false}) { + void setVariable(String name, Value value, AstNode? nodeWithSpan, + {String? namespace, bool global = false}) { if (namespace != null) { _getModule(namespace).setVariable(name, value, nodeWithSpan); return; @@ -574,14 +587,15 @@ class AsyncEnvironment { } _variables.first[name] = value; - if (_variableNodes != null) _variableNodes.first[name] = nodeWithSpan; + if (nodeWithSpan != null) _variableNodes?.first[name] = nodeWithSpan; return; } - if (_nestedForwardedModules != null && + var nestedForwardedModules = _nestedForwardedModules; + if (nestedForwardedModules != null && !_variableIndices.containsKey(name) && _variableIndex(name) == null) { - for (var modules in _nestedForwardedModules.reversed) { + for (var modules in nestedForwardedModules.reversed) { for (var module in modules.reversed) { if (module.variables.containsKey(name)) { module.setVariable(name, value, nodeWithSpan); @@ -592,7 +606,7 @@ class AsyncEnvironment { } var index = _lastVariableName == name - ? _lastVariableIndex + ? _lastVariableIndex! : _variableIndices.putIfAbsent( name, () => _variableIndex(name) ?? _variables.length - 1); if (!_inSemiGlobalScope && index == 0) { @@ -603,7 +617,7 @@ class AsyncEnvironment { _lastVariableName = name; _lastVariableIndex = index; _variables[index][name] = value; - if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; + _variableNodes?[index][name] = nodeWithSpan!; } /// Sets the variable named [name] to [value], associated with @@ -615,13 +629,15 @@ class AsyncEnvironment { /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. - void setLocalVariable(String name, Value value, AstNode nodeWithSpan) { + void setLocalVariable(String name, Value value, AstNode? nodeWithSpan) { var index = _variables.length - 1; _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; _variables[index][name] = value; - if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; + if (nodeWithSpan != null) { + _variableNodes?[index][name] = nodeWithSpan; + } } /// Returns the value of the function named [name], optionally with the given @@ -629,7 +645,7 @@ class AsyncEnvironment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - AsyncCallable getFunction(String name, {String namespace}) { + AsyncCallable? getFunction(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).functions[name]; var index = _functionIndices[name]; @@ -647,12 +663,12 @@ class AsyncEnvironment { /// Returns the value of the function named [name] from a namespaceless /// module, or `null` if no such function is declared in any namespaceless /// module. - AsyncCallable _getFunctionFromGlobalModule(String name) => + AsyncCallable? _getFunctionFromGlobalModule(String name) => _fromOneModule(name, "function", (module) => module.functions[name]); /// Returns the index of the last map in [_functions] that has a [name] key, /// or `null` if none exists. - int _functionIndex(String name) { + int? _functionIndex(String name) { for (var i = _functions.length - 1; i >= 0; i--) { if (_functions[i].containsKey(name)) return i; } @@ -663,7 +679,7 @@ class AsyncEnvironment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - bool functionExists(String name, {String namespace}) => + bool functionExists(String name, {String? namespace}) => getFunction(name, namespace: namespace) != null; /// Sets the variable named [name] to [value] in the current scope. @@ -678,7 +694,7 @@ class AsyncEnvironment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose mixins named [name]. - AsyncCallable getMixin(String name, {String namespace}) { + AsyncCallable? getMixin(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).mixins[name]; var index = _mixinIndices[name]; @@ -696,12 +712,12 @@ class AsyncEnvironment { /// Returns the value of the mixin named [name] from a namespaceless /// module, or `null` if no such mixin is declared in any namespaceless /// module. - AsyncCallable _getMixinFromGlobalModule(String name) => + AsyncCallable? _getMixinFromGlobalModule(String name) => _fromOneModule(name, "mixin", (module) => module.mixins[name]); /// Returns the index of the last map in [_mixins] that has a [name] key, or /// `null` if none exists. - int _mixinIndex(String name) { + int? _mixinIndex(String name) { for (var i = _mixins.length - 1; i >= 0; i--) { if (_mixins[i].containsKey(name)) return i; } @@ -712,7 +728,7 @@ class AsyncEnvironment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - bool mixinExists(String name, {String namespace}) => + bool mixinExists(String name, {String? namespace}) => getMixin(name, namespace: namespace) != null; /// Sets the variable named [name] to [value] in the current scope. @@ -723,7 +739,7 @@ class AsyncEnvironment { } /// Sets [content] as [this.content] for the duration of [callback]. - Future withContent(UserDefinedCallable content, + Future withContent(UserDefinedCallable? content, Future callback()) async { var oldContent = _content; _content = content; @@ -802,12 +818,12 @@ class AsyncEnvironment { var configuration = {}; for (var i = 0; i < _variables.length; i++) { var values = _variables[i]; - var nodes = - _variableNodes == null ? {} : _variableNodes[i]; - for (var name in values.keys) { + var nodes = _variableNodes?[i] ?? {}; + for (var entry in values.entries) { // Implicit configurations are never invalid, making [configurationSpan] // unnecessary, so we pass null here to avoid having to compute it. - configuration[name] = ConfiguredValue(values[name], null, nodes[name]); + configuration[entry.key] = + ConfiguredValue.implicit(entry.value, nodes[entry.key]); } } return Configuration.implicit(configuration); @@ -815,15 +831,15 @@ class AsyncEnvironment { /// Returns a module that represents the top-level members defined in [this], /// that contains [css] as its CSS tree, which can be extended using - /// [extender]. - Module toModule(CssStylesheet css, Extender extender) { + /// [extensionStore]. + Module toModule(CssStylesheet css, ExtensionStore extensionStore) { assert(atRoot); - return _EnvironmentModule(this, css, extender, + return _EnvironmentModule(this, css, extensionStore, forwarded: _forwardedModules); } /// Returns a module with the same members and upstream modules as [this], but - /// an empty stylesheet and extender. + /// an empty stylesheet and extension store. /// /// 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 @@ -833,7 +849,7 @@ class AsyncEnvironment { this, CssStylesheet(const [], SourceFile.decoded(const [], url: "").span(0)), - Extender.empty, + ExtensionStore.empty, forwarded: _forwardedModules); } @@ -858,9 +874,10 @@ 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)) { - if (_nestedForwardedModules != null) { - for (var modules in _nestedForwardedModules.reversed) { + T? _fromOneModule(String name, String type, T? callback(Module module)) { + var nestedForwardedModules = _nestedForwardedModules; + if (nestedForwardedModules != null) { + for (var modules in nestedForwardedModules.reversed) { for (var module in modules.reversed) { var value = callback(module); if (value != null) return value; @@ -868,23 +885,26 @@ class AsyncEnvironment { } } - T value; - Object identity; + T? value; + Object? identity; for (var module in _globalModules) { var valueInModule = callback(module); if (valueInModule == null) continue; - var identityFromModule = valueInModule is AsyncCallable + Object? identityFromModule = valueInModule is AsyncCallable ? valueInModule : module.variableIdentity(name); if (identityFromModule == identity) continue; if (value != null) { + var spans = _globalModuleNodes.entries.map( + (entry) => callback(entry.key).andThen((_) => entry.value.span)); + throw MultiSpanSassScriptException( 'This $type is available from multiple global modules.', '$type use', { - for (var entry in _globalModuleNodes.entries) - if (callback(entry.key) != null) entry.value.span: 'includes $type' + for (var span in spans) + if (span != null) span: 'includes $type' }); } @@ -897,14 +917,14 @@ class AsyncEnvironment { /// A module that represents the top-level members defined in an [Environment]. class _EnvironmentModule implements Module { - Uri get url => css.span.sourceUrl; + Uri? get url => css.span.sourceUrl; final List upstream; final Map variables; - final Map variableNodes; + final Map? variableNodes; final Map functions; final Map mixins; - final Extender extender; + final ExtensionStore extensionStore; final CssStylesheet css; final bool transitivelyContainsCss; final bool transitivelyContainsExtensions; @@ -920,21 +940,21 @@ class _EnvironmentModule implements Module { /// defined at all. final Map _modulesByVariable; - factory _EnvironmentModule( - AsyncEnvironment environment, CssStylesheet css, Extender extender, - {Set forwarded}) { + factory _EnvironmentModule(AsyncEnvironment environment, CssStylesheet css, + ExtensionStore extensionStore, + {Set? forwarded}) { forwarded ??= const {}; return _EnvironmentModule._( environment, css, - extender, + extensionStore, _makeModulesByVariable(forwarded), _memberMap(environment._variables.first, forwarded.map((module) => module.variables)), - environment._variableNodes == null - ? null - : _memberMap(environment._variableNodes.first, - forwarded.map((module) => module.variableNodes)), + environment._variableNodes.andThen((nodes) => _memberMap( + nodes.first, + // dart-lang/sdk#45348 + forwarded!.map((module) => module.variableNodes!))), _memberMap(environment._functions.first, forwarded.map((module) => module.functions)), _memberMap(environment._mixins.first, @@ -942,7 +962,7 @@ class _EnvironmentModule implements Module { transitivelyContainsCss: css.children.isNotEmpty || environment._allModules .any((module) => module.transitivelyContainsCss), - transitivelyContainsExtensions: !extender.isEmpty || + transitivelyContainsExtensions: !extensionStore.isEmpty || environment._allModules .any((module) => module.transitivelyContainsExtensions)); } @@ -987,17 +1007,17 @@ class _EnvironmentModule implements Module { _EnvironmentModule._( this._environment, this.css, - this.extender, + this.extensionStore, this._modulesByVariable, this.variables, this.variableNodes, this.functions, this.mixins, - {@required this.transitivelyContainsCss, - @required this.transitivelyContainsExtensions}) + {required this.transitivelyContainsCss, + required this.transitivelyContainsExtensions}) : upstream = _environment._allModules; - void setVariable(String name, Value value, AstNode nodeWithSpan) { + void setVariable(String name, Value value, AstNode? nodeWithSpan) { var module = _modulesByVariable[name]; if (module != null) { module.setVariable(name, value, nodeWithSpan); @@ -1009,8 +1029,8 @@ class _EnvironmentModule implements Module { } _environment._variables.first[name] = value; - if (_environment._variableNodes != null) { - _environment._variableNodes.first[name] = nodeWithSpan; + if (nodeWithSpan != null) { + _environment._variableNodes?.first[name] = nodeWithSpan; } return; } @@ -1024,11 +1044,11 @@ class _EnvironmentModule implements Module { Module cloneCss() { if (css.children.isEmpty) return this; - var newCssAndExtender = cloneCssStylesheet(css, extender); + var newCssAndExtensionStore = cloneCssStylesheet(css, extensionStore); return _EnvironmentModule._( _environment, - newCssAndExtender.item1, - newCssAndExtender.item2, + newCssAndExtensionStore.item1, + newCssAndExtensionStore.item2, _modulesByVariable, variables, variableNodes, diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 58e5a1b6d..2d25a0e2b 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; +import 'package:package_config/package_config_types.dart'; import 'package:path/path.dart' as p; import 'package:tuple/tuple.dart'; @@ -11,8 +12,7 @@ import 'importer.dart'; import 'importer/utils.dart'; import 'io.dart'; import 'logger.dart'; -import 'sync_package_resolver.dart'; -import 'utils.dart'; // ignore: unused_import +import 'utils.dart'; /// An in-memory cache of parsed stylesheets that have been imported by Sass. class AsyncImportCache { @@ -31,11 +31,11 @@ class AsyncImportCache { /// /// This cache isn't used for relative imports, because they're /// context-dependent. - final Map, Tuple3> + final Map, Tuple3?> _canonicalizeCache; /// The parsed stylesheets for each canonicalized import URL. - final Map _importCache; + final Map _importCache; /// The import results for each canonicalized import URL. final Map _resultsCache; @@ -52,33 +52,34 @@ class AsyncImportCache { /// * Each load path specified in the `SASS_PATH` environment variable, which /// should be semicolon-separated on Windows and colon-separated elsewhere. /// - /// * `package:` resolution using [packageResolver], which is a - /// [`SyncPackageResolver`][] from the `package_resolver` package. Note that + /// * `package:` resolution using [packageConfig], which is a + /// [`PackageConfig`][] from the `package_config` package. Note that /// this is a shorthand for adding a [PackageImporter] to [importers]. /// - /// [`SyncPackageResolver`]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html - AsyncImportCache(Iterable importers, - {Iterable loadPaths, - SyncPackageResolver packageResolver, - Logger logger}) - : _importers = _toImporters(importers, loadPaths, packageResolver), + /// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html + AsyncImportCache( + {Iterable? importers, + Iterable? loadPaths, + PackageConfig? packageConfig, + Logger? logger}) + : _importers = _toImporters(importers, loadPaths, packageConfig), _logger = logger ?? const Logger.stderr(), _canonicalizeCache = {}, _importCache = {}, _resultsCache = {}; /// Creates an import cache without any globally-available importers. - AsyncImportCache.none({Logger logger}) + AsyncImportCache.none({Logger? logger}) : _importers = const [], _logger = logger ?? const Logger.stderr(), _canonicalizeCache = {}, _importCache = {}, _resultsCache = {}; - /// Converts the user's [importers], [loadPaths], and [packageResolver] + /// Converts the user's [importers], [loadPaths], and [packageConfig] /// options into a single list of importers. - static List _toImporters(Iterable importers, - Iterable loadPaths, SyncPackageResolver packageResolver) { + static List _toImporters(Iterable? importers, + Iterable? loadPaths, PackageConfig? packageConfig) { var sassPath = getEnvironmentVariable('SASS_PATH'); return [ ...?importers, @@ -87,7 +88,7 @@ class AsyncImportCache { if (sassPath != null) for (var path in sassPath.split(isWindows ? ';' : ':')) FilesystemImporter(path), - if (packageResolver != null) PackageImporter(packageResolver) + if (packageConfig != null) PackageImporter(packageConfig) ]; } @@ -103,10 +104,12 @@ 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, - {AsyncImporter baseImporter, Uri baseUrl, bool forImport = false}) async { + Future?> canonicalize(Uri url, + {AsyncImporter? baseImporter, + Uri? baseUrl, + bool forImport = false}) async { if (baseImporter != null) { - var resolvedUrl = baseUrl != null ? baseUrl.resolveUri(url) : url; + var resolvedUrl = baseUrl?.resolveUri(url) ?? url; var canonicalUrl = await _canonicalize(baseImporter, resolvedUrl, forImport); if (canonicalUrl != null) { @@ -129,7 +132,7 @@ class AsyncImportCache { /// Calls [importer.canonicalize] and prints a deprecation warning if it /// returns a relative URL. - Future _canonicalize( + Future _canonicalize( AsyncImporter importer, Uri url, bool forImport) async { var result = await (forImport ? inImportRule(() => importer.canonicalize(url)) @@ -152,8 +155,10 @@ 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, - {AsyncImporter baseImporter, Uri baseUrl, bool forImport = false}) async { + Future?> 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; @@ -173,8 +178,8 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// importers may return for legacy reasons. /// /// Caches the result of the import and uses cached results if possible. - Future importCanonical(AsyncImporter importer, Uri canonicalUrl, - [Uri originalUrl]) async { + Future importCanonical(AsyncImporter importer, Uri canonicalUrl, + [Uri? originalUrl]) async { return await putIfAbsentAsync(_importCache, canonicalUrl, () async { var result = await importer.load(canonicalUrl); if (result == null) return null; @@ -195,9 +200,10 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// 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( + var url = minBy( _canonicalizeCache.values - .where((tuple) => tuple?.item2 == canonicalUrl) + .whereNotNull() + .where((tuple) => tuple.item2 == canonicalUrl) .map((tuple) => tuple.item3), (url) => url.path.length); if (url == null) return canonicalUrl; diff --git a/lib/src/callable/async.dart b/lib/src/callable/async.dart index 18ab8b69d..f4413bf81 100644 --- a/lib/src/callable/async.dart +++ b/lib/src/callable/async.dart @@ -40,6 +40,6 @@ abstract class AsyncCallable { AsyncBuiltInCallable.function(name, arguments, (arguments) { var result = callback(arguments); if (result is ext.Value) return result as Value; - return (result as Future).then((value) => value as Value); + return result.then((value) => value as Value); }); } diff --git a/lib/src/callable/async_built_in.dart b/lib/src/callable/async_built_in.dart index 5d0ed1ccf..6751c0889 100644 --- a/lib/src/callable/async_built_in.dart +++ b/lib/src/callable/async_built_in.dart @@ -37,7 +37,7 @@ class AsyncBuiltInCallable implements AsyncCallable { /// If passed, [url] is the URL of the module in which the function is /// defined. AsyncBuiltInCallable.function(String name, String arguments, - FutureOr callback(List arguments), {Object url}) + FutureOr callback(List arguments), {Object? url}) : this.parsed( name, ArgumentDeclaration.parse('@function $name($arguments) {', @@ -54,12 +54,16 @@ class AsyncBuiltInCallable implements AsyncCallable { /// defined. AsyncBuiltInCallable.mixin(String name, String arguments, FutureOr callback(List arguments), - {Object url}) + {Object? url}) : this.parsed(name, ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url), (arguments) async { await callback(arguments); - return null; + // We could encode the fact that functions return values and mixins + // don't in the type system, but that would get very messy very + // quickly so it's easier to just return Sass's `null` for mixins and + // simply ignore it at the call site. + return sassNull; }); /// Creates a callable with a single [arguments] declaration and a single diff --git a/lib/src/callable/built_in.dart b/lib/src/callable/built_in.dart index 58a9c5818..1b4a2aeb9 100644 --- a/lib/src/callable/built_in.dart +++ b/lib/src/callable/built_in.dart @@ -32,7 +32,7 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// defined. BuiltInCallable.function( String name, String arguments, Value callback(List arguments), - {Object url}) + {Object? url}) : this.parsed( name, ArgumentDeclaration.parse('@function $name($arguments) {', @@ -49,12 +49,12 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// defined. BuiltInCallable.mixin( String name, String arguments, void callback(List arguments), - {Object url}) + {Object? url}) : this.parsed(name, ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url), (arguments) { callback(arguments); - return null; + return sassNull; }); /// Creates a callable with a single [arguments] declaration and a single @@ -74,7 +74,7 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// defined. BuiltInCallable.overloadedFunction( this.name, Map overloads, - {Object url}) + {Object? url}) : _overloads = [ for (var entry in overloads.entries) Tuple2( @@ -93,8 +93,8 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { /// [ArgumentDeclaration]. Tuple2 callbackFor( int positional, Set names) { - Tuple2 fuzzyMatch; - int minMismatchDistance; + Tuple2? fuzzyMatch; + int? minMismatchDistance; for (var overload in _overloads) { // Ideally, find an exact match. @@ -114,7 +114,8 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable { fuzzyMatch = overload; } - return fuzzyMatch; + if (fuzzyMatch != null) return fuzzyMatch; + throw StateError("BuiltInCallable $name may not have empty overloads."); } /// Returns a copy of this callable with the given [name]. diff --git a/lib/src/compile.dart b/lib/src/compile.dart index d2b495657..ed47a446c 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: b2cd6037efa37e300daa45ebed20cb4b61526161 +// Checksum: dcb7cfbedf1e1189808c0056debf6a68bd387dab // // ignore_for_file: unused_import @@ -25,7 +25,6 @@ import 'importer.dart'; import 'importer/node.dart'; import 'io.dart'; import 'logger.dart'; -import 'sync_package_resolver.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/evaluate.dart'; @@ -36,25 +35,25 @@ import 'visitor/serialize.dart'; /// /// At most one of `importCache` and `nodeImporter` may be provided at once. CompileResult compile(String path, - {Syntax syntax, - Logger logger, - ImportCache importCache, - NodeImporter nodeImporter, - Iterable functions, - OutputStyle style, + {Syntax? syntax, + Logger? logger, + ImportCache? importCache, + NodeImporter? nodeImporter, + Iterable? functions, + OutputStyle? style, bool useSpaces = true, - int indentWidth, - LineFeed lineFeed, + int? indentWidth, + LineFeed? lineFeed, bool sourceMap = false, bool charset = true}) { // If the syntax is different than the importer would default to, we have to // parse the file manually and we can't store it in the cache. - Stylesheet stylesheet; + Stylesheet? stylesheet; if (nodeImporter == null && (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= ImportCache.none(logger: logger); stylesheet = importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), p.toUri(path)); + FilesystemImporter('.'), p.toUri(canonicalize(path)), p.toUri(path))!; } else { stylesheet = Stylesheet.parse( readFile(path), syntax ?? Syntax.forPath(path), @@ -81,20 +80,19 @@ CompileResult compile(String path, /// /// At most one of `importCache` and `nodeImporter` may be provided at once. CompileResult compileString(String source, - {Syntax syntax, - Logger logger, - ImportCache importCache, - NodeImporter nodeImporter, - Iterable importers, - Iterable loadPaths, - SyncPackageResolver packageResolver, - Importer importer, - Iterable functions, - OutputStyle style, + {Syntax? syntax, + Logger? logger, + ImportCache? importCache, + NodeImporter? nodeImporter, + Iterable? importers, + Iterable? loadPaths, + Importer? importer, + Iterable? functions, + OutputStyle? style, bool useSpaces = true, - int indentWidth, - LineFeed lineFeed, - Object url, + int? indentWidth, + LineFeed? lineFeed, + Object? url, bool sourceMap = false, bool charset = true}) { var stylesheet = @@ -120,15 +118,15 @@ CompileResult compileString(String source, /// Arguments are handled as for [compileString]. CompileResult _compileStylesheet( Stylesheet stylesheet, - Logger logger, - ImportCache importCache, - NodeImporter nodeImporter, + Logger? logger, + ImportCache? importCache, + NodeImporter? nodeImporter, Importer importer, - Iterable functions, - OutputStyle style, + Iterable? functions, + OutputStyle? style, bool useSpaces, - int indentWidth, - LineFeed lineFeed, + int? indentWidth, + LineFeed? lineFeed, bool sourceMap, bool charset) { var evaluateResult = evaluate(stylesheet, @@ -147,11 +145,12 @@ CompileResult _compileStylesheet( sourceMap: sourceMap, charset: charset); - if (serializeResult.sourceMap != null && importCache != null) { + 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( - serializeResult.sourceMap.urls, + resultSourceMap.urls, (url) => url == '' ? Uri.dataFromString(stylesheet.span.file.getText(0), encoding: utf8) diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 2f4b29398..1522781ad 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -12,6 +12,12 @@ import 'util/unprefixed_map_view.dart'; /// A set of variables meant to configure a module by overriding its /// `!default` declarations. +/// +/// A configuration may be either *implicit*, meaning that it's either empty or +/// created by importing a file containing a `@forward` rule; or *explicit*, +/// 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 { /// A map from variable names (without `$`) to values. /// @@ -20,52 +26,22 @@ class Configuration { Map get values => UnmodifiableMapView(_values); final Map _values; - /// The node whose span indicates where the configuration was declared. - /// - /// This is `null` for implicit configurations. - final AstNode nodeWithSpan; - - /// Whether or not this configuration is implicit. - /// - /// Implicit configurations are created when a file containing a `@forward` - /// rule is imported, while explicit configurations are created by the - /// `with` clause of a `@use` rule. - /// - /// Both types of configuration pass through `@forward` rules, but explicit - /// 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. - final bool isImplicit; - - /// Creates an explicit configuration with the given [values]. - Configuration(Map values, this.nodeWithSpan) - : _values = values, - isImplicit = false; - /// Creates an implicit configuration with the given [values]. - /// - /// See [isImplicit] for details. - Configuration.implicit(Map values) - : _values = values, - nodeWithSpan = null, - isImplicit = true; + Configuration.implicit(this._values); /// The empty configuration, which indicates that the module has not been /// configured. /// /// Empty configurations are always considered implicit, since they are /// ignored if the module has already been loaded. - const Configuration.empty() - : _values = const {}, - nodeWithSpan = null, - isImplicit = true; + const Configuration.empty() : _values = const {}; bool get isEmpty => values.isEmpty; /// Removes a variable with [name] from this configuration, returning it. /// /// If no such variable exists in this configuration, returns null. - ConfiguredValue remove(String name) => isEmpty ? null : _values.remove(name); + ConfiguredValue? remove(String name) => isEmpty ? null : _values.remove(name); /// Creates a new configuration from this one based on a `@forward` rule. Configuration throughForward(ForwardRule forward) { @@ -76,16 +52,46 @@ 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. - if (forward.prefix != null) { - newValues = UnprefixedMapView(newValues, forward.prefix); - } - if (forward.shownVariables != null) { - newValues = LimitedMapView.safelist(newValues, forward.shownVariables); - } else if (forward.hiddenVariables?.isNotEmpty ?? false) { - newValues = LimitedMapView.blocklist(newValues, forward.hiddenVariables); + var prefix = forward.prefix; + if (prefix != null) newValues = UnprefixedMapView(newValues, prefix); + + var shownVariables = forward.shownVariables; + var hiddenVariables = forward.hiddenVariables; + if (shownVariables != null) { + newValues = LimitedMapView.safelist(newValues, shownVariables); + } else if (hiddenVariables != null && hiddenVariables.isNotEmpty) { + newValues = LimitedMapView.blocklist(newValues, hiddenVariables); } - return isImplicit - ? Configuration.implicit(newValues) - : Configuration(newValues, nodeWithSpan); + return _withValues(newValues); } + + /// Returns a copy of [this] with the given [values] map. + Configuration _withValues(Map values) => + Configuration.implicit(values); + + String toString() => + "(" + + values.entries + .map((entry) => "\$${entry.key}: ${entry.value}") + .join(", ") + + ")"; +} + +/// A [Configuratoin] that was created with an explicit `with` clause of a +/// `@use` rule. +/// +/// Both types of configuration pass through `@forward` rules, but explicit +/// 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 { + /// The node whose span indicates where the configuration was declared. + final AstNode nodeWithSpan; + + ExplicitConfiguration(Map values, this.nodeWithSpan) + : super.implicit(values); + + /// Returns a copy of [this] with the given [values] map. + Configuration _withValues(Map values) => + ExplicitConfiguration(values, nodeWithSpan); } diff --git a/lib/src/configured_value.dart b/lib/src/configured_value.dart index beeb76193..01d46b3c4 100644 --- a/lib/src/configured_value.dart +++ b/lib/src/configured_value.dart @@ -12,14 +12,22 @@ class ConfiguredValue { /// The value of the variable. final Value value; - /// The span where the variable's configuration was written. - final FileSpan configurationSpan; + /// The span where the variable's configuration was written, or `null` if this + /// value was configured implicitly. + final FileSpan? configurationSpan; /// The [AstNode] where the variable's value originated. /// - /// This is used to generate source maps and can be `null` if source map - /// generation is disabled. - final AstNode assignmentNode; + /// This is used to generate source maps. + final AstNode? assignmentNode; - ConfiguredValue(this.value, this.configurationSpan, [this.assignmentNode]); + /// Creates a variable value that's been configured explicitly with a `with` + /// clause. + ConfiguredValue.explicit( + this.value, this.configurationSpan, this.assignmentNode); + + /// Creates a variable value that's implicitly configured by setting a + /// variable prior to an `@import` of a file that contains a `@forward`. + ConfiguredValue.implicit(this.value, this.assignmentNode) + : configurationSpan = null; } diff --git a/lib/src/environment.dart b/lib/src/environment.dart index f9d6eaa9e..da4910951 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,13 +5,12 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 9f4ee98a1c9e90d8d5277e0c2b0355460cda8788 +// Checksum: bb0b47fc04e32f36a0f87dc73bdfe3f89dc51aa4 // // ignore_for_file: unused_import import 'dart:collection'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; @@ -22,11 +21,12 @@ import 'callable.dart'; import 'configuration.dart'; import 'configured_value.dart'; import 'exception.dart'; -import 'extend/extender.dart'; +import 'extend/extension_store.dart'; import 'module.dart'; import 'module/forwarded_view.dart'; import 'module/shadowed_view.dart'; import 'util/merged_map_view.dart'; +import 'util/nullable.dart'; import 'util/public_member_map_view.dart'; import 'utils.dart'; import 'value.dart'; @@ -55,20 +55,20 @@ class Environment { /// The modules forwarded by this module. /// /// This is `null` if there are no forwarded modules. - Set> _forwardedModules; + Set>? _forwardedModules; /// A map from modules in [_forwardedModules] to the nodes whose spans /// indicate where those modules were originally forwarded. /// /// This is `null` if there are no forwarded modules. - Map, AstNode> _forwardedModuleNodes; + Map, AstNode>? _forwardedModuleNodes; /// Modules forwarded by nested imports at each lexical scope level *beneath /// the global scope*. /// /// This is `null` until it's needed, since most environments won't ever use /// this. - List>> _nestedForwardedModules; + List>>? _nestedForwardedModules; /// Modules from [_modules], [_globalModules], and [_forwardedModules], in the /// order in which they were `@use`d. @@ -89,7 +89,7 @@ class Environment { /// 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> _variableNodes; + final List>? _variableNodes; /// A map of variable names to their indices in [_variables]. /// @@ -124,8 +124,8 @@ class Environment { /// The content block passed to the lexically-enclosing mixin, or `null` if /// this is not in a mixin, or if no content block was passed. - UserDefinedCallable get content => _content; - UserDefinedCallable _content; + UserDefinedCallable? get content => _content; + UserDefinedCallable? _content; /// Whether the environment is lexically at the root of the document. bool get atRoot => _variables.length == 1; @@ -144,10 +144,10 @@ class Environment { /// /// This is cached to speed up repeated references to the same variable, as /// well as references to the last variable's [FileSpan]. - String _lastVariableName; + String? _lastVariableName; /// The index in [_variables] of the last variable that was accessed. - int _lastVariableIndex; + int? _lastVariableIndex; /// Creates an [Environment]. /// @@ -241,7 +241,7 @@ class Environment { /// [namespace], or if [namespace] is `null` and [module] defines a variable /// with the same name as a variable defined in this environment. void addModule(Module module, AstNode nodeWithSpan, - {String namespace}) { + {String? namespace}) { if (namespace == null) { _globalModules.add(module); _globalModuleNodes[module] = nodeWithSpan; @@ -256,10 +256,11 @@ class Environment { } } else { if (_modules.containsKey(namespace)) { + var span = _namespaceNodes[namespace]?.span; throw MultiSpanSassScriptException( "There's already a module with namespace \"$namespace\".", "new @use", - {_namespaceNodes[namespace].span: "original @use"}); + {if (span != null) span: "original @use"}); } _modules[namespace] = module; @@ -271,16 +272,16 @@ class Environment { /// Exposes the members in [module] to downstream modules as though they were /// defined in this module, according to the modifications defined by [rule]. void forwardModule(Module module, ForwardRule rule) { - _forwardedModules ??= {}; - _forwardedModuleNodes ??= {}; + var forwardedModules = (_forwardedModules ??= {}); + var forwardedModuleNodes = (_forwardedModuleNodes ??= {}); var view = ForwardedModuleView.ifNecessary(module, rule); - for (var other in _forwardedModules) { + for (var other in forwardedModules) { _assertNoConflicts( - view.variables, other.variables, view, other, "variable", rule); + view.variables, other.variables, view, other, "variable"); _assertNoConflicts( - view.functions, other.functions, view, other, "function", rule); - _assertNoConflicts(view.mixins, other.mixins, view, other, "mixin", rule); + view.functions, other.functions, view, other, "function"); + _assertNoConflicts(view.mixins, other.mixins, view, other, "mixin"); } // Add the original module to [_allModules] (rather than the @@ -288,8 +289,8 @@ class Environment { // `==`. This is safe because upstream modules are only used for collating // CSS, not for the members they expose. _allModules.add(module); - _forwardedModules.add(view); - _forwardedModuleNodes[view] = rule; + forwardedModules.add(view); + forwardedModuleNodes[view] = rule; } /// Throws a [SassScriptException] if [newMembers] from [newModule] has any @@ -301,8 +302,7 @@ class Environment { Map oldMembers, Module newModule, Module oldModule, - String type, - AstNode newModuleNodeWithSpan) { + String type) { Map smaller; Map larger; if (newMembers.length < oldMembers.length) { @@ -322,10 +322,11 @@ class Environment { } if (type == "variable") name = "\$$name"; + var span = _forwardedModuleNodes?[oldModule]?.span; throw MultiSpanSassScriptException( 'Two forwarded modules both define a $type named $name.', "new @forward", - {_forwardedModuleNodes[oldModule].span: "original @forward"}); + {if (span != null) span: "original @forward"}); } } @@ -340,17 +341,19 @@ class Environment { // Omit modules from [forwarded] that are already globally available and // forwarded in this module. - if (_forwardedModules != null) { + var forwardedModules = _forwardedModules; + if (forwardedModules != null) { forwarded = { for (var module in forwarded) - if (!_forwardedModules.contains(module) || + if (!forwardedModules.contains(module) || !_globalModules.contains(module)) module }; + } else { + forwardedModules = _forwardedModules ??= {}; } - _forwardedModules ??= {}; - _forwardedModuleNodes ??= {}; + var forwardedModuleNodes = _forwardedModuleNodes ??= {}; var forwardedVariableNames = forwarded.expand((module) => module.variables.keys).toSet(); @@ -372,34 +375,38 @@ class Environment { if (!shadowed.isEmpty) { _globalModules.add(shadowed); - _globalModuleNodes[shadowed] = _globalModuleNodes.remove(module); + _globalModuleNodes[shadowed] = _globalModuleNodes.remove(module)!; } } } - for (var module in _forwardedModules.toList()) { + + for (var module in forwardedModules.toList()) { var shadowed = ShadowedModuleView.ifNecessary(module, variables: forwardedVariableNames, mixins: forwardedMixinNames, functions: forwardedFunctionNames); if (shadowed != null) { - _forwardedModules.remove(module); + forwardedModules.remove(module); if (!shadowed.isEmpty) { - _forwardedModules.add(shadowed); - _forwardedModuleNodes[shadowed] = - _forwardedModuleNodes.remove(module); + forwardedModules.add(shadowed); + forwardedModuleNodes[shadowed] = + forwardedModuleNodes.remove(module)!; } } } _globalModules.addAll(forwarded); - _globalModuleNodes.addAll(module._environment._forwardedModuleNodes); - _forwardedModules.addAll(forwarded); - _forwardedModuleNodes.addAll(module._environment._forwardedModuleNodes); + _globalModuleNodes + .addAll(module._environment._forwardedModuleNodes ?? const {}); + forwardedModules.addAll(forwarded); + forwardedModuleNodes + .addAll(module._environment._forwardedModuleNodes ?? const {}); } else { - _nestedForwardedModules ??= - List.generate(_variables.length - 1, (_) => []); - _nestedForwardedModules.last.addAll(forwarded); + (_nestedForwardedModules ??= + List.generate(_variables.length - 1, (_) => [])) + .last + .addAll(forwarded); } // Remove existing member definitions that are now shadowed by the @@ -407,7 +414,7 @@ class Environment { for (var variable in forwardedVariableNames) { _variableIndices.remove(variable); _variables.last.remove(variable); - if (_variableNodes != null) _variableNodes.last.remove(variable); + _variableNodes?.last.remove(variable); } for (var function in forwardedFunctionNames) { _functionIndices.remove(function); @@ -425,11 +432,11 @@ class Environment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose variables named [name]. - Value getVariable(String name, {String namespace}) { + Value? getVariable(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).variables[name]; if (_lastVariableName == name) { - return _variables[_lastVariableIndex][name] ?? + return _variables[_lastVariableIndex!][name] ?? _getVariableFromGlobalModule(name); } @@ -457,7 +464,7 @@ class Environment { /// Returns the value of the variable named [name] from a namespaceless /// module, or `null` if no such variable is declared in any namespaceless /// module. - Value _getVariableFromGlobalModule(String name) => + Value? _getVariableFromGlobalModule(String name) => _fromOneModule(name, "variable", (module) => module.variables[name]); /// Returns the node for the variable named [name], or `null` if no such @@ -468,11 +475,18 @@ class Environment { /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't /// required, since some nodes need to do real work to manufacture a source /// span. - AstNode getVariableNode(String name, {String namespace}) { - if (namespace != null) return _getModule(namespace).variableNodes[name]; + AstNode? getVariableNode(String name, {String? namespace}) { + var variableNodes = _variableNodes; + if (variableNodes == null) { + throw StateError( + "getVariableNodes() should only be called if sourceMap = true was " + "passed in."); + } + + if (namespace != null) return _getModule(namespace).variableNodes![name]; if (_lastVariableName == name) { - return _variableNodes[_lastVariableIndex][name] ?? + return variableNodes[_lastVariableIndex!][name] ?? _getVariableNodeFromGlobalModule(name); } @@ -480,7 +494,7 @@ class Environment { if (index != null) { _lastVariableName = name; _lastVariableIndex = index; - return _variableNodes[index][name] ?? + return variableNodes[index][name] ?? _getVariableNodeFromGlobalModule(name); } @@ -490,8 +504,7 @@ class Environment { _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; - return _variableNodes[index][name] ?? - _getVariableNodeFromGlobalModule(name); + return variableNodes[index][name] ?? _getVariableNodeFromGlobalModule(name); } /// Returns the node for the variable named [name] from a namespaceless @@ -502,11 +515,11 @@ class Environment { /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't /// required, since some nodes need to do real work to manufacture a source /// span. - AstNode _getVariableNodeFromGlobalModule(String name) { + AstNode? _getVariableNodeFromGlobalModule(String name) { // 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 _globalModules) { - var value = module.variableNodes[name]; + var value = module.variableNodes![name]; if (value != null) return value; } return null; @@ -519,7 +532,7 @@ class Environment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - bool globalVariableExists(String name, {String namespace}) { + bool globalVariableExists(String name, {String? namespace}) { if (namespace != null) { return _getModule(namespace).variables.containsKey(name); } @@ -529,7 +542,7 @@ class Environment { /// Returns the index of the last map in [_variables] that has a [name] key, /// or `null` if none exists. - int _variableIndex(String name) { + int? _variableIndex(String name) { for (var i = _variables.length - 1; i >= 0; i--) { if (_variables[i].containsKey(name)) return i; } @@ -554,8 +567,8 @@ class Environment { /// defined with the given namespace, if no variable with the given [name] is /// defined in module with the given namespace, or if no [namespace] is passed /// and multiple global modules define variables named [name]. - void setVariable(String name, Value value, AstNode nodeWithSpan, - {String namespace, bool global = false}) { + void setVariable(String name, Value value, AstNode? nodeWithSpan, + {String? namespace, bool global = false}) { if (namespace != null) { _getModule(namespace).setVariable(name, value, nodeWithSpan); return; @@ -582,14 +595,15 @@ class Environment { } _variables.first[name] = value; - if (_variableNodes != null) _variableNodes.first[name] = nodeWithSpan; + if (nodeWithSpan != null) _variableNodes?.first[name] = nodeWithSpan; return; } - if (_nestedForwardedModules != null && + var nestedForwardedModules = _nestedForwardedModules; + if (nestedForwardedModules != null && !_variableIndices.containsKey(name) && _variableIndex(name) == null) { - for (var modules in _nestedForwardedModules.reversed) { + for (var modules in nestedForwardedModules.reversed) { for (var module in modules.reversed) { if (module.variables.containsKey(name)) { module.setVariable(name, value, nodeWithSpan); @@ -600,7 +614,7 @@ class Environment { } var index = _lastVariableName == name - ? _lastVariableIndex + ? _lastVariableIndex! : _variableIndices.putIfAbsent( name, () => _variableIndex(name) ?? _variables.length - 1); if (!_inSemiGlobalScope && index == 0) { @@ -611,7 +625,7 @@ class Environment { _lastVariableName = name; _lastVariableIndex = index; _variables[index][name] = value; - if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; + _variableNodes?[index][name] = nodeWithSpan!; } /// Sets the variable named [name] to [value], associated with @@ -623,13 +637,15 @@ class Environment { /// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. - void setLocalVariable(String name, Value value, AstNode nodeWithSpan) { + void setLocalVariable(String name, Value value, AstNode? nodeWithSpan) { var index = _variables.length - 1; _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; _variables[index][name] = value; - if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; + if (nodeWithSpan != null) { + _variableNodes?[index][name] = nodeWithSpan; + } } /// Returns the value of the function named [name], optionally with the given @@ -637,7 +653,7 @@ class Environment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - Callable getFunction(String name, {String namespace}) { + Callable? getFunction(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).functions[name]; var index = _functionIndices[name]; @@ -655,12 +671,12 @@ class Environment { /// Returns the value of the function named [name] from a namespaceless /// module, or `null` if no such function is declared in any namespaceless /// module. - Callable _getFunctionFromGlobalModule(String name) => + Callable? _getFunctionFromGlobalModule(String name) => _fromOneModule(name, "function", (module) => module.functions[name]); /// Returns the index of the last map in [_functions] that has a [name] key, /// or `null` if none exists. - int _functionIndex(String name) { + int? _functionIndex(String name) { for (var i = _functions.length - 1; i >= 0; i--) { if (_functions[i].containsKey(name)) return i; } @@ -671,7 +687,7 @@ class Environment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - bool functionExists(String name, {String namespace}) => + bool functionExists(String name, {String? namespace}) => getFunction(name, namespace: namespace) != null; /// Sets the variable named [name] to [value] in the current scope. @@ -686,7 +702,7 @@ class Environment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose mixins named [name]. - Callable getMixin(String name, {String namespace}) { + Callable? getMixin(String name, {String? namespace}) { if (namespace != null) return _getModule(namespace).mixins[name]; var index = _mixinIndices[name]; @@ -704,12 +720,12 @@ class Environment { /// Returns the value of the mixin named [name] from a namespaceless /// module, or `null` if no such mixin is declared in any namespaceless /// module. - Callable _getMixinFromGlobalModule(String name) => + Callable? _getMixinFromGlobalModule(String name) => _fromOneModule(name, "mixin", (module) => module.mixins[name]); /// Returns the index of the last map in [_mixins] that has a [name] key, or /// `null` if none exists. - int _mixinIndex(String name) { + int? _mixinIndex(String name) { for (var i = _mixins.length - 1; i >= 0; i--) { if (_mixins[i].containsKey(name)) return i; } @@ -720,7 +736,7 @@ class Environment { /// /// Throws a [SassScriptException] if there is no module named [namespace], or /// if multiple global modules expose functions named [name]. - bool mixinExists(String name, {String namespace}) => + bool mixinExists(String name, {String? namespace}) => getMixin(name, namespace: namespace) != null; /// Sets the variable named [name] to [value] in the current scope. @@ -731,7 +747,7 @@ class Environment { } /// Sets [content] as [this.content] for the duration of [callback]. - void withContent(UserDefinedCallable content, void callback()) { + void withContent(UserDefinedCallable? content, void callback()) { var oldContent = _content; _content = content; callback(); @@ -808,12 +824,12 @@ class Environment { var configuration = {}; for (var i = 0; i < _variables.length; i++) { var values = _variables[i]; - var nodes = - _variableNodes == null ? {} : _variableNodes[i]; - for (var name in values.keys) { + var nodes = _variableNodes?[i] ?? {}; + for (var entry in values.entries) { // Implicit configurations are never invalid, making [configurationSpan] // unnecessary, so we pass null here to avoid having to compute it. - configuration[name] = ConfiguredValue(values[name], null, nodes[name]); + configuration[entry.key] = + ConfiguredValue.implicit(entry.value, nodes[entry.key]); } } return Configuration.implicit(configuration); @@ -821,15 +837,15 @@ class Environment { /// Returns a module that represents the top-level members defined in [this], /// that contains [css] as its CSS tree, which can be extended using - /// [extender]. - Module toModule(CssStylesheet css, Extender extender) { + /// [extensionStore]. + Module toModule(CssStylesheet css, ExtensionStore extensionStore) { assert(atRoot); - return _EnvironmentModule(this, css, extender, + return _EnvironmentModule(this, css, extensionStore, forwarded: _forwardedModules); } /// Returns a module with the same members and upstream modules as [this], but - /// an empty stylesheet and extender. + /// an empty stylesheet and extension store. /// /// 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 @@ -839,7 +855,7 @@ class Environment { this, CssStylesheet(const [], SourceFile.decoded(const [], url: "").span(0)), - Extender.empty, + ExtensionStore.empty, forwarded: _forwardedModules); } @@ -864,10 +880,11 @@ class Environment { /// /// 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)) { - if (_nestedForwardedModules != null) { - for (var modules in _nestedForwardedModules.reversed) { + T? _fromOneModule( + String name, String type, T? callback(Module module)) { + var nestedForwardedModules = _nestedForwardedModules; + if (nestedForwardedModules != null) { + for (var modules in nestedForwardedModules.reversed) { for (var module in modules.reversed) { var value = callback(module); if (value != null) return value; @@ -875,23 +892,26 @@ class Environment { } } - T value; - Object identity; + T? value; + Object? identity; for (var module in _globalModules) { var valueInModule = callback(module); if (valueInModule == null) continue; - var identityFromModule = valueInModule is Callable + Object? identityFromModule = valueInModule is Callable ? valueInModule : module.variableIdentity(name); if (identityFromModule == identity) continue; if (value != null) { + var spans = _globalModuleNodes.entries.map( + (entry) => callback(entry.key).andThen((_) => entry.value.span)); + throw MultiSpanSassScriptException( 'This $type is available from multiple global modules.', '$type use', { - for (var entry in _globalModuleNodes.entries) - if (callback(entry.key) != null) entry.value.span: 'includes $type' + for (var span in spans) + if (span != null) span: 'includes $type' }); } @@ -904,14 +924,14 @@ class Environment { /// A module that represents the top-level members defined in an [Environment]. class _EnvironmentModule implements Module { - Uri get url => css.span.sourceUrl; + Uri? get url => css.span.sourceUrl; final List> upstream; final Map variables; - final Map variableNodes; + final Map? variableNodes; final Map functions; final Map mixins; - final Extender extender; + final ExtensionStore extensionStore; final CssStylesheet css; final bool transitivelyContainsCss; final bool transitivelyContainsExtensions; @@ -928,20 +948,20 @@ class _EnvironmentModule implements Module { final Map> _modulesByVariable; factory _EnvironmentModule( - Environment environment, CssStylesheet css, Extender extender, - {Set> forwarded}) { + Environment environment, CssStylesheet css, ExtensionStore extensionStore, + {Set>? forwarded}) { forwarded ??= const {}; return _EnvironmentModule._( environment, css, - extender, + extensionStore, _makeModulesByVariable(forwarded), _memberMap(environment._variables.first, forwarded.map((module) => module.variables)), - environment._variableNodes == null - ? null - : _memberMap(environment._variableNodes.first, - forwarded.map((module) => module.variableNodes)), + environment._variableNodes.andThen((nodes) => _memberMap( + nodes.first, + // dart-lang/sdk#45348 + forwarded!.map((module) => module.variableNodes!))), _memberMap(environment._functions.first, forwarded.map((module) => module.functions)), _memberMap(environment._mixins.first, @@ -949,7 +969,7 @@ class _EnvironmentModule implements Module { transitivelyContainsCss: css.children.isNotEmpty || environment._allModules .any((module) => module.transitivelyContainsCss), - transitivelyContainsExtensions: !extender.isEmpty || + transitivelyContainsExtensions: !extensionStore.isEmpty || environment._allModules .any((module) => module.transitivelyContainsExtensions)); } @@ -995,17 +1015,17 @@ class _EnvironmentModule implements Module { _EnvironmentModule._( this._environment, this.css, - this.extender, + this.extensionStore, this._modulesByVariable, this.variables, this.variableNodes, this.functions, this.mixins, - {@required this.transitivelyContainsCss, - @required this.transitivelyContainsExtensions}) + {required this.transitivelyContainsCss, + required this.transitivelyContainsExtensions}) : upstream = _environment._allModules; - void setVariable(String name, Value value, AstNode nodeWithSpan) { + void setVariable(String name, Value value, AstNode? nodeWithSpan) { var module = _modulesByVariable[name]; if (module != null) { module.setVariable(name, value, nodeWithSpan); @@ -1017,8 +1037,8 @@ class _EnvironmentModule implements Module { } _environment._variables.first[name] = value; - if (_environment._variableNodes != null) { - _environment._variableNodes.first[name] = nodeWithSpan; + if (nodeWithSpan != null) { + _environment._variableNodes?.first[name] = nodeWithSpan; } return; } @@ -1032,11 +1052,11 @@ class _EnvironmentModule implements Module { Module cloneCss() { if (css.children.isEmpty) return this; - var newCssAndExtender = cloneCssStylesheet(css, extender); + var newCssAndExtensionStore = cloneCssStylesheet(css, extensionStore); return _EnvironmentModule._( _environment, - newCssAndExtender.item1, - newCssAndExtender.item2, + newCssAndExtensionStore.item1, + newCssAndExtensionStore.item2, _modulesByVariable, variables, variableNodes, diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 72d61ee82..1811e0d96 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -7,6 +7,7 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:term_glyph/term_glyph.dart' as term_glyph; +import 'util/nullable.dart'; import 'utils.dart'; import 'value.dart'; @@ -21,7 +22,7 @@ class SassException extends SourceSpanException { SassException(String message, FileSpan span) : super(message, span); - String toString({Object color}) { + String toString({Object? color}) { var buffer = StringBuffer() ..writeln("Error: $message") ..write(span.highlight(color: color)); @@ -88,9 +89,9 @@ class MultiSpanSassException extends SassException : secondarySpans = Map.unmodifiable(secondarySpans), super(message, span); - String toString({Object color, String secondaryColor}) { + String toString({Object? color, String? secondaryColor}) { var useColor = false; - String primaryColor; + String? primaryColor; if (color is String) { useColor = true; primaryColor = color; @@ -98,12 +99,14 @@ class MultiSpanSassException extends SassException useColor = true; } - var buffer = StringBuffer() - ..writeln("Error: $message") - ..write(span.highlightMultiple(primaryLabel, secondarySpans, - color: useColor, - primaryColor: primaryColor, - secondaryColor: secondaryColor)); + var buffer = StringBuffer("Error: $message\n"); + + span + .highlightMultiple(primaryLabel, secondarySpans, + color: useColor, + primaryColor: primaryColor, + secondaryColor: secondaryColor) + .andThen(buffer.write); for (var frame in trace.toString().split("\n")) { if (frame.isEmpty) continue; diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 6e799118f..1714390c2 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -30,7 +30,7 @@ import 'options.dart'; /// or that of a file it imports is more recent than [destination]'s /// modification time. Note that these modification times are cached by [graph]. Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, - String source, String destination, + String? source, String? destination, {bool ifModified = false}) async { var importer = FilesystemImporter('.'); if (ifModified) { @@ -58,7 +58,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, CompileResult result; try { if (options.asynchronous) { - var importCache = AsyncImportCache([], + var importCache = AsyncImportCache( loadPaths: options.loadPaths, logger: options.logger); result = source == null @@ -121,7 +121,8 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, if (options.color) buffer.write('\u001b[32m'); var sourceName = source == null ? 'stdin' : p.prettyUri(p.toUri(source)); - var destinationName = p.prettyUri(p.toUri(destination)); + // `destination` is guaranteed to be non-null in update and watch mode. + var destinationName = p.prettyUri(p.toUri(destination!)); buffer.write('Compiled $sourceName to $destinationName.'); if (options.color) buffer.write('\u001b[0m'); print(buffer); @@ -136,7 +137,7 @@ Future compileStylesheet(ExecutableOptions options, StylesheetGraph graph, /// /// Returns the source map comment to add to the end of the CSS file. String _writeSourceMap( - ExecutableOptions options, SingleMapping sourceMap, String destination) { + ExecutableOptions options, SingleMapping? sourceMap, String? destination) { if (sourceMap == null) return ""; if (destination != null) { @@ -155,7 +156,9 @@ String _writeSourceMap( url = Uri.dataFromString(sourceMapText, mimeType: 'application/json', encoding: utf8); } else { - var sourceMapPath = destination + '.map'; + // [destination] can't be null here because --embed-source-map is + // incompatible with writing to stdout. + var sourceMapPath = destination! + '.map'; ensureDir(p.dirname(sourceMapPath)); writeFile(sourceMapPath, sourceMapText); diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 5fb54eb4d..985b4e0e2 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -7,9 +7,9 @@ import 'dart:collection'; import 'package:args/args.dart'; import 'package:charcode/charcode.dart'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:term_glyph/term_glyph.dart' as term_glyph; +import 'package:tuple/tuple.dart'; import '../../sass.dart'; import '../io.dart'; @@ -122,8 +122,7 @@ class ExecutableOptions { static String get usage => _parser.usage; /// Shorthand for throwing a [UsageException] with the given [message]. - @alwaysThrows - static void _fail(String message) => throw UsageException(message); + static Never _fail(String message) => throw UsageException(message); /// The parsed options passed by the user to the executable. final ArgResults _options; @@ -132,10 +131,8 @@ class ExecutableOptions { bool get version => _options['version'] as bool; /// Whether to run an interactive shell. - bool get interactive { - if (_interactive != null) return _interactive; - _interactive = _options['interactive'] as bool; - if (!_interactive) return false; + late final bool interactive = () { + if (!(_options['interactive'] as bool)) return false; var invalidOptions = [ 'stdin', 'indented', 'style', 'source-map', 'source-map-urls', // @@ -147,15 +144,13 @@ class ExecutableOptions { } } return true; - } - - bool _interactive; + }(); /// Whether to parse the source file with the indented syntax. /// /// This may be `null`, indicating that this should be determined by each /// stylesheet's extension. - bool get indented => _ifParsed('indented') as bool; + bool? get indented => _ifParsed('indented') as bool?; /// Whether to use ANSI terminal colors. bool get color => _options.wasParsed('color') @@ -206,7 +201,7 @@ class ExecutableOptions { /// Whether to emit error messages as CSS stylesheets bool get emitErrorCss => - _options['error-css'] as bool ?? + _options['error-css'] as bool? ?? sourcesToDestinations.values.any((destination) => destination != null); /// A map from source paths to the destination paths where the compiled CSS @@ -217,23 +212,23 @@ class ExecutableOptions { /// A `null` source indicates that a stylesheet should be read from standard /// input. A `null` destination indicates that a stylesheet should be written /// to standard output. - Map get sourcesToDestinations { + Map get sourcesToDestinations { _ensureSources(); - return _sourcesToDestinations; + return _sourcesToDestinations!; } - Map _sourcesToDestinations; + Map? _sourcesToDestinations; /// A map from source directories to the destination directories where the /// compiled CSS for stylesheets in the source directories should be written. /// /// Considers keys to be the same if they represent the same path on disk. - Map get sourceDirectoriesToDestinations { + Map get sourceDirectoriesToDestinations { _ensureSources(); return _sourceDirectoriesToDestinations; } - Map _sourceDirectoriesToDestinations; + late final Map _sourceDirectoriesToDestinations; /// Ensure that both [sourcesToDestinations] and [sourceDirectories] have been /// computed. @@ -302,8 +297,11 @@ class ExecutableOptions { _fail("--watch is not allowed when printing to stdout."); } } - _sourcesToDestinations = - UnmodifiableMapView(p.PathMap.of({source: destination})); + + var map = + p.PathMap(); // p.PathMap.of() doesn't support null keys. + map[source] = destination; + _sourcesToDestinations = UnmodifiableMapView(map); } _sourceDirectoriesToDestinations = const {}; return; @@ -326,25 +324,9 @@ class ExecutableOptions { continue; } - String source; - String destination; - for (var i = 0; i < argument.length; i++) { - // A colon at position 1 may be a Windows drive letter and not a - // separator. - if (i == 1 && _isWindowsPath(argument, i - 1)) continue; - - if (argument.codeUnitAt(i) == $colon) { - if (source == null) { - source = argument.substring(0, i); - destination = argument.substring(i + 1); - } else if (i != source.length + 2 || - !_isWindowsPath(argument, i - 1)) { - // A colon 2 characters after the separator may also be a Windows - // drive letter. - _fail('"$argument" may only contain one ":".'); - } - } - } + var sourceAndDestination = _splitSourceAndDestination(argument); + var source = sourceAndDestination.item1; + var destination = sourceAndDestination.item2; if (!seen.add(source)) _fail('Duplicate source "$source".'); @@ -362,6 +344,30 @@ class ExecutableOptions { UnmodifiableMapView(sourceDirectoriesToDestinations); } + /// Splits an argument that contains a colon and returns its source and its + /// destination component. + Tuple2 _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. + if (i == 1 && _isWindowsPath(argument, i - 1)) continue; + + if (argument.codeUnitAt(i) == $colon) { + var nextColon = argument.indexOf(':', i + 1); + // A colon 2 characters after the separator may also be a Windows + // drive letter. + if (nextColon == i + 2 && _isWindowsPath(argument, i + 1)) { + nextColon = argument.indexOf(':', nextColon + 1); + } + if (nextColon != -1) _fail('"$argument" may only contain one ":".'); + + return Tuple2(argument.substring(0, i), argument.substring(i + 1)); + } + } + + throw ArgumentError('Expected "$argument" to contain a colon.'); + } + /// Returns whether [string] contains an absolute Windows path at [index]. bool _isWindowsPath(String string, int index) => string.length > index + 2 && @@ -456,18 +462,20 @@ class ExecutableOptions { /// [destination]) according to the `source-map-urls` option. /// /// If [url] isn't a `file:` URL, returns it as-is. - Uri sourceMapUrl(Uri url, String destination) { + Uri sourceMapUrl(Uri url, String? destination) { if (url.scheme.isNotEmpty && url.scheme != 'file') return url; var path = p.fromUri(url); return p.toUri(_options['source-map-urls'] == 'relative' && !_writeToStdout - ? p.relative(path, from: p.dirname(destination)) + // [destination] can't be null here because --source-map-urls=relative + // is incompatible with writing to stdout. + ? p.relative(path, from: p.dirname(destination!)) : p.absolute(path)); } /// Returns the value of [name] in [options] if it was explicitly provided by /// the user, and `null` otherwise. - Object _ifParsed(String name) => + Object? _ifParsed(String name) => _options.wasParsed(name) ? _options[name] : null; } diff --git a/lib/src/executable/repl.dart b/lib/src/executable/repl.dart index cbf6097d3..eb3e4fca4 100644 --- a/lib/src/executable/repl.dart +++ b/lib/src/executable/repl.dart @@ -22,8 +22,7 @@ Future repl(ExecutableOptions options) async { var logger = TrackingLogger(options.logger); var evaluator = Evaluator( importer: FilesystemImporter('.'), - importCache: - ImportCache(const [], loadPaths: options.loadPaths, logger: logger), + importCache: ImportCache(loadPaths: options.loadPaths, logger: logger), logger: logger); await for (String line in repl.runAsync()) { if (line.trim().isEmpty) continue; diff --git a/lib/src/executable/watch.dart b/lib/src/executable/watch.dart index 3d06ae14f..36e74fa1c 100644 --- a/lib/src/executable/watch.dart +++ b/lib/src/executable/watch.dart @@ -20,8 +20,8 @@ import 'options.dart'; /// Watches all the files in [graph] for changes and updates them as necessary. Future watch(ExecutableOptions options, StylesheetGraph graph) async { var directoriesToWatch = [ - ...options.sourceDirectoriesToDestinations.keys, - ...options.sourcesToDestinations.keys.map(p.dirname), + ..._sourceDirectoriesToDestinations(options).keys, + for (var dir in _sourcesToDestinations(options).keys) p.dirname(dir), ...options.loadPaths ]; @@ -39,12 +39,12 @@ 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 source in options.sourcesToDestinations.keys) { - var destination = options.sourcesToDestinations[source]; - graph.addCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(source)), p.toUri(source), + for (var entry in _sourcesToDestinations(options).entries) { + graph.addCanonical(FilesystemImporter('.'), + p.toUri(canonicalize(entry.key)), p.toUri(entry.key), recanonicalize: false); - var success = await watcher.compile(source, destination, ifModified: true); + var success = + await watcher.compile(entry.key, entry.value, ifModified: true); if (!success && options.stopOnError) { dirWatcher.events.listen(null).cancel(); return; @@ -82,7 +82,11 @@ class _Watcher { exitCode = 65; return false; } on FileSystemException catch (error, stackTrace) { - _printError("Error reading ${p.relative(error.path)}: ${error.message}.", + var path = error.path; + _printError( + path == null + ? error.message + : "Error reading ${p.relative(path)}: ${error.message}.", stackTrace); exitCode = 66; return false; @@ -150,12 +154,13 @@ class _Watcher { /// Returns whether all necessary recompilations succeeded. Future _handleModify(String path) async { var url = _canonicalize(path); - if (!_graph.nodes.containsKey(url)) return _handleAdd(path); - // 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. + // 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]); } @@ -208,8 +213,11 @@ class _Watcher { } } - return typeForPath.keys - .map((path) => WatchEvent(typeForPath[path], path)); + return [ + for (var entry in typeForPath.entries) + // PathMap always has nullable keys + WatchEvent(entry.value, entry.key!) + ]; }); } @@ -253,17 +261,16 @@ class _Watcher { /// the CSS file it should be compiled to. /// /// Otherwise, returns `null`. - String _destinationFor(String source) { - var destination = _options.sourcesToDestinations[source]; + String? _destinationFor(String source) { + var destination = _sourcesToDestinations(_options)[source]; if (destination != null) return destination; if (p.basename(source).startsWith('_')) return null; - for (var sourceDir in _options.sourceDirectoriesToDestinations.keys) { - if (!p.isWithin(sourceDir, source)) continue; + for (var entry in _sourceDirectoriesToDestinations(_options).entries) { + if (!p.isWithin(entry.key, source)) continue; - var destination = p.join( - _options.sourceDirectoriesToDestinations[sourceDir], - p.setExtension(p.relative(source, from: sourceDir), '.css')); + var destination = p.join(entry.value, + p.setExtension(p.relative(source, from: entry.key), '.css')); // Don't compile ".css" files to their own locations. if (!p.equals(destination, source)) return destination; @@ -272,3 +279,14 @@ class _Watcher { return null; } } + +/// Exposes [options.sourcesToDestinations] as a non-nullable map, since stdin +/// inputs and stdout outputs aren't allowed in `--watch` mode. +Map _sourcesToDestinations(ExecutableOptions options) => + options.sourcesToDestinations.cast(); + +/// Exposes [options.sourcesDirectoriesToDestinations] as a non-nullable map, +/// since stdin inputs and stdout outputs aren't allowed in `--watch` mode. +Map _sourceDirectoriesToDestinations( + ExecutableOptions options) => + options.sourceDirectoriesToDestinations.cast(); diff --git a/lib/src/extend/empty_extender.dart b/lib/src/extend/empty_extension_store.dart similarity index 65% rename from lib/src/extend/empty_extender.dart rename to lib/src/extend/empty_extension_store.dart index 013ed0efa..4bb9e7a29 100644 --- a/lib/src/extend/empty_extender.dart +++ b/lib/src/extend/empty_extension_store.dart @@ -10,15 +10,15 @@ import '../ast/css.dart'; import '../ast/css/modifiable.dart'; import '../ast/selector.dart'; import '../ast/sass.dart'; -import 'extender.dart'; +import 'extension_store.dart'; import 'extension.dart'; -class EmptyExtender implements Extender { +class EmptyExtensionStore implements ExtensionStore { bool get isEmpty => true; Set get simpleSelectors => const UnmodifiableSetView.empty(); - const EmptyExtender(); + const EmptyExtensionStore(); Iterable extensionsWhereTarget( bool callback(SimpleSelector target)) => @@ -26,24 +26,24 @@ class EmptyExtender implements Extender { ModifiableCssValue addSelector( SelectorList selector, FileSpan span, - [List mediaContext]) { + [List? mediaContext]) { throw UnsupportedError( - "addSelector() can't be called for a const Extender."); + "addSelector() can't be called for a const ExtensionStore."); } void addExtension( CssValue extender, SimpleSelector target, ExtendRule extend, - [List mediaContext]) { + [List? mediaContext]) { throw UnsupportedError( - "addExtension() can't be called for a const Extender."); + "addExtension() can't be called for a const ExtensionStore."); } - void addExtensions(Iterable extenders) { + void addExtensions(Iterable extenders) { throw UnsupportedError( - "addExtensions() can't be called for a const Extender."); + "addExtensions() can't be called for a const ExtensionStore."); } - Tuple2, ModifiableCssValue>> - clone() => const Tuple2(EmptyExtender(), {}); + clone() => const Tuple2(EmptyExtensionStore(), {}); } diff --git a/lib/src/extend/extender.dart b/lib/src/extend/extender.dart index 32ca2ef23..5f2b48ae7 100644 --- a/lib/src/extend/extender.dart +++ b/lib/src/extend/extender.dart @@ -1,948 +1,60 @@ -// Copyright 2016 Google Inc. Use of this source code is governed by an +// Copyright 2021 Google Inc. Use of this source code is governed by an // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import '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/css/modifiable.dart'; import '../ast/selector.dart'; -import '../ast/sass.dart'; import '../exception.dart'; import '../utils.dart'; -import 'empty_extender.dart'; -import 'extension.dart'; -import 'merged_extension.dart'; -import 'functions.dart'; -import 'mode.dart'; -/// Tracks selectors and extensions, and applies the latter to the former. +/// A selector that's extending another selector, such as `A` in `A {@extend +/// B}`. class Extender { - /// An [Extender] that contains no extensions and can have no extensions added. - static const empty = EmptyExtender(); - - /// A map from all simple selectors in the stylesheet to the selector lists - /// that contain them. - /// - /// This is used to find which selectors an `@extend` applies to and adjust - /// them. - final Map>> _selectors; - - /// A map from all extended simple selectors to the sources of those - /// extensions. - final Map> _extensions; - - /// A map from all simple selectors in extenders to the extensions that those - /// extenders define. - final Map> _extensionsByExtender; - - /// A map from CSS selectors to the media query contexts they're defined in. - /// - /// This tracks the contexts in which each selector's style rule is defined. - /// If a rule is defined at the top level, it doesn't have an entry. - final Map, List> - _mediaContexts; - - /// A map from [SimpleSelector]s to the specificity of their source - /// selectors. - /// - /// This tracks the maximum specificity of the [ComplexSelector] that - /// originally contained each [SimpleSelector]. This allows us to ensure that - /// we don't trim any selectors that need to exist to satisfy the [second law - /// of extend][]. - /// - /// [second law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 - final Map _sourceSpecificity; - - /// A set of [ComplexSelector]s that were originally part of - /// their component [SelectorList]s, as opposed to being added by `@extend`. - /// - /// This allows us to ensure that we don't trim any selectors that need to - /// exist to satisfy the [first law of extend][]. - /// - /// [first law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 - final Set _originals; - - /// The mode that controls this extender's behavior. - final ExtendMode _mode; - - /// Whether this extender has no extensions. - bool get isEmpty => _extensions.isEmpty; - - /// Extends [selector] with [source] extender and [targets] extendees. - /// - /// This works as though `source {@extend target}` were written in the - /// stylesheet, with the exception that [target] can contain compound - /// selectors which must be extended as a unit. - static SelectorList extend( - SelectorList selector, SelectorList source, SelectorList targets) => - _extendOrReplace(selector, source, targets, ExtendMode.allTargets); - - /// Returns a copy of [selector] with [targets] replaced by [source]. - static SelectorList replace( - SelectorList selector, SelectorList source, SelectorList targets) => - _extendOrReplace(selector, source, targets, ExtendMode.replace); - - /// A helper function for [extend] and [replace]. - static SelectorList _extendOrReplace(SelectorList selector, - SelectorList source, SelectorList targets, ExtendMode mode) { - var extenders = { - for (var complex in source.components) complex: Extension.oneOff(complex) - }; - - var compoundTargets = [ - for (var complex in targets.components) - if (complex.components.length != 1) - throw SassScriptException("Can't extend complex selector $complex.") - else - complex.components.first as CompoundSelector - ]; - - var extensions = { - for (var compound in compoundTargets) - for (var simple in compound.components) simple: extenders - }; - - var extender = Extender._mode(mode); - if (!selector.isInvisible) { - extender._originals.addAll(selector.components); - } - selector = extender._extendList(selector, extensions, null); - - return selector; - } - - /// The set of all simple selectors in selectors handled by this extender. - /// - /// This includes simple selectors that were added because of downstream - /// extensions. - Set get simpleSelectors => MapKeySet(_selectors); - - Extender() : this._mode(ExtendMode.normal); - - Extender._mode(this._mode) - : _selectors = {}, - _extensions = {}, - _extensionsByExtender = {}, - _mediaContexts = {}, - _sourceSpecificity = Map.identity(), - _originals = Set.identity(); - - Extender._(this._selectors, this._extensions, this._extensionsByExtender, - this._mediaContexts, this._sourceSpecificity, this._originals) - : _mode = ExtendMode.normal; - - /// Returns all mandatory extensions in this extender for whose targets - /// [callback] returns `true`. - /// - /// This un-merges any [MergedExtension] so only base [Extension]s are - /// returned. - Iterable extensionsWhereTarget( - bool callback(SimpleSelector target)) sync* { - for (var target in _extensions.keys) { - if (!callback(target)) continue; - for (var extension in _extensions[target].values) { - if (extension is MergedExtension) { - yield* extension - .unmerge() - .where((extension) => !extension.isOptional); - } else if (!extension.isOptional) { - yield extension; - } - } - } - } - - /// Adds [selector] to this extender. - /// - /// Extends [selector] using any registered extensions, then returns an empty - /// [ModifiableCssValue] containing the resulting selector. If any more - /// relevant extensions are added, the returned selector is automatically - /// updated. - /// - /// The [mediaContext] is the media query context in which the selector was - /// defined, or `null` if it was defined at the top level of the document. - ModifiableCssValue addSelector( - SelectorList selector, FileSpan span, - [List mediaContext]) { - var originalSelector = selector; - if (!originalSelector.isInvisible) { - for (var complex in originalSelector.components) { - _originals.add(complex); - } - } - - if (_extensions.isNotEmpty) { - try { - selector = _extendList(originalSelector, _extensions, mediaContext); - } on SassException catch (error) { - throw SassException( - "From ${error.span.message('')}\n" - "${error.message}", - span); - } - } - - var modifiableSelector = ModifiableCssValue(selector, span); - if (mediaContext != null) _mediaContexts[modifiableSelector] = mediaContext; - _registerSelector(selector, modifiableSelector); - - return modifiableSelector; - } - - /// Registers the [SimpleSelector]s in [list] to point to [selector] in - /// [_selectors]. - void _registerSelector( - SelectorList list, ModifiableCssValue selector) { - for (var complex in list.components) { - for (var component in complex.components) { - if (component is CompoundSelector) { - for (var simple in component.components) { - _selectors.putIfAbsent(simple, () => {}).add(selector); - - if (simple is PseudoSelector && simple.selector != null) { - _registerSelector(simple.selector, selector); - } - } - } - } - } - } - - /// Adds an extension to this extender. - /// - /// The [extender] is the selector for the style rule in which the extension - /// is defined, and [target] is the selector passed to `@extend`. The [extend] - /// provides the extend span and indicates whether the extension is optional. - /// - /// The [mediaContext] defines the media query context in which the extension - /// is defined. It can only extend selectors within the same context. A `null` - /// context indicates no media queries. - void addExtension( - CssValue extender, SimpleSelector target, ExtendRule extend, - [List mediaContext]) { - var selectors = _selectors[target]; - var existingExtensions = _extensionsByExtender[target]; - - Map newExtensions; - var sources = _extensions.putIfAbsent(target, () => {}); - for (var complex in extender.value.components) { - var state = Extension( - complex, target, extender.span, extend.span, mediaContext, - optional: extend.isOptional); - - var existingState = sources[complex]; - if (existingState != null) { - // 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(existingState, state); - continue; - } - - sources[complex] = state; - - for (var component in complex.components) { - if (component is CompoundSelector) { - for (var simple in component.components) { - _extensionsByExtender.putIfAbsent(simple, () => []).add(state); - // Only source specificity for the original selector is relevant. - // Selectors generated by `@extend` don't get new specificity. - _sourceSpecificity.putIfAbsent( - simple, () => complex.maxSpecificity); - } - } - } - - if (selectors != null || existingExtensions != null) { - newExtensions ??= {}; - newExtensions[complex] = state; - } - } - - if (newExtensions == null) return; - - var newExtensionsByTarget = {target: newExtensions}; - if (existingExtensions != null) { - var additionalExtensions = - _extendExistingExtensions(existingExtensions, newExtensionsByTarget); - if (additionalExtensions != null) { - mapAddAll2(newExtensionsByTarget, additionalExtensions); - } - } - - if (selectors != null) { - _extendExistingSelectors(selectors, newExtensionsByTarget); - } - } - - /// Extend [extensions] using [newExtensions]. - /// - /// Note that this does duplicate some work done by - /// [_extendExistingSelectors], but it's necessary to expand each extension's - /// extender separately without reference to the full selector list, so that - /// relevant results don't get trimmed too early. - /// - /// Returns extensions that should be added to [newExtensions] before - /// extending selectors in order to properly handle extension loops such as: - /// - /// .c {x: y; @extend .a} - /// .x.y.a {@extend .b} - /// .z.b {@extend .c} - /// - /// Returns `null` if there are no extensions to add. - Map> - _extendExistingExtensions(List extensions, - Map> newExtensions) { - Map> additionalExtensions; - - for (var extension in extensions.toList()) { - var sources = _extensions[extension.target]; - - // [_extendExistingSelectors] would have thrown already. - List selectors; - try { - selectors = _extendComplex( - extension.extender, newExtensions, extension.mediaContext); - if (selectors == null) continue; - } on SassException catch (error) { - throw SassException( - "From ${extension.extenderSpan.message('')}\n" - "${error.message}", - error.span); - } - - var containsExtension = selectors.first == extension.extender; - var first = false; - 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; - } - - var withExtender = extension.withExtender(complex); - var existingExtension = sources[complex]; - if (existingExtension != null) { - sources[complex] = - MergedExtension.merge(existingExtension, withExtender); - } else { - sources[complex] = withExtender; - - for (var component in complex.components) { - if (component is CompoundSelector) { - for (var simple in component.components) { - _extensionsByExtender - .putIfAbsent(simple, () => []) - .add(withExtender); - } - } - } - - if (newExtensions.containsKey(extension.target)) { - additionalExtensions ??= {}; - var additionalSources = - additionalExtensions.putIfAbsent(extension.target, () => {}); - additionalSources[complex] = withExtender; - } - } - } - - // If [selectors] doesn't contain [extension.extender], for example if it - // was replaced due to :not() expansion, we must get rid of the old - // version. - if (!containsExtension) sources.remove(extension.extender); - } - - return additionalExtensions; - } - - /// Extend [extensions] using [newExtensions]. - void _extendExistingSelectors(Set> selectors, - Map> newExtensions) { - for (var selector in selectors) { - var oldValue = selector.value; - try { - selector.value = _extendList( - selector.value, newExtensions, _mediaContexts[selector]); - } on SassException catch (error) { - throw SassException( - "From ${selector.span.message('')}\n" - "${error.message}", - error.span); - } - - // If no extends actually happenedit (for example becaues unification - // failed), we don't need to re-register the selector. - if (identical(oldValue, selector.value)) continue; - _registerSelector(selector.value, selector); - } - } - - /// Extends [this] with all the extensions in [extensions]. - /// - /// These extensions will extend all selectors already in [this], but they - /// will *not* extend other extensions from [extenders]. - void addExtensions(Iterable extenders) { - // Extensions already in [this] whose extenders are extended by - // [extensions], and thus which need to be updated. - List extensionsToExtend; - - // Selectors that contain simple selectors that are extended by - // [extensions], and thus which need to be extended themselves. - Set> selectorsToExtend; - - // An extension map with the same structure as [_extensions] that only - // includes extensions from [extenders]. - Map> newExtensions; - - for (var extender in extenders) { - if (extender.isEmpty) continue; - _sourceSpecificity.addAll(extender._sourceSpecificity); - extender._extensions.forEach((target, newSources) { - // Private selectors can't be extended across module boundaries. - if (target is PlaceholderSelector && target.isPrivate) return; - - // Find existing extensions to extend. - var extensionsForTarget = _extensionsByExtender[target]; - if (extensionsForTarget != null) { - extensionsToExtend ??= []; - extensionsToExtend.addAll(extensionsForTarget); - } - - // Find existing selectors to extend. - var selectorsForTarget = _selectors[target]; - if (selectorsForTarget != null) { - selectorsToExtend ??= {}; - selectorsToExtend.addAll(selectorsForTarget); - } - - // Add [newSources] to [_extensions]. - var existingSources = _extensions[target]; - if (existingSources == null) { - _extensions[target] = extender._extensions[target]; - if (extensionsForTarget != null || selectorsForTarget != null) { - newExtensions ??= {}; - newExtensions[target] = extender._extensions[target]; - } - } else { - newSources.forEach((extender, extension) { - // If [extender] already extends [target] in [_extensions], we don't - // need to re-run the extension. - if (existingSources.containsKey(extender)) return; - existingSources[extender] = extension; - - if (extensionsForTarget != null || selectorsForTarget != null) { - newExtensions ??= {}; - newExtensions - .putIfAbsent(target, () => {}) - .putIfAbsent(extender, () => extension); - } - }); - } - }); - } - - if (newExtensions == null) return; - - if (extensionsToExtend != null) { - // We can ignore the return value here because it's only useful for extend - // loops, which can't exist across module boundaries. - _extendExistingExtensions(extensionsToExtend, newExtensions); - } - - if (selectorsToExtend != null) { - _extendExistingSelectors(selectorsToExtend, newExtensions); - } - } - - /// Extends [list] using [extensions]. - SelectorList _extendList( - SelectorList list, - Map> extensions, - List mediaQueryContext) { - // This could be written more simply using [List.map], but we want to avoid - // any allocations in the common case where no extends apply. - List extended; - for (var i = 0; i < list.components.length; i++) { - var complex = list.components[i]; - var result = _extendComplex(complex, extensions, mediaQueryContext); - if (result == null) { - if (extended != null) extended.add(complex); - } else { - extended ??= i == 0 ? [] : list.components.sublist(0, i).toList(); - extended.addAll(result); - } - } - if (extended == null) return list; - - return SelectorList(_trim(extended, _originals.contains) - .where((complex) => complex != null)); - } + /// The selector in which the `@extend` appeared. + final ComplexSelector selector; - /// Extends [complex] using [extensions], and returns the contents of a - /// [SelectorList]. - List _extendComplex( - ComplexSelector complex, - Map> extensions, - List mediaQueryContext) { - // The complex selectors that each compound selector in [complex.components] - // can expand to. - // - // For example, given - // - // .a .b {...} - // .x .y {@extend .b} - // - // this will contain - // - // [ - // [.a], - // [.b, .x .y] - // ] - // - // This could be written more simply using [List.map], but we want to avoid - // any allocations in the common case where no extends apply. - List> extendedNotExpanded; - var isOriginal = _originals.contains(complex); - for (var i = 0; i < complex.components.length; i++) { - var component = complex.components[i]; - if (component is CompoundSelector) { - var extended = _extendCompound(component, extensions, mediaQueryContext, - inOriginal: isOriginal); - if (extended == null) { - extendedNotExpanded?.add([ - ComplexSelector([component]) - ]); - } else { - extendedNotExpanded ??= complex.components - .take(i) - .map((component) => [ - ComplexSelector([component], lineBreak: complex.lineBreak) - ]) - .toList(); - extendedNotExpanded.add(extended); - } - } else { - extendedNotExpanded?.add([ - ComplexSelector([component]) - ]); - } - } - if (extendedNotExpanded == null) return null; + /// The minimum specificity required for any selector generated from this + /// extender. + final int specificity; - var first = true; - return paths(extendedNotExpanded).expand((path) { - return weave(path.map((complex) => complex.components).toList()) - .map((components) { - var outputComplex = ComplexSelector(components, - lineBreak: complex.lineBreak || - path.any((inputComplex) => inputComplex.lineBreak)); + /// Whether this extender represents a selector that was originally in the + /// document, rather than one defined with `@extend`. + final bool isOriginal; - // Make sure that copies of [complex] retain their status as "original" - // selectors. This includes selectors that are modified because a :not() - // was extended into. - if (first && _originals.contains(complex)) { - _originals.add(outputComplex); - } - first = false; + /// The media query context to which this extension is restricted, or `null` + /// if it can apply within any context. + final List? mediaContext; - return outputComplex; - }); - }).toList(); - } + /// The span in which this selector was defined. + final FileSpan span; - /// Extends [compound] using [extensions], and returns the contents of a - /// [SelectorList]. + /// Creates a new extender. /// - /// The [inOriginal] parameter indicates whether this is in an original - /// complex selector, meaning that [compound] should not be trimmed out. - List _extendCompound( - CompoundSelector compound, - Map> extensions, - List mediaQueryContext, - {bool inOriginal}) { - // If there's more than one target and they all need to match, we track - // which targets are actually extended. - var targetsUsed = _mode == ExtendMode.normal || extensions.length < 2 - ? null - : {}; - - // The complex selectors produced from each component of [compound]. - List> options; - for (var i = 0; i < compound.components.length; i++) { - var simple = compound.components[i]; - var extended = - _extendSimple(simple, extensions, mediaQueryContext, targetsUsed); - if (extended == null) { - options?.add([_extensionForSimple(simple)]); - } else { - if (options == null) { - options = []; - if (i != 0) { - options.add([_extensionForCompound(compound.components.take(i))]); - } - } - - options.addAll(extended); - } - } - if (options == null) return null; - - // If [_mode] isn't [ExtendMode.normal] and we didn't use all the targets in - // [extensions], extension fails for [compound]. - if (targetsUsed != null && targetsUsed.length != extensions.length) { - return null; - } - - // Optimize for the simple case of a single simple selector that doesn't - // need any unification. - if (options.length == 1) { - return options.first.map((state) { - state.assertCompatibleMediaContext(mediaQueryContext); - return state.extender; - }).toList(); - } - - // Find all paths through [options]. In this case, each path represents a - // different unification of the base selector. For example, if we have: - // - // .a.b {...} - // .w .x {@extend .a} - // .y .z {@extend .b} - // - // then [options] is `[[.a, .w .x], [.b, .y .z]]` and `paths(options)` is - // - // [ - // [.a, .b], - // [.a, .y .z], - // [.w .x, .b], - // [.w .x, .y .z] - // ] - // - // We then unify each path to get a list of complex selectors: - // - // [ - // [.a.b], - // [.y .a.z], - // [.w .x.b], - // [.w .y .x.z, .y .w .x.z] - // ] - var first = _mode != ExtendMode.replace; - var unifiedPaths = paths(options).map((path) { - List> complexes; - if (first) { - // The first path is always the original selector. We can't just - // return [compound] directly because pseudo selectors may be - // modified, but we don't have to do any unification. - first = false; - complexes = [ - [ - CompoundSelector(path.expand((state) { - assert(state.extender.components.length == 1); - return (state.extender.components.last as CompoundSelector) - .components; - })) - ] - ]; - } else { - var toUnify = QueueList>(); - List originals; - for (var state in path) { - if (state.isOriginal) { - originals ??= []; - originals.addAll( - (state.extender.components.last as CompoundSelector) - .components); - } else { - toUnify.add(state.extender.components); - } - } - - if (originals != null) { - toUnify.addFirst([CompoundSelector(originals)]); - } - - complexes = unifyComplex(toUnify); - if (complexes == null) return null; - } - - var lineBreak = false; - for (var state in path) { - state.assertCompatibleMediaContext(mediaQueryContext); - lineBreak = lineBreak || state.extender.lineBreak; - } + /// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`. + Extender(this.selector, this.span, + {this.mediaContext, int? specificity, bool original = false}) + : specificity = specificity ?? selector.maxSpecificity, + isOriginal = original; - return complexes - .map( - (components) => ComplexSelector(components, lineBreak: lineBreak)) - .toList(); - }); - - // If we're preserving the original selector, mark the first unification as - // such so [_trim] doesn't get rid of it. - var isOriginal = (ComplexSelector _) => false; - if (inOriginal && _mode != ExtendMode.replace) { - var original = unifiedPaths.first.first; - isOriginal = (complex) => complex == original; - } - - return _trim( - unifiedPaths - .where((complexes) => complexes != null) - .expand((l) => l) - .toList(), - isOriginal); - } - - Iterable> _extendSimple( - SimpleSelector simple, - Map> extensions, - List mediaQueryContext, - Set targetsUsed) { - // Extends [simple] without extending the contents of any selector pseudos - // it contains. - List withoutPseudo(SimpleSelector simple) { - var extenders = extensions[simple]; - if (extenders == null) return null; - targetsUsed?.add(simple); - if (_mode == ExtendMode.replace) return extenders.values.toList(); - - return [_extensionForSimple(simple), ...extenders.values]; - } - - if (simple is PseudoSelector && simple.selector != null) { - var extended = _extendPseudo(simple, extensions, mediaQueryContext); - if (extended != null) { - return extended.map( - (pseudo) => withoutPseudo(pseudo) ?? [_extensionForSimple(pseudo)]); - } - } - - var result = withoutPseudo(simple); - return result == null ? null : [result]; - } - - /// Returns a one-off [Extension] whose extender is composed solely of a - /// compound selector containing [simples]. - Extension _extensionForCompound(Iterable simples) { - var compound = CompoundSelector(simples); - return Extension.oneOff(ComplexSelector([compound]), - specificity: _sourceSpecificityFor(compound), isOriginal: true); - } - - /// Returns a one-off [Extension] whose extender is composed solely of - /// [simple]. - Extension _extensionForSimple(SimpleSelector simple) => Extension.oneOff( - ComplexSelector([ - CompoundSelector([simple]) - ]), - specificity: _sourceSpecificity[simple] ?? 0, - isOriginal: true); - - /// Extends [pseudo] using [extensions], and returns a list of resulting - /// pseudo selectors. - List _extendPseudo( - PseudoSelector pseudo, - Map> extensions, - List mediaQueryContext) { - var extended = _extendList(pseudo.selector, extensions, mediaQueryContext); - if (identical(extended, pseudo.selector)) return null; - - // For `:not()`, we usually want to get rid of any complex selectors because - // that will cause the selector to fail to parse on all browsers at time of - // writing. We can keep them if either the original selector had a complex - // selector, or the result of extending has only complex selectors, because - // either way we aren't breaking anything that isn't already broken. - Iterable complexes = extended.components; - if (pseudo.normalizedName == "not" && - !pseudo.selector.components - .any((complex) => complex.components.length > 1) && - extended.components.any((complex) => complex.components.length == 1)) { - complexes = extended.components - .where((complex) => complex.components.length <= 1); - } - - complexes = complexes.expand((complex) { - if (complex.components.length != 1) return [complex]; - if (complex.components.first is! CompoundSelector) return [complex]; - var compound = complex.components.first as CompoundSelector; - if (compound.components.length != 1) return [complex]; - if (compound.components.first is! PseudoSelector) return [complex]; - var innerPseudo = compound.components.first as PseudoSelector; - if (innerPseudo.selector == null) return [complex]; - - switch (pseudo.normalizedName) { - case 'not': - // In theory, if there's a `:not` nested within another `:not`, the - // inner `:not`'s contents should be unified with the return value. - // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should - // become `.foo:not(.bar)`. However, this is a narrow edge case and - // supporting it properly would make this code and the code calling it - // a lot more complicated, so it's not supported for now. - if (innerPseudo.normalizedName != 'matches') return []; - return innerPseudo.selector.components; - - case 'matches': - case 'any': - case 'current': - case 'nth-child': - case 'nth-last-child': - // As above, we could theoretically support :not within :matches, but - // doing so would require this method and its callers to handle much - // more complex cases that likely aren't worth the pain. - if (innerPseudo.name != pseudo.name) return []; - if (innerPseudo.argument != pseudo.argument) return []; - return innerPseudo.selector.components; - - case 'has': - case 'host': - case 'host-context': - case 'slotted': - // We can't expand nested selectors here, because each layer adds an - // additional layer of semantics. For example, `:has(:has(img))` - // doesn't match `
` but `:has(img)` does. - return [complex]; - - default: - return []; - } - }); - - // Older browsers support `:not`, but only with a single complex selector. - // In order to support those browsers, we break up the contents of a `:not` - // unless it originally contained a selector list. - if (pseudo.normalizedName == 'not' && - pseudo.selector.components.length == 1) { - var result = complexes - .map((complex) => pseudo.withSelector(SelectorList([complex]))) - .toList(); - return result.isEmpty ? null : result; - } else { - return [pseudo.withSelector(SelectorList(complexes))]; + /// Asserts that the [mediaContext] for a selector is compatible with the + /// query context for this extender. + void assertCompatibleMediaContext(List? mediaContext) { + if (this.mediaContext == null) return; + if (mediaContext != null && listEquals(this.mediaContext, mediaContext)) { + return; } - } - - // Removes elements from [selectors] if they're subselectors of other - // elements. - // - // The [isOriginal] callback indicates which selectors are original to the - // document, and thus should never be trimmed. - List _trim(List selectors, - bool isOriginal(ComplexSelector complex)) { - // Avoid truly horrific quadratic behavior. - // - // TODO(nweiz): I think there may be a way to get perfect trimming without - // going quadratic by building some sort of trie-like data structure that - // can be used to look up superselectors. - if (selectors.length > 100) return selectors; - - // This is n² on the sequences, but only comparing between separate - // sequences should limit the quadratic behavior. We iterate from last to - // first and reverse the result so that, if two selectors are identical, we - // keep the first one. - var result = QueueList(); - var numOriginals = 0; - outer: - for (var i = selectors.length - 1; i >= 0; i--) { - var complex1 = selectors[i]; - if (isOriginal(complex1)) { - // Make sure we don't include duplicate originals, which could happen if - // a style rule extends a component of its own selector. - for (var j = 0; j < numOriginals; j++) { - if (result[j] == complex1) { - rotateSlice(result, 0, j + 1); - continue outer; - } - } - - numOriginals++; - result.addFirst(complex1); - continue; - } - - // The maximum specificity of the sources that caused [complex1] to be - // generated. In order for [complex1] to be removed, there must be another - // selector that's a superselector of it *and* that has specificity - // greater or equal to this. - var maxSpecificity = 0; - for (var component in complex1.components) { - if (component is CompoundSelector) { - maxSpecificity = - math.max(maxSpecificity, _sourceSpecificityFor(component)); - } - } - - // Look in [result] rather than [selectors] for selectors after [i]. This - // ensures that we aren't comparing against a selector that's already been - // trimmed, and thus that if there are two identical selectors only one is - // trimmed. - if (result.any((complex2) => - complex2.minSpecificity >= maxSpecificity && - complex2.isSuperselector(complex1))) { - continue; - } - if (selectors.take(i).any((complex2) => - complex2.minSpecificity >= maxSpecificity && - complex2.isSuperselector(complex1))) { - continue; - } - - result.addFirst(complex1); - } - return result; - } - - /// Returns the maximum specificity for sources that went into producing - /// [compound]. - int _sourceSpecificityFor(CompoundSelector compound) { - var specificity = 0; - for (var simple in compound.components) { - specificity = math.max(specificity, _sourceSpecificity[simple] ?? 0); - } - return specificity; + throw SassException( + "You may not @extend selectors across media queries.", span); } - /// Returns a copy of [this] that extends new selectors, as well as a map from - /// the selectors extended by [this] to the selectors extended by the new - /// [Extender]. - Tuple2, ModifiableCssValue>> clone() { - var newSelectors = - >>{}; - var newMediaContexts = - , List>{}; - var oldToNewSelectors = - , ModifiableCssValue>{}; + Extender withSelector(ComplexSelector newSelector) => + Extender(newSelector, span, + mediaContext: mediaContext, + specificity: specificity, + original: isOriginal); - _selectors.forEach((simple, selectors) { - var newSelectorSet = >{}; - newSelectors[simple] = newSelectorSet; - - for (var selector in selectors) { - var newSelector = ModifiableCssValue(selector.value, selector.span); - newSelectorSet.add(newSelector); - oldToNewSelectors[selector] = newSelector; - - var mediaContext = _mediaContexts[selector]; - if (mediaContext != null) newMediaContexts[newSelector] = mediaContext; - } - }); - - return Tuple2( - Extender._( - newSelectors, - copyMapOfMap(_extensions), - copyMapOfList(_extensionsByExtender), - newMediaContexts, - Map.identity()..addAll(_sourceSpecificity), - Set.identity()..addAll(_originals)), - oldToNewSelectors); - } + String toString() => selector.toString(); } diff --git a/lib/src/extend/extension.dart b/lib/src/extend/extension.dart index 03e9e7a7c..0c3afe61a 100644 --- a/lib/src/extend/extension.dart +++ b/lib/src/extend/extension.dart @@ -14,34 +14,19 @@ import '../utils.dart'; /// The target of the extension is represented externally, in the map that /// contains this extender. class Extension { - /// The selector in which the `@extend` appeared. - final ComplexSelector extender; + /// The extender (such as `A` in `A {@extend B}`). + final Extender extender; /// The selector that's being extended. - /// - /// `null` for one-off extensions. final SimpleSelector target; - /// The minimum specificity required for any selector generated from this - /// extender. - final int specificity; + /// The media query context to which this extension is restricted, or `null` + /// if it can apply within any context. + final List? mediaContext; /// Whether this extension is optional. final bool isOptional; - /// Whether this is a one-off extender representing a selector that was - /// originally in the document, rather than one defined with `@extend`. - final bool isOriginal; - - /// The media query context to which this extend is restricted, or `null` if - /// it can apply within any context. - final List mediaContext; - - /// The span in which [extender] was defined. - /// - /// `null` for one-off extensions. - final FileSpan extenderSpan; - /// The span for an `@extend` rule that defined this extension. /// /// If any extend rule for this is extension is mandatory, this is guaranteed @@ -51,43 +36,68 @@ class Extension { /// Creates a new extension. /// /// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`. - Extension(ComplexSelector extender, this.target, this.extenderSpan, this.span, - this.mediaContext, - {int specificity, bool optional = false}) - : extender = extender, - specificity = specificity ?? extender.maxSpecificity, - isOptional = optional, - isOriginal = false; - - /// Creates a one-off extension that's not intended to be modified over time. + Extension( + ComplexSelector extender, FileSpan extenderSpan, this.target, this.span, + {this.mediaContext, bool optional = false}) + : extender = Extender(extender, extenderSpan), + isOptional = optional { + this.extender._extension = this; + } + + Extension withExtender(ComplexSelector newExtender) => + Extension(newExtender, extender.span, target, span, + mediaContext: mediaContext, optional: isOptional); + + String toString() => + "$extender {@extend $target${isOptional ? ' !optional' : ''}}"; +} + +/// A selector that's extending another selector, such as `A` in `A {@extend +/// B}`. +class Extender { + /// The selector in which the `@extend` appeared. + final ComplexSelector selector; + + /// The minimum specificity required for any selector generated from this + /// extender. + final int specificity; + + /// Whether this extender represents a selector that was originally in the + /// document, rather than one defined with `@extend`. + final bool isOriginal; + + /// The extension that created this [Extender]. + /// + /// Not all [Extender]s are created by extensions. Some simply represent the + /// original selectors that exist in the document. + Extension? _extension; + + /// The span in which this selector was defined. + final FileSpan span; + + /// Creates a new extender. /// /// If [specificity] isn't passed, it defaults to `extender.maxSpecificity`. - Extension.oneOff(ComplexSelector extender, - {int specificity, this.isOriginal = false}) - : extender = extender, - target = null, - extenderSpan = null, - specificity = specificity ?? extender.maxSpecificity, - isOptional = true, - mediaContext = null, - span = null; + Extender(this.selector, this.span, {int? specificity, bool original = false}) + : specificity = specificity ?? selector.maxSpecificity, + isOriginal = original; /// Asserts that the [mediaContext] for a selector is compatible with the /// query context for this extender. - void assertCompatibleMediaContext(List mediaContext) { - if (this.mediaContext == null) return; - if (mediaContext != null && listEquals(this.mediaContext, mediaContext)) { + void assertCompatibleMediaContext(List? mediaContext) { + var extension = _extension; + if (extension == null) return; + + var expectedMediaContext = extension.mediaContext; + if (expectedMediaContext == null) return; + if (mediaContext != null && + listEquals(expectedMediaContext, mediaContext)) { return; } throw SassException( - "You may not @extend selectors across media queries.", span); + "You may not @extend selectors across media queries.", extension.span); } - Extension withExtender(ComplexSelector newExtender) => - Extension(newExtender, target, extenderSpan, span, mediaContext, - specificity: specificity, optional: isOptional); - - String toString() => - "$extender {@extend $target${isOptional ? ' !optional' : ''}}"; + String toString() => selector.toString(); } diff --git a/lib/src/extend/extension_store.dart b/lib/src/extend/extension_store.dart new file mode 100644 index 000000000..4f705c130 --- /dev/null +++ b/lib/src/extend/extension_store.dart @@ -0,0 +1,980 @@ +// Copyright 2016 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; +import 'package:tuple/tuple.dart'; + +import '../ast/css.dart'; +import '../ast/css/modifiable.dart'; +import '../ast/selector.dart'; +import '../ast/sass.dart'; +import '../exception.dart'; +import '../utils.dart'; +import '../util/nullable.dart'; +import 'empty_extension_store.dart'; +import 'extension.dart'; +import 'merged_extension.dart'; +import 'functions.dart'; +import 'mode.dart'; + +/// Tracks selectors and extensions, and applies the latter to the former. +class ExtensionStore { + /// An [ExtensionStore] that contains no extensions and can have no extensions added. + static const empty = EmptyExtensionStore(); + + /// A map from all simple selectors in the stylesheet to the selector lists + /// that contain them. + /// + /// This is used to find which selectors an `@extend` applies to and adjust + /// them. + final Map>> _selectors; + + /// A map from all extended simple selectors to the sources of those + /// extensions. + final Map> _extensions; + + /// A map from all simple selectors in extenders to the extensions that those + /// extenders define. + final Map> _extensionsByExtender; + + /// A map from CSS selectors to the media query contexts they're defined in. + /// + /// This tracks the contexts in which each selector's style rule is defined. + /// If a rule is defined at the top level, it doesn't have an entry. + final Map, List> + _mediaContexts; + + /// A map from [SimpleSelector]s to the specificity of their source + /// selectors. + /// + /// This tracks the maximum specificity of the [ComplexSelector] that + /// originally contained each [SimpleSelector]. This allows us to ensure that + /// we don't trim any selectors that need to exist to satisfy the [second law + /// of extend][]. + /// + /// [second law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 + final Map _sourceSpecificity; + + /// A set of [ComplexSelector]s that were originally part of + /// their component [SelectorList]s, as opposed to being added by `@extend`. + /// + /// This allows us to ensure that we don't trim any selectors that need to + /// exist to satisfy the [first law of extend][]. + /// + /// [first law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 + final Set _originals; + + /// The mode that controls this extender's behavior. + final ExtendMode _mode; + + /// Whether this extender has no extensions. + bool get isEmpty => _extensions.isEmpty; + + /// Extends [selector] with [source] extender and [targets] extendees. + /// + /// This works as though `source {@extend target}` were written in the + /// stylesheet, with the exception that [target] can contain compound + /// selectors which must be extended as a unit. + static SelectorList extend(SelectorList selector, SelectorList source, + SelectorList targets, FileSpan span) => + _extendOrReplace(selector, source, targets, ExtendMode.allTargets, span); + + /// Returns a copy of [selector] with [targets] replaced by [source]. + static SelectorList replace(SelectorList selector, SelectorList source, + SelectorList targets, FileSpan span) => + _extendOrReplace(selector, source, targets, ExtendMode.replace, span); + + /// A helper function for [extend] and [replace]. + static SelectorList _extendOrReplace( + SelectorList selector, + SelectorList source, + SelectorList targets, + ExtendMode mode, + FileSpan span) { + var compoundTargets = [ + for (var complex in targets.components) + if (complex.components.length != 1) + throw SassScriptException("Can't extend complex selector $complex.") + else + complex.components.first as CompoundSelector + ]; + + var extensions = { + for (var compound in compoundTargets) + for (var simple in compound.components) + simple: { + for (var complex in source.components) + complex: Extension(complex, span, simple, span, optional: true) + } + }; + + var extender = ExtensionStore._mode(mode); + if (!selector.isInvisible) { + extender._originals.addAll(selector.components); + } + selector = extender._extendList(selector, span, extensions); + + return selector; + } + + /// The set of all simple selectors in selectors handled by this extender. + /// + /// This includes simple selectors that were added because of downstream + /// extensions. + Set get simpleSelectors => MapKeySet(_selectors); + + ExtensionStore() : this._mode(ExtendMode.normal); + + ExtensionStore._mode(this._mode) + : _selectors = {}, + _extensions = {}, + _extensionsByExtender = {}, + _mediaContexts = {}, + _sourceSpecificity = Map.identity(), + _originals = Set.identity(); + + ExtensionStore._( + this._selectors, + this._extensions, + this._extensionsByExtender, + this._mediaContexts, + this._sourceSpecificity, + this._originals) + : _mode = ExtendMode.normal; + + /// Returns all mandatory extensions in this extender for whose targets + /// [callback] returns `true`. + /// + /// This un-merges any [MergedExtension] so only base [Extension]s are + /// 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) { + if (extension is MergedExtension) { + yield* extension + .unmerge() + .where((extension) => !extension.isOptional); + } else if (!extension.isOptional) { + yield extension; + } + } + } + } + + /// Adds [selector] to this extender. + /// + /// Extends [selector] using any registered extensions, then returns an empty + /// [ModifiableCssValue] containing the resulting selector. If any more + /// relevant extensions are added, the returned selector is automatically + /// updated. + /// + /// The [mediaContext] is the media query context in which the selector was + /// defined, or `null` if it was defined at the top level of the document. + ModifiableCssValue addSelector( + SelectorList selector, FileSpan selectorSpan, + [List? mediaContext]) { + var originalSelector = selector; + if (!originalSelector.isInvisible) { + for (var complex in originalSelector.components) { + _originals.add(complex); + } + } + + if (_extensions.isNotEmpty) { + try { + selector = _extendList( + originalSelector, selectorSpan, _extensions, mediaContext); + } on SassException catch (error) { + throw SassException( + "From ${error.span.message('')}\n" + "${error.message}", + error.span); + } + } + + var modifiableSelector = ModifiableCssValue(selector, selectorSpan); + if (mediaContext != null) _mediaContexts[modifiableSelector] = mediaContext; + _registerSelector(selector, modifiableSelector); + + return modifiableSelector; + } + + /// Registers the [SimpleSelector]s in [list] to point to [selector] in + /// [_selectors]. + void _registerSelector( + SelectorList list, ModifiableCssValue selector) { + for (var complex in list.components) { + for (var component in complex.components) { + if (component is! CompoundSelector) continue; + + for (var simple in component.components) { + _selectors.putIfAbsent(simple, () => {}).add(selector); + if (simple is! PseudoSelector) continue; + + var selectorInPseudo = simple.selector; + if (selectorInPseudo != null) { + _registerSelector(selectorInPseudo, selector); + } + } + } + } + } + + /// Adds an extension to this extender. + /// + /// The [extender] is the selector for the style rule in which the extension + /// is defined, and [target] is the selector passed to `@extend`. The [extend] + /// provides the extend span and indicates whether the extension is optional. + /// + /// The [mediaContext] defines the media query context in which the extension + /// is defined. It can only extend selectors within the same context. A `null` + /// context indicates no media queries. + void addExtension( + CssValue extender, SimpleSelector target, ExtendRule extend, + [List? mediaContext]) { + var selectors = _selectors[target]; + var existingExtensions = _extensionsByExtender[target]; + + Map? newExtensions; + var sources = _extensions.putIfAbsent(target, () => {}); + for (var complex in extender.value.components) { + var extension = Extension(complex, extender.span, target, extend.span, + mediaContext: mediaContext, optional: extend.isOptional); + + var existingExtension = sources[complex]; + if (existingExtension != null) { + // 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 component in complex.components) { + if (component is CompoundSelector) { + for (var simple in component.components) { + _extensionsByExtender.putIfAbsent(simple, () => []).add(extension); + // Only source specificity for the original selector is relevant. + // Selectors generated by `@extend` don't get new specificity. + _sourceSpecificity.putIfAbsent( + simple, () => complex.maxSpecificity); + } + } + } + + if (selectors != null || existingExtensions != null) { + newExtensions ??= {}; + newExtensions[complex] = extension; + } + } + + if (newExtensions == null) return; + + var newExtensionsByTarget = {target: newExtensions}; + if (existingExtensions != null) { + var additionalExtensions = + _extendExistingExtensions(existingExtensions, newExtensionsByTarget); + if (additionalExtensions != null) { + mapAddAll2(newExtensionsByTarget, additionalExtensions); + } + } + + if (selectors != null) { + _extendExistingSelectors(selectors, newExtensionsByTarget); + } + } + + /// Extend [extensions] using [newExtensions]. + /// + /// Note that this does duplicate some work done by + /// [_extendExistingSelectors], but it's necessary to expand each extension's + /// extender separately without reference to the full selector list, so that + /// relevant results don't get trimmed too early. + /// + /// Returns extensions that should be added to [newExtensions] before + /// extending selectors in order to properly handle extension loops such as: + /// + /// .c {x: y; @extend .a} + /// .x.y.a {@extend .b} + /// .z.b {@extend .c} + /// + /// Returns `null` if there are no extensions to add. + Map>? + _extendExistingExtensions(List extensions, + Map> newExtensions) { + Map>? additionalExtensions; + + for (var extension in extensions.toList()) { + var sources = _extensions[extension.target]!; + + List? selectors; + try { + selectors = _extendComplex(extension.extender.selector, + extension.extender.span, newExtensions, extension.mediaContext); + if (selectors == null) continue; + } on SassException catch (error) { + throw SassException( + "From ${extension.extender.span.message('')}\n" + "${error.message}", + error.span); + } + + var containsExtension = selectors.first == extension.extender.selector; + var first = false; + 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; + } + + var withExtender = extension.withExtender(complex); + var existingExtension = sources[complex]; + if (existingExtension != null) { + sources[complex] = + MergedExtension.merge(existingExtension, withExtender); + } else { + sources[complex] = withExtender; + + for (var component in complex.components) { + if (component is CompoundSelector) { + for (var simple in component.components) { + _extensionsByExtender + .putIfAbsent(simple, () => []) + .add(withExtender); + } + } + } + + if (newExtensions.containsKey(extension.target)) { + additionalExtensions ??= {}; + var additionalSources = + additionalExtensions.putIfAbsent(extension.target, () => {}); + additionalSources[complex] = withExtender; + } + } + } + + // If [selectors] doesn't contain [extension.extender], for example if it + // was replaced due to :not() expansion, we must get rid of the old + // version. + if (!containsExtension) sources.remove(extension.extender); + } + + return additionalExtensions; + } + + /// Extend [extensions] using [newExtensions]. + void _extendExistingSelectors(Set> selectors, + Map> newExtensions) { + for (var selector in selectors) { + var oldValue = selector.value; + try { + selector.value = _extendList(selector.value, selector.span, + newExtensions, _mediaContexts[selector]); + } on SassException catch (error) { + // TODO(nweiz): Make this a MultiSpanSassException. + throw SassException( + "From ${selector.span.message('')}\n" + "${error.message}", + error.span); + } + + // If no extends actually happenedit (for example becaues unification + // failed), we don't need to re-register the selector. + if (identical(oldValue, selector.value)) continue; + _registerSelector(selector.value, selector); + } + } + + /// Extends [this] with all the extensions in [extensions]. + /// + /// These extensions will extend all selectors already in [this], but they + /// will *not* extend other extensions from [extenders]. + void addExtensions(Iterable extensionStores) { + // Extensions already in [this] whose extenders are extended by + // [extensions], and thus which need to be updated. + List? extensionsToExtend; + + // Selectors that contain simple selectors that are extended by + // [extensions], and thus which need to be extended themselves. + Set>? selectorsToExtend; + + // An extension map with the same structure as [_extensions] that only + // includes extensions from [extensionStores]. + Map>? newExtensions; + + for (var extensionStore in extensionStores) { + if (extensionStore.isEmpty) continue; + _sourceSpecificity.addAll(extensionStore._sourceSpecificity); + extensionStore._extensions.forEach((target, newSources) { + // Private selectors can't be extended across module boundaries. + if (target is PlaceholderSelector && target.isPrivate) return; + + // Find existing extensions to extend. + var extensionsForTarget = _extensionsByExtender[target]; + if (extensionsForTarget != null) { + (extensionsToExtend ??= []).addAll(extensionsForTarget); + } + + // Find existing selectors to extend. + var selectorsForTarget = _selectors[target]; + if (selectorsForTarget != null) { + (selectorsToExtend ??= {}).addAll(selectorsForTarget); + } + + // Add [newSources] to [_extensions]. + var existingSources = _extensions[target]; + if (existingSources == null) { + _extensions[target] = newSources; + if (extensionsForTarget != null || selectorsForTarget != null) { + (newExtensions ??= {})[target] = newSources; + } + } else { + newSources.forEach((extender, extension) { + // If [extender] already extends [target] in [_extensions], we don't + // need to re-run the extension. + if (existingSources.containsKey(extender)) return; + existingSources[extender] = extension; + + if (extensionsForTarget != null || selectorsForTarget != null) { + (newExtensions ??= {}) + .putIfAbsent(target, () => {}) + .putIfAbsent(extender, () => extension); + } + }); + } + }); + } + + // We can't just naively check for `null` here due to dart-lang/sdk#45348. + newExtensions.andThen((newExtensions) { + // 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)); + + selectorsToExtend.andThen((selectorsToExtend) => + _extendExistingSelectors(selectorsToExtend, newExtensions)); + }); + } + + /// Extends [list] using [extensions]. + SelectorList _extendList(SelectorList list, FileSpan listSpan, + Map> extensions, + [List? mediaQueryContext]) { + // This could be written more simply using [List.map], but we want to avoid + // any allocations in the common case where no extends apply. + List? extended; + for (var i = 0; i < list.components.length; i++) { + var complex = list.components[i]; + var result = + _extendComplex(complex, listSpan, extensions, mediaQueryContext); + if (result == null) { + if (extended != null) extended.add(complex); + } else { + extended ??= i == 0 ? [] : list.components.sublist(0, i).toList(); + extended.addAll(result); + } + } + if (extended == null) return list; + + return SelectorList(_trim(extended, _originals.contains)); + } + + /// Extends [complex] using [extensions], and returns the contents of a + /// [SelectorList]. + List? _extendComplex( + ComplexSelector complex, + FileSpan complexSpan, + Map> extensions, + List? mediaQueryContext) { + // The complex selectors that each compound selector in [complex.components] + // can expand to. + // + // For example, given + // + // .a .b {...} + // .x .y {@extend .b} + // + // this will contain + // + // [ + // [.a], + // [.b, .x .y] + // ] + // + // This could be written more simply using [List.map], but we want to avoid + // any allocations in the common case where no extends apply. + List>? extendedNotExpanded; + var isOriginal = _originals.contains(complex); + for (var i = 0; i < complex.components.length; i++) { + var component = complex.components[i]; + if (component is CompoundSelector) { + var extended = _extendCompound( + component, complexSpan, extensions, mediaQueryContext, + inOriginal: isOriginal); + if (extended == null) { + extendedNotExpanded?.add([ + ComplexSelector([component]) + ]); + } else { + extendedNotExpanded ??= complex.components + .take(i) + .map((component) => [ + ComplexSelector([component], lineBreak: complex.lineBreak) + ]) + .toList(); + extendedNotExpanded.add(extended); + } + } else { + extendedNotExpanded?.add([ + ComplexSelector([component]) + ]); + } + } + if (extendedNotExpanded == null) return null; + + var first = true; + return paths(extendedNotExpanded).expand((path) { + return weave(path.map((complex) => complex.components).toList()) + .map((components) { + var outputComplex = ComplexSelector(components, + lineBreak: complex.lineBreak || + path.any((inputComplex) => inputComplex.lineBreak)); + + // Make sure that copies of [complex] retain their status as "original" + // selectors. This includes selectors that are modified because a :not() + // was extended into. + if (first && _originals.contains(complex)) { + _originals.add(outputComplex); + } + first = false; + + return outputComplex; + }); + }).toList(); + } + + /// Extends [compound] using [extensions], and returns the contents of a + /// [SelectorList]. + /// + /// The [inOriginal] parameter indicates whether this is in an original + /// complex selector, meaning that [compound] should not be trimmed out. + List? _extendCompound( + CompoundSelector compound, + FileSpan compoundSpan, + Map> extensions, + List? mediaQueryContext, + {required bool inOriginal}) { + // If there's more than one target and they all need to match, we track + // which targets are actually extended. + var targetsUsed = _mode == ExtendMode.normal || extensions.length < 2 + ? null + : {}; + + // The complex selectors produced from each component of [compound]. + List>? options; + for (var i = 0; i < compound.components.length; i++) { + var simple = compound.components[i]; + var extended = _extendSimple( + simple, compoundSpan, extensions, mediaQueryContext, targetsUsed); + if (extended == null) { + options?.add([_extenderForSimple(simple, compoundSpan)]); + } else { + if (options == null) { + options = []; + if (i != 0) { + options.add([ + _extenderForCompound(compound.components.take(i), compoundSpan) + ]); + } + } + + options.addAll(extended); + } + } + if (options == null) return null; + + // If [_mode] isn't [ExtendMode.normal] and we didn't use all the targets in + // [extensions], extension fails for [compound]. + if (targetsUsed != null && targetsUsed.length != extensions.length) { + return null; + } + + // Optimize for the simple case of a single simple selector that doesn't + // need any unification. + if (options.length == 1) { + return options.first.map((extender) { + extender.assertCompatibleMediaContext(mediaQueryContext); + return extender.selector; + }).toList(); + } + + // Find all paths through [options]. In this case, each path represents a + // different unification of the base selector. For example, if we have: + // + // .a.b {...} + // .w .x {@extend .a} + // .y .z {@extend .b} + // + // then [options] is `[[.a, .w .x], [.b, .y .z]]` and `paths(options)` is + // + // [ + // [.a, .b], + // [.a, .y .z], + // [.w .x, .b], + // [.w .x, .y .z] + // ] + // + // We then unify each path to get a list of complex selectors: + // + // [ + // [.a.b], + // [.y .a.z], + // [.w .x.b], + // [.w .y .x.z, .y .w .x.z] + // ] + // + // And finally flatten them to get: + // + // [ + // .a.b, + // .y .a.z, + // .w .x.b, + // .w .y .x.z, + // .y .w .x.z + // ] + var first = _mode != ExtendMode.replace; + var result = paths(options) + .map((path) { + List>? complexes; + if (first) { + // The first path is always the original selector. We can't just + // return [compound] directly because pseudo selectors may be + // modified, but we don't have to do any unification. + first = false; + complexes = [ + [ + CompoundSelector(path.expand((extender) { + assert(extender.selector.components.length == 1); + return (extender.selector.components.last as CompoundSelector) + .components; + })) + ] + ]; + } else { + var toUnify = QueueList>(); + List? originals; + for (var extender in path) { + if (extender.isOriginal) { + originals ??= []; + originals.addAll( + (extender.selector.components.last as CompoundSelector) + .components); + } else { + toUnify.add(extender.selector.components); + } + } + + if (originals != null) { + toUnify.addFirst([CompoundSelector(originals)]); + } + + complexes = unifyComplex(toUnify); + if (complexes == null) return null; + } + + var lineBreak = false; + for (var extender in path) { + extender.assertCompatibleMediaContext(mediaQueryContext); + lineBreak = lineBreak || extender.selector.lineBreak; + } + + return complexes + .map((components) => + ComplexSelector(components, lineBreak: lineBreak)) + .toList(); + }) + .whereNotNull() + .expand((l) => l) + .toList(); + + // If we're preserving the original selector, mark the first unification as + // such so [_trim] doesn't get rid of it. + var isOriginal = (ComplexSelector _) => false; + if (inOriginal && _mode != ExtendMode.replace) { + var original = result.first; + isOriginal = (complex) => complex == original; + } + + return _trim(result, isOriginal); + } + + Iterable>? _extendSimple( + SimpleSelector simple, + FileSpan simpleSpan, + Map> extensions, + List? mediaQueryContext, + Set? targetsUsed) { + // Extends [simple] without extending the contents of any selector pseudos + // it contains. + List? withoutPseudo(SimpleSelector simple) { + var extensionsForSimple = extensions[simple]; + if (extensionsForSimple == null) return null; + targetsUsed?.add(simple); + + return [ + if (_mode != ExtendMode.replace) _extenderForSimple(simple, simpleSpan), + for (var extension in extensionsForSimple.values) extension.extender + ]; + } + + if (simple is PseudoSelector && simple.selector != null) { + var extended = + _extendPseudo(simple, simpleSpan, extensions, mediaQueryContext); + if (extended != null) { + return extended.map((pseudo) => + withoutPseudo(pseudo) ?? [_extenderForSimple(pseudo, simpleSpan)]); + } + } + + return withoutPseudo(simple).andThen((result) => [result]); + } + + /// Returns an [Extender] composed solely of a compound selector containing + /// [simples]. + Extender _extenderForCompound( + Iterable simples, FileSpan span) { + var compound = CompoundSelector(simples); + return Extender(ComplexSelector([compound]), span, + specificity: _sourceSpecificityFor(compound), original: true); + } + + /// Returns an [Extender] composed solely of [simple]. + Extender _extenderForSimple(SimpleSelector simple, FileSpan span) => Extender( + ComplexSelector([ + CompoundSelector([simple]) + ]), + span, + specificity: _sourceSpecificity[simple] ?? 0, + original: true); + + /// Extends [pseudo] using [extensions], and returns a list of resulting + /// pseudo selectors. + /// + /// This requires that [pseudo] have a selector argument. + List? _extendPseudo( + PseudoSelector pseudo, + FileSpan pseudoSpan, + Map> extensions, + List? mediaQueryContext) { + var selector = pseudo.selector; + if (selector == null) { + throw ArgumentError("Selector $pseudo must have a selector argument."); + } + + var extended = + _extendList(selector, pseudoSpan, extensions, mediaQueryContext); + if (identical(extended, selector)) return null; + + // For `:not()`, we usually want to get rid of any complex selectors because + // that will cause the selector to fail to parse on all browsers at time of + // writing. We can keep them if either the original selector had a complex + // selector, or the result of extending has only complex selectors, because + // either way we aren't breaking anything that isn't already broken. + Iterable complexes = extended.components; + if (pseudo.normalizedName == "not" && + !selector.components.any((complex) => complex.components.length > 1) && + extended.components.any((complex) => complex.components.length == 1)) { + complexes = extended.components + .where((complex) => complex.components.length <= 1); + } + + complexes = complexes.expand((complex) { + if (complex.components.length != 1) return [complex]; + if (complex.components.first is! CompoundSelector) return [complex]; + var compound = complex.components.first as CompoundSelector; + if (compound.components.length != 1) return [complex]; + if (compound.components.first is! PseudoSelector) return [complex]; + var innerPseudo = compound.components.first as PseudoSelector; + var innerSelector = innerPseudo.selector; + if (innerSelector == null) return [complex]; + + switch (pseudo.normalizedName) { + case 'not': + // In theory, if there's a `:not` nested within another `:not`, the + // inner `:not`'s contents should be unified with the return value. + // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should + // become `.foo:not(.bar)`. However, this is a narrow edge case and + // supporting it properly would make this code and the code calling it + // a lot more complicated, so it's not supported for now. + if (innerPseudo.normalizedName != 'matches') return []; + return innerSelector.components; + + case 'matches': + case 'any': + case 'current': + case 'nth-child': + case 'nth-last-child': + // As above, we could theoretically support :not within :matches, but + // doing so would require this method and its callers to handle much + // more complex cases that likely aren't worth the pain. + if (innerPseudo.name != pseudo.name) return []; + if (innerPseudo.argument != pseudo.argument) return []; + return innerSelector.components; + + case 'has': + case 'host': + case 'host-context': + case 'slotted': + // We can't expand nested selectors here, because each layer adds an + // additional layer of semantics. For example, `:has(:has(img))` + // doesn't match `
` but `:has(img)` does. + return [complex]; + + default: + return []; + } + }); + + // Older browsers support `:not`, but only with a single complex selector. + // In order to support those browsers, we break up the contents of a `:not` + // unless it originally contained a selector list. + if (pseudo.normalizedName == 'not' && selector.components.length == 1) { + var result = complexes + .map((complex) => pseudo.withSelector(SelectorList([complex]))) + .toList(); + return result.isEmpty ? null : result; + } else { + return [pseudo.withSelector(SelectorList(complexes))]; + } + } + + // Removes elements from [selectors] if they're subselectors of other + // elements. + // + // The [isOriginal] callback indicates which selectors are original to the + // document, and thus should never be trimmed. + List _trim(List selectors, + bool isOriginal(ComplexSelector complex)) { + // Avoid truly horrific quadratic behavior. + // + // TODO(nweiz): I think there may be a way to get perfect trimming without + // going quadratic by building some sort of trie-like data structure that + // can be used to look up superselectors. + if (selectors.length > 100) return selectors; + + // This is n² on the sequences, but only comparing between separate + // sequences should limit the quadratic behavior. We iterate from last to + // first and reverse the result so that, if two selectors are identical, we + // keep the first one. + var result = QueueList(); + var numOriginals = 0; + outer: + for (var i = selectors.length - 1; i >= 0; i--) { + var complex1 = selectors[i]; + if (isOriginal(complex1)) { + // Make sure we don't include duplicate originals, which could happen if + // a style rule extends a component of its own selector. + for (var j = 0; j < numOriginals; j++) { + if (result[j] == complex1) { + rotateSlice(result, 0, j + 1); + continue outer; + } + } + + numOriginals++; + result.addFirst(complex1); + continue; + } + + // The maximum specificity of the sources that caused [complex1] to be + // generated. In order for [complex1] to be removed, there must be another + // selector that's a superselector of it *and* that has specificity + // greater or equal to this. + var maxSpecificity = 0; + for (var component in complex1.components) { + if (component is CompoundSelector) { + maxSpecificity = + math.max(maxSpecificity, _sourceSpecificityFor(component)); + } + } + + // Look in [result] rather than [selectors] for selectors after [i]. This + // ensures that we aren't comparing against a selector that's already been + // trimmed, and thus that if there are two identical selectors only one is + // trimmed. + if (result.any((complex2) => + complex2.minSpecificity >= maxSpecificity && + complex2.isSuperselector(complex1))) { + continue; + } + + if (selectors.take(i).any((complex2) => + complex2.minSpecificity >= maxSpecificity && + complex2.isSuperselector(complex1))) { + continue; + } + + result.addFirst(complex1); + } + return result; + } + + /// Returns the maximum specificity for sources that went into producing + /// [compound]. + int _sourceSpecificityFor(CompoundSelector compound) { + var specificity = 0; + for (var simple in compound.components) { + specificity = math.max(specificity, _sourceSpecificity[simple] ?? 0); + } + return specificity; + } + + /// Returns a copy of [this] that extends new selectors, as well as a map from + /// the selectors extended by [this] to the selectors extended by the new + /// [ExtensionStore]. + Tuple2, ModifiableCssValue>> clone() { + var newSelectors = + >>{}; + var newMediaContexts = + , List>{}; + var oldToNewSelectors = + , ModifiableCssValue>{}; + + _selectors.forEach((simple, selectors) { + var newSelectorSet = >{}; + newSelectors[simple] = newSelectorSet; + + for (var selector in selectors) { + var newSelector = ModifiableCssValue(selector.value, selector.span); + newSelectorSet.add(newSelector); + oldToNewSelectors[selector] = newSelector; + + var mediaContext = _mediaContexts[selector]; + if (mediaContext != null) newMediaContexts[newSelector] = mediaContext; + } + }); + + return Tuple2( + 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 deb97e615..25bc199ce 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -4,9 +4,9 @@ /// This library contains utility functions related to extending selectors. /// -/// These functions aren't private methods on [Extender] because they also need -/// to be accessible from elsewhere in the codebase. In addition, they aren't -/// instance methods on other objects because their APIs aren't a good +/// These functions aren't private methods on [ExtensionStore] because they also +/// need to be accessible from elsewhere in the codebase. In addition, they +/// aren't instance methods on other objects because their APIs aren't a good /// fit—usually because they deal with raw component lists rather than selector /// classes, to reduce allocations. @@ -27,33 +27,33 @@ final _subselectorPseudos = {'matches', 'any', 'nth-child', 'nth-last-child'}; /// matched by both [complex1] and [complex2]. /// /// If no such list can be produced, returns `null`. -List> unifyComplex( +List>? unifyComplex( List> complexes) { assert(complexes.isNotEmpty); if (complexes.length == 1) return complexes; - List unifiedBase; + List? unifiedBase; for (var complex in complexes) { var base = complex.last; - if (base is CompoundSelector) { - if (unifiedBase == null) { - unifiedBase = base.components; - } else { - for (var simple in base.components) { - unifiedBase = simple.unify(unifiedBase); - if (unifiedBase == null) return null; - } - } + if (base is! CompoundSelector) return null; + + assert(base.components.isNotEmpty); + if (unifiedBase == null) { + unifiedBase = base.components; } else { - return null; + for (var simple in base.components) { + unifiedBase = simple.unify(unifiedBase!); // dart-lang/sdk#45348 + if (unifiedBase == null) return null; + } } } var complexesWithoutBases = complexes .map((complex) => complex.sublist(0, complex.length - 1)) .toList(); - complexesWithoutBases.last.add(CompoundSelector(unifiedBase)); + // By the time we make it here, [unifiedBase] must be non-null. + complexesWithoutBases.last.add(CompoundSelector(unifiedBase!)); return weave(complexesWithoutBases); } @@ -61,12 +61,13 @@ List> unifyComplex( /// both [compound1] and [compound2]. /// /// If no such selector can be produced, returns `null`. -CompoundSelector unifyCompound( +CompoundSelector? unifyCompound( List compound1, List compound2) { var result = compound2; for (var simple in compound1) { - result = simple.unify(result); - if (result == null) return null; + var unified = simple.unify(result); + if (unified == null) return null; + result = unified; } return CompoundSelector(result); @@ -77,10 +78,10 @@ CompoundSelector unifyCompound( /// [UniversalSelector]s or [TypeSelector]s. /// /// If no such selector can be produced, returns `null`. -SimpleSelector unifyUniversalAndElement( +SimpleSelector? unifyUniversalAndElement( SimpleSelector selector1, SimpleSelector selector2) { - String namespace1; - String name1; + String? namespace1; + String? name1; if (selector1 is UniversalSelector) { namespace1 = selector1.namespace; } else if (selector1 is TypeSelector) { @@ -91,8 +92,8 @@ SimpleSelector unifyUniversalAndElement( 'must be a UniversalSelector or a TypeSelector'); } - String namespace2; - String name2; + String? namespace2; + String? name2; if (selector2 is UniversalSelector) { namespace2 = selector2.namespace; } else if (selector2 is TypeSelector) { @@ -103,7 +104,7 @@ SimpleSelector unifyUniversalAndElement( 'must be a UniversalSelector or a TypeSelector'); } - String namespace; + String? namespace; if (namespace1 == namespace2 || namespace2 == '*') { namespace = namespace1; } else if (namespace1 == '*') { @@ -112,7 +113,7 @@ SimpleSelector unifyUniversalAndElement( return null; } - String name; + String? name; if (name1 == name2 || name2 == null) { name = name1; } else if (name1 == null || name1 == '*') { @@ -179,7 +180,7 @@ List> weave( /// identical to the intersection of all elements matched by `A X` and all /// elements matched by `B X`. Some `AB_i` are elided to reduce the size of /// the output. -Iterable> _weaveParents( +Iterable>? _weaveParents( List parents1, List parents2) { var queue1 = Queue.of(parents1); @@ -246,7 +247,7 @@ Iterable> _weaveParents( /// If the first element of [queue] has a `::root` selector, removes and returns /// that element. -CompoundSelector _firstIfRoot(Queue queue) { +CompoundSelector? _firstIfRoot(Queue queue) { if (queue.isEmpty) return null; var first = queue.first; if (first is CompoundSelector) { @@ -264,7 +265,7 @@ CompoundSelector _firstIfRoot(Queue queue) { /// /// If there are no combinators to be merged, returns an empty list. If the /// combinators can't be merged, returns `null`. -List _mergeInitialCombinators( +List? _mergeInitialCombinators( Queue components1, Queue components2) { var combinators1 = []; @@ -290,10 +291,10 @@ List _mergeInitialCombinators( /// /// If there are no combinators to be merged, returns an empty list. If the /// sequences can't be merged, returns `null`. -List>> _mergeFinalCombinators( +List>>? _mergeFinalCombinators( Queue components1, Queue components2, - [QueueList>> result]) { + [QueueList>>? result]) { result ??= QueueList(); if ((components1.isEmpty || components1.last is! Combinator) && (components2.isEmpty || components2.last is! Combinator)) { @@ -435,7 +436,7 @@ List>> _mergeFinalCombinators( components1.removeLast(); } result.addFirst([ - [components2.removeLast(), combinator2] + [components2.removeLast(), combinator2!] ]); return _mergeFinalCombinators(components1, components2, result); } @@ -662,7 +663,7 @@ bool complexIsSuperselector(List complex1, /// know if the parent selectors in the selector argument match [parents]. bool compoundIsSuperselector( CompoundSelector compound1, CompoundSelector compound2, - {Iterable parents}) { + {Iterable? parents}) { // Every selector in [compound1.components] must have a matching selector in // [compound2.components]. for (var simple1 in compound1.components) { @@ -700,17 +701,16 @@ bool _simpleIsSuperselectorOfCompound( if (simple == theirSimple) return true; // Some selector pseudoclasses can match normal selectors. - if (theirSimple is PseudoSelector && - theirSimple.selector != null && - _subselectorPseudos.contains(theirSimple.normalizedName)) { - return theirSimple.selector.components.every((complex) { - if (complex.components.length != 1) return false; - var compound = complex.components.single as CompoundSelector; - return compound.components.contains(simple); - }); - } else { - return false; - } + if (theirSimple is! PseudoSelector) return false; + var selector = theirSimple.selector; + if (selector == null) return false; + if (!_subselectorPseudos.contains(theirSimple.normalizedName)) return false; + + return selector.components.every((complex) { + if (complex.components.length != 1) return false; + var compound = complex.components.single as CompoundSelector; + return compound.components.contains(simple); + }); }); } @@ -726,29 +726,34 @@ bool _simpleIsSuperselectorOfCompound( /// know if the parent selectors in the selector argument match [parents]. bool _selectorPseudoIsSuperselector( PseudoSelector pseudo1, CompoundSelector compound2, - {Iterable parents}) { + {Iterable? parents}) { + 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 'matches': case 'any': - var pseudos = _selectorPseudosNamed(compound2, pseudo1.name); - return pseudos.any((pseudo2) { - return pseudo1.selector.isSuperselector(pseudo2.selector); - }) || - pseudo1.selector.components.any((complex1) => complexIsSuperselector( + var selectors = _selectorPseudoArgs(compound2, pseudo1.name); + return selectors + .any((selector2) => selector1.isSuperselector(selector2)) || + selector1.components.any((complex1) => complexIsSuperselector( complex1.components, [...?parents, compound2])); case 'has': case 'host': case 'host-context': - return _selectorPseudosNamed(compound2, pseudo1.name) - .any((pseudo2) => pseudo1.selector.isSuperselector(pseudo2.selector)); + return _selectorPseudoArgs(compound2, pseudo1.name) + .any((selector2) => selector1.isSuperselector(selector2)); case 'slotted': - return _selectorPseudosNamed(compound2, pseudo1.name, isClass: false) - .any((pseudo2) => pseudo1.selector.isSuperselector(pseudo2.selector)); + return _selectorPseudoArgs(compound2, pseudo1.name, isClass: false) + .any((selector2) => selector1.isSuperselector(selector2)); case 'not': - return pseudo1.selector.components.every((complex) { + return selector1.components.every((complex) { return compound2.components.any((simple2) { if (simple2 is TypeSelector) { var compound1 = complex.components.last; @@ -761,9 +766,10 @@ bool _selectorPseudoIsSuperselector( compound1.components.any( (simple1) => simple1 is IDSelector && simple1 != simple2); } else if (simple2 is PseudoSelector && - simple2.name == pseudo1.name && - simple2.selector != null) { - return listIsSuperselector(simple2.selector.components, [complex]); + simple2.name == pseudo1.name) { + var selector2 = simple2.selector; + if (selector2 == null) return false; + return listIsSuperselector(selector2.components, [complex]); } else { return false; } @@ -771,27 +777,31 @@ bool _selectorPseudoIsSuperselector( }); case 'current': - return _selectorPseudosNamed(compound2, pseudo1.name) - .any((pseudo2) => pseudo1.selector == pseudo2.selector); + return _selectorPseudoArgs(compound2, pseudo1.name) + .any((selector2) => selector1 == selector2); case 'nth-child': case 'nth-last-child': - return compound2.components.any((pseudo2) => - pseudo2 is PseudoSelector && - pseudo2.name == pseudo1.name && - pseudo2.argument == pseudo1.argument && - pseudo1.selector.isSuperselector(pseudo2.selector)); + return compound2.components.any((pseudo2) { + if (pseudo2 is! PseudoSelector) return false; + if (pseudo2.name != pseudo1.name) return false; + if (pseudo2.argument != pseudo1.argument) return false; + var selector2 = pseudo2.selector; + if (selector2 == null) return false; + return selector1.isSuperselector(selector2); + }); default: throw "unreachable"; } } -/// Returns all pseudo selectors in [compound] that have a selector argument, -/// and that have the given [name]. -Iterable _selectorPseudosNamed( +/// Returns all the selector arguments of pseudo selectors in [compound] with +/// the given [name]. +Iterable _selectorPseudoArgs( CompoundSelector compound, String name, {bool isClass = true}) => - compound.components.whereType().where((pseudo) => - pseudo.isClass == isClass && - pseudo.selector != null && - pseudo.name == name); + compound.components + .whereType() + .where((pseudo) => pseudo.isClass == isClass && pseudo.name == name) + .map((pseudo) => pseudo.selector) + .whereNotNull(); diff --git a/lib/src/extend/merged_extension.dart b/lib/src/extend/merged_extension.dart index 05fbbb216..ddffad5e1 100644 --- a/lib/src/extend/merged_extension.dart +++ b/lib/src/extend/merged_extension.dart @@ -26,7 +26,8 @@ class MergedExtension extends Extension { /// Throws an [ArgumentError] if [left] and [right] don't have the same /// extender and target. static Extension merge(Extension left, Extension right) { - if (left.extender != right.extender || left.target != right.target) { + if (left.extender.selector != right.extender.selector || + left.target != right.target) { throw ArgumentError("$left and $right aren't the same extension."); } @@ -49,20 +50,23 @@ class MergedExtension extends Extension { } MergedExtension._(this.left, this.right) - : super(left.extender, left.target, left.extenderSpan, left.span, - left.mediaContext ?? right.mediaContext, - specificity: left.specificity, optional: true); + : super( + left.extender.selector, left.extender.span, left.target, left.span, + mediaContext: left.mediaContext ?? right.mediaContext, + optional: true); - /// Returns all leaf-node [Extension]s in the tree or [MergedExtension]s. + /// Returns all leaf-node [Extension]s in the tree of [MergedExtension]s. Iterable unmerge() sync* { + var left = this.left; if (left is MergedExtension) { - yield* (left as MergedExtension).unmerge(); + yield* left.unmerge(); } else { yield left; } + var right = this.right; if (right is MergedExtension) { - yield* (right as MergedExtension).unmerge(); + yield* right.unmerge(); } else { yield right; } diff --git a/lib/src/functions.dart b/lib/src/functions.dart index d29de59a7..f83695b17 100644 --- a/lib/src/functions.dart +++ b/lib/src/functions.dart @@ -3,9 +3,12 @@ // https://opensource.org/licenses/MIT. import 'dart:collection'; +import 'dart:async'; import 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; +import 'ast/node.dart'; import 'callable.dart'; import 'functions/color.dart' as color; import 'functions/list.dart' as list; @@ -46,3 +49,21 @@ final coreModules = UnmodifiableListView([ selector.module, string.module ]); + +/// Returns the span for the currently executing callable. +/// +/// For normal exception reporting, this should be avoided in favor of throwing +/// [SassScriptException]s. It should only be used when calling APIs that +/// require spans. +FileSpan get currentCallableSpan { + var node = Zone.current[#_currentCallableNode]; + if (node is AstNode) return node.span; + + throw StateError("currentCallableSpan may only be called within an " + "active Sass callable."); +} + +/// Runs [callback] in a zone with [callableNode]'s span available from +/// [currentCallableSpan]. +T withCurrentCallableNode(AstNode callableNode, T callback()) => + runZoned(callback, zoneValues: {#_currentCallableNode: callableNode}); diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index a49ba6ff0..910465e2f 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -10,6 +10,7 @@ import '../callable.dart'; import '../exception.dart'; import '../module/built_in.dart'; import '../util/number.dart'; +import '../util/nullable.dart'; import '../utils.dart'; import '../value.dart'; import '../warn.dart'; @@ -451,7 +452,7 @@ SassColor _updateComponents(List arguments, /// /// [max] should be 255 for RGB channels, 1 for the alpha channel, and 100 /// for saturation, lightness, whiteness, and blackness. - num getParam(String name, num max, + num? getParam(String name, num max, {bool checkPercent = false, bool assertPercent = false}) { var number = keywords.remove(name)?.assertNumber(name); if (number == null) return null; @@ -496,14 +497,14 @@ SassColor _updateComponents(List arguments, } /// Updates [current] based on [param], clamped within [max]. - num updateValue(num current, num param, num max) { + num updateValue(num current, num? param, num max) { if (param == null) return current; if (change) return param; if (adjust) return (current + param).clamp(0, max); return current + (param > 0 ? max - current : current) * (param / 100); } - int updateRgb(int current, num param) => + int updateRgb(int current, num? param) => fuzzyRound(updateValue(current, param, 255)); if (hasRgb) { @@ -574,9 +575,8 @@ Value _rgb(String name, List arguments) { fuzzyRound(_percentageOrUnitless(red, 255, "red")), fuzzyRound(_percentageOrUnitless(green, 255, "green")), fuzzyRound(_percentageOrUnitless(blue, 255, "blue")), - alpha == null - ? null - : _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")); + alpha.andThen((alpha) => + _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"))); } Value _rgbTwoArg(String name, List arguments) { @@ -628,13 +628,12 @@ Value _hsl(String name, List arguments) { hue.value, saturation.value.clamp(0, 100), lightness.value.clamp(0, 100), - alpha == null - ? null - : _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")); + alpha.andThen((alpha) => + _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"))); } /// Prints a deprecation warning if [hue] has a unit other than `deg`. -void _checkAngle(SassNumber angle, [String name]) { +void _checkAngle(SassNumber angle, [String? name]) { if (!angle.hasUnits || angle.hasUnit('deg')) return; var message = StringBuffer() @@ -698,9 +697,8 @@ Value _hwb(List arguments) { hue.value, whiteness.valueInRange(0, 100, "whiteness"), blackness.valueInRange(0, 100, "whiteness"), - alpha == null - ? null - : _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha")); + alpha.andThen((alpha) => + _percentageOrUnitless(alpha.assertNumber("alpha"), 1, "alpha"))); } Object /* SassString | List */ _parseChannels( @@ -735,14 +733,10 @@ Object /* SassString | List */ _parseChannels( } var maybeSlashSeparated = list[2]; - if (maybeSlashSeparated is SassNumber && - maybeSlashSeparated.asSlash != null) { - return [ - list[0], - list[1], - maybeSlashSeparated.asSlash.item1, - maybeSlashSeparated.asSlash.item2 - ]; + 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("/")) { diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index 76a1f2289..d211e3dd8 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -81,7 +81,7 @@ final _merge = BuiltInCallable.overloadedFunction("merge", { } var map2 = args.last.assertMap("map2"); return _modify(map1, args.take(args.length - 1), (oldValue) { - var nestedMap = oldValue?.tryMap(); + var nestedMap = oldValue.tryMap(); if (nestedMap == null) return map2; return SassMap({...nestedMap.contents, ...map2.contents}); }); @@ -99,12 +99,12 @@ final _deepRemove = var map = arguments[0].assertMap("map"); var keys = [arguments[1], ...arguments[2].asList]; return _modify(map, keys.take(keys.length - 1), (value) { - var nestedMap = value?.tryMap(); - if (nestedMap?.contents?.containsKey(keys.last) ?? false) { + var nestedMap = value.tryMap(); + if (nestedMap != null && nestedMap.contents.containsKey(keys.last)) { return SassMap(Map.of(nestedMap.contents)..remove(keys.last)); } return value; - }); + }, addNesting: false); }); final _remove = BuiltInCallable.overloadedFunction("remove", { @@ -157,33 +157,31 @@ final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) { /// /// If more than one key is provided, this means the map targeted for update is /// nested within [map]. The multiple [keys] form a path of nested maps that -/// leads to the targeted map. If any value along the path is not a map, and -/// `modify(null)` returns null, this inserts a new map at that key and -/// overwrites the current value. Otherwise, this fails and returns [map] with -/// no changes. +/// leads to the targeted value, which is passed to [modify]. +/// +/// If any value along the path (other than the last one) is not a map and +/// [addNesting] is `true`, this creates nested maps to match [keys] and passes +/// [sassNull] to [modify]. Otherwise, this fails and returns [map] with no +/// changes. /// /// If no keys are provided, this passes [map] directly to modify and returns /// the result. -Value _modify(SassMap map, Iterable keys, Value modify(Value old)) { +Value _modify(SassMap map, Iterable keys, Value modify(Value old), + {bool addNesting = true}) { var keyIterator = keys.iterator; - SassMap _modifyNestedMap(SassMap map, [Value newValue]) { + SassMap _modifyNestedMap(SassMap map) { var mutableMap = Map.of(map.contents); var key = keyIterator.current; if (!keyIterator.moveNext()) { - mutableMap[key] = newValue ?? modify(mutableMap[key]); + mutableMap[key] = modify(mutableMap[key] ?? sassNull); return SassMap(mutableMap); } var nestedMap = mutableMap[key]?.tryMap(); - if (nestedMap == null) { - // We pass null to `modify` here to indicate there's no existing value. - newValue = modify(null); - if (newValue == null) return SassMap(mutableMap); - } + if (nestedMap == null && !addNesting) return SassMap(mutableMap); - nestedMap ??= const SassMap.empty(); - mutableMap[key] = _modifyNestedMap(nestedMap, newValue); + mutableMap[key] = _modifyNestedMap(nestedMap ?? const SassMap.empty()); return SassMap(mutableMap); } diff --git a/lib/src/functions/math.dart b/lib/src/functions/math.dart index c87083d32..fbfbb4302 100644 --- a/lib/src/functions/math.dart +++ b/lib/src/functions/math.dart @@ -57,7 +57,7 @@ final _clamp = _function("clamp", r"$min, $number, $max", (arguments) { final _floor = _numberFunction("floor", (value) => value.floor()); final _max = _function("max", r"$numbers...", (arguments) { - SassNumber max; + SassNumber? max; for (var value in arguments[0].asList) { var number = value.assertNumber(); if (max == null || max.lessThan(number).isTruthy) max = number; @@ -67,7 +67,7 @@ final _max = _function("max", r"$numbers...", (arguments) { }); final _min = _function("min", r"$numbers...", (arguments) { - SassNumber min; + SassNumber? min; for (var value in arguments[0].asList) { var number = value.assertNumber(); if (min == null || min.greaterThan(number).isTruthy) min = number; @@ -144,10 +144,11 @@ final _pow = _function("pow", r"$base, $exponent", (arguments) { if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) { return SassNumber(double.nan); } else if (fuzzyEquals(baseValue, 0)) { - if (exponentValue.isFinite && - fuzzyIsInt(exponentValue) && - fuzzyAsInt(exponentValue) % 2 == 1) { - exponentValue = fuzzyRound(exponentValue); + if (exponentValue.isFinite) { + var intExponent = fuzzyAsInt(exponentValue); + if (intExponent != null && intExponent % 2 == 1) { + exponentValue = fuzzyRound(exponentValue); + } } } else if (baseValue.isFinite && fuzzyLessThan(baseValue, 0) && @@ -156,10 +157,11 @@ final _pow = _function("pow", r"$base, $exponent", (arguments) { exponentValue = fuzzyRound(exponentValue); } else if (baseValue.isInfinite && fuzzyLessThan(baseValue, 0) && - exponentValue.isFinite && - fuzzyIsInt(exponentValue) && - fuzzyAsInt(exponentValue) % 2 == 1) { - exponentValue = fuzzyRound(exponentValue); + exponentValue.isFinite) { + var intExponent = fuzzyAsInt(exponentValue); + if (intExponent != null && intExponent % 2 == 1) { + exponentValue = fuzzyRound(exponentValue); + } } return SassNumber(math.pow(baseValue, exponentValue)); }); diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart index 6af3174c1..83eb77e8f 100644 --- a/lib/src/functions/selector.dart +++ b/lib/src/functions/selector.dart @@ -9,7 +9,8 @@ import 'package:collection/collection.dart'; import '../ast/selector.dart'; import '../callable.dart'; import '../exception.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; +import '../functions.dart'; import '../module/built_in.dart'; import '../value.dart'; @@ -87,7 +88,8 @@ final _extend = var target = arguments[1].assertSelector(name: "extendee"); var source = arguments[2].assertSelector(name: "extender"); - return Extender.extend(selector, source, target).asSassList; + return ExtensionStore.extend(selector, source, target, currentCallableSpan) + .asSassList; }); final _replace = @@ -96,7 +98,8 @@ final _replace = var target = arguments[1].assertSelector(name: "original"); var source = arguments[2].assertSelector(name: "replacement"); - return Extender.replace(selector, source, target).asSassList; + return ExtensionStore.replace(selector, source, target, currentCallableSpan) + .asSassList; }); final _unify = _function("unify", r"$selector1, $selector2", (arguments) { @@ -130,7 +133,7 @@ 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) { +CompoundSelector? _prependParent(CompoundSelector compound) { var first = compound.components.first; if (first is UniversalSelector) return null; if (first is TypeSelector) { diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 96c5d628a..ad88e54bc 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,11 +5,12 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 6ac1ee07d6b46134f1616d82782180f1cc3b6d81 +// Checksum: 950db49eb9e3a85f35bc4a3d7cfe029fb60ae498 // // ignore_for_file: unused_import import 'package:collection/collection.dart'; +import 'package:package_config/package_config_types.dart'; import 'package:path/path.dart' as p; import 'package:tuple/tuple.dart'; @@ -18,8 +19,7 @@ import 'importer.dart'; import 'importer/utils.dart'; import 'io.dart'; import 'logger.dart'; -import 'sync_package_resolver.dart'; -import 'utils.dart'; // ignore: unused_import +import 'utils.dart'; /// An in-memory cache of parsed stylesheets that have been imported by Sass. class ImportCache { @@ -38,10 +38,10 @@ class ImportCache { /// /// This cache isn't used for relative imports, because they're /// context-dependent. - final Map, Tuple3> _canonicalizeCache; + final Map, Tuple3?> _canonicalizeCache; /// The parsed stylesheets for each canonicalized import URL. - final Map _importCache; + final Map _importCache; /// The import results for each canonicalized import URL. final Map _resultsCache; @@ -58,33 +58,34 @@ class ImportCache { /// * Each load path specified in the `SASS_PATH` environment variable, which /// should be semicolon-separated on Windows and colon-separated elsewhere. /// - /// * `package:` resolution using [packageResolver], which is a - /// [`SyncPackageResolver`][] from the `package_resolver` package. Note that + /// * `package:` resolution using [packageConfig], which is a + /// [`PackageConfig`][] from the `package_config` package. Note that /// this is a shorthand for adding a [PackageImporter] to [importers]. /// - /// [`SyncPackageResolver`]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html - ImportCache(Iterable importers, - {Iterable loadPaths, - SyncPackageResolver packageResolver, - Logger logger}) - : _importers = _toImporters(importers, loadPaths, packageResolver), + /// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html + ImportCache( + {Iterable? importers, + Iterable? loadPaths, + PackageConfig? packageConfig, + Logger? logger}) + : _importers = _toImporters(importers, loadPaths, packageConfig), _logger = logger ?? const Logger.stderr(), _canonicalizeCache = {}, _importCache = {}, _resultsCache = {}; /// Creates an import cache without any globally-available importers. - ImportCache.none({Logger logger}) + ImportCache.none({Logger? logger}) : _importers = const [], _logger = logger ?? const Logger.stderr(), _canonicalizeCache = {}, _importCache = {}, _resultsCache = {}; - /// Converts the user's [importers], [loadPaths], and [packageResolver] + /// Converts the user's [importers], [loadPaths], and [packageConfig] /// options into a single list of importers. - static List _toImporters(Iterable importers, - Iterable loadPaths, SyncPackageResolver packageResolver) { + static List _toImporters(Iterable? importers, + Iterable? loadPaths, PackageConfig? packageConfig) { var sassPath = getEnvironmentVariable('SASS_PATH'); return [ ...?importers, @@ -93,7 +94,7 @@ class ImportCache { if (sassPath != null) for (var path in sassPath.split(isWindows ? ';' : ':')) FilesystemImporter(path), - if (packageResolver != null) PackageImporter(packageResolver) + if (packageConfig != null) PackageImporter(packageConfig) ]; } @@ -109,10 +110,10 @@ 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, - {Importer baseImporter, Uri baseUrl, bool forImport = false}) { + Tuple3? canonicalize(Uri url, + {Importer? baseImporter, Uri? baseUrl, bool forImport = false}) { if (baseImporter != null) { - var resolvedUrl = baseUrl != null ? baseUrl.resolveUri(url) : url; + var resolvedUrl = baseUrl?.resolveUri(url) ?? url; var canonicalUrl = _canonicalize(baseImporter, resolvedUrl, forImport); if (canonicalUrl != null) { return Tuple3(baseImporter, canonicalUrl, resolvedUrl); @@ -133,7 +134,7 @@ class ImportCache { /// Calls [importer.canonicalize] and prints a deprecation warning if it /// returns a relative URL. - Uri _canonicalize(Importer importer, Uri url, bool forImport) { + Uri? _canonicalize(Importer importer, Uri url, bool forImport) { var result = (forImport ? inImportRule(() => importer.canonicalize(url)) : importer.canonicalize(url)); @@ -155,8 +156,8 @@ 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 baseImporter, Uri baseUrl, bool forImport = false}) { + Tuple2? 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; @@ -175,8 +176,8 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// importers may return for legacy reasons. /// /// Caches the result of the import and uses cached results if possible. - Stylesheet importCanonical(Importer importer, Uri canonicalUrl, - [Uri originalUrl]) { + Stylesheet? importCanonical(Importer importer, Uri canonicalUrl, + [Uri? originalUrl]) { return _importCache.putIfAbsent(canonicalUrl, () { var result = importer.load(canonicalUrl); if (result == null) return null; @@ -197,9 +198,10 @@ Relative canonical URLs are deprecated and will eventually be disallowed. /// 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( + var url = minBy( _canonicalizeCache.values - .where((tuple) => tuple?.item2 == canonicalUrl) + .whereNotNull() + .where((tuple) => tuple.item2 == canonicalUrl) .map((tuple) => tuple.item3), (url) => url.path.length); if (url == null) return canonicalUrl; diff --git a/lib/src/importer.dart b/lib/src/importer.dart index 861528276..6370db06d 100644 --- a/lib/src/importer.dart +++ b/lib/src/importer.dart @@ -31,9 +31,9 @@ abstract class Importer extends AsyncImporter { /// those created from Dart code with plain strings. static final Importer noOp = NoOpImporter(); - Uri canonicalize(Uri url); + Uri? canonicalize(Uri url); - ImporterResult load(Uri url); + ImporterResult? load(Uri url); DateTime modificationTime(Uri url) => DateTime.now(); diff --git a/lib/src/importer/async.dart b/lib/src/importer/async.dart index 80b8c8eb8..3d2591bac 100644 --- a/lib/src/importer/async.dart +++ b/lib/src/importer/async.dart @@ -72,7 +72,7 @@ abstract class AsyncImporter { /// same result. Calling [canonicalize] with a URL returned by [canonicalize] /// must return that URL. Calling [canonicalize] with a URL relative to one /// returned by [canonicalize] must return a meaningful result. - FutureOr canonicalize(Uri url); + FutureOr canonicalize(Uri url); /// Loads the Sass text for the given [url], or returns `null` if /// this importer can't find the stylesheet it refers to. @@ -93,7 +93,7 @@ abstract class AsyncImporter { /// will be used as the wrapped exception's message; otherwise, the exception /// object's `toString()` will be used. This means it's safe for importers to /// throw plain strings. - FutureOr load(Uri url); + FutureOr load(Uri url); /// Returns the time that the Sass file at [url] was last modified. /// diff --git a/lib/src/importer/filesystem.dart b/lib/src/importer/filesystem.dart index ac02f7b96..36192f07a 100644 --- a/lib/src/importer/filesystem.dart +++ b/lib/src/importer/filesystem.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart' as p; import '../importer.dart'; import '../io.dart' as io; import '../syntax.dart'; +import '../util/nullable.dart'; import 'utils.dart'; /// An importer that loads files from a load path on the filesystem. @@ -19,13 +20,13 @@ class FilesystemImporter extends Importer { /// Creates an importer that loads files relative to [loadPath]. FilesystemImporter(String loadPath) : _loadPath = p.absolute(loadPath); - Uri canonicalize(Uri url) { + Uri? canonicalize(Uri url) { if (url.scheme != 'file' && url.scheme != '') return null; - var resolved = resolveImportPath(p.join(_loadPath, p.fromUri(url))); - return resolved == null ? null : p.toUri(io.canonicalize(resolved)); + return resolveImportPath(p.join(_loadPath, p.fromUri(url))) + .andThen((resolved) => p.toUri(io.canonicalize(resolved))); } - ImporterResult load(Uri url) { + ImporterResult? load(Uri url) { var path = p.fromUri(url); return ImporterResult(io.readFile(path), sourceMapUrl: url, syntax: Syntax.forPath(path)); diff --git a/lib/src/importer/no_op.dart b/lib/src/importer/no_op.dart index dde55c060..e6261499d 100644 --- a/lib/src/importer/no_op.dart +++ b/lib/src/importer/no_op.dart @@ -9,8 +9,8 @@ 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 { - Uri canonicalize(Uri url) => null; - ImporterResult load(Uri url) => null; + Uri? canonicalize(Uri url) => null; + ImporterResult? load(Uri url) => null; bool couldCanonicalize(Uri url, Uri canonicalUrl) => false; String toString() => "(unknown)"; diff --git a/lib/src/importer/node/implementation.dart b/lib/src/importer/node/implementation.dart index ce88f7598..019395c19 100644 --- a/lib/src/importer/node/implementation.dart +++ b/lib/src/importer/node/implementation.dart @@ -12,6 +12,7 @@ import '../../io.dart'; import '../../node/function.dart'; import '../../node/importer_result.dart'; import '../../node/utils.dart'; +import '../../util/nullable.dart'; import '../utils.dart'; /// An importer that encapsulates Node Sass's import logic. @@ -67,7 +68,7 @@ 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) { + Tuple2? load(String url, Uri? previous, bool forImport) { var parsed = Uri.parse(url); if (parsed.scheme == '' || parsed.scheme == 'file') { var result = _resolveRelativePath(p.fromUri(parsed), previous, forImport); @@ -75,8 +76,7 @@ class NodeImporter { } // The previous URL is always an absolute file path for filesystem imports. - var previousString = - previous.scheme == 'file' ? p.fromUri(previous) : previous.toString(); + var previousString = _previousToString(previous); for (var importer in _importers) { var value = call2(importer, _context, url, previousString); if (value != null) { @@ -84,7 +84,7 @@ class NodeImporter { } } - return _resolveLoadPathFromUrl(parsed, previous, forImport); + return _resolveLoadPathFromUrl(parsed, forImport); } /// Asynchronously loads the stylesheet at [url]. @@ -92,8 +92,8 @@ 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( - String url, Uri previous, bool forImport) async { + Future?> loadAsync( + String url, Uri? previous, bool forImport) async { var parsed = Uri.parse(url); if (parsed.scheme == '' || parsed.scheme == 'file') { var result = _resolveRelativePath(p.fromUri(parsed), previous, forImport); @@ -101,8 +101,7 @@ class NodeImporter { } // The previous URL is always an absolute file path for filesystem imports. - var previousString = - previous.scheme == 'file' ? p.fromUri(previous) : previous.toString(); + var previousString = _previousToString(previous); for (var importer in _importers) { var value = await _callImporterAsync(importer, url, previousString); if (value != null) { @@ -110,24 +109,27 @@ class NodeImporter { } } - return _resolveLoadPathFromUrl(parsed, previous, forImport); + return _resolveLoadPathFromUrl(parsed, forImport); } /// Tries to load a stylesheet at the given [path] relative to [previous]. /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2 _resolveRelativePath( - String path, Uri previous, bool forImport) { + Tuple2? _resolveRelativePath( + String path, Uri? previous, bool forImport) { if (p.isAbsolute(path)) return _tryPath(path, forImport); + if (previous?.scheme != 'file') return null; // 1: Filesystem imports relative to the base file. - if (previous.scheme == 'file') { - var result = - _tryPath(p.join(p.dirname(p.fromUri(previous)), path), forImport); - if (result != null) return result; - } - return null; + return _tryPath(p.join(p.dirname(p.fromUri(previous)), path), forImport); + } + + /// 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(); } /// Tries to load a stylesheet at the given [url] from a load path (including @@ -135,10 +137,9 @@ class NodeImporter { /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2 _resolveLoadPathFromUrl( - Uri url, Uri previous, bool forImport) => + Tuple2? _resolveLoadPathFromUrl(Uri url, bool forImport) => url.scheme == '' || url.scheme == 'file' - ? _resolveLoadPath(p.fromUri(url), previous, forImport) + ? _resolveLoadPath(p.fromUri(url), forImport) : null; /// Tries to load a stylesheet at the given [path] from a load path (including @@ -146,8 +147,7 @@ class NodeImporter { /// /// Returns the stylesheet at that path and the URL used to load it, or `null` /// if loading failed. - Tuple2 _resolveLoadPath( - String path, Uri previous, bool forImport) { + Tuple2? _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; @@ -165,37 +165,35 @@ 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) { - var resolved = forImport - ? inImportRule(() => resolveImportPath(path)) - : resolveImportPath(path); - return resolved == null - ? null - : Tuple2(readFile(resolved), p.toUri(resolved).toString()); - } + Tuple2? _tryPath(String path, bool forImport) => (forImport + ? inImportRule(() => resolveImportPath(path)) + : resolveImportPath(path)) + .andThen((resolved) => + Tuple2(readFile(resolved), p.toUri(resolved).toString())); /// Converts an importer's return [value] to a tuple that can be returned by /// [load]. - Tuple2 _handleImportResult( - String url, Uri previous, Object value, bool forImport) { + Tuple2? _handleImportResult( + String url, Uri? previous, Object value, bool forImport) { if (isJSError(value)) throw value; if (value is! NodeImporterResult) return null; - var result = value as NodeImporterResult; - if (result.file == null) { - return Tuple2(result.contents ?? '', url); - } else if (result.contents != null) { - return Tuple2(result.contents, result.file); + var file = value.file; + var contents = value.contents; + if (file == null) { + return Tuple2(contents ?? '', url); + } else if (contents != null) { + return Tuple2(contents, file); } else { - var resolved = _resolveRelativePath(result.file, previous, forImport) ?? - _resolveLoadPath(result.file, previous, forImport); + var resolved = _resolveRelativePath(file, previous, forImport) ?? + _resolveLoadPath(file, forImport); if (resolved != null) return resolved; throw "Can't find stylesheet to import."; } } /// Calls an importer that may or may not be asynchronous. - Future _callImporterAsync( + Future _callImporterAsync( JSFunction importer, String url, String previousString) async { var completer = Completer(); diff --git a/lib/src/importer/node/interface.dart b/lib/src/importer/node/interface.dart index 4ae9afb85..fb65865b9 100644 --- a/lib/src/importer/node/interface.dart +++ b/lib/src/importer/node/interface.dart @@ -8,9 +8,10 @@ class NodeImporter { NodeImporter(Object context, Iterable includePaths, Iterable importers); - Tuple2 load(String url, Uri previous, bool forImport) => null; + Tuple2? load(String url, Uri? previous, bool forImport) => + throw ''; - Future> loadAsync( - String url, Uri previous, bool forImport) => - null; + Future?> loadAsync( + String url, Uri? previous, bool forImport) => + throw ''; } diff --git a/lib/src/importer/package.dart b/lib/src/importer/package.dart index 79eded476..fee85228f 100644 --- a/lib/src/importer/package.dart +++ b/lib/src/importer/package.dart @@ -3,9 +3,9 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; +import 'package:package_config/package_config_types.dart'; import '../importer.dart'; -import '../sync_package_resolver.dart'; /// A filesystem importer to use when resolving the results of `package:` URLs. /// @@ -17,30 +17,30 @@ final _filesystemImporter = FilesystemImporter('.'); @sealed class PackageImporter extends Importer { /// The resolver that converts `package:` imports to `file:`. - final SyncPackageResolver _packageResolver; + final PackageConfig _packageConfig; /// Creates an importer that loads stylesheets from `package:` URLs according - /// to [packageResolver], which is a [SyncPackageResolver][] from the - /// `package_resolver` package. + /// to [packageConfig], which is a [PackageConfig][] from the `package_config` + /// package. /// - /// [SyncPackageResolver]: https://www.dartdocs.org/documentation/package_resolver/latest/package_resolver/SyncPackageResolver-class.html - PackageImporter(this._packageResolver); + /// [`PackageConfig`]: https://pub.dev/documentation/package_config/latest/package_config.package_config/PackageConfig-class.html + PackageImporter(this._packageConfig); - Uri canonicalize(Uri url) { + Uri? canonicalize(Uri url) { if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); if (url.scheme != 'package') return null; - var resolved = _packageResolver.resolveUri(url); + var resolved = _packageConfig.resolve(url); if (resolved == null) throw "Unknown package."; if (resolved.scheme.isNotEmpty && resolved.scheme != 'file') { - throw "Unsupported URL ${resolved}."; + throw "Unsupported URL $resolved."; } return _filesystemImporter.canonicalize(resolved); } - ImporterResult load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => _filesystemImporter.load(url); DateTime modificationTime(Uri url) => _filesystemImporter.modificationTime(url); diff --git a/lib/src/importer/result.dart b/lib/src/importer/result.dart index 9fd53cc29..1b3f30dc6 100644 --- a/lib/src/importer/result.dart +++ b/lib/src/importer/result.dart @@ -23,7 +23,7 @@ class ImporterResult { /// automatically from [contents]. Uri get sourceMapUrl => _sourceMapUrl ?? Uri.dataFromString(contents, encoding: utf8); - final Uri _sourceMapUrl; + final Uri? _sourceMapUrl; /// The syntax to use to parse the stylesheet. final Syntax syntax; @@ -37,9 +37,9 @@ class ImporterResult { /// because old clients may still be passing the deprecated [indented] /// parameter instead. ImporterResult(this.contents, - {Uri sourceMapUrl, - Syntax syntax, - @Deprecated("Use the syntax parameter instead.") bool indented}) + {Uri? sourceMapUrl, + Syntax? syntax, + @Deprecated("Use the syntax parameter instead.") bool? indented}) : _sourceMapUrl = sourceMapUrl, syntax = syntax ?? (indented == true ? Syntax.sass : Syntax.scss) { if (sourceMapUrl?.scheme == '') { diff --git a/lib/src/importer/utils.dart b/lib/src/importer/utils.dart index 8bf19a736..6e5d51fef 100644 --- a/lib/src/importer/utils.dart +++ b/lib/src/importer/utils.dart @@ -42,15 +42,15 @@ Future inImportRuleAsync(Future callback()) async { /// /// This tries to fill in extensions and partial prefixes and check for a /// directory default. If no file can be found, it returns `null`. -String resolveImportPath(String path) { +String? resolveImportPath(String path) { var extension = p.extension(path); if (extension == '.sass' || extension == '.scss' || extension == '.css') { - return _ifInImport(() => _exactlyOne( + return _ifInImport(() => _exactlyOne( _tryPath('${p.withoutExtension(path)}.import$extension'))) ?? _exactlyOne(_tryPath(path)); } - return _ifInImport( + return _ifInImport( () => _exactlyOne(_tryPathWithExtensions('$path.import'))) ?? _exactlyOne(_tryPathWithExtensions(path)) ?? _tryPathAsDirectory(path); @@ -75,10 +75,10 @@ List _tryPath(String path) { /// index file exists. /// /// Otherwise, returns `null`. -String _tryPathAsDirectory(String path) { +String? _tryPathAsDirectory(String path) { if (!dirExists(path)) return null; - return _ifInImport(() => + return _ifInImport(() => _exactlyOne(_tryPathWithExtensions(p.join(path, 'index.import')))) ?? _exactlyOne(_tryPathWithExtensions(p.join(path, 'index'))); } @@ -87,7 +87,7 @@ String _tryPathAsDirectory(String path) { /// /// If it contains no paths, returns `null`. If it contains more than one, /// throws an exception. -String _exactlyOne(List paths) { +String? _exactlyOne(List paths) { if (paths.isEmpty) return null; if (paths.length == 1) return paths.first; @@ -98,4 +98,4 @@ String _exactlyOne(List paths) { /// If [_inImportRule] is `true`, invokes callback and returns the result. /// /// Otherwise, returns `null`. -T _ifInImport(T callback()) => _inImportRule ? callback() : null; +T? _ifInImport(T callback()) => _inImportRule ? callback() : null; diff --git a/lib/src/interpolation_buffer.dart b/lib/src/interpolation_buffer.dart index 6fdb193f8..68cd37701 100644 --- a/lib/src/interpolation_buffer.dart +++ b/lib/src/interpolation_buffer.dart @@ -18,7 +18,7 @@ class InterpolationBuffer implements StringSink { /// The contents of the [Interpolation] so far. /// /// This contains [String]s and [Expression]s. - final _contents = []; + final _contents = []; /// Returns whether this buffer has no contents. bool get isEmpty => _contents.isEmpty && _text.isEmpty; @@ -32,11 +32,11 @@ class InterpolationBuffer implements StringSink { _text.clear(); } - void write(Object obj) => _text.write(obj); - void writeAll(Iterable objects, [String separator = '']) => + void write(Object? obj) => _text.write(obj); + void writeAll(Iterable objects, [String separator = '']) => _text.writeAll(objects, separator); void writeCharCode(int character) => _text.writeCharCode(character); - void writeln([Object obj = '']) => _text.writeln(obj); + void writeln([Object? obj = '']) => _text.writeln(obj); /// Adds [expression] to this buffer. void add(Expression expression) { diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart index 2835ee87f..0ee35bc53 100644 --- a/lib/src/io/interface.dart +++ b/lib/src/io/interface.dart @@ -13,7 +13,7 @@ class Stderr { /// by a newline. /// /// If [object] is `null`, just writes a newline. - void writeln([Object object]) {} + void writeln([Object? object]) {} /// Flushes any buffered text. void flush() {} @@ -21,77 +21,78 @@ class Stderr { /// An error thrown by [readFile]. class FileSystemException { - String get message => null; - String get path => null; + String get message => throw ''; + String? get path => throw ''; } /// The standard error for the current process. -Stderr get stderr => null; +Stderr get stderr => throw ''; /// Whether the current process is running on Windows. -bool get isWindows => false; +bool get isWindows => throw ''; /// Whether the current process is running on Mac OS. -bool get isMacOS => false; +bool get isMacOS => throw ''; /// Returns whether or not stdout is connected to an interactive terminal. -bool get hasTerminal => false; +bool get hasTerminal => throw ''; /// Whether we're running as Node.JS. -bool get isNode => false; +bool get isNode => throw ''; /// Whether this process is connected to a terminal that supports ANSI escape /// sequences. -bool get supportsAnsiEscapes => false; +bool get supportsAnsiEscapes => throw ''; /// The current working directory. -String get currentPath => null; +String get currentPath => throw ''; /// Reads the file at [path] as a UTF-8 encoded string. /// /// Throws a [FileSystemException] if reading fails, and a [SassException] if /// the file isn't valid UTF-8. -String readFile(String path) => null; +String readFile(String path) => throw ''; /// Writes [contents] to the file at [path], encoded as UTF-8. /// /// Throws a [FileSystemException] if writing fails. -void writeFile(String path, String contents) => null; +void writeFile(String path, String contents) => throw ''; /// Deletes the file at [path]. /// /// Throws a [FileSystemException] if deletion fails. -void deleteFile(String path) => null; +void deleteFile(String path) => throw ''; /// Reads from the standard input for the current process until it closes, /// returning the contents. -Future readStdin() async => null; +Future readStdin() async => throw ''; /// Returns whether a file at [path] exists. -bool fileExists(String path) => null; +bool fileExists(String path) => throw ''; /// Returns whether a dir at [path] exists. -bool dirExists(String path) => null; +bool dirExists(String path) => throw ''; /// Ensures that a directory exists at [path], creating it and its ancestors if /// necessary. -void ensureDir(String path) => null; +void ensureDir(String path) => throw ''; /// Lists the files (not sub-directories) in the directory at [path]. /// /// If [recursive] is `true`, this lists files in directories transitively /// beneath [path] as well. -Iterable listDir(String path, {bool recursive = false}) => null; +Iterable listDir(String path, {bool recursive = false}) => throw ''; /// Returns the modification time of the file at [path]. -DateTime modificationTime(String path) => null; +DateTime modificationTime(String path) => throw ''; /// Returns the value of the environment variable with the given [name], or /// `null` if it's not set. -String getEnvironmentVariable(String name) => null; +String? getEnvironmentVariable(String name) => throw ''; /// Gets and sets the exit code that the process will use when it exits. -int exitCode; +int get exitCode => throw ''; +set exitCode(int value) => throw ''; /// Recursively watches the directory at [path] for modifications. /// @@ -101,4 +102,5 @@ int exitCode; /// /// If [poll] is `true`, this manually checks the filesystem for changes /// periodically rather than using a native filesystem monitoring API. -Future> watchDir(String path, {bool poll = false}) => null; +Future> watchDir(String path, {bool poll = false}) => + throw ''; diff --git a/lib/src/io/node.dart b/lib/src/io/node.dart index 203ceb3e8..f8ac7f129 100644 --- a/lib/src/io/node.dart +++ b/lib/src/io/node.dart @@ -33,7 +33,7 @@ class Stderr { void write(Object object) => _stderr.write(object.toString()); - void writeln([Object object]) { + void writeln([Object? object]) { _stderr.write("${object ?? ''}\n"); } @@ -58,7 +58,7 @@ String readFile(String path) { } /// Wraps `fs.readFileSync` to throw a [FileSystemException]. -Object _readFile(String path, [String encoding]) => +Object? _readFile(String path, [String? encoding]) => _systemErrorToFileSystemException(() => fs.readFileSync(path, encoding)); void writeFile(String path, String contents) => @@ -76,20 +76,18 @@ Future readStdin() async { }); // Node defaults all buffers to 'utf8'. var sink = utf8.decoder.startChunkedConversion(innerSink); - process.stdin.on('data', allowInterop(([Object chunk]) { - assert(chunk != null); + process.stdin.on('data', allowInterop(([Object? chunk]) { sink.add(chunk as List); })); - process.stdin.on('end', allowInterop(([Object _]) { + process.stdin.on('end', allowInterop(([Object? _]) { // Callback for 'end' receives no args. assert(_ == null); sink.close(); })); - process.stdin.on('error', allowInterop(([Object e]) { - assert(e != null); + process.stdin.on('error', allowInterop(([Object? e]) { stderr.writeln('Failed to read from stdin'); stderr.writeln(e); - completer.completeError(e); + completer.completeError(e!); })); return completer.future; } @@ -175,8 +173,8 @@ DateTime modificationTime(String path) => _systemErrorToFileSystemException(() => DateTime.fromMillisecondsSinceEpoch(fs.statSync(path).mtime.getTime())); -String getEnvironmentVariable(String name) => - getProperty(process.env, name) as String; +String? getEnvironmentVariable(String name) => + getProperty(process.env as Object, name) as String?; /// Runs callback and converts any [JsSystemError]s it throws into /// [FileSystemException]s. @@ -192,7 +190,13 @@ T _systemErrorToFileSystemException(T callback()) { final stderr = Stderr(process.stderr); -bool get hasTerminal => process.stdout.isTTY ?? false; +/// We can't use [process.stdout.isTTY] from `node_interop` because of +/// pulyaevskiy/node-interop#93: it declares `isTTY` as always non-nullably +/// available, but in practice it's undefined if stdout isn't a TTY. +@JS('process.stdout.isTTY') +external bool? get isTTY; + +bool get hasTerminal => isTTY == true; bool get isWindows => process.platform == 'win32'; @@ -215,7 +219,7 @@ Future> watchDir(String path, {bool poll = false}) { // Don't assign the controller until after the ready event fires. Otherwise, // Chokidar will give us a bunch of add events for files that already exist. - StreamController controller; + StreamController? controller; watcher ..on( 'add', @@ -233,10 +237,12 @@ Future> watchDir(String path, {bool poll = false}) { var completer = Completer>(); watcher.on('ready', allowInterop(() { - controller = StreamController(onCancel: () { + // dart-lang/sdk#45348 + var stream = (controller = StreamController(onCancel: () { watcher.close(); - }); - completer.complete(controller.stream); + })) + .stream; + completer.complete(stream); })); return completer.future; diff --git a/lib/src/io/vm.dart b/lib/src/io/vm.dart index aa5eaf064..d6ab96c1c 100644 --- a/lib/src/io/vm.dart +++ b/lib/src/io/vm.dart @@ -85,7 +85,7 @@ DateTime modificationTime(String path) { return stat.modified; } -String getEnvironmentVariable(String name) => io.Platform.environment[name]; +String? getEnvironmentVariable(String name) => io.Platform.environment[name]; Future> watchDir(String path, {bool poll = false}) async { var watcher = poll ? PollingDirectoryWatcher(path) : DirectoryWatcher(path); diff --git a/lib/src/logger.dart b/lib/src/logger.dart index bf9cbb187..56b422c67 100644 --- a/lib/src/logger.dart +++ b/lib/src/logger.dart @@ -26,7 +26,7 @@ abstract class Logger { /// a deprecation warning. Implementations should surface all this information /// to the end user. void warn(String message, - {FileSpan span, Trace trace, bool deprecation = false}); + {FileSpan? span, Trace? trace, bool deprecation = false}); /// Emits a debugging message associated with the given [span]. void debug(String message, SourceSpan span); @@ -35,6 +35,6 @@ abstract class Logger { /// A logger that emits no messages. class _QuietLogger implements Logger { void warn(String message, - {FileSpan span, Trace trace, bool deprecation = false}) {} + {FileSpan? span, Trace? trace, bool deprecation = false}) {} void debug(String message, SourceSpan span) {} } diff --git a/lib/src/logger/stderr.dart b/lib/src/logger/stderr.dart index 7f4a21fe3..d9d1f90d6 100644 --- a/lib/src/logger/stderr.dart +++ b/lib/src/logger/stderr.dart @@ -18,7 +18,7 @@ class StderrLogger implements Logger { const StderrLogger({this.color = false}); void warn(String message, - {FileSpan span, Trace trace, bool deprecation = false}) { + {FileSpan? span, Trace? trace, bool deprecation = false}) { if (color) { // Bold yellow. stderr.write('\u001b[33m\u001b[1m'); diff --git a/lib/src/logger/tracking.dart b/lib/src/logger/tracking.dart index 4591be3b0..e7ec08696 100644 --- a/lib/src/logger/tracking.dart +++ b/lib/src/logger/tracking.dart @@ -22,7 +22,7 @@ class TrackingLogger implements Logger { TrackingLogger(this._logger); void warn(String message, - {FileSpan span, Trace trace, bool deprecation = false}) { + {FileSpan? span, Trace? trace, bool deprecation = false}) { _emittedWarning = true; _logger.warn(message, span: span, trace: trace, deprecation: deprecation); } diff --git a/lib/src/module.dart b/lib/src/module.dart index 189f4d732..cbd2d32c5 100644 --- a/lib/src/module.dart +++ b/lib/src/module.dart @@ -7,7 +7,7 @@ import 'package:source_span/source_span.dart'; import 'ast/css.dart'; import 'ast/node.dart'; import 'callable.dart'; -import 'extend/extender.dart'; +import 'extend/extension_store.dart'; import 'value.dart'; /// The interface for a Sass module. @@ -16,7 +16,7 @@ abstract class Module { /// /// This may be `null` if the module was loaded from a string without a URL /// provided. - Uri get url; + Uri? get url; /// Modules that this module uses. List> get upstream; @@ -34,7 +34,7 @@ abstract class Module { /// /// Implementations must ensure that this has the same keys as [variables] if /// it's not `null`. - Map get variableNodes; + Map? get variableNodes; /// The module's functions. /// @@ -50,7 +50,7 @@ abstract class Module { /// The extensions defined in this module, which is also able to update /// [css]'s style rules in-place based on downstream extensions. - Extender get extender; + ExtensionStore get extensionStore; /// The module's CSS tree. CssStylesheet get css; @@ -71,7 +71,7 @@ abstract class Module { /// /// Throws a [SassScriptException] if this module doesn't define a variable /// named [name]. - void setVariable(String name, Value value, AstNode nodeWithSpan); + void setVariable(String name, Value value, AstNode? nodeWithSpan); /// Returns an opaque object that will be equal to another /// `variableIdentity()` return value for the same name in another module if diff --git a/lib/src/module/built_in.dart b/lib/src/module/built_in.dart index 5e70aac29..ae1d2d86b 100644 --- a/lib/src/module/built_in.dart +++ b/lib/src/module/built_in.dart @@ -8,7 +8,7 @@ import '../ast/css.dart'; import '../ast/node.dart'; import '../callable.dart'; import '../exception.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; import '../module.dart'; import '../value.dart'; @@ -21,13 +21,15 @@ class BuiltInModule implements Module { List> get upstream => const []; Map get variableNodes => const {}; - Extender get extender => Extender.empty; + ExtensionStore get extensionStore => ExtensionStore.empty; CssStylesheet get css => CssStylesheet.empty(url: url); bool get transitivelyContainsCss => false; bool get transitivelyContainsExtensions => false; BuiltInModule(String name, - {Iterable functions, Iterable mixins, Map variables}) + {Iterable? functions, + Iterable? mixins, + Map? variables}) : url = Uri(scheme: "sass", path: name), functions = _callableMap(functions), mixins = _callableMap(mixins), @@ -36,13 +38,13 @@ class BuiltInModule implements Module { /// Returns a map from [callables]' names to their values. static Map _callableMap( - Iterable callables) => + Iterable? callables) => UnmodifiableMapView(callables == null ? {} : UnmodifiableMapView( {for (var callable in callables) callable.name: callable})); - void setVariable(String name, Value value, AstNode nodeWithSpan) { + void setVariable(String name, Value value, AstNode? nodeWithSpan) { if (!variables.containsKey(name)) { throw SassScriptException("Undefined variable."); } diff --git a/lib/src/module/forwarded_view.dart b/lib/src/module/forwarded_view.dart index 1c5dfb144..14874e82a 100644 --- a/lib/src/module/forwarded_view.dart +++ b/lib/src/module/forwarded_view.dart @@ -7,9 +7,10 @@ import '../ast/node.dart'; import '../ast/sass.dart'; import '../callable.dart'; import '../exception.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; import '../module.dart'; import '../util/limited_map_view.dart'; +import '../util/nullable.dart'; import '../util/prefixed_map_view.dart'; import '../value.dart'; @@ -21,16 +22,16 @@ class ForwardedModuleView implements Module { /// The rule that determines how this module's members should be exposed. final ForwardRule _rule; - Uri get url => _inner.url; + Uri? get url => _inner.url; List> get upstream => _inner.upstream; - Extender get extender => _inner.extender; + ExtensionStore get extensionStore => _inner.extensionStore; CssStylesheet get css => _inner.css; bool get transitivelyContainsCss => _inner.transitivelyContainsCss; bool get transitivelyContainsExtensions => _inner.transitivelyContainsExtensions; final Map variables; - final Map variableNodes; + final Map? variableNodes; final Map functions; final Map mixins; @@ -41,9 +42,8 @@ class ForwardedModuleView implements Module { if (rule.prefix == null && rule.shownMixinsAndFunctions == null && rule.shownVariables == null && - (rule.hiddenMixinsAndFunctions == null || - rule.hiddenMixinsAndFunctions.isEmpty) && - (rule.hiddenVariables == null || rule.hiddenVariables.isEmpty)) { + (rule.hiddenMixinsAndFunctions?.isEmpty ?? false) && + (rule.hiddenVariables?.isEmpty ?? false)) { return inner; } else { return ForwardedModuleView(inner, rule); @@ -53,10 +53,8 @@ class ForwardedModuleView implements Module { ForwardedModuleView(this._inner, this._rule) : variables = _forwardedMap(_inner.variables, _rule.prefix, _rule.shownVariables, _rule.hiddenVariables), - variableNodes = _inner.variableNodes == null - ? null - : _forwardedMap(_inner.variableNodes, _rule.prefix, - _rule.shownVariables, _rule.hiddenVariables), + variableNodes = _inner.variableNodes.andThen((inner) => _forwardedMap( + inner, _rule.prefix, _rule.shownVariables, _rule.hiddenVariables)), functions = _forwardedMap(_inner.functions, _rule.prefix, _rule.shownMixinsAndFunctions, _rule.hiddenMixinsAndFunctions), mixins = _forwardedMap(_inner.mixins, _rule.prefix, @@ -66,8 +64,8 @@ class ForwardedModuleView implements Module { /// [safelist], with the given [prefix], if given. /// /// Only one of [blocklist] or [safelist] may be non-`null`. - static Map _forwardedMap(Map map, String prefix, - Set safelist, Set blocklist) { + static Map _forwardedMap(Map map, String? prefix, + Set? safelist, Set? blocklist) { assert(safelist == null || blocklist == null); if (prefix == null && safelist == null && @@ -88,20 +86,22 @@ class ForwardedModuleView implements Module { return map; } - void setVariable(String name, Value value, AstNode nodeWithSpan) { - if (_rule.shownVariables != null && !_rule.shownVariables.contains(name)) { + void setVariable(String name, Value value, AstNode? nodeWithSpan) { + var shownVariables = _rule.shownVariables; + var hiddenVariables = _rule.hiddenVariables; + if (shownVariables != null && !shownVariables.contains(name)) { throw SassScriptException("Undefined variable."); - } else if (_rule.hiddenVariables != null && - _rule.hiddenVariables.contains(name)) { + } else if (hiddenVariables != null && hiddenVariables.contains(name)) { throw SassScriptException("Undefined variable."); } - if (_rule.prefix != null) { - if (!name.startsWith(_rule.prefix)) { + var prefix = _rule.prefix; + if (prefix != null) { + if (!name.startsWith(prefix)) { throw SassScriptException("Undefined variable."); } - name = name.substring(_rule.prefix.length); + name = name.substring(prefix.length); } return _inner.setVariable(name, value, nodeWithSpan); @@ -110,9 +110,10 @@ class ForwardedModuleView implements Module { Object variableIdentity(String name) { assert(variables.containsKey(name)); - if (_rule.prefix != null) { - assert(name.startsWith(_rule.prefix)); - name = name.substring(_rule.prefix.length); + var prefix = _rule.prefix; + if (prefix != null) { + assert(name.startsWith(prefix)); + name = name.substring(prefix.length); } return _inner.variableIdentity(name); diff --git a/lib/src/module/shadowed_view.dart b/lib/src/module/shadowed_view.dart index d27c526ab..f9f5516e4 100644 --- a/lib/src/module/shadowed_view.dart +++ b/lib/src/module/shadowed_view.dart @@ -6,9 +6,10 @@ import '../ast/css.dart'; import '../ast/node.dart'; import '../callable.dart'; import '../exception.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; import '../module.dart'; import '../util/limited_map_view.dart'; +import '../util/nullable.dart'; import '../utils.dart'; import '../value.dart'; @@ -18,16 +19,16 @@ class ShadowedModuleView implements Module { /// The wrapped module. final Module _inner; - Uri get url => _inner.url; + Uri? get url => _inner.url; List> get upstream => _inner.upstream; - Extender get extender => _inner.extender; + ExtensionStore get extensionStore => _inner.extensionStore; CssStylesheet get css => _inner.css; bool get transitivelyContainsCss => _inner.transitivelyContainsCss; bool get transitivelyContainsExtensions => _inner.transitivelyContainsExtensions; final Map variables; - final Map variableNodes; + final Map? variableNodes; final Map functions; final Map mixins; @@ -39,14 +40,14 @@ class ShadowedModuleView implements Module { css.children.isEmpty; /// Like [ShadowedModuleView], but returns `null` if [inner] would be unchanged. - static ShadowedModuleView ifNecessary( + static ShadowedModuleView? ifNecessary( Module inner, - {Set variables, - Set functions, - Set mixins}) => - _needsBlacklist(inner.variables, variables) || - _needsBlacklist(inner.functions, functions) || - _needsBlacklist(inner.mixins, mixins) + {Set? variables, + Set? functions, + Set? mixins}) => + _needsBlocklist(inner.variables, variables) || + _needsBlocklist(inner.functions, functions) || + _needsBlocklist(inner.mixins, mixins) ? ShadowedModuleView(inner, variables: variables, functions: functions, mixins: mixins) : null; @@ -54,9 +55,10 @@ class ShadowedModuleView implements Module { /// Returns a view of [inner] that doesn't include the given [variables], /// [functions], or [mixins]. ShadowedModuleView(this._inner, - {Set variables, Set functions, Set mixins}) + {Set? variables, Set? functions, Set? mixins}) : variables = _shadowedMap(_inner.variables, variables), - variableNodes = _shadowedMap(_inner.variableNodes, variables), + variableNodes = + _inner.variableNodes.andThen((map) => _shadowedMap(map, variables)), functions = _shadowedMap(_inner.functions, functions), mixins = _shadowedMap(_inner.mixins, mixins); @@ -65,16 +67,17 @@ class ShadowedModuleView implements Module { /// Returns a view of [map] with all keys in [blocklist] omitted. static Map _shadowedMap( - Map map, Set blocklist) { - if (map == null || !_needsBlacklist(map, blocklist)) return map; - return LimitedMapView.blocklist(map, blocklist); - } + Map map, Set? blocklist) => + blocklist == null || !_needsBlocklist(map, blocklist) + ? map + : LimitedMapView.blocklist(map, blocklist); /// Returns whether any of [map]'s keys are in [blocklist]. - static bool _needsBlacklist(Map map, Set blocklist) => + static bool _needsBlocklist( + Map map, Set? blocklist) => blocklist != null && map.isNotEmpty && blocklist.any(map.containsKey); - void setVariable(String name, Value value, AstNode nodeWithSpan) { + void setVariable(String name, Value value, AstNode? nodeWithSpan) { if (!variables.containsKey(name)) { throw SassScriptException("Undefined variable."); } else { diff --git a/lib/src/node.dart b/lib/src/node.dart index 065e5543b..89c8e6dfa 100644 --- a/lib/src/node.dart +++ b/lib/src/node.dart @@ -21,7 +21,6 @@ import 'importer/node.dart'; import 'node/exports.dart'; import 'node/function.dart'; import 'node/render_context.dart'; -import 'node/render_context_options.dart'; import 'node/render_options.dart'; import 'node/render_result.dart'; import 'node/types.dart'; @@ -29,6 +28,7 @@ import 'node/value.dart'; import 'node/utils.dart'; import 'parse/scss.dart'; import 'syntax.dart'; +import 'util/nullable.dart'; import 'value.dart'; import 'visitor/serialize.dart'; @@ -65,9 +65,10 @@ void main() { /// /// [render]: https://github.com/sass/node-sass#options void _render( - RenderOptions options, void callback(Object error, RenderResult result)) { - if (options.fiber != null) { - options.fiber.call(allowInterop(() { + RenderOptions options, void callback(Object? error, RenderResult? result)) { + var fiber = options.fiber; + if (fiber != null) { + fiber.call(allowInterop(() { try { callback(null, _renderSync(options)); } catch (error) { @@ -91,10 +92,12 @@ void _render( /// Converts Sass to CSS asynchronously. Future _renderAsync(RenderOptions options) async { var start = DateTime.now(); - var file = options.file == null ? null : p.absolute(options.file); CompileResult result; - if (options.data != null) { - result = await compileStringAsync(options.data, + + var data = options.data; + var file = options.file.andThen(p.absolute); + if (data != null) { + result = await compileStringAsync(data, nodeImporter: _parseImporter(options, start), functions: _parseFunctions(options, start, asynch: true), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, @@ -102,9 +105,9 @@ Future _renderAsync(RenderOptions options) async { useSpaces: options.indentType != 'tab', indentWidth: _parseIndentWidth(options.indentWidth), lineFeed: _parseLineFeed(options.linefeed), - url: options.file == null ? 'stdin' : p.toUri(file).toString(), + url: file == null ? 'stdin' : p.toUri(file).toString(), sourceMap: _enableSourceMaps(options)); - } else if (options.file != null) { + } else if (file != null) { result = await compileAsync(file, nodeImporter: _parseImporter(options, start), functions: _parseFunctions(options, start, asynch: true), @@ -130,10 +133,12 @@ Future _renderAsync(RenderOptions options) async { RenderResult _renderSync(RenderOptions options) { try { var start = DateTime.now(); - var file = options.file == null ? null : p.absolute(options.file); CompileResult result; - if (options.data != null) { - result = compileString(options.data, + + var data = options.data; + var file = options.file.andThen(p.absolute); + if (data != null) { + result = compileString(data, nodeImporter: _parseImporter(options, start), functions: _parseFunctions(options, start).cast(), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, @@ -141,9 +146,9 @@ RenderResult _renderSync(RenderOptions options) { useSpaces: options.indentType != 'tab', indentWidth: _parseIndentWidth(options.indentWidth), lineFeed: _parseLineFeed(options.linefeed), - url: options.file == null ? 'stdin' : p.toUri(file).toString(), + url: file == null ? 'stdin' : p.toUri(file).toString(), sourceMap: _enableSourceMaps(options)); - } else if (options.file != null) { + } else if (file != null) { result = compile(file, nodeImporter: _parseImporter(options, start), functions: _parseFunctions(options, start).cast(), @@ -172,9 +177,7 @@ JsError _wrapException(Object exception) { return _newRenderError(exception.toString().replaceFirst("Error: ", ""), line: exception.span.start.line + 1, column: exception.span.start.column + 1, - file: exception.span.sourceUrl == null - ? 'stdin' - : p.fromUri(exception.span.sourceUrl), + file: exception.span.sourceUrl.andThen(p.fromUri) ?? 'stdin', status: 1); } else { return JsError(exception.toString()); @@ -188,29 +191,31 @@ JsError _wrapException(Object exception) { /// return a `List` if [asynch] is `false`. List _parseFunctions(RenderOptions options, DateTime start, {bool asynch = false}) { - if (options.functions == null) return const []; + var functions = options.functions; + if (functions == null) return const []; var result = []; - jsForEach(options.functions, (signature, callback) { + jsForEach(functions, (signature, callback) { Tuple2 tuple; try { tuple = ScssParser(signature as String).parseSignature(); } on SassFormatException catch (error) { throw SassFormatException( - 'Invalid signature "${signature}": ${error.message}', error.span); + 'Invalid signature "$signature": ${error.message}', error.span); } var context = _contextWithOptions(options, start); - if (options.fiber != null) { + var fiber = options.fiber; + if (fiber != null) { result.add(BuiltInCallable.parsed(tuple.item1, tuple.item2, (arguments) { - var fiber = options.fiber.current; + var currentFiber = fiber.current; var jsArguments = [ ...arguments.map(wrapValue), - allowInterop(([Object result]) { + allowInterop(([Object? result]) { // Schedule a microtask so we don't try to resume the running fiber // if [importer] calls `done()` synchronously. - scheduleMicrotask(() => fiber.run(result)); + scheduleMicrotask(() => currentFiber.run(result)); }) ]; var result = (callback as JSFunction).apply(context, jsArguments); @@ -218,7 +223,7 @@ List _parseFunctions(RenderOptions options, DateTime start, // Run `fiber.yield()` in runZoned() so that Dart resets the current // zone once it's done. Otherwise, interweaving fibers can leave // `Zone.current` in an inconsistent state. - ? runZoned(() => options.fiber.yield()) + ? runZoned(() => fiber.yield()) : result); })); } else if (!asynch) { @@ -230,10 +235,10 @@ List _parseFunctions(RenderOptions options, DateTime start, } else { result.add(AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2, (arguments) async { - var completer = Completer(); + var completer = Completer(); var jsArguments = [ ...arguments.map(wrapValue), - allowInterop(([Object result]) => completer.complete(result)) + allowInterop(([Object? result]) => completer.complete(result)) ]; var result = (callback as JSFunction).apply(context, jsArguments); return unwrapValue( @@ -250,31 +255,33 @@ 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 if (options.importer is List) { + importers = (options.importer as List).cast(); } else { importers = [options.importer as JSFunction]; } - RenderContext context; - if (importers.isNotEmpty) context = _contextWithOptions(options, start); + var context = importers.isNotEmpty + ? _contextWithOptions(options, start) + : const Object(); - if (options.fiber != null) { + var fiber = options.fiber; + if (fiber != null) { importers = importers.map((importer) { return allowInteropCaptureThis( - (Object thisArg, String url, String previous, [Object _]) { - var fiber = options.fiber.current; + (Object thisArg, String url, String previous, [Object? _]) { + var currentFiber = fiber.current; var result = call3(importer, thisArg, url, previous, allowInterop((Object result) { // Schedule a microtask so we don't try to resume the running fiber if // [importer] calls `done()` synchronously. - scheduleMicrotask(() => fiber.run(result)); + scheduleMicrotask(() => currentFiber.run(result)); })); // Run `fiber.yield()` in runZoned() so that Dart resets the current // zone once it's done. Otherwise, interweaving fibers can leave // `Zone.current` in an inconsistent state. - if (isUndefined(result)) return runZoned(() => options.fiber.yield()); + if (isUndefined(result)) return runZoned(() => fiber.yield()); return result; }) as JSFunction; }).toList(); @@ -298,8 +305,8 @@ RenderContext _contextWithOptions(RenderOptions options, DateTime start) { indentType: options.indentType == 'tab' ? 1 : 0, indentWidth: _parseIndentWidth(options.indentWidth) ?? 2, linefeed: _parseLineFeed(options.linefeed).text, - result: RenderResult( - stats: RenderResultStats( + result: RenderContextResult( + stats: RenderContextResultStats( start: start.millisecondsSinceEpoch, entry: options.file ?? 'data')))); context.options.context = context; @@ -307,20 +314,20 @@ RenderContext _contextWithOptions(RenderOptions options, DateTime start) { } /// Parse [style] into an [OutputStyle]. -OutputStyle _parseOutputStyle(String style) { +OutputStyle _parseOutputStyle(String? style) { if (style == null || style == 'expanded') return OutputStyle.expanded; if (style == 'compressed') return OutputStyle.compressed; throw ArgumentError('Unsupported output style "$style".'); } /// Parses the indentation width into an [int]. -int _parseIndentWidth(Object width) { +int? _parseIndentWidth(Object? width) { if (width == null) return null; return width is int ? width : int.parse(width.toString()); } /// Parses the name of a line feed type into a [LineFeed]. -LineFeed _parseLineFeed(String str) { +LineFeed _parseLineFeed(String? str) { switch (str) { case 'cr': return LineFeed.cr; @@ -339,29 +346,31 @@ RenderResult _newRenderResult( var end = DateTime.now(); var css = result.css; - Uint8List sourceMapBytes; + Uint8List? sourceMapBytes; if (_enableSourceMaps(options)) { - var sourceMapPath = options.sourceMap is String - ? options.sourceMap as String - : options.outFile + '.map'; + var sourceMapOption = options.sourceMap; + var sourceMapPath = + sourceMapOption is String ? sourceMapOption : options.outFile! + '.map'; var sourceMapDir = p.dirname(sourceMapPath); - result.sourceMap.sourceRoot = options.sourceMapRoot; - if (options.outFile == null) { - if (options.file == null) { - result.sourceMap.targetUrl = 'stdin.css'; + var sourceMap = result.sourceMap!; + sourceMap.sourceRoot = options.sourceMapRoot; + var outFile = options.outFile; + if (outFile == null) { + var file = options.file; + if (file == null) { + sourceMap.targetUrl = 'stdin.css'; } else { - result.sourceMap.targetUrl = - p.toUri(p.setExtension(options.file, '.css')).toString(); + sourceMap.targetUrl = p.toUri(p.setExtension(file, '.css')).toString(); } } else { - result.sourceMap.targetUrl = - p.toUri(p.relative(options.outFile, from: sourceMapDir)).toString(); + sourceMap.targetUrl = + p.toUri(p.relative(outFile, from: sourceMapDir)).toString(); } var sourceMapDirUrl = p.toUri(sourceMapDir).toString(); - for (var i = 0; i < result.sourceMap.urls.length; i++) { - var source = result.sourceMap.urls[i]; + for (var i = 0; i < sourceMap.urls.length; i++) { + var source = sourceMap.urls[i]; if (source == "stdin") continue; // URLs handled by Node importers that directly return file contents are @@ -369,19 +378,19 @@ RenderResult _newRenderResult( // not be intended as `file:` URLs, but there's nothing we can do about it // either way so we keep them as-is. if (p.url.isRelative(source) || p.url.isRootRelative(source)) continue; - result.sourceMap.urls[i] = p.url.relative(source, from: sourceMapDirUrl); + sourceMap.urls[i] = p.url.relative(source, from: sourceMapDirUrl); } - var json = result.sourceMap - .toJson(includeSourceContents: isTruthy(options.sourceMapContents)); + var json = sourceMap.toJson( + includeSourceContents: isTruthy(options.sourceMapContents)); sourceMapBytes = utf8Encode(jsonEncode(json)); if (!isTruthy(options.omitSourceMapUrl)) { var url = isTruthy(options.sourceMapEmbed) ? Uri.dataFromBytes(sourceMapBytes, mimeType: "application/json") - : p.toUri(options.outFile == null + : p.toUri(outFile == null ? sourceMapPath - : p.relative(sourceMapPath, from: p.dirname(options.outFile))); + : p.relative(sourceMapPath, from: p.dirname(outFile))); css += "\n\n/*# sourceMappingURL=$url */"; } } @@ -405,7 +414,7 @@ bool _enableSourceMaps(RenderOptions options) => /// Creates a [JsError] with the given fields added to it so it acts like a Node /// Sass error. JsError _newRenderError(String message, - {int line, int column, String file, int status}) { + {int? line, int? column, String? file, int? status}) { var error = JsError(message); setProperty(error, 'formatted', 'Error: $message'); if (line != null) setProperty(error, 'line', line); diff --git a/lib/src/node/chokidar.dart b/lib/src/node/chokidar.dart index 8ceb972ee..e94b9def8 100644 --- a/lib/src/node/chokidar.dart +++ b/lib/src/node/chokidar.dart @@ -12,10 +12,10 @@ class Chokidar { @JS() @anonymous class ChokidarOptions { - external bool get disableGlobbing; - external bool get usePolling; + external bool? get disableGlobbing; + external bool? get usePolling; - external factory ChokidarOptions({bool disableGlobbing, bool usePolling}); + external factory ChokidarOptions({bool? disableGlobbing, bool? usePolling}); } @JS() diff --git a/lib/src/node/fiber.dart b/lib/src/node/fiber.dart index 419f86b69..2bc3011ac 100644 --- a/lib/src/node/fiber.dart +++ b/lib/src/node/fiber.dart @@ -8,15 +8,15 @@ import 'package:js/js.dart'; @anonymous class FiberClass { // Work around sdk#31490. - external Fiber call(Object function()); + external Fiber call(Object? function()); external Fiber get current; - external Object yield([Object value]); + external Object yield([Object? value]); } @JS() @anonymous class Fiber { - external Object run([Object value]); + external Object run([Object? value]); } diff --git a/lib/src/node/function.dart b/lib/src/node/function.dart index 0ce234782..20153d7f6 100644 --- a/lib/src/node/function.dart +++ b/lib/src/node/function.dart @@ -6,11 +6,11 @@ import 'package:js/js.dart'; @JS("Function") class JSFunction implements Function { - external JSFunction(String arg1, [String arg2, String arg3]); + external JSFunction(String arg1, [String? arg2, String? arg3]); // Note that this just invokes the function with the given arguments, rather // than calling `Function.prototype.call()`. See sdk#31271. - external Object call([Object arg1, Object arg2, Object arg3]); + external Object? call([Object? arg1, Object? arg2, Object? arg3]); - external Object apply(Object thisArg, [List args]); + external Object? apply(Object thisArg, [List? args]); } diff --git a/lib/src/node/importer_result.dart b/lib/src/node/importer_result.dart index 9144441b4..b13d3302c 100644 --- a/lib/src/node/importer_result.dart +++ b/lib/src/node/importer_result.dart @@ -7,8 +7,8 @@ import 'package:js/js.dart'; @JS() @anonymous class NodeImporterResult { - external String get file; - external String get contents; + external String? get file; + external String? get contents; - external factory NodeImporterResult({String file, String contents}); + external factory NodeImporterResult({String? file, String? contents}); } diff --git a/lib/src/node/render_context.dart b/lib/src/node/render_context.dart index 79103b22a..8fccdabf4 100644 --- a/lib/src/node/render_context.dart +++ b/lib/src/node/render_context.dart @@ -4,12 +4,56 @@ import 'package:js/js.dart'; -import 'render_context_options.dart'; - @JS() @anonymous class RenderContext { external RenderContextOptions get options; - external factory RenderContext({RenderContextOptions options}); + external factory RenderContext({required RenderContextOptions options}); +} + +@JS() +@anonymous +class RenderContextOptions { + external String? get file; + external String? get data; + external String get includePaths; + external int get precision; + external int get style; + external int get indentType; + external int get indentWidth; + external String get linefeed; + external RenderContext get context; + external set context(RenderContext value); + external RenderContextResult get result; + + external factory RenderContextOptions( + {String? file, + String? data, + required String includePaths, + required int precision, + required int style, + required int indentType, + required int indentWidth, + required String linefeed, + required RenderContextResult result}); +} + +@JS() +@anonymous +class RenderContextResult { + external RenderContextResultStats get stats; + + external factory RenderContextResult( + {required RenderContextResultStats stats}); +} + +@JS() +@anonymous +class RenderContextResultStats { + external int get start; + external String get entry; + + external factory RenderContextResultStats( + {required int start, required String entry}); } diff --git a/lib/src/node/render_context_options.dart b/lib/src/node/render_context_options.dart deleted file mode 100644 index 953e1eca3..000000000 --- a/lib/src/node/render_context_options.dart +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2016 Google Inc. Use of this source code is governed by an -// MIT-style license that can be found in the LICENSE file or at -// https://opensource.org/licenses/MIT. - -import 'package:js/js.dart'; - -import 'render_context.dart'; -import 'render_result.dart'; - -@JS() -@anonymous -class RenderContextOptions { - external String get file; - external String get data; - external String get includePaths; - external int get precision; - external int get style; - external int get indentType; - external int get indentWidth; - external String get linefeed; - external RenderContext get context; - external set context(RenderContext value); - external RenderResult get result; - - external factory RenderContextOptions( - {String file, - String data, - String includePaths, - int precision, - int style, - int indentType, - int indentWidth, - String linefeed, - RenderResult result}); -} diff --git a/lib/src/node/render_options.dart b/lib/src/node/render_options.dart index 0f98f4a59..9b6dea08d 100644 --- a/lib/src/node/render_options.dart +++ b/lib/src/node/render_options.dart @@ -9,40 +9,40 @@ import 'fiber.dart'; @JS() @anonymous class RenderOptions { - external String get file; - external String get data; - external dynamic get importer; - external dynamic get functions; - external List get includePaths; - external bool get indentedSyntax; - external bool get omitSourceMapUrl; - external String get outFile; - external String get outputStyle; - external String get indentType; - external dynamic get indentWidth; - external String get linefeed; - external FiberClass get fiber; - external Object get sourceMap; - external bool get sourceMapContents; - external bool get sourceMapEmbed; - external String get sourceMapRoot; + external String? /*?*/ get file; + external String? get data; + external Object? get importer; + external Object? get functions; + external List? get includePaths; + external bool? get indentedSyntax; + external bool? get omitSourceMapUrl; + external String? get outFile; + external String? get outputStyle; + external String? get indentType; + external Object? get indentWidth; + external String? get linefeed; + external FiberClass? get fiber; + external Object? get sourceMap; + external bool? get sourceMapContents; + external bool? get sourceMapEmbed; + external String? get sourceMapRoot; external factory RenderOptions( - {String file, - String data, - Object importer, - Object functions, - List includePaths, - bool indentedSyntax, - bool omitSourceMapUrl, - String outFile, - String outputStyle, - String indentType, - Object indentWidth, - String linefeed, - FiberClass fiber, - Object sourceMap, - bool sourceMapContents, - bool sourceMapEmbed, - String sourceMapRoot}); + {String? file, + String? data, + Object? importer, + Object? functions, + List? includePaths, + bool? indentedSyntax, + bool? omitSourceMapUrl, + String? outFile, + String? outputStyle, + String? indentType, + Object? indentWidth, + String? linefeed, + FiberClass? fiber, + Object? sourceMap, + bool? sourceMapContents, + bool? sourceMapEmbed, + String? sourceMapRoot}); } diff --git a/lib/src/node/render_result.dart b/lib/src/node/render_result.dart index 2804eb998..3e8a9ee54 100644 --- a/lib/src/node/render_result.dart +++ b/lib/src/node/render_result.dart @@ -10,11 +10,13 @@ import 'package:js/js.dart'; @anonymous class RenderResult { external Uint8List get css; - external Uint8List get map; + external Uint8List? get map; external RenderResultStats get stats; external factory RenderResult( - {Uint8List css, Uint8List map, RenderResultStats stats}); + {required Uint8List css, + Uint8List? map, + required RenderResultStats stats}); } @JS() @@ -27,9 +29,9 @@ class RenderResultStats { external List get includedFiles; external factory RenderResultStats( - {String entry, - int start, - int end, - int duration, - List includedFiles}); + {required String entry, + required int start, + required int end, + required int duration, + required List includedFiles}); } diff --git a/lib/src/node/types.dart b/lib/src/node/types.dart index 993459dfe..7bad3a94a 100644 --- a/lib/src/node/types.dart +++ b/lib/src/node/types.dart @@ -17,12 +17,12 @@ class Types { external set Error(Function function); external factory Types( - {Function Boolean, - Function Color, - Function List, - Function Map, - Function Null, - Function Number, - Function String, - Function Error}); + {Function? Boolean, + Function? Color, + Function? List, + Function? Map, + Function? Null, + Function? Number, + Function? String, + Function? Error}); } diff --git a/lib/src/node/utils.dart b/lib/src/node/utils.dart index 00a23b466..a80ca8a47 100644 --- a/lib/src/node/utils.dart +++ b/lib/src/node/utils.dart @@ -19,7 +19,7 @@ void setToString(Object object, String body()) => /// Adds a `toString()` method to [klass] that forwards to Dart's `toString()`. void forwardToString(Function klass) { - setProperty(getProperty(klass, 'prototype'), 'toString', + setProperty(getProperty(klass, 'prototype') as Object, 'toString', allowInteropCaptureThis((Object thisArg) => thisArg.toString())); } @@ -29,7 +29,7 @@ void jsThrow(Object error) => _jsThrow.call(error); final _jsThrow = JSFunction("error", "throw error;"); /// Returns whether or not [value] is the JS `undefined` value. -bool isUndefined(Object value) => _isUndefined.call(value) as bool; +bool isUndefined(Object? value) => _isUndefined.call(value) as bool; final _isUndefined = JSFunction("value", "return value === undefined;"); @@ -50,19 +50,19 @@ external Function get jsErrorConstructor; bool isJSError(Object value) => jsInstanceOf(value, jsErrorConstructor); /// Invokes [function] with [thisArg] as `this`. -Object call2(JSFunction function, Object thisArg, Object arg1, Object arg2) => +Object? call2(JSFunction function, Object thisArg, Object arg1, Object arg2) => function.apply(thisArg, [arg1, arg2]); /// Invokes [function] with [thisArg] as `this`. -Object call3(JSFunction function, Object thisArg, Object arg1, Object arg2, +Object? call3(JSFunction function, Object thisArg, Object arg1, Object arg2, Object arg3) => function.apply(thisArg, [arg1, arg2, arg3]); @JS("Object.keys") -external List _keys(Object object); +external List _keys(Object? object); /// Invokes [callback] for each key/value pair in [object]. -void jsForEach(Object object, void callback(Object key, Object value)) { +void jsForEach(Object object, void callback(Object key, Object? value)) { for (var key in _keys(object)) { callback(key, getProperty(object, key)); } @@ -76,7 +76,7 @@ Function createClass( String name, Function constructor, Map methods) { var klass = allowInteropCaptureThis(constructor); _defineProperty(klass, 'name', _PropertyDescriptor(value: name)); - var prototype = getProperty(klass, 'prototype'); + var prototype = getProperty(klass, 'prototype') as Object; methods.forEach((name, body) { setProperty(prototype, name, allowInteropCaptureThis(body)); }); @@ -84,7 +84,7 @@ Function createClass( } @JS("Object.getPrototypeOf") -external Object _getPrototypeOf(Object object); +external Object? _getPrototypeOf(Object object); @JS("Object.setPrototypeOf") external void _setPrototypeOf(Object object, Object prototype); @@ -98,7 +98,7 @@ external void _defineProperty( class _PropertyDescriptor { external Object get value; - external factory _PropertyDescriptor({Object value}); + external factory _PropertyDescriptor({Object? value}); } @JS("Object.create") @@ -106,22 +106,23 @@ external Object _create(Object prototype); /// Sets the name of `object`'s class to `name`. void setClassName(Object object, String name) { - _defineProperty(getProperty(object, "constructor"), "name", + _defineProperty(getProperty(object, "constructor") as Object, "name", _PropertyDescriptor(value: name)); } /// Injects [constructor] into the inheritance chain for [object]'s class. void injectSuperclass(Object object, Function constructor) { - var prototype = _getPrototypeOf(object); + var prototype = _getPrototypeOf(object)!; var parent = _getPrototypeOf(prototype); if (parent != null) { - _setPrototypeOf(getProperty(constructor, 'prototype'), parent); + _setPrototypeOf(getProperty(constructor, 'prototype') as Object, parent); } - _setPrototypeOf(prototype, _create(getProperty(constructor, 'prototype'))); + _setPrototypeOf( + prototype, _create(getProperty(constructor, 'prototype') as Object)); } /// Returns whether [value] is truthy according to JavaScript. -bool isTruthy(Object value) => value != false && value != null; +bool isTruthy(Object? value) => value != false && value != null; @JS('Buffer.from') external Uint8List _buffer(String text, String encoding); diff --git a/lib/src/node/value.dart b/lib/src/node/value.dart index a3136c7a6..38f9bc322 100644 --- a/lib/src/node/value.dart +++ b/lib/src/node/value.dart @@ -23,7 +23,7 @@ export 'value/string.dart'; /// Unwraps a value wrapped with [wrapValue]. /// /// If [object] is a JS error, throws it. -Value unwrapValue(Object object) { +Value unwrapValue(Object? object) { if (object != null) { if (object is Value) return object; var value = getProperty(object, 'dartValue'); diff --git a/lib/src/node/value/boolean.dart b/lib/src/node/value/boolean.dart index d4b800fd3..e547ef0e2 100644 --- a/lib/src/node/value/boolean.dart +++ b/lib/src/node/value/boolean.dart @@ -24,7 +24,7 @@ final Function booleanConstructor = () { setClassName(sassTrue, "SassBoolean"); forwardToString(constructor); setProperty( - getProperty(constructor, "prototype"), + getProperty(constructor, "prototype") as Object, "getValue", allowInteropCaptureThis( (Object thisArg) => identical(thisArg, sassTrue))); diff --git a/lib/src/node/value/color.dart b/lib/src/node/value/color.dart index 8bd650166..859d99f1b 100644 --- a/lib/src/node/value/color.dart +++ b/lib/src/node/value/color.dart @@ -17,12 +17,13 @@ class _NodeSassColor { /// Creates a new `sass.types.Color` object wrapping [value]. Object newNodeSassColor(SassColor value) => - callConstructor(colorConstructor, [null, null, null, null, value]); + callConstructor(colorConstructor, [null, null, null, null, value]) + as Object; /// The JS constructor for the `sass.types.Color` class. final Function colorConstructor = createClass('SassColor', - (_NodeSassColor thisArg, num redOrArgb, - [num green, num blue, num alpha, SassColor dartValue]) { + (_NodeSassColor thisArg, num? redOrArgb, + [num? green, num? blue, num? alpha, SassColor? dartValue]) { if (dartValue != null) { thisArg.dartValue = dartValue; return; @@ -35,14 +36,15 @@ final Function colorConstructor = createClass('SassColor', // // The latter takes an integer that's interpreted as the hex value 0xAARRGGBB. num red; - if (green == null) { + if (green == null || blue == null) { var argb = redOrArgb as int; alpha = (argb >> 24) / 0xff; red = (argb >> 16) % 0x100; green = (argb >> 8) % 0x100; blue = argb % 0x100; } else { - red = redOrArgb; + // Either [dartValue] or [redOrArgb] must be passed. + red = redOrArgb!; } thisArg.dartValue = SassColor.rgb( diff --git a/lib/src/node/value/list.dart b/lib/src/node/value/list.dart index 36edd4cf6..836fcacb5 100644 --- a/lib/src/node/value/list.dart +++ b/lib/src/node/value/list.dart @@ -18,14 +18,15 @@ class _NodeSassList { /// Creates a new `sass.types.List` object wrapping [value]. Object newNodeSassList(SassList value) => - callConstructor(listConstructor, [null, null, value]); + callConstructor(listConstructor, [null, null, value]) as Object; /// The JS constructor for the `sass.types.List` class. final Function listConstructor = createClass('SassList', - (_NodeSassList thisArg, int length, - [bool commaSeparator, SassList dartValue]) { + (_NodeSassList thisArg, int? length, + [bool? commaSeparator, SassList? dartValue]) { thisArg.dartValue = dartValue ?? - SassList(Iterable.generate(length, (_) => sassNull), + // Either [dartValue] or [length] must be passed. + SassList(Iterable.generate(length!, (_) => sassNull), (commaSeparator ?? true) ? ListSeparator.comma : ListSeparator.space); }, { 'getValue': (_NodeSassList thisArg, int index) => diff --git a/lib/src/node/value/map.dart b/lib/src/node/value/map.dart index 8cc78a0ed..69b4d897e 100644 --- a/lib/src/node/value/map.dart +++ b/lib/src/node/value/map.dart @@ -18,13 +18,15 @@ class _NodeSassMap { /// Creates a new `sass.types.Map` object wrapping [value]. Object newNodeSassMap(SassMap value) => - callConstructor(mapConstructor, [null, value]); + callConstructor(mapConstructor, [null, value]) as Object; /// The JS constructor for the `sass.types.Map` class. final Function mapConstructor = createClass('SassMap', - (_NodeSassMap thisArg, int length, [SassMap dartValue]) { + (_NodeSassMap thisArg, int? length, [SassMap? dartValue]) { thisArg.dartValue = dartValue ?? - SassMap(Map.fromIterables(Iterable.generate(length, (i) => SassNumber(i)), + SassMap(Map.fromIterables( + // Either [dartValue] or [length] must be passed. + Iterable.generate(length!, (i) => SassNumber(i)), Iterable.generate(length, (_) => sassNull))); }, { 'getKey': (_NodeSassMap thisArg, int index) => @@ -39,14 +41,14 @@ final Function mapConstructor = createClass('SassMap', var newKey = unwrapValue(key); var newMap = {}; var i = 0; - for (var oldKey in thisArg.dartValue.contents.keys) { + for (var oldEntry in thisArg.dartValue.contents.entries) { if (i == index) { - newMap[newKey] = oldMap[oldKey]; + newMap[newKey] = oldEntry.value; } else { - if (newKey == oldKey) { + if (newKey == oldEntry.key) { throw ArgumentError.value(key, 'key', "is already in the map"); } - newMap[oldKey] = oldMap[oldKey]; + newMap[oldEntry.key] = oldEntry.value; } i++; } diff --git a/lib/src/node/value/number.dart b/lib/src/node/value/number.dart index cf2bd549f..b967832fc 100644 --- a/lib/src/node/value/number.dart +++ b/lib/src/node/value/number.dart @@ -17,12 +17,14 @@ class _NodeSassNumber { /// Creates a new `sass.types.Number` object wrapping [value]. Object newNodeSassNumber(SassNumber value) => - callConstructor(numberConstructor, [null, null, value]); + callConstructor(numberConstructor, [null, null, value]) as Object; /// The JS constructor for the `sass.types.Number` class. final Function numberConstructor = createClass('SassNumber', - (_NodeSassNumber thisArg, num value, [String unit, SassNumber dartValue]) { - thisArg.dartValue = dartValue ?? _parseNumber(value, unit); + (_NodeSassNumber thisArg, num? value, + [String? unit, SassNumber? dartValue]) { + // Either [dartValue] or [value] must be passed. + thisArg.dartValue = dartValue ?? _parseNumber(value!, unit); }, { 'getValue': (_NodeSassNumber thisArg) => thisArg.dartValue.value, 'setValue': (_NodeSassNumber thisArg, num value) { @@ -42,7 +44,7 @@ final Function numberConstructor = createClass('SassNumber', /// Parses a [SassNumber] from [value] and [unit], using Node Sass's unit /// format. -SassNumber _parseNumber(num value, String unit) { +SassNumber _parseNumber(num value, String? unit) { if (unit == null || unit.isEmpty) return SassNumber(value); if (!unit.contains("*") && !unit.contains("/")) { return SassNumber(value, unit); diff --git a/lib/src/node/value/string.dart b/lib/src/node/value/string.dart index 72bdf2710..b79af1e16 100644 --- a/lib/src/node/value/string.dart +++ b/lib/src/node/value/string.dart @@ -17,12 +17,13 @@ class _NodeSassString { /// Creates a new `sass.types.String` object wrapping [value]. Object newNodeSassString(SassString value) => - callConstructor(stringConstructor, [null, value]); + callConstructor(stringConstructor, [null, value]) as Object; /// The JS constructor for the `sass.types.String` class. final Function stringConstructor = createClass('SassString', - (_NodeSassString thisArg, String value, [SassString dartValue]) { - thisArg.dartValue = dartValue ?? SassString(value, quotes: false); + (_NodeSassString thisArg, String? value, [SassString? dartValue]) { + // Either [dartValue] or [value] must be passed. + thisArg.dartValue = dartValue ?? SassString(value!, quotes: false); }, { 'getValue': (_NodeSassString thisArg) => thisArg.dartValue.text, 'setValue': (_NodeSassString thisArg, String value) { diff --git a/lib/src/parse/at_root_query.dart b/lib/src/parse/at_root_query.dart index f4245245f..b0d0a395e 100644 --- a/lib/src/parse/at_root_query.dart +++ b/lib/src/parse/at_root_query.dart @@ -10,7 +10,7 @@ import 'parser.dart'; /// A parser for `@at-root` queries. class AtRootQueryParser extends Parser { - AtRootQueryParser(String contents, {Object url, Logger logger}) + AtRootQueryParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); AtRootQuery parse() { diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index d2bd10ffd..e0295cc29 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -27,7 +27,7 @@ final _disallowedFunctionNames = class CssParser extends ScssParser { bool get plainCss => true; - CssParser(String contents, {Object url, Logger logger}) + CssParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); void silentComment() { @@ -64,14 +64,7 @@ class CssParser extends ScssParser { almostAnyValue(); error("This at-rule isn't allowed in plain CSS.", scanner.spanFrom(start)); - break; - - case "charset": - string(); - if (!root) { - error("This at-rule is not allowed here.", scanner.spanFrom(start)); - } - return null; + case "import": return _cssImportRule(start); case "media": @@ -112,7 +105,7 @@ class CssParser extends ScssParser { Expression identifierLike() { var start = scanner.state; var identifier = interpolatedIdentifier(); - var plain = identifier.asPlain; + var plain = identifier.asPlain!; // CSS doesn't allow non-plain identifiers var specialFunction = trySpecialFunction(plain.toLowerCase(), start); if (specialFunction != null) return specialFunction; diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart index 0d3d30e7b..1229b6d61 100644 --- a/lib/src/parse/keyframe_selector.dart +++ b/lib/src/parse/keyframe_selector.dart @@ -10,7 +10,7 @@ import 'parser.dart'; /// A parser for `@keyframes` block selectors. class KeyframeSelectorParser extends Parser { - KeyframeSelectorParser(String contents, {Object url, Logger logger}) + KeyframeSelectorParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); List parse() { diff --git a/lib/src/parse/media_query.dart b/lib/src/parse/media_query.dart index eee235c25..d9f5cbbb3 100644 --- a/lib/src/parse/media_query.dart +++ b/lib/src/parse/media_query.dart @@ -11,7 +11,7 @@ import 'parser.dart'; /// A parser for `@media` queries. class MediaQueryParser extends Parser { - MediaQueryParser(String contents, {Object url, Logger logger}) + MediaQueryParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); List parse() { @@ -29,8 +29,8 @@ class MediaQueryParser extends Parser { /// Consumes a single media query. CssMediaQuery _mediaQuery() { // This is somewhat duplicated in StylesheetParser._mediaQuery. - String modifier; - String type; + String? modifier; + String? type; if (scanner.peekChar() != $lparen) { var identifier1 = identifier(); whitespace(); diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 1a8102cde..621270825 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -28,11 +28,11 @@ class Parser { /// Parses [text] as a CSS identifier and returns the result. /// /// Throws a [SassFormatException] if parsing fails. - static String parseIdentifier(String text, {Logger logger}) => + static String parseIdentifier(String text, {Logger? logger}) => Parser(text, logger: logger)._parseIdentifier(); /// Returns whether [text] is a valid CSS identifier. - static bool isIdentifier(String text, {Logger logger}) { + static bool isIdentifier(String text, {Logger? logger}) { try { parseIdentifier(text, logger: logger); return true; @@ -44,11 +44,11 @@ class Parser { /// Returns whether [text] starts like a variable declaration. /// /// Ignores everything after the `:`. - static bool isVariableDeclarationLike(String text, {Logger logger}) => + static bool isVariableDeclarationLike(String text, {Logger? logger}) => Parser(text, logger: logger)._isVariableDeclarationLike(); @protected - Parser(String contents, {Object url, Logger logger}) + Parser(String contents, {Object? url, Logger? logger}) : scanner = SpanScanner(contents, sourceUrl: url), logger = logger ?? const Logger.stderr(); @@ -224,8 +224,7 @@ class Parser { var quote = scanner.readChar(); if (quote != $single_quote && quote != $double_quote) { - scanner.error("Expected string.", - position: quote == null ? scanner.position : scanner.position - 1); + scanner.error("Expected string.", position: scanner.position - 1); } var buffer = StringBuffer(); @@ -325,7 +324,7 @@ class Parser { case $lparen: case $lbrace: case $lbracket: - buffer.writeCharCode(next); + buffer.writeCharCode(next!); // dart-lang/sdk#45357 brackets.add(opposite(scanner.readChar())); wroteNewline = false; break; @@ -334,7 +333,7 @@ class Parser { case $rbrace: case $rbracket: if (brackets.isEmpty) break loop; - buffer.writeCharCode(next); + buffer.writeCharCode(next!); // dart-lang/sdk#45357 scanner.expectChar(brackets.removeLast()); wroteNewline = false; break; @@ -375,7 +374,7 @@ class Parser { /// Consumes a `url()` token if possible, and returns `null` otherwise. @protected - String tryUrl() { + String? tryUrl() { // NOTE: this logic is largely duplicated in ScssParser._tryUrlContents. // Most changes here should be mirrored there. @@ -445,7 +444,6 @@ class Parser { return ""; } else if (isNewline(first)) { scanner.error("Expected escape sequence."); - return null; } else if (isHex(first)) { for (var i = 0; i < 6; i++) { var next = scanner.peekChar(); @@ -465,7 +463,6 @@ class Parser { } on RangeError { scanner.error("Invalid Unicode code point.", position: start, length: scanner.position - start); - return ''; } } else if (value <= 0x1F || value == 0x7F || @@ -491,7 +488,6 @@ class Parser { return 0xFFFD; } else if (isNewline(first)) { scanner.error("Expected escape sequence."); - return 0; } else if (isHex(first)) { var value = 0; for (var i = 0; i < 6; i++) { @@ -517,7 +513,7 @@ class Parser { // // Returns whether or not the character was consumed. @protected - bool scanCharIf(bool condition(int character)) { + bool scanCharIf(bool condition(int? character)) { var next = scanner.peekChar(); if (!condition(next)) return false; scanner.readChar(); @@ -595,7 +591,7 @@ class Parser { /// /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier @protected - bool lookingAtIdentifier([int forward]) { + bool lookingAtIdentifier([int? forward]) { // See also [ScssParser._lookingAtInterpolatedIdentifier]. forward ??= 0; @@ -637,7 +633,7 @@ class Parser { /// Consumes an identifier and asserts that its name exactly matches [text]. @protected void expectIdentifier(String text, - {String name, bool caseSensitive = false}) { + {String? name, bool caseSensitive = false}) { name ??= '"$text"'; var start = scanner.position; @@ -664,8 +660,7 @@ class Parser { /// Throws an error associated with [span]. @protected - @alwaysThrows - void error(String message, FileSpan span) => + Never error(String message, FileSpan span) => throw StringScannerException(message, span, scanner.string); /// Runs callback and, if it throws a [SourceSpanFormatException], rethrows it @@ -684,7 +679,7 @@ class Parser { /// If [message] is passed, prints that as well. This is intended for use when /// debugging parser failures. @protected - void debug([Object message]) { + void debug([Object? message]) { if (message == null) { print(scanner.emptySpan.highlight(color: true)); } else { @@ -721,7 +716,7 @@ class Parser { /// rather than the line where the problem actually occurred. int _firstNewlineBefore(int position) { var index = position - 1; - int lastNewline; + int? lastNewline; while (index >= 0) { var codeUnit = scanner.string.codeUnitAt(index); if (!isWhitespace(codeUnit)) return lastNewline ?? position; diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index adf5aa249..006554553 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -21,24 +21,24 @@ class SassParser extends StylesheetParser { /// position, or `null` if that hasn't been computed yet. /// /// A source line is any line that's not entirely whitespace. - int _nextIndentation; + int? _nextIndentation; /// The beginning of the next source line after the scanner's position, or - /// `null` if that hasn't been computed yet. + /// `null` if the next indentation hasn't been computed yet. /// /// A source line is any line that's not entirely whitespace. - LineScannerState _nextIndentationEnd; + LineScannerState? _nextIndentationEnd; /// Whether the document is indented using spaces or tabs. /// /// If this is `true`, the document is indented using spaces. If it's `false`, /// the document is indented using tabs. If it's `null`, we haven't yet seen /// the indentation character used by the document. - bool _spaces; + bool? _spaces; bool get indented => true; - SassParser(String contents, {Object url, Logger logger}) + SassParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); Interpolation styleRuleSelector() { @@ -54,12 +54,12 @@ class SassParser extends StylesheetParser { return buffer.interpolation(scanner.spanFrom(start)); } - void expectStatementSeparator([String name]) { + void expectStatementSeparator([String? name]) { if (!atEndOfStatement()) _expectNewline(); if (_peekIndentation() <= currentIndentation) return; scanner.error( "Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}.", - position: _nextIndentationEnd.position); + position: _nextIndentationEnd!.position); } bool atEndOfStatement() { @@ -136,12 +136,13 @@ class SassParser extends StylesheetParser { List children(Statement child()) { var children = []; _whileIndentedLower(() { - children.add(_child(child)); + var parsedChild = _child(child); + if (parsedChild != null) children.add(parsedChild); }); return children; } - List statements(Statement statement()) { + List statements(Statement? statement()) { var first = scanner.peekChar(); if (first == $tab || first == $space) { scanner.error("Indenting at the beginning of the document is illegal.", @@ -163,7 +164,7 @@ 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()) { + Statement? _child(Statement? child()) { switch (scanner.peekChar()) { // Ignore empty lines. case $cr: @@ -173,25 +174,19 @@ class SassParser extends StylesheetParser { case $dollar: return variableDeclarationWithoutNamespace(); - break; case $slash: switch (scanner.peekChar(1)) { case $slash: return _silentComment(); - break; case $asterisk: return _loudComment(); - break; default: return child(); - break; } - break; default: return child(); - break; } } @@ -236,9 +231,8 @@ class SassParser extends StylesheetParser { } } while (scanner.scan("//")); - lastSilentComment = + return lastSilentComment = SilentComment(buffer.toString(), scanner.spanFrom(start)); - return lastSilentComment; } /// Consumes an indented-style loud context. @@ -340,7 +334,6 @@ class SassParser extends StylesheetParser { switch (scanner.peekChar()) { case $semicolon: scanner.error("semicolons aren't allowed in the indented syntax."); - return; case $cr: scanner.readChar(); if (scanner.peekChar() == $lf) scanner.readChar(); @@ -373,7 +366,7 @@ class SassParser extends StylesheetParser { /// runs [body] to consume the next statement. void _whileIndentedLower(void body()) { var parentIndentation = currentIndentation; - int childIndentation; + int? childIndentation; while (_peekIndentation() > parentIndentation) { var indentation = _readIndentation(); childIndentation ??= indentation; @@ -391,9 +384,9 @@ class SassParser extends StylesheetParser { /// Consumes indentation whitespace and returns the indentation level of the /// next line. int _readIndentation() { - if (_nextIndentation == null) _peekIndentation(); - _currentIndentation = _nextIndentation; - scanner.state = _nextIndentationEnd; + var currentIndentation = + _currentIndentation = _nextIndentation ??= _peekIndentation(); + scanner.state = _nextIndentationEnd!; _nextIndentation = null; _nextIndentationEnd = null; return currentIndentation; @@ -401,7 +394,8 @@ class SassParser extends StylesheetParser { /// Returns the indentation level of the next line. int _peekIndentation() { - if (_nextIndentation != null) return _nextIndentation; + var cached = _nextIndentation; + if (cached != null) return cached; if (scanner.isDone) { _nextIndentation = 0; @@ -414,13 +408,10 @@ class SassParser extends StylesheetParser { scanner.error("Expected newline.", position: scanner.position); } - bool containsTab; - bool containsSpace; + var containsTab = false; + var containsSpace = false; + var nextIndentation = 0; do { - containsTab = false; - containsSpace = false; - _nextIndentation = 0; - while (true) { var next = scanner.peekChar(); if (next == $space) { @@ -430,7 +421,7 @@ class SassParser extends StylesheetParser { } else { break; } - _nextIndentation++; + nextIndentation++; scanner.readChar(); } @@ -444,10 +435,11 @@ class SassParser extends StylesheetParser { _checkIndentationConsistency(containsTab, containsSpace); - if (_nextIndentation > 0) _spaces ??= containsSpace; + _nextIndentation = nextIndentation; + if (nextIndentation > 0) _spaces ??= containsSpace; _nextIndentationEnd = scanner.state; scanner.state = start; - return _nextIndentation; + return nextIndentation; } /// Ensures that the document uses consistent characters for indentation. diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index fa9facb2c..a03b7e6f5 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -13,14 +13,14 @@ import 'stylesheet.dart'; /// A parser for the CSS-compatible syntax. class ScssParser extends StylesheetParser { bool get indented => false; - int get currentIndentation => null; + int get currentIndentation => 0; - ScssParser(String contents, {Object url, Logger logger}) + ScssParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); Interpolation styleRuleSelector() => almostAnyValue(); - void expectStatementSeparator([String name]) { + void expectStatementSeparator([String? name]) { whitespaceWithoutComments(); if (scanner.isDone) return; var next = scanner.peekChar(); @@ -101,7 +101,7 @@ class ScssParser extends StylesheetParser { } } - List statements(Statement statement()) { + List statements(Statement? statement()) { var statements = []; whitespaceWithoutComments(); while (!scanner.isDone) { @@ -157,9 +157,8 @@ class ScssParser extends StylesheetParser { scanner.spanFrom(start)); } - lastSilentComment = SilentComment( + return lastSilentComment = SilentComment( scanner.substring(start.position), scanner.spanFrom(start)); - return lastSilentComment; } /// Consumes a statement-level loud comment block. diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 3ef290083..78af93ca8 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -33,8 +33,8 @@ class SelectorParser extends Parser { final bool _allowPlaceholder; SelectorParser(String contents, - {Object url, - Logger logger, + {Object? url, + Logger? logger, bool allowParent = true, bool allowPlaceholder = true}) : _allowParent = allowParent, @@ -158,7 +158,7 @@ class SelectorParser extends Parser { /// /// If [allowParent] is passed, it controls whether the parent selector `&` is /// allowed. Otherwise, it defaults to [_allowParent]. - SimpleSelector _simpleSelector({bool allowParent}) { + SimpleSelector _simpleSelector({bool? allowParent}) { var start = scanner.state; allowParent ??= _allowParent; switch (scanner.peekChar()) { @@ -208,7 +208,8 @@ class SelectorParser extends Parser { : identifier(); whitespace(); - var modifier = isAlphabetic(scanner.peekChar()) + next = scanner.peekChar(); + var modifier = next != null && isAlphabetic(next) ? String.fromCharCode(scanner.readChar()) : null; @@ -262,7 +263,6 @@ class SelectorParser extends Parser { default: scanner.error('Expected "]".', position: start.position); - throw "Unreachable"; } } @@ -306,8 +306,8 @@ class SelectorParser extends Parser { whitespace(); var unvendored = unvendor(name); - String argument; - SelectorList selector; + String? argument; + SelectorList? selector; if (element) { if (_selectorPseudoElements.contains(unvendored)) { selector = _selectorList(); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index a0b1a8398..8dbddf592 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -18,6 +18,7 @@ import '../interpolation_buffer.dart'; import '../logger.dart'; import '../util/character.dart'; import '../utils.dart'; +import '../util/nullable.dart'; import '../value.dart'; import 'parser.dart'; @@ -44,7 +45,7 @@ abstract class StylesheetParser extends Parser { /// Whether the current mixin contains at least one `@content` rule. /// /// This is `null` unless [_inMixin] is `true`. - bool _mixinHasContent; + bool? _mixinHasContent; /// Whether the parser is currently parsing a content block passed to a mixin. var _inContentBlock = false; @@ -73,9 +74,9 @@ abstract class StylesheetParser extends Parser { /// The silent comment this parser encountered previously. @protected - SilentComment lastSilentComment; + SilentComment? lastSilentComment; - StylesheetParser(String contents, {Object url, Logger logger}) + StylesheetParser(String contents, {Object? url, Logger? logger}) : super(contents, url: url, logger: logger); // ## Statements @@ -85,7 +86,17 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; // Allow a byte-order mark at the beginning of the document. scanner.scanChar(0xFEFF); - var statements = this.statements(() => _statement(root: true)); + var statements = this.statements(() { + // Handle this specially so that [atRule] always returns a non-nullable + // Statement. + if (scanner.scan('@charset')) { + whitespace(); + string(); + return null; + } + + return _statement(root: true); + }); scanner.expectDone(); /// Ensure that all gloal variable assignments produce a variable in this @@ -178,7 +189,6 @@ abstract class StylesheetParser extends Parser { case $rbrace: scanner.error('unmatched "}".', length: 1); - return null; default: return _inStyleRule || _inUnknownAtRule || _inMixin || _inContentBlock @@ -201,10 +211,10 @@ abstract class StylesheetParser extends Parser { /// used for the declaration. @protected VariableDeclaration variableDeclarationWithoutNamespace( - [String namespace, LineScannerState start]) { + [String? namespace, LineScannerState? start_]) { var precedingComment = lastSilentComment; lastSilentComment = null; - start ??= scanner.state; + var start = start_ ?? scanner.state; // dart-lang/sdk#45348 var name = variableName(); if (namespace != null) _assertPublic(name, () => scanner.spanFrom(start)); @@ -373,7 +383,7 @@ abstract class StylesheetParser extends Parser { if (name.initialPlain.startsWith('--')) { var value = StringExpression(_interpolatedDeclarationValue()); expectStatementSeparator("custom property"); - return Declaration(name, scanner.spanFrom(start), value: value); + return Declaration(name, value, scanner.spanFrom(start)); } if (scanner.scanChar($colon)) { @@ -389,7 +399,7 @@ abstract class StylesheetParser extends Parser { var postColonWhitespace = rawText(whitespace); if (lookingAtChildren()) { return _withChildren(_declarationChild, start, - (children, span) => Declaration(name, span, children: children)); + (children, span) => Declaration.nested(name, children, span)); } midBuffer.write(postColonWhitespace); @@ -433,10 +443,10 @@ abstract class StylesheetParser extends Parser { _declarationChild, start, (children, span) => - Declaration(name, span, value: value, children: children)); + Declaration.nested(name, children, span, value: value)); } else { expectStatementSeparator(); - return Declaration(name, scanner.spanFrom(start), value: value); + return Declaration(name, value, scanner.spanFrom(start)); } } @@ -470,9 +480,10 @@ abstract class StylesheetParser extends Parser { /// Consumes a [StyleRule], optionally with a [buffer] that may contain some /// text that has already been parsed. - StyleRule _styleRule([InterpolationBuffer buffer, LineScannerState start]) { + StyleRule _styleRule( + [InterpolationBuffer? buffer, LineScannerState? start_]) { _isUseAllowed = false; - start ??= scanner.state; + var start = start_ ?? scanner.state; // dart-lang/sdk#45348 var interpolation = styleRuleSelector(); if (buffer != null) { @@ -539,7 +550,7 @@ abstract class StylesheetParser extends Parser { if (parseCustomProperties && name.initialPlain.startsWith('--')) { var value = StringExpression(_interpolatedDeclarationValue()); expectStatementSeparator("custom property"); - return Declaration(name, scanner.spanFrom(start), value: value); + return Declaration(name, value, scanner.spanFrom(start)); } whitespace(); @@ -549,7 +560,7 @@ abstract class StylesheetParser extends Parser { scanner.error("Nested declarations aren't allowed in plain CSS."); } return _withChildren(_declarationChild, start, - (children, span) => Declaration(name, span, children: children)); + (children, span) => Declaration.nested(name, children, span)); } var value = expression(); @@ -561,10 +572,10 @@ abstract class StylesheetParser extends Parser { _declarationChild, start, (children, span) => - Declaration(name, span, value: value, children: children)); + Declaration.nested(name, children, span, value: value)); } else { expectStatementSeparator(); - return Declaration(name, scanner.spanFrom(start), value: value); + return Declaration(name, value, scanner.spanFrom(start)); } } @@ -604,11 +615,6 @@ abstract class StylesheetParser extends Parser { switch (name.asPlain) { case "at-root": return _atRootRule(start); - case "charset": - _isUseAllowed = wasUseAllowed; - if (!root) _disallowedAtRule(start); - string(); - return null; case "content": return _contentRule(start); case "debug": @@ -902,7 +908,6 @@ abstract class StylesheetParser extends Parser { case "not": case "clamp": error("Invalid function name.", scanner.spanFrom(start)); - break; } whitespace(); @@ -926,7 +931,7 @@ abstract class StylesheetParser extends Parser { expectIdentifier("from"); whitespace(); - bool exclusive; + bool? exclusive; var from = expression(until: () { if (!lookingAtIdentifier()) return false; if (scanIdentifier("to")) { @@ -946,8 +951,8 @@ abstract class StylesheetParser extends Parser { return _withChildren(child, start, (children, span) { _inControlDirective = wasInControlDirective; - - return ForRule(variable, from, to, children, span, exclusive: exclusive); + return ForRule(variable, from, to, children, span, + exclusive: exclusive!); // dart-lang/sdk#45348 }); } @@ -958,7 +963,7 @@ abstract class StylesheetParser extends Parser { var url = _urlString(); whitespace(); - String prefix; + String? prefix; if (scanIdentifier("as")) { whitespace(); prefix = identifier(normalize: true); @@ -966,10 +971,10 @@ abstract class StylesheetParser extends Parser { whitespace(); } - Set shownMixinsAndFunctions; - Set shownVariables; - Set hiddenMixinsAndFunctions; - Set hiddenVariables; + Set? shownMixinsAndFunctions; + Set? shownVariables; + Set? hiddenMixinsAndFunctions; + Set? hiddenVariables; if (scanIdentifier("show")) { var members = _memberList(); shownMixinsAndFunctions = members.item1; @@ -990,11 +995,11 @@ abstract class StylesheetParser extends Parser { if (shownMixinsAndFunctions != null) { return ForwardRule.show( - url, shownMixinsAndFunctions, shownVariables, span, + url, shownMixinsAndFunctions, shownVariables!, span, prefix: prefix, configuration: configuration); } else if (hiddenMixinsAndFunctions != null) { return ForwardRule.hide( - url, hiddenMixinsAndFunctions, hiddenVariables, span, + url, hiddenMixinsAndFunctions, hiddenVariables!, span, prefix: prefix, configuration: configuration); } else { return ForwardRule(url, span, @@ -1038,7 +1043,7 @@ abstract class StylesheetParser extends Parser { whitespaceWithoutComments(); var clauses = [IfClause(condition, children)]; - IfClause lastClause; + ElseClause? lastClause; while (scanElse(ifIndentation)) { whitespace(); @@ -1046,7 +1051,7 @@ abstract class StylesheetParser extends Parser { whitespace(); clauses.add(IfClause(expression(), this.children(child))); } else { - lastClause = IfClause.last(this.children(child)); + lastClause = ElseClause(this.children(child)); break; } } @@ -1137,8 +1142,8 @@ abstract class StylesheetParser extends Parser { /// Consumes a supports condition and/or a media query after an `@import`. /// /// Returns `null` if neither type of query can be found. - Tuple2 tryImportQueries() { - SupportsCondition supports; + Tuple2? tryImportQueries() { + SupportsCondition? supports; if (scanIdentifier("supports")) { scanner.expectChar($lparen); var start = scanner.state; @@ -1171,7 +1176,7 @@ abstract class StylesheetParser extends Parser { /// /// [start] should point before the `@`. IncludeRule _includeRule(LineScannerState start) { - String namespace; + String? namespace; var name = identifier(); if (scanner.scanChar($dot)) { namespace = name; @@ -1186,22 +1191,21 @@ abstract class StylesheetParser extends Parser { : ArgumentInvocation.empty(scanner.emptySpan); whitespace(); - ArgumentDeclaration contentArguments; + ArgumentDeclaration? contentArguments; if (scanIdentifier("using")) { whitespace(); contentArguments = _argumentDeclaration(); whitespace(); } - ContentBlock content; + ContentBlock? content; if (contentArguments != null || lookingAtChildren()) { - contentArguments ??= ArgumentDeclaration.empty(span: scanner.emptySpan); - + var contentArguments_ = contentArguments ?? + ArgumentDeclaration.empty(span: scanner.emptySpan); var wasInContentBlock = _inContentBlock; _inContentBlock = true; - content = _withChildren(_statement, start, (children, span) { - return ContentBlock(contentArguments, children, span); - }); + content = _withChildren(_statement, start, + (children, span) => ContentBlock(contentArguments_, children, span)); _inContentBlock = wasInContentBlock; } else { expectStatementSeparator(); @@ -1248,7 +1252,7 @@ abstract class StylesheetParser extends Parser { _mixinHasContent = false; return _withChildren(_statement, start, (children, span) { - var hadContent = _mixinHasContent; + var hadContent = _mixinHasContent!; _inMixin = false; _mixinHasContent = null; @@ -1384,7 +1388,9 @@ relase. For details, see http://bit.ly/moz-document. /// Parses the namespace of a `@use` rule from an `as` clause, or returns the /// default namespace from its URL. - String _useNamespace(Uri url, LineScannerState start) { + /// + /// Returns `null` to indicate a `@use` rule without a URL. + String? _useNamespace(Uri url, LineScannerState start) { if (scanIdentifier("as")) { whitespace(); return scanner.scanChar($asterisk) ? null : identifier(); @@ -1408,7 +1414,7 @@ relase. For details, see http://bit.ly/moz-document. /// `!default` flag. /// /// Returns `null` if there is no `with` clause. - List _configuration({bool allowGuarded = false}) { + List? _configuration({bool allowGuarded = false}) { if (!scanIdentifier("with")) return null; var variableNames = {}; @@ -1486,7 +1492,7 @@ relase. For details, see http://bit.ly/moz-document. var wasInUnknownAtRule = _inUnknownAtRule; _inUnknownAtRule = true; - Interpolation value; + Interpolation? value; var next = scanner.peekChar(); if (next != $exclamation && !atEndOfStatement()) value = almostAnyValue(); @@ -1523,13 +1529,13 @@ relase. For details, see http://bit.ly/moz-document. whitespace(); var arguments = []; var named = {}; - String restArgument; + String? restArgument; while (scanner.peekChar() == $dollar) { var variableStart = scanner.state; var name = variableName(); whitespace(); - Expression defaultValue; + Expression? defaultValue; if (scanner.scanChar($colon)) { whitespace(); defaultValue = _expressionUntilComma(); @@ -1569,8 +1575,8 @@ relase. For details, see http://bit.ly/moz-document. var positional = []; var named = {}; - Expression rest; - Expression keywordRest; + Expression? rest; + Expression? keywordRest; while (_lookingAtExpression()) { var expression = _expressionUntilComma(singleEquals: !mixin); whitespace(); @@ -1621,10 +1627,10 @@ relase. For details, see http://bit.ly/moz-document. /// expression. @protected Expression expression( - {bool bracketList = false, bool singleEquals = false, bool until()}) { + {bool bracketList = false, bool singleEquals = false, bool until()?}) { if (until != null && until()) scanner.error("Expected expression."); - LineScannerState beforeBracket; + LineScannerState? beforeBracket; if (bracketList) { beforeBracket = scanner.state; scanner.expectChar($lbracket); @@ -1639,52 +1645,74 @@ relase. For details, see http://bit.ly/moz-document. var start = scanner.state; var wasInParentheses = _inParentheses; - List commaExpressions; + // We use the convention below of referring to nullable variables that are + // shared across anonymous functions in this method with a trailling + // underscore. This allows us to copy them to non-underscored local + // variables to make it easier for Dart's type system to reason about their + // local nullability. + + List? commaExpressions_; - List spaceExpressions; + List? spaceExpressions_; - // Operators whose right-hand operands are not fully parsed yet, in order of + // Operators whose right-hand operands_ are not fully parsed yet, in order of // appearance in the document. Because a low-precedence operator will cause - // parsing to finish for all preceding higher-precedence operators, this is + // parsing to finish for all preceding higher-precedence operators_, this is // naturally ordered from lowest to highest precedence. - List operators; + List? operators_; - // The left-hand sides of [operators]. `operands[n]` is the left-hand side - // of `operators[n]`. - List operands; + // The left-hand sides of [operators_]. `operands_[n]` is the left-hand side + // of `operators_[n]`. + List? operands_; /// Whether the single expression parsed so far may be interpreted as /// slash-separated numbers. var allowSlash = lookingAtNumber(); - /// The leftmost expression that's been fully-parsed. Never `null`. - var singleExpression = _singleExpression(); + /// The leftmost expression that's been fully-parsed. This can be null in + /// special cases where the expression begins with a sub-expression but has + /// a later character that indicates that the outer expression isn't done, + /// as here: + /// + /// foo, bar + /// ^ + Expression? singleExpression_ = _singleExpression(); // Resets the scanner state to the state it was at at the beginning of the // expression, except for [_inParentheses]. void resetState() { - commaExpressions = null; - spaceExpressions = null; - operators = null; - operands = null; + commaExpressions_ = null; + spaceExpressions_ = null; + operators_ = null; + operands_ = null; scanner.state = start; allowSlash = lookingAtNumber(); - singleExpression = _singleExpression(); + singleExpression_ = _singleExpression(); } void resolveOneOperation() { - var operator = operators.removeLast(); + var operator = operators_!.removeLast(); + var operands = operands_!; + + var singleExpression = singleExpression_; + if (singleExpression == null) { + scanner.error("Expected expression.", + position: scanner.position - operator.operator.length, + length: operator.operator.length); + } + if (operator != BinaryOperator.dividedBy) allowSlash = false; if (allowSlash && !_inParentheses) { - singleExpression = BinaryOperationExpression.slash( + singleExpression_ = BinaryOperationExpression.slash( operands.removeLast(), singleExpression); } else { - singleExpression = BinaryOperationExpression( + singleExpression_ = BinaryOperationExpression( operator, operands.removeLast(), singleExpression); } } void resolveOperations() { + var operators = operators_; if (operators == null) return; while (operators.isNotEmpty) { resolveOneOperation(); @@ -1692,7 +1720,7 @@ relase. For details, see http://bit.ly/moz-document. } void addSingleExpression(Expression expression, {bool number = false}) { - if (singleExpression != null) { + if (singleExpression_ != null) { // If we discover we're parsing a list whose first element is a division // operation, and we're in parentheses, reparse outside of a paren // context. This ensures that `(1/2 1)` doesn't perform division on its @@ -1705,15 +1733,18 @@ relase. For details, see http://bit.ly/moz-document. } } - spaceExpressions ??= []; + var spaceExpressions = spaceExpressions_ ??= []; resolveOperations(); - spaceExpressions.add(singleExpression); + + // [singleExpression_] was non-null before, and [resolveOperations] + // can't make it null, it can only change it. + spaceExpressions.add(singleExpression_!); allowSlash = number; } else if (!number) { allowSlash = false; } - singleExpression = expression; + singleExpression_ = expression; } void addOperator(BinaryOperator operator) { @@ -1727,29 +1758,41 @@ relase. For details, see http://bit.ly/moz-document. allowSlash = allowSlash && operator == BinaryOperator.dividedBy; - operators ??= []; - operands ??= []; + var operators = operators_ ??= []; + var operands = operands_ ??= []; while (operators.isNotEmpty && operators.last.precedence >= operator.precedence) { resolveOneOperation(); } operators.add(operator); + var singleExpression = singleExpression_; + if (singleExpression == null) { + scanner.error("Expected expression.", + position: scanner.position - operator.operator.length, + length: operator.operator.length); + } operands.add(singleExpression); + whitespace(); allowSlash = allowSlash && lookingAtNumber(); - singleExpression = _singleExpression(); - allowSlash = allowSlash && singleExpression is NumberExpression; + singleExpression_ = _singleExpression(); + allowSlash = allowSlash && singleExpression_ is NumberExpression; } void resolveSpaceExpressions() { 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 = null; + singleExpression_ = ListExpression( + spaceExpressions, ListSeparator.space, + span: spaceExpressions.first.span.expand(singleExpression.span)); + spaceExpressions_ = null; } } @@ -1831,7 +1874,7 @@ relase. For details, see http://bit.ly/moz-document. break; case $plus: - if (singleExpression == null) { + if (singleExpression_ == null) { addSingleExpression(_unaryOperation()); } else { scanner.readChar(); @@ -1843,12 +1886,12 @@ relase. For details, see http://bit.ly/moz-document. var next = scanner.peekChar(1); if ((isDigit(next) || next == $dot) && // Make sure `1-2` parses as `1 - 2`, not `1 (-2)`. - (singleExpression == null || + (singleExpression_ == null || isWhitespace(scanner.peekChar(-1)))) { addSingleExpression(_number(), number: true); } else if (_lookingAtInterpolatedIdentifier()) { addSingleExpression(identifierLike()); - } else if (singleExpression == null) { + } else if (singleExpression_ == null) { addSingleExpression(_unaryOperation()); } else { scanner.readChar(); @@ -1857,7 +1900,7 @@ relase. For details, see http://bit.ly/moz-document. break; case $slash: - if (singleExpression == null) { + if (singleExpression_ == null) { addSingleExpression(_unaryOperation()); } else { scanner.readChar(); @@ -1979,14 +2022,18 @@ relase. For details, see http://bit.ly/moz-document. } } - commaExpressions ??= []; - if (singleExpression == null) scanner.error("Expected expression."); + var commaExpressions = commaExpressions_ ??= []; + if (singleExpression_ == null) scanner.error("Expected expression."); resolveSpaceExpressions(); - commaExpressions.add(singleExpression); + + // [resolveSpaceExpressions can modify [singleExpression_], but it + // can't set it to null`. + commaExpressions.add(singleExpression_!); + scanner.readChar(); allowSlash = true; - singleExpression = null; + singleExpression_ = null; break; default: @@ -2000,26 +2047,30 @@ relase. For details, see http://bit.ly/moz-document. } if (bracketList) scanner.expectChar($rbracket); + + var commaExpressions = commaExpressions_; + var spaceExpressions = spaceExpressions_; if (commaExpressions != null) { resolveSpaceExpressions(); _inParentheses = wasInParentheses; + var singleExpression = singleExpression_; if (singleExpression != null) commaExpressions.add(singleExpression); return ListExpression(commaExpressions, ListSeparator.comma, brackets: bracketList, - span: bracketList ? scanner.spanFrom(beforeBracket) : null); + span: scanner.spanFrom(beforeBracket ?? start)); } else if (bracketList && spaceExpressions != null) { resolveOperations(); return ListExpression( - spaceExpressions..add(singleExpression), ListSeparator.space, - brackets: true, span: scanner.spanFrom(beforeBracket)); + spaceExpressions..add(singleExpression_!), ListSeparator.space, + brackets: true, span: scanner.spanFrom(beforeBracket!)); } else { resolveSpaceExpressions(); if (bracketList) { - singleExpression = ListExpression( - [singleExpression], ListSeparator.undecided, - brackets: true, span: scanner.spanFrom(beforeBracket)); + singleExpression_ = ListExpression( + [singleExpression_!], ListSeparator.undecided, + brackets: true, span: scanner.spanFrom(beforeBracket!)); } - return singleExpression; + return singleExpression_!; } } @@ -2072,7 +2123,6 @@ relase. For details, see http://bit.ly/moz-document. } else { return identifierLike(); } - break; case $0: case $1: @@ -2085,7 +2135,6 @@ relase. For details, see http://bit.ly/moz-document. case $8: case $9: return _number(); - break; case $a: case $b: @@ -2140,12 +2189,10 @@ relase. For details, see http://bit.ly/moz-document. case $_: case $backslash: return identifierLike(); - break; default: if (first != null && first >= 0x80) return identifierLike(); scanner.error("Expected expression."); - return null; } } @@ -2347,7 +2394,7 @@ relase. For details, see http://bit.ly/moz-document. /// Returns the unsary operator corresponding to [character], or `null` if /// the character is not a unary operator. - UnaryOperator _unaryOperatorFor(int character) { + UnaryOperator? _unaryOperatorFor(int character) { switch (character) { case $plus: return UnaryOperator.plus; @@ -2375,7 +2422,7 @@ relase. For details, see http://bit.ly/moz-document. number += _tryDecimal(allowTrailingDot: scanner.position != start.position); number *= _tryExponent(); - String unit; + String? unit; if (scanner.scanChar($percent)) { unit = "%"; } else if (lookingAtIdentifier() && @@ -2559,7 +2606,8 @@ relase. For details, see http://bit.ly/moz-document. if (plain != null) { if (plain == "if") { var invocation = _argumentInvocation(); - return IfExpression(invocation, spanForList([identifier, invocation])); + return IfExpression( + invocation, identifier.span.expand(invocation.span)); } else if (plain == "not") { whitespace(); return UnaryOperationExpression( @@ -2627,7 +2675,7 @@ relase. For details, see http://bit.ly/moz-document. /// Otherwise, returns `null`. [start] is the location before the beginning of /// [name]. @protected - Expression trySpecialFunction(String name, LineScannerState start) { + Expression? trySpecialFunction(String name, LineScannerState start) { var normalized = unvendor(name); InterpolationBuffer buffer; @@ -2675,8 +2723,8 @@ relase. For details, see http://bit.ly/moz-document. break; case "url": - var contents = _tryUrlContents(start); - return contents == null ? null : StringExpression(contents); + return _tryUrlContents(start) + .andThen((contents) => StringExpression(contents)); case "clamp": // Vendor-prefixed clamp() functions aren't parsed specially, because @@ -2838,7 +2886,7 @@ relase. For details, see http://bit.ly/moz-document. /// /// [start] is the position before the beginning of the name. [name] is the /// function's name; it defaults to `"url"`. - Interpolation _tryUrlContents(LineScannerState start, {String name}) { + Interpolation? _tryUrlContents(LineScannerState start, {String? name}) { // NOTE: this logic is largely duplicated in Parser.tryUrl. Most changes // here should be mirrored there. @@ -3077,7 +3125,7 @@ relase. For details, see http://bit.ly/moz-document. case $lparen: case $lbrace: case $lbracket: - buffer.writeCharCode(next); + buffer.writeCharCode(next!); // dart-lang/sdk#45357 brackets.add(opposite(scanner.readChar())); wroteNewline = false; break; @@ -3086,7 +3134,7 @@ relase. For details, see http://bit.ly/moz-document. case $rbrace: case $rbracket: if (brackets.isEmpty) break loop; - buffer.writeCharCode(next); + buffer.writeCharCode(next!); // dart-lang/sdk#45357 scanner.expectChar(brackets.removeLast()); wroteNewline = false; break; @@ -3293,17 +3341,20 @@ relase. For details, see http://bit.ly/moz-document. buffer.add(expression()); } else { var next = scanner.peekChar(); - var isAngle = next == $langle || next == $rangle; - if (isAngle || next == $equal) { + if (next == $langle || next == $rangle || next == $equal) { buffer.writeCharCode($space); buffer.writeCharCode(scanner.readChar()); - if (isAngle && scanner.scanChar($equal)) buffer.writeCharCode($equal); + if ((next == $langle || next == $rangle) && scanner.scanChar($equal)) { + buffer.writeCharCode($equal); + } buffer.writeCharCode($space); whitespace(); buffer.add(_expressionUntilComparison()); - if (isAngle && scanner.scanChar(next)) { + if ((next == $langle || next == $rangle) && + // dart-lang/sdk#45356 + scanner.scanChar(next!)) { buffer.writeCharCode($space); buffer.writeCharCode(next); if (scanner.scanChar($equal)) buffer.writeCharCode($equal); @@ -3343,7 +3394,7 @@ relase. For details, see http://bit.ly/moz-document. var condition = _supportsConditionInParens(); whitespace(); - String operator; + String? operator; while (lookingAtIdentifier()) { if (operator != null) { expectIdentifier(operator); @@ -3455,7 +3506,7 @@ relase. For details, see http://bit.ly/moz-document. /// If [interpolation] is followed by `"and"` or `"or"`, parse it as a supports operation. /// /// Otherwise, return `null` without moving the scanner position. - SupportsOperation _trySupportsOperation( + SupportsOperation? _trySupportsOperation( Interpolation interpolation, LineScannerState start) { if (interpolation.contents.length != 1) return null; var expression = interpolation.contents.first; @@ -3464,8 +3515,8 @@ relase. For details, see http://bit.ly/moz-document. var beforeWhitespace = scanner.state; whitespace(); - SupportsOperation operation; - String operator; + SupportsOperation? operation; + String? operator; while (lookingAtIdentifier()) { if (operator != null) { expectIdentifier(operator); @@ -3481,9 +3532,7 @@ relase. For details, see http://bit.ly/moz-document. whitespace(); var right = _supportsConditionInParens(); operation = SupportsOperation( - operation ?? - SupportsInterpolation( - expression as Expression, interpolation.span), + operation ?? SupportsInterpolation(expression, interpolation.span), right, operator, scanner.spanFrom(start)); @@ -3622,7 +3671,7 @@ relase. For details, see http://bit.ly/moz-document. /// /// This consumes whitespace, but nothing else, including comments. @protected - void expectStatementSeparator([String name]); + void expectStatementSeparator([String? name]); /// Whether the scanner is positioned at the end of a statement. @protected @@ -3655,5 +3704,5 @@ relase. For details, see http://bit.ly/moz-document. /// The [statement] callback may return `null`, indicating that a statement /// was consumed that shouldn't be added to the AST. @protected - List statements(Statement statement()); + List statements(Statement? statement()); } diff --git a/lib/src/stylesheet_graph.dart b/lib/src/stylesheet_graph.dart index 02e8f7481..9d9e7ca03 100644 --- a/lib/src/stylesheet_graph.dart +++ b/lib/src/stylesheet_graph.dart @@ -3,13 +3,13 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; -import 'package:meta/meta.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/nullable.dart'; import 'visitor/find_dependencies.dart'; /// A graph of the import relationships between stylesheets, available via @@ -36,7 +36,7 @@ class StylesheetGraph { /// /// Returns `true` if the import cache can't find a stylesheet at [url]. bool modifiedSince(Uri url, DateTime since, - [Importer baseImporter, Uri baseUrl]) { + [Importer? baseImporter, Uri? baseUrl]) { DateTime transitiveModificationTime(StylesheetNode node) { return _transitiveModificationTimes.putIfAbsent(node.canonicalUrl, () { var latest = node.importer.modificationTime(node.canonicalUrl); @@ -65,7 +65,7 @@ class StylesheetGraph { /// import [url] (resolved relative to [baseUrl] if it's passed). /// /// Returns `null` if the import cache can't find a stylesheet at [url]. - StylesheetNode _add(Uri url, [Importer baseImporter, Uri baseUrl]) { + StylesheetNode? _add(Uri url, [Importer? baseImporter, Uri? baseUrl]) { var tuple = _ignoreErrors(() => importCache.canonicalize(url, baseImporter: baseImporter, baseUrl: baseUrl)); if (tuple == null) return null; @@ -113,7 +113,7 @@ class StylesheetGraph { /// /// The first map contains stylesheets depended on via `@use` and `@forward` /// while the second map contains those depended on via `@import`. - Tuple2, Map> _upstreamNodes( + Tuple2, Map> _upstreamNodes( Stylesheet stylesheet, Importer baseImporter, Uri baseUrl) { var active = {baseUrl}; var tuple = findDependencies(stylesheet); @@ -221,18 +221,18 @@ class StylesheetGraph { /// If [forImport] is `true`, this re-runs canonicalization for /// [node.upstreamImports]. Otherwise, it re-runs canonicalization for /// [node.upstream]. - Map _recanonicalizeImportsForNode( + Map _recanonicalizeImportsForNode( StylesheetNode node, Importer importer, Uri canonicalUrl, - {@required bool forImport}) { + {required bool forImport}) { var map = forImport ? node.upstreamImports : node.upstream; - var newMap = {}; + var newMap = {}; map.forEach((url, upstream) { if (!importer.couldCanonicalize(url, canonicalUrl)) return; 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; + Tuple3? result; try { result = importCache.canonicalize(url, baseImporter: node.importer, @@ -257,7 +257,7 @@ class StylesheetGraph { /// /// The [active] set should contain the canonical URLs that are currently /// being imported. It's used to detect circular imports. - StylesheetNode _nodeFor( + StylesheetNode? _nodeFor( Uri url, Importer baseImporter, Uri baseUrl, Set active, {bool forImport = false}) { var tuple = _ignoreErrors(() => importCache.canonicalize(url, @@ -295,7 +295,7 @@ class StylesheetGraph { /// If [callback] throws any errors, ignores them and returns `null`. This is /// used to wrap calls to the import cache, since importer errors should be /// surfaced by the compilation process rather than the graph. - T _ignoreErrors(T callback()) { + T? _ignoreErrors(T callback()) { try { return callback(); } catch (_) { @@ -325,23 +325,23 @@ class StylesheetNode { /// the stylesheets those rules refer to. /// /// This may have `null` values, which indicate failed loads. - Map get upstream => UnmodifiableMapView(_upstream); - Map _upstream; + Map get upstream => UnmodifiableMapView(_upstream); + Map _upstream; /// A map from non-canonicalized `@import` URLs in [stylesheet] to the /// stylesheets those imports refer to. /// /// This may have `null` values, which indicate failed imports. - Map get upstreamImports => + Map get upstreamImports => UnmodifiableMapView(_upstreamImports); - Map _upstreamImports; + Map _upstreamImports; /// The stylesheets that import [stylesheet]. Set get downstream => UnmodifiableSetView(_downstream); final _downstream = {}; StylesheetNode._(this._stylesheet, this.importer, this.canonicalUrl, - Tuple2, Map> allUpstream) + Tuple2, Map> allUpstream) : _upstream = allUpstream.item1, _upstreamImports = allUpstream.item2 { for (var node in upstream.values.followedBy(upstreamImports.values)) { @@ -352,12 +352,12 @@ class StylesheetNode { /// Updates [upstream] and [upstreamImports] from [newUpstream] and /// [newUpstreamImports] and adjusts upstream nodes' [downstream] fields /// accordingly. - void _replaceUpstream(Map newUpstream, - Map newUpstreamImports) { - var oldUpstream = {...upstream.values, ...upstreamImports.values} - ..remove(null); - var newUpstreamSet = {...newUpstream.values, ...newUpstreamImports.values} - ..remove(null); + void _replaceUpstream(Map newUpstream, + Map newUpstreamImports) { + var oldUpstream = + {...upstream.values, ...upstreamImports.values}.removeNull(); + var newUpstreamSet = + {...newUpstream.values, ...newUpstreamImports.values}.removeNull(); for (var removed in oldUpstream.difference(newUpstreamSet)) { var wasRemoved = removed._downstream.remove(this); @@ -398,5 +398,6 @@ class StylesheetNode { } } - String toString() => p.prettyUri(stylesheet.span.sourceUrl); + String toString() => + stylesheet.span.sourceUrl.andThen(p.prettyUri) ?? ''; } diff --git a/lib/src/sync_package_resolver.dart b/lib/src/sync_package_resolver.dart deleted file mode 100644 index f61ae7be2..000000000 --- a/lib/src/sync_package_resolver.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'package:package_resolver/package_resolver.dart' - if (dart.library.js) 'sync_package_resolver/node.dart'; diff --git a/lib/src/sync_package_resolver/node.dart b/lib/src/sync_package_resolver/node.dart deleted file mode 100644 index c0984dced..000000000 --- a/lib/src/sync_package_resolver/node.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -class SyncPackageResolver { - static final _error = - UnsupportedError('SyncPackageResolver is not supported in JS.'); - - static Future get current => throw _error; - - Uri resolveUri(Object packageUri) => throw _error; - - factory SyncPackageResolver.config(Map configMap) => - throw _error; -} diff --git a/lib/src/util/character.dart b/lib/src/util/character.dart index 13d2e937d..5bf423740 100644 --- a/lib/src/util/character.dart +++ b/lib/src/util/character.dart @@ -11,15 +11,15 @@ import 'package:charcode/charcode.dart'; const _asciiCaseBit = 0x20; /// Returns whether [character] is an ASCII whitespace character. -bool isWhitespace(int character) => +bool isWhitespace(int? character) => isSpaceOrTab(character) || isNewline(character); /// Returns whether [character] is an ASCII newline. -bool isNewline(int character) => +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; +bool isSpaceOrTab(int? character) => character == $space || character == $tab; /// Returns whether [character] is a letter or number. bool isAlphanumeric(int character) => @@ -31,7 +31,7 @@ bool isAlphabetic(int character) => (character >= $A && character <= $Z); /// Returns whether [character] is a number. -bool isDigit(int character) => +bool isDigit(int? character) => character != null && character >= $0 && character <= $9; /// Returns whether [character] is legal as the start of a Sass identifier. @@ -43,7 +43,7 @@ bool isName(int character) => isNameStart(character) || isDigit(character) || character == $minus; /// Returns whether [character] is a hexadeicmal digit. -bool isHex(int character) { +bool isHex(int? character) { if (character == null) return false; if (isDigit(character)) return true; if (character >= $a && character <= $f) return true; @@ -57,7 +57,7 @@ bool isHighSurrogate(int character) => // Returns whether [character] can start a simple selector other than a type // selector. -bool isSimpleSelectorStart(int character) => +bool isSimpleSelectorStart(int? character) => character == $asterisk || character == $lbracket || character == $dot || @@ -118,7 +118,8 @@ int opposite(int character) { case $lbracket: return $rbracket; default: - return null; + throw ArgumentError( + '"${String.fromCharCode(character)}" isn\'t a brace-like character.'); } } diff --git a/lib/src/util/fixed_length_list_builder.dart b/lib/src/util/fixed_length_list_builder.dart deleted file mode 100644 index 880f8ac24..000000000 --- a/lib/src/util/fixed_length_list_builder.dart +++ /dev/null @@ -1,67 +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. - -/// A class for efficiently adding elements to a list whose final length is -/// known in advance. -class FixedLengthListBuilder { - /// The list to which elements are being added. - final List _list; - - /// The index at which to add the next element to the list. - /// - /// This is set to -1 once the list has been returned. - var _index = 0; - - /// Creates a new builder that creates a list of length [length]. - FixedLengthListBuilder(int length) : _list = List(length); - - /// Adds [element] to the next available space in the list. - /// - /// This may only be called if [build] has not yet been called, and if the - /// list is not yet full. - void add(T element) { - _checkUnbuilt(); - _list[_index] = element; - _index++; - } - - /// Adds all elements in [elements] to the next available spaces in the list. - /// - /// This may only be called if [build] has not yet been called, and if the - /// list has room for all of [elements]. - void addAll(Iterable elements) { - _checkUnbuilt(); - _list.setAll(_index, elements); - _index += elements.length; - } - - /// Adds the elements from [start] (inclusive) to [end] (exclusive) of - /// [elements] to the next available spaces in the list. - /// - /// The [end] defaults to `elements.length`. - /// - /// This may only be called if [build] has not yet been called, and if the - /// list has room for all the elements to add. - void addRange(Iterable elements, int start, [int end]) { - _checkUnbuilt(); - var length = (end ?? elements.length) - start; - _list.setRange(_index, _index + length, elements, start); - _index += length; - } - - /// Returns the mutable, fixed-length built list. - /// - /// Any spaces in the list that haven't had elements added explicitly will be - /// `null`. This may only be called once. - List build() { - _checkUnbuilt(); - _index = -1; - return _list; - } - - /// Throws a [StateError] if [build] has been called already. - void _checkUnbuilt() { - if (_index == -1) throw StateError("build() has already been called."); - } -} diff --git a/lib/src/util/limited_map_view.dart b/lib/src/util/limited_map_view.dart index 308a65862..754e5c9f8 100644 --- a/lib/src/util/limited_map_view.dart +++ b/lib/src/util/limited_map_view.dart @@ -44,8 +44,8 @@ class LimitedMapView extends UnmodifiableMapBase { if (!blocklist.contains(key)) key }; - V operator [](Object key) => _keys.contains(key) ? _map[key] : null; - bool containsKey(Object key) => _keys.contains(key); + V? operator [](Object? key) => _keys.contains(key) ? _map[key] : null; + bool containsKey(Object? key) => _keys.contains(key); - V remove(Object key) => _keys.contains(key) ? _map.remove(key) : null; + V? remove(Object? key) => _keys.contains(key) ? _map.remove(key) : null; } diff --git a/lib/src/util/merged_map_view.dart b/lib/src/util/merged_map_view.dart index 9dbb2d262..df749871f 100644 --- a/lib/src/util/merged_map_view.dart +++ b/lib/src/util/merged_map_view.dart @@ -44,10 +44,7 @@ class MergedMapView extends MapBase { } } - V operator [](Object key) { - var child = _mapsByKey[key]; - return child == null ? null : child[key]; - } + V? operator [](Object? key) => _mapsByKey[key as K]?[key]; operator []=(K key, V value) { var child = _mapsByKey[key]; @@ -58,7 +55,7 @@ class MergedMapView extends MapBase { child[key] = value; } - V remove(Object key) { + V? remove(Object? key) { throw UnsupportedError("Entries may not be removed from MergedMapView."); } @@ -66,5 +63,5 @@ class MergedMapView extends MapBase { throw UnsupportedError("Entries may not be removed from MergedMapView."); } - bool containsKey(Object key) => _mapsByKey.containsKey(key); + bool containsKey(Object? key) => _mapsByKey.containsKey(key); } diff --git a/lib/src/util/multi_dir_watcher.dart b/lib/src/util/multi_dir_watcher.dart index 19b157a01..9543a1c08 100644 --- a/lib/src/util/multi_dir_watcher.dart +++ b/lib/src/util/multi_dir_watcher.dart @@ -16,7 +16,7 @@ class MultiDirWatcher { /// A map from paths to the event streams for those paths. /// /// No key in this map is a parent directories of any other key in this map. - final _watchers = >{}; + final _watchers = p.PathMap>(); /// The stream of events from all directories that are being watched. Stream get events => _group.stream; @@ -37,7 +37,10 @@ class MultiDirWatcher { /// from [directory]. Future watch(String directory) { var isParentOfExistingDir = false; - for (var existingDir in _watchers.keys.toList()) { + for (var entry in _watchers.entries.toList()) { + var existingDir = entry.key!; // dart-lang/path#100 + var existingWatcher = entry.value; + if (!isParentOfExistingDir && (p.equals(existingDir, directory) || p.isWithin(existingDir, directory))) { @@ -45,7 +48,8 @@ class MultiDirWatcher { } if (p.isWithin(directory, existingDir)) { - _group.remove(_watchers.remove(existingDir)); + _watchers.remove(existingDir); + _group.remove(existingWatcher); isParentOfExistingDir = true; } } diff --git a/lib/src/util/no_source_map_buffer.dart b/lib/src/util/no_source_map_buffer.dart index 7e955ee7f..0e2bd257d 100644 --- a/lib/src/util/no_source_map_buffer.dart +++ b/lib/src/util/no_source_map_buffer.dart @@ -18,16 +18,16 @@ class NoSourceMapBuffer implements SourceMapBuffer { Map get sourceFiles => const {}; T forSpan(SourceSpan span, T callback()) => callback(); - void write(Object object) => _buffer.write(object); - void writeAll(Iterable objects, [String separator = ""]) => + void write(Object? object) => _buffer.write(object); + void writeAll(Iterable objects, [String separator = ""]) => _buffer.writeAll(objects, separator); void writeCharCode(int charCode) => _buffer.writeCharCode(charCode); - void writeln([Object object = ""]) => _buffer.writeln(object); + void writeln([Object? object = ""]) => _buffer.writeln(object); String toString() => _buffer.toString(); void clear() => throw UnsupportedError("SourceMapBuffer.clear() is not supported."); - SingleMapping buildSourceMap({String prefix}) => throw UnsupportedError( + SingleMapping buildSourceMap({String? prefix}) => throw UnsupportedError( "NoSourceMapBuffer.buildSourceMap() is not supported."); } diff --git a/lib/src/util/nullable.dart b/lib/src/util/nullable.dart new file mode 100644 index 000000000..f5bf6258c --- /dev/null +++ b/lib/src/util/nullable.dart @@ -0,0 +1,23 @@ +// Copyright 2020 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 NullableExtension on T? { + /// If [this] is `null`, returns `null`. Otherwise, runs [fn] and returns its + /// result. + /// + /// Based on Rust's `Option.and_then`. + V? andThen(V Function(T value)? fn) { + var self = this; // dart-lang/language#1520 + return self == null ? null : fn!(self); + } +} + +extension SetExtension on Set { + /// Destructively removes the `null` element from this set, if it exists, and + /// returns a view of it casted to a non-nullable type. + Set removeNull() { + remove(null); + return cast(); + } +} diff --git a/lib/src/util/number.dart b/lib/src/util/number.dart index f2f341f90..c47ab44da 100644 --- a/lib/src/util/number.dart +++ b/lib/src/util/number.dart @@ -54,7 +54,7 @@ bool fuzzyIsInt(num number) { /// [int]. /// /// Otherwise, returns `null`. -int fuzzyAsInt(num number) => fuzzyIsInt(number) ? number.round() : null; +int? fuzzyAsInt(num number) => fuzzyIsInt(number) ? number.round() : null; /// Rounds [number] to the nearest integer. /// @@ -75,7 +75,7 @@ int fuzzyRound(num number) { /// /// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the /// appropriate value. -num fuzzyCheckRange(num number, num min, num max) { +num? fuzzyCheckRange(num number, num min, num max) { if (fuzzyEquals(number, min)) return min; if (fuzzyEquals(number, max)) return max; if (number > min && number < max) return number; @@ -86,7 +86,7 @@ num fuzzyCheckRange(num number, num min, num max) { /// /// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the /// appropriate value. [name] is used in error reporting. -num fuzzyAssertRange(num number, int min, int max, [String name]) { +num fuzzyAssertRange(num number, int min, int max, [String? name]) { var result = fuzzyCheckRange(number, min, max); if (result != null) return result; throw RangeError.range( diff --git a/lib/src/util/prefixed_map_view.dart b/lib/src/util/prefixed_map_view.dart index 29c0b3565..6f923db87 100644 --- a/lib/src/util/prefixed_map_view.dart +++ b/lib/src/util/prefixed_map_view.dart @@ -21,11 +21,11 @@ class PrefixedMapView extends UnmodifiableMapBase { /// Creates a new prefixed map view. PrefixedMapView(this._map, this._prefix); - V operator [](Object key) => key is String && key.startsWith(_prefix) + V? operator [](Object? key) => key is String && key.startsWith(_prefix) ? _map[key.substring(_prefix.length)] : null; - bool containsKey(Object key) => key is String && key.startsWith(_prefix) + bool containsKey(Object? key) => key is String && key.startsWith(_prefix) ? _map.containsKey(key.substring(_prefix.length)) : false; } @@ -33,7 +33,7 @@ class PrefixedMapView extends UnmodifiableMapBase { /// The implementation of [PrefixedMapViews.keys]. class _PrefixedKeys extends IterableBase { /// The view whose keys are being iterated over. - final PrefixedMapView _view; + final PrefixedMapView _view; int get length => _view.length; Iterator get iterator => @@ -41,5 +41,5 @@ class _PrefixedKeys extends IterableBase { _PrefixedKeys(this._view); - bool contains(Object key) => _view.containsKey(key); + bool contains(Object? key) => _view.containsKey(key); } diff --git a/lib/src/util/public_member_map_view.dart b/lib/src/util/public_member_map_view.dart index 4ab9c190d..e8110ed34 100644 --- a/lib/src/util/public_member_map_view.dart +++ b/lib/src/util/public_member_map_view.dart @@ -18,10 +18,10 @@ class PublicMemberMapView extends UnmodifiableMapBase { PublicMemberMapView(this._inner); - bool containsKey(Object key) => + bool containsKey(Object? key) => key is String && isPublic(key) && _inner.containsKey(key); - V operator [](Object key) { + V? operator [](Object? key) { if (key is String && isPublic(key)) return _inner[key]; return null; } diff --git a/lib/src/util/source_map_buffer.dart b/lib/src/util/source_map_buffer.dart index 7907051d6..282042351 100644 --- a/lib/src/util/source_map_buffer.dart +++ b/lib/src/util/source_map_buffer.dart @@ -24,7 +24,7 @@ class SourceMapBuffer implements StringBuffer { for (var entry in _sourceFiles.entries) entry.key.toString(): entry.value }); - final _sourceFiles = {}; + final _sourceFiles = {}; /// The index of the current line in [_buffer]. var _line = 0; @@ -91,7 +91,7 @@ class SourceMapBuffer implements StringBuffer { void clear() => throw UnsupportedError("SourceMapBuffer.clear() is not supported."); - void write(Object object) { + void write(Object? object) { var string = object.toString(); _buffer.write(string); @@ -104,7 +104,7 @@ class SourceMapBuffer implements StringBuffer { } } - void writeAll(Iterable objects, [String separator = ""]) => + void writeAll(Iterable objects, [String separator = ""]) => write(objects.join(separator)); void writeCharCode(int charCode) { @@ -116,7 +116,7 @@ class SourceMapBuffer implements StringBuffer { } } - void writeln([Object object = ""]) { + void writeln([Object? object = ""]) { // Special-case the common case. if (identical(object, "")) { _buffer.writeln(); @@ -160,7 +160,7 @@ class SourceMapBuffer implements StringBuffer { /// forward by the number of characters and lines in [prefix]. /// /// [SingleMapping.targetUrl] will be `null`. - SingleMapping buildSourceMap({String prefix}) { + SingleMapping buildSourceMap({String? prefix}) { if (prefix == null || prefix.isEmpty) { return SingleMapping.fromEntries(_entries); } diff --git a/lib/src/util/unprefixed_map_view.dart b/lib/src/util/unprefixed_map_view.dart index e11de4532..708e0e22b 100644 --- a/lib/src/util/unprefixed_map_view.dart +++ b/lib/src/util/unprefixed_map_view.dart @@ -25,18 +25,18 @@ class UnprefixedMapView extends UnmodifiableMapBase { /// Creates a new unprefixed map view. UnprefixedMapView(this._map, this._prefix); - V operator [](Object key) => key is String ? _map[_prefix + key] : null; + V? operator [](Object? key) => key is String ? _map[_prefix + key] : null; - bool containsKey(Object key) => + bool containsKey(Object? key) => key is String ? _map.containsKey(_prefix + key) : false; - V remove(Object key) => key is String ? _map.remove(_prefix + key) : null; + V? remove(Object? key) => key is String ? _map.remove(_prefix + key) : null; } /// The implementation of [UnprefixedMapViews.keys]. class _UnprefixedKeys extends IterableBase { /// The view whose keys are being iterated over. - final UnprefixedMapView _view; + final UnprefixedMapView _view; Iterator get iterator => _view._map.keys .where((key) => key.startsWith(_view._prefix)) @@ -45,5 +45,5 @@ class _UnprefixedKeys extends IterableBase { _UnprefixedKeys(this._view); - bool contains(Object key) => _view.containsKey(key); + bool contains(Object? key) => _view.containsKey(key); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index cb3a34479..3781fe1ff 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -10,14 +10,13 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:term_glyph/term_glyph.dart' as glyph; -import 'ast/node.dart'; import 'util/character.dart'; /// The URL used in stack traces when no source URL is available. final _noSourceUrl = Uri.parse("-"); /// Converts [iter] into a sentence, separating each word with [conjunction]. -String toSentence(Iterable iter, [String conjunction]) { +String toSentence(Iterable iter, [String? conjunction]) { conjunction ??= "and"; if (iter.length == 1) return iter.first.toString(); return iter.take(iter.length - 1).join(", ") + " $conjunction ${iter.last}"; @@ -31,7 +30,7 @@ String indent(String string, int indentation) => /// /// By default, this just adds "s" to the end of [name] to get the plural. If /// [plural] is passed, that's used instead. -String pluralize(String name, int number, {String plural}) { +String pluralize(String name, int number, {String? plural}) { if (number == 1) return name; if (plural != null) return plural; return '${name}s'; @@ -69,7 +68,7 @@ String trimAscii(String string, {bool excludeEscape = false}) { return start == null ? "" : string.substring( - start, _lastNonWhitespace(string, excludeEscape: excludeEscape) + 1); + start, _lastNonWhitespace(string, excludeEscape: excludeEscape)! + 1); } /// Like [String.trimLeft], but only trims ASCII whitespace. @@ -89,7 +88,7 @@ String trimAsciiRight(String string, {bool excludeEscape = false}) { /// Returns the index of the first character in [string] that's not ASCII /// whitespace, or [null] if [string] is entirely spaces. -int _firstNonWhitespace(String string) { +int? _firstNonWhitespace(String string) { for (var i = 0; i < string.length; i++) { if (!isWhitespace(string.codeUnitAt(i))) return i; } @@ -101,7 +100,7 @@ int _firstNonWhitespace(String string) { /// /// If [excludeEscape] is `true`, this doesn't move past whitespace that's /// included in a CSS escape. -int _lastNonWhitespace(String string, {bool excludeEscape = false}) { +int? _lastNonWhitespace(String string, {bool excludeEscape = false}) { for (var i = string.length - 1; i >= 0; i--) { var codeUnit = string.codeUnitAt(i); if (!isWhitespace(codeUnit)) { @@ -147,7 +146,8 @@ List flattenVertically(Iterable> iterable) { } /// Returns the first element of [iterable], or `null` if the iterable is empty. -T firstOrNull(Iterable iterable) { +// TODO(nweiz): Use package:collection +T? firstOrNull(Iterable iterable) { var iterator = iterable.iterator; return iterator.moveNext() ? iterator.current : null; } @@ -186,8 +186,8 @@ int iterableHash(Iterable iterable) => const IterableEquality().hash(iterable); /// Returns whether [list1] and [list2] have the same contents. -bool listEquals(List list1, List list2) => - const ListEquality().equals(list1, list2); +bool listEquals(List? list1, List? list2) => + const ListEquality().equals(list1, list2); /// Returns a hash code for [list] that matches [listEquals]. int listHash(List list) => const ListEquality().hash(list); @@ -204,30 +204,12 @@ int mapHash(Map map) => /// /// By default, the frame's URL is set to `span.sourceUrl`. However, if [url] is /// passed, it's used instead. -Frame frameForSpan(SourceSpan span, String member, {Uri url}) => Frame( +Frame frameForSpan(SourceSpan span, String member, {Uri? url}) => Frame( url ?? span.sourceUrl ?? _noSourceUrl, span.start.line + 1, span.start.column + 1, member); -/// Returns a source span that covers the spans of both the first and last nodes -/// in [nodes]. -/// -/// If [nodes] is empty, or if either the first or last node has a `null` span, -/// returns `null`. -FileSpan spanForList(List nodes) { - if (nodes.isEmpty) return null; - - // Spans may be null for dynamically-constructed ASTs. - var left = nodes.first?.span; - if (left == null) return null; - - var right = nodes.last?.span; - if (right == null) return null; - - return left.expand(right); -} - /// Returns the variable name (including the leading `$`) from a [span] that /// covers a variable declaration, which includes the variable name as well as /// the colon and expression following it. @@ -254,7 +236,7 @@ String unvendor(String name) { } /// Returns whether [string1] and [string2] are equal, ignoring ASCII case. -bool equalsIgnoreCase(String string1, String string2) { +bool equalsIgnoreCase(String? string1, String? string2) { if (identical(string1, string2)) return true; if (string1 == null || string2 == null) return false; if (string1.length != string2.length) return false; @@ -296,15 +278,15 @@ void mapInPlace(List list, T function(T element)) { /// list. If it returns `null`, the elements are considered unequal; otherwise, /// it should return the element to include in the return value. List longestCommonSubsequence(List list1, List list2, - {T select(T element1, T element2)}) { + {T? select(T element1, T element2)?}) { select ??= (element1, element2) => element1 == element2 ? element1 : null; var lengths = List.generate( list1.length + 1, (_) => List.filled(list2.length + 1, 0), growable: false); - var selections = List>.generate( - list1.length, (_) => List(list2.length), + var selections = List>.generate( + list1.length, (_) => List.filled(list2.length, null), growable: false); for (var i = 0; i < list1.length; i++) { @@ -330,25 +312,17 @@ List longestCommonSubsequence(List list1, List list2, return backtrack(list1.length - 1, list2.length - 1); } -/// Removes and returns the first value in [list] that matches [test]. +/// Removes the first value in [list] that matches [test]. /// -/// By default, throws a [StateError] if no value matches. If [orElse] is -/// passed, its return value is used instead. -T removeFirstWhere(List list, bool test(T value), {T orElse()}) { - T toRemove; - for (var element in list) { - if (!test(element)) continue; - toRemove = element; - break; +/// If [orElse] is passed, calls it if no value matches. +void removeFirstWhere(List list, bool test(T value), {void orElse()?}) { + for (var i = 0; i < list.length; i++) { + if (!test(list[i])) continue; + list.removeAt(i); + return; } - if (toRemove == null) { - if (orElse != null) return orElse(); - throw StateError("No such element."); - } else { - list.remove(toRemove); - return toRemove; - } + if (orElse != null) orElse(); } /// Like [Map.addAll], but for two-layer maps. @@ -357,8 +331,9 @@ T removeFirstWhere(List list, bool test(T value), {T orElse()}) { void mapAddAll2( Map> destination, Map> source) { source.forEach((key, inner) { - if (destination.containsKey(key)) { - destination[key].addAll(inner); + var innerDestination = destination[key]; + if (innerDestination != null) { + innerDestination.addAll(inner); } else { destination[key] = inner; } @@ -394,7 +369,7 @@ Future> mapAsync( /// same key. Future putIfAbsentAsync( Map map, K key, Future ifAbsent()) async { - if (map.containsKey(key)) return map[key]; + if (map.containsKey(key)) return map[key]!; var value = await ifAbsent(); map[key] = value; return value; diff --git a/lib/src/value.dart b/lib/src/value.dart index 1bd532caf..cff8293ce 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -64,7 +64,7 @@ abstract class Value implements ext.Value { /// Returns Dart's `null` value if this is [sassNull], and returns [this] /// otherwise. - Value get realNull => this; + Value? get realNull => this; const Value(); @@ -74,36 +74,36 @@ abstract class Value implements ext.Value { /// It's not guaranteed to be stable across versions. T accept(ValueVisitor visitor); - int sassIndexToListIndex(ext.Value sassIndex, [String name]) { + int sassIndexToListIndex(ext.Value sassIndex, [String? name]) { var index = sassIndex.assertNumber(name).assertInt(name); if (index == 0) throw _exception("List index may not be 0.", name); if (index.abs() > lengthAsList) { throw _exception( - "Invalid index $sassIndex for a list with ${lengthAsList} elements.", + "Invalid index $sassIndex for a list with $lengthAsList elements.", name); } return index < 0 ? lengthAsList + index : index - 1; } - SassBoolean assertBoolean([String name]) => + SassBoolean assertBoolean([String? name]) => throw _exception("$this is not a boolean.", name); - SassColor assertColor([String name]) => + SassColor assertColor([String? name]) => throw _exception("$this is not a color.", name); - SassFunction assertFunction([String name]) => + SassFunction assertFunction([String? name]) => throw _exception("$this is not a function reference.", name); - SassMap assertMap([String name]) => + SassMap assertMap([String? name]) => throw _exception("$this is not a map.", name); - SassMap tryMap() => null; + SassMap? tryMap() => null; - SassNumber assertNumber([String name]) => + SassNumber assertNumber([String? name]) => throw _exception("$this is not a number.", name); - SassString assertString([String name]) => + SassString assertString([String? name]) => throw _exception("$this is not a string.", name); /// Parses [this] as a selector list, in the same manner as the @@ -115,7 +115,7 @@ abstract class Value implements ext.Value { /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SelectorList assertSelector({String name, bool allowParent = false}) { + SelectorList assertSelector({String? name, bool allowParent = false}) { var string = _selectorString(name); try { return SelectorList.parse(string, allowParent: allowParent); @@ -135,7 +135,8 @@ abstract class Value implements ext.Value { /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SimpleSelector assertSimpleSelector({String name, bool allowParent = false}) { + SimpleSelector assertSimpleSelector( + {String? name, bool allowParent = false}) { var string = _selectorString(name); try { return SimpleSelector.parse(string, allowParent: allowParent); @@ -156,7 +157,7 @@ abstract class Value implements ext.Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. CompoundSelector assertCompoundSelector( - {String name, bool allowParent = false}) { + {String? name, bool allowParent = false}) { var string = _selectorString(name); try { return CompoundSelector.parse(string, allowParent: allowParent); @@ -172,7 +173,7 @@ abstract class Value implements ext.Value { /// /// Throws a [SassScriptException] if [this] isn't a type or a structure that /// can be parsed as a selector. - String _selectorString([String name]) { + String _selectorString([String? name]) { var string = _selectorStringOrNull(); if (string != null) return string; @@ -187,7 +188,7 @@ abstract class Value implements ext.Value { /// /// Returns `null` if [this] isn't a type or a structure that can be parsed as /// a selector. - String _selectorStringOrNull() { + String? _selectorStringOrNull() { if (this is SassString) return (this as SassString).text; if (this is! SassList) return null; var list = this as SassList; @@ -200,7 +201,7 @@ abstract class Value implements ext.Value { result.add(complex.text); } else if (complex is SassList && complex.separator == ListSeparator.space) { - var string = complex._selectorString(); + var string = complex._selectorStringOrNull(); if (string == null) return null; result.add(string); } else { @@ -222,7 +223,7 @@ abstract class Value implements ext.Value { /// Returns a new list containing [contents] that defaults to this value's /// separator and brackets. SassList changeListContents(Iterable contents, - {ListSeparator separator, bool brackets}) { + {ListSeparator? separator, bool? brackets}) { return SassList(contents, separator ?? this.separator, brackets: brackets ?? hasBrackets); } @@ -346,6 +347,6 @@ abstract class Value implements ext.Value { String toString() => serializeValue(this, inspect: true); /// Throws a [SassScriptException] with the given [message]. - SassScriptException _exception(String message, [String name]) => + SassScriptException _exception(String message, [String? name]) => SassScriptException(name == null ? message : "\$$name: $message"); } diff --git a/lib/src/value/boolean.dart b/lib/src/value/boolean.dart index cf2baa14a..8b755428f 100644 --- a/lib/src/value/boolean.dart +++ b/lib/src/value/boolean.dart @@ -23,7 +23,7 @@ class SassBoolean extends Value implements ext.SassBoolean { T accept(ValueVisitor visitor) => visitor.visitBoolean(this); - SassBoolean assertBoolean([String name]) => this; + SassBoolean assertBoolean([String? name]) => this; Value unaryNot() => value ? sassFalse : sassTrue; } diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index c815954f8..686b4e9ec 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -15,45 +15,45 @@ import 'external/value.dart' as ext; class SassColor extends Value implements ext.SassColor { int get red { if (_red == null) _hslToRgb(); - return _red; + return _red!; } - int _red; + int? _red; int get green { if (_green == null) _hslToRgb(); - return _green; + return _green!; } - int _green; + int? _green; int get blue { if (_blue == null) _hslToRgb(); - return _blue; + return _blue!; } - int _blue; + int? _blue; num get hue { if (_hue == null) _rgbToHsl(); - return _hue; + return _hue!; } - num _hue; + num? _hue; num get saturation { if (_saturation == null) _rgbToHsl(); - return _saturation; + return _saturation!; } - num _saturation; + num? _saturation; num get lightness { if (_lightness == null) _rgbToHsl(); - return _lightness; + return _lightness!; } - num _lightness; + num? _lightness; num get whiteness { // Because HWB is (currently) used much less frequently than HSL or RGB, we @@ -73,29 +73,29 @@ class SassColor extends Value implements ext.SassColor { /// The original string representation of this color, or `null` if one is /// unavailable. - String get original => originalSpan?.text; + String? get original => originalSpan?.text; /// The span tracking the location in which this color was originally defined. /// /// This is tracked as a span to avoid extra substring allocations. - final FileSpan originalSpan; + final FileSpan? originalSpan; SassColor.rgb(this._red, this._green, this._blue, - [num alpha, this.originalSpan]) + [num? alpha, this.originalSpan]) : alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha") { RangeError.checkValueInInterval(red, 0, 255, "red"); RangeError.checkValueInInterval(green, 0, 255, "green"); RangeError.checkValueInInterval(blue, 0, 255, "blue"); } - SassColor.hsl(num hue, num saturation, num lightness, [num alpha]) + SassColor.hsl(num hue, num saturation, num lightness, [num? alpha]) : _hue = hue % 360, _saturation = fuzzyAssertRange(saturation, 0, 100, "saturation"), _lightness = fuzzyAssertRange(lightness, 0, 100, "lightness"), alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha"), originalSpan = null; - factory SassColor.hwb(num hue, num whiteness, num blackness, [num alpha]) { + factory SassColor.hwb(num hue, num whiteness, num blackness, [num? alpha]) { // From https://www.w3.org/TR/css-color-4/#hwb-to-rgb var scaledHue = hue % 360 / 360; var scaledWhiteness = @@ -129,17 +129,18 @@ class SassColor extends Value implements ext.SassColor { T accept(ValueVisitor visitor) => visitor.visitColor(this); - SassColor assertColor([String name]) => this; + SassColor assertColor([String? name]) => this; - SassColor changeRgb({int red, int green, int blue, num alpha}) => + SassColor changeRgb({int? red, int? green, int? blue, num? alpha}) => SassColor.rgb(red ?? this.red, green ?? this.green, blue ?? this.blue, alpha ?? this.alpha); - SassColor changeHsl({num hue, num saturation, num lightness, num alpha}) => + SassColor changeHsl( + {num? hue, num? saturation, num? lightness, num? alpha}) => SassColor.hsl(hue ?? this.hue, saturation ?? this.saturation, lightness ?? this.lightness, alpha ?? this.alpha); - SassColor changeHwb({num hue, num whiteness, num blackness, num alpha}) => + SassColor changeHwb({num? hue, num? whiteness, num? blackness, num? alpha}) => SassColor.hwb(hue ?? this.hue, whiteness ?? this.whiteness, blackness ?? this.blackness, alpha ?? this.alpha); @@ -198,11 +199,11 @@ class SassColor extends Value implements ext.SassColor { _hue = (240 + 60 * (scaledRed - scaledGreen) / delta) % 360; } - _lightness = 50 * (max + min); + var lightness = _lightness = 50 * (max + min); if (max == min) { _saturation = 0; - } else if (_lightness < 50) { + } else if (lightness < 50) { _saturation = 100 * delta / (max + min); } else { _saturation = 100 * delta / (2 - max - min); diff --git a/lib/src/value/external/color.dart b/lib/src/value/external/color.dart index 3bdc4efdd..cf961bbb2 100644 --- a/lib/src/value/external/color.dart +++ b/lib/src/value/external/color.dart @@ -41,31 +41,31 @@ abstract class SassColor extends Value { /// /// Throws a [RangeError] if [red], [green], and [blue] aren't between `0` and /// `255`, or if [alpha] isn't between `0` and `1`. - factory SassColor.rgb(int red, int green, int blue, [num alpha]) = + factory SassColor.rgb(int red, int green, int blue, [num? alpha]) = internal.SassColor.rgb; /// Creates an HSL color. /// /// Throws a [RangeError] if [saturation] or [lightness] aren't between `0` /// and `100`, or if [alpha] isn't between `0` and `1`. - factory SassColor.hsl(num hue, num saturation, num lightness, [num alpha]) = + factory SassColor.hsl(num hue, num saturation, num lightness, [num? alpha]) = internal.SassColor.hsl; /// Creates an HWB color. /// /// Throws a [RangeError] if [whiteness] or [blackness] aren't between `0` and /// `100`, or if [alpha] isn't between `0` and `1`. - factory SassColor.hwb(num hue, num whiteness, num blackness, [num alpha]) = + factory SassColor.hwb(num hue, num whiteness, num blackness, [num? alpha]) = internal.SassColor.hwb; /// Changes one or more of this color's RGB channels and returns the result. - SassColor changeRgb({int red, int green, int blue, num alpha}); + SassColor changeRgb({int? red, int? green, int? blue, num? alpha}); /// Changes one or more of this color's HSL channels and returns the result. - SassColor changeHsl({num hue, num saturation, num lightness, num alpha}); + SassColor changeHsl({num? hue, num? saturation, num? lightness, num? alpha}); /// Changes one or more of this color's HWB channels and returns the result. - SassColor changeHwb({num hue, num whiteness, num blackness, num alpha}); + SassColor changeHwb({num? hue, num? whiteness, num? blackness, num? alpha}); /// Returns a new copy of this color with the alpha channel set to [alpha]. SassColor changeAlpha(num alpha); diff --git a/lib/src/value/external/list.dart b/lib/src/value/external/list.dart index 87dd6c751..d97124c2f 100644 --- a/lib/src/value/external/list.dart +++ b/lib/src/value/external/list.dart @@ -18,7 +18,7 @@ abstract class SassList extends Value { /// Returns an empty list with the given [separator] and [brackets]. /// /// The [separator] defaults to [ListSeparator.undecided], and [brackets] defaults to `false`. - const factory SassList.empty({ListSeparator separator, bool brackets}) = + const factory SassList.empty({ListSeparator? separator, bool brackets}) = internal.SassList.empty; /// Returns an empty list with the given [separator] and [brackets]. diff --git a/lib/src/value/external/number.dart b/lib/src/value/external/number.dart index 628b9aea0..6385b9658 100644 --- a/lib/src/value/external/number.dart +++ b/lib/src/value/external/number.dart @@ -51,18 +51,18 @@ abstract class SassNumber extends Value { /// If [this] is an integer according to [isInt], returns [value] as an [int]. /// /// Otherwise, returns `null`. - int get asInt; + int? get asInt; /// Creates a number, optionally with a single numerator unit. /// /// This matches the numbers that can be written as literals. /// [SassNumber.withUnits] can be used to construct more complex units. - factory SassNumber(num value, [String unit]) = internal.SassNumber; + factory SassNumber(num value, [String? unit]) = internal.SassNumber; /// Creates a number with full [numeratorUnits] and [denominatorUnits]. factory SassNumber.withUnits(num value, - {List numeratorUnits, - List denominatorUnits}) = internal.SassNumber.withUnits; + {List? numeratorUnits, + List? denominatorUnits}) = internal.SassNumber.withUnits; /// Returns [value] as an [int], if it's an integer value according to /// [isInt]. @@ -70,7 +70,7 @@ abstract class SassNumber extends Value { /// Throws a [SassScriptException] if [value] isn't an integer. If this came /// from a function argument, [name] is the argument name (without the `$`). /// It's used for error reporting. - int assertInt([String name]); + int assertInt([String? name]); /// If [value] is between [min] and [max], returns it. /// @@ -78,7 +78,7 @@ abstract class SassNumber extends Value { /// appropriate value. Otherwise, this throws a [SassScriptException]. If this /// came from a function argument, [name] is the argument name (without the /// `$`). It's used for error reporting. - num valueInRange(num min, num max, [String name]); + num valueInRange(num min, num max, [String? name]); /// Returns whether [this] has [unit] as its only unit (and as a numerator). bool hasUnit(String unit); @@ -93,13 +93,13 @@ abstract class SassNumber extends Value { /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - void assertUnit(String unit, [String name]); + void assertUnit(String unit, [String? name]); /// Throws a [SassScriptException] unless [this] has no units. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - void assertNoUnits([String name]); + void assertNoUnits([String? name]); /// Returns a copy of this number, converted to the same units as [other]. /// @@ -117,7 +117,7 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`) and [otherName] is the argument name for [other]. These /// are used for error reporting. - SassNumber coerceToMatch(SassNumber other, [String name, String otherName]); + SassNumber coerceToMatch(SassNumber other, [String? name, String? otherName]); /// Returns [value], converted to the same units as [other]. /// @@ -132,7 +132,7 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`) and [otherName] is the argument name for [other]. These /// are used for error reporting. - num coerceValueToMatch(SassNumber other, [String name, String otherName]); + num coerceValueToMatch(SassNumber other, [String? name, String? otherName]); /// Returns a copy of this number, converted to the same units as [other]. /// @@ -146,7 +146,8 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`) and [otherName] is the argument name for [other]. These /// are used for error reporting. - SassNumber convertToMatch(SassNumber other, [String name, String otherName]); + SassNumber convertToMatch(SassNumber other, + [String? name, String? otherName]); /// Returns [value], converted to the same units as [other]. /// @@ -157,7 +158,7 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`) and [otherName] is the argument name for [other]. These /// are used for error reporting. - num convertValueToMatch(SassNumber other, [String name, String otherName]); + num convertValueToMatch(SassNumber other, [String? name, String? otherName]); /// Returns a copy of this number, converted to the units represented by /// [newNumerators] and [newDenominators]. @@ -176,7 +177,7 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. SassNumber coerce(List newNumerators, List newDenominators, - [String name]); + [String? name]); /// Returns [value], converted to the units represented by [newNumerators] and /// [newDenominators]. @@ -192,14 +193,14 @@ abstract class SassNumber extends Value { /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. num coerceValue(List newNumerators, List newDenominators, - [String name]); + [String? name]); /// This has been renamed [coerceValue] for consistency with [coerceToMatch], /// [coerceValueToMatch], [convertToMatch], and [convertValueToMatch]. @deprecated num valueInUnits(List newNumerators, List newDenominators, - [String name]); + [String? name]); /// A shorthand for [coerceValue] with only one numerator unit. - num coerceValueToUnit(String unit, [String name]); + num coerceValueToUnit(String unit, [String? name]); } diff --git a/lib/src/value/external/string.dart b/lib/src/value/external/string.dart index f352e0591..eec9de362 100644 --- a/lib/src/value/external/string.dart +++ b/lib/src/value/external/string.dart @@ -74,7 +74,7 @@ abstract class SassString extends Value { /// number isn't an integer, or if that integer isn't a valid index for this /// string. If [sassIndex] came from a function argument, [name] is the /// argument name (without the `$`). It's used for error reporting. - int sassIndexToStringIndex(Value sassIndex, [String name]); + int sassIndexToStringIndex(Value sassIndex, [String? name]); /// Converts [sassIndex] into a Dart-style index into [text]`.runes`. /// @@ -87,5 +87,5 @@ abstract class SassString extends Value { /// number isn't an integer, or if that integer isn't a valid index for this /// string. If [sassIndex] came from a function argument, [name] is the /// argument name (without the `$`). It's used for error reporting. - int sassIndexToRuneIndex(Value sassIndex, [String name]); + int sassIndexToRuneIndex(Value sassIndex, [String? name]); } diff --git a/lib/src/value/external/value.dart b/lib/src/value/external/value.dart index 2089726c5..f6b19ec9f 100644 --- a/lib/src/value/external/value.dart +++ b/lib/src/value/external/value.dart @@ -63,7 +63,7 @@ abstract class Value { /// Returns Dart's `null` value if this is [sassNull], and returns [this] /// otherwise. - Value get realNull; + Value? get realNull; /// Converts [sassIndex] into a Dart-style index into the list returned by /// [asList]. @@ -75,7 +75,7 @@ abstract class Value { /// number isn't an integer, or if that integer isn't a valid index for /// [asList]. If [sassIndex] came from a function argument, [name] is the /// argument name (without the `$`). It's used for error reporting. - int sassIndexToListIndex(Value sassIndex, [String name]); + int sassIndexToListIndex(Value sassIndex, [String? name]); /// Throws a [SassScriptException] if [this] isn't a boolean. /// @@ -84,41 +84,41 @@ abstract class Value { /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SassBoolean assertBoolean([String name]); + SassBoolean assertBoolean([String? name]); /// Throws a [SassScriptException] if [this] isn't a color. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SassColor assertColor([String name]); + SassColor assertColor([String? name]); /// Throws a [SassScriptException] if [this] isn't a function reference. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SassFunction assertFunction([String name]); + SassFunction assertFunction([String? name]); /// Throws a [SassScriptException] if [this] isn't a map. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SassMap assertMap([String name]); + SassMap assertMap([String? name]); /// Returns [this] as a [SassMap] if it is one (including empty lists, which /// count as empty maps) or returns `null` if it's not. - SassMap tryMap(); + SassMap? tryMap(); /// Throws a [SassScriptException] if [this] isn't a number. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SassNumber assertNumber([String name]); + SassNumber assertNumber([String? name]); /// Throws a [SassScriptException] if [this] isn't a string. /// /// If this came from a function argument, [name] is the argument name /// (without the `$`). It's used for error reporting. - SassString assertString([String name]); + SassString assertString([String? name]); /// Returns a valid CSS representation of [this]. /// diff --git a/lib/src/value/function.dart b/lib/src/value/function.dart index 431c5332d..3f86a2620 100644 --- a/lib/src/value/function.dart +++ b/lib/src/value/function.dart @@ -14,7 +14,7 @@ class SassFunction extends Value implements internal.SassFunction { T accept(ValueVisitor visitor) => visitor.visitFunction(this); - SassFunction assertFunction([String name]) => this; + SassFunction assertFunction([String? name]) => this; bool operator ==(Object other) => other is SassFunction && callable == other.callable; diff --git a/lib/src/value/list.dart b/lib/src/value/list.dart index 1960e0c5e..4e973465e 100644 --- a/lib/src/value/list.dart +++ b/lib/src/value/list.dart @@ -27,7 +27,7 @@ class SassList extends Value implements ext.SassList { int get lengthAsList => asList.length; - const SassList.empty({ListSeparator separator, bool brackets = false}) + const SassList.empty({ListSeparator? separator, bool brackets = false}) : _contents = const [], separator = separator ?? ListSeparator.undecided, hasBrackets = brackets; @@ -43,10 +43,10 @@ class SassList extends Value implements ext.SassList { T accept(ValueVisitor visitor) => visitor.visitList(this); - SassMap assertMap([String name]) => + SassMap assertMap([String? name]) => asList.isEmpty ? const SassMap.empty() : super.assertMap(name); - SassMap tryMap() => asList.isEmpty ? const SassMap.empty() : null; + SassMap? tryMap() => asList.isEmpty ? const SassMap.empty() : null; bool operator ==(Object other) => (other is SassList && @@ -79,7 +79,7 @@ class ListSeparator { /// /// If the separator of a list has not been decided, this value will be /// `null`. - final String separator; + final String? separator; const ListSeparator._(this._name, this.separator); diff --git a/lib/src/value/map.dart b/lib/src/value/map.dart index 971b858c9..1609fb2ef 100644 --- a/lib/src/value/map.dart +++ b/lib/src/value/map.dart @@ -30,7 +30,7 @@ class SassMap extends Value implements ext.SassMap { T accept(ValueVisitor visitor) => visitor.visitMap(this); - SassMap assertMap([String name]) => this; + SassMap assertMap([String? name]) => this; SassMap tryMap() => this; diff --git a/lib/src/value/null.dart b/lib/src/value/null.dart index 68f4f6a2a..183ac43d1 100644 --- a/lib/src/value/null.dart +++ b/lib/src/value/null.dart @@ -16,7 +16,7 @@ class SassNull extends Value { bool get isBlank => true; - Value get realNull => null; + Value? get realNull => null; const SassNull._(); diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index dfb9acf9b..c4c3e2b0f 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -167,33 +167,38 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// The representation of this number as two slash-separated numbers, if it /// has one. - final Tuple2 asSlash; + final Tuple2? asSlash; bool get isInt => fuzzyIsInt(value); - int get asInt => fuzzyAsInt(value); + int? get asInt => fuzzyAsInt(value); /// Returns a human readable string representation of this number's units. String get unitString => hasUnits ? _unitString(numeratorUnits, denominatorUnits) : ''; - factory SassNumber(num value, [String unit]) => unit == null + factory SassNumber(num value, [String? unit]) => unit == null ? UnitlessSassNumber(value) : SingleUnitSassNumber(value, unit); factory SassNumber.withUnits(num value, - {List numeratorUnits, List denominatorUnits}) { - var emptyNumerator = numeratorUnits == null || numeratorUnits.isEmpty; - var emptyDenominator = denominatorUnits == null || denominatorUnits.isEmpty; - if (emptyNumerator && emptyDenominator) return UnitlessSassNumber(value); - - if (emptyDenominator && numeratorUnits.length == 1) { - return SingleUnitSassNumber(value, numeratorUnits[0]); + {List? numeratorUnits, List? denominatorUnits}) { + if (denominatorUnits == null || denominatorUnits.isEmpty) { + if (numeratorUnits == null || numeratorUnits.isEmpty) { + return UnitlessSassNumber(value); + } else if (numeratorUnits.length == 1) { + return SingleUnitSassNumber(value, numeratorUnits[0]); + } else { + return ComplexSassNumber( + value, List.unmodifiable(numeratorUnits), const []); + } } else { return ComplexSassNumber( value, - emptyNumerator ? const [] : List.unmodifiable(numeratorUnits), - emptyDenominator ? const [] : List.unmodifiable(denominatorUnits)); + numeratorUnits == null || numeratorUnits.isEmpty + ? const [] + : List.unmodifiable(numeratorUnits), + List.unmodifiable(denominatorUnits)); } } @@ -214,15 +219,15 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// [numerator] and [denominator]. SassNumber withSlash(SassNumber numerator, SassNumber denominator); - SassNumber assertNumber([String name]) => this; + SassNumber assertNumber([String? name]) => this; - int assertInt([String name]) { + int assertInt([String? name]) { var integer = fuzzyAsInt(value); if (integer != null) return integer; throw _exception("$this is not an int.", name); } - num valueInRange(num min, num max, [String name]) { + num valueInRange(num min, num max, [String? name]) { var result = fuzzyCheckRange(value, min, max); if (result != null) return result; throw _exception( @@ -230,35 +235,35 @@ abstract class SassNumber extends Value implements ext.SassNumber { name); } - void assertUnit(String unit, [String name]) { + void assertUnit(String unit, [String? name]) { if (hasUnit(unit)) return; throw _exception('Expected $this to have unit "$unit".', name); } - void assertNoUnits([String name]) { + void assertNoUnits([String? name]) { if (!hasUnits) return; throw _exception('Expected $this to have no units.', name); } SassNumber coerceToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => SassNumber.withUnits(coerceValueToMatch(other, name, otherName), numeratorUnits: other.numeratorUnits, denominatorUnits: other.denominatorUnits); num coerceValueToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, coerceUnitless: true, name: name, other: other, otherName: otherName); SassNumber convertToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => SassNumber.withUnits(convertValueToMatch(other, name, otherName), numeratorUnits: other.numeratorUnits, denominatorUnits: other.denominatorUnits); num convertValueToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, coerceUnitless: false, name: name, @@ -266,21 +271,21 @@ abstract class SassNumber extends Value implements ext.SassNumber { otherName: otherName); SassNumber coerce(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => SassNumber.withUnits(coerceValue(newNumerators, newDenominators, name), numeratorUnits: newNumerators, denominatorUnits: newDenominators); num coerceValue(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => _coerceOrConvertValue(newNumerators, newDenominators, coerceUnitless: true, name: name); - num coerceValueToUnit(String unit, [String name]) => + num coerceValueToUnit(String unit, [String? name]) => coerceValue([unit], [], name); @deprecated num valueInUnits(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => coerceValue(newNumerators, newDenominators, name); /// Converts [value] to [newNumerators] and [newDenominators]. @@ -295,10 +300,10 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// error reporting. num _coerceOrConvertValue( List newNumerators, List newDenominators, - {@required bool coerceUnitless, - String name, - ext.SassNumber other, - String otherName}) { + {required bool coerceUnitless, + String? name, + ext.SassNumber? other, + String? otherName}) { assert( other == null || (listEquals(other.numeratorUnits, newNumerators) && @@ -333,7 +338,7 @@ abstract class SassNumber extends Value implements ext.SassNumber { // and make it clear exactly which units are convertible. return _exception( "Expected $this to have ${a(type)} unit " - "(${_unitsByType[type].join(', ')}).", + "(${_unitsByType[type]!.join(', ')}).", name); } } @@ -528,10 +533,7 @@ abstract class SassNumber extends Value implements ext.SassNumber { if (factor == null) return false; value /= factor; return true; - }, orElse: () { - newNumerators.add(numerator); - return null; - }); + }, orElse: () => newNumerators.add(numerator)); } var mutableDenominatorUnits = denominatorUnits.toList(); @@ -541,10 +543,7 @@ abstract class SassNumber extends Value implements ext.SassNumber { if (factor == null) return false; value /= factor; return true; - }, orElse: () { - newNumerators.add(numerator); - return null; - }); + }, orElse: () => newNumerators.add(numerator)); } return SassNumber.withUnits(value, @@ -557,20 +556,17 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// unit in [units2]. bool _areAnyConvertible(List units1, List units2) { return units1.any((unit1) { - if (!_isConvertable(unit1)) return units2.contains(unit1); var innerMap = _conversions[unit1]; + if (innerMap == null) return units2.contains(unit1); return units2.any(innerMap.containsKey); }); } - /// Returns whether [unit] can be converted to or from any other units. - bool _isConvertable(String unit) => _conversions.containsKey(unit); - /// Returns the number of [unit1]s per [unit2]. /// /// Equivalently, `1unit2 * conversionFactor(unit1, unit2) = 1unit1`. @protected - num conversionFactor(String unit1, String unit2) { + num? conversionFactor(String unit1, String unit2) { if (unit1 == unit2) return 1; var innerMap = _conversions[unit1]; if (innerMap == null) return null; @@ -629,12 +625,12 @@ abstract class SassNumber extends Value implements ext.SassNumber { if (units.isEmpty) return units; if (units.length == 1) { var type = _typesByUnit[units.first]; - return type == null ? units : [_unitsByType[type].first]; + return type == null ? units : [_unitsByType[type]!.first]; } return units.map((unit) { var type = _typesByUnit[unit]; - return type == null ? unit : _unitsByType[type].first; + return type == null ? unit : _unitsByType[type]!.first; }).toList() ..sort(); } @@ -656,6 +652,6 @@ abstract class SassNumber extends Value implements ext.SassNumber { } /// Throws a [SassScriptException] with the given [message]. - SassScriptException _exception(String message, [String name]) => + SassScriptException _exception(String message, [String? name]) => SassScriptException(name == null ? message : "\$$name: $message"); } diff --git a/lib/src/value/number/complex.dart b/lib/src/value/number/complex.dart index 3b3014cd6..8fca749d5 100644 --- a/lib/src/value/number/complex.dart +++ b/lib/src/value/number/complex.dart @@ -24,7 +24,7 @@ class ComplexSassNumber extends SassNumber { List.unmodifiable(denominatorUnits)); ComplexSassNumber._(num value, this.numeratorUnits, this.denominatorUnits, - [Tuple2 asSlash]) + [Tuple2? asSlash]) : super.protected(value, asSlash) { assert(numeratorUnits.length > 1 || denominatorUnits.isNotEmpty); } diff --git a/lib/src/value/number/single_unit.dart b/lib/src/value/number/single_unit.dart index fa29d5d63..e58517627 100644 --- a/lib/src/value/number/single_unit.dart +++ b/lib/src/value/number/single_unit.dart @@ -9,6 +9,7 @@ import 'package:tuple/tuple.dart'; import '../../util/number.dart'; import '../../utils.dart'; +import '../../util/nullable.dart'; import '../../value.dart'; import '../external/value.dart' as ext; import '../number.dart'; @@ -26,7 +27,7 @@ class SingleUnitSassNumber extends SassNumber { bool get hasUnits => true; SingleUnitSassNumber(num value, this._unit, - [Tuple2 asSlash]) + [Tuple2? asSlash]) : super.protected(value, asSlash); SassNumber withValue(num value) => SingleUnitSassNumber(value, _unit); @@ -39,21 +40,21 @@ class SingleUnitSassNumber extends SassNumber { bool compatibleWithUnit(String unit) => conversionFactor(_unit, unit) != null; SassNumber coerceToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => convertToMatch(other, name, otherName); num coerceValueToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => convertValueToMatch(other, name, otherName); SassNumber convertToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => (other is SingleUnitSassNumber ? _coerceToUnit(other._unit) : null) ?? // Call this to generate a consistent error message. super.convertToMatch(other, name, otherName); num convertValueToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => (other is SingleUnitSassNumber ? _coerceValueToUnit(other._unit) : null) ?? @@ -61,7 +62,7 @@ class SingleUnitSassNumber extends SassNumber { super.convertValueToMatch(other, name, otherName); SassNumber coerce(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => (newNumerators.length == 1 && newDenominators.isEmpty ? _coerceToUnit(newNumerators[0]) : null) ?? @@ -69,32 +70,29 @@ class SingleUnitSassNumber extends SassNumber { super.coerce(newNumerators, newDenominators, name); num coerceValue(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => (newNumerators.length == 1 && newDenominators.isEmpty ? _coerceValueToUnit(newNumerators[0]) : null) ?? // Call this to generate a consistent error message. super.coerceValue(newNumerators, newDenominators, name); - num coerceValueToUnit(String unit, [String name]) => + num coerceValueToUnit(String unit, [String? name]) => _coerceValueToUnit(unit) ?? // Call this to generate a consistent error message. super.coerceValueToUnit(unit, name); /// A shorthand for [coerce] with only one numerator unit, except that it /// returns `null` if coercion fails. - SassNumber _coerceToUnit(String unit) { + SassNumber? _coerceToUnit(String unit) { if (_unit == unit) return this; - - var factor = conversionFactor(unit, _unit); - return factor == null ? null : SingleUnitSassNumber(value * factor, unit); + return conversionFactor(unit, _unit) + .andThen((factor) => SingleUnitSassNumber(value * factor, unit)); } /// Like [coerceValueToUnit], except that it returns `null` if coercion fails. - num _coerceValueToUnit(String unit) { - var factor = conversionFactor(unit, _unit); - return factor == null ? null : value * factor; - } + num? _coerceValueToUnit(String unit) => + conversionFactor(unit, _unit).andThen((factor) => value * factor); SassNumber multiplyUnits( num value, List otherNumerators, List otherDenominators) { @@ -107,7 +105,6 @@ class SingleUnitSassNumber extends SassNumber { return true; }, orElse: () { newNumerators = [_unit, ...newNumerators]; - return null; }); return SassNumber.withUnits(value, diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index b9fcdb5cb..b826a69de 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -19,7 +19,7 @@ class UnitlessSassNumber extends SassNumber { bool get hasUnits => false; - UnitlessSassNumber(num value, [Tuple2 asSlash]) + UnitlessSassNumber(num value, [Tuple2? asSlash]) : super.protected(value, asSlash); SassNumber withValue(num value) => UnitlessSassNumber(value); @@ -32,37 +32,37 @@ class UnitlessSassNumber extends SassNumber { bool compatibleWithUnit(String unit) => true; SassNumber coerceToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => (other as SassNumber).withValue(value); num coerceValueToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => value; SassNumber convertToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => other.hasUnits // Call this to generate a consistent error message. ? super.convertToMatch(other, name, otherName) : this; num convertValueToMatch(ext.SassNumber other, - [String name, String otherName]) => + [String? name, String? otherName]) => other.hasUnits // Call this to generate a consistent error message. ? super.convertValueToMatch(other, name, otherName) : value; SassNumber coerce(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => SassNumber.withUnits(value, numeratorUnits: newNumerators, denominatorUnits: newDenominators); num coerceValue(List newNumerators, List newDenominators, - [String name]) => + [String? name]) => value; - num coerceValueToUnit(String unit, [String name]) => value; + num coerceValueToUnit(String unit, [String? name]) => value; SassBoolean greaterThan(Value other) { if (other is SassNumber) { diff --git a/lib/src/value/string.dart b/lib/src/value/string.dart index 152b22180..1a5d4ba85 100644 --- a/lib/src/value/string.dart +++ b/lib/src/value/string.dart @@ -22,12 +22,7 @@ class SassString extends Value implements ext.SassString { final bool hasQuotes; - int get sassLength { - _sassLength ??= text.runes.length; - return _sassLength; - } - - int _sassLength; + late final int sassLength = text.runes.length; bool get isSpecialNumber { if (hasQuotes) return false; @@ -89,11 +84,11 @@ class SassString extends Value implements ext.SassString { SassString(this.text, {bool quotes = true}) : hasQuotes = quotes; - int sassIndexToStringIndex(ext.Value sassIndex, [String name]) => + int sassIndexToStringIndex(ext.Value sassIndex, [String? name]) => codepointIndexToCodeUnitIndex( text, sassIndexToRuneIndex(sassIndex, name)); - int sassIndexToRuneIndex(ext.Value sassIndex, [String name]) { + int sassIndexToRuneIndex(ext.Value sassIndex, [String? name]) { var index = sassIndex.assertNumber(name).assertInt(name); if (index == 0) throw _exception("String index may not be 0.", name); if (index.abs() > sassLength) { @@ -107,7 +102,7 @@ class SassString extends Value implements ext.SassString { T accept(ValueVisitor visitor) => visitor.visitString(this); - SassString assertString([String name]) => this; + SassString assertString([String? name]) => this; Value plus(Value other) { if (other is SassString) { @@ -122,6 +117,6 @@ class SassString extends Value implements ext.SassString { int get hashCode => text.hashCode; /// Throws a [SassScriptException] with the given [message]. - SassScriptException _exception(String message, [String name]) => + SassScriptException _exception(String message, [String? name]) => SassScriptException(name == null ? message : "\$$name: $message"); } diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index b62955c0f..83b910270 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -7,7 +7,6 @@ import 'dart:math' as math; import 'package:charcode/charcode.dart'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; @@ -25,7 +24,7 @@ import '../color_names.dart'; import '../configuration.dart'; import '../configured_value.dart'; import '../exception.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; import '../extend/extension.dart'; import '../functions.dart'; import '../functions/meta.dart' as meta; @@ -37,8 +36,8 @@ import '../module.dart'; import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; import '../syntax.dart'; -import '../util/fixed_length_list_builder.dart'; import '../utils.dart'; +import '../util/nullable.dart'; import '../value.dart'; import '../warn.dart'; import 'interface/css.dart'; @@ -72,11 +71,11 @@ typedef _ScopeCallback = Future Function( /// /// Throws a [SassRuntimeException] if evaluation fails. Future evaluateAsync(Stylesheet stylesheet, - {AsyncImportCache importCache, - NodeImporter nodeImporter, - AsyncImporter importer, - Iterable functions, - Logger logger, + {AsyncImportCache? importCache, + NodeImporter? nodeImporter, + AsyncImporter? importer, + Iterable? functions, + Logger? logger, bool sourceMap = false}) => _EvaluateVisitor( importCache: importCache, @@ -92,17 +91,17 @@ class AsyncEvaluator { /// The visitor that evaluates each expression and statement. final _EvaluateVisitor _visitor; - /// The importer to use to resolve `@use` rules in [_importer]. - final AsyncImporter _importer; + /// The importer to use to resolve `@use` rules in [_visitor]. + final AsyncImporter? _importer; /// Creates an evaluator. /// /// Arguments are the same as for [evaluateAsync]. AsyncEvaluator( - {AsyncImportCache importCache, - AsyncImporter importer, - Iterable functions, - Logger logger}) + {AsyncImportCache? importCache, + AsyncImporter? importer, + Iterable? functions, + Logger? logger}) : _visitor = _EvaluateVisitor( importCache: importCache, functions: functions, logger: logger), _importer = importer; @@ -119,15 +118,15 @@ class AsyncEvaluator { /// A visitor that executes Sass code to produce a CSS tree. class _EvaluateVisitor implements - StatementVisitor>, + StatementVisitor>, ExpressionVisitor>, CssVisitor> { /// The import cache used to import other stylesheets. - final AsyncImportCache _importCache; + final AsyncImportCache? _importCache; /// The Node Sass-compatible importer to use when loading new Sass files when /// compiled to Node.js. - final NodeImporter _nodeImporter; + final NodeImporter? _nodeImporter; /// Built-in functions that are globally-acessible, even under the new module /// system. @@ -156,16 +155,22 @@ class _EvaluateVisitor AsyncEnvironment _environment; /// The style rule that defines the current parent selector, if any. - ModifiableCssStyleRule _styleRule; + /// + /// This doesn't take into consideration any intermediate `@at-root` rules. In + /// the common case where those rules are relevant, use [_styleRule] instead. + ModifiableCssStyleRule? _styleRuleIgnoringAtRoot; /// The current media queries, if any. - List _mediaQueries; + List? _mediaQueries; /// The current parent node in the output CSS tree. - ModifiableCssParentNode _parent; + ModifiableCssParentNode get _parent => _assertInModule(__parent, "__parent"); + set _parent(ModifiableCssParentNode value) => __parent = value; + + ModifiableCssParentNode? __parent; /// The name of the current declaration parent. - String _declarationName; + String? _declarationName; /// The human-readable name of the current stack frame. var _member = "root stylesheet"; @@ -176,12 +181,12 @@ class _EvaluateVisitor /// [AstNode] rather than a [FileSpan] so we can avoid calling [AstNode.span] /// if the span isn't required, since some nodes need to do real work to /// manufacture a source span. - AstNode _callableNode; + AstNode? _callableNode; /// The span for the current import that's being resolved. /// /// This is used to produce warnings for importers. - FileSpan _importSpan; + FileSpan? _importSpan; /// Whether we're currently executing a function. var _inFunction = false; @@ -189,8 +194,8 @@ class _EvaluateVisitor /// Whether we're currently building the output of an unknown at rule. var _inUnknownAtRule = false; - /// Whether we're currently building the output of a style rule. - bool get _inStyleRule => _styleRule != null && !_atRootExcludingStyleRule; + ModifiableCssStyleRule? get _styleRule => + _atRootExcludingStyleRule ? null : _styleRuleIgnoringAtRoot; /// Whether we're directly within an `@at-root` rule that excludes style /// rules. @@ -215,7 +220,7 @@ class _EvaluateVisitor /// entrypoint module). /// /// This is used to ensure that we don't get into an infinite load loop. - final _activeModules = {}; + final _activeModules = {}; /// The dynamic call stack representing function invocations, mixin /// invocations, and imports surrounding the current context. @@ -237,17 +242,23 @@ class _EvaluateVisitor /// /// If this is `null`, relative imports aren't supported in the current /// stylesheet. - AsyncImporter _importer; + AsyncImporter? _importer; /// The stylesheet that's currently being evaluated. - Stylesheet _stylesheet; + Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet"); + set _stylesheet(Stylesheet value) => __stylesheet = value; + Stylesheet? __stylesheet; /// The root stylesheet node. - ModifiableCssStylesheet _root; + ModifiableCssStylesheet get _root => _assertInModule(__root, "_root"); + set _root(ModifiableCssStylesheet value) => __root = value; + ModifiableCssStylesheet? __root; /// The first index in [_root.children] after the initial block of CSS /// imports. - int _endOfImports; + int get _endOfImports => _assertInModule(__endOfImports, "_endOfImports"); + set _endOfImports(int value) => __endOfImports = value; + int? __endOfImports; /// Plain-CSS imports that didn't appear in the initial block of CSS imports. /// @@ -256,11 +267,14 @@ class _EvaluateVisitor /// /// This is `null` unless there are any out-of-order imports in the current /// stylesheet. - List _outOfOrderImports; + List? _outOfOrderImports; - /// The extender that tracks extensions and style rules for the current + /// The extension store that tracks extensions and style rules for the current /// module. - Extender _extender; + ExtensionStore get _extensionStore => + _assertInModule(__extensionStore, "_extensionStore"); + set _extensionStore(ExtensionStore value) => __extensionStore = value; + ExtensionStore? __extensionStore; /// The configuration for the current module. /// @@ -271,10 +285,10 @@ class _EvaluateVisitor /// /// Most arguments are the same as those to [evaluateAsync]. _EvaluateVisitor( - {AsyncImportCache importCache, - NodeImporter nodeImporter, - Iterable functions, - Logger logger, + {AsyncImportCache? importCache, + NodeImporter? nodeImporter, + Iterable? functions, + Logger? logger, bool sourceMap = false}) : _importCache = nodeImporter == null ? importCache ?? AsyncImportCache.none(logger: logger) @@ -369,7 +383,7 @@ class _EvaluateVisitor var callable = css ? PlainCssCallable(name.text) : _addExceptionSpan( - _callableNode, + _callableNode!, () => _getFunction(name.text.replaceAll("_", "-"), namespace: module?.text)); if (callable != null) return SassFunction(callable); @@ -382,8 +396,9 @@ class _EvaluateVisitor var function = arguments[0]; var args = arguments[1] as SassArgumentList; - var invocation = ArgumentInvocation([], {}, _callableNode.span, - rest: ValueExpression(args, _callableNode.span), + var callableNode = _callableNode!; + var invocation = ArgumentInvocation([], {}, callableNode.span, + rest: ValueExpression(args, callableNode.span), keywordRest: args.keywords.isEmpty ? null : ValueExpression( @@ -391,7 +406,7 @@ class _EvaluateVisitor for (var entry in args.keywords.entries) SassString(entry.key, quotes: false): entry.value }), - _callableNode.span)); + callableNode.span)); if (function is SassString) { warn( @@ -399,17 +414,18 @@ class _EvaluateVisitor "in Dart Sass 2.0.0. Use call(get-function($function)) instead.", deprecation: true); + var callableNode = _callableNode!; var expression = FunctionExpression( - Interpolation([function.text], _callableNode.span), + Interpolation([function.text], callableNode.span), invocation, - _callableNode.span); + callableNode.span); return await expression.accept(this); } var callable = function.assertFunction("function").callable; if (callable is AsyncCallable) { return await _runFunctionCallable( - invocation, callable, _callableNode); + invocation, callable, _callableNode!); } else { throw SassScriptException( "The function ${callable.name} is asynchronous.\n" @@ -422,12 +438,13 @@ class _EvaluateVisitor AsyncBuiltInCallable.mixin("load-css", r"$url, $with: null", (arguments) async { var url = Uri.parse(arguments[0].assertString("url").text); - var withMap = arguments[1].realNull?.assertMap("with")?.contents; + var withMap = arguments[1].realNull?.assertMap("with").contents; + var callableNode = _callableNode!; var configuration = const Configuration.empty(); if (withMap != null) { var values = {}; - var span = _callableNode.span; + var span = callableNode.span; withMap.forEach((variable, value) { var name = variable.assertString("with key").text.replaceAll("_", "-"); @@ -435,14 +452,14 @@ class _EvaluateVisitor throw "The variable \$$name was configured twice."; } - values[name] = ConfiguredValue(value, span); + values[name] = ConfiguredValue.explicit(value, span, callableNode); }); - configuration = Configuration(values, _callableNode); + configuration = ExplicitConfiguration(values, callableNode); } - await _loadModule(url, "load-css()", _callableNode, + await _loadModule(url, "load-css()", callableNode, (module) => _combineCss(module, clone: true).accept(this), - baseUrl: _callableNode.span?.sourceUrl, + baseUrl: callableNode.span.sourceUrl, configuration: configuration, namesInErrors: true); _assertConfigurationIsEmpty(configuration, nameInError: true); @@ -464,9 +481,9 @@ class _EvaluateVisitor } } - Future run(AsyncImporter importer, Stylesheet node) async { - return _withWarnCallback(() async { - var url = node.span?.sourceUrl; + Future run(AsyncImporter? importer, Stylesheet node) async { + return _withWarnCallback(node, () async { + var url = node.span.sourceUrl; if (url != null) { _activeModules[url] = null; if (_asNodeSass) { @@ -484,37 +501,55 @@ class _EvaluateVisitor }); } - Future runExpression(AsyncImporter importer, Expression expression) => - _withWarnCallback(() => _withFakeStylesheet( - importer, expression, () => expression.accept(this))); + Future runExpression(AsyncImporter? importer, Expression expression) => + _withWarnCallback( + expression, + () => _withFakeStylesheet( + importer, expression, () => expression.accept(this))); - Future runStatement(AsyncImporter importer, Statement statement) => - _withWarnCallback(() => _withFakeStylesheet( - importer, statement, () => statement.accept(this))); + Future runStatement(AsyncImporter? importer, Statement statement) => + _withWarnCallback( + statement, + () => _withFakeStylesheet( + importer, statement, () => statement.accept(this))); /// Runs [callback] with a definition for the top-level `warn` function. - T _withWarnCallback(T callback()) { + /// + /// If no other span can be found to report a warning, falls back on + /// [nodeWithSpan]'s. + T _withWarnCallback(AstNode nodeWithSpan, T callback()) { return withWarnCallback( (message, deprecation) => _warn( - message, _importSpan ?? _callableNode.span, + message, _importSpan ?? _callableNode?.span ?? nodeWithSpan.span, deprecation: deprecation), callback); } + /// Asserts that [value] is not `null` and returns it. + /// + /// This is used for fields that are set whenever the evaluator is evaluating + /// a module, which is to say essentially all the time (unless running via + /// [runExpression] or [runStatement]). + T _assertInModule(T? value, String name) { + if (value != null) return value; + throw StateError("Can't access $name outside of a module."); + } + /// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet] /// with [nodeWithSpan]'s source span. - Future _withFakeStylesheet(AsyncImporter importer, AstNode nodeWithSpan, - FutureOr callback()) async { + Future _withFakeStylesheet(AsyncImporter? importer, + AstNode nodeWithSpan, FutureOr callback()) async { var oldImporter = _importer; _importer = importer; - var oldStylesheet = _stylesheet; + + assert(__stylesheet == null); _stylesheet = Stylesheet(const [], nodeWithSpan.span); try { return await callback(); } finally { _importer = oldImporter; - _stylesheet = oldStylesheet; + __stylesheet = null; } } @@ -536,17 +571,17 @@ class _EvaluateVisitor /// the stack frame for the duration of the [callback]. Future _loadModule(Uri url, String stackFrame, AstNode nodeWithSpan, void callback(Module module), - {Uri baseUrl, - Configuration configuration, + {Uri? baseUrl, + Configuration? configuration, bool namesInErrors = false}) async { var builtInModule = _builtInModules[url]; if (builtInModule != null) { - if (configuration != null && !configuration.isImplicit) { + if (configuration is ExplicitConfiguration) { throw _exception( namesInErrors ? "Built-in module $url can't be configured." : "Built-in modules can't be configured.", - nodeWithSpan.span); + configuration.nodeWithSpan.span); } _addExceptionSpan(nodeWithSpan, () => callback(builtInModule)); @@ -560,18 +595,18 @@ class _EvaluateVisitor var stylesheet = result.item2; var canonicalUrl = stylesheet.span.sourceUrl; - if (_activeModules.containsKey(canonicalUrl)) { + 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."; - var previousLoad = _activeModules[canonicalUrl]; - throw previousLoad == null - ? _exception(message) - : _multiSpanException( - message, "new load", {previousLoad.span: "original load"}); + + throw _activeModules[canonicalUrl].andThen((previousLoad) => + _multiSpanException(message, "new load", + {previousLoad.span: "original load"})) ?? + _exception(message); } - _activeModules[canonicalUrl] = nodeWithSpan; + if (canonicalUrl != null) _activeModules[canonicalUrl] = nodeWithSpan; Module module; try { @@ -584,7 +619,7 @@ class _EvaluateVisitor } try { - await callback(module); + callback(module); } on SassRuntimeException { rethrow; } on MultiSpanSassException catch (error) { @@ -609,26 +644,29 @@ class _EvaluateVisitor /// If [namesInErrors] is `true`, this includes the names of modules in errors /// relating to them. This should only be `true` if the names won't be obvious /// from the source span. - Future _execute(AsyncImporter importer, Stylesheet stylesheet, - {Configuration configuration, - AstNode nodeWithSpan, + Future _execute(AsyncImporter? importer, Stylesheet stylesheet, + {Configuration? configuration, + AstNode? nodeWithSpan, bool namesInErrors = false}) async { var url = stylesheet.span.sourceUrl; var alreadyLoaded = _modules[url]; if (alreadyLoaded != null) { - if (!(configuration ?? _configuration).isImplicit) { + var currentConfiguration = configuration ?? _configuration; + if (currentConfiguration is ExplicitConfiguration) { var message = namesInErrors ? "${p.prettyUri(url)} was already loaded, so it can't be " "configured using \"with\"." : "This module was already loaded, so it can't be configured using " "\"with\"."; - var existingNode = _moduleNodes[url]; + var existingSpan = _moduleNodes[url]?.span; + var configurationSpan = configuration == null + ? currentConfiguration.nodeWithSpan.span + : null; var secondarySpans = { - if (existingNode != null) existingNode.span: "original load", - if (configuration == null) - _configuration.nodeWithSpan.span: "configuration" + if (existingSpan != null) existingSpan: "original load", + if (configurationSpan != null) configurationSpan: "configuration" }; throw secondarySpans.isEmpty @@ -640,16 +678,16 @@ class _EvaluateVisitor } var environment = AsyncEnvironment(sourceMap: _sourceMap); - CssStylesheet css; - var extender = Extender(); + late CssStylesheet css; + var extensionStore = ExtensionStore(); await _withEnvironment(environment, () async { var oldImporter = _importer; - var oldStylesheet = _stylesheet; - var oldRoot = _root; - var oldParent = _parent; - var oldEndOfImports = _endOfImports; + var oldStylesheet = __stylesheet; + var oldRoot = __root; + var oldParent = __parent; + var oldEndOfImports = __endOfImports; var oldOutOfOrderImports = _outOfOrderImports; - var oldExtender = _extender; + var oldExtensionStore = __extensionStore; var oldStyleRule = _styleRule; var oldMediaQueries = _mediaQueries; var oldDeclarationName = _declarationName; @@ -659,12 +697,12 @@ class _EvaluateVisitor var oldConfiguration = _configuration; _importer = importer; _stylesheet = stylesheet; - _root = ModifiableCssStylesheet(stylesheet.span); - _parent = _root; + var root = __root = ModifiableCssStylesheet(stylesheet.span); + _parent = root; _endOfImports = 0; _outOfOrderImports = null; - _extender = extender; - _styleRule = null; + _extensionStore = extensionStore; + _styleRuleIgnoringAtRoot = null; _mediaQueries = null; _declarationName = null; _inUnknownAtRule = false; @@ -674,17 +712,17 @@ class _EvaluateVisitor await visitStylesheet(stylesheet); css = _outOfOrderImports == null - ? _root + ? root : CssStylesheet(_addOutOfOrderImports(), stylesheet.span); _importer = oldImporter; - _stylesheet = oldStylesheet; - _root = oldRoot; - _parent = oldParent; - _endOfImports = oldEndOfImports; + __stylesheet = oldStylesheet; + __root = oldRoot; + __parent = oldParent; + __endOfImports = oldEndOfImports; _outOfOrderImports = oldOutOfOrderImports; - _extender = oldExtender; - _styleRule = oldStyleRule; + __extensionStore = oldExtensionStore; + _styleRuleIgnoringAtRoot = oldStyleRule; _mediaQueries = oldMediaQueries; _declarationName = oldDeclarationName; _inUnknownAtRule = oldInUnknownAtRule; @@ -693,23 +731,26 @@ class _EvaluateVisitor _configuration = oldConfiguration; }); - var module = environment.toModule(css, extender); - _modules[url] = module; - _moduleNodes[url] = nodeWithSpan; + var module = environment.toModule(css, extensionStore); + if (url != null) { + _modules[url] = module; + if (nodeWithSpan != null) _moduleNodes[url] = nodeWithSpan; + } + return module; } /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted /// after [_endOfImports], if necessary. List _addOutOfOrderImports() { - if (_outOfOrderImports == null) return _root.children; + var outOfOrderImports = _outOfOrderImports; + if (outOfOrderImports == null) return _root.children; - var statements = FixedLengthListBuilder( - _root.children.length + _outOfOrderImports.length) - ..addRange(_root.children, 0, _endOfImports) - ..addAll(_outOfOrderImports) - ..addRange(_root.children, _endOfImports); - return statements.build(); + return [ + ..._root.children.take(_endOfImports), + ...outOfOrderImports, + ..._root.children.skip(_endOfImports) + ]; } /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all @@ -721,8 +762,8 @@ class _EvaluateVisitor /// that they don't modify [root] or its dependencies. CssStylesheet _combineCss(Module root, {bool clone = false}) { if (!root.upstream.any((module) => module.transitivelyContainsCss)) { - var selectors = root.extender.simpleSelectors; - var unsatisfiedExtension = firstOrNull(root.extender + var selectors = root.extensionStore.simpleSelectors; + var unsatisfiedExtension = firstOrNull(root.extensionStore .extensionsWhereTarget((target) => !selectors.contains(target))); if (unsatisfiedExtension != null) { _throwForUnsatisfiedExtension(unsatisfiedExtension); @@ -757,11 +798,12 @@ class _EvaluateVisitor /// Extends the selectors in each module with the extensions defined in /// downstream modules. void _extendModules(List sortedModules) { - // All the extenders directly downstream of a given module (indexed by its - // canonical URL). It's important that we create this in topological order, - // so that by the time we're processing a module we've already filled in all - // its downstream extenders and we can use them to extend that module. - var downstreamExtenders = >{}; + // All the [ExtensionStore]s directly downstream of a given module (indexed + // by its canonical URL). It's important that we create this in topological + // order, so that by the time we're processing a module we've already filled + // in all its downstream [ExtensionStore]s and we can use them to extend + // that module. + var downstreamExtensionStores = >{}; /// Extensions that haven't yet been satisfied by some upstream module. This /// adds extensions when they're defined but not satisfied, and removes them @@ -769,31 +811,35 @@ class _EvaluateVisitor var unsatisfiedExtensions = Set.identity(); for (var module in sortedModules) { - // Create a snapshot of the simple selectors currently in the extender so - // that we don't consider an extension "satisfied" below because of a - // simple selector added by another (sibling) extension. - var originalSelectors = module.extender.simpleSelectors.toSet(); + // Create a snapshot of the simple selectors currently in the + // [ExtensionStore] so that we don't consider an extension "satisfied" + // below because of a simple selector added by another (sibling) + // extension. + var originalSelectors = module.extensionStore.simpleSelectors.toSet(); // Add all as-yet-unsatisfied extensions before adding downstream - // extenders, because those are all in [unsatisfiedExtensions] already. - unsatisfiedExtensions.addAll(module.extender.extensionsWhereTarget( + // [ExtensionStore]s, because those are all in [unsatisfiedExtensions] + // already. + unsatisfiedExtensions.addAll(module.extensionStore.extensionsWhereTarget( (target) => !originalSelectors.contains(target))); - var extenders = downstreamExtenders[module.url]; - if (extenders != null) module.extender.addExtensions(extenders); - if (module.extender.isEmpty) continue; + downstreamExtensionStores[module.url] + .andThen(module.extensionStore.addExtensions); + if (module.extensionStore.isEmpty) continue; for (var upstream in module.upstream) { - downstreamExtenders - .putIfAbsent(upstream.url, () => []) - .add(module.extender); + var url = upstream.url; + if (url == null) continue; + downstreamExtensionStores + .putIfAbsent(url, () => []) + .add(module.extensionStore); } // Remove all extensions that are now satisfied after adding downstream - // extenders so it counts any downstream extensions that have been newly - // satisfied. - unsatisfiedExtensions.removeAll( - module.extender.extensionsWhereTarget(originalSelectors.contains)); + // [ExtensionStore]s so it counts any downstream extensions that have been + // newly satisfied. + unsatisfiedExtensions.removeAll(module.extensionStore + .extensionsWhereTarget(originalSelectors.contains)); } if (unsatisfiedExtensions.isNotEmpty) { @@ -802,8 +848,7 @@ class _EvaluateVisitor } /// Throws an exception indicating that [extension] is unsatisfied. - @alwaysThrows - void _throwForUnsatisfiedExtension(Extension extension) { + Never _throwForUnsatisfiedExtension(Extension extension) { throw SassException( 'The target selector was not found.\n' 'Use "@extend ${extension.target} !optional" to avoid this error.', @@ -854,27 +899,35 @@ class _EvaluateVisitor // ## Statements - Future visitStylesheet(Stylesheet node) async { + Future visitStylesheet(Stylesheet node) async { for (var child in node.children) { await child.accept(this); } return null; } - Future visitAtRootRule(AtRootRule node) async { + Future visitAtRootRule(AtRootRule node) async { var query = AtRootQuery.defaultQuery; - if (node.query != null) { + var unparsedQuery = node.query; + if (unparsedQuery != null) { var resolved = - await _performInterpolation(node.query, warnForColor: true); + await _performInterpolation(unparsedQuery, warnForColor: true); query = _adjustParseError( - node.query, () => AtRootQuery.parse(resolved, logger: _logger)); + unparsedQuery, () => AtRootQuery.parse(resolved, logger: _logger)); } var parent = _parent; var included = []; while (parent is! CssStylesheet) { if (!query.excludes(parent)) included.add(parent); - parent = parent.parent; + + var grandparent = parent.parent; + if (grandparent == null) { + throw StateError( + "CssNodes must have a CssStylesheet transitive parent node."); + } + + parent = grandparent; } var root = _trimIncluded(included); @@ -889,17 +942,20 @@ class _EvaluateVisitor return null; } - var innerCopy = - included.isEmpty ? null : included.first.copyWithoutChildren(); - var outerCopy = innerCopy; - for (var node in included.skip(1)) { - var copy = node.copyWithoutChildren(); - copy.addChild(outerCopy); - outerCopy = copy; + var innerCopy = root; + if (included.isNotEmpty) { + innerCopy = included.first.copyWithoutChildren(); + var outerCopy = innerCopy; + for (var node in included.skip(1)) { + var copy = node.copyWithoutChildren(); + copy.addChild(outerCopy); + outerCopy = copy; + } + + root.addChild(outerCopy); } - if (outerCopy != null) root.addChild(outerCopy); - await _scopeForAtRoot(node, innerCopy ?? root, query, included)(() async { + await _scopeForAtRoot(node, innerCopy, query, included)(() async { for (var child in node.children) { await child.accept(this); } @@ -908,8 +964,8 @@ class _EvaluateVisitor return null; } - /// Destructively trims a trailing sublist that matches the current list of - /// parents from [nodes]. + /// Destructively trims a trailing sublist from [nodes] that matches the + /// current list of parents. /// /// [nodes] should be a list of parents included by an `@at-root` rule, from /// innermost to outermost. If it contains a trailing sublist that's @@ -922,19 +978,30 @@ class _EvaluateVisitor if (nodes.isEmpty) return _root; var parent = _parent; - int innermostContiguous; - var i = 0; - for (; i < nodes.length; i++) { + int? innermostContiguous; + for (var i = 0; i < nodes.length; i++) { while (parent != nodes[i]) { innermostContiguous = null; - parent = parent.parent; + + var grandparent = parent.parent; + if (grandparent == null) { + throw ArgumentError( + "Expected ${nodes[i]} to be an ancestor of $this."); + } + + parent = grandparent; } innermostContiguous ??= i; - parent = parent.parent; + + var grandparent = parent.parent; + if (grandparent == null) { + throw ArgumentError("Expected ${nodes[i]} to be an ancestor of $this."); + } + parent = grandparent; } if (parent != _root) return _root; - var root = nodes[innermostContiguous]; + var root = nodes[innermostContiguous!]; nodes.removeRange(innermostContiguous, nodes.length); return root; } @@ -999,7 +1066,7 @@ class _EvaluateVisitor Future visitContentBlock(ContentBlock node) => throw UnsupportedError( "Evaluation handles @include and its content block together."); - Future visitContentRule(ContentRule node) async { + Future visitContentRule(ContentRule node) async { var content = _environment.content; if (content == null) return null; @@ -1013,15 +1080,15 @@ class _EvaluateVisitor return null; } - Future visitDebugRule(DebugRule node) async { + Future visitDebugRule(DebugRule node) async { var value = await node.expression.accept(this); _logger.debug( value is SassString ? value.text : value.toString(), node.span); return null; } - Future visitDeclaration(Declaration node) async { - if (!_inStyleRule && !_inUnknownAtRule && !_inKeyframes) { + Future visitDeclaration(Declaration node) async { + if (_styleRule == null && !_inUnknownAtRule && !_inKeyframes) { throw _exception( "Declarations may only be used within style rules.", node.span); } @@ -1030,9 +1097,8 @@ class _EvaluateVisitor if (_declarationName != null) { name = CssValue("$_declarationName-${name.value}", name.span); } - var cssValue = node.value == null - ? null - : CssValue(await node.value.accept(this), node.value.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. @@ -1040,17 +1106,19 @@ class _EvaluateVisitor (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { _parent.addChild(ModifiableCssDeclaration(name, cssValue, node.span, parsedAsCustomProperty: node.isCustomProperty, - valueSpanForMap: _expressionNode(node.value)?.span)); - } else if (name.value.startsWith('--') && node.children == null) { + valueSpanForMap: + _sourceMap ? node.value.andThen(_expressionNode)?.span : null)); + } else if (name.value.startsWith('--') && cssValue != null) { throw _exception( - "Custom property values may not be empty.", node.value.span); + "Custom property values may not be empty.", cssValue.span); } - if (node.children != null) { + var children = node.children; + if (children != null) { var oldDeclarationName = _declarationName; _declarationName = name.value; await _environment.scope(() async { - for (var child in node.children) { + for (var child in children) { await child.accept(this); } }, when: node.hasDeclarations); @@ -1063,7 +1131,7 @@ class _EvaluateVisitor /// Returns whether [value] is an empty list. bool _isEmptyList(Value value) => value.asList.isEmpty; - Future visitEachRule(EachRule node) async { + Future visitEachRule(EachRule node) async { var list = await node.list.accept(this); var nodeWithSpan = _expressionNode(node.list); var setVariables = node.variables.length == 1 @@ -1100,8 +1168,9 @@ class _EvaluateVisitor (await node.expression.accept(this)).toString(), node.span); } - Future visitExtendRule(ExtendRule node) async { - if (!_inStyleRule || _declarationName != null) { + Future visitExtendRule(ExtendRule node) async { + var styleRule = _styleRule; + if (styleRule == null || _declarationName != null) { throw _exception( "@extend may only be used within style rules.", node.span); } @@ -1134,14 +1203,14 @@ class _EvaluateVisitor targetText.span); } - _extender.addExtension( - _styleRule.selector, compound.components.first, node, _mediaQueries); + _extensionStore.addExtension( + styleRule.selector, compound.components.first, node, _mediaQueries); } return null; } - Future visitAtRule(AtRule node) async { + Future visitAtRule(AtRule node) async { // NOTE: this logic is largely duplicated in [visitCssAtRule]. Most changes // here should be mirrored there. @@ -1152,12 +1221,11 @@ class _EvaluateVisitor var name = await _interpolationToValue(node.name); - var value = node.value == null - ? null - : await _interpolationToValue(node.value, - trim: true, warnForColor: true); + var value = await node.value.andThen((value) => + _interpolationToValue(value, trim: true, warnForColor: true)); - if (node.children == null) { + var children = node.children; + if (children == null) { _parent.addChild( ModifiableCssAtRule(name, node.span, childless: true, value: value)); return null; @@ -1173,8 +1241,9 @@ class _EvaluateVisitor await _withParent(ModifiableCssAtRule(name, node.span, value: value), () async { - if (!_inStyleRule || _inKeyframes) { - for (var child in node.children) { + var styleRule = _styleRule; + if (styleRule == null || _inKeyframes) { + for (var child in children) { await child.accept(this); } } else { @@ -1182,8 +1251,8 @@ class _EvaluateVisitor // declarations immediately inside it have somewhere to go. // // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}". - await _withParent(_styleRule.copyWithoutChildren(), () async { - for (var child in node.children) { + await _withParent(styleRule.copyWithoutChildren(), () async { + for (var child in children) { await child.accept(this); } }, scopeWhen: false); @@ -1197,7 +1266,7 @@ class _EvaluateVisitor return null; } - Future visitForRule(ForRule node) async { + Future visitForRule(ForRule node) async { var fromNumber = await _addExceptionSpanAsync( node.from, () async => (await node.from.accept(this)).assertNumber()); var toNumber = await _addExceptionSpanAsync( @@ -1231,7 +1300,7 @@ class _EvaluateVisitor }, semiGlobal: true); } - Future visitForwardRule(ForwardRule node) async { + Future visitForwardRule(ForwardRule node) async { var oldConfiguration = _configuration; var adjustedConfiguration = oldConfiguration.throughForward(node); @@ -1251,8 +1320,7 @@ class _EvaluateVisitor if (!variable.isGuarded) variable.name }); - _assertConfigurationIsEmpty(newConfiguration, - only: {for (var variable in node.configuration) variable.name}); + _assertConfigurationIsEmpty(newConfiguration); } else { _configuration = adjustedConfiguration; await _loadModule(node.url, "@forward", node, (module) { @@ -1264,7 +1332,7 @@ class _EvaluateVisitor return null; } - /// Updates [configuration] to include [node]'s configuration and return the + /// Updates [configuration] to include [node]'s configuration and returns the /// result. Future _addForwardConfiguration( Configuration configuration, ForwardRule node) async { @@ -1278,20 +1346,24 @@ class _EvaluateVisitor } } - newValues[variable.name] = ConfiguredValue( + newValues[variable.name] = ConfiguredValue.explicit( (await variable.expression.accept(this)).withoutSlash(), variable.span, _expressionNode(variable.expression)); } - return Configuration(newValues, node); + if (configuration is ExplicitConfiguration || configuration.isEmpty) { + return ExplicitConfiguration(newValues, node); + } else { + return Configuration.implicit(newValues); + } } /// Remove configured values from [upstream] that have been removed from /// [downstream], unless they match a name in [except]. void _removeUsedConfiguration( Configuration upstream, Configuration downstream, - {@required Set except}) { + {required Set except}) { for (var name in upstream.values.keys.toList()) { if (except.contains(name)) continue; if (!downstream.values.containsKey(name)) upstream.remove(name); @@ -1307,26 +1379,29 @@ class _EvaluateVisitor /// variable in the error message. This should only be `true` if the name /// won't be obvious from the source span. void _assertConfigurationIsEmpty(Configuration configuration, - {Set only, bool nameInError = false}) { - configuration.values.forEach((name, value) { - if (only != null && !only.contains(name)) return; + {bool nameInError = false}) { + // By definition, implicit configurations are allowed to only use a subset + // of their values. + if (configuration is! ExplicitConfiguration) return; + if (configuration.isEmpty) return; - throw _exception( - nameInError - ? "\$$name was not declared with !default in the @used module." - : "This variable was not declared with !default in the @used " - "module.", - value.configurationSpan); - }); + var entry = configuration.values.entries.first; + throw _exception( + nameInError + ? "\$${entry.key} was not declared with !default in the @used " + "module." + : "This variable was not declared with !default in the @used " + "module.", + entry.value.configurationSpan); } - Future visitFunctionRule(FunctionRule node) async { + Future visitFunctionRule(FunctionRule node) async { _environment.setFunction(UserDefinedCallable(node, _environment.closure())); return null; } - Future visitIfRule(IfRule node) async { - var clause = node.lastClause; + Future visitIfRule(IfRule node) async { + IfRuleClause? clause = node.lastClause; for (var clauseToCheck in node.clauses) { if ((await clauseToCheck.expression.accept(this)).isTruthy) { clause = clauseToCheck; @@ -1337,12 +1412,13 @@ class _EvaluateVisitor return await _environment.scope( () => _handleReturn( - clause.children, (child) => child.accept(this)), + clause!.children, // dart-lang/sdk#45348 + (child) => child.accept(this)), semiGlobal: true, when: clause.hasDeclarations); } - Future visitImportRule(ImportRule node) async { + Future visitImportRule(ImportRule node) async { for (var import in node.imports) { if (import is DynamicImport) { await _visitDynamicImport(import); @@ -1362,14 +1438,15 @@ class _EvaluateVisitor var stylesheet = result.item2; var url = stylesheet.span.sourceUrl; - if (_activeModules.containsKey(url)) { - var previousLoad = _activeModules[url]; - throw previousLoad == null - ? _exception("This file is already being loaded.") - : _multiSpanException("This file is already being loaded.", - "new load", {previousLoad.span: "original load"}); + if (url != null) { + if (_activeModules.containsKey(url)) { + throw _activeModules[url].andThen((previousLoad) => + _multiSpanException("This file is already being loaded.", + "new load", {previousLoad.span: "original load"})) ?? + _exception("This file is already being loaded."); + } + _activeModules[url] = import; } - _activeModules[url] = import; // If the imported stylesheet doesn't use any modules, we can inject its // CSS directly into the current stylesheet. If it does use modules, we @@ -1387,7 +1464,7 @@ class _EvaluateVisitor return; } - List children; + late List children; var environment = _environment.forImport(); await _withEnvironment(environment, () async { var oldImporter = _importer; @@ -1451,22 +1528,23 @@ class _EvaluateVisitor /// /// This first tries loading [url] relative to [baseUrl], which defaults to /// `_stylesheet.span.sourceUrl`. - Future> _loadStylesheet( + Future> _loadStylesheet( String url, FileSpan span, - {Uri baseUrl, bool forImport = false}) async { + {Uri? baseUrl, bool forImport = false}) async { try { assert(_importSpan == null); _importSpan = span; - if (_nodeImporter != null) { - var stylesheet = await _importLikeNode(url, forImport); - if (stylesheet != null) return Tuple2(null, stylesheet); - } else { - var tuple = await _importCache.import(Uri.parse(url), + var importCache = _importCache; + if (importCache != null) { + var tuple = await importCache.import(Uri.parse(url), baseImporter: _importer, - baseUrl: baseUrl ?? _stylesheet?.span?.sourceUrl, + baseUrl: baseUrl ?? _stylesheet.span.sourceUrl, forImport: forImport); if (tuple != null) return tuple; + } else { + var stylesheet = await _importLikeNode(url, forImport); + if (stylesheet != null) return Tuple2(null, stylesheet); } if (url.startsWith('package:') && isNode) { @@ -1479,9 +1557,9 @@ class _EvaluateVisitor } on SassException catch (error) { throw _exception(error.message, error.span); } catch (error) { - String message; + String? message; try { - message = error.message as String; + message = (error as dynamic).message as String; } catch (_) { message = error.toString(); } @@ -1494,9 +1572,10 @@ class _EvaluateVisitor /// Imports a stylesheet using [_nodeImporter]. /// /// Returns the [Stylesheet], or `null` if the import failed. - Future _importLikeNode(String originalUrl, bool forImport) async { - var result = await _nodeImporter.loadAsync( - originalUrl, _stylesheet.span?.sourceUrl, forImport); + Future _importLikeNode( + String originalUrl, bool forImport) async { + var result = await _nodeImporter! + .loadAsync(originalUrl, _stylesheet.span.sourceUrl, forImport); if (result == null) return null; var contents = result.item1; @@ -1515,19 +1594,18 @@ class _EvaluateVisitor // here should be mirrored there. var url = await _interpolationToValue(import.url); - var supports = import.supports; - var resolvedSupports = supports is SupportsDeclaration - ? "${await _evaluateToCss(supports.name)}: " - "${await _evaluateToCss(supports.value)}" - : (supports == null ? null : await _visitSupportsCondition(supports)); - var mediaQuery = - import.media == null ? null : await _visitMediaQueries(import.media); + var supports = await import.supports.andThen((supports) async { + var arg = supports is SupportsDeclaration + ? "${await _evaluateToCss(supports.name)}: " + "${await _evaluateToCss(supports.value)}" + : await supports.andThen(_visitSupportsCondition); + return CssValue("supports($arg)", supports.span); + }); + var rawMedia = import.media; + var mediaQuery = await rawMedia.andThen(_visitMediaQueries); var node = ModifiableCssImport(url, import.span, - supports: resolvedSupports == null - ? null - : CssValue("supports($resolvedSupports)", import.supports.span), - media: mediaQuery); + supports: supports, media: mediaQuery); if (_parent != _root) { _parent.addChild(node); @@ -1535,13 +1613,12 @@ class _EvaluateVisitor _root.addChild(node); _endOfImports++; } else { - _outOfOrderImports ??= []; - _outOfOrderImports.add(node); + (_outOfOrderImports ??= []).add(node); } return null; } - Future visitIncludeRule(IncludeRule node) async { + Future visitIncludeRule(IncludeRule node) async { var mixin = _addExceptionSpan(node, () => _environment.getMixin(node.name, namespace: node.namespace)); if (mixin == null) { @@ -1566,10 +1643,8 @@ class _EvaluateVisitor _stackTrace(node.spanWithoutContent)); } - var contentCallable = node.content == null - ? null - : UserDefinedCallable(node.content, _environment.closure()); - + var contentCallable = node.content.andThen( + (content) => UserDefinedCallable(content, _environment.closure())); await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () async { await _environment.withContent(contentCallable, () async { @@ -1589,12 +1664,12 @@ class _EvaluateVisitor return null; } - Future visitMixinRule(MixinRule node) async { + Future visitMixinRule(MixinRule node) async { _environment.setMixin(UserDefinedCallable(node, _environment.closure())); return null; } - Future visitLoudComment(LoudComment node) async { + Future visitLoudComment(LoudComment node) async { // NOTE: this logic is largely duplicated in [visitCssComment]. Most changes // here should be mirrored there. @@ -1610,7 +1685,7 @@ class _EvaluateVisitor return null; } - Future visitMediaRule(MediaRule node) async { + Future visitMediaRule(MediaRule node) async { // NOTE: this logic is largely duplicated in [visitCssMediaRule]. Most // changes here should be mirrored there. @@ -1620,15 +1695,15 @@ class _EvaluateVisitor } var queries = await _visitMediaQueries(node.query); - var mergedQueries = _mediaQueries == null - ? null - : _mergeMediaQueries(_mediaQueries, queries); + var mergedQueries = _mediaQueries + .andThen((mediaQueries) => _mergeMediaQueries(mediaQueries, queries)); if (mergedQueries != null && mergedQueries.isEmpty) return null; await _withParent( ModifiableCssMediaRule(mergedQueries ?? queries, node.span), () async { await _withMediaQueries(mergedQueries ?? queries, () async { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { await child.accept(this); } @@ -1638,7 +1713,7 @@ class _EvaluateVisitor // // For example, "a {@media screen {b: c}}" should produce // "@media screen {a {b: c}}". - await _withParent(_styleRule.copyWithoutChildren(), () async { + await _withParent(styleRule.copyWithoutChildren(), () async { for (var child in node.children) { await child.accept(this); } @@ -1672,7 +1747,7 @@ class _EvaluateVisitor /// Returns the empty list if there are no contexts that match both [queries1] /// and [queries2], or `null` if there are contexts that can't be represented /// by media queries. - List _mergeMediaQueries( + List? _mergeMediaQueries( Iterable queries1, Iterable queries2) { var queries = []; for (var query1 in queries1) { @@ -1689,9 +1764,9 @@ class _EvaluateVisitor Future visitReturnRule(ReturnRule node) => node.expression.accept(this); - Future visitSilentComment(SilentComment node) async => null; + Future visitSilentComment(SilentComment node) async => null; - Future visitStyleRule(StyleRule node) async { + Future visitStyleRule(StyleRule node) async { // NOTE: this logic is largely duplicated in [visitCssStyleRule]. Most // changes here should be mirrored there. @@ -1732,10 +1807,10 @@ class _EvaluateVisitor parsedSelector = _addExceptionSpan( node.selector, () => parsedSelector.resolveParentSelectors( - _styleRule?.originalSelector, + _styleRuleIgnoringAtRoot?.originalSelector, implicitParent: !_atRootExcludingStyleRule)); - var selector = _extender.addSelector( + var selector = _extensionStore.addSelector( parsedSelector, node.selector.span, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: parsedSelector); @@ -1752,7 +1827,7 @@ class _EvaluateVisitor scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - if (!_inStyleRule && _parent.children.isNotEmpty) { + if (_styleRule == null && _parent.children.isNotEmpty) { var lastChild = _parent.children.last; lastChild.isGroupEnd = true; } @@ -1760,7 +1835,7 @@ class _EvaluateVisitor return null; } - Future visitSupportsRule(SupportsRule node) async { + Future visitSupportsRule(SupportsRule node) async { // NOTE: this logic is largely duplicated in [visitCssSupportsRule]. Most // changes here should be mirrored there. @@ -1774,7 +1849,8 @@ class _EvaluateVisitor await _visitSupportsCondition(node.condition), node.condition.span); await _withParent(ModifiableCssSupportsRule(condition, node.span), () async { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { await child.accept(this); } @@ -1784,7 +1860,7 @@ class _EvaluateVisitor // // For example, "a {@supports (a: b) {b: c}}" should produce "@supports // (a: b) {a {b: c}}". - await _withParent(_styleRule.copyWithoutChildren(), () async { + await _withParent(styleRule.copyWithoutChildren(), () async { for (var child in node.children) { await child.accept(this); } @@ -1816,7 +1892,8 @@ class _EvaluateVisitor } else if (condition is SupportsAnything) { return "(${await _performInterpolation(condition.contents)})"; } else { - return null; + throw ArgumentError( + "Unknown supports condition type ${condition.runtimeType}."); } } @@ -1827,7 +1904,7 @@ class _EvaluateVisitor /// [SupportsOperation], and is used to determine whether parentheses are /// necessary if [condition] is also a [SupportsOperation]. Future _parenthesize(SupportsCondition condition, - [String operator]) async { + [String? operator]) async { if ((condition is SupportsNegation) || (condition is SupportsOperation && (operator == null || operator != condition.operator))) { @@ -1837,7 +1914,7 @@ class _EvaluateVisitor } } - Future visitVariableDeclaration(VariableDeclaration node) async { + Future visitVariableDeclaration(VariableDeclaration node) async { if (node.isGuarded) { if (node.namespace == null && _environment.atRoot) { var override = _configuration.remove(node.name); @@ -1881,12 +1958,12 @@ class _EvaluateVisitor return null; } - Future visitUseRule(UseRule node) async { + Future visitUseRule(UseRule node) async { var configuration = node.configuration.isEmpty ? const Configuration.empty() - : Configuration({ + : ExplicitConfiguration({ for (var variable in node.configuration) - variable.name: ConfiguredValue( + variable.name: ConfiguredValue.explicit( (await variable.expression.accept(this)).withoutSlash(), variable.span, _expressionNode(variable.expression)) @@ -1900,7 +1977,7 @@ class _EvaluateVisitor return null; } - Future visitWarnRule(WarnRule node) async { + Future visitWarnRule(WarnRule node) async { var value = await _addExceptionSpanAsync(node, () => node.expression.accept(this)); _logger.warn( @@ -1909,7 +1986,7 @@ class _EvaluateVisitor return null; } - Future visitWhileRule(WhileRule node) { + Future visitWhileRule(WhileRule node) { return _environment.scope(() async { while ((await node.condition.accept(this)).isTruthy) { var result = await _handleReturn( @@ -1980,13 +2057,13 @@ class _EvaluateVisitor } else { return result; } - break; case BinaryOperator.modulo: var right = await node.right.accept(this); return left.modulo(right); + default: - return null; + throw ArgumentError("Unknown binary operator ${node.operator}."); } }); } @@ -2028,9 +2105,9 @@ class _EvaluateVisitor _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.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"]!; return await ((await condition.accept(this)).isTruthy ? ifTrue : ifFalse) .accept(this); @@ -2059,12 +2136,15 @@ class _EvaluateVisitor for (var pair in node.pairs) { var keyValue = await pair.item1.accept(this); var valueValue = await pair.item2.accept(this); - if (map.containsKey(keyValue)) { + + var oldValue = map[keyValue]; + if (oldValue != null) { + var oldValueSpan = keyNodes[keyValue]?.span; throw MultiSpanSassRuntimeException( 'Duplicate key.', pair.item1.span, 'second key', - {keyNodes[keyValue].span: 'first key'}, + {if (oldValueSpan != null) oldValueSpan: 'first key'}, _stackTrace(pair.item1.span)); } map[keyValue] = valueValue; @@ -2075,7 +2155,7 @@ class _EvaluateVisitor Future visitFunctionExpression(FunctionExpression node) async { var plainName = node.name.asPlain; - AsyncCallable function; + AsyncCallable? function; if (plainName != null) { function = _addExceptionSpan( node, @@ -2106,7 +2186,7 @@ class _EvaluateVisitor /// Like `_environment.getFunction`, but also returns built-in /// globally-available functions. - AsyncCallable _getFunction(String name, {String namespace}) { + AsyncCallable? _getFunction(String name, {String? namespace}) { var local = _environment.getFunction(name, namespace: namespace); if (local != null || namespace != null) return local; return _builtInFunctions[name]; @@ -2114,14 +2194,16 @@ class _EvaluateVisitor /// Evaluates the arguments in [arguments] as applied to [callable], and /// invokes [run] in a scope with those arguments defined. - Future _runUserDefinedCallable( + Future _runUserDefinedCallable( ArgumentInvocation arguments, UserDefinedCallable callable, AstNode nodeWithSpan, - Future run()) async { + Future run()) async { var evaluated = await _evaluateArguments(arguments); - var name = callable.name == null ? "@content" : callable.name + "()"; + var name = callable.name; + if (name != "@content") name += "()"; + return await _withStackFrame(name, nodeWithSpan, () { // Add an extra closure() call so that modifications to the environment // don't affect the underlying environment closure. @@ -2137,7 +2219,7 @@ class _EvaluateVisitor _environment.setLocalVariable( declaredArguments[i].name, evaluated.positional[i].withoutSlash(), - _sourceMap ? evaluated.positionalNodes[i] : null); + evaluated.positionalNodes?[i]); } for (var i = evaluated.positional.length; @@ -2145,18 +2227,17 @@ class _EvaluateVisitor i++) { var argument = declaredArguments[i]; var value = evaluated.named.remove(argument.name) ?? - await argument.defaultValue.accept(this); + await argument.defaultValue!.accept>(this); _environment.setLocalVariable( argument.name, value.withoutSlash(), - _sourceMap - ? evaluated.namedNodes[argument.name] ?? - _expressionNode(argument.defaultValue) - : null); + evaluated.namedNodes?[argument.name] ?? + argument.defaultValue.andThen(_expressionNode)); } - SassArgumentList argumentList; - if (callable.declaration.arguments.restArgument != null) { + SassArgumentList? argumentList; + var restArgument = callable.declaration.arguments.restArgument; + if (restArgument != null) { var rest = evaluated.positional.length > declaredArguments.length ? evaluated.positional.sublist(declaredArguments.length) : const []; @@ -2167,9 +2248,7 @@ class _EvaluateVisitor ? ListSeparator.comma : evaluated.separator); _environment.setLocalVariable( - callable.declaration.arguments.restArgument, - argumentList, - nodeWithSpan); + restArgument, argumentList, nodeWithSpan); } var result = await run(); @@ -2194,15 +2273,10 @@ class _EvaluateVisitor /// Evaluates [arguments] as applied to [callable]. Future _runFunctionCallable(ArgumentInvocation arguments, - AsyncCallable callable, AstNode nodeWithSpan) async { + AsyncCallable? callable, AstNode nodeWithSpan) async { if (callable is AsyncBuiltInCallable) { - var result = await _runBuiltInCallable(arguments, callable, nodeWithSpan); - if (result == null) { - throw _exception( - "Custom functions may not return Dart's null.", nodeWithSpan.span); - } - - return result.withoutSlash(); + return (await _runBuiltInCallable(arguments, callable, nodeWithSpan)) + .withoutSlash(); } else if (callable is UserDefinedCallable) { return (await _runUserDefinedCallable(arguments, callable, nodeWithSpan, () async { @@ -2233,16 +2307,17 @@ class _EvaluateVisitor buffer.write(await _evaluateToCss(argument)); } - var rest = await arguments.rest?.accept(this); - if (rest != null) { + var restArg = arguments.rest; + if (restArg != null) { + var rest = await restArg.accept(this); if (!first) buffer.write(", "); - buffer.write(_serialize(rest, arguments.rest)); + buffer.write(_serialize(rest, restArg)); } buffer.writeCharCode($rparen); return SassString(buffer.toString(), quotes: false); } else { - return null; + throw ArgumentError('Unknown callable type ${callable.runtimeType}.'); } } @@ -2268,10 +2343,10 @@ class _EvaluateVisitor i++) { var argument = declaredArguments[i]; evaluated.positional.add(evaluated.named.remove(argument.name) ?? - await argument.defaultValue?.accept(this)); + await argument.defaultValue!.accept(this)); } - SassArgumentList argumentList; + SassArgumentList? argumentList; if (overload.restArgument != null) { var rest = const []; if (evaluated.positional.length > declaredArguments.length) { @@ -2291,7 +2366,8 @@ class _EvaluateVisitor Value result; try { - result = await callback(evaluated.positional); + result = await withCurrentCallableNode( + nodeWithSpan, () => callback(evaluated.positional)); } on SassRuntimeException { rethrow; } on MultiSpanSassScriptException catch (error) { @@ -2305,9 +2381,9 @@ class _EvaluateVisitor throw MultiSpanSassRuntimeException(error.message, error.span, error.primaryLabel, error.secondarySpans, _stackTrace(error.span)); } catch (error) { - String message; + String? message; try { - message = error.message as String; + message = (error as dynamic).message as String; } catch (_) { message = error.toString(); } @@ -2318,6 +2394,7 @@ class _EvaluateVisitor if (argumentList == null) return result; if (evaluated.named.isEmpty) return result; if (argumentList.wereKeywordsAccessed) return result; + throw MultiSpanSassRuntimeException( "No ${pluralize('argument', evaluated.named.keys.length)} named " "${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.", @@ -2332,7 +2409,7 @@ class _EvaluateVisitor /// If [trackSpans] is `true`, this tracks the source spans of the arguments /// being passed in. It defaults to [_sourceMap]. Future<_ArgumentResults> _evaluateArguments(ArgumentInvocation arguments, - {bool trackSpans}) async { + {bool? trackSpans}) async { trackSpans ??= _sourceMap; var positional = [ @@ -2356,16 +2433,17 @@ class _EvaluateVisitor } : null; - if (arguments.rest == null) { + var restArgs = arguments.rest; + if (restArgs == null) { return _ArgumentResults(positional, named, ListSeparator.undecided, positionalNodes: positionalNodes, namedNodes: namedNodes); } - var rest = await arguments.rest.accept(this); - var restNodeForSpan = trackSpans ? _expressionNode(arguments.rest) : null; + var rest = await restArgs.accept(this); + var restNodeForSpan = _expressionNode(restArgs); var separator = ListSeparator.undecided; if (rest is SassMap) { - _addRestMap(named, rest, arguments.rest); + _addRestMap(named, rest, restArgs, (value) => value); namedNodes?.addAll({ for (var key in rest.contents.keys) (key as SassString).text: restNodeForSpan @@ -2386,16 +2464,16 @@ class _EvaluateVisitor positionalNodes?.add(restNodeForSpan); } - if (arguments.keywordRest == null) { + var keywordRestArgs = arguments.keywordRest; + if (keywordRestArgs == null) { return _ArgumentResults(positional, named, separator, positionalNodes: positionalNodes, namedNodes: namedNodes); } - var keywordRest = await arguments.keywordRest.accept(this); - var keywordRestNodeForSpan = - trackSpans ? _expressionNode(arguments.keywordRest) : null; + var keywordRest = await keywordRestArgs.accept(this); + var keywordRestNodeForSpan = _expressionNode(keywordRestArgs); if (keywordRest is SassMap) { - _addRestMap(named, keywordRest, arguments.keywordRest); + _addRestMap(named, keywordRest, keywordRestArgs, (value) => value); namedNodes?.addAll({ for (var key in keywordRest.contents.keys) (key as SassString).text: keywordRestNodeForSpan @@ -2405,7 +2483,7 @@ class _EvaluateVisitor } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", - arguments.keywordRest.span); + keywordRestArgs.span); } } @@ -2416,40 +2494,44 @@ class _EvaluateVisitor /// for macros such as `if()`. Future, Map>> _evaluateMacroArguments(CallableInvocation invocation) async { - if (invocation.arguments.rest == null) { + var restArgs_ = invocation.arguments.rest; + if (restArgs_ == null) { return Tuple2( invocation.arguments.positional, invocation.arguments.named); } + var restArgs = restArgs_; // dart-lang/sdk#45348 var positional = invocation.arguments.positional.toList(); var named = Map.of(invocation.arguments.named); - var rest = await invocation.arguments.rest.accept(this); + var rest = await restArgs.accept(this); if (rest is SassMap) { - _addRestMap(named, rest, invocation, (value) => ValueExpression(value)); + _addRestMap(named, rest, invocation, + (value) => ValueExpression(value, restArgs.span)); } else if (rest is SassList) { - positional.addAll(rest.asList.map((value) => ValueExpression(value))); + positional.addAll( + rest.asList.map((value) => ValueExpression(value, restArgs.span))); if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { - named[key] = ValueExpression(value); + named[key] = ValueExpression(value, restArgs.span); }); } } else { - positional.add(ValueExpression(rest)); + positional.add(ValueExpression(rest, restArgs.span)); } - if (invocation.arguments.keywordRest == null) { - return Tuple2(positional, named); - } + var keywordRestArgs_ = invocation.arguments.keywordRest; + if (keywordRestArgs_ == null) return Tuple2(positional, named); + var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348 - var keywordRest = await invocation.arguments.keywordRest.accept(this); + var keywordRest = await keywordRestArgs.accept(this); if (keywordRest is SassMap) { - _addRestMap( - named, keywordRest, invocation, (value) => ValueExpression(value)); + _addRestMap(named, keywordRest, invocation, + (value) => ValueExpression(value, keywordRestArgs.span)); return Tuple2(positional, named); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", - invocation.span); + keywordRestArgs.span); } } @@ -2465,8 +2547,7 @@ class _EvaluateVisitor /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. void _addRestMap(Map values, SassMap map, AstNode nodeWithSpan, - [T convert(Value value)]) { - convert ??= (value) => value as T; + T convert(Value value)) { map.contents.forEach((key, value) { if (key is SassString) { values[key.text] = convert(value); @@ -2486,10 +2567,8 @@ class _EvaluateVisitor _addExceptionSpan( nodeWithSpan, () => arguments.verify(positional, MapKeySet(named))); - Future visitSelectorExpression(SelectorExpression node) async { - if (_styleRule == null) return sassNull; - return _styleRule.originalSelector.asSassList; - } + Future visitSelectorExpression(SelectorExpression node) async => + _styleRuleIgnoringAtRoot?.originalSelector.asSassList ?? sassNull; Future visitStringExpression(StringExpression node) async { // Don't use [performInterpolation] here because we need to get the raw text @@ -2585,8 +2664,7 @@ class _EvaluateVisitor _root.addChild(modifiableNode); _endOfImports++; } else { - _outOfOrderImports ??= []; - _outOfOrderImports.add(modifiableNode); + (_outOfOrderImports ??= []).add(modifiableNode); } } @@ -2611,16 +2689,16 @@ class _EvaluateVisitor "Media rules may not be used within nested declarations.", node.span); } - var mergedQueries = _mediaQueries == null - ? null - : _mergeMediaQueries(_mediaQueries, node.queries); + var mergedQueries = _mediaQueries.andThen( + (mediaQueries) => _mergeMediaQueries(mediaQueries, node.queries)); if (mergedQueries != null && mergedQueries.isEmpty) return null; await _withParent( ModifiableCssMediaRule(mergedQueries ?? node.queries, node.span), () async { await _withMediaQueries(mergedQueries ?? node.queries, () async { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { await child.accept(this); } @@ -2630,7 +2708,7 @@ class _EvaluateVisitor // // For example, "a {@media screen {b: c}}" should produce // "@media screen {a {b: c}}". - await _withParent(_styleRule.copyWithoutChildren(), () async { + await _withParent(styleRule.copyWithoutChildren(), () async { for (var child in node.children) { await child.accept(this); } @@ -2653,10 +2731,11 @@ class _EvaluateVisitor "Style rules may not be used within nested declarations.", node.span); } + var styleRule = _styleRule; var originalSelector = node.selector.value.resolveParentSelectors( - _styleRule?.originalSelector, + styleRule?.originalSelector, implicitParent: !_atRootExcludingStyleRule); - var selector = _extender.addSelector( + var selector = _extensionStore.addSelector( originalSelector, node.selector.span, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: originalSelector); @@ -2671,7 +2750,7 @@ class _EvaluateVisitor }, through: (node) => node is CssStyleRule, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - if (!_inStyleRule && _parent.children.isNotEmpty) { + if (styleRule == null && _parent.children.isNotEmpty) { var lastChild = _parent.children.last; lastChild.isGroupEnd = true; } @@ -2695,7 +2774,8 @@ class _EvaluateVisitor await _withParent(ModifiableCssSupportsRule(node.condition, node.span), () async { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { await child.accept(this); } @@ -2705,7 +2785,7 @@ class _EvaluateVisitor // // For example, "a {@supports (a: b) {b: c}}" should produce "@supports // (a: b) {a {b: c}}". - await _withParent(_styleRule.copyWithoutChildren(), () async { + await _withParent(styleRule.copyWithoutChildren(), () async { for (var child in node.children) { await child.accept(this); } @@ -2720,8 +2800,8 @@ class _EvaluateVisitor /// /// Returns the value returned by [callback], or `null` if it only ever /// returned `null`. - Future _handleReturn( - List list, Future callback(T value)) async { + Future _handleReturn( + List list, Future callback(T value)) async { for (var value in list) { var result = await callback(value); if (result != null) return result; @@ -2768,7 +2848,8 @@ class _EvaluateVisitor namesByColor.containsKey(result)) { var alternative = BinaryOperationExpression( BinaryOperator.plus, - StringExpression(Interpolation([""], null), quotes: true), + StringExpression(Interpolation([""], interpolation.span), + quotes: true), expression); _warn( "You probably don't mean to use the color value " @@ -2807,13 +2888,15 @@ class _EvaluateVisitor /// where that variable was originally declared. Otherwise, this will just /// return [expression]. /// - /// Returns `null` if [_sourceMap] is `false`. - /// /// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. AstNode _expressionNode(Expression expression) { - if (!_sourceMap) return null; + // If we aren't making a source map this doesn't matter, but we still return + // the expression so we don't have to make the type (and everything + // downstream of it) nullable. + if (!_sourceMap) return expression; + if (expression is VariableExpression) { return _environment.getVariableNode(expression.name, namespace: expression.namespace) ?? @@ -2833,7 +2916,7 @@ class _EvaluateVisitor /// Runs [callback] in a new environment scope unless [scopeWhen] is false. Future _withParent( S node, Future callback(), - {bool through(CssNode node), bool scopeWhen = true}) async { + {bool through(CssNode node)?, bool scopeWhen = true}) async { _addChild(node, through: through); var oldParent = _parent; @@ -2849,19 +2932,25 @@ class _EvaluateVisitor /// If [through] is passed, [node] is added as a child of the first parent for /// which [through] returns `false` instead. That parent is copied unless it's the /// lattermost child of its parent. - void _addChild(ModifiableCssNode node, {bool through(CssNode node)}) { + void _addChild(ModifiableCssNode node, {bool through(CssNode node)?}) { // Go up through parents that match [through]. var parent = _parent; if (through != null) { while (through(parent)) { - parent = parent.parent; + var grandparent = parent.parent; + if (grandparent == null) { + 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 // the parent. Instead, we should create a copy and add it after the // interstitial sibling. if (parent.hasFollowingSibling) { - var grandparent = parent.parent; + // A node with siblings must have a parent + var grandparent = parent.parent!; parent = parent.copyWithoutChildren(); grandparent.addChild(parent); } @@ -2873,16 +2962,16 @@ class _EvaluateVisitor /// Runs [callback] with [rule] as the current style rule. Future _withStyleRule( ModifiableCssStyleRule rule, Future callback()) async { - var oldRule = _styleRule; - _styleRule = rule; + var oldRule = _styleRuleIgnoringAtRoot; + _styleRuleIgnoringAtRoot = rule; var result = await callback(); - _styleRule = oldRule; + _styleRuleIgnoringAtRoot = oldRule; return result; } /// Runs [callback] with [queries] as the current media queries. Future _withMediaQueries( - List queries, Future callback()) async { + List? queries, Future callback()) async { var oldMediaQueries = _mediaQueries; _mediaQueries = queries; var result = await callback(); @@ -2911,16 +3000,13 @@ class _EvaluateVisitor /// Creates a new stack frame with location information from [member] and /// [span]. - Frame _stackFrame(String member, FileSpan span) { - var url = span.sourceUrl; - if (url != null && _importCache != null) url = _importCache.humanize(url); - return frameForSpan(span, member, url: url); - } + Frame _stackFrame(String member, FileSpan span) => frameForSpan(span, member, + url: span.sourceUrl.andThen((url) => _importCache?.humanize(url) ?? url)); /// Returns a stack trace at the current point. /// /// If [span] is passed, it's used for the innermost stack frame. - Trace _stackTrace([FileSpan span]) { + Trace _stackTrace([FileSpan? span]) { var frames = [ ..._stack.map((tuple) => _stackFrame(tuple.item1, tuple.item2.span)), if (span != null) _stackFrame(_member, span) @@ -2936,7 +3022,7 @@ class _EvaluateVisitor /// Returns a [SassRuntimeException] with the given [message]. /// /// If [span] is passed, it's used for the innermost stack frame. - SassRuntimeException _exception(String message, [FileSpan span]) => + SassRuntimeException _exception(String message, [FileSpan? span]) => SassRuntimeException( message, span ?? _stack.last.item2.span, _stackTrace(span)); @@ -3064,8 +3150,7 @@ class _ImportedCssVisitor implements ModifiableCssVisitor { _visitor._addChild(node); _visitor._endOfImports++; } else { - _visitor._outOfOrderImports ??= []; - _visitor._outOfOrderImports.add(node); + (_visitor._outOfOrderImports ??= []).add(node); } } @@ -3077,9 +3162,9 @@ class _ImportedCssVisitor implements ModifiableCssVisitor { // Whether [node.query] has been merged with [_visitor._mediaQueries]. If it // has been merged, merging again is a no-op; if it hasn't been merged, // merging again will fail. - var hasBeenMerged = _visitor._mediaQueries == null || - _visitor._mergeMediaQueries(_visitor._mediaQueries, node.queries) != - null; + var mediaQueries = _visitor._mediaQueries; + var hasBeenMerged = mediaQueries == null || + _visitor._mergeMediaQueries(mediaQueries, node.queries) != null; _visitor._addChild(node, through: (node) => @@ -3126,7 +3211,7 @@ class _ArgumentResults { /// 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; + final List? positionalNodes; /// Arguments passed by name. final Map named; @@ -3137,7 +3222,7 @@ class _ArgumentResults { /// 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; + final Map? namedNodes; /// The separator used for the rest argument list, if any. final ListSeparator separator; diff --git a/lib/src/visitor/clone_css.dart b/lib/src/visitor/clone_css.dart index ae4fe1151..ba4bf7333 100644 --- a/lib/src/visitor/clone_css.dart +++ b/lib/src/visitor/clone_css.dart @@ -7,27 +7,27 @@ import 'package:tuple/tuple.dart'; import '../ast/css.dart'; import '../ast/css/modifiable.dart'; import '../ast/selector.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; import 'interface/css.dart'; /// Returns deep copies of both [stylesheet] and [extender]. /// /// The [extender] must be associated with [stylesheet]. -Tuple2 cloneCssStylesheet( - CssStylesheet stylesheet, Extender extender) { - var result = extender.clone(); - var newExtender = result.item1; +Tuple2 cloneCssStylesheet( + CssStylesheet stylesheet, ExtensionStore extensionStore) { + var result = extensionStore.clone(); + var newExtensionStore = result.item1; var oldToNewSelectors = result.item2; return Tuple2( _CloneCssVisitor(oldToNewSelectors).visitCssStylesheet(stylesheet), - newExtender); + newExtensionStore); } /// A visitor that creates a deep (and mutable) copy of a [CssStylesheet]. class _CloneCssVisitor implements CssVisitor { /// A map from selectors in the original stylesheet to selectors generated for - /// the new stylesheet using [Extender.clone]. + /// the new stylesheet using [ExtensionStore.clone]. final Map, ModifiableCssValue> _oldToNewSelectors; @@ -62,8 +62,8 @@ class _CloneCssVisitor implements CssVisitor { var newSelector = _oldToNewSelectors[node.selector]; if (newSelector == null) { throw StateError( - "The Extender and CssStylesheet passed to cloneCssStylesheet() must " - "come from the same compilation."); + "The ExtensionStore and CssStylesheet passed to cloneCssStylesheet() " + "must come from the same compilation."); } return _visitChildren( diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index aa2df383e..dc2b70566 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: 0e9e7b6b99be25500cfd6469b0366190e92c43f4 +// Checksum: 6b82405fdc448ac69ca703bc6bffbb8d50f3fced // // ignore_for_file: unused_import @@ -16,7 +16,6 @@ import 'dart:math' as math; import 'package:charcode/charcode.dart'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; @@ -34,7 +33,7 @@ import '../color_names.dart'; import '../configuration.dart'; import '../configured_value.dart'; import '../exception.dart'; -import '../extend/extender.dart'; +import '../extend/extension_store.dart'; import '../extend/extension.dart'; import '../functions.dart'; import '../functions/meta.dart' as meta; @@ -46,8 +45,8 @@ import '../module.dart'; import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; import '../syntax.dart'; -import '../util/fixed_length_list_builder.dart'; import '../utils.dart'; +import '../util/nullable.dart'; import '../value.dart'; import '../warn.dart'; import 'interface/css.dart'; @@ -80,11 +79,11 @@ typedef _ScopeCallback = void Function(void Function() callback); /// /// Throws a [SassRuntimeException] if evaluation fails. EvaluateResult evaluate(Stylesheet stylesheet, - {ImportCache importCache, - NodeImporter nodeImporter, - Importer importer, - Iterable functions, - Logger logger, + {ImportCache? importCache, + NodeImporter? nodeImporter, + Importer? importer, + Iterable? functions, + Logger? logger, bool sourceMap = false}) => _EvaluateVisitor( importCache: importCache, @@ -100,17 +99,17 @@ class Evaluator { /// The visitor that evaluates each expression and statement. final _EvaluateVisitor _visitor; - /// The importer to use to resolve `@use` rules in [_importer]. - final Importer _importer; + /// The importer to use to resolve `@use` rules in [_visitor]. + final Importer? _importer; /// Creates an evaluator. /// /// Arguments are the same as for [evaluate]. Evaluator( - {ImportCache importCache, - Importer importer, - Iterable functions, - Logger logger}) + {ImportCache? importCache, + Importer? importer, + Iterable? functions, + Logger? logger}) : _visitor = _EvaluateVisitor( importCache: importCache, functions: functions, logger: logger), _importer = importer; @@ -127,15 +126,15 @@ class Evaluator { /// A visitor that executes Sass code to produce a CSS tree. class _EvaluateVisitor implements - StatementVisitor, + StatementVisitor, ExpressionVisitor, CssVisitor { /// The import cache used to import other stylesheets. - final ImportCache _importCache; + final ImportCache? _importCache; /// The Node Sass-compatible importer to use when loading new Sass files when /// compiled to Node.js. - final NodeImporter _nodeImporter; + final NodeImporter? _nodeImporter; /// Built-in functions that are globally-acessible, even under the new module /// system. @@ -164,16 +163,22 @@ class _EvaluateVisitor Environment _environment; /// The style rule that defines the current parent selector, if any. - ModifiableCssStyleRule _styleRule; + /// + /// This doesn't take into consideration any intermediate `@at-root` rules. In + /// the common case where those rules are relevant, use [_styleRule] instead. + ModifiableCssStyleRule? _styleRuleIgnoringAtRoot; /// The current media queries, if any. - List _mediaQueries; + List? _mediaQueries; /// The current parent node in the output CSS tree. - ModifiableCssParentNode _parent; + ModifiableCssParentNode get _parent => _assertInModule(__parent, "__parent"); + set _parent(ModifiableCssParentNode value) => __parent = value; + + ModifiableCssParentNode? __parent; /// The name of the current declaration parent. - String _declarationName; + String? _declarationName; /// The human-readable name of the current stack frame. var _member = "root stylesheet"; @@ -184,12 +189,12 @@ class _EvaluateVisitor /// [AstNode] rather than a [FileSpan] so we can avoid calling [AstNode.span] /// if the span isn't required, since some nodes need to do real work to /// manufacture a source span. - AstNode _callableNode; + AstNode? _callableNode; /// The span for the current import that's being resolved. /// /// This is used to produce warnings for importers. - FileSpan _importSpan; + FileSpan? _importSpan; /// Whether we're currently executing a function. var _inFunction = false; @@ -197,8 +202,8 @@ class _EvaluateVisitor /// Whether we're currently building the output of an unknown at rule. var _inUnknownAtRule = false; - /// Whether we're currently building the output of a style rule. - bool get _inStyleRule => _styleRule != null && !_atRootExcludingStyleRule; + ModifiableCssStyleRule? get _styleRule => + _atRootExcludingStyleRule ? null : _styleRuleIgnoringAtRoot; /// Whether we're directly within an `@at-root` rule that excludes style /// rules. @@ -223,7 +228,7 @@ class _EvaluateVisitor /// entrypoint module). /// /// This is used to ensure that we don't get into an infinite load loop. - final _activeModules = {}; + final _activeModules = {}; /// The dynamic call stack representing function invocations, mixin /// invocations, and imports surrounding the current context. @@ -245,17 +250,23 @@ class _EvaluateVisitor /// /// If this is `null`, relative imports aren't supported in the current /// stylesheet. - Importer _importer; + Importer? _importer; /// The stylesheet that's currently being evaluated. - Stylesheet _stylesheet; + Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet"); + set _stylesheet(Stylesheet value) => __stylesheet = value; + Stylesheet? __stylesheet; /// The root stylesheet node. - ModifiableCssStylesheet _root; + ModifiableCssStylesheet get _root => _assertInModule(__root, "_root"); + set _root(ModifiableCssStylesheet value) => __root = value; + ModifiableCssStylesheet? __root; /// The first index in [_root.children] after the initial block of CSS /// imports. - int _endOfImports; + int get _endOfImports => _assertInModule(__endOfImports, "_endOfImports"); + set _endOfImports(int value) => __endOfImports = value; + int? __endOfImports; /// Plain-CSS imports that didn't appear in the initial block of CSS imports. /// @@ -264,11 +275,14 @@ class _EvaluateVisitor /// /// This is `null` unless there are any out-of-order imports in the current /// stylesheet. - List _outOfOrderImports; + List? _outOfOrderImports; - /// The extender that tracks extensions and style rules for the current + /// The extension store that tracks extensions and style rules for the current /// module. - Extender _extender; + ExtensionStore get _extensionStore => + _assertInModule(__extensionStore, "_extensionStore"); + set _extensionStore(ExtensionStore value) => __extensionStore = value; + ExtensionStore? __extensionStore; /// The configuration for the current module. /// @@ -279,10 +293,10 @@ class _EvaluateVisitor /// /// Most arguments are the same as those to [evaluate]. _EvaluateVisitor( - {ImportCache importCache, - NodeImporter nodeImporter, - Iterable functions, - Logger logger, + {ImportCache? importCache, + NodeImporter? nodeImporter, + Iterable? functions, + Logger? logger, bool sourceMap = false}) : _importCache = nodeImporter == null ? importCache ?? ImportCache.none(logger: logger) @@ -377,7 +391,7 @@ class _EvaluateVisitor var callable = css ? PlainCssCallable(name.text) : _addExceptionSpan( - _callableNode, + _callableNode!, () => _getFunction(name.text.replaceAll("_", "-"), namespace: module?.text)); if (callable != null) return SassFunction(callable); @@ -389,8 +403,9 @@ class _EvaluateVisitor var function = arguments[0]; var args = arguments[1] as SassArgumentList; - var invocation = ArgumentInvocation([], {}, _callableNode.span, - rest: ValueExpression(args, _callableNode.span), + var callableNode = _callableNode!; + var invocation = ArgumentInvocation([], {}, callableNode.span, + rest: ValueExpression(args, callableNode.span), keywordRest: args.keywords.isEmpty ? null : ValueExpression( @@ -398,7 +413,7 @@ class _EvaluateVisitor for (var entry in args.keywords.entries) SassString(entry.key, quotes: false): entry.value }), - _callableNode.span)); + callableNode.span)); if (function is SassString) { warn( @@ -406,16 +421,17 @@ class _EvaluateVisitor "in Dart Sass 2.0.0. Use call(get-function($function)) instead.", deprecation: true); + var callableNode = _callableNode!; var expression = FunctionExpression( - Interpolation([function.text], _callableNode.span), + Interpolation([function.text], callableNode.span), invocation, - _callableNode.span); + callableNode.span); return expression.accept(this); } var callable = function.assertFunction("function").callable; if (callable is Callable) { - return _runFunctionCallable(invocation, callable, _callableNode); + return _runFunctionCallable(invocation, callable, _callableNode!); } else { throw SassScriptException( "The function ${callable.name} is asynchronous.\n" @@ -427,12 +443,13 @@ class _EvaluateVisitor var metaMixins = [ BuiltInCallable.mixin("load-css", r"$url, $with: null", (arguments) { var url = Uri.parse(arguments[0].assertString("url").text); - var withMap = arguments[1].realNull?.assertMap("with")?.contents; + var withMap = arguments[1].realNull?.assertMap("with").contents; + var callableNode = _callableNode!; var configuration = const Configuration.empty(); if (withMap != null) { var values = {}; - var span = _callableNode.span; + var span = callableNode.span; withMap.forEach((variable, value) { var name = variable.assertString("with key").text.replaceAll("_", "-"); @@ -440,14 +457,14 @@ class _EvaluateVisitor throw "The variable \$$name was configured twice."; } - values[name] = ConfiguredValue(value, span); + values[name] = ConfiguredValue.explicit(value, span, callableNode); }); - configuration = Configuration(values, _callableNode); + configuration = ExplicitConfiguration(values, callableNode); } - _loadModule(url, "load-css()", _callableNode, + _loadModule(url, "load-css()", callableNode, (module) => _combineCss(module, clone: true).accept(this), - baseUrl: _callableNode.span?.sourceUrl, + baseUrl: callableNode.span.sourceUrl, configuration: configuration, namesInErrors: true); _assertConfigurationIsEmpty(configuration, nameInError: true); @@ -469,9 +486,9 @@ class _EvaluateVisitor } } - EvaluateResult run(Importer importer, Stylesheet node) { - return _withWarnCallback(() { - var url = node.span?.sourceUrl; + EvaluateResult run(Importer? importer, Stylesheet node) { + return _withWarnCallback(node, () { + var url = node.span.sourceUrl; if (url != null) { _activeModules[url] = null; if (_asNodeSass) { @@ -489,37 +506,55 @@ class _EvaluateVisitor }); } - Value runExpression(Importer importer, Expression expression) => - _withWarnCallback(() => _withFakeStylesheet( - importer, expression, () => expression.accept(this))); + Value runExpression(Importer? importer, Expression expression) => + _withWarnCallback( + expression, + () => _withFakeStylesheet( + importer, expression, () => expression.accept(this))); - void runStatement(Importer importer, Statement statement) => - _withWarnCallback(() => _withFakeStylesheet( - importer, statement, () => statement.accept(this))); + void runStatement(Importer? importer, Statement statement) => + _withWarnCallback( + statement, + () => _withFakeStylesheet( + importer, statement, () => statement.accept(this))); /// Runs [callback] with a definition for the top-level `warn` function. - T _withWarnCallback(T callback()) { + /// + /// If no other span can be found to report a warning, falls back on + /// [nodeWithSpan]'s. + T _withWarnCallback(AstNode nodeWithSpan, T callback()) { return withWarnCallback( (message, deprecation) => _warn( - message, _importSpan ?? _callableNode.span, + message, _importSpan ?? _callableNode?.span ?? nodeWithSpan.span, deprecation: deprecation), callback); } + /// Asserts that [value] is not `null` and returns it. + /// + /// This is used for fields that are set whenever the evaluator is evaluating + /// a module, which is to say essentially all the time (unless running via + /// [runExpression] or [runStatement]). + T _assertInModule(T? value, String name) { + if (value != null) return value; + throw StateError("Can't access $name outside of a module."); + } + /// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet] /// with [nodeWithSpan]'s source span. T _withFakeStylesheet( - Importer importer, AstNode nodeWithSpan, T callback()) { + Importer? importer, AstNode nodeWithSpan, T callback()) { var oldImporter = _importer; _importer = importer; - var oldStylesheet = _stylesheet; + + assert(__stylesheet == null); _stylesheet = Stylesheet(const [], nodeWithSpan.span); try { return callback(); } finally { _importer = oldImporter; - _stylesheet = oldStylesheet; + __stylesheet = null; } } @@ -541,15 +576,17 @@ class _EvaluateVisitor /// the stack frame for the duration of the [callback]. void _loadModule(Uri url, String stackFrame, AstNode nodeWithSpan, void callback(Module module), - {Uri baseUrl, Configuration configuration, bool namesInErrors = false}) { + {Uri? baseUrl, + Configuration? configuration, + bool namesInErrors = false}) { var builtInModule = _builtInModules[url]; if (builtInModule != null) { - if (configuration != null && !configuration.isImplicit) { + if (configuration is ExplicitConfiguration) { throw _exception( namesInErrors ? "Built-in module $url can't be configured." : "Built-in modules can't be configured.", - nodeWithSpan.span); + configuration.nodeWithSpan.span); } _addExceptionSpan(nodeWithSpan, () => callback(builtInModule)); @@ -563,18 +600,18 @@ class _EvaluateVisitor var stylesheet = result.item2; var canonicalUrl = stylesheet.span.sourceUrl; - if (_activeModules.containsKey(canonicalUrl)) { + 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."; - var previousLoad = _activeModules[canonicalUrl]; - throw previousLoad == null - ? _exception(message) - : _multiSpanException( - message, "new load", {previousLoad.span: "original load"}); + + throw _activeModules[canonicalUrl].andThen((previousLoad) => + _multiSpanException(message, "new load", + {previousLoad.span: "original load"})) ?? + _exception(message); } - _activeModules[canonicalUrl] = nodeWithSpan; + if (canonicalUrl != null) _activeModules[canonicalUrl] = nodeWithSpan; Module module; try { @@ -612,26 +649,29 @@ class _EvaluateVisitor /// If [namesInErrors] is `true`, this includes the names of modules in errors /// relating to them. This should only be `true` if the names won't be obvious /// from the source span. - Module _execute(Importer importer, Stylesheet stylesheet, - {Configuration configuration, - AstNode nodeWithSpan, + Module _execute(Importer? importer, Stylesheet stylesheet, + {Configuration? configuration, + AstNode? nodeWithSpan, bool namesInErrors = false}) { var url = stylesheet.span.sourceUrl; var alreadyLoaded = _modules[url]; if (alreadyLoaded != null) { - if (!(configuration ?? _configuration).isImplicit) { + var currentConfiguration = configuration ?? _configuration; + if (currentConfiguration is ExplicitConfiguration) { var message = namesInErrors ? "${p.prettyUri(url)} was already loaded, so it can't be " "configured using \"with\"." : "This module was already loaded, so it can't be configured using " "\"with\"."; - var existingNode = _moduleNodes[url]; + var existingSpan = _moduleNodes[url]?.span; + var configurationSpan = configuration == null + ? currentConfiguration.nodeWithSpan.span + : null; var secondarySpans = { - if (existingNode != null) existingNode.span: "original load", - if (configuration == null) - _configuration.nodeWithSpan.span: "configuration" + if (existingSpan != null) existingSpan: "original load", + if (configurationSpan != null) configurationSpan: "configuration" }; throw secondarySpans.isEmpty @@ -643,16 +683,16 @@ class _EvaluateVisitor } var environment = Environment(sourceMap: _sourceMap); - CssStylesheet css; - var extender = Extender(); + late CssStylesheet css; + var extensionStore = ExtensionStore(); _withEnvironment(environment, () { var oldImporter = _importer; - var oldStylesheet = _stylesheet; - var oldRoot = _root; - var oldParent = _parent; - var oldEndOfImports = _endOfImports; + var oldStylesheet = __stylesheet; + var oldRoot = __root; + var oldParent = __parent; + var oldEndOfImports = __endOfImports; var oldOutOfOrderImports = _outOfOrderImports; - var oldExtender = _extender; + var oldExtensionStore = __extensionStore; var oldStyleRule = _styleRule; var oldMediaQueries = _mediaQueries; var oldDeclarationName = _declarationName; @@ -662,12 +702,12 @@ class _EvaluateVisitor var oldConfiguration = _configuration; _importer = importer; _stylesheet = stylesheet; - _root = ModifiableCssStylesheet(stylesheet.span); - _parent = _root; + var root = __root = ModifiableCssStylesheet(stylesheet.span); + _parent = root; _endOfImports = 0; _outOfOrderImports = null; - _extender = extender; - _styleRule = null; + _extensionStore = extensionStore; + _styleRuleIgnoringAtRoot = null; _mediaQueries = null; _declarationName = null; _inUnknownAtRule = false; @@ -677,17 +717,17 @@ class _EvaluateVisitor visitStylesheet(stylesheet); css = _outOfOrderImports == null - ? _root + ? root : CssStylesheet(_addOutOfOrderImports(), stylesheet.span); _importer = oldImporter; - _stylesheet = oldStylesheet; - _root = oldRoot; - _parent = oldParent; - _endOfImports = oldEndOfImports; + __stylesheet = oldStylesheet; + __root = oldRoot; + __parent = oldParent; + __endOfImports = oldEndOfImports; _outOfOrderImports = oldOutOfOrderImports; - _extender = oldExtender; - _styleRule = oldStyleRule; + __extensionStore = oldExtensionStore; + _styleRuleIgnoringAtRoot = oldStyleRule; _mediaQueries = oldMediaQueries; _declarationName = oldDeclarationName; _inUnknownAtRule = oldInUnknownAtRule; @@ -696,23 +736,26 @@ class _EvaluateVisitor _configuration = oldConfiguration; }); - var module = environment.toModule(css, extender); - _modules[url] = module; - _moduleNodes[url] = nodeWithSpan; + var module = environment.toModule(css, extensionStore); + if (url != null) { + _modules[url] = module; + if (nodeWithSpan != null) _moduleNodes[url] = nodeWithSpan; + } + return module; } /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted /// after [_endOfImports], if necessary. List _addOutOfOrderImports() { - if (_outOfOrderImports == null) return _root.children; + var outOfOrderImports = _outOfOrderImports; + if (outOfOrderImports == null) return _root.children; - var statements = FixedLengthListBuilder( - _root.children.length + _outOfOrderImports.length) - ..addRange(_root.children, 0, _endOfImports) - ..addAll(_outOfOrderImports) - ..addRange(_root.children, _endOfImports); - return statements.build(); + return [ + ..._root.children.take(_endOfImports), + ...outOfOrderImports, + ..._root.children.skip(_endOfImports) + ]; } /// Returns a new stylesheet containing [root]'s CSS as well as the CSS of all @@ -724,8 +767,8 @@ class _EvaluateVisitor /// that they don't modify [root] or its dependencies. CssStylesheet _combineCss(Module root, {bool clone = false}) { if (!root.upstream.any((module) => module.transitivelyContainsCss)) { - var selectors = root.extender.simpleSelectors; - var unsatisfiedExtension = firstOrNull(root.extender + var selectors = root.extensionStore.simpleSelectors; + var unsatisfiedExtension = firstOrNull(root.extensionStore .extensionsWhereTarget((target) => !selectors.contains(target))); if (unsatisfiedExtension != null) { _throwForUnsatisfiedExtension(unsatisfiedExtension); @@ -760,11 +803,12 @@ class _EvaluateVisitor /// Extends the selectors in each module with the extensions defined in /// downstream modules. void _extendModules(List> sortedModules) { - // All the extenders directly downstream of a given module (indexed by its - // canonical URL). It's important that we create this in topological order, - // so that by the time we're processing a module we've already filled in all - // its downstream extenders and we can use them to extend that module. - var downstreamExtenders = >{}; + // All the [ExtensionStore]s directly downstream of a given module (indexed + // by its canonical URL). It's important that we create this in topological + // order, so that by the time we're processing a module we've already filled + // in all its downstream [ExtensionStore]s and we can use them to extend + // that module. + var downstreamExtensionStores = >{}; /// Extensions that haven't yet been satisfied by some upstream module. This /// adds extensions when they're defined but not satisfied, and removes them @@ -772,31 +816,35 @@ class _EvaluateVisitor var unsatisfiedExtensions = Set.identity(); for (var module in sortedModules) { - // Create a snapshot of the simple selectors currently in the extender so - // that we don't consider an extension "satisfied" below because of a - // simple selector added by another (sibling) extension. - var originalSelectors = module.extender.simpleSelectors.toSet(); + // Create a snapshot of the simple selectors currently in the + // [ExtensionStore] so that we don't consider an extension "satisfied" + // below because of a simple selector added by another (sibling) + // extension. + var originalSelectors = module.extensionStore.simpleSelectors.toSet(); // Add all as-yet-unsatisfied extensions before adding downstream - // extenders, because those are all in [unsatisfiedExtensions] already. - unsatisfiedExtensions.addAll(module.extender.extensionsWhereTarget( + // [ExtensionStore]s, because those are all in [unsatisfiedExtensions] + // already. + unsatisfiedExtensions.addAll(module.extensionStore.extensionsWhereTarget( (target) => !originalSelectors.contains(target))); - var extenders = downstreamExtenders[module.url]; - if (extenders != null) module.extender.addExtensions(extenders); - if (module.extender.isEmpty) continue; + downstreamExtensionStores[module.url] + .andThen(module.extensionStore.addExtensions); + if (module.extensionStore.isEmpty) continue; for (var upstream in module.upstream) { - downstreamExtenders - .putIfAbsent(upstream.url, () => []) - .add(module.extender); + var url = upstream.url; + if (url == null) continue; + downstreamExtensionStores + .putIfAbsent(url, () => []) + .add(module.extensionStore); } // Remove all extensions that are now satisfied after adding downstream - // extenders so it counts any downstream extensions that have been newly - // satisfied. - unsatisfiedExtensions.removeAll( - module.extender.extensionsWhereTarget(originalSelectors.contains)); + // [ExtensionStore]s so it counts any downstream extensions that have been + // newly satisfied. + unsatisfiedExtensions.removeAll(module.extensionStore + .extensionsWhereTarget(originalSelectors.contains)); } if (unsatisfiedExtensions.isNotEmpty) { @@ -805,8 +853,7 @@ class _EvaluateVisitor } /// Throws an exception indicating that [extension] is unsatisfied. - @alwaysThrows - void _throwForUnsatisfiedExtension(Extension extension) { + Never _throwForUnsatisfiedExtension(Extension extension) { throw SassException( 'The target selector was not found.\n' 'Use "@extend ${extension.target} !optional" to avoid this error.', @@ -857,26 +904,34 @@ class _EvaluateVisitor // ## Statements - Value visitStylesheet(Stylesheet node) { + Value? visitStylesheet(Stylesheet node) { for (var child in node.children) { child.accept(this); } return null; } - Value visitAtRootRule(AtRootRule node) { + Value? visitAtRootRule(AtRootRule node) { var query = AtRootQuery.defaultQuery; - if (node.query != null) { - var resolved = _performInterpolation(node.query, warnForColor: true); + var unparsedQuery = node.query; + if (unparsedQuery != null) { + var resolved = _performInterpolation(unparsedQuery, warnForColor: true); query = _adjustParseError( - node.query, () => AtRootQuery.parse(resolved, logger: _logger)); + unparsedQuery, () => AtRootQuery.parse(resolved, logger: _logger)); } var parent = _parent; var included = []; while (parent is! CssStylesheet) { if (!query.excludes(parent)) included.add(parent); - parent = parent.parent; + + var grandparent = parent.parent; + if (grandparent == null) { + throw StateError( + "CssNodes must have a CssStylesheet transitive parent node."); + } + + parent = grandparent; } var root = _trimIncluded(included); @@ -891,17 +946,20 @@ class _EvaluateVisitor return null; } - var innerCopy = - included.isEmpty ? null : included.first.copyWithoutChildren(); - var outerCopy = innerCopy; - for (var node in included.skip(1)) { - var copy = node.copyWithoutChildren(); - copy.addChild(outerCopy); - outerCopy = copy; + var innerCopy = root; + if (included.isNotEmpty) { + innerCopy = included.first.copyWithoutChildren(); + var outerCopy = innerCopy; + for (var node in included.skip(1)) { + var copy = node.copyWithoutChildren(); + copy.addChild(outerCopy); + outerCopy = copy; + } + + root.addChild(outerCopy); } - if (outerCopy != null) root.addChild(outerCopy); - _scopeForAtRoot(node, innerCopy ?? root, query, included)(() { + _scopeForAtRoot(node, innerCopy, query, included)(() { for (var child in node.children) { child.accept(this); } @@ -910,8 +968,8 @@ class _EvaluateVisitor return null; } - /// Destructively trims a trailing sublist that matches the current list of - /// parents from [nodes]. + /// Destructively trims a trailing sublist from [nodes] that matches the + /// current list of parents. /// /// [nodes] should be a list of parents included by an `@at-root` rule, from /// innermost to outermost. If it contains a trailing sublist that's @@ -924,19 +982,30 @@ class _EvaluateVisitor if (nodes.isEmpty) return _root; var parent = _parent; - int innermostContiguous; - var i = 0; - for (; i < nodes.length; i++) { + int? innermostContiguous; + for (var i = 0; i < nodes.length; i++) { while (parent != nodes[i]) { innermostContiguous = null; - parent = parent.parent; + + var grandparent = parent.parent; + if (grandparent == null) { + throw ArgumentError( + "Expected ${nodes[i]} to be an ancestor of $this."); + } + + parent = grandparent; } innermostContiguous ??= i; - parent = parent.parent; + + var grandparent = parent.parent; + if (grandparent == null) { + throw ArgumentError("Expected ${nodes[i]} to be an ancestor of $this."); + } + parent = grandparent; } if (parent != _root) return _root; - var root = nodes[innermostContiguous]; + var root = nodes[innermostContiguous!]; nodes.removeRange(innermostContiguous, nodes.length); return root; } @@ -1001,7 +1070,7 @@ class _EvaluateVisitor Value visitContentBlock(ContentBlock node) => throw UnsupportedError( "Evaluation handles @include and its content block together."); - Value visitContentRule(ContentRule node) { + Value? visitContentRule(ContentRule node) { var content = _environment.content; if (content == null) return null; @@ -1015,15 +1084,15 @@ class _EvaluateVisitor return null; } - Value visitDebugRule(DebugRule node) { + Value? visitDebugRule(DebugRule node) { var value = node.expression.accept(this); _logger.debug( value is SassString ? value.text : value.toString(), node.span); return null; } - Value visitDeclaration(Declaration node) { - if (!_inStyleRule && !_inUnknownAtRule && !_inKeyframes) { + Value? visitDeclaration(Declaration node) { + if (_styleRule == null && !_inUnknownAtRule && !_inKeyframes) { throw _exception( "Declarations may only be used within style rules.", node.span); } @@ -1032,9 +1101,8 @@ class _EvaluateVisitor if (_declarationName != null) { name = CssValue("$_declarationName-${name.value}", name.span); } - var cssValue = node.value == null - ? null - : CssValue(node.value.accept(this), node.value.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. @@ -1042,17 +1110,19 @@ class _EvaluateVisitor (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { _parent.addChild(ModifiableCssDeclaration(name, cssValue, node.span, parsedAsCustomProperty: node.isCustomProperty, - valueSpanForMap: _expressionNode(node.value)?.span)); - } else if (name.value.startsWith('--') && node.children == null) { + valueSpanForMap: + _sourceMap ? node.value.andThen(_expressionNode)?.span : null)); + } else if (name.value.startsWith('--') && cssValue != null) { throw _exception( - "Custom property values may not be empty.", node.value.span); + "Custom property values may not be empty.", cssValue.span); } - if (node.children != null) { + var children = node.children; + if (children != null) { var oldDeclarationName = _declarationName; _declarationName = name.value; _environment.scope(() { - for (var child in node.children) { + for (var child in children) { child.accept(this); } }, when: node.hasDeclarations); @@ -1065,7 +1135,7 @@ class _EvaluateVisitor /// Returns whether [value] is an empty list. bool _isEmptyList(Value value) => value.asList.isEmpty; - Value visitEachRule(EachRule node) { + Value? visitEachRule(EachRule node) { var list = node.list.accept(this); var nodeWithSpan = _expressionNode(node.list); var setVariables = node.variables.length == 1 @@ -1101,8 +1171,9 @@ class _EvaluateVisitor throw _exception(node.expression.accept(this).toString(), node.span); } - Value visitExtendRule(ExtendRule node) { - if (!_inStyleRule || _declarationName != null) { + Value? visitExtendRule(ExtendRule node) { + var styleRule = _styleRule; + if (styleRule == null || _declarationName != null) { throw _exception( "@extend may only be used within style rules.", node.span); } @@ -1134,14 +1205,14 @@ class _EvaluateVisitor targetText.span); } - _extender.addExtension( - _styleRule.selector, compound.components.first, node, _mediaQueries); + _extensionStore.addExtension( + styleRule.selector, compound.components.first, node, _mediaQueries); } return null; } - Value visitAtRule(AtRule node) { + Value? visitAtRule(AtRule node) { // NOTE: this logic is largely duplicated in [visitCssAtRule]. Most changes // here should be mirrored there. @@ -1152,11 +1223,11 @@ class _EvaluateVisitor var name = _interpolationToValue(node.name); - var value = node.value == null - ? null - : _interpolationToValue(node.value, trim: true, warnForColor: true); + var value = node.value.andThen((value) => + _interpolationToValue(value, trim: true, warnForColor: true)); - if (node.children == null) { + var children = node.children; + if (children == null) { _parent.addChild( ModifiableCssAtRule(name, node.span, childless: true, value: value)); return null; @@ -1171,8 +1242,9 @@ class _EvaluateVisitor } _withParent(ModifiableCssAtRule(name, node.span, value: value), () { - if (!_inStyleRule || _inKeyframes) { - for (var child in node.children) { + var styleRule = _styleRule; + if (styleRule == null || _inKeyframes) { + for (var child in children) { child.accept(this); } } else { @@ -1180,8 +1252,8 @@ class _EvaluateVisitor // declarations immediately inside it have somewhere to go. // // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}". - _withParent(_styleRule.copyWithoutChildren(), () { - for (var child in node.children) { + _withParent(styleRule.copyWithoutChildren(), () { + for (var child in children) { child.accept(this); } }, scopeWhen: false); @@ -1195,7 +1267,7 @@ class _EvaluateVisitor return null; } - Value visitForRule(ForRule node) { + Value? visitForRule(ForRule node) { var fromNumber = _addExceptionSpan( node.from, () => node.from.accept(this).assertNumber()); var toNumber = @@ -1229,7 +1301,7 @@ class _EvaluateVisitor }, semiGlobal: true); } - Value visitForwardRule(ForwardRule node) { + Value? visitForwardRule(ForwardRule node) { var oldConfiguration = _configuration; var adjustedConfiguration = oldConfiguration.throughForward(node); @@ -1249,8 +1321,7 @@ class _EvaluateVisitor if (!variable.isGuarded) variable.name }); - _assertConfigurationIsEmpty(newConfiguration, - only: {for (var variable in node.configuration) variable.name}); + _assertConfigurationIsEmpty(newConfiguration); } else { _configuration = adjustedConfiguration; _loadModule(node.url, "@forward", node, (module) { @@ -1262,7 +1333,7 @@ class _EvaluateVisitor return null; } - /// Updates [configuration] to include [node]'s configuration and return the + /// Updates [configuration] to include [node]'s configuration and returns the /// result. Configuration _addForwardConfiguration( Configuration configuration, ForwardRule node) { @@ -1276,20 +1347,24 @@ class _EvaluateVisitor } } - newValues[variable.name] = ConfiguredValue( + newValues[variable.name] = ConfiguredValue.explicit( variable.expression.accept(this).withoutSlash(), variable.span, _expressionNode(variable.expression)); } - return Configuration(newValues, node); + if (configuration is ExplicitConfiguration || configuration.isEmpty) { + return ExplicitConfiguration(newValues, node); + } else { + return Configuration.implicit(newValues); + } } /// Remove configured values from [upstream] that have been removed from /// [downstream], unless they match a name in [except]. void _removeUsedConfiguration( Configuration upstream, Configuration downstream, - {@required Set except}) { + {required Set except}) { for (var name in upstream.values.keys.toList()) { if (except.contains(name)) continue; if (!downstream.values.containsKey(name)) upstream.remove(name); @@ -1305,26 +1380,29 @@ class _EvaluateVisitor /// variable in the error message. This should only be `true` if the name /// won't be obvious from the source span. void _assertConfigurationIsEmpty(Configuration configuration, - {Set only, bool nameInError = false}) { - configuration.values.forEach((name, value) { - if (only != null && !only.contains(name)) return; - - throw _exception( - nameInError - ? "\$$name was not declared with !default in the @used module." - : "This variable was not declared with !default in the @used " - "module.", - value.configurationSpan); - }); - } - - Value visitFunctionRule(FunctionRule node) { + {bool nameInError = false}) { + // By definition, implicit configurations are allowed to only use a subset + // of their values. + if (configuration is! ExplicitConfiguration) return; + if (configuration.isEmpty) return; + + var entry = configuration.values.entries.first; + throw _exception( + nameInError + ? "\$${entry.key} was not declared with !default in the @used " + "module." + : "This variable was not declared with !default in the @used " + "module.", + entry.value.configurationSpan); + } + + Value? visitFunctionRule(FunctionRule node) { _environment.setFunction(UserDefinedCallable(node, _environment.closure())); return null; } - Value visitIfRule(IfRule node) { - var clause = node.lastClause; + Value? visitIfRule(IfRule node) { + IfRuleClause? clause = node.lastClause; for (var clauseToCheck in node.clauses) { if (clauseToCheck.expression.accept(this).isTruthy) { clause = clauseToCheck; @@ -1335,12 +1413,13 @@ class _EvaluateVisitor return _environment.scope( () => _handleReturn( - clause.children, (child) => child.accept(this)), + clause!.children, // dart-lang/sdk#45348 + (child) => child.accept(this)), semiGlobal: true, when: clause.hasDeclarations); } - Value visitImportRule(ImportRule node) { + Value? visitImportRule(ImportRule node) { for (var import in node.imports) { if (import is DynamicImport) { _visitDynamicImport(import); @@ -1359,14 +1438,15 @@ class _EvaluateVisitor var stylesheet = result.item2; var url = stylesheet.span.sourceUrl; - if (_activeModules.containsKey(url)) { - var previousLoad = _activeModules[url]; - throw previousLoad == null - ? _exception("This file is already being loaded.") - : _multiSpanException("This file is already being loaded.", - "new load", {previousLoad.span: "original load"}); + if (url != null) { + if (_activeModules.containsKey(url)) { + throw _activeModules[url].andThen((previousLoad) => + _multiSpanException("This file is already being loaded.", + "new load", {previousLoad.span: "original load"})) ?? + _exception("This file is already being loaded."); + } + _activeModules[url] = import; } - _activeModules[url] = import; // If the imported stylesheet doesn't use any modules, we can inject its // CSS directly into the current stylesheet. If it does use modules, we @@ -1384,7 +1464,7 @@ class _EvaluateVisitor return; } - List children; + late List children; var environment = _environment.forImport(); _withEnvironment(environment, () { var oldImporter = _importer; @@ -1448,21 +1528,22 @@ class _EvaluateVisitor /// /// This first tries loading [url] relative to [baseUrl], which defaults to /// `_stylesheet.span.sourceUrl`. - Tuple2 _loadStylesheet(String url, FileSpan span, - {Uri baseUrl, bool forImport = false}) { + Tuple2 _loadStylesheet(String url, FileSpan span, + {Uri? baseUrl, bool forImport = false}) { try { assert(_importSpan == null); _importSpan = span; - if (_nodeImporter != null) { - var stylesheet = _importLikeNode(url, forImport); - if (stylesheet != null) return Tuple2(null, stylesheet); - } else { - var tuple = _importCache.import(Uri.parse(url), + var importCache = _importCache; + if (importCache != null) { + var tuple = importCache.import(Uri.parse(url), baseImporter: _importer, - baseUrl: baseUrl ?? _stylesheet?.span?.sourceUrl, + baseUrl: baseUrl ?? _stylesheet.span.sourceUrl, forImport: forImport); if (tuple != null) return tuple; + } else { + var stylesheet = _importLikeNode(url, forImport); + if (stylesheet != null) return Tuple2(null, stylesheet); } if (url.startsWith('package:') && isNode) { @@ -1475,9 +1556,9 @@ class _EvaluateVisitor } on SassException catch (error) { throw _exception(error.message, error.span); } catch (error) { - String message; + String? message; try { - message = error.message as String; + message = (error as dynamic).message as String; } catch (_) { message = error.toString(); } @@ -1490,9 +1571,9 @@ class _EvaluateVisitor /// Imports a stylesheet using [_nodeImporter]. /// /// Returns the [Stylesheet], or `null` if the import failed. - Stylesheet _importLikeNode(String originalUrl, bool forImport) { + Stylesheet? _importLikeNode(String originalUrl, bool forImport) { var result = - _nodeImporter.load(originalUrl, _stylesheet.span?.sourceUrl, forImport); + _nodeImporter!.load(originalUrl, _stylesheet.span.sourceUrl, forImport); if (result == null) return null; var contents = result.item1; @@ -1511,19 +1592,18 @@ class _EvaluateVisitor // here should be mirrored there. var url = _interpolationToValue(import.url); - var supports = import.supports; - var resolvedSupports = supports is SupportsDeclaration - ? "${_evaluateToCss(supports.name)}: " - "${_evaluateToCss(supports.value)}" - : (supports == null ? null : _visitSupportsCondition(supports)); - var mediaQuery = - import.media == null ? null : _visitMediaQueries(import.media); + var supports = import.supports.andThen((supports) { + var arg = supports is SupportsDeclaration + ? "${_evaluateToCss(supports.name)}: " + "${_evaluateToCss(supports.value)}" + : supports.andThen(_visitSupportsCondition); + return CssValue("supports($arg)", supports.span); + }); + var rawMedia = import.media; + var mediaQuery = rawMedia.andThen(_visitMediaQueries); var node = ModifiableCssImport(url, import.span, - supports: resolvedSupports == null - ? null - : CssValue("supports($resolvedSupports)", import.supports.span), - media: mediaQuery); + supports: supports, media: mediaQuery); if (_parent != _root) { _parent.addChild(node); @@ -1531,13 +1611,12 @@ class _EvaluateVisitor _root.addChild(node); _endOfImports++; } else { - _outOfOrderImports ??= []; - _outOfOrderImports.add(node); + (_outOfOrderImports ??= []).add(node); } return null; } - Value visitIncludeRule(IncludeRule node) { + Value? visitIncludeRule(IncludeRule node) { var mixin = _addExceptionSpan(node, () => _environment.getMixin(node.name, namespace: node.namespace)); if (mixin == null) { @@ -1562,10 +1641,8 @@ class _EvaluateVisitor _stackTrace(node.spanWithoutContent)); } - var contentCallable = node.content == null - ? null - : UserDefinedCallable(node.content, _environment.closure()); - + var contentCallable = node.content.andThen( + (content) => UserDefinedCallable(content, _environment.closure())); _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () { _environment.withContent(contentCallable, () { _environment.asMixin(() { @@ -1584,12 +1661,12 @@ class _EvaluateVisitor return null; } - Value visitMixinRule(MixinRule node) { + Value? visitMixinRule(MixinRule node) { _environment.setMixin(UserDefinedCallable(node, _environment.closure())); return null; } - Value visitLoudComment(LoudComment node) { + Value? visitLoudComment(LoudComment node) { // NOTE: this logic is largely duplicated in [visitCssComment]. Most changes // here should be mirrored there. @@ -1605,7 +1682,7 @@ class _EvaluateVisitor return null; } - Value visitMediaRule(MediaRule node) { + Value? visitMediaRule(MediaRule node) { // NOTE: this logic is largely duplicated in [visitCssMediaRule]. Most // changes here should be mirrored there. @@ -1615,15 +1692,15 @@ class _EvaluateVisitor } var queries = _visitMediaQueries(node.query); - var mergedQueries = _mediaQueries == null - ? null - : _mergeMediaQueries(_mediaQueries, queries); + var mergedQueries = _mediaQueries + .andThen((mediaQueries) => _mergeMediaQueries(mediaQueries, queries)); if (mergedQueries != null && mergedQueries.isEmpty) return null; _withParent(ModifiableCssMediaRule(mergedQueries ?? queries, node.span), () { _withMediaQueries(mergedQueries ?? queries, () { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { child.accept(this); } @@ -1633,7 +1710,7 @@ class _EvaluateVisitor // // For example, "a {@media screen {b: c}}" should produce // "@media screen {a {b: c}}". - _withParent(_styleRule.copyWithoutChildren(), () { + _withParent(styleRule.copyWithoutChildren(), () { for (var child in node.children) { child.accept(this); } @@ -1665,7 +1742,7 @@ class _EvaluateVisitor /// Returns the empty list if there are no contexts that match both [queries1] /// and [queries2], or `null` if there are contexts that can't be represented /// by media queries. - List _mergeMediaQueries( + List? _mergeMediaQueries( Iterable queries1, Iterable queries2) { var queries = []; for (var query1 in queries1) { @@ -1681,9 +1758,9 @@ class _EvaluateVisitor Value visitReturnRule(ReturnRule node) => node.expression.accept(this); - Value visitSilentComment(SilentComment node) => null; + Value? visitSilentComment(SilentComment node) => null; - Value visitStyleRule(StyleRule node) { + Value? visitStyleRule(StyleRule node) { // NOTE: this logic is largely duplicated in [visitCssStyleRule]. Most // changes here should be mirrored there. @@ -1724,10 +1801,10 @@ class _EvaluateVisitor parsedSelector = _addExceptionSpan( node.selector, () => parsedSelector.resolveParentSelectors( - _styleRule?.originalSelector, + _styleRuleIgnoringAtRoot?.originalSelector, implicitParent: !_atRootExcludingStyleRule)); - var selector = _extender.addSelector( + var selector = _extensionStore.addSelector( parsedSelector, node.selector.span, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: parsedSelector); @@ -1744,7 +1821,7 @@ class _EvaluateVisitor scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - if (!_inStyleRule && _parent.children.isNotEmpty) { + if (_styleRule == null && _parent.children.isNotEmpty) { var lastChild = _parent.children.last; lastChild.isGroupEnd = true; } @@ -1752,7 +1829,7 @@ class _EvaluateVisitor return null; } - Value visitSupportsRule(SupportsRule node) { + Value? visitSupportsRule(SupportsRule node) { // NOTE: this logic is largely duplicated in [visitCssSupportsRule]. Most // changes here should be mirrored there. @@ -1765,7 +1842,8 @@ class _EvaluateVisitor var condition = CssValue(_visitSupportsCondition(node.condition), node.condition.span); _withParent(ModifiableCssSupportsRule(condition, node.span), () { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { child.accept(this); } @@ -1775,7 +1853,7 @@ class _EvaluateVisitor // // For example, "a {@supports (a: b) {b: c}}" should produce "@supports // (a: b) {a {b: c}}". - _withParent(_styleRule.copyWithoutChildren(), () { + _withParent(styleRule.copyWithoutChildren(), () { for (var child in node.children) { child.accept(this); } @@ -1807,7 +1885,8 @@ class _EvaluateVisitor } else if (condition is SupportsAnything) { return "(${_performInterpolation(condition.contents)})"; } else { - return null; + throw ArgumentError( + "Unknown supports condition type ${condition.runtimeType}."); } } @@ -1817,7 +1896,7 @@ class _EvaluateVisitor /// If [operator] is passed, it's the operator for the surrounding /// [SupportsOperation], and is used to determine whether parentheses are /// necessary if [condition] is also a [SupportsOperation]. - String _parenthesize(SupportsCondition condition, [String operator]) { + String _parenthesize(SupportsCondition condition, [String? operator]) { if ((condition is SupportsNegation) || (condition is SupportsOperation && (operator == null || operator != condition.operator))) { @@ -1827,7 +1906,7 @@ class _EvaluateVisitor } } - Value visitVariableDeclaration(VariableDeclaration node) { + Value? visitVariableDeclaration(VariableDeclaration node) { if (node.isGuarded) { if (node.namespace == null && _environment.atRoot) { var override = _configuration.remove(node.name); @@ -1871,12 +1950,12 @@ class _EvaluateVisitor return null; } - Value visitUseRule(UseRule node) { + Value? visitUseRule(UseRule node) { var configuration = node.configuration.isEmpty ? const Configuration.empty() - : Configuration({ + : ExplicitConfiguration({ for (var variable in node.configuration) - variable.name: ConfiguredValue( + variable.name: ConfiguredValue.explicit( variable.expression.accept(this).withoutSlash(), variable.span, _expressionNode(variable.expression)) @@ -1890,7 +1969,7 @@ class _EvaluateVisitor return null; } - Value visitWarnRule(WarnRule node) { + Value? visitWarnRule(WarnRule node) { var value = _addExceptionSpan(node, () => node.expression.accept(this)); _logger.warn( value is SassString ? value.text : _serialize(value, node.expression), @@ -1898,7 +1977,7 @@ class _EvaluateVisitor return null; } - Value visitWhileRule(WhileRule node) { + Value? visitWhileRule(WhileRule node) { return _environment.scope(() { while (node.condition.accept(this).isTruthy) { var result = _handleReturn( @@ -1969,13 +2048,13 @@ class _EvaluateVisitor } else { return result; } - break; case BinaryOperator.modulo: var right = node.right.accept(this); return left.modulo(right); + default: - return null; + throw ArgumentError("Unknown binary operator ${node.operator}."); } }); } @@ -2016,9 +2095,9 @@ class _EvaluateVisitor _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.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"]!; return (condition.accept(this).isTruthy ? ifTrue : ifFalse).accept(this); } @@ -2044,12 +2123,15 @@ class _EvaluateVisitor for (var pair in node.pairs) { var keyValue = pair.item1.accept(this); var valueValue = pair.item2.accept(this); - if (map.containsKey(keyValue)) { + + var oldValue = map[keyValue]; + if (oldValue != null) { + var oldValueSpan = keyNodes[keyValue]?.span; throw MultiSpanSassRuntimeException( 'Duplicate key.', pair.item1.span, 'second key', - {keyNodes[keyValue].span: 'first key'}, + {if (oldValueSpan != null) oldValueSpan: 'first key'}, _stackTrace(pair.item1.span)); } map[keyValue] = valueValue; @@ -2060,7 +2142,7 @@ class _EvaluateVisitor Value visitFunctionExpression(FunctionExpression node) { var plainName = node.name.asPlain; - Callable function; + Callable? function; if (plainName != null) { function = _addExceptionSpan( node, @@ -2091,7 +2173,7 @@ class _EvaluateVisitor /// Like `_environment.getFunction`, but also returns built-in /// globally-available functions. - Callable _getFunction(String name, {String namespace}) { + Callable? _getFunction(String name, {String? namespace}) { var local = _environment.getFunction(name, namespace: namespace); if (local != null || namespace != null) return local; return _builtInFunctions[name]; @@ -2099,14 +2181,16 @@ class _EvaluateVisitor /// Evaluates the arguments in [arguments] as applied to [callable], and /// invokes [run] in a scope with those arguments defined. - Value _runUserDefinedCallable( + V _runUserDefinedCallable( ArgumentInvocation arguments, UserDefinedCallable callable, AstNode nodeWithSpan, - Value run()) { + V run()) { var evaluated = _evaluateArguments(arguments); - var name = callable.name == null ? "@content" : callable.name + "()"; + var name = callable.name; + if (name != "@content") name += "()"; + return _withStackFrame(name, nodeWithSpan, () { // Add an extra closure() call so that modifications to the environment // don't affect the underlying environment closure. @@ -2122,7 +2206,7 @@ class _EvaluateVisitor _environment.setLocalVariable( declaredArguments[i].name, evaluated.positional[i].withoutSlash(), - _sourceMap ? evaluated.positionalNodes[i] : null); + evaluated.positionalNodes?[i]); } for (var i = evaluated.positional.length; @@ -2130,18 +2214,17 @@ class _EvaluateVisitor i++) { var argument = declaredArguments[i]; var value = evaluated.named.remove(argument.name) ?? - argument.defaultValue.accept(this); + argument.defaultValue!.accept(this); _environment.setLocalVariable( argument.name, value.withoutSlash(), - _sourceMap - ? evaluated.namedNodes[argument.name] ?? - _expressionNode(argument.defaultValue) - : null); + evaluated.namedNodes?[argument.name] ?? + argument.defaultValue.andThen(_expressionNode)); } - SassArgumentList argumentList; - if (callable.declaration.arguments.restArgument != null) { + SassArgumentList? argumentList; + var restArgument = callable.declaration.arguments.restArgument; + if (restArgument != null) { var rest = evaluated.positional.length > declaredArguments.length ? evaluated.positional.sublist(declaredArguments.length) : const []; @@ -2152,9 +2235,7 @@ class _EvaluateVisitor ? ListSeparator.comma : evaluated.separator); _environment.setLocalVariable( - callable.declaration.arguments.restArgument, - argumentList, - nodeWithSpan); + restArgument, argumentList, nodeWithSpan); } var result = run(); @@ -2179,15 +2260,10 @@ class _EvaluateVisitor /// Evaluates [arguments] as applied to [callable]. Value _runFunctionCallable( - ArgumentInvocation arguments, Callable callable, AstNode nodeWithSpan) { + ArgumentInvocation arguments, Callable? callable, AstNode nodeWithSpan) { if (callable is BuiltInCallable) { - var result = _runBuiltInCallable(arguments, callable, nodeWithSpan); - if (result == null) { - throw _exception( - "Custom functions may not return Dart's null.", nodeWithSpan.span); - } - - return result.withoutSlash(); + return _runBuiltInCallable(arguments, callable, nodeWithSpan) + .withoutSlash(); } else if (callable is UserDefinedCallable) { return _runUserDefinedCallable(arguments, callable, nodeWithSpan, () { for (var statement in callable.declaration.children) { @@ -2216,16 +2292,17 @@ class _EvaluateVisitor buffer.write(_evaluateToCss(argument)); } - var rest = arguments.rest?.accept(this); - if (rest != null) { + var restArg = arguments.rest; + if (restArg != null) { + var rest = restArg.accept(this); if (!first) buffer.write(", "); - buffer.write(_serialize(rest, arguments.rest)); + buffer.write(_serialize(rest, restArg)); } buffer.writeCharCode($rparen); return SassString(buffer.toString(), quotes: false); } else { - return null; + throw ArgumentError('Unknown callable type ${callable.runtimeType}.'); } } @@ -2251,10 +2328,10 @@ class _EvaluateVisitor i++) { var argument = declaredArguments[i]; evaluated.positional.add(evaluated.named.remove(argument.name) ?? - argument.defaultValue?.accept(this)); + argument.defaultValue!.accept(this)); } - SassArgumentList argumentList; + SassArgumentList? argumentList; if (overload.restArgument != null) { var rest = const []; if (evaluated.positional.length > declaredArguments.length) { @@ -2274,7 +2351,8 @@ class _EvaluateVisitor Value result; try { - result = callback(evaluated.positional); + result = withCurrentCallableNode( + nodeWithSpan, () => callback(evaluated.positional)); } on SassRuntimeException { rethrow; } on MultiSpanSassScriptException catch (error) { @@ -2288,9 +2366,9 @@ class _EvaluateVisitor throw MultiSpanSassRuntimeException(error.message, error.span, error.primaryLabel, error.secondarySpans, _stackTrace(error.span)); } catch (error) { - String message; + String? message; try { - message = error.message as String; + message = (error as dynamic).message as String; } catch (_) { message = error.toString(); } @@ -2301,6 +2379,7 @@ class _EvaluateVisitor if (argumentList == null) return result; if (evaluated.named.isEmpty) return result; if (argumentList.wereKeywordsAccessed) return result; + throw MultiSpanSassRuntimeException( "No ${pluralize('argument', evaluated.named.keys.length)} named " "${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.", @@ -2315,7 +2394,7 @@ class _EvaluateVisitor /// If [trackSpans] is `true`, this tracks the source spans of the arguments /// being passed in. It defaults to [_sourceMap]. _ArgumentResults _evaluateArguments(ArgumentInvocation arguments, - {bool trackSpans}) { + {bool? trackSpans}) { trackSpans ??= _sourceMap; var positional = [ @@ -2339,16 +2418,17 @@ class _EvaluateVisitor } : null; - if (arguments.rest == null) { + var restArgs = arguments.rest; + if (restArgs == null) { return _ArgumentResults(positional, named, ListSeparator.undecided, positionalNodes: positionalNodes, namedNodes: namedNodes); } - var rest = arguments.rest.accept(this); - var restNodeForSpan = trackSpans ? _expressionNode(arguments.rest) : null; + var rest = restArgs.accept(this); + var restNodeForSpan = _expressionNode(restArgs); var separator = ListSeparator.undecided; if (rest is SassMap) { - _addRestMap(named, rest, arguments.rest); + _addRestMap(named, rest, restArgs, (value) => value); namedNodes?.addAll({ for (var key in rest.contents.keys) (key as SassString).text: restNodeForSpan @@ -2369,16 +2449,16 @@ class _EvaluateVisitor positionalNodes?.add(restNodeForSpan); } - if (arguments.keywordRest == null) { + var keywordRestArgs = arguments.keywordRest; + if (keywordRestArgs == null) { return _ArgumentResults(positional, named, separator, positionalNodes: positionalNodes, namedNodes: namedNodes); } - var keywordRest = arguments.keywordRest.accept(this); - var keywordRestNodeForSpan = - trackSpans ? _expressionNode(arguments.keywordRest) : null; + var keywordRest = keywordRestArgs.accept(this); + var keywordRestNodeForSpan = _expressionNode(keywordRestArgs); if (keywordRest is SassMap) { - _addRestMap(named, keywordRest, arguments.keywordRest); + _addRestMap(named, keywordRest, keywordRestArgs, (value) => value); namedNodes?.addAll({ for (var key in keywordRest.contents.keys) (key as SassString).text: keywordRestNodeForSpan @@ -2388,7 +2468,7 @@ class _EvaluateVisitor } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", - arguments.keywordRest.span); + keywordRestArgs.span); } } @@ -2399,40 +2479,44 @@ class _EvaluateVisitor /// for macros such as `if()`. Tuple2, Map> _evaluateMacroArguments( CallableInvocation invocation) { - if (invocation.arguments.rest == null) { + var restArgs_ = invocation.arguments.rest; + if (restArgs_ == null) { return Tuple2( invocation.arguments.positional, invocation.arguments.named); } + var restArgs = restArgs_; // dart-lang/sdk#45348 var positional = invocation.arguments.positional.toList(); var named = Map.of(invocation.arguments.named); - var rest = invocation.arguments.rest.accept(this); + var rest = restArgs.accept(this); if (rest is SassMap) { - _addRestMap(named, rest, invocation, (value) => ValueExpression(value)); + _addRestMap(named, rest, invocation, + (value) => ValueExpression(value, restArgs.span)); } else if (rest is SassList) { - positional.addAll(rest.asList.map((value) => ValueExpression(value))); + positional.addAll( + rest.asList.map((value) => ValueExpression(value, restArgs.span))); if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { - named[key] = ValueExpression(value); + named[key] = ValueExpression(value, restArgs.span); }); } } else { - positional.add(ValueExpression(rest)); + positional.add(ValueExpression(rest, restArgs.span)); } - if (invocation.arguments.keywordRest == null) { - return Tuple2(positional, named); - } + var keywordRestArgs_ = invocation.arguments.keywordRest; + if (keywordRestArgs_ == null) return Tuple2(positional, named); + var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348 - var keywordRest = invocation.arguments.keywordRest.accept(this); + var keywordRest = keywordRestArgs.accept(this); if (keywordRest is SassMap) { - _addRestMap( - named, keywordRest, invocation, (value) => ValueExpression(value)); + _addRestMap(named, keywordRest, invocation, + (value) => ValueExpression(value, keywordRestArgs.span)); return Tuple2(positional, named); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", - invocation.span); + keywordRestArgs.span); } } @@ -2448,8 +2532,7 @@ class _EvaluateVisitor /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. void _addRestMap(Map values, SassMap map, AstNode nodeWithSpan, - [T convert(Value value)]) { - convert ??= (value) => value as T; + T convert(Value value)) { map.contents.forEach((key, value) { if (key is SassString) { values[key.text] = convert(value); @@ -2469,10 +2552,8 @@ class _EvaluateVisitor _addExceptionSpan( nodeWithSpan, () => arguments.verify(positional, MapKeySet(named))); - Value visitSelectorExpression(SelectorExpression node) { - if (_styleRule == null) return sassNull; - return _styleRule.originalSelector.asSassList; - } + Value visitSelectorExpression(SelectorExpression node) => + _styleRuleIgnoringAtRoot?.originalSelector.asSassList ?? sassNull; SassString visitStringExpression(StringExpression node) { // Don't use [performInterpolation] here because we need to get the raw text @@ -2567,8 +2648,7 @@ class _EvaluateVisitor _root.addChild(modifiableNode); _endOfImports++; } else { - _outOfOrderImports ??= []; - _outOfOrderImports.add(modifiableNode); + (_outOfOrderImports ??= []).add(modifiableNode); } } @@ -2593,15 +2673,15 @@ class _EvaluateVisitor "Media rules may not be used within nested declarations.", node.span); } - var mergedQueries = _mediaQueries == null - ? null - : _mergeMediaQueries(_mediaQueries, node.queries); + var mergedQueries = _mediaQueries.andThen( + (mediaQueries) => _mergeMediaQueries(mediaQueries, node.queries)); if (mergedQueries != null && mergedQueries.isEmpty) return null; _withParent( ModifiableCssMediaRule(mergedQueries ?? node.queries, node.span), () { _withMediaQueries(mergedQueries ?? node.queries, () { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { child.accept(this); } @@ -2611,7 +2691,7 @@ class _EvaluateVisitor // // For example, "a {@media screen {b: c}}" should produce // "@media screen {a {b: c}}". - _withParent(_styleRule.copyWithoutChildren(), () { + _withParent(styleRule.copyWithoutChildren(), () { for (var child in node.children) { child.accept(this); } @@ -2634,10 +2714,11 @@ class _EvaluateVisitor "Style rules may not be used within nested declarations.", node.span); } + var styleRule = _styleRule; var originalSelector = node.selector.value.resolveParentSelectors( - _styleRule?.originalSelector, + styleRule?.originalSelector, implicitParent: !_atRootExcludingStyleRule); - var selector = _extender.addSelector( + var selector = _extensionStore.addSelector( originalSelector, node.selector.span, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, originalSelector: originalSelector); @@ -2652,7 +2733,7 @@ class _EvaluateVisitor }, through: (node) => node is CssStyleRule, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - if (!_inStyleRule && _parent.children.isNotEmpty) { + if (styleRule == null && _parent.children.isNotEmpty) { var lastChild = _parent.children.last; lastChild.isGroupEnd = true; } @@ -2675,7 +2756,8 @@ class _EvaluateVisitor } _withParent(ModifiableCssSupportsRule(node.condition, node.span), () { - if (!_inStyleRule) { + var styleRule = _styleRule; + if (styleRule == null) { for (var child in node.children) { child.accept(this); } @@ -2685,7 +2767,7 @@ class _EvaluateVisitor // // For example, "a {@supports (a: b) {b: c}}" should produce "@supports // (a: b) {a {b: c}}". - _withParent(_styleRule.copyWithoutChildren(), () { + _withParent(styleRule.copyWithoutChildren(), () { for (var child in node.children) { child.accept(this); } @@ -2700,7 +2782,7 @@ class _EvaluateVisitor /// /// Returns the value returned by [callback], or `null` if it only ever /// returned `null`. - Value _handleReturn(List list, Value callback(T value)) { + Value? _handleReturn(List list, Value? callback(T value)) { for (var value in list) { var result = callback(value); if (result != null) return result; @@ -2746,7 +2828,8 @@ class _EvaluateVisitor namesByColor.containsKey(result)) { var alternative = BinaryOperationExpression( BinaryOperator.plus, - StringExpression(Interpolation([""], null), quotes: true), + StringExpression(Interpolation([""], interpolation.span), + quotes: true), expression); _warn( "You probably don't mean to use the color value " @@ -2783,13 +2866,15 @@ class _EvaluateVisitor /// where that variable was originally declared. Otherwise, this will just /// return [expression]. /// - /// Returns `null` if [_sourceMap] is `false`. - /// /// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling /// [AstNode.span] if the span isn't required, since some nodes need to do /// real work to manufacture a source span. AstNode _expressionNode(Expression expression) { - if (!_sourceMap) return null; + // If we aren't making a source map this doesn't matter, but we still return + // the expression so we don't have to make the type (and everything + // downstream of it) nullable. + if (!_sourceMap) return expression; + if (expression is VariableExpression) { return _environment.getVariableNode(expression.name, namespace: expression.namespace) ?? @@ -2808,7 +2893,7 @@ class _EvaluateVisitor /// /// Runs [callback] in a new environment scope unless [scopeWhen] is false. T _withParent(S node, T callback(), - {bool through(CssNode node), bool scopeWhen = true}) { + {bool through(CssNode node)?, bool scopeWhen = true}) { _addChild(node, through: through); var oldParent = _parent; @@ -2824,19 +2909,25 @@ class _EvaluateVisitor /// If [through] is passed, [node] is added as a child of the first parent for /// which [through] returns `false` instead. That parent is copied unless it's the /// lattermost child of its parent. - void _addChild(ModifiableCssNode node, {bool through(CssNode node)}) { + void _addChild(ModifiableCssNode node, {bool through(CssNode node)?}) { // Go up through parents that match [through]. var parent = _parent; if (through != null) { while (through(parent)) { - parent = parent.parent; + var grandparent = parent.parent; + if (grandparent == null) { + 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 // the parent. Instead, we should create a copy and add it after the // interstitial sibling. if (parent.hasFollowingSibling) { - var grandparent = parent.parent; + // A node with siblings must have a parent + var grandparent = parent.parent!; parent = parent.copyWithoutChildren(); grandparent.addChild(parent); } @@ -2847,15 +2938,15 @@ class _EvaluateVisitor /// Runs [callback] with [rule] as the current style rule. T _withStyleRule(ModifiableCssStyleRule rule, T callback()) { - var oldRule = _styleRule; - _styleRule = rule; + var oldRule = _styleRuleIgnoringAtRoot; + _styleRuleIgnoringAtRoot = rule; var result = callback(); - _styleRule = oldRule; + _styleRuleIgnoringAtRoot = oldRule; return result; } /// Runs [callback] with [queries] as the current media queries. - T _withMediaQueries(List queries, T callback()) { + T _withMediaQueries(List? queries, T callback()) { var oldMediaQueries = _mediaQueries; _mediaQueries = queries; var result = callback(); @@ -2883,16 +2974,13 @@ class _EvaluateVisitor /// Creates a new stack frame with location information from [member] and /// [span]. - Frame _stackFrame(String member, FileSpan span) { - var url = span.sourceUrl; - if (url != null && _importCache != null) url = _importCache.humanize(url); - return frameForSpan(span, member, url: url); - } + Frame _stackFrame(String member, FileSpan span) => frameForSpan(span, member, + url: span.sourceUrl.andThen((url) => _importCache?.humanize(url) ?? url)); /// Returns a stack trace at the current point. /// /// If [span] is passed, it's used for the innermost stack frame. - Trace _stackTrace([FileSpan span]) { + Trace _stackTrace([FileSpan? span]) { var frames = [ ..._stack.map((tuple) => _stackFrame(tuple.item1, tuple.item2.span)), if (span != null) _stackFrame(_member, span) @@ -2908,7 +2996,7 @@ class _EvaluateVisitor /// Returns a [SassRuntimeException] with the given [message]. /// /// If [span] is passed, it's used for the innermost stack frame. - SassRuntimeException _exception(String message, [FileSpan span]) => + SassRuntimeException _exception(String message, [FileSpan? span]) => SassRuntimeException( message, span ?? _stack.last.item2.span, _stackTrace(span)); @@ -3019,8 +3107,7 @@ class _ImportedCssVisitor implements ModifiableCssVisitor { _visitor._addChild(node); _visitor._endOfImports++; } else { - _visitor._outOfOrderImports ??= []; - _visitor._outOfOrderImports.add(node); + (_visitor._outOfOrderImports ??= []).add(node); } } @@ -3032,9 +3119,9 @@ class _ImportedCssVisitor implements ModifiableCssVisitor { // Whether [node.query] has been merged with [_visitor._mediaQueries]. If it // has been merged, merging again is a no-op; if it hasn't been merged, // merging again will fail. - var hasBeenMerged = _visitor._mediaQueries == null || - _visitor._mergeMediaQueries(_visitor._mediaQueries, node.queries) != - null; + var mediaQueries = _visitor._mediaQueries; + var hasBeenMerged = mediaQueries == null || + _visitor._mergeMediaQueries(mediaQueries, node.queries) != null; _visitor._addChild(node, through: (node) => @@ -3065,7 +3152,7 @@ class _ArgumentResults { /// 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; + final List? positionalNodes; /// Arguments passed by name. final Map named; @@ -3076,7 +3163,7 @@ class _ArgumentResults { /// 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; + final Map? namedNodes; /// The separator used for the rest argument list, if any. final ListSeparator separator; diff --git a/lib/src/visitor/find_dependencies.dart b/lib/src/visitor/find_dependencies.dart index d2963ec50..80a876fa5 100644 --- a/lib/src/visitor/find_dependencies.dart +++ b/lib/src/visitor/find_dependencies.dart @@ -16,7 +16,7 @@ Tuple2, List> findDependencies(Stylesheet stylesheet) => /// A visitor that traverses a stylesheet and records, all `@import`, `@use`, /// and `@forward` rules it contains. -class _FindDependenciesVisitor extends RecursiveStatementVisitor { +class _FindDependenciesVisitor extends RecursiveStatementVisitor { final _usesAndForwards = []; final _imports = []; diff --git a/lib/src/visitor/recursive_ast.dart b/lib/src/visitor/recursive_ast.dart index 3900253ad..018d70fce 100644 --- a/lib/src/visitor/recursive_ast.dart +++ b/lib/src/visitor/recursive_ast.dart @@ -11,82 +11,72 @@ import 'recursive_statement.dart'; /// /// This extends [RecursiveStatementVisitor] to traverse each expression in /// addition to each statement. -/// -/// The default implementation of the visit methods all return `null`. -abstract class RecursiveAstVisitor extends RecursiveStatementVisitor - implements ExpressionVisitor { +abstract class RecursiveAstVisitor extends RecursiveStatementVisitor + implements ExpressionVisitor { void visitExpression(Expression expression) { expression.accept(this); } - T visitBinaryOperationExpression(BinaryOperationExpression node) { + void visitBinaryOperationExpression(BinaryOperationExpression node) { node.left.accept(this); node.right.accept(this); - return null; } - T visitBooleanExpression(BooleanExpression node) => null; + void visitBooleanExpression(BooleanExpression node) {} - T visitColorExpression(ColorExpression node) => null; + void visitColorExpression(ColorExpression node) {} - T visitForwardRule(ForwardRule node) { + void visitForwardRule(ForwardRule node) { for (var variable in node.configuration) { variable.expression.accept(this); } - return null; } - T visitFunctionExpression(FunctionExpression node) { + void visitFunctionExpression(FunctionExpression node) { visitInterpolation(node.name); visitArgumentInvocation(node.arguments); - return null; } - T visitIfExpression(IfExpression node) { + void visitIfExpression(IfExpression node) { visitArgumentInvocation(node.arguments); - return null; } - T visitListExpression(ListExpression node) { + void visitListExpression(ListExpression node) { for (var item in node.contents) { item.accept(this); } - return null; } - T visitMapExpression(MapExpression node) { + void visitMapExpression(MapExpression node) { for (var pair in node.pairs) { pair.item1.accept(this); pair.item2.accept(this); } - return null; } - T visitNullExpression(NullExpression node) => null; + void visitNullExpression(NullExpression node) {} - T visitNumberExpression(NumberExpression node) => null; + void visitNumberExpression(NumberExpression node) {} - T visitParenthesizedExpression(ParenthesizedExpression node) => - node.expression.accept(this); + void visitParenthesizedExpression(ParenthesizedExpression node) {} - T visitSelectorExpression(SelectorExpression node) => null; + void visitSelectorExpression(SelectorExpression node) {} - T visitStringExpression(StringExpression node) { + void visitStringExpression(StringExpression node) { visitInterpolation(node.text); - return null; } - T visitUnaryOperationExpression(UnaryOperationExpression node) => - node.operand.accept(this); + void visitUnaryOperationExpression(UnaryOperationExpression node) { + node.operand.accept(this); + } - T visitUseRule(UseRule node) { + void visitUseRule(UseRule node) { for (var variable in node.configuration) { variable.expression.accept(this); } - return null; } - T visitValueExpression(ValueExpression node) => null; + void visitValueExpression(ValueExpression node) {} - T visitVariableExpression(VariableExpression node) => null; + void visitVariableExpression(VariableExpression node) {} } diff --git a/lib/src/visitor/recursive_statement.dart b/lib/src/visitor/recursive_statement.dart index ca09c8c2c..2c64f82bd 100644 --- a/lib/src/visitor/recursive_statement.dart +++ b/lib/src/visitor/recursive_statement.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import '../ast/sass.dart'; +import '../util/nullable.dart'; import 'interface/statement.dart'; /// A visitor that recursively traverses each statement in a Sass AST. @@ -19,141 +20,129 @@ import 'interface/statement.dart'; /// * [visitChildren] /// * [visitInterpolation] /// * [visitExpression] -/// -/// The default implementation of the visit methods all return `null`. -abstract class RecursiveStatementVisitor implements StatementVisitor { - T visitAtRootRule(AtRootRule node) { - if (node.query != null) visitInterpolation(node.query); - return visitChildren(node); +abstract class RecursiveStatementVisitor implements StatementVisitor { + void visitAtRootRule(AtRootRule node) { + node.query.andThen(visitInterpolation); + visitChildren(node.children); } - T visitAtRule(AtRule node) { + void visitAtRule(AtRule node) { visitInterpolation(node.name); - if (node.value != null) visitInterpolation(node.value); - return node.children == null ? null : visitChildren(node); + node.value.andThen(visitInterpolation); + node.children.andThen(visitChildren); } - T visitContentBlock(ContentBlock node) => visitCallableDeclaration(node); + void visitContentBlock(ContentBlock node) => visitCallableDeclaration(node); - T visitContentRule(ContentRule node) { + void visitContentRule(ContentRule node) { visitArgumentInvocation(node.arguments); - return null; } - T visitDebugRule(DebugRule node) { + void visitDebugRule(DebugRule node) { visitExpression(node.expression); - return null; } - T visitDeclaration(Declaration node) { + void visitDeclaration(Declaration node) { visitInterpolation(node.name); - if (node.value != null) visitExpression(node.value); - return node.children == null ? null : visitChildren(node); + node.value.andThen(visitExpression); + node.children.andThen(visitChildren); } - T visitEachRule(EachRule node) { + void visitEachRule(EachRule node) { visitExpression(node.list); - return visitChildren(node); + visitChildren(node.children); } - T visitErrorRule(ErrorRule node) { + void visitErrorRule(ErrorRule node) { visitExpression(node.expression); - return null; } - T visitExtendRule(ExtendRule node) { + void visitExtendRule(ExtendRule node) { visitInterpolation(node.selector); - return null; } - T visitForRule(ForRule node) { + void visitForRule(ForRule node) { visitExpression(node.from); visitExpression(node.to); - return visitChildren(node); + visitChildren(node.children); } - T visitForwardRule(ForwardRule node) => null; + void visitForwardRule(ForwardRule node) {} - T visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node); + void visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node); - T visitIfRule(IfRule node) { + void visitIfRule(IfRule node) { for (var clause in node.clauses) { - _visitIfClause(clause); + visitExpression(clause.expression); + for (var child in clause.children) { + child.accept(this); + } } - if (node.lastClause != null) _visitIfClause(node.lastClause); - return null; - } - /// Visits [clause]'s expression and children. - void _visitIfClause(IfClause clause) { - if (clause.expression != null) visitExpression(clause.expression); - for (var child in clause.children) { - child.accept(this); - } + node.lastClause.andThen((lastClause) { + for (var child in lastClause.children) { + child.accept(this); + } + }); } - T visitImportRule(ImportRule node) { + void visitImportRule(ImportRule node) { for (var import in node.imports) { if (import is StaticImport) { visitInterpolation(import.url); - if (import.supports != null) visitSupportsCondition(import.supports); - if (import.media != null) visitInterpolation(import.media); + import.supports.andThen(visitSupportsCondition); + import.media.andThen(visitInterpolation); } } - return null; } - T visitIncludeRule(IncludeRule node) { + void visitIncludeRule(IncludeRule node) { visitArgumentInvocation(node.arguments); - return node.content == null ? null : visitContentBlock(node.content); + node.content.andThen(visitContentBlock); } - T visitLoudComment(LoudComment node) { + void visitLoudComment(LoudComment node) { visitInterpolation(node.text); - return null; } - T visitMediaRule(MediaRule node) { + void visitMediaRule(MediaRule node) { visitInterpolation(node.query); - return visitChildren(node); + visitChildren(node.children); } - T visitMixinRule(MixinRule node) => visitCallableDeclaration(node); + void visitMixinRule(MixinRule node) => visitCallableDeclaration(node); - T visitReturnRule(ReturnRule node) { + void visitReturnRule(ReturnRule node) { visitExpression(node.expression); - return null; } - T visitSilentComment(SilentComment node) => null; + void visitSilentComment(SilentComment node) {} - T visitStyleRule(StyleRule node) { + void visitStyleRule(StyleRule node) { visitInterpolation(node.selector); - return visitChildren(node); + visitChildren(node.children); } - T visitStylesheet(Stylesheet node) => visitChildren(node); + void visitStylesheet(Stylesheet node) => visitChildren(node.children); - T visitSupportsRule(SupportsRule node) { + void visitSupportsRule(SupportsRule node) { visitSupportsCondition(node.condition); - return visitChildren(node); + visitChildren(node.children); } - T visitUseRule(UseRule node) => null; + void visitUseRule(UseRule node) {} - T visitVariableDeclaration(VariableDeclaration node) { + void visitVariableDeclaration(VariableDeclaration node) { visitExpression(node.expression); - return null; } - T visitWarnRule(WarnRule node) { + void visitWarnRule(WarnRule node) { visitExpression(node.expression); - return null; } - T visitWhileRule(WhileRule node) { + void visitWhileRule(WhileRule node) { visitExpression(node.condition); - return visitChildren(node); + visitChildren(node.children); } /// Visits each of [node]'s expressions and children. @@ -161,11 +150,11 @@ abstract class RecursiveStatementVisitor implements StatementVisitor { /// The default implementations of [visitFunctionRule] and [visitMixinRule] /// call this. @protected - T visitCallableDeclaration(CallableDeclaration node) { + void visitCallableDeclaration(CallableDeclaration node) { for (var argument in node.arguments.arguments) { - if (argument.defaultValue != null) visitExpression(argument.defaultValue); + argument.defaultValue.andThen(visitExpression); } - return visitChildren(node); + visitChildren(node.children); } /// Visits each expression in an [invocation]. @@ -180,12 +169,8 @@ abstract class RecursiveStatementVisitor implements StatementVisitor { for (var expression in invocation.named.values) { visitExpression(expression); } - if (invocation.rest != null) { - visitExpression(invocation.rest); - } - if (invocation.keywordRest != null) { - visitExpression(invocation.keywordRest); - } + invocation.rest.andThen(visitExpression); + invocation.keywordRest.andThen(visitExpression); } /// Visits each expression in [condition]. @@ -207,16 +192,15 @@ abstract class RecursiveStatementVisitor implements StatementVisitor { } } - /// Visits each of [node]'s children. + /// Visits each child in [children]. /// /// The default implementation of the visit methods for all [ParentStatement]s - /// call this and return its result. + /// call this. @protected - T visitChildren(ParentStatement node) { - for (var child in node.children) { + void visitChildren(List children) { + for (var child in children) { child.accept(this); } - return null; } /// Visits each expression in an [interpolation]. diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index e5654fef3..8e0245f9f 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -44,11 +44,11 @@ import 'interface/value.dart'; /// If [charset] is `true`, this will include a `@charset` declaration or a BOM /// if the stylesheet contains any non-ASCII characters. SerializeResult serialize(CssNode node, - {OutputStyle style, + {OutputStyle? style, bool inspect = false, bool useSpaces = true, - int indentWidth, - LineFeed lineFeed, + int? indentWidth, + LineFeed? lineFeed, bool sourceMap = false, bool charset = true}) { indentWidth ??= 2; @@ -137,12 +137,12 @@ class _SerializeVisitor bool get _isCompressed => _style == OutputStyle.compressed; _SerializeVisitor( - {OutputStyle style, + {OutputStyle? style, bool inspect = false, bool quote = true, bool useSpaces = true, - int indentWidth, - LineFeed lineFeed, + int? indentWidth, + LineFeed? lineFeed, bool sourceMap = true}) : _buffer = sourceMap ? SourceMapBuffer() : NoSourceMapBuffer(), _style = style ?? OutputStyle.expanded, @@ -155,7 +155,7 @@ class _SerializeVisitor } void visitCssStylesheet(CssStylesheet node) { - CssNode previous; + CssNode? previous; for (var i = 0; i < node.children.length; i++) { var child = node.children[i]; if (_isInvisible(child)) continue; @@ -188,10 +188,7 @@ class _SerializeVisitor return; } - if (node.span != null) { - minimumIndentation = - math.min(minimumIndentation, node.span.start.column); - } + minimumIndentation = math.min(minimumIndentation, node.span.start.column); _writeIndentation(); _writeWithIndent(node.text, minimumIndentation); @@ -205,9 +202,10 @@ class _SerializeVisitor _buffer.writeCharCode($at); _write(node.name); - if (node.value != null) { + var value = node.value; + if (value != null) { _buffer.writeCharCode($space); - _write(node.value); + _write(value); } }); @@ -242,14 +240,16 @@ class _SerializeVisitor _writeOptionalSpace(); _for(node.url, () => _writeImportUrl(node.url.value)); - if (node.supports != null) { + var supports = node.supports; + if (supports != null) { _writeOptionalSpace(); - _write(node.supports); + _write(supports); } - if (node.media != null) { + var media = node.media; + if (media != null) { _writeOptionalSpace(); - _writeBetween(node.media, _commaSeparator, _visitMediaQuery); + _writeBetween(media, _commaSeparator, _visitMediaQuery); } }); } @@ -389,11 +389,8 @@ class _SerializeVisitor return; } - if (node.value.span != null) { - minimumIndentation = - math.min(minimumIndentation, node.name.span.start.column); - } - + minimumIndentation = + math.min(minimumIndentation, node.name.span.start.column); _writeWithIndent(value, minimumIndentation); } @@ -402,12 +399,12 @@ class _SerializeVisitor /// /// Returns `null` if [text] contains no newlines, and -1 if it contains /// newlines but no lines are indented. - int _minimumIndentation(String text) { + int? _minimumIndentation(String text) { var scanner = LineScanner(text); while (!scanner.isDone && scanner.readChar() != $lf) {} if (scanner.isDone) return scanner.peekChar(-1) == $lf ? -1 : null; - int min; + int? min; while (!scanner.isDone) { while (!scanner.isDone) { var next = scanner.peekChar(); @@ -607,10 +604,10 @@ class _SerializeVisitor throw SassScriptException("$map isn't a valid CSS value."); } _buffer.writeCharCode($lparen); - _writeBetween(map.contents.keys, ", ", (key) { - _writeMapElement(key); + _writeBetween>(map.contents.entries, ", ", (entry) { + _writeMapElement(entry.key); _buffer.write(": "); - _writeMapElement(map.contents[key]); + _writeMapElement(entry.value); }); _buffer.writeCharCode($rparen); } @@ -630,10 +627,11 @@ class _SerializeVisitor } void visitNumber(SassNumber value) { - if (value.asSlash != null) { - visitNumber(value.asSlash.item1); + var asSlash = value.asSlash; + if (asSlash != null) { + visitNumber(asSlash.item1); _buffer.writeCharCode($slash); - visitNumber(value.asSlash.item2); + visitNumber(asSlash.item2); return; } @@ -693,8 +691,8 @@ class _SerializeVisitor /// Otherwise, returns [text] as-is. String _removeExponent(String text) { // Don't allocate this until we know [text] contains exponent notation. - StringBuffer buffer; - int exponent; + StringBuffer? buffer; + late int exponent; for (var i = 0; i < text.length; i++) { var codeUnit = text.codeUnitAt(i); if (codeUnit != $e) continue; @@ -932,17 +930,19 @@ class _SerializeVisitor void visitAttributeSelector(AttributeSelector attribute) { _buffer.writeCharCode($lbracket); _buffer.write(attribute.name); - if (attribute.op != null) { + + var value = attribute.value; + if (value != null) { _buffer.write(attribute.op); - if (Parser.isIdentifier(attribute.value) && + if (Parser.isIdentifier(value) && // Emit identifiers that start with `--` with quotes, because IE11 // doesn't consider them to be valid identifiers. - !attribute.value.startsWith('--')) { - _buffer.write(attribute.value); + !value.startsWith('--')) { + _buffer.write(value); if (attribute.modifier != null) _buffer.writeCharCode($space); } else { - _visitQuotedString(attribute.value); + _visitQuotedString(value); if (attribute.modifier != null) _writeOptionalSpace(); } if (attribute.modifier != null) _buffer.write(attribute.modifier); @@ -956,7 +956,7 @@ class _SerializeVisitor } void visitComplexSelector(ComplexSelector complex) { - ComplexSelectorComponent lastComponent; + ComplexSelectorComponent? lastComponent; for (var component in complex.components) { if (lastComponent != null && !_omitSpacesAround(lastComponent) && @@ -1026,10 +1026,11 @@ class _SerializeVisitor } void visitPseudoSelector(PseudoSelector pseudo) { + var innerSelector = pseudo.selector; // `:not(%a)` is semantically identical to `*`. - if (pseudo.selector != null && + if (innerSelector != null && pseudo.name == 'not' && - pseudo.selector.isInvisible) { + innerSelector.isInvisible) { return; } @@ -1043,7 +1044,7 @@ class _SerializeVisitor _buffer.write(pseudo.argument); if (pseudo.selector != null) _buffer.writeCharCode($space); } - if (pseudo.selector != null) visitSelectorList(pseudo.selector); + if (innerSelector != null) visitSelectorList(innerSelector); _buffer.writeCharCode($rparen); } @@ -1078,24 +1079,25 @@ class _SerializeVisitor } _writeLineFeed(); - CssNode previous; + CssNode? previous_; _indent(() { for (var i = 0; i < children.length; i++) { var child = children[i]; if (_isInvisible(child)) continue; + var previous = previous_; // dart-lang/sdk#45348 if (previous != null) { if (_requiresSemicolon(previous)) _buffer.writeCharCode($semicolon); _writeLineFeed(); if (previous.isGroupEnd) _writeLineFeed(); } - previous = child; + previous_ = child; child.accept(this); } }); - if (_requiresSemicolon(previous) && !_isCompressed) { + if (_requiresSemicolon(previous_!) && !_isCompressed) { _buffer.writeCharCode($semicolon); } _writeLineFeed(); @@ -1104,7 +1106,7 @@ class _SerializeVisitor } /// Whether [node] requires a semicolon to be written after it. - bool _requiresSemicolon(CssNode node) => + bool _requiresSemicolon(CssNode? node) => node is CssParentNode ? node.isChildless : node is! CssComment; /// Writes a line feed, unless this emitting compressed CSS. @@ -1233,13 +1235,13 @@ class SerializeResult { /// 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; + final SingleMapping? sourceMap; /// A map from source file URLs to the corresponding [SourceFile]s. /// /// This can be passed to [sourceMap]'s [Mapping.spanFor] method. It's `null` /// if source mapping was disabled for this compilation. - final Map sourceFiles; + final Map? sourceFiles; SerializeResult(this.css, {this.sourceMap, this.sourceFiles}); } diff --git a/package.json b/package.json index 348bfed51..236146a4a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ ], "name": "sass", "devDependencies": { - "chokidar": ">=2.0.0 <4.0.0", + "chokidar": ">=3.0.0 <4.0.0", "fibers": ">=1.0.0 <5.0.0", "intercept-stdout": "^0.1.2", "node-sass": "^4.11.0" diff --git a/package/package.json b/package/package.json index 379ba662d..9a56c9e2b 100644 --- a/package/package.json +++ b/package/package.json @@ -17,7 +17,7 @@ "node": ">=8.9.0" }, "dependencies": { - "chokidar": ">=2.0.0 <4.0.0" + "chokidar": ">=3.0.0 <4.0.0" }, "keywords": ["style", "scss", "sass", "preprocessor", "css"] } diff --git a/pubspec.yaml b/pubspec.yaml index edd4de456..722cd36e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,40 +9,39 @@ executables: sass: sass environment: - sdk: '>=2.6.0 <3.0.0' + sdk: '>=2.12.0 <3.0.0' dependencies: - args: ">=1.4.0 <3.0.0" - async: ">=1.10.0 <3.0.0" - charcode: "^1.1.0" - cli_repl: ">=0.1.3 <0.3.0" - collection: "^1.8.0" - meta: "^1.1.7" - node_interop: "^1.1.0" - js: "^0.6.0" - package_resolver: "^1.0.0" - path: "^1.6.0" - source_maps: "^0.10.5" - source_span: "^1.6.0" - stack_trace: ">=0.9.0 <2.0.0" - stream_transform: ">=0.0.20 <3.0.0" - string_scanner: ">=0.1.5 <2.0.0" - term_glyph: "^1.0.0" - tuple: "^1.0.0" - watcher: ">=0.9.6 <2.0.0" + args: ^2.0.0 + async: ^2.5.0 + charcode: ^1.2.0 + cli_repl: ^0.2.1 + collection: ^1.15.0 + meta: ^1.3.0 + node_interop: ^2.0.0 + js: ^0.6.3 + path: ^1.8.0 + source_maps: ^0.10.10 + source_span: ^1.8.1 + stack_trace: ^1.10.0 + stream_transform: ^2.0.0 + string_scanner: ^1.1.0 + term_glyph: ^1.2.0 + tuple: ^2.0.0 + watcher: ^1.0.0 dev_dependencies: - archive: ">=1.0.0 <3.0.0" - analyzer: "^0.40.0" - cli_pkg: "^1.1.0" - crypto: ">=0.9.2 <3.0.0" - dart_style: "^1.2.0" - grinder: "^0.8.0" - node_preamble: "^1.1.0" - pedantic: "^1.0.0" - pub_semver: "^1.0.0" - stream_channel: ">=1.0.0 <3.0.0" - test_descriptor: "^1.1.0" - test_process: "^1.0.0-rc.1" - test: ">=0.12.42 <2.0.0" - yaml: "^2.0.0" + archive: ^3.1.2 + analyzer: ^1.1.0 + cli_pkg: ^1.3.0 + crypto: ^3.0.0 + dart_style: ^2.0.0 + grinder: ^0.9.0 + node_preamble: ^2.0.0 + pedantic: ^1.11.0 + pub_semver: ^2.0.0 + stream_channel: ^2.1.0 + test_descriptor: ^2.0.0 + test_process: ^2.0.0 + test: ^1.16.7 + yaml: ^3.1.0 diff --git a/test/cli/dart_test.dart b/test/cli/dart_test.dart index 639d08865..27a14c626 100644 --- a/test/cli/dart_test.dart +++ b/test/cli/dart_test.dart @@ -4,6 +4,8 @@ @TestOn('vm') +import 'dart:convert'; + import 'package:cli_pkg/testing.dart' as pkg; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -28,6 +30,6 @@ void main() { void ensureSnapshotUpToDate() => pkg.ensureExecutableUpToDate("sass"); Future runSass(Iterable arguments, - {Map environment}) => + {Map? environment}) => pkg.start("sass", arguments, - environment: environment, workingDirectory: d.sandbox); + environment: environment, workingDirectory: d.sandbox, encoding: utf8); diff --git a/test/cli/node_test.dart b/test/cli/node_test.dart index e27bb4fc5..9169cc6aa 100644 --- a/test/cli/node_test.dart +++ b/test/cli/node_test.dart @@ -5,6 +5,8 @@ @TestOn('vm') @Tags(['node']) +import 'dart:convert'; + import 'package:cli_pkg/testing.dart' as pkg; import 'package:test_process/test_process.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -29,6 +31,9 @@ void main() { } Future runSass(Iterable arguments, - {Map environment}) => + {Map? environment}) => pkg.start("sass", arguments, - environment: environment, workingDirectory: d.sandbox, node: true); + environment: environment, + workingDirectory: d.sandbox, + encoding: utf8, + node: true); diff --git a/test/cli/shared.dart b/test/cli/shared.dart index 736686da3..fb945ad7b 100644 --- a/test/cli/shared.dart +++ b/test/cli/shared.dart @@ -13,11 +13,11 @@ import 'package:test_process/test_process.dart'; /// Defines test that are shared between the Dart and Node.js CLI test suites. void sharedTests( Future runSass(Iterable arguments, - {Map environment})) { + {Map? environment})) { /// Runs the executable on [arguments] plus an output file, then verifies that /// the contents of the output file match [expected]. Future expectCompiles(List arguments, Object expected, - {Map environment}) async { + {Map? environment}) async { var sass = await runSass([...arguments, "out.css", "--no-source-map"], environment: environment); await sass.shouldExit(0); diff --git a/test/cli/shared/source_maps.dart b/test/cli/shared/source_maps.dart index 2df6c58ad..09d9a8b87 100644 --- a/test/cli/shared/source_maps.dart +++ b/test/cli/shared/source_maps.dart @@ -18,7 +18,7 @@ import '../../utils.dart'; /// Defines test that are shared between the Dart and Node.js CLI test suites. void sharedTests(Future runSass(Iterable arguments)) { group("for a simple compilation", () { - Map map; + late Map map; setUp(() async { await d.file("test.scss", "a {b: 1 + 2}").create(); @@ -35,7 +35,7 @@ void sharedTests(Future runSass(Iterable arguments)) { }); test("contains mappings", () { - SingleMapping sourceMap; + late SingleMapping sourceMap; sass.compileString("a {b: 1 + 2}", sourceMap: (map) => sourceMap = map); expect(map, containsPair("mappings", sourceMap.toJson()["mappings"])); }); @@ -278,7 +278,7 @@ void sharedTests(Future runSass(Iterable arguments)) { await d.file("test.scss", "a {b: 1 + 2}").create(); }); - Map map; + Map? map; group("with the target in the same directory", () { setUp(() async { await (await runSass(["--embed-source-map", "test.scss", "out.css"])) @@ -288,7 +288,7 @@ void sharedTests(Future runSass(Iterable arguments)) { }); test("contains mappings in the generated CSS", () { - SingleMapping sourceMap; + late SingleMapping sourceMap; sass.compileString("a {b: 1 + 2}", sourceMap: (map) => sourceMap = map); expect(map, containsPair("mappings", sourceMap.toJson()["mappings"])); }); @@ -359,5 +359,5 @@ void sharedTests(Future runSass(Iterable arguments)) { } /// Reads the file at [path] within [d.sandbox] and JSON-decodes it. -Map _readJson(String path) => - jsonDecode(readFile(d.path(path))) as Map; +Map _readJson(String path) => + jsonDecode(readFile(d.path(path))) as Map; diff --git a/test/dart_api/function_test.dart b/test/dart_api/function_test.dart index fcda027ee..bebefde0d 100644 --- a/test/dart_api/function_test.dart +++ b/test/dart_api/function_test.dart @@ -81,13 +81,6 @@ void main() { }, throwsA(const TypeMatcher())); }); - test("gracefully handles a custom function returning null", () { - expect(() { - compileString('a {b: foo()}', - functions: [Callable("foo", "", (arguments) => null)]); - }, throwsA(const TypeMatcher())); - }); - test("supports default argument values", () { var css = compileString('a {b: foo()}', functions: [ Callable("foo", r"$arg: 1", expectAsync1((arguments) { @@ -123,8 +116,8 @@ void main() { var list = arguments[0] as SassArgumentList; expect(list.asList, hasLength(0)); expect(list.keywords, contains("bar")); - expect(list.keywords["bar"].assertNumber().value, equals(1)); - return list.keywords["bar"]; + expect(list.keywords["bar"]!.assertNumber().value, equals(1)); + return list.keywords["bar"]!; })) ]); diff --git a/test/dart_api/importer_test.dart b/test/dart_api/importer_test.dart index c48aa95dc..7ffae8f9c 100644 --- a/test/dart_api/importer_test.dart +++ b/test/dart_api/importer_test.dart @@ -13,6 +13,7 @@ import 'package:sass/sass.dart'; import 'package:sass/src/exception.dart'; import 'test_importer.dart'; +import '../utils.dart'; void main() { test("uses an importer to resolve an @import", () { @@ -92,7 +93,7 @@ void main() { }); test("uses an importer's source map URL", () { - SingleMapping map; + late SingleMapping map; compileString('@import "orange";', importers: [ TestImporter((url) => Uri.parse("u:$url"), (url) { @@ -107,7 +108,7 @@ void main() { }); test("uses a data: source map URL if the importer doesn't provide one", () { - SingleMapping map; + late SingleMapping map; compileString('@import "orange";', importers: [ TestImporter((url) => Uri.parse("u:$url"), (url) { @@ -128,7 +129,7 @@ void main() { compileString('@import "orange";', importers: [ TestImporter((url) { throw "this import is bad actually"; - }, expectAsync1((_) => null, count: 0)) + }, expectNever1) ]); }, throwsA(predicate((error) { expect(error, const TypeMatcher()); diff --git a/test/dart_api/logger_test.dart b/test/dart_api/logger_test.dart index a981c1b54..b22564ff9 100644 --- a/test/dart_api/logger_test.dart +++ b/test/dart_api/logger_test.dart @@ -19,10 +19,11 @@ void main() { compileString(''' @mixin foo {@warn heck} @include foo; - ''', logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ''', logger: _TestLogger.withWarn((message, + {span, trace, deprecation = false}) { expect(message, equals("heck")); expect(span, isNull); - expect(trace.frames.first.member, equals('foo()')); + expect(trace!.frames.first.member, equals('foo()')); expect(deprecation, isFalse); mustBeCalled(); })); @@ -30,8 +31,8 @@ void main() { test("stringifies the argument", () { var mustBeCalled = expectAsync0(() {}); - compileString('@warn #abc', - logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + compileString('@warn #abc', logger: + _TestLogger.withWarn((message, {span, trace, deprecation = false}) { expect(message, equals("#abc")); mustBeCalled(); })); @@ -39,8 +40,8 @@ void main() { test("doesn't inspect the argument", () { var mustBeCalled = expectAsync0(() {}); - compileString('@warn null', - logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + compileString('@warn null', logger: + _TestLogger.withWarn((message, {span, trace, deprecation = false}) { expect(message, isEmpty); mustBeCalled(); })); @@ -76,11 +77,11 @@ void main() { test("with a parser warning passes the message and span", () { var mustBeCalled = expectAsync0(() {}); - compileString('a {b: c && d}', - logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + compileString('a {b: c && d}', logger: + _TestLogger.withWarn((message, {span, trace, deprecation = false}) { expect(message, contains('"&&" means two copies')); - expect(span.start.line, equals(0)); + expect(span!.start.line, equals(0)); expect(span.start.column, equals(8)); expect(span.end.line, equals(0)); expect(span.end.column, equals(10)); @@ -96,15 +97,16 @@ void main() { compileString(''' @mixin foo {#{blue} {x: y}} @include foo; - ''', logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ''', logger: + _TestLogger.withWarn((message, {span, trace, deprecation = false}) { expect(message, contains("color value blue")); - expect(span.start.line, equals(0)); + expect(span!.start.line, equals(0)); expect(span.start.column, equals(22)); expect(span.end.line, equals(0)); expect(span.end.column, equals(26)); - expect(trace.frames.first.member, equals('foo()')); + expect(trace!.frames.first.member, equals('foo()')); expect(deprecation, isFalse); mustBeCalled(); })); @@ -122,15 +124,16 @@ void main() { warn("heck"); return sassNull; })) - ], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ], logger: _TestLogger.withWarn((message, + {span, trace, deprecation = false}) { expect(message, equals("heck")); - expect(span.start.line, equals(0)); + expect(span!.start.line, equals(0)); expect(span.start.column, equals(33)); expect(span.end.line, equals(0)); expect(span.end.column, equals(38)); - expect(trace.frames.first.member, equals('bar()')); + expect(trace!.frames.first.member, equals('bar()')); expect(deprecation, isFalse); mustBeCalled(); })); @@ -146,15 +149,16 @@ void main() { warn("heck"); return sassNull; })) - ], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ], logger: _TestLogger.withWarn((message, + {span, trace, deprecation = false}) { expect(message, equals("heck")); - expect(span.start.line, equals(0)); + expect(span!.start.line, equals(0)); expect(span.start.column, equals(33)); expect(span.end.line, equals(0)); expect(span.end.column, equals(38)); - expect(trace.frames.first.member, equals('bar()')); + expect(trace!.frames.first.member, equals('bar()')); expect(deprecation, isFalse); mustBeCalled(); })); @@ -171,15 +175,16 @@ void main() { warn("heck"); return sassNull; })) - ], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ], logger: _TestLogger.withWarn((message, + {span, trace, deprecation = false}) { expect(message, equals("heck")); - expect(span.start.line, equals(0)); + expect(span!.start.line, equals(0)); expect(span.start.column, equals(33)); expect(span.end.line, equals(0)); expect(span.end.column, equals(38)); - expect(trace.frames.first.member, equals('bar()')); + expect(trace!.frames.first.member, equals('bar()')); expect(deprecation, isFalse); mustBeCalled(); })); @@ -193,15 +198,16 @@ void main() { warn("heck"); return ImporterResult("", indented: false); }) - ], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ], logger: + _TestLogger.withWarn((message, {span, trace, deprecation = false}) { expect(message, equals("heck")); - expect(span.start.line, equals(0)); + expect(span!.start.line, equals(0)); expect(span.start.column, equals(8)); expect(span.end.line, equals(0)); expect(span.end.column, equals(13)); - expect(trace.frames.first.member, equals('@import')); + expect(trace!.frames.first.member, equals('@import')); expect(deprecation, isFalse); mustBeCalled(); })); @@ -214,7 +220,8 @@ void main() { warn("heck", deprecation: true); return sassNull; })) - ], logger: _TestLogger.withWarn((message, {span, trace, deprecation}) { + ], logger: + _TestLogger.withWarn((message, {span, trace, deprecation = false}) { expect(message, equals("heck")); expect(deprecation, isTrue); mustBeCalled(); @@ -229,7 +236,7 @@ void main() { /// A [Logger] whose [warn] and [debug] methods are provided by callbacks. class _TestLogger implements Logger { - final void Function(String, {FileSpan span, Trace trace, bool deprecation}) + final void Function(String, {FileSpan? span, Trace? trace, bool deprecation}) _warn; final void Function(String, SourceSpan) _debug; @@ -238,7 +245,7 @@ class _TestLogger implements Logger { _TestLogger.withDebug(this._debug) : _warn = const Logger.stderr().warn; void warn(String message, - {FileSpan span, Trace trace, bool deprecation = false}) => + {FileSpan? span, Trace? trace, bool deprecation = false}) => _warn(message, span: span, trace: trace, deprecation: deprecation); void debug(String message, SourceSpan span) => _debug(message, span); } diff --git a/test/dart_api/test_importer.dart b/test/dart_api/test_importer.dart index 5dac8e0d7..87af315df 100644 --- a/test/dart_api/test_importer.dart +++ b/test/dart_api/test_importer.dart @@ -7,12 +7,12 @@ import 'package:sass/sass.dart'; /// An [Importer] whose [canonicalize] and [load] methods are provided by /// closures. class TestImporter extends Importer { - final Uri Function(Uri url) _canonicalize; - final ImporterResult Function(Uri url) _load; + final Uri? Function(Uri url) _canonicalize; + final ImporterResult? Function(Uri url) _load; TestImporter(this._canonicalize, this._load); - Uri canonicalize(Uri url) => _canonicalize(url); + Uri? canonicalize(Uri url) => _canonicalize(url); - ImporterResult load(Uri url) => _load(url); + ImporterResult? load(Uri url) => _load(url); } diff --git a/test/dart_api/value/boolean_test.dart b/test/dart_api/value/boolean_test.dart index 875431485..606cfab7d 100644 --- a/test/dart_api/value/boolean_test.dart +++ b/test/dart_api/value/boolean_test.dart @@ -12,7 +12,7 @@ import 'utils.dart'; void main() { group("true", () { - Value value; + late Value value; setUp(() => value = parseValue("true")); test("is truthy", () { @@ -38,7 +38,7 @@ void main() { }); group("false", () { - Value value; + late Value value; setUp(() => value = parseValue("false")); test("is falsey", () { diff --git a/test/dart_api/value/color_test.dart b/test/dart_api/value/color_test.dart index 1fda28a75..c3a8ca240 100644 --- a/test/dart_api/value/color_test.dart +++ b/test/dart_api/value/color_test.dart @@ -13,7 +13,7 @@ import 'utils.dart'; void main() { group("an RGB color", () { - SassColor value; + late SassColor value; setUp(() => value = parseValue("#123456") as SassColor); test("has RGB channels", () { @@ -191,7 +191,7 @@ void main() { }); group("an HSL color", () { - SassColor value; + late SassColor value; setUp(() => value = parseValue("hsl(120, 42%, 42%)") as SassColor); test("has RGB channels", () { @@ -267,7 +267,7 @@ void main() { }); group("new SassColor.hwb()", () { - SassColor value; + late SassColor value; setUp(() => value = SassColor.hwb(120, 42, 42)); test("has RGB channels", () { diff --git a/test/dart_api/value/function_test.dart b/test/dart_api/value/function_test.dart index bca96635a..0b107b25b 100644 --- a/test/dart_api/value/function_test.dart +++ b/test/dart_api/value/function_test.dart @@ -12,7 +12,7 @@ import 'utils.dart'; void main() { group("a function value", () { - SassFunction value; + late SassFunction value; setUp(() => value = parseValue("get-function('red')") as SassFunction); test("has a callable with the given name", () { diff --git a/test/dart_api/value/list_test.dart b/test/dart_api/value/list_test.dart index 9db1dce88..b0ddcaa20 100644 --- a/test/dart_api/value/list_test.dart +++ b/test/dart_api/value/list_test.dart @@ -12,7 +12,7 @@ import 'utils.dart'; void main() { group("a comma-separated list", () { - Value value; + late Value value; setUp(() => value = parseValue("a, b, c")); test("is comma-separated", () { @@ -125,7 +125,7 @@ void main() { }); group("a single-element list", () { - Value value; + late Value value; setUp(() => value = parseValue("[1]")); test("has an undecided separator", () { @@ -152,7 +152,7 @@ void main() { }); group("an empty list", () { - Value value; + late Value value; setUp(() => value = parseValue("()")); test("has an undecided separator", () { @@ -169,7 +169,7 @@ void main() { test("counts as an empty map", () { expect(value.assertMap().contents, isEmpty); - expect(value.tryMap().contents, isEmpty); + expect(value.tryMap()!.contents, isEmpty); }); test("isn't any other type", () { @@ -191,7 +191,7 @@ void main() { }); group("a scalar value", () { - Value value; + late Value value; setUp(() => value = parseValue("blue")); test("has an undecided separator", () { diff --git a/test/dart_api/value/map_test.dart b/test/dart_api/value/map_test.dart index 23e241cb8..8c92c50a5 100644 --- a/test/dart_api/value/map_test.dart +++ b/test/dart_api/value/map_test.dart @@ -12,7 +12,7 @@ import 'utils.dart'; void main() { group("a map with contents", () { - SassMap value; + late SassMap value; setUp(() => value = parseValue("(a: b, c: d)") as SassMap); test("has an undecided separator", () { @@ -141,7 +141,7 @@ void main() { }); group("an empty map", () { - SassMap value; + late SassMap value; setUp(() => value = parseValue("map-remove((a: b), a)") as SassMap); test("has an undecided separator", () { diff --git a/test/dart_api/value/null_test.dart b/test/dart_api/value/null_test.dart index 6e3e9295d..329f45f4d 100644 --- a/test/dart_api/value/null_test.dart +++ b/test/dart_api/value/null_test.dart @@ -11,7 +11,7 @@ import 'package:sass/sass.dart'; import 'utils.dart'; void main() { - Value value; + late Value value; setUp(() => value = parseValue("null")); test("is falsey", () { diff --git a/test/dart_api/value/number_test.dart b/test/dart_api/value/number_test.dart index 6ab1138db..89e1eea46 100644 --- a/test/dart_api/value/number_test.dart +++ b/test/dart_api/value/number_test.dart @@ -14,7 +14,7 @@ import 'utils.dart'; void main() { group("a unitless integer", () { - SassNumber value; + late SassNumber value; setUp(() => value = parseValue("123") as SassNumber); test("has the correct value", () { @@ -139,7 +139,7 @@ void main() { }); group("a unitless double", () { - SassNumber value; + late SassNumber value; setUp(() => value = parseValue("123.456") as SassNumber); test("has the correct value", () { @@ -154,7 +154,7 @@ void main() { }); group("a unitless fuzzy integer", () { - SassNumber value; + late SassNumber value; setUp(() => value = parseValue("123.000000000001") as SassNumber); test("has the correct value", () { @@ -197,7 +197,7 @@ void main() { }); group("an integer with a single numerator unit", () { - SassNumber value; + late SassNumber value; setUp(() => value = parseValue("123px") as SassNumber); test("has that unit", () { @@ -312,7 +312,7 @@ void main() { }); group("an integer with numerator and denominator units", () { - SassNumber value; + late SassNumber value; setUp(() => value = parseValue("123px / 5ms") as SassNumber); test("has those units", () { diff --git a/test/dart_api/value/string_test.dart b/test/dart_api/value/string_test.dart index 3e8c3642e..f625ce8b3 100644 --- a/test/dart_api/value/string_test.dart +++ b/test/dart_api/value/string_test.dart @@ -12,7 +12,7 @@ import 'utils.dart'; void main() { group("an unquoted ASCII string", () { - SassString value; + late SassString value; setUp(() => value = parseValue("foobar") as SassString); test("has the correct text", () { @@ -125,7 +125,7 @@ void main() { }); group("a quoted ASCII string", () { - SassString value; + late SassString value; setUp(() => value = parseValue('"foobar"') as SassString); test("has the correct text", () { @@ -143,7 +143,7 @@ void main() { }); group("an unquoted Unicde", () { - SassString value; + late SassString value; setUp(() => value = parseValue("a👭b👬c") as SassString); test("sassLength returns the length", () { diff --git a/test/dart_api/value/utils.dart b/test/dart_api/value/utils.dart index 0bec45ea3..ae2435de3 100644 --- a/test/dart_api/value/utils.dart +++ b/test/dart_api/value/utils.dart @@ -9,7 +9,7 @@ import 'package:sass/src/exception.dart'; /// Parses [source] by way of a function call. Value parseValue(String source) { - Value value; + late Value value; compileString("a {b: foo(($source))}", functions: [ Callable("foo", r"$arg", expectAsync1((arguments) { expect(arguments, hasLength(1)); diff --git a/test/dart_api_test.dart b/test/dart_api_test.dart index 7d628b4f4..5ccb36961 100644 --- a/test/dart_api_test.dart +++ b/test/dart_api_test.dart @@ -4,7 +4,7 @@ @TestOn('vm') -import 'package:package_resolver/package_resolver.dart'; +import 'package:package_config/package_config.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; @@ -93,10 +93,10 @@ void main() { await d .file("test.scss", '@import "package:fake_package/test";') .create(); - var resolver = SyncPackageResolver.config( - {"fake_package": p.toUri(d.path('subdir'))}); + var config = + PackageConfig([Package('fake_package', p.toUri(d.path('subdir/')))]); - var css = compile(d.path("test.scss"), packageResolver: resolver); + var css = compile(d.path("test.scss"), packageConfig: config); expect(css, equals("a {\n b: 3;\n}")); }); @@ -109,10 +109,10 @@ void main() { await d .file("test.scss", '@import "package:fake_package/test";') .create(); - var resolver = SyncPackageResolver.config( - {"fake_package": p.toUri(d.path('subdir'))}); + var config = + PackageConfig([Package('fake_package', p.toUri(d.path('subdir/')))]); - var css = compile(d.path("test.scss"), packageResolver: resolver); + var css = compile(d.path("test.scss"), packageConfig: config); expect(css, equals("a {\n b: 3;\n}")); }); @@ -120,9 +120,9 @@ void main() { await d .file("test.scss", '@import "package:fake_package/test_aux";') .create(); - var resolver = SyncPackageResolver.config({}); - expect(() => compile(d.path("test.scss"), packageResolver: resolver), + expect( + () => compile(d.path("test.scss"), packageConfig: PackageConfig([])), throwsA(const TypeMatcher())); }); }); @@ -166,9 +166,9 @@ void main() { expect(css, equals("a {\n b: from-importer;\n}")); }); - test("importers take precedence over packageResolver", () async { + test("importers take precedence over packageConfig", () async { await d.dir("package", - [d.file("other.scss", "a {b: from-package-resolver}")]).create(); + [d.file("other.scss", "a {b: from-package-config}")]).create(); await d.dir( "importer", [d.file("other.scss", "a {b: from-importer}")]).create(); await d @@ -177,11 +177,11 @@ void main() { var css = compile(d.path("test.scss"), importers: [ - PackageImporter(SyncPackageResolver.config( - {"fake_package": p.toUri(d.path('importer'))})) + PackageImporter(PackageConfig( + [Package('fake_package', p.toUri(d.path('importer/')))])) ], - packageResolver: SyncPackageResolver.config( - {"fake_package": p.toUri(d.path('package'))})); + packageConfig: PackageConfig( + [Package('fake_package', p.toUri(d.path('package/')))])); expect(css, equals("a {\n b: from-importer;\n}")); }); }); diff --git a/test/doc_comments_test.dart b/test/doc_comments_test.dart index 9abdbcb87..7e20c5311 100644 --- a/test/doc_comments_test.dart +++ b/test/doc_comments_test.dart @@ -16,7 +16,7 @@ void main() { final variable = stylesheet.children.whereType().first; - expect(variable.comment.docComment, equals('Results my vary.')); + expect(variable.comment!.docComment, equals('Results my vary.')); }); test('attach to function rules', () { @@ -29,7 +29,7 @@ void main() { final stylesheet = Stylesheet.parseScss(contents); final function = stylesheet.children.whereType().first; - expect(function.comment.docComment, equals('A fun function!')); + expect(function.comment!.docComment, equals('A fun function!')); }); test('attach to mixin rules', () { @@ -43,7 +43,7 @@ void main() { final stylesheet = Stylesheet.parseScss(contents); final mix = stylesheet.children.whereType().first; - expect(mix.comment.docComment, equals('Mysterious mixin.')); + expect(mix.comment!.docComment, equals('Mysterious mixin.')); }); test('are null when there are no triple-slash comments', () { @@ -54,7 +54,7 @@ void main() { final variable = stylesheet.children.whereType().first; - expect(variable.comment.docComment, isNull); + expect(variable.comment!.docComment, isNull); }); test('are not carried over across members', () { @@ -75,8 +75,8 @@ void main() { final mix = stylesheet.children.whereType().first; final function = stylesheet.children.whereType().first; - expect(mix.comment.docComment, equals('Mysterious mixin.')); - expect(function.comment.docComment, equals('A fun function!')); + expect(mix.comment!.docComment, equals('Mysterious mixin.')); + expect(function.comment!.docComment, equals('A fun function!')); }); test('do not include double-slash comments', () { @@ -92,7 +92,7 @@ void main() { final variable = stylesheet.children.whereType().first; - expect(variable.comment.docComment, equals('Line 1\nLine 2\nLine 3')); + expect(variable.comment!.docComment, equals('Line 1\nLine 2\nLine 3')); }); }); @@ -105,7 +105,7 @@ $vary: 5.16em'''; final variable = stylesheet.children.whereType().first; - expect(variable.comment.docComment, equals('Results my vary.')); + expect(variable.comment!.docComment, equals('Results my vary.')); }); test('attach to function rules', () { @@ -117,7 +117,7 @@ $vary: 5.16em'''; final stylesheet = Stylesheet.parseSass(contents); final function = stylesheet.children.whereType().first; - expect(function.comment.docComment, equals('A fun function!')); + expect(function.comment!.docComment, equals('A fun function!')); }); test('attach to mixin rules', () { @@ -130,7 +130,7 @@ $vary: 5.16em'''; final stylesheet = Stylesheet.parseSass(contents); final mix = stylesheet.children.whereType().first; - expect(mix.comment.docComment, equals('Mysterious mixin.')); + expect(mix.comment!.docComment, equals('Mysterious mixin.')); }); test('are null when there are no triple-slash comments', () { @@ -141,7 +141,7 @@ $vary: 5.16em'''; final variable = stylesheet.children.whereType().first; - expect(variable.comment.docComment, isNull); + expect(variable.comment!.docComment, isNull); }); test('are not carried over across members', () { @@ -160,8 +160,8 @@ $vary: 5.16em'''; final mix = stylesheet.children.whereType().first; final function = stylesheet.children.whereType().first; - expect(mix.comment.docComment, equals('Mysterious mixin.')); - expect(function.comment.docComment, equals('A fun function!')); + expect(mix.comment!.docComment, equals('Mysterious mixin.')); + expect(function.comment!.docComment, equals('A fun function!')); }); test('do not include double-slash comments', () { @@ -176,7 +176,7 @@ $vary: 5.16em'''; final variable = stylesheet.children.whereType().first; - expect(variable.comment.docComment, equals('Line 1\nLine 2')); + expect(variable.comment!.docComment, equals('Line 1\nLine 2')); }); test('are compacted into one from adjacent comments', () { @@ -192,7 +192,7 @@ $vary: 5.16em'''; stylesheet.children.whereType().first; expect(stylesheet.children.length, equals(2)); - expect(variable.comment.docComment, + expect(variable.comment!.docComment, equals('Line 1\nLine 2\nLine 3\nLine 4')); }); }); diff --git a/test/double_check_test.dart b/test/double_check_test.dart index b18502cdb..5b9d38445 100644 --- a/test/double_check_test.dart +++ b/test/double_check_test.dart @@ -21,14 +21,16 @@ void main() { synchronize.sources.forEach((sourcePath, targetPath) { test(targetPath, () { + var message = "$targetPath is out-of-date.\n" + "Run pub run grinder to update it."; + var target = File(targetPath).readAsStringSync(); - var actualHash = checksumPattern.firstMatch(target)[1]; + var match = checksumPattern.firstMatch(target); + if (match == null) fail(message); var source = File(sourcePath).readAsBytesSync(); var expectedHash = sha1.convert(source).toString(); - expect(actualHash, equals(expectedHash), - reason: "$targetPath is out-of-date.\n" - "Run pub run grinder to update it."); + expect(match[1], equals(expectedHash), reason: message); }); }); }, @@ -44,7 +46,7 @@ void main() { var changelogVersion = firstLine.substring(3); var pubspec = loadYaml(File("pubspec.yaml").readAsStringSync(), - sourceUrl: "pubspec.yaml") as Map; + sourceUrl: Uri(path: "pubspec.yaml")) as Map; expect(pubspec, containsPair("version", isA())); var pubspecVersion = pubspec["version"] as String; diff --git a/test/ensure_npm_package.dart b/test/ensure_npm_package.dart index 250f4be06..5e73c3c54 100644 --- a/test/ensure_npm_package.dart +++ b/test/ensure_npm_package.dart @@ -18,7 +18,7 @@ Future ensureNpmPackage() async { import 'package:cli_pkg/testing.dart' as pkg; import 'package:stream_channel/stream_channel.dart'; - void hybridMain(StreamChannel channel) async { + void hybridMain(StreamChannel channel) async { pkg.ensureExecutableUpToDate("sass", node: true); channel.sink.close(); } diff --git a/test/hybrid.dart b/test/hybrid.dart index 2629dd552..d4ec47941 100644 --- a/test/hybrid.dart +++ b/test/hybrid.dart @@ -12,20 +12,21 @@ Future createTempDir() async => (await runHybridExpression( /// Writes [text] to [path]. Future writeTextFile(String path, String text) => runHybridExpression( - 'new File(message[0]).writeAsString(message[1])', [path, text]); + 'File(message[0]).writeAsString(message[1])', [path, text]); /// Creates a directory at [path]. Future createDirectory(String path) => - runHybridExpression('new Directory(message).create()', path); + runHybridExpression('Directory(message).create()', path); /// Recursively deletes the directory at [path]. Future deleteDirectory(String path) => - runHybridExpression('new Directory(message).delete(recursive: true)', path); + runHybridExpression('Directory(message).delete(recursive: true)', path); /// Runs [expression], which may be asynchronous, in a hybrid isolate. /// /// Returns the result of [expression] if it's JSON-serializable. -Future runHybridExpression(String expression, [Object message]) async { +Future runHybridExpression(String expression, + [Object? message]) async { var channel = spawnHybridCode(''' import 'dart:async'; import 'dart:convert'; @@ -33,8 +34,8 @@ Future runHybridExpression(String expression, [Object message]) async { import 'package:stream_channel/stream_channel.dart'; - hybridMain(StreamChannel channel, message) async { - var result = await ${expression}; + hybridMain(StreamChannel channel, dynamic message) async { + var result = await $expression; channel.sink.add(_isJsonSafe(result) ? jsonEncode(result) : 'null'); channel.sink.close(); } diff --git a/test/node_api/api.dart b/test/node_api/api.dart index 772bed676..8a0dbddbe 100644 --- a/test/node_api/api.dart +++ b/test/node_api/api.dart @@ -43,7 +43,7 @@ external FiberClass _requireFiber(String path); class Sass { external RenderResult renderSync(RenderOptions args); external void render(RenderOptions args, - void callback(RenderError error, RenderResult result)); + void callback(RenderError? error, RenderResult? result)); external SassTypes get types; external Object get NULL; external NodeSassBoolean get TRUE; @@ -117,9 +117,9 @@ class NodeSassList { class NodeSassMap { external Constructor get constructor; external Object getValue(int index); - external void setValue(int index, Object value); + external void setValue(int index, Object? value); external Object getKey(int index); - external void setKey(int index, Object value); + external void setKey(int index, Object? value); external int getLength(); } diff --git a/test/node_api/function_test.dart b/test/node_api/function_test.dart index e56584025..1ce978002 100644 --- a/test/node_api/function_test.dart +++ b/test/node_api/function_test.dart @@ -186,7 +186,7 @@ void main() { }); group('this', () { - String sassPath; + late String sassPath; setUp(() async { sassPath = p.join(sandbox, 'test.scss'); }); @@ -369,7 +369,7 @@ void main() { render(RenderOptions( data: "a {b: foo()}", functions: jsify({ - "foo": allowInterop((void done(Object result)) { + "foo": allowInterop((void done(Object? result)) { Timer(Duration.zero, () { done(callConstructor(sass.types.Number, [1])); }); @@ -413,7 +413,7 @@ void main() { var error = await renderError(RenderOptions( data: "a {b: foo()}", functions: jsify({ - "foo": allowInterop((void done(Object result)) { + "foo": allowInterop((void done(Object? result)) { Timer(Duration.zero, () { done(callConstructor(sass.types.Error, ["aw beans"])); }); @@ -426,7 +426,7 @@ void main() { var error = await renderError(RenderOptions( data: "a {b: foo()}", functions: jsify({ - "foo": allowInterop((void done(Object result)) { + "foo": allowInterop((void done(Object? result)) { Timer(Duration.zero, () { done(null); }); @@ -481,7 +481,7 @@ void main() { render(RenderOptions( data: "a {b: foo()}", functions: jsify({ - "foo": allowInterop((void done(Object result)) { + "foo": allowInterop((void done(Object? result)) { Timer(Duration.zero, () { done(callConstructor(sass.types.Number, [1])); }); @@ -518,7 +518,7 @@ void main() { var error = await renderError(RenderOptions( data: "a {b: foo()}", functions: jsify({ - "foo": allowInterop((void done(Object result)) { + "foo": allowInterop((void done(Object? result)) { Timer(Duration.zero, () { done(null); }); diff --git a/test/node_api/importer_test.dart b/test/node_api/importer_test.dart index f475cc93f..2a2c47f95 100644 --- a/test/node_api/importer_test.dart +++ b/test/node_api/importer_test.dart @@ -21,12 +21,12 @@ import '../hybrid.dart'; import 'api.dart'; import 'utils.dart'; -String sassPath; - void main() { setUpAll(ensureNpmPackage); useSandbox(); + late String sassPath; + setUp(() async { sassPath = p.join(sandbox, 'test.scss'); await writeTextFile(sassPath, 'a {b: c}'); @@ -274,7 +274,7 @@ void main() { }); group("in the sandbox directory", () { - String oldWorkingDirectory; + late String oldWorkingDirectory; setUp(() { oldWorkingDirectory = currentPath; process.chdir(sandbox); diff --git a/test/node_api/intercept_stdout.dart b/test/node_api/intercept_stdout.dart index 54a8b76a7..dad528fcf 100644 --- a/test/node_api/intercept_stdout.dart +++ b/test/node_api/intercept_stdout.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'package:js/js.dart'; typedef _InterceptStdout = void Function() Function( - String Function(String), String Function(String)); + String Function(String)?, String Function(String)); @JS('require') external _InterceptStdout _require(String name); @@ -19,8 +19,8 @@ final _interceptStdout = _require("intercept-stdout"); /// /// Note that the piped text is not necessarily separated by lines. Stream interceptStderr() { - void Function() unhook; - StreamController controller; + late void Function() unhook; + late StreamController controller; controller = StreamController(onListen: () { unhook = _interceptStdout(null, allowInterop((text) { controller.add(text); diff --git a/test/node_api/source_map_test.dart b/test/node_api/source_map_test.dart index ff87e1512..a0cf525ef 100644 --- a/test/node_api/source_map_test.dart +++ b/test/node_api/source_map_test.dart @@ -28,17 +28,17 @@ void main() { useSandbox(); group("a basic invocation", () { - String css; - Map map; + late String css; + late Map map; setUp(() { var result = sass.renderSync( RenderOptions(data: "a {b: c}", sourceMap: true, outFile: "out.css")); css = utf8.decode(result.css); - map = _jsonUtf8.decode(result.map) as Map; + map = _jsonUtf8.decode(result.map!) as Map; }); test("includes correct mappings", () { - SingleMapping expectedMap; + late SingleMapping expectedMap; dart_sass.compileString("a {b: c}", sourceMap: (map) => expectedMap = map); expectedMap.targetUrl = "out.css"; @@ -158,7 +158,7 @@ void main() { test("emits a source map", () { var result = sass.renderSync( RenderOptions(data: "a {b: c}", sourceMap: "out.css.map")); - var map = _jsonUtf8.decode(result.map) as Map; + var map = _jsonUtf8.decode(result.map!) as Map; expect(map, containsPair("sources", ["stdin"])); }); @@ -168,7 +168,7 @@ void main() { var result = sass.renderSync(RenderOptions( file: p.join(sandbox, "test.scss"), sourceMap: "out.css.map")); - var map = _jsonUtf8.decode(result.map) as Map; + var map = _jsonUtf8.decode(result.map!) as Map; expect( map, containsPair( @@ -182,7 +182,7 @@ void main() { var result = sass.renderSync(RenderOptions( file: p.join(sandbox, "test"), sourceMap: "out.css.map")); - var map = _jsonUtf8.decode(result.map) as Map; + var map = _jsonUtf8.decode(result.map!) as Map; expect( map, containsPair( @@ -192,7 +192,7 @@ void main() { test("derives the target URL from stdin", () { var result = sass.renderSync( RenderOptions(data: "a {b: c}", sourceMap: "out.css.map")); - var map = _jsonUtf8.decode(result.map) as Map; + var map = _jsonUtf8.decode(result.map!) as Map; expect(map, containsPair("file", "stdin.css")); }); @@ -301,7 +301,7 @@ void main() { sourceMapEmbed: true)); var map = embeddedSourceMap(utf8.decode(result.css)); - expect(map, equals(_jsonUtf8.decode(result.map))); + expect(map, equals(_jsonUtf8.decode(result.map!))); }); group("with sourceMapRoot", () { @@ -331,5 +331,5 @@ void main() { } /// Renders [options] and returns the decoded source map. -Map _renderSourceMap(RenderOptions options) => - _jsonUtf8.decode(sass.renderSync(options).map) as Map; +Map _renderSourceMap(RenderOptions options) => + _jsonUtf8.decode(sass.renderSync(options).map!) as Map; diff --git a/test/node_api/utils.dart b/test/node_api/utils.dart index 3e5aff785..f9a186aa9 100644 --- a/test/node_api/utils.dart +++ b/test/node_api/utils.dart @@ -12,6 +12,7 @@ import 'package:node_interop/node_interop.dart'; import 'package:sass/src/io.dart'; import 'package:sass/src/node/function.dart'; +import 'package:sass/src/util/nullable.dart'; import '../hybrid.dart'; import 'api.dart'; @@ -19,21 +20,28 @@ import 'api.dart'; @JS('process.env') external Object get _environment; -String sandbox; +String get sandbox { + var sandbox = _sandbox; + if (sandbox != null) return sandbox; + fail("useSandbox() must be called in any test file that uses the sandbox " + "field."); +} + +String? _sandbox; void useSandbox() { setUp(() async { - sandbox = await createTempDir(); + _sandbox = await createTempDir(); }); tearDown(() async { - if (sandbox != null) await deleteDirectory(sandbox); + await sandbox.andThen(deleteDirectory); }); } /// Validates that a [RenderError]'s `toString()` and `message` both equal /// [text]. -Matcher toStringAndMessageEqual(String text) => predicate((error) { +Matcher toStringAndMessageEqual(String text) => predicate((dynamic error) { expect(error.toString(), equals("Error: $text")); expect(error.message, equals(text)); expect(error.formatted, equals("Error: $text")); @@ -46,7 +54,7 @@ Future render(RenderOptions options) { sass.render(options, allowInterop(Zone.current.bindBinaryCallbackGuarded((error, result) { expect(error, isNull); - completer.complete(utf8.decode(result.css)); + completer.complete(utf8.decode(result!.css)); }))); return completer.future; } @@ -58,7 +66,7 @@ Future renderError(RenderOptions options) { sass.render(options, allowInterop(Zone.current.bindBinaryCallbackGuarded((error, result) { expect(result, isNull); - completer.complete(error); + completer.complete(error!); }))); return completer.future; } @@ -103,7 +111,7 @@ void runTestInSandbox() { } /// Sets the environment variable [name] to [value] within this process. -void setEnvironmentVariable(String name, String value) { +void setEnvironmentVariable(String name, String? value) { setProperty(_environment, name, value); } diff --git a/test/node_api/value/color_test.dart b/test/node_api/value/color_test.dart index 0ba05979c..012ef1909 100644 --- a/test/node_api/value/color_test.dart +++ b/test/node_api/value/color_test.dart @@ -18,7 +18,7 @@ import 'utils.dart'; void main() { group("from a parameter", () { - NodeSassColor color; + late NodeSassColor color; setUp(() { color = parseValue("rgba(42, 84, 126, 0.42)"); }); diff --git a/test/node_api/value/list_test.dart b/test/node_api/value/list_test.dart index c23310330..20129d847 100644 --- a/test/node_api/value/list_test.dart +++ b/test/node_api/value/list_test.dart @@ -16,7 +16,7 @@ import 'utils.dart'; void main() { group("an argument list", () { - NodeSassList args; + late NodeSassList args; setUp(() { renderSync(RenderOptions( data: "a {b: foo(1, 'a', blue)}", @@ -47,7 +47,7 @@ void main() { group("a list", () { group("from a parameter", () { - NodeSassList list; + late NodeSassList list; setUp(() { list = parseValue("1, 'a', blue"); }); diff --git a/test/node_api/value/map_test.dart b/test/node_api/value/map_test.dart index 973bc9c63..c80c50bd3 100644 --- a/test/node_api/value/map_test.dart +++ b/test/node_api/value/map_test.dart @@ -16,7 +16,7 @@ import 'utils.dart'; void main() { group("from a parameter", () { - NodeSassMap map; + late NodeSassMap map; setUp(() { map = parseValue("(a: 2, 1: blue, red: b)"); }); diff --git a/test/node_api/value/null_test.dart b/test/node_api/value/null_test.dart index c0c14b49d..a13320d21 100644 --- a/test/node_api/value/null_test.dart +++ b/test/node_api/value/null_test.dart @@ -14,7 +14,7 @@ import 'utils.dart'; void main() { group("from a parameter", () { - NodeSassNull value; + late NodeSassNull value; setUp(() { value = parseValue("null"); }); diff --git a/test/node_api/value/number_test.dart b/test/node_api/value/number_test.dart index 64929a2fb..a431baac6 100644 --- a/test/node_api/value/number_test.dart +++ b/test/node_api/value/number_test.dart @@ -16,7 +16,7 @@ import 'utils.dart'; void main() { group("from a parameter", () { - NodeSassNumber number; + late NodeSassNumber number; setUp(() { number = parseValue("1px"); }); diff --git a/test/node_api/value/string_test.dart b/test/node_api/value/string_test.dart index 321c39858..e385646b3 100644 --- a/test/node_api/value/string_test.dart +++ b/test/node_api/value/string_test.dart @@ -16,7 +16,7 @@ import 'utils.dart'; void main() { group("from a parameter", () { - NodeSassString string; + late NodeSassString string; setUp(() { string = parseValue("abc"); }); diff --git a/test/node_api/value/utils.dart b/test/node_api/value/utils.dart index f69c83da7..01c079a59 100644 --- a/test/node_api/value/utils.dart +++ b/test/node_api/value/utils.dart @@ -14,7 +14,7 @@ import '../utils.dart'; /// Parses [source] by way of a function call. T parseValue(String source) { - T value; + late T value; renderSync(RenderOptions( data: "a {b: foo(($source))}", functions: jsify({ @@ -28,4 +28,4 @@ T parseValue(String source) { /// A matcher that matches values that are JS `instanceof` [type]. Matcher isJSInstanceOf(Object type) => predicate( - (value) => jsInstanceOf(value, type), "to be an instance of $type"); + (value) => jsInstanceOf(value!, type), "to be an instance of $type"); diff --git a/test/node_api_test.dart b/test/node_api_test.dart index f7511a38f..3146dbbd4 100644 --- a/test/node_api_test.dart +++ b/test/node_api_test.dart @@ -20,12 +20,12 @@ import 'node_api/intercept_stdout.dart'; import 'node_api/utils.dart'; import 'utils.dart'; -String sassPath; - void main() { setUpAll(ensureNpmPackage); useSandbox(); + late String sassPath; + setUp(() async { sassPath = p.join(sandbox, 'test.scss'); await writeTextFile(sassPath, 'a {b: c}'); @@ -311,12 +311,11 @@ a { }); test("includes timing information", () { - var result = sass.renderSync(RenderOptions(file: sassPath)); - expect(result.stats.start, const TypeMatcher()); - expect(result.stats.end, const TypeMatcher()); - expect(result.stats.start, lessThanOrEqualTo(result.stats.end)); - expect(result.stats.duration, - equals(result.stats.end - result.stats.start)); + var stats = sass.renderSync(RenderOptions(file: sassPath)).stats; + expect(stats.start, const TypeMatcher()); + expect(stats.end, const TypeMatcher()); + expect(stats.start, lessThanOrEqualTo(stats.end)); + expect(stats.duration, equals(stats.end - stats.start)); }); group("has includedFiles which", () { @@ -351,7 +350,7 @@ a { }); group("the error object", () { - RenderError error; + late RenderError error; group("for a parse error in a file", () { setUp(() async { await writeTextFile(sassPath, "a {b: }"); diff --git a/test/source_map_test.dart b/test/source_map_test.dart index e6e5d22e3..6d1c3a828 100644 --- a/test/source_map_test.dart +++ b/test/source_map_test.dart @@ -683,7 +683,7 @@ void main() { """, sourceMap: (_) {}); }, throwsA(predicate((untypedError) { var error = untypedError as SourceSpanException; - expect(error.span.text, equals(r"$map")); + expect(error.span!.text, equals(r"$map")); return true; }))); }); @@ -710,14 +710,14 @@ void main() { /// /// This also re-indents the input strings with [_reindent]. void _expectSourceMap(String sass, String scss, String css, - {Importer importer, OutputStyle style}) { + {Importer? importer, OutputStyle? style}) { _expectSassSourceMap(sass, css, importer: importer, style: style); _expectScssSourceMap(scss, css, importer: importer, style: style); } /// Like [_expectSourceMap], but with only SCSS source. void _expectScssSourceMap(String scss, String css, - {Importer importer, OutputStyle style}) { + {Importer? importer, OutputStyle? style}) { var scssTuple = _extractLocations(_reindent(scss)); var scssText = scssTuple.item1; var scssLocations = _tuplesToMap(scssTuple.item2); @@ -726,7 +726,7 @@ void _expectScssSourceMap(String scss, String css, var cssText = cssTuple.item1; var cssLocations = cssTuple.item2; - SingleMapping scssMap; + late SingleMapping scssMap; var scssOutput = compileString(scssText, sourceMap: (map) => scssMap = map, importer: importer, style: style); expect(scssOutput, equals(cssText)); @@ -735,7 +735,7 @@ void _expectScssSourceMap(String scss, String css, /// Like [_expectSourceMap], but with only indented source. void _expectSassSourceMap(String sass, String css, - {Importer importer, OutputStyle style}) { + {Importer? importer, OutputStyle? style}) { var sassTuple = _extractLocations(_reindent(sass)); var sassText = sassTuple.item1; var sassLocations = _tuplesToMap(sassTuple.item2); @@ -744,7 +744,7 @@ void _expectSassSourceMap(String sass, String css, var cssText = cssTuple.item1; var cssLocations = cssTuple.item2; - SingleMapping sassMap; + late SingleMapping sassMap; var sassOutput = compileString(sassText, indented: true, sourceMap: (map) => sassMap = map, @@ -830,7 +830,7 @@ void _expectMapMatches( for (var tuple in targetLocations) { var name = tuple.item1; var expectedTarget = tuple.item2; - var expectedSource = sourceLocations[name]; + var expectedSource = sourceLocations[name]!; if (!entryIter.moveNext()) { fail('Missing mapping "$name", expected ' @@ -903,12 +903,14 @@ String _mapToString(SingleMapping map, String sourceText, String targetText) { var entryIter = entries.iterator..moveNext(); while (!targetScanner.isDone) { var entry = entryIter.current; - if (entry != null && - targetScanner.line == entry.target.line && + if (targetScanner.line == entry.target.line && targetScanner.column == entry.target.column) { var name = entryNames[Tuple2(entry.source.line, entry.source.column)]; targetBuffer.write("{{$name}}"); - entryIter.moveNext(); + if (!entryIter.moveNext()) { + targetBuffer.write(targetScanner.rest); + break; + } } targetBuffer.writeCharCode(targetScanner.readChar()); diff --git a/test/utils.dart b/test/utils.dart index fdbe402c3..e4cb40cca 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -23,14 +23,19 @@ Future get tick => /// Loads and decodes the source map embedded as a `data:` URI in [css]. /// /// Throws a [TestFailure] if [css] doesn't have such a source map. -Map embeddedSourceMap(String css) { +Map embeddedSourceMap(String css) { expect(css, matches(_sourceMapCommentRegExp)); - var match = _sourceMapCommentRegExp.firstMatch(css); - var data = Uri.parse(match[1]).data; + var match = _sourceMapCommentRegExp.firstMatch(css)!; + var data = Uri.parse(match[1]!).data!; expect(data.mimeType, equals("application/json")); - return jsonDecode(data.contentAsString()) as Map; + return jsonDecode(data.contentAsString()) as Map; } +/// Returns a function with one argument that fails the test if it's ever +/// called. +Never Function(Object? arg) get expectNever1 => + expectAsync1((_) => throw '', count: 0); + // Like `p.prettyUri()`, but for a non-URL path. String prettyPath(String path) => p.prettyUri(p.toUri(path)); diff --git a/tool/grind.dart b/tool/grind.dart index 547db8941..1162cf0f9 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -30,11 +30,11 @@ void main(List args) { pkg.jsModuleMainLibrary.value = "lib/src/node.dart"; pkg.npmPackageJson.fn = () => json.decode(File("package/package.json").readAsStringSync()) - as Map; + as Map; pkg.npmReadme.fn = () => _readAndResolveMarkdown("package/README.npm.md"); pkg.standaloneName.value = "dart-sass"; - pkg.githubUser.value = Platform.environment["GH_USER"]; - pkg.githubPassword.value = Platform.environment["GH_TOKEN"]; + pkg.githubUser.fn = () => Platform.environment["GH_USER"]!; + pkg.githubPassword.fn = () => Platform.environment["GH_TOKEN"]!; pkg.githubReleaseNotes.fn = () => "To install Sass ${pkg.version}, download one of the packages below " @@ -110,7 +110,7 @@ final _readAndResolveRegExp = RegExp( String _readAndResolveMarkdown(String path) => File(path) .readAsStringSync() .replaceAllMapped(_readAndResolveRegExp, (match) { - String included; + late String included; try { included = File(p.join(p.dirname(path), p.fromUri(match[1]))) .readAsStringSync(); @@ -118,7 +118,7 @@ String _readAndResolveMarkdown(String path) => File(path) _matchError(match, error.toString(), url: p.toUri(path)); } - Match headerMatch; + late Match headerMatch; try { headerMatch = "# ${match[2]}".allMatches(included).first; } on StateError { @@ -141,7 +141,7 @@ String _readAndResolveMarkdown(String path) => File(path) }); /// Throws a nice [SourceSpanException] associated with [match]. -void _matchError(Match match, String message, {Object url}) { +void _matchError(Match match, String message, {Object? url}) { var file = SourceFile.fromString(match.input, url: url); throw SourceSpanException(message, file.span(match.start, match.end)); } diff --git a/tool/grind/benchmark.dart b/tool/grind/benchmark.dart index 3617fddac..e0e416c2e 100644 --- a/tool/grind/benchmark.dart +++ b/tool/grind/benchmark.dart @@ -71,7 +71,7 @@ Future benchmarkGenerate() async { /// it's written after [text]. If the file already exists and is the expected /// length, it's not written. Future _writeNTimes(String path, String text, num times, - {String header, String footer}) async { + {String? header, String? footer}) async { var file = File(path); var expectedLength = (header == null ? 0 : header.length + 1) + (text.length + 1) * times + @@ -183,7 +183,7 @@ I ran five instances of each configuration and recorded the fastest time. buffer.writeln("Running on a file containing $description:"); buffer.writeln(); - Duration sasscTime; + Duration? sasscTime; if (!libsassIncompatible.contains(info[1])) { sasscTime = await _benchmark(p.join(sassc, 'bin', 'sassc'), [path]); buffer.writeln("* sassc: ${_formatTime(sasscTime)}"); @@ -244,12 +244,12 @@ Future _benchmark(String executable, List arguments) async { // chance to warm up at the OS level. await _benchmarkOnce(executable, arguments); - Duration lowest; + Duration? lowest; for (var i = 0; i < 5; i++) { var duration = await _benchmarkOnce(executable, arguments); if (lowest == null || duration < lowest) lowest = duration; } - return lowest; + return lowest!; } Future _benchmarkOnce( @@ -263,10 +263,14 @@ Future _benchmarkOnce( var match = RegExp(r"(\d+)m(\d+)\.(\d+)s").firstMatch(result.stderr as String); + if (match == null) { + fail("Process didn't print the expected format:\n${result.stderr}"); + } + return Duration( - minutes: int.parse(match[1]), - seconds: int.parse(match[2]), - milliseconds: int.parse(match[3])); + minutes: int.parse(match[1]!), + seconds: int.parse(match[2]!), + milliseconds: int.parse(match[3]!)); } String _formatTime(Duration duration) => diff --git a/tool/grind/synchronize.dart b/tool/grind/synchronize.dart index 8b171e830..452e16df4 100644 --- a/tool/grind/synchronize.dart +++ b/tool/grind/synchronize.dart @@ -15,6 +15,8 @@ import 'package:dart_style/dart_style.dart'; import 'package:grinder/grinder.dart'; import 'package:path/path.dart' as p; +import 'package:sass/src/util/nullable.dart'; + /// The files to compile to synchronous versions. final sources = const { 'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart', @@ -166,7 +168,7 @@ class _Visitor extends RecursiveAstVisitor { _write(arguments.first); _buffer.write(".${_synchronizeName(node.methodName.name)}"); - if (node.typeArguments != null) _write(node.typeArguments); + node.typeArguments.andThen(_write); _buffer.write("("); _position = arguments[1].beginToken.offset; @@ -186,15 +188,16 @@ class _Visitor extends RecursiveAstVisitor { void visitTypeName(TypeName node) { if (["Future", "FutureOr"].contains(node.name.name)) { _skip(node.name.beginToken); - if (node.typeArguments != null) { - _skip(node.typeArguments.leftBracket); - node.typeArguments.arguments.first.accept(this); - _skip(node.typeArguments.rightBracket); + var typeArguments = node.typeArguments; + if (typeArguments != null) { + _skip(typeArguments.leftBracket); + typeArguments.arguments.first.accept(this); + _skip(typeArguments.rightBracket); } else { _buffer.write("void"); } } else if (node.name.name == "Module") { - _skip(node.name.beginToken); + _skipNode(node); _buffer.write("Module"); } else { super.visitTypeName(node); @@ -203,7 +206,7 @@ class _Visitor extends RecursiveAstVisitor { /// Writes [_source] to [_buffer] up to the beginning of [token], then puts /// [_position] after [token] so it doesn't get written. - void _skip(Token token) { + void _skip(Token? token) { if (token == null) return; _buffer.write(_source.substring(_position, token.offset)); _position = token.end; @@ -212,7 +215,6 @@ class _Visitor extends RecursiveAstVisitor { /// Writes [_source] to [_buffer] up to the beginning of [node], then puts /// [_position] after [node] so it doesn't get written. void _skipNode(AstNode node) { - if (node == null) return; _writeTo(node); _position = node.endToken.end; }