Support configuring modules through imports (#885)

Fixes #882

See sass/sass-spec#1497
This commit is contained in:
Jennifer Thakar 2019-11-26 09:46:51 -08:00 committed by GitHub
parent 15be59be31
commit 8270dc1664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 219 additions and 147 deletions

View File

@ -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

View File

@ -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 = <String, ConfiguredValue>{};
for (var i = 0; i < _variables.length; i++) {
var values = _variables[i];
var nodes =
_variableNodes == null ? <String, AstNode>{} : _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].

View File

@ -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<String, ConfiguredValue> get values => UnmodifiableMapView(_values);
final Map<String, ConfiguredValue> _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<String, ConfiguredValue> 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);
}

View File

@ -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]);
}

View File

@ -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 = <String, ConfiguredValue>{};
for (var i = 0; i < _variables.length; i++) {
var values = _variables[i];
var nodes =
_variableNodes == null ? <String, AstNode>{} : _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].

View File

@ -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 <String, _ConfiguredValue>{};
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 <String, _ConfiguredValue>{};
var configuration = const Configuration.empty();
if (withMap != null) {
configuration = {};
var values = <String, ConfiguredValue>{};
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<void> _loadModule(Uri url, String stackFrame, AstNode nodeForSpan,
void callback(Module module),
{Uri baseUrl,
Map<String, _ConfiguredValue> 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<Module> _execute(AsyncImporter importer, Stylesheet stylesheet,
{Map<String, _ConfiguredValue> 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<Value> 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<Value> 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]);
}

View File

@ -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 <String, _ConfiguredValue>{};
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 <String, _ConfiguredValue>{};
var configuration = const Configuration.empty();
if (withMap != null) {
configuration = {};
var values = <String, ConfiguredValue>{};
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<Callable> module),
{Uri baseUrl,
Map<String, _ConfiguredValue> 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<Callable> _execute(Importer importer, Stylesheet stylesheet,
{Map<String, _ConfiguredValue> 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]);
}