From 649cf6badc8ef0c13baed95196e18161efc517ed Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Sun, 9 Jun 2019 21:31:06 +0100 Subject: [PATCH 1/2] Add support for "@use with" --- lib/src/ast/sass/statement/use_rule.dart | 38 +++- lib/src/async_environment.dart | 5 +- lib/src/environment.dart | 7 +- lib/src/parse/stylesheet.dart | 81 +++++++-- lib/src/util/limited_map_view.dart | 8 +- lib/src/util/prefixed_map_view.dart | 15 +- lib/src/util/unprefixed_map_view.dart | 59 +++++++ lib/src/visitor/async_evaluate.dart | 214 ++++++++++++++++------ lib/src/visitor/evaluate.dart | 215 +++++++++++++++++------ test/source_map_test.dart | 45 ++++- 10 files changed, 536 insertions(+), 151 deletions(-) create mode 100644 lib/src/util/unprefixed_map_view.dart diff --git a/lib/src/ast/sass/statement/use_rule.dart b/lib/src/ast/sass/statement/use_rule.dart index 97532283b..34ea63d7d 100644 --- a/lib/src/ast/sass/statement/use_rule.dart +++ b/lib/src/ast/sass/statement/use_rule.dart @@ -2,9 +2,14 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'dart:collection'; + import 'package:source_span/source_span.dart'; +import 'package:tuple/tuple.dart'; +import '../../../utils.dart'; import '../../../visitor/interface/statement.dart'; +import '../expression.dart'; import '../expression/string.dart'; import '../statement.dart'; @@ -19,12 +24,39 @@ class UseRule implements Statement { /// can be accessed without a namespace. final String namespace; + /// A map from variable names to their values and the spans for those + /// variables, used to configure the loaded modules. + final Map> configuration; + final FileSpan span; - UseRule(this.url, this.namespace, this.span); + UseRule(this.url, this.namespace, this.span, + {Map> configuration}) + : configuration = configuration == null + ? const {} + : UnmodifiableMapView(normalizedMap(configuration)); T accept(StatementVisitor visitor) => visitor.visitUseRule(this); - String toString() => "@use ${StringExpression.quoteText(url.toString())} as " - "${namespace ?? "*"};"; + String toString() { + var buffer = + StringBuffer("@use ${StringExpression.quoteText(url.toString())}"); + + var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last; + var dot = basename.indexOf("."); + if (namespace != basename.substring(0, dot == -1 ? basename.length : dot)) { + buffer.write(" as ${namespace ?? "*"}"); + } + + if (configuration.isNotEmpty) { + buffer.write(" with ("); + buffer.write(configuration.entries + .map((entry) => "\$${entry.key}: ${entry.value.item1}") + .join(", ")); + buffer.write(")"); + } + + buffer.write(";"); + return buffer.toString(); + } } diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index e1dcfc497..13b609ea4 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -114,6 +114,9 @@ class AsyncEnvironment { UserDefinedCallable get content => _content; UserDefinedCallable _content; + /// Whether the environment is lexically at the top level of a stylesheet. + bool get atRoot => _variables.length == 1; + /// Whether the environment is lexically within a mixin. bool get inMixin => _inMixin; var _inMixin = false; @@ -450,7 +453,7 @@ class AsyncEnvironment { return; } - if (global || _variables.length == 1) { + if (global || atRoot) { // Don't set the index if there's already a variable with the given name, // since local accesses should still return the local variable. _variableIndices.putIfAbsent(name, () { diff --git a/lib/src/environment.dart b/lib/src/environment.dart index fbd48c501..bfeaa2578 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 02d98a8b9466b9a25d2ef91fb803d87921749700 +// Checksum: 06c4fa92b6e49d29ab8992fe494edd1a494ff4af // // ignore_for_file: unused_import @@ -120,6 +120,9 @@ class Environment { UserDefinedCallable get content => _content; UserDefinedCallable _content; + /// Whether the environment is lexically at the top level of a stylesheet. + bool get atRoot => _variables.length == 1; + /// Whether the environment is lexically within a mixin. bool get inMixin => _inMixin; var _inMixin = false; @@ -456,7 +459,7 @@ class Environment { return; } - if (global || _variables.length == 1) { + if (global || atRoot) { // Don't set the index if there's already a variable with the given name, // since local accesses should still return the local variable. _variableIndices.putIfAbsent(name, () { diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 2c61cc808..f4758969f 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -28,7 +28,9 @@ import 'parser.dart'; /// `feature.use` branch. It allows us to avoid having separate development /// tracks as much as possible without shipping `@use` support until we're /// ready. -const _parseUse = true; +/// +/// This should not be referenced outside the Sass package. +const parseUse = true; /// The base class for both the SCSS and indented syntax parsers. /// @@ -885,7 +887,7 @@ abstract class StylesheetParser extends Parser { expectStatementSeparator("@forward rule"); var span = scanner.spanFrom(start); - if (!_parseUse) { + if (!parseUse) { error( "@forward is coming soon, but it's not supported in this version of " "Dart Sass.", @@ -1271,25 +1273,14 @@ relase. For details, see http://bit.ly/moz-document. var url = _urlString(); whitespace(); - String namespace; - if (scanIdentifier("as")) { - whitespace(); - namespace = scanner.scanChar($asterisk) ? null : identifier(); - } else { - var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last; - var dot = basename.indexOf("."); - namespace = basename.substring(0, dot == -1 ? basename.length : dot); + var namespace = _useNamespace(url, start); + whitespace(); + var configuration = _useConfiguration(); - try { - namespace = Parser.parseIdentifier(namespace, logger: logger); - } on SassFormatException { - error('Invalid Sass identifier "$namespace"', scanner.spanFrom(start)); - } - } expectStatementSeparator("@use rule"); var span = scanner.spanFrom(start); - if (!_parseUse) { + if (!parseUse) { error( "@use is coming soon, but it's not supported in this version of " "Dart Sass.", @@ -1299,7 +1290,61 @@ relase. For details, see http://bit.ly/moz-document. } expectStatementSeparator("@use rule"); - return UseRule(url, namespace, span); + return UseRule(url, namespace, span, configuration: configuration); + } + + /// 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) { + if (scanIdentifier("as")) { + whitespace(); + return scanner.scanChar($asterisk) ? null : identifier(); + } + + var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last; + var dot = basename.indexOf("."); + var namespace = basename.substring(0, dot == -1 ? basename.length : dot); + try { + return Parser.parseIdentifier(namespace, logger: logger); + } on SassFormatException { + error('Invalid Sass identifier "$namespace"', scanner.spanFrom(start)); + } + } + + /// Returns the map from variable names to expressions from a `@use` rule's + /// `with` clause. + /// + /// Returns `null` if there is no `with` clause. + Map> _useConfiguration() { + if (!scanIdentifier("with")) return null; + + var configuration = normalizedMap>(); + whitespace(); + scanner.expectChar($lparen); + + while (true) { + whitespace(); + + var variableStart = scanner.state; + var name = variableName(); + whitespace(); + scanner.expectChar($colon); + whitespace(); + var expression = _expressionUntilComma(); + var span = scanner.spanFrom(variableStart); + + if (configuration.containsKey(name)) { + error("The same variable may only be configured once.", span); + } + configuration[name] = Tuple2(expression, span); + + if (!scanner.scanChar($comma)) break; + whitespace(); + if (!_lookingAtExpression()) break; + } + + scanner.expectChar($rparen); + return configuration; } /// Consumes a `@warn` rule. diff --git a/lib/src/util/limited_map_view.dart b/lib/src/util/limited_map_view.dart index c03b13488..1d184c1c9 100644 --- a/lib/src/util/limited_map_view.dart +++ b/lib/src/util/limited_map_view.dart @@ -8,13 +8,17 @@ import 'package:collection/collection.dart'; import '../utils.dart'; -/// An unmodifiable view of a map that only allows certain keys to be accessed. +/// A mostly-unmodifiable view of a map that only allows certain keys to be +/// accessed. /// /// Whether or not the underlying map contains keys that aren't allowed, this /// view will behave as though it doesn't contain them. /// /// The underlying map's values may change independently of this view, but its /// set of keys may not. +/// +/// This is unmodifiable *except for the [remove] method*, which is used for +/// `@used with` to mark configured variables as used. class LimitedMapView extends UnmodifiableMapBase { /// The wrapped map. final Map _map; @@ -42,4 +46,6 @@ class LimitedMapView extends UnmodifiableMapBase { 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; } diff --git a/lib/src/util/prefixed_map_view.dart b/lib/src/util/prefixed_map_view.dart index f19dff301..c0dc8a203 100644 --- a/lib/src/util/prefixed_map_view.dart +++ b/lib/src/util/prefixed_map_view.dart @@ -6,9 +6,6 @@ import 'dart:collection'; /// An unmodifiable view of a map with string keys that allows keys to be /// accessed with an additional prefix. -/// -/// Whether or not the underlying map contains keys that aren't allowed, this -/// view will behave as though it doesn't contain them. class PrefixedMapView extends UnmodifiableMapBase { /// The wrapped map. final Map _map; @@ -33,18 +30,18 @@ class PrefixedMapView extends UnmodifiableMapBase { {bool equals(String string1, String string2)}) : _equals = equals ?? ((string1, string2) => string1 == string2); - V operator [](Object key) => key is String && _startsWith(key, _prefix) + V operator [](Object key) => key is String && _startsWithPrefix(key) ? _map[key.substring(_prefix.length)] : null; - bool containsKey(Object key) => key is String && _startsWith(key, _prefix) + bool containsKey(Object key) => key is String && _startsWithPrefix(key) ? _map.containsKey(key.substring(_prefix.length)) : false; - /// Returns whether [string] begins with [prefix] according to [_equals]. - bool _startsWith(String string, String prefix) => - string.length >= prefix.length && - _equals(string.substring(0, prefix.length), prefix); + /// Returns whether [string] begins with [_prefix] according to [_equals]. + bool _startsWithPrefix(String string) => + string.length >= _prefix.length && + _equals(string.substring(0, _prefix.length), _prefix); } /// The implementation of [PrefixedMapViews.keys]. diff --git a/lib/src/util/unprefixed_map_view.dart b/lib/src/util/unprefixed_map_view.dart new file mode 100644 index 000000000..ce45848a2 --- /dev/null +++ b/lib/src/util/unprefixed_map_view.dart @@ -0,0 +1,59 @@ +// Copyright 2019 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:collection'; + +/// A mostly-unmodifiable view of a map with string keys that only allows keys +/// with a given prefix to be accessed, and presents them as though they didn't +/// have that prefix. +/// +/// Whether or not the underlying map contains keys without the given prefix, +/// this view will behave as though it doesn't contain them. +/// +/// This is unmodifiable *except for the [remove] method*, which is used for +/// `@used with` to mark configured variables as used. +class UnprefixedMapView extends UnmodifiableMapBase { + /// The wrapped map. + final Map _map; + + /// The prefix to remove from the map keys. + final String _prefix; + + /// The equality operation to use for comparing map keys. + final bool Function(String string1, String string2) _equals; + + Iterable get keys => _UnprefixedKeys(this); + + /// Creates a new unprefixed map view. + /// + /// The map's notion of equality must match [equals], and must be stable over + /// substrings (that is, if `T == S`, then for all ranges `i..j`, + /// `T[i..j] == S[i..j]`). + UnprefixedMapView(this._map, this._prefix, + {bool equals(String string1, String string2)}) + : _equals = equals ?? ((string1, string2) => string1 == string2); + + V operator [](Object key) => key is String ? _map[_prefix + key] : null; + + bool containsKey(Object key) => + key is String ? _map.containsKey(_prefix + key) : false; + + 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; + + Iterator get iterator => _view._map.keys + .where((key) => + key.length >= _view._prefix.length && + _view._equals(key.substring(0, _view._prefix.length), _view._prefix)) + .iterator; + + _UnprefixedKeys(this._view); + + bool contains(Object key) => _view.containsKey(key); +} diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 902a92518..f6c4d272c 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -37,6 +37,8 @@ import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; import '../syntax.dart'; import '../util/fixed_length_list_builder.dart'; +import '../util/limited_map_view.dart'; +import '../util/unprefixed_map_view.dart'; import '../utils.dart'; import '../value.dart'; import '../warn.dart'; @@ -238,6 +240,10 @@ class _EvaluateVisitor /// module. Extender _extender; + /// A map from variable names to the values that override their `!default` + /// definitions in this module. + Map _configuration; + _EvaluateVisitor( {AsyncImportCache importCache, NodeImporter nodeImporter, @@ -429,12 +435,24 @@ class _EvaluateVisitor /// Loads the module at [url] and passes it to [callback]. /// + /// The [configuration] overrides values for `!default` variables defined in + /// the module or modules it forwards and/or imports. If it's not passed, the + /// current configuration is used instead. Throws a [SassRuntimeException] if + /// a configured variable is not declared with `!default`. + /// /// The [stackFrame] and [nodeForSpan] are used for the name and location of /// the stack frame in which the new module is executed. Future _loadModule(Uri url, String stackFrame, AstNode nodeForSpan, - void callback(Module module)) async { + void callback(Module module), + {Map configuration}) async { var builtInModule = _builtInModules[url]; if (builtInModule != null) { + if (configuration != null || _configuration != null) { + throw _exception( + "This variable was not declared with !default in the @used module.", + configuration.values.first.configurationSpan); + } + callback(builtInModule); return; } @@ -453,7 +471,8 @@ class _EvaluateVisitor Module module; try { - module = await _execute(importer, stylesheet); + module = + await _execute(importer, stylesheet, configuration: configuration); } finally { _activeModules.remove(canonicalUrl); } @@ -467,62 +486,90 @@ class _EvaluateVisitor } /// Executes [stylesheet], loaded by [importer], to produce a module. - Future _execute(AsyncImporter importer, Stylesheet stylesheet) { + /// + /// The [configuration] overrides values for `!default` variables defined in + /// the module or modules it forwards and/or imports. If it's not passed, the + /// current configuration is used instead. Throws a [SassRuntimeException] if + /// a configured variable is not declared with `!default`. + Future _execute(AsyncImporter importer, Stylesheet stylesheet, + {Map configuration}) async { var url = stylesheet.span.sourceUrl; - return putIfAbsentAsync(_modules, url, () async { - var environment = AsyncEnvironment(sourceMap: _sourceMap); - CssStylesheet css; - var extender = Extender(); - await _withEnvironment(environment, () async { - var oldImporter = _importer; - var oldStylesheet = _stylesheet; - var oldRoot = _root; - var oldParent = _parent; - var oldEndOfImports = _endOfImports; - var oldOutOfOrderImports = _outOfOrderImports; - var oldExtender = _extender; - var oldStyleRule = _styleRule; - var oldMediaQueries = _mediaQueries; - var oldDeclarationName = _declarationName; - var oldInUnknownAtRule = _inUnknownAtRule; - var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; - var oldInKeyframes = _inKeyframes; - _importer = importer; - _stylesheet = stylesheet; - _root = ModifiableCssStylesheet(stylesheet.span); - _parent = _root; - _endOfImports = 0; - _outOfOrderImports = null; - _extender = extender; - _styleRule = null; - _mediaQueries = null; - _declarationName = null; - _inUnknownAtRule = false; - _atRootExcludingStyleRule = false; - _inKeyframes = false; - await visitStylesheet(stylesheet); - css = _outOfOrderImports == null - ? _root - : CssStylesheet(_addOutOfOrderImports(), stylesheet.span); + var alreadyLoaded = _modules[url]; + if (alreadyLoaded != null) { + if (configuration != null || _configuration != null) { + throw _exception( + "This module was already loaded, so it can't be configured using " + "\"with\"."); + } - _importer = oldImporter; - _stylesheet = oldStylesheet; - _root = oldRoot; - _parent = oldParent; - _endOfImports = oldEndOfImports; - _outOfOrderImports = oldOutOfOrderImports; - _extender = oldExtender; - _styleRule = oldStyleRule; - _mediaQueries = oldMediaQueries; - _declarationName = oldDeclarationName; - _inUnknownAtRule = oldInUnknownAtRule; - _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - _inKeyframes = oldInKeyframes; - }); + return alreadyLoaded; + } - return environment.toModule(css, extender); + var environment = AsyncEnvironment(sourceMap: _sourceMap); + CssStylesheet css; + var extender = Extender(); + await _withEnvironment(environment, () async { + var oldImporter = _importer; + var oldStylesheet = _stylesheet; + var oldRoot = _root; + var oldParent = _parent; + var oldEndOfImports = _endOfImports; + var oldOutOfOrderImports = _outOfOrderImports; + var oldExtender = _extender; + var oldStyleRule = _styleRule; + var oldMediaQueries = _mediaQueries; + var oldDeclarationName = _declarationName; + var oldInUnknownAtRule = _inUnknownAtRule; + var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; + var oldInKeyframes = _inKeyframes; + var oldConfiguration = _configuration; + _importer = importer; + _stylesheet = stylesheet; + _root = ModifiableCssStylesheet(stylesheet.span); + _parent = _root; + _endOfImports = 0; + _outOfOrderImports = null; + _extender = extender; + _styleRule = null; + _mediaQueries = null; + _declarationName = null; + _inUnknownAtRule = false; + _atRootExcludingStyleRule = false; + _inKeyframes = false; + + if (configuration != null) _configuration = normalizedMap(configuration); + + await visitStylesheet(stylesheet); + css = _outOfOrderImports == null + ? _root + : CssStylesheet(_addOutOfOrderImports(), stylesheet.span); + + _importer = oldImporter; + _stylesheet = oldStylesheet; + _root = oldRoot; + _parent = oldParent; + _endOfImports = oldEndOfImports; + _outOfOrderImports = oldOutOfOrderImports; + _extender = oldExtender; + _styleRule = oldStyleRule; + _mediaQueries = oldMediaQueries; + _declarationName = oldDeclarationName; + _inUnknownAtRule = oldInUnknownAtRule; + _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; + _inKeyframes = oldInKeyframes; + + if (configuration != null && _configuration.isNotEmpty) { + throw _exception( + "This variable was not declared with !default in the @used module.", + _configuration.values.first.configurationSpan); + } + _configuration = oldConfiguration; }); + + var module = environment.toModule(css, extender); + _modules[url] = module; + return module; } /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted @@ -1054,10 +1101,32 @@ class _EvaluateVisitor } Future visitForwardRule(ForwardRule node) async { + // Only allow variables that are visible through the `@forward` to be + // configured. These views support [Map.remove] so we can mark when a + // configuration variable is used by removing it even when the underlying + // map is wrapped. + var oldConfiguration = _configuration; + if (_configuration != null) { + if (node.prefix != null) { + _configuration = UnprefixedMapView(_configuration, node.prefix, + equals: equalsIgnoreSeparator); + } + + if (node.shownVariables != null) { + _configuration = + LimitedMapView.whitelist(_configuration, node.shownVariables); + } else if (node.hiddenVariables != null && + node.hiddenVariables.isNotEmpty) { + _configuration = + LimitedMapView.blacklist(_configuration, node.hiddenVariables); + } + } + await _loadModule(node.url, "@forward", node, (module) { _environment.forwardModule(module, node); }); + _configuration = oldConfiguration; return null; } @@ -1526,6 +1595,18 @@ class _EvaluateVisitor Future visitVariableDeclaration(VariableDeclaration node) async { if (node.isGuarded) { + if (node.namespace == null && _environment.atRoot) { + var override = _configuration?.remove(node.name); + if (override != null) { + _addExceptionSpan(node, () { + _environment.setVariable( + node.name, override.value, override.assignmentNode, + global: true); + }); + return null; + } + } + var value = _addExceptionSpan(node, () => _environment.getVariable(node.name, namespace: node.namespace)); if (value != null && value != sassNull) return null; @@ -1553,7 +1634,16 @@ class _EvaluateVisitor Future visitUseRule(UseRule node) async { await _loadModule(node.url, "@use", node, (module) { _environment.addModule(module, namespace: node.namespace); - }); + }, + configuration: node.configuration.isEmpty + ? null + : { + for (var entry in node.configuration.entries) + entry.key: _ConfiguredValue( + (await entry.value.item1.accept(this)).withoutSlash(), + entry.value.item2, + _expressionNode(entry.value.item1)) + }); return null; } @@ -2714,3 +2804,19 @@ class _ArgumentResults { _ArgumentResults(this.positional, this.named, this.separator, {this.positionalNodes, this.namedNodes}); } + +/// A variable value that's been configured using `@use ... with`. +class _ConfiguredValue { + /// The value of the variable. + final Value value; + + /// The span where the variable's configuration was written. + final FileSpan configurationSpan; + + /// The [AstNode] where the variable's value originated. + /// + /// This is used to generate source maps. + final AstNode assignmentNode; + + _ConfiguredValue(this.value, this.configurationSpan, [this.assignmentNode]); +} diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 0cfff23db..bc3052d8d 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: 1106d5c41e569e6a152baee453a942441fa3c609 +// Checksum: c8480b485dd00869ad214a82eddb7dc3b40e90b5 // // ignore_for_file: unused_import @@ -46,6 +46,8 @@ import '../module/built_in.dart'; import '../parse/keyframe_selector.dart'; import '../syntax.dart'; import '../util/fixed_length_list_builder.dart'; +import '../util/limited_map_view.dart'; +import '../util/unprefixed_map_view.dart'; import '../utils.dart'; import '../value.dart'; import '../warn.dart'; @@ -246,6 +248,10 @@ class _EvaluateVisitor /// module. Extender _extender; + /// A map from variable names to the values that override their `!default` + /// definitions in this module. + Map _configuration; + _EvaluateVisitor( {ImportCache importCache, NodeImporter nodeImporter, @@ -435,12 +441,24 @@ class _EvaluateVisitor /// Loads the module at [url] and passes it to [callback]. /// + /// The [configuration] overrides values for `!default` variables defined in + /// the module or modules it forwards and/or imports. If it's not passed, the + /// current configuration is used instead. Throws a [SassRuntimeException] if + /// a configured variable is not declared with `!default`. + /// /// The [stackFrame] and [nodeForSpan] are used for the name and location of /// the stack frame in which the new module is executed. void _loadModule(Uri url, String stackFrame, AstNode nodeForSpan, - void callback(Module module)) { + void callback(Module module), + {Map configuration}) { var builtInModule = _builtInModules[url]; if (builtInModule != null) { + if (configuration != null || _configuration != null) { + throw _exception( + "This variable was not declared with !default in the @used module.", + configuration.values.first.configurationSpan); + } + callback(builtInModule); return; } @@ -459,7 +477,7 @@ class _EvaluateVisitor Module module; try { - module = _execute(importer, stylesheet); + module = _execute(importer, stylesheet, configuration: configuration); } finally { _activeModules.remove(canonicalUrl); } @@ -473,62 +491,90 @@ class _EvaluateVisitor } /// Executes [stylesheet], loaded by [importer], to produce a module. - Module _execute(Importer importer, Stylesheet stylesheet) { + /// + /// The [configuration] overrides values for `!default` variables defined in + /// the module or modules it forwards and/or imports. If it's not passed, the + /// current configuration is used instead. Throws a [SassRuntimeException] if + /// a configured variable is not declared with `!default`. + Module _execute(Importer importer, Stylesheet stylesheet, + {Map configuration}) { var url = stylesheet.span.sourceUrl; - return _modules.putIfAbsent(url, () { - var environment = Environment(sourceMap: _sourceMap); - CssStylesheet css; - var extender = Extender(); - _withEnvironment(environment, () { - var oldImporter = _importer; - var oldStylesheet = _stylesheet; - var oldRoot = _root; - var oldParent = _parent; - var oldEndOfImports = _endOfImports; - var oldOutOfOrderImports = _outOfOrderImports; - var oldExtender = _extender; - var oldStyleRule = _styleRule; - var oldMediaQueries = _mediaQueries; - var oldDeclarationName = _declarationName; - var oldInUnknownAtRule = _inUnknownAtRule; - var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; - var oldInKeyframes = _inKeyframes; - _importer = importer; - _stylesheet = stylesheet; - _root = ModifiableCssStylesheet(stylesheet.span); - _parent = _root; - _endOfImports = 0; - _outOfOrderImports = null; - _extender = extender; - _styleRule = null; - _mediaQueries = null; - _declarationName = null; - _inUnknownAtRule = false; - _atRootExcludingStyleRule = false; - _inKeyframes = false; - visitStylesheet(stylesheet); - css = _outOfOrderImports == null - ? _root - : CssStylesheet(_addOutOfOrderImports(), stylesheet.span); + var alreadyLoaded = _modules[url]; + if (alreadyLoaded != null) { + if (configuration != null || _configuration != null) { + throw _exception( + "This module was already loaded, so it can't be configured using " + "\"with\"."); + } - _importer = oldImporter; - _stylesheet = oldStylesheet; - _root = oldRoot; - _parent = oldParent; - _endOfImports = oldEndOfImports; - _outOfOrderImports = oldOutOfOrderImports; - _extender = oldExtender; - _styleRule = oldStyleRule; - _mediaQueries = oldMediaQueries; - _declarationName = oldDeclarationName; - _inUnknownAtRule = oldInUnknownAtRule; - _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; - _inKeyframes = oldInKeyframes; - }); + return alreadyLoaded; + } - return environment.toModule(css, extender); + var environment = Environment(sourceMap: _sourceMap); + CssStylesheet css; + var extender = Extender(); + _withEnvironment(environment, () { + var oldImporter = _importer; + var oldStylesheet = _stylesheet; + var oldRoot = _root; + var oldParent = _parent; + var oldEndOfImports = _endOfImports; + var oldOutOfOrderImports = _outOfOrderImports; + var oldExtender = _extender; + var oldStyleRule = _styleRule; + var oldMediaQueries = _mediaQueries; + var oldDeclarationName = _declarationName; + var oldInUnknownAtRule = _inUnknownAtRule; + var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; + var oldInKeyframes = _inKeyframes; + var oldConfiguration = _configuration; + _importer = importer; + _stylesheet = stylesheet; + _root = ModifiableCssStylesheet(stylesheet.span); + _parent = _root; + _endOfImports = 0; + _outOfOrderImports = null; + _extender = extender; + _styleRule = null; + _mediaQueries = null; + _declarationName = null; + _inUnknownAtRule = false; + _atRootExcludingStyleRule = false; + _inKeyframes = false; + + if (configuration != null) _configuration = normalizedMap(configuration); + + visitStylesheet(stylesheet); + css = _outOfOrderImports == null + ? _root + : CssStylesheet(_addOutOfOrderImports(), stylesheet.span); + + _importer = oldImporter; + _stylesheet = oldStylesheet; + _root = oldRoot; + _parent = oldParent; + _endOfImports = oldEndOfImports; + _outOfOrderImports = oldOutOfOrderImports; + _extender = oldExtender; + _styleRule = oldStyleRule; + _mediaQueries = oldMediaQueries; + _declarationName = oldDeclarationName; + _inUnknownAtRule = oldInUnknownAtRule; + _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; + _inKeyframes = oldInKeyframes; + + if (configuration != null && _configuration.isNotEmpty) { + throw _exception( + "This variable was not declared with !default in the @used module.", + _configuration.values.first.configurationSpan); + } + _configuration = oldConfiguration; }); + + var module = environment.toModule(css, extender); + _modules[url] = module; + return module; } /// Returns a copy of [_root.children] with [_outOfOrderImports] inserted @@ -1055,10 +1101,32 @@ class _EvaluateVisitor } Value visitForwardRule(ForwardRule node) { + // Only allow variables that are visible through the `@forward` to be + // configured. These views support [Map.remove] so we can mark when a + // configuration variable is used by removing it even when the underlying + // map is wrapped. + var oldConfiguration = _configuration; + if (_configuration != null) { + if (node.prefix != null) { + _configuration = UnprefixedMapView(_configuration, node.prefix, + equals: equalsIgnoreSeparator); + } + + if (node.shownVariables != null) { + _configuration = + LimitedMapView.whitelist(_configuration, node.shownVariables); + } else if (node.hiddenVariables != null && + node.hiddenVariables.isNotEmpty) { + _configuration = + LimitedMapView.blacklist(_configuration, node.hiddenVariables); + } + } + _loadModule(node.url, "@forward", node, (module) { _environment.forwardModule(module, node); }); + _configuration = oldConfiguration; return null; } @@ -1520,6 +1588,18 @@ class _EvaluateVisitor Value visitVariableDeclaration(VariableDeclaration node) { if (node.isGuarded) { + if (node.namespace == null && _environment.atRoot) { + var override = _configuration?.remove(node.name); + if (override != null) { + _addExceptionSpan(node, () { + _environment.setVariable( + node.name, override.value, override.assignmentNode, + global: true); + }); + return null; + } + } + var value = _addExceptionSpan(node, () => _environment.getVariable(node.name, namespace: node.namespace)); if (value != null && value != sassNull) return null; @@ -1547,7 +1627,16 @@ class _EvaluateVisitor Value visitUseRule(UseRule node) { _loadModule(node.url, "@use", node, (module) { _environment.addModule(module, namespace: node.namespace); - }); + }, + configuration: node.configuration.isEmpty + ? null + : { + for (var entry in node.configuration.entries) + entry.key: _ConfiguredValue( + entry.value.item1.accept(this).withoutSlash(), + entry.value.item2, + _expressionNode(entry.value.item1)) + }); return null; } @@ -2663,3 +2752,19 @@ class _ArgumentResults { _ArgumentResults(this.positional, this.named, this.separator, {this.positionalNodes, this.namedNodes}); } + +/// A variable value that's been configured using `@use ... with`. +class _ConfiguredValue { + /// The value of the variable. + final Value value; + + /// The span where the variable's configuration was written. + final FileSpan configurationSpan; + + /// The [AstNode] where the variable's value originated. + /// + /// This is used to generate source maps. + final AstNode assignmentNode; + + _ConfiguredValue(this.value, this.configurationSpan, [this.assignmentNode]); +} diff --git a/test/source_map_test.dart b/test/source_map_test.dart index fb698e0f6..44ffde8bd 100644 --- a/test/source_map_test.dart +++ b/test/source_map_test.dart @@ -10,8 +10,11 @@ import 'package:test/test.dart'; import 'package:tuple/tuple.dart'; import 'package:sass/sass.dart'; +import 'package:sass/src/parse/stylesheet.dart'; import 'package:sass/src/utils.dart'; +import 'dart_api/test_importer.dart'; + main() { group("maps source to target for", () { group("a style rule", () { @@ -543,6 +546,27 @@ main() { """); }); + if (parseUse) { + test("a @use rule with a with clause", () { + _expectScssSourceMap(r""" + $var1: {{1}}new value; + @use 'other' with ($var2: $var1); + + {{2}}a { + {{3}}b: $other.var2; + } + """, """ + {{2}}a { + {{3}}b: {{1}}new value; + } + """, + importer: TestImporter( + (url) => Uri.parse("u:$url"), + (_) => ImporterResult(r"$var2: default value !default;", + syntax: Syntax.scss))); + }); + } + group("a mixin argument that is", () { test("the default value", () { _expectScssSourceMap(r""" @@ -689,13 +713,14 @@ main() { /// /// This also re-indents the input strings with [_reindent]. void _expectSourceMap(String sass, String scss, String css, - {OutputStyle style}) { - _expectSassSourceMap(sass, css, style: style); - _expectScssSourceMap(scss, css, style: 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, {OutputStyle style}) { +void _expectScssSourceMap(String scss, String css, + {Importer importer, OutputStyle style}) { var scssTuple = _extractLocations(_reindent(scss)); var scssText = scssTuple.item1; var scssLocations = _tuplesToMap(scssTuple.item2); @@ -705,14 +730,15 @@ void _expectScssSourceMap(String scss, String css, {OutputStyle style}) { var cssLocations = cssTuple.item2; SingleMapping scssMap; - var scssOutput = - compileString(scssText, sourceMap: (map) => scssMap = map, style: style); + var scssOutput = compileString(scssText, + sourceMap: (map) => scssMap = map, importer: importer, style: style); expect(scssOutput, equals(cssText)); _expectMapMatches(scssMap, scssText, cssText, scssLocations, cssLocations); } /// Like [_expectSourceMap], but with only indented source. -void _expectSassSourceMap(String sass, String css, {OutputStyle style}) { +void _expectSassSourceMap(String sass, String css, + {Importer importer, OutputStyle style}) { var sassTuple = _extractLocations(_reindent(sass)); var sassText = sassTuple.item1; var sassLocations = _tuplesToMap(sassTuple.item2); @@ -723,7 +749,10 @@ void _expectSassSourceMap(String sass, String css, {OutputStyle style}) { SingleMapping sassMap; var sassOutput = compileString(sassText, - indented: true, sourceMap: (map) => sassMap = map, style: style); + indented: true, + sourceMap: (map) => sassMap = map, + importer: importer, + style: style); expect(sassOutput, equals(cssText)); _expectMapMatches(sassMap, sassText, cssText, sassLocations, cssLocations); } From c95b2e0d3af36a601cb22b71e9bcd437e8789b96 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 26 Jun 2019 14:21:44 -0700 Subject: [PATCH 2/2] Make the error message for configuring a built-in module clearer --- lib/src/visitor/async_evaluate.dart | 3 +-- lib/src/visitor/evaluate.dart | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index f6c4d272c..6016b6aa5 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -449,8 +449,7 @@ class _EvaluateVisitor if (builtInModule != null) { if (configuration != null || _configuration != null) { throw _exception( - "This variable was not declared with !default in the @used module.", - configuration.values.first.configurationSpan); + "Built-in modules can't be configured.", nodeForSpan.span); } callback(builtInModule); diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index bc3052d8d..7729fcdd4 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: c8480b485dd00869ad214a82eddb7dc3b40e90b5 +// Checksum: 08fe4369e45ce8e6db2ab909bb66ec8373a0799e // // ignore_for_file: unused_import @@ -455,8 +455,7 @@ class _EvaluateVisitor if (builtInModule != null) { if (configuration != null || _configuration != null) { throw _exception( - "This variable was not declared with !default in the @used module.", - configuration.values.first.configurationSpan); + "Built-in modules can't be configured.", nodeForSpan.span); } callback(builtInModule);