From 397c5f9f2b18712000298b866f0aebc66cc60468 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 22 May 2019 19:02:21 -0700 Subject: [PATCH] Add support for @forward See sass/sass#1094 See #554 --- lib/src/ast/sass.dart | 1 + lib/src/ast/sass/statement/forward_rule.dart | 126 +++++++++++ lib/src/async_environment.dart | 210 ++++++++++++++++-- lib/src/environment.dart | 213 +++++++++++++++++-- lib/src/module/forwarded_view.dart | 96 +++++++++ lib/src/parse/parser.dart | 11 + lib/src/parse/stylesheet.dart | 100 ++++++++- lib/src/utils.dart | 19 ++ lib/src/visitor/async_evaluate.dart | 20 +- lib/src/visitor/evaluate.dart | 22 +- lib/src/visitor/interface/statement.dart | 1 + lib/src/visitor/recursive_statement.dart | 2 + 12 files changed, 766 insertions(+), 55 deletions(-) create mode 100644 lib/src/ast/sass/statement/forward_rule.dart create mode 100644 lib/src/module/forwarded_view.dart diff --git a/lib/src/ast/sass.dart b/lib/src/ast/sass.dart index 4e85726e3..a8940af85 100644 --- a/lib/src/ast/sass.dart +++ b/lib/src/ast/sass.dart @@ -40,6 +40,7 @@ export 'sass/statement/each_rule.dart'; export 'sass/statement/error_rule.dart'; export 'sass/statement/extend_rule.dart'; export 'sass/statement/for_rule.dart'; +export 'sass/statement/forward_rule.dart'; export 'sass/statement/function_rule.dart'; export 'sass/statement/if_rule.dart'; export 'sass/statement/import_rule.dart'; diff --git a/lib/src/ast/sass/statement/forward_rule.dart b/lib/src/ast/sass/statement/forward_rule.dart new file mode 100644 index 000000000..728397036 --- /dev/null +++ b/lib/src/ast/sass/statement/forward_rule.dart @@ -0,0 +1,126 @@ +// 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 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; + +import '../../../utils.dart'; +import '../../../visitor/interface/statement.dart'; +import '../expression/string.dart'; +import '../statement.dart'; + +/// A `@forward` rule. +class ForwardRule implements Statement { + /// The URI of the module to forward. + /// + /// If this is relative, it's relative to the containing file. + final Uri url; + + /// The set of mixin and function names that may be accessed from the + /// forwarded module. + /// + /// If this is empty, no mixins or functions may be accessed. If it's `null`, + /// it imposes no restrictions on which mixins and function may be accessed. + /// + /// 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; + + /// The set of variable names (without `$`) that may be accessed from the + /// forwarded module. + /// + /// If this is empty, no variables may be accessed. If it's `null`, it imposes + /// no restrictions on which variables may be accessed. + /// + /// 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; + + /// The set of mixin and function names that may not be accessed from the + /// forwarded module. + /// + /// If this is empty, any mixins or functions may be accessed. If it's `null`, + /// it imposes no restrictions on which mixins or functions may be accessed. + /// + /// 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; + + /// The set of variable names (without `$`) that may be accessed from the + /// forwarded module. + /// + /// If this is empty, any variables may be accessed. If it's `null`, it + /// imposes no restrictions on which variables may be accessed. + /// + /// If this is non-`null`, [shownMixinsAndFunctions] and [shownVariables] are + /// guaranteed to both be `null` and [shownMixinsAndFunctions] is guaranteed + /// to be non-`null`. + 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 FileSpan span; + + /// Creates a `@forward` rule that allows all members to be accessed. + ForwardRule(this.url, this.span, {this.prefix}) + : shownMixinsAndFunctions = null, + shownVariables = null, + hiddenMixinsAndFunctions = null, + hiddenVariables = null; + + /// Creates a `@forward` rule that allows only members included in + /// [shownMixinsAndFunctions] and [shownVariables] to be accessed. + ForwardRule.show(this.url, Iterable shownMixinsAndFunctions, + Iterable shownVariables, this.span, + {this.prefix}) + : shownMixinsAndFunctions = + UnmodifiableSetView(normalizedSet(shownMixinsAndFunctions)), + shownVariables = UnmodifiableSetView(normalizedSet(shownVariables)), + hiddenMixinsAndFunctions = null, + hiddenVariables = null; + + /// Creates a `@forward` rule that allows only members not included in + /// [hiddenMixinsAndFunctions] and [hiddenVariables] to be accessed. + ForwardRule.hide(this.url, Iterable hiddenMixinsAndFunctions, + Iterable hiddenVariables, this.span, + {this.prefix}) + : shownMixinsAndFunctions = null, + shownVariables = null, + hiddenMixinsAndFunctions = + UnmodifiableSetView(normalizedSet(hiddenMixinsAndFunctions)), + hiddenVariables = UnmodifiableSetView(normalizedSet(hiddenVariables)); + + T accept(StatementVisitor visitor) => visitor.visitForwardRule(this); + + String toString() { + var buffer = + StringBuffer("@forward ${StringExpression.quoteText(url.toString())}"); + + if (shownMixinsAndFunctions != null) { + buffer + ..write(" show ") + ..write(_memberList(shownMixinsAndFunctions, shownVariables)); + } else if (hiddenMixinsAndFunctions != null) { + buffer + ..write(" hide ") + ..write(_memberList(hiddenMixinsAndFunctions, hiddenVariables)); + } + + if (prefix != null) buffer.write(" as $prefix*"); + buffer.write(";"); + return buffer.toString(); + } + + /// Returns a combined list of names of the given members. + String _memberList( + Iterable mixinsAndFunctions, Iterable variables) => + shownMixinsAndFunctions + .followedBy(shownVariables.map((name) => "\$$name")) + .join(", "); +} diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index f83da37fd..39733b657 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -4,15 +4,19 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'ast/css.dart'; import 'ast/node.dart'; +import 'ast/sass.dart'; import 'callable.dart'; import 'exception.dart'; import 'extend/extender.dart'; import 'module.dart'; +import 'module/forwarded_view.dart'; +import 'util/merged_map_view.dart'; import 'util/public_member_map_view.dart'; import 'utils.dart'; import 'value.dart'; @@ -31,8 +35,13 @@ class AsyncEnvironment { /// This is `null` if there are no namespaceless modules. Set _globalModules; - /// Modules from both [_modules] and [_global], in the order in which they - /// were `@use`d. + /// The modules forwarded by this module. + /// + /// This is `null` if there are no forwarded modules. + List _forwardedModules; + + /// Modules from [_modules], [_globalModules], and [_forwardedModules], in the + /// order in which they were `@use`d. final List _allModules; /// A list of variables defined at each lexical scope level. @@ -128,6 +137,7 @@ class AsyncEnvironment { AsyncEnvironment({bool sourceMap = false}) : _modules = {}, _globalModules = null, + _forwardedModules = null, _allModules = [], _variables = [normalizedMap()], _variableNodes = sourceMap ? [normalizedMap()] : null, @@ -140,6 +150,7 @@ class AsyncEnvironment { AsyncEnvironment._( this._modules, this._globalModules, + this._forwardedModules, this._allModules, this._variables, this._variableNodes, @@ -162,6 +173,7 @@ class AsyncEnvironment { AsyncEnvironment closure() => AsyncEnvironment._( _modules, _globalModules, + _forwardedModules, _allModules, _variables.toList(), _variableNodes?.toList(), @@ -176,6 +188,7 @@ class AsyncEnvironment { AsyncEnvironment global() => AsyncEnvironment._( {}, null, + null, [], _variables.toList(), _variableNodes?.toList(), @@ -215,6 +228,82 @@ 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 ??= []; + + var view = ForwardedModuleView(module, rule); + for (var other in _forwardedModules) { + _assertNoConflicts(view.variables, other.variables, "variable", other); + _assertNoConflicts(view.functions, other.functions, "function", other); + _assertNoConflicts(view.mixins, other.mixins, "mixin", other); + } + + // Add the original module to [_allModules] (rather than the + // [ForwardedModuleView]) so that we can de-duplicate upstream modules using + // `==`. This is safe because upstream modules are only used for collating + // CSS, not for the members they expose. + _allModules.add(module); + _forwardedModules.add(view); + } + + /// Throws a [SassScriptException] if [newMembers] has any keys that overlap + /// with [oldMembers]. + /// + /// The [type] and [oldModule] is used for error reporting. + void _assertNoConflicts(Map newMembers, + Map oldMembers, String type, Module oldModule) { + Map smaller; + Map larger; + if (newMembers.length < oldMembers.length) { + smaller = newMembers; + larger = oldMembers; + } else { + smaller = oldMembers; + larger = newMembers; + } + + for (var name in smaller.keys) { + if (larger.containsKey(name)) { + if (type == "variable") name = "\$$name"; + throw SassScriptException( + 'Module ${p.prettyUri(oldModule.url)} and the new module both ' + 'forward a $type named $name.'); + } + } + } + + /// Makes the members forwarded by [module] available in the current + /// environment. + /// + /// This is called when [module] is `@import`ed. + void importForwards(Module module) { + if (module is _EnvironmentModule) { + for (var forwarded + in module._environment._forwardedModules ?? const []) { + _globalModules ??= {}; + _globalModules.add(forwarded); + + // Remove existing definitions that the forwarded members are now + // shadowing. + for (var variable in forwarded.variables.keys) { + _variableIndices.remove(variable); + _variables[0].remove(variable); + if (_variableNodes != null) _variableNodes[0].remove(variable); + } + for (var function in forwarded.functions.keys) { + _functionIndices.remove(function); + _functions[0].remove(function); + } + for (var mixin in forwarded.mixins.keys) { + _mixinIndices.remove(mixin); + _mixins[0].remove(mixin); + } + } + } + } + /// Returns the value of the variable named [name], optionally with the given /// [namespace], or `null` if no such variable is declared. /// @@ -577,7 +666,7 @@ class AsyncEnvironment { /// that contains [css] as its CSS tree, which can be extended using /// [extender]. Module toModule(CssStylesheet css, Extender extender) => - _EnvironmentModule(this, css, extender); + _EnvironmentModule(this, css, extender, forwarded: _forwardedModules); /// Returns the module with the given [namespace], or throws a /// [SassScriptException] if none exists. @@ -633,24 +722,98 @@ class _EnvironmentModule implements Module { /// The environment that defines this module's members. final AsyncEnvironment _environment; - // TODO(nweiz): Use custom [UnmodifiableMapView]s that forbid access to - // private members. - _EnvironmentModule(this._environment, this.css, this.extender) - : upstream = _environment._allModules, - variables = PublicMemberMapView(_environment._variables.first), - variableNodes = _environment._variableNodes == null + /// A map from variable names to the modules in which those variables appear, + /// used to determine where variables should be set. + /// + /// Variables that don't appear in this map are either defined directly in + /// this module (if they appear in `_environment._variables.first`) or not + /// defined at all. + final Map _modulesByVariable; + + factory _EnvironmentModule( + AsyncEnvironment environment, CssStylesheet css, Extender extender, + {List forwarded}) { + forwarded ??= const []; + return _EnvironmentModule._( + environment, + css, + extender, + _makeModulesByVariable(forwarded), + _memberMap(environment._variables.first, + forwarded.map((module) => module.variables)), + environment._variableNodes == null ? null - : PublicMemberMapView(_environment._variableNodes.first), - functions = PublicMemberMapView(_environment._functions.first), - mixins = PublicMemberMapView(_environment._mixins.first), - transitivelyContainsCss = css.children.isNotEmpty || - _environment._allModules + : _memberMap(environment._variableNodes.first, + forwarded.map((module) => module.variableNodes)), + _memberMap(environment._functions.first, + forwarded.map((module) => module.functions)), + _memberMap(environment._mixins.first, + forwarded.map((module) => module.mixins)), + transitivelyContainsCss: css.children.isNotEmpty || + environment._allModules .any((module) => module.transitivelyContainsCss), - transitivelyContainsExtensions = !extender.isEmpty || - _environment._allModules - .any((module) => module.transitivelyContainsExtensions); + transitivelyContainsExtensions: !extender.isEmpty || + environment._allModules + .any((module) => module.transitivelyContainsExtensions)); + } + + /// Create [_modulesByVariable] for a set of forwarded modules. + static Map _makeModulesByVariable(List forwarded) { + if (forwarded.isEmpty) return const {}; + + var modulesByVariable = normalizedMap(); + for (var module in forwarded) { + if (module is _EnvironmentModule) { + // Flatten nested forwarded modules to avoid O(depth) overhead. + for (var child in module._modulesByVariable.values) { + setAll(modulesByVariable, child.variables.keys, child); + } + setAll(modulesByVariable, module._environment._variables.first.keys, + module); + } else { + setAll(modulesByVariable, module.variables.keys, module); + } + } + return modulesByVariable; + } + + /// Returns a map that exposes the public members of [localMap] as well as all + /// the members of [maps]. + static Map _memberMap( + Map localMap, Iterable> otherMaps) { + localMap = PublicMemberMapView(localMap); + if (otherMaps.isEmpty) return localMap; + + var allMaps = [ + for (var map in otherMaps) if (map.isNotEmpty) map, + localMap + ]; + if (allMaps.length == 1) return localMap; + + return MergedMapView(allMaps, + equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator); + } + + _EnvironmentModule._( + this._environment, + this.css, + this.extender, + this._modulesByVariable, + this.variables, + this.variableNodes, + this.functions, + this.mixins, + {@required this.transitivelyContainsCss, + @required this.transitivelyContainsExtensions}) + : upstream = _environment._allModules; void setVariable(String name, Value value, AstNode nodeWithSpan) { + var module = _modulesByVariable[name]; + if (module != null) { + module.setVariable(name, value, nodeWithSpan); + return; + } + if (!_environment._variables.first.containsKey(name)) { throw SassScriptException("Undefined variable."); } @@ -666,8 +829,17 @@ class _EnvironmentModule implements Module { if (css.children.isEmpty) return this; var newCssAndExtender = cloneCssStylesheet(css, extender); - return _EnvironmentModule( - _environment, newCssAndExtender.item1, newCssAndExtender.item2); + return _EnvironmentModule._( + _environment, + newCssAndExtender.item1, + newCssAndExtender.item2, + _modulesByVariable, + variables, + variableNodes, + functions, + mixins, + transitivelyContainsCss: transitivelyContainsCss, + transitivelyContainsExtensions: transitivelyContainsExtensions); } String toString() => p.prettyUri(css.span.sourceUrl); diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 31490833b..84322814a 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,19 +5,23 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 739a592852b730f66c9f195b65307450edd14898 +// Checksum: e828781d4c708957403e9786663473327e3971f6 // // ignore_for_file: unused_import +import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'ast/css.dart'; import 'ast/node.dart'; +import 'ast/sass.dart'; import 'callable.dart'; import 'exception.dart'; import 'extend/extender.dart'; import 'module.dart'; +import 'module/forwarded_view.dart'; +import 'util/merged_map_view.dart'; import 'util/public_member_map_view.dart'; import 'utils.dart'; import 'value.dart'; @@ -36,8 +40,13 @@ class Environment { /// This is `null` if there are no namespaceless modules. Set> _globalModules; - /// Modules from both [_modules] and [_global], in the order in which they - /// were `@use`d. + /// The modules forwarded by this module. + /// + /// This is `null` if there are no forwarded modules. + List> _forwardedModules; + + /// Modules from [_modules], [_globalModules], and [_forwardedModules], in the + /// order in which they were `@use`d. final List> _allModules; /// A list of variables defined at each lexical scope level. @@ -133,6 +142,7 @@ class Environment { Environment({bool sourceMap = false}) : _modules = {}, _globalModules = null, + _forwardedModules = null, _allModules = [], _variables = [normalizedMap()], _variableNodes = sourceMap ? [normalizedMap()] : null, @@ -145,6 +155,7 @@ class Environment { Environment._( this._modules, this._globalModules, + this._forwardedModules, this._allModules, this._variables, this._variableNodes, @@ -167,6 +178,7 @@ class Environment { Environment closure() => Environment._( _modules, _globalModules, + _forwardedModules, _allModules, _variables.toList(), _variableNodes?.toList(), @@ -181,6 +193,7 @@ class Environment { Environment global() => Environment._( {}, null, + null, [], _variables.toList(), _variableNodes?.toList(), @@ -220,6 +233,82 @@ 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 ??= []; + + var view = ForwardedModuleView(module, rule); + for (var other in _forwardedModules) { + _assertNoConflicts(view.variables, other.variables, "variable", other); + _assertNoConflicts(view.functions, other.functions, "function", other); + _assertNoConflicts(view.mixins, other.mixins, "mixin", other); + } + + // Add the original module to [_allModules] (rather than the + // [ForwardedModuleView]) so that we can de-duplicate upstream modules using + // `==`. This is safe because upstream modules are only used for collating + // CSS, not for the members they expose. + _allModules.add(module); + _forwardedModules.add(view); + } + + /// Throws a [SassScriptException] if [newMembers] has any keys that overlap + /// with [oldMembers]. + /// + /// The [type] and [oldModule] is used for error reporting. + void _assertNoConflicts(Map newMembers, + Map oldMembers, String type, Module oldModule) { + Map smaller; + Map larger; + if (newMembers.length < oldMembers.length) { + smaller = newMembers; + larger = oldMembers; + } else { + smaller = oldMembers; + larger = newMembers; + } + + for (var name in smaller.keys) { + if (larger.containsKey(name)) { + if (type == "variable") name = "\$$name"; + throw SassScriptException( + 'Module ${p.prettyUri(oldModule.url)} and the new module both ' + 'forward a $type named $name.'); + } + } + } + + /// Makes the members forwarded by [module] available in the current + /// environment. + /// + /// This is called when [module] is `@import`ed. + void importForwards(Module module) { + if (module is _EnvironmentModule) { + for (var forwarded in module._environment._forwardedModules ?? + const >[]) { + _globalModules ??= {}; + _globalModules.add(forwarded); + + // Remove existing definitions that the forwarded members are now + // shadowing. + for (var variable in forwarded.variables.keys) { + _variableIndices.remove(variable); + _variables[0].remove(variable); + if (_variableNodes != null) _variableNodes[0].remove(variable); + } + for (var function in forwarded.functions.keys) { + _functionIndices.remove(function); + _functions[0].remove(function); + } + for (var mixin in forwarded.mixins.keys) { + _mixinIndices.remove(mixin); + _mixins[0].remove(mixin); + } + } + } + } + /// Returns the value of the variable named [name], optionally with the given /// [namespace], or `null` if no such variable is declared. /// @@ -580,7 +669,7 @@ class Environment { /// that contains [css] as its CSS tree, which can be extended using /// [extender]. Module toModule(CssStylesheet css, Extender extender) => - _EnvironmentModule(this, css, extender); + _EnvironmentModule(this, css, extender, forwarded: _forwardedModules); /// Returns the module with the given [namespace], or throws a /// [SassScriptException] if none exists. @@ -637,24 +726,99 @@ class _EnvironmentModule implements Module { /// The environment that defines this module's members. final Environment _environment; - // TODO(nweiz): Use custom [UnmodifiableMapView]s that forbid access to - // private members. - _EnvironmentModule(this._environment, this.css, this.extender) - : upstream = _environment._allModules, - variables = PublicMemberMapView(_environment._variables.first), - variableNodes = _environment._variableNodes == null + /// A map from variable names to the modules in which those variables appear, + /// used to determine where variables should be set. + /// + /// Variables that don't appear in this map are either defined directly in + /// this module (if they appear in `_environment._variables.first`) or not + /// defined at all. + final Map> _modulesByVariable; + + factory _EnvironmentModule( + Environment environment, CssStylesheet css, Extender extender, + {List> forwarded}) { + forwarded ??= const []; + return _EnvironmentModule._( + environment, + css, + extender, + _makeModulesByVariable(forwarded), + _memberMap(environment._variables.first, + forwarded.map((module) => module.variables)), + environment._variableNodes == null ? null - : PublicMemberMapView(_environment._variableNodes.first), - functions = PublicMemberMapView(_environment._functions.first), - mixins = PublicMemberMapView(_environment._mixins.first), - transitivelyContainsCss = css.children.isNotEmpty || - _environment._allModules + : _memberMap(environment._variableNodes.first, + forwarded.map((module) => module.variableNodes)), + _memberMap(environment._functions.first, + forwarded.map((module) => module.functions)), + _memberMap(environment._mixins.first, + forwarded.map((module) => module.mixins)), + transitivelyContainsCss: css.children.isNotEmpty || + environment._allModules .any((module) => module.transitivelyContainsCss), - transitivelyContainsExtensions = !extender.isEmpty || - _environment._allModules - .any((module) => module.transitivelyContainsExtensions); + transitivelyContainsExtensions: !extender.isEmpty || + environment._allModules + .any((module) => module.transitivelyContainsExtensions)); + } + + /// Create [_modulesByVariable] for a set of forwarded modules. + static Map> _makeModulesByVariable( + List> forwarded) { + if (forwarded.isEmpty) return const {}; + + var modulesByVariable = normalizedMap>(); + for (var module in forwarded) { + if (module is _EnvironmentModule) { + // Flatten nested forwarded modules to avoid O(depth) overhead. + for (var child in module._modulesByVariable.values) { + setAll(modulesByVariable, child.variables.keys, child); + } + setAll(modulesByVariable, module._environment._variables.first.keys, + module); + } else { + setAll(modulesByVariable, module.variables.keys, module); + } + } + return modulesByVariable; + } + + /// Returns a map that exposes the public members of [localMap] as well as all + /// the members of [maps]. + static Map _memberMap( + Map localMap, Iterable> otherMaps) { + localMap = PublicMemberMapView(localMap); + if (otherMaps.isEmpty) return localMap; + + var allMaps = [ + for (var map in otherMaps) if (map.isNotEmpty) map, + localMap + ]; + if (allMaps.length == 1) return localMap; + + return MergedMapView(allMaps, + equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator); + } + + _EnvironmentModule._( + this._environment, + this.css, + this.extender, + this._modulesByVariable, + this.variables, + this.variableNodes, + this.functions, + this.mixins, + {@required this.transitivelyContainsCss, + @required this.transitivelyContainsExtensions}) + : upstream = _environment._allModules; void setVariable(String name, Value value, AstNode nodeWithSpan) { + var module = _modulesByVariable[name]; + if (module != null) { + module.setVariable(name, value, nodeWithSpan); + return; + } + if (!_environment._variables.first.containsKey(name)) { throw SassScriptException("Undefined variable."); } @@ -670,8 +834,17 @@ class _EnvironmentModule implements Module { if (css.children.isEmpty) return this; var newCssAndExtender = cloneCssStylesheet(css, extender); - return _EnvironmentModule( - _environment, newCssAndExtender.item1, newCssAndExtender.item2); + return _EnvironmentModule._( + _environment, + newCssAndExtender.item1, + newCssAndExtender.item2, + _modulesByVariable, + variables, + variableNodes, + functions, + mixins, + transitivelyContainsCss: transitivelyContainsCss, + transitivelyContainsExtensions: transitivelyContainsExtensions); } String toString() => p.prettyUri(css.span.sourceUrl); diff --git a/lib/src/module/forwarded_view.dart b/lib/src/module/forwarded_view.dart new file mode 100644 index 000000000..5ae7a862f --- /dev/null +++ b/lib/src/module/forwarded_view.dart @@ -0,0 +1,96 @@ +// 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 '../ast/css.dart'; +import '../ast/node.dart'; +import '../ast/sass.dart'; +import '../callable.dart'; +import '../exception.dart'; +import '../extend/extender.dart'; +import '../module.dart'; +import '../util/limited_map_view.dart'; +import '../util/prefixed_map_view.dart'; +import '../utils.dart'; +import '../value.dart'; + +/// A [Module] that exposes members according to a [ForwardRule]. +class ForwardedModuleView implements Module { + /// The wrapped module. + final Module _inner; + + /// The rule that determines how this module's members should be exposed. + final ForwardRule _rule; + + Uri get url => _inner.url; + List> get upstream => _inner.upstream; + Extender get extender => _inner.extender; + CssStylesheet get css => _inner.css; + bool get transitivelyContainsCss => _inner.transitivelyContainsCss; + bool get transitivelyContainsExtensions => + _inner.transitivelyContainsExtensions; + + final Map variables; + final Map variableNodes; + final Map functions; + final Map mixins; + + 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), + functions = _forwardedMap(_inner.functions, _rule.prefix, + _rule.shownMixinsAndFunctions, _rule.hiddenMixinsAndFunctions), + mixins = _forwardedMap(_inner.mixins, _rule.prefix, + _rule.shownMixinsAndFunctions, _rule.hiddenMixinsAndFunctions); + + /// Wraps [map] so that it only shows members allowed by [blacklist] or + /// [whitelist], with the given [prefix], if given. + /// + /// Only one of [blacklist] or [whitelist] may be non-`null`. + static Map _forwardedMap(Map map, String prefix, + Set whitelist, Set blacklist) { + assert(whitelist == null || blacklist == null); + if (prefix == null && + whitelist == null && + (blacklist == null || blacklist.isEmpty)) { + return map; + } + + if (prefix != null) { + map = PrefixedMapView(map, prefix, equals: equalsIgnoreSeparator); + } + + if (whitelist != null) { + map = LimitedMapView.whitelist(map, whitelist); + } else if (blacklist != null && blacklist.isNotEmpty) { + map = LimitedMapView.blacklist(map, blacklist); + } + + return map; + } + + void setVariable(String name, Value value, AstNode nodeWithSpan) { + if (_rule.shownVariables != null && !_rule.shownVariables.contains(name)) { + throw SassScriptException("Undefined variable."); + } else if (_rule.hiddenVariables != null && + _rule.hiddenVariables.contains(name)) { + throw SassScriptException("Undefined variable."); + } + + if (_rule.prefix != null) { + if (!startsWithIgnoreSeparator(name, _rule.prefix)) { + throw SassScriptException("Undefined variable."); + } + + name = name.substring(_rule.prefix.length); + } + + return _inner.setVariable(name, value, nodeWithSpan); + } + + Module cloneCss() => ForwardedModuleView(_inner.cloneCss(), _rule); +} diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index baaabc771..62990d76d 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -632,6 +632,17 @@ class Parser { void error(String message, FileSpan span) => throw StringScannerException(message, span, scanner.string); + /// Runs callback and, if it throws a [SourceSpanFormatException], rethrows it + /// with [message] as its message. + @protected + T withErrorMessage(String message, T callback()) { + try { + return callback(); + } on SourceSpanFormatException catch (error) { + throw SourceSpanFormatException(message, error.span, error.source); + } + } + /// Prints a source span highlight of the current location being scanned. /// /// If [message] is passed, prints that as well. This is intended for use when diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 92ef50b67..f1ea6d1fb 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -534,6 +534,10 @@ abstract class StylesheetParser extends Parser { return _extendRule(start); case "for": return _forRule(start, child); + case "forward": + _isUseAllowed = wasUseAllowed; + if (!root) _disallowedAtRule(start); + return _forwardRule(start); case "function": return _functionRule(start); case "if": @@ -850,6 +854,82 @@ abstract class StylesheetParser extends Parser { }); } + /// Consumes a `@forward` rule. + /// + /// [start] should point before the `@`. + ForwardRule _forwardRule(LineScannerState start) { + var url = _urlString(); + whitespace(); + + String prefix; + if (scanIdentifier("as")) { + whitespace(); + prefix = identifier(); + scanner.expectChar($asterisk); + whitespace(); + } + + Set shownMixinsAndFunctions; + Set shownVariables; + Set hiddenMixinsAndFunctions; + Set hiddenVariables; + if (scanIdentifier("show")) { + var members = _memberList(); + shownMixinsAndFunctions = members.item1; + shownVariables = members.item2; + } else if (scanIdentifier("hide")) { + var members = _memberList(); + hiddenMixinsAndFunctions = members.item1; + hiddenVariables = members.item2; + } + + expectStatementSeparator("@forward rule"); + var span = scanner.spanFrom(start); + if (!_parseUse) { + error( + "@forward is coming soon, but it's not supported in this version of " + "Dart Sass.", + span); + } else if (!_isUseAllowed) { + error("@forward rules must be written before any other rules.", span); + } + + if (shownMixinsAndFunctions != null) { + return ForwardRule.show( + url, shownMixinsAndFunctions, shownVariables, span, + prefix: prefix); + } else if (hiddenMixinsAndFunctions != null) { + return ForwardRule.hide( + url, hiddenMixinsAndFunctions, hiddenVariables, span, + prefix: prefix); + } else { + return ForwardRule(url, span, prefix: prefix); + } + } + + /// Consumes a list of members that may contain either plain identifiers or + /// variable names. + /// + /// The plain identifiers are returned in the first set, and the variable + /// names in the second. + Tuple2, Set> _memberList() { + var identifiers = Set(); + var variables = Set(); + do { + whitespace(); + withErrorMessage("Expected variable, mixin, or function name", () { + if (scanner.peekChar() == $dollar) { + variables.add(variableName()); + } else { + identifiers.add(identifier()); + } + }); + whitespace(); + } while (scanner.scanChar($comma)); + + return Tuple2(identifiers, variables); + } + /// Consumes an `@if` rule. /// /// [start] should point before the `@`. [child] is called to consume any @@ -1188,13 +1268,7 @@ relase. For details, see http://bit.ly/moz-document. /// /// [start] should point before the `@`. UseRule _useRule(LineScannerState start) { - var urlString = string(); - Uri url; - try { - url = Uri.parse(urlString); - } on FormatException catch (innerError) { - error("Invalid URL: ${innerError.message}", scanner.spanFrom(start)); - } + var url = _urlString(); whitespace(); String namespace; @@ -1223,6 +1297,7 @@ relase. For details, see http://bit.ly/moz-document. } else if (!_isUseAllowed) { error("@use rules must be written before any other rules.", span); } + expectStatementSeparator("@use rule"); return UseRule(url, namespace, span); } @@ -3236,6 +3311,17 @@ relase. For details, see http://bit.ly/moz-document. return result; } + /// Consumes a string that contains a valid URL. + Uri _urlString() { + var start = scanner.state; + var url = string(); + try { + return Uri.parse(url); + } on FormatException catch (innerError) { + error("Invalid URL: ${innerError.message}", scanner.spanFrom(start)); + } + } + /// Like [identifier], but rejects identifiers that begin with `_` or `-`. String _publicIdentifier() { var start = scanner.state; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index d45e6c9a9..bb9402d55 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -274,6 +274,25 @@ bool startsWithIgnoreCase(String string, String prefix) { return true; } +/// Returns whether [string] begins with [prefix] if `-` and `_` are +/// considered equivalent. +bool startsWithIgnoreSeparator(String string, String prefix) { + if (string.length < prefix.length) return false; + for (var i = 0; i < prefix.length; i++) { + var stringCodeUnit = string.codeUnitAt(i); + var prefixCodeUnit = prefix.codeUnitAt(i); + if (stringCodeUnit == prefixCodeUnit) continue; + if (stringCodeUnit == $dash) { + if (prefixCodeUnit != $underscore) return false; + } else if (stringCodeUnit == $underscore) { + if (prefixCodeUnit != $dash) return false; + } else { + return false; + } + } + return true; +} + /// Returns an empty map that uses [equalsIgnoreSeparator] for key equality. /// /// If [source] is passed, copies it into the map. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 6f58e4685..dc16d1295 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -975,6 +975,14 @@ class _EvaluateVisitor }, semiGlobal: true); } + Future visitForwardRule(ForwardRule node) async { + await _loadModule(node.url, "@forward", node, (module) { + _environment.forwardModule(module, node); + }); + + return null; + } + Future visitFunctionRule(FunctionRule node) async { _environment.setFunction(UserDefinedCallable(node, _environment.closure())); return null; @@ -1021,8 +1029,9 @@ class _EvaluateVisitor _activeModules.add(url); - // TODO(nweiz): If [stylesheet] contains no `@use` rules, just evaluate it - // directly in [_root] rather than making a new stylesheet. + // TODO(nweiz): If [stylesheet] contains no `@use` or `@forward` rules, just + // evaluate it directly in [_root] rather than making a new + // [ModifiableCssStylesheet] and manually copying members. List children; var environment = _environment.global(); @@ -1053,10 +1062,13 @@ class _EvaluateVisitor }); }); - // Create a dummy module with empty CSS and no extensions to combine all - // the CSS from modules used by [stylesheet]. + // Create a dummy module with empty CSS and no extensions to make forwarded + // members available in the current import context and to combine all the + // CSS from modules used by [stylesheet]. var module = environment.toModule( CssStylesheet(const [], stylesheet.span), Extender.empty); + _environment.importForwards(module); + if (module.transitivelyContainsCss) { // If any transitively used module contains extensions, we need to clone // all modules' CSS. Otherwise, it's possible that they'll be used or diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index dc604f620..0c8200571 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: 36af5d91812c44a7f77ef0b7f580fa9fb8520ad0 +// Checksum: 66510adda04cb00e75c60fc354b66081e24d1f40 // // ignore_for_file: unused_import @@ -977,6 +977,14 @@ class _EvaluateVisitor }, semiGlobal: true); } + Value visitForwardRule(ForwardRule node) { + _loadModule(node.url, "@forward", node, (module) { + _environment.forwardModule(module, node); + }); + + return null; + } + Value visitFunctionRule(FunctionRule node) { _environment.setFunction(UserDefinedCallable(node, _environment.closure())); return null; @@ -1023,8 +1031,9 @@ class _EvaluateVisitor _activeModules.add(url); - // TODO(nweiz): If [stylesheet] contains no `@use` rules, just evaluate it - // directly in [_root] rather than making a new stylesheet. + // TODO(nweiz): If [stylesheet] contains no `@use` or `@forward` rules, just + // evaluate it directly in [_root] rather than making a new + // [ModifiableCssStylesheet] and manually copying members. List children; var environment = _environment.global(); @@ -1055,10 +1064,13 @@ class _EvaluateVisitor }); }); - // Create a dummy module with empty CSS and no extensions to combine all - // the CSS from modules used by [stylesheet]. + // Create a dummy module with empty CSS and no extensions to make forwarded + // members available in the current import context and to combine all the + // CSS from modules used by [stylesheet]. var module = environment.toModule( CssStylesheet(const [], stylesheet.span), Extender.empty); + _environment.importForwards(module); + if (module.transitivelyContainsCss) { // If any transitively used module contains extensions, we need to clone // all modules' CSS. Otherwise, it's possible that they'll be used or diff --git a/lib/src/visitor/interface/statement.dart b/lib/src/visitor/interface/statement.dart index 64d050f2f..eaf387d82 100644 --- a/lib/src/visitor/interface/statement.dart +++ b/lib/src/visitor/interface/statement.dart @@ -18,6 +18,7 @@ abstract class StatementVisitor { T visitErrorRule(ErrorRule node); T visitExtendRule(ExtendRule node); T visitForRule(ForRule node); + T visitForwardRule(ForwardRule node); T visitFunctionRule(FunctionRule node); T visitIfRule(IfRule node); T visitImportRule(ImportRule node); diff --git a/lib/src/visitor/recursive_statement.dart b/lib/src/visitor/recursive_statement.dart index 13ddc7307..d1eb9a38b 100644 --- a/lib/src/visitor/recursive_statement.dart +++ b/lib/src/visitor/recursive_statement.dart @@ -72,6 +72,8 @@ abstract class RecursiveStatementVisitor implements StatementVisitor { return visitChildren(node); } + T visitForwardRule(ForwardRule node) => null; + T visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node); T visitIfRule(IfRule node) {