diff --git a/CHANGELOG.md b/CHANGELOG.md index 10566e5d..46559a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 1.24.0 +* Support configuring modules through `@import` rules. + +## 1.23.8 + * **Potentially breaking bug fix:** Members loaded through a nested `@import` are no longer ever accessible outside that nested context. @@ -14,7 +18,7 @@ ## 1.23.7 -* No user-visible changes. +* No user-visible changes ## 1.23.6 diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index 1e1a3cbe..6c13a89d 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -13,6 +13,8 @@ import 'ast/css.dart'; import 'ast/node.dart'; import 'ast/sass.dart'; import 'callable.dart'; +import 'configuration.dart'; +import 'configured_value.dart'; import 'exception.dart'; import 'extend/extender.dart'; import 'module.dart'; @@ -730,6 +732,23 @@ class AsyncEnvironment { } } + /// Creates an implicit configuration from the variables declared in this + /// environment. + Configuration toImplicitConfiguration() { + 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) { + // 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]); + } + } + return Configuration(configuration, isImplicit: true); + } + /// 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]. diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart new file mode 100644 index 00000000..a882cb75 --- /dev/null +++ b/lib/src/configuration.dart @@ -0,0 +1,77 @@ +// Copyright 2019 Google LLC. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:collection'; + +import 'ast/sass.dart'; +import 'configured_value.dart'; +import 'util/limited_map_view.dart'; +import 'util/unprefixed_map_view.dart'; + +/// A set of variables meant to configure a module by overriding its +/// `!default` declarations. +class Configuration { + /// A map from variable names (without `$`) to values. + /// + /// This map may not be modified directly. To remove a value from this + /// configuration, use the [remove] method. + Map get values => UnmodifiableMapView(_values); + final Map _values; + + /// 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; + + Configuration(Map values, {this.isImplicit = false}) + : _values = 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 {}, + isImplicit = true; + + 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); + + /// Creates a new configuration from this one based on a `@forward` rule. + Configuration throughForward(ForwardRule forward) { + if (isEmpty) return const Configuration.empty(); + var newValues = _values; + + // 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. + if (forward.prefix != null) { + newValues = UnprefixedMapView(newValues, forward.prefix); + } + if (forward.shownVariables != null) { + newValues = LimitedMapView.whitelist(newValues, forward.shownVariables); + } else if (forward.hiddenVariables?.isNotEmpty ?? false) { + newValues = LimitedMapView.blacklist(newValues, forward.hiddenVariables); + } + return Configuration(newValues, isImplicit: isImplicit); + } + + /// Creates a copy of this configuration. + Configuration clone() => isEmpty + ? const Configuration.empty() + : Configuration({...values}, isImplicit: isImplicit); +} diff --git a/lib/src/configured_value.dart b/lib/src/configured_value.dart new file mode 100644 index 00000000..beeb7619 --- /dev/null +++ b/lib/src/configured_value.dart @@ -0,0 +1,25 @@ +// Copyright 2019 Google LLC. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'package:source_span/source_span.dart'; + +import 'ast/node.dart'; +import 'value.dart'; + +/// A variable value that's been configured for a [Configuration]. +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 and can be `null` if source map + /// generation is disabled. + final AstNode assignmentNode; + + ConfiguredValue(this.value, this.configurationSpan, [this.assignmentNode]); +} diff --git a/lib/src/environment.dart b/lib/src/environment.dart index a1575f2b..9c5e43f1 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: 0459cbe5c439f3d45f24b739ed1f36b517d338b8 +// Checksum: 7da67a8956ec74db270764e941b674dcc315a488 // // ignore_for_file: unused_import @@ -19,6 +19,8 @@ import 'ast/css.dart'; import 'ast/node.dart'; import 'ast/sass.dart'; import 'callable.dart'; +import 'configuration.dart'; +import 'configured_value.dart'; import 'exception.dart'; import 'extend/extender.dart'; import 'module.dart'; @@ -734,6 +736,23 @@ class Environment { } } + /// Creates an implicit configuration from the variables declared in this + /// environment. + Configuration toImplicitConfiguration() { + 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) { + // 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]); + } + } + return Configuration(configuration, isImplicit: true); + } + /// 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]. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 93fb404a..9fad2c86 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -22,6 +22,8 @@ import '../async_environment.dart'; import '../async_import_cache.dart'; import '../callable.dart'; import '../color_names.dart'; +import '../configuration.dart'; +import '../configured_value.dart'; import '../exception.dart'; import '../extend/extender.dart'; import '../extend/extension.dart'; @@ -37,8 +39,6 @@ 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'; @@ -251,13 +251,10 @@ class _EvaluateVisitor /// module. Extender _extender; - /// A map from variable names to the values that override their `!default` - /// definitions in this module. + /// The configuration for the current module. /// /// If this is empty, that indicates that the current module is not confiured. - /// Note that it may be unmodifiable when empty, in which case [Map.remove] - /// must not be called. - var _configuration = const {}; + var _configuration = const Configuration.empty(); /// Creates a new visitor. /// @@ -413,19 +410,20 @@ class _EvaluateVisitor var url = Uri.parse(arguments[0].assertString("module").text); var withMap = arguments[1].realNull?.assertMap("with")?.contents; - var configuration = const {}; + var configuration = const Configuration.empty(); if (withMap != null) { - configuration = {}; + var values = {}; var span = _callableNode.span; withMap.forEach((variable, value) { var name = variable.assertString("with key").text.replaceAll("_", "-"); - if (configuration.containsKey(name)) { + if (values.containsKey(name)) { throw "The variable \$$name was configured twice."; } - configuration[name] = _ConfiguredValue(value, span); + values[name] = ConfiguredValue(value, span); }); + configuration = Configuration(values); } await _loadModule(url, "load-css()", _callableNode, @@ -523,11 +521,11 @@ class _EvaluateVisitor Future _loadModule(Uri url, String stackFrame, AstNode nodeForSpan, void callback(Module module), {Uri baseUrl, - Map configuration, + Configuration configuration, bool namesInErrors = false}) async { var builtInModule = _builtInModules[url]; if (builtInModule != null) { - if (configuration != null && configuration.isNotEmpty) { + if (configuration != null && !configuration.isImplicit) { throw _exception( namesInErrors ? "Built-in module $url can't be configured." @@ -576,22 +574,20 @@ class _EvaluateVisitor /// Executes [stylesheet], loaded by [importer], to produce a module. /// - /// 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`. + /// If [configuration] is not passed, the current configuration is used + /// instead. Throws a [SassRuntimeException] if a configured variable is not + /// declared with `!default`. /// /// If [namesInErrors] is `true`, this includes the names of modules or /// configured variables 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, - {Map configuration, - bool namesInErrors = false}) async { + {Configuration configuration, bool namesInErrors = false}) async { var url = stylesheet.span.sourceUrl; var alreadyLoaded = _modules[url]; if (alreadyLoaded != null) { - if ((configuration ?? _configuration).isNotEmpty) { + if (!(configuration ?? _configuration).isImplicit) { throw _exception(namesInErrors ? "${p.prettyUri(url)} was already loaded, so it can't be " "configured using \"with\"." @@ -634,10 +630,7 @@ class _EvaluateVisitor _atRootExcludingStyleRule = false; _inKeyframes = false; - if (configuration != null) { - _configuration = - configuration.isEmpty ? const {} : Map.of(configuration); - } + if (configuration != null) _configuration = configuration.clone(); await visitStylesheet(stylesheet); css = _outOfOrderImports == null @@ -658,14 +651,16 @@ class _EvaluateVisitor _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; _inKeyframes = oldInKeyframes; - if (configuration != null && _configuration.isNotEmpty) { + if (configuration != null && + !_configuration.isEmpty && + !_configuration.isImplicit) { throw _exception( namesInErrors - ? "\$${_configuration.keys.first} was not declared with " + ? "\$${_configuration.values.keys.first} was not declared with " "!default in the @used module." : "This variable was not declared with !default in the @used " "module.", - _configuration.values.first.configurationSpan); + _configuration.values.values.first.configurationSpan); } _configuration = oldConfiguration; }); @@ -1203,25 +1198,8 @@ 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.isNotEmpty) { - if (node.prefix != null) { - _configuration = UnprefixedMapView(_configuration, node.prefix); - } - - if (node.shownVariables != null) { - _configuration = - LimitedMapView.whitelist(_configuration, node.shownVariables); - } else if (node.hiddenVariables != null && - node.hiddenVariables.isNotEmpty) { - _configuration = - LimitedMapView.blacklist(_configuration, node.hiddenVariables); - } - } + _configuration = _configuration.throughForward(node); await _loadModule(node.url, "@forward", node, (module) { _environment.forwardModule(module, node); @@ -1303,6 +1281,7 @@ class _EvaluateVisitor var oldParent = _parent; var oldEndOfImports = _endOfImports; var oldOutOfOrderImports = _outOfOrderImports; + var oldConfiguration = _configuration; _importer = importer; _stylesheet = stylesheet; _root = ModifiableCssStylesheet(stylesheet.span); @@ -1310,6 +1289,12 @@ class _EvaluateVisitor _endOfImports = 0; _outOfOrderImports = null; + // This configuration is only used if it passes through a `@forward` + // rule, so we avoid creating unnecessary ones for performance reasons. + if (stylesheet.forwards.isNotEmpty) { + _configuration = environment.toImplicitConfiguration(); + } + await visitStylesheet(stylesheet); children = _addOutOfOrderImports(); @@ -1319,6 +1304,7 @@ class _EvaluateVisitor _parent = oldParent; _endOfImports = oldEndOfImports; _outOfOrderImports = oldOutOfOrderImports; + _configuration = oldConfiguration; }); // Create a dummy module with empty CSS and no extensions to make forwarded @@ -1726,12 +1712,7 @@ class _EvaluateVisitor Future visitVariableDeclaration(VariableDeclaration node) async { if (node.isGuarded) { if (node.namespace == null && _environment.atRoot) { - // Explicitly check whether [_configuration] is empty because if it is, - // it may be a constant map which doesn't support `remove()`. - // - // See also dart-lang/sdk#38540. - var override = - _configuration.isEmpty ? null : _configuration.remove(node.name); + var override = _configuration.remove(node.name); if (override != null) { _addExceptionSpan(node, () { _environment.setVariable( @@ -1777,14 +1758,14 @@ class _EvaluateVisitor _environment.addModule(module, namespace: node.namespace); }, configuration: node.configuration.isEmpty - ? const {} - : { + ? const Configuration.empty() + : Configuration({ for (var entry in node.configuration.entries) - entry.key: _ConfiguredValue( + entry.key: ConfiguredValue( (await entry.value.item1.accept(this)).withoutSlash(), entry.value.item2, _expressionNode(entry.value.item1)) - }); + })); return null; } @@ -2982,19 +2963,3 @@ 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 edb3595a..c9e912ee 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: 8b0be6a2009429b4a3a915f5ad5850e7891dd94d +// Checksum: 63ce60ba47ac04b49e0a3edbe9038fb13b037e64 // // ignore_for_file: unused_import @@ -31,6 +31,8 @@ import '../environment.dart'; import '../import_cache.dart'; import '../callable.dart'; import '../color_names.dart'; +import '../configuration.dart'; +import '../configured_value.dart'; import '../exception.dart'; import '../extend/extender.dart'; import '../extend/extension.dart'; @@ -46,8 +48,6 @@ 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'; @@ -259,13 +259,10 @@ class _EvaluateVisitor /// module. Extender _extender; - /// A map from variable names to the values that override their `!default` - /// definitions in this module. + /// The configuration for the current module. /// /// If this is empty, that indicates that the current module is not confiured. - /// Note that it may be unmodifiable when empty, in which case [Map.remove] - /// must not be called. - var _configuration = const {}; + var _configuration = const Configuration.empty(); /// Creates a new visitor. /// @@ -419,19 +416,20 @@ class _EvaluateVisitor var url = Uri.parse(arguments[0].assertString("module").text); var withMap = arguments[1].realNull?.assertMap("with")?.contents; - var configuration = const {}; + var configuration = const Configuration.empty(); if (withMap != null) { - configuration = {}; + var values = {}; var span = _callableNode.span; withMap.forEach((variable, value) { var name = variable.assertString("with key").text.replaceAll("_", "-"); - if (configuration.containsKey(name)) { + if (values.containsKey(name)) { throw "The variable \$$name was configured twice."; } - configuration[name] = _ConfiguredValue(value, span); + values[name] = ConfiguredValue(value, span); }); + configuration = Configuration(values); } _loadModule(url, "load-css()", _callableNode, @@ -528,12 +526,10 @@ class _EvaluateVisitor /// the stack frame for the duration of the [callback]. void _loadModule(Uri url, String stackFrame, AstNode nodeForSpan, void callback(Module module), - {Uri baseUrl, - Map configuration, - bool namesInErrors = false}) { + {Uri baseUrl, Configuration configuration, bool namesInErrors = false}) { var builtInModule = _builtInModules[url]; if (builtInModule != null) { - if (configuration != null && configuration.isNotEmpty) { + if (configuration != null && !configuration.isImplicit) { throw _exception( namesInErrors ? "Built-in module $url can't be configured." @@ -582,22 +578,20 @@ class _EvaluateVisitor /// Executes [stylesheet], loaded by [importer], to produce a module. /// - /// 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`. + /// If [configuration] is not passed, the current configuration is used + /// instead. Throws a [SassRuntimeException] if a configured variable is not + /// declared with `!default`. /// /// If [namesInErrors] is `true`, this includes the names of modules or /// configured variables 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, - {Map configuration, - bool namesInErrors = false}) { + {Configuration configuration, bool namesInErrors = false}) { var url = stylesheet.span.sourceUrl; var alreadyLoaded = _modules[url]; if (alreadyLoaded != null) { - if ((configuration ?? _configuration).isNotEmpty) { + if (!(configuration ?? _configuration).isImplicit) { throw _exception(namesInErrors ? "${p.prettyUri(url)} was already loaded, so it can't be " "configured using \"with\"." @@ -640,10 +634,7 @@ class _EvaluateVisitor _atRootExcludingStyleRule = false; _inKeyframes = false; - if (configuration != null) { - _configuration = - configuration.isEmpty ? const {} : Map.of(configuration); - } + if (configuration != null) _configuration = configuration.clone(); visitStylesheet(stylesheet); css = _outOfOrderImports == null @@ -664,14 +655,16 @@ class _EvaluateVisitor _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; _inKeyframes = oldInKeyframes; - if (configuration != null && _configuration.isNotEmpty) { + if (configuration != null && + !_configuration.isEmpty && + !_configuration.isImplicit) { throw _exception( namesInErrors - ? "\$${_configuration.keys.first} was not declared with " + ? "\$${_configuration.values.keys.first} was not declared with " "!default in the @used module." : "This variable was not declared with !default in the @used " "module.", - _configuration.values.first.configurationSpan); + _configuration.values.values.first.configurationSpan); } _configuration = oldConfiguration; }); @@ -1204,25 +1197,8 @@ 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.isNotEmpty) { - if (node.prefix != null) { - _configuration = UnprefixedMapView(_configuration, node.prefix); - } - - if (node.shownVariables != null) { - _configuration = - LimitedMapView.whitelist(_configuration, node.shownVariables); - } else if (node.hiddenVariables != null && - node.hiddenVariables.isNotEmpty) { - _configuration = - LimitedMapView.blacklist(_configuration, node.hiddenVariables); - } - } + _configuration = _configuration.throughForward(node); _loadModule(node.url, "@forward", node, (module) { _environment.forwardModule(module, node); @@ -1304,6 +1280,7 @@ class _EvaluateVisitor var oldParent = _parent; var oldEndOfImports = _endOfImports; var oldOutOfOrderImports = _outOfOrderImports; + var oldConfiguration = _configuration; _importer = importer; _stylesheet = stylesheet; _root = ModifiableCssStylesheet(stylesheet.span); @@ -1311,6 +1288,12 @@ class _EvaluateVisitor _endOfImports = 0; _outOfOrderImports = null; + // This configuration is only used if it passes through a `@forward` + // rule, so we avoid creating unnecessary ones for performance reasons. + if (stylesheet.forwards.isNotEmpty) { + _configuration = environment.toImplicitConfiguration(); + } + visitStylesheet(stylesheet); children = _addOutOfOrderImports(); @@ -1320,6 +1303,7 @@ class _EvaluateVisitor _parent = oldParent; _endOfImports = oldEndOfImports; _outOfOrderImports = oldOutOfOrderImports; + _configuration = oldConfiguration; }); // Create a dummy module with empty CSS and no extensions to make forwarded @@ -1720,12 +1704,7 @@ class _EvaluateVisitor Value visitVariableDeclaration(VariableDeclaration node) { if (node.isGuarded) { if (node.namespace == null && _environment.atRoot) { - // Explicitly check whether [_configuration] is empty because if it is, - // it may be a constant map which doesn't support `remove()`. - // - // See also dart-lang/sdk#38540. - var override = - _configuration.isEmpty ? null : _configuration.remove(node.name); + var override = _configuration.remove(node.name); if (override != null) { _addExceptionSpan(node, () { _environment.setVariable( @@ -1771,14 +1750,14 @@ class _EvaluateVisitor _environment.addModule(module, namespace: node.namespace); }, configuration: node.configuration.isEmpty - ? const {} - : { + ? const Configuration.empty() + : Configuration({ for (var entry in node.configuration.entries) - entry.key: _ConfiguredValue( + entry.key: ConfiguredValue( entry.value.item1.accept(this).withoutSlash(), entry.value.item2, _expressionNode(entry.value.item1)) - }); + })); return null; } @@ -2931,19 +2910,3 @@ 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]); -}