Merge branch 'master' into math-functions

This commit is contained in:
awjin 2020-01-16 14:55:40 -08:00
commit 4c0c6b48e4
30 changed files with 1035 additions and 467 deletions

View File

@ -27,6 +27,11 @@
* Add the variables `$pi` and `$e` to the built-in "sass:math" module.
## 1.24.5
* Highlight contextually-relevant sections of the stylesheet in error messages,
rather than only highlighting the section where the error was detected.
## 1.24.4
### JavaScript API

View File

@ -11,4 +11,24 @@ abstract class AstNode {
/// This indicates where in the source Sass or SCSS stylesheet the node was
/// defined.
FileSpan get span;
/// Returns an [AstNode] that doesn't have any data and whose span is
/// generated by [callback].
///
/// A number of APIs take [AstNode]s instead of spans because computing spans
/// eagerly can be expensive. This allows arbitrary spans to be passed to
/// those callbacks while still being lazily computed.
factory AstNode.fake(FileSpan Function() callback) = _FakeAstNode;
AstNode();
}
/// An [AstNode] that just exposes a single span generated by a callback.
class _FakeAstNode implements AstNode {
FileSpan get span => _callback();
/// The callback to use to generate [span].
final FileSpan Function() _callback;
_FakeAstNode(this._callback);
}

View File

@ -8,6 +8,7 @@ import '../../exception.dart';
import '../../logger.dart';
import '../../parse/scss.dart';
import '../../utils.dart';
import '../../util/character.dart';
import 'argument.dart';
import 'node.dart';
@ -22,6 +23,32 @@ class ArgumentDeclaration implements SassNode {
final FileSpan span;
/// Returns [span] expanded to include an identifier immediately before the
/// declaration, if possible.
FileSpan get spanWithName {
var text = span.file.getText(0);
// Move backwards through and whitspace between the name and the arguments.
var i = span.start.offset - 1;
while (i > 0 && isWhitespace(text.codeUnitAt(i))) {
i--;
}
// Then move backwards through the name itself.
if (!isName(text.codeUnitAt(i))) return span;
i--;
while (i >= 0 && isName(text.codeUnitAt(i))) {
i--;
}
// If the name didn't start with [isNameStart], it's not a valid identifier.
if (!isNameStart(text.codeUnitAt(i + 1))) return span;
// Trim because it's possible that this span is empty (for example, a mixin
// may be declared without an argument list).
return span.file.span(i + 1, span.end.offset).trim();
}
/// The name of the rest argument as written in the document, without
/// underscores converted to hyphens and including the leading `$`.
///
@ -47,16 +74,15 @@ class ArgumentDeclaration implements SassNode {
: arguments = const [],
restArgument = null;
/// Parses an argument declaration from [contents], which should not include
/// parentheses.
/// Parses an argument declaration from [contents], which should be of the
/// form `@rule name(args) {`.
///
/// If passed, [url] is the name of the file from which [contents] comes.
///
/// Throws a [SassFormatException] if parsing fails.
factory ArgumentDeclaration.parse(String contents,
{Object url, Logger logger}) =>
ScssParser("($contents)", url: url, logger: logger)
.parseArgumentDeclaration();
ScssParser(contents, url: url, logger: logger).parseArgumentDeclaration();
/// Throws a [SassScriptException] if [positional] and [names] aren't valid
/// for this argument declaration.
@ -73,27 +99,34 @@ class ArgumentDeclaration implements SassNode {
} else if (names.contains(argument.name)) {
namedUsed++;
} else if (argument.defaultValue == null) {
throw SassScriptException(
"Missing argument ${_originalArgumentName(argument.name)}.");
throw MultiSpanSassScriptException(
"Missing argument ${_originalArgumentName(argument.name)}.",
"invocation",
{spanWithName: "declaration"});
}
}
if (restArgument != null) return;
if (positional > arguments.length) {
throw SassScriptException("Only ${arguments.length} "
"${names.isEmpty ? '' : 'positional '}"
"${pluralize('argument', arguments.length)} allowed, but "
"${positional} ${pluralize('was', positional, plural: 'were')} "
"passed.");
throw MultiSpanSassScriptException(
"Only ${arguments.length} "
"${names.isEmpty ? '' : 'positional '}"
"${pluralize('argument', arguments.length)} allowed, but "
"${positional} ${pluralize('was', positional, plural: 'were')} "
"passed.",
"invocation",
{spanWithName: "declaration"});
}
if (namedUsed < names.length) {
var unknownNames = Set.of(names)
..removeAll(arguments.map((argument) => argument.name));
throw SassScriptException(
throw MultiSpanSassScriptException(
"No ${pluralize('argument', unknownNames.length)} named "
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.");
"${toSentence(unknownNames.map((name) => "\$$name"), 'or')}.",
"invocation",
{spanWithName: "declaration"});
}
}

View File

@ -17,8 +17,8 @@ import '../callable_invocation.dart';
/// evaluated.
class IfExpression implements Expression, CallableInvocation {
/// The declaration of `if()`, as though it were a normal function.
static final declaration =
ArgumentDeclaration.parse(r"$condition, $if-true, $if-false");
static final declaration = ArgumentDeclaration.parse(
r"@function if($condition, $if-true, $if-false) {");
/// The arguments passed to `if()`.
final ArgumentInvocation arguments;

View File

@ -4,6 +4,7 @@
import 'package:source_span/source_span.dart';
import '../../../utils.dart';
import '../../../visitor/interface/statement.dart';
import '../argument_invocation.dart';
import '../callable_invocation.dart';
@ -29,6 +30,11 @@ class IncludeRule implements Statement, CallableInvocation {
final FileSpan span;
/// Returns this include's span, without its content block (if it has one).
FileSpan get spanWithoutContent => content == null
? span
: span.file.span(span.start.offset, arguments.span.end.offset).trim();
IncludeRule(this.name, this.arguments, this.span,
{this.namespace, this.content});

View File

@ -35,16 +35,32 @@ class AsyncEnvironment {
Map<String, Module> get modules => UnmodifiableMapView(_modules);
final Map<String, Module> _modules;
/// A map from module namespaces to the nodes whose spans indicate where those
/// modules were originally loaded.
final Map<String, AstNode> _namespaceNodes;
/// The namespaceless modules used in the current scope.
///
/// This is `null` if there are no namespaceless modules.
Set<Module> _globalModules;
/// A map from modules in [_globalModules] to the nodes whose spans
/// indicate where those modules were originally loaded.
///
/// This is `null` if there are no namespaceless modules.
Map<Module, AstNode> _globalModuleNodes;
/// The modules forwarded by this module.
///
/// This is `null` if there are no forwarded modules.
List<Module> _forwardedModules;
/// A map from modules in [_forwardedModules] to the nodes whose spans
/// indicate where those modules were originally forwarded.
///
/// This is `null` if there are no forwarded modules.
Map<Module, AstNode> _forwardedModuleNodes;
/// Modules forwarded by nested imports at each lexical scope level *beneath
/// the global scope*.
///
@ -136,8 +152,11 @@ class AsyncEnvironment {
/// If [sourceMap] is `true`, this tracks variables' source locations
AsyncEnvironment({bool sourceMap = false})
: _modules = {},
_namespaceNodes = {},
_globalModules = null,
_globalModuleNodes = null,
_forwardedModules = null,
_forwardedModuleNodes = null,
_nestedForwardedModules = null,
_allModules = [],
_variables = [{}],
@ -150,8 +169,11 @@ class AsyncEnvironment {
AsyncEnvironment._(
this._modules,
this._namespaceNodes,
this._globalModules,
this._globalModuleNodes,
this._forwardedModules,
this._forwardedModuleNodes,
this._nestedForwardedModules,
this._allModules,
this._variables,
@ -174,8 +196,11 @@ class AsyncEnvironment {
/// when the closure was created will be reflected.
AsyncEnvironment closure() => AsyncEnvironment._(
_modules,
_namespaceNodes,
_globalModules,
_globalModuleNodes,
_forwardedModules,
_forwardedModuleNodes,
_nestedForwardedModules,
_allModules,
_variables.toList(),
@ -190,6 +215,9 @@ class AsyncEnvironment {
/// functions, and mixins, but not its modules.
AsyncEnvironment global() => AsyncEnvironment._(
{},
{},
null,
null,
null,
null,
null,
@ -202,16 +230,20 @@ class AsyncEnvironment {
/// Adds [module] to the set of modules visible in this environment.
///
/// [nodeWithSpan]'s span is used to report any errors with the module.
///
/// If [namespace] is passed, the module is made available under that
/// namespace.
///
/// Throws a [SassScriptException] if there's already a module with the given
/// [namespace], or if [namespace] is `null` and [module] defines a variable
/// with the same name as a variable defined in this environment.
void addModule(Module module, {String namespace}) {
void addModule(Module module, AstNode nodeWithSpan, {String namespace}) {
if (namespace == null) {
_globalModules ??= {};
_globalModuleNodes ??= {};
_globalModules.add(module);
_globalModuleNodes[module] = nodeWithSpan;
_allModules.add(module);
for (var name in _variables.first.keys) {
@ -223,11 +255,14 @@ class AsyncEnvironment {
}
} else {
if (_modules.containsKey(namespace)) {
throw SassScriptException(
"There's already a module with namespace \"$namespace\".");
throw MultiSpanSassScriptException(
"There's already a module with namespace \"$namespace\".",
"new @use",
{_namespaceNodes[namespace].span: "original @use"});
}
_modules[namespace] = module;
_namespaceNodes[namespace] = nodeWithSpan;
_allModules.add(module);
}
}
@ -236,12 +271,15 @@ class AsyncEnvironment {
/// defined in this module, according to the modifications defined by [rule].
void forwardModule(Module module, ForwardRule rule) {
_forwardedModules ??= [];
_forwardedModuleNodes ??= {};
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);
_assertNoConflicts(
view.variables, other.variables, "variable", other, rule);
_assertNoConflicts(
view.functions, other.functions, "function", other, rule);
_assertNoConflicts(view.mixins, other.mixins, "mixin", other, rule);
}
// Add the original module to [_allModules] (rather than the
@ -250,14 +288,19 @@ class AsyncEnvironment {
// CSS, not for the members they expose.
_allModules.add(module);
_forwardedModules.add(view);
_forwardedModuleNodes[view] = rule;
}
/// Throws a [SassScriptException] if [newMembers] has any keys that overlap
/// with [oldMembers].
///
/// The [type] and [oldModule] is used for error reporting.
void _assertNoConflicts(Map<String, Object> newMembers,
Map<String, Object> oldMembers, String type, Module oldModule) {
/// The [type], [other], [newModuleNodeWithSpan] are used for error reporting.
void _assertNoConflicts(
Map<String, Object> newMembers,
Map<String, Object> oldMembers,
String type,
Module other,
AstNode newModuleNodeWithSpan) {
Map<String, Object> smaller;
Map<String, Object> larger;
if (newMembers.length < oldMembers.length) {
@ -271,9 +314,10 @@ class AsyncEnvironment {
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.');
throw MultiSpanSassScriptException(
'Two forwarded modules both define a $type named $name.',
"new @forward",
{_forwardedModuleNodes[other].span: "original @forward"});
}
}
}
@ -288,7 +332,9 @@ class AsyncEnvironment {
if (forwarded == null) return;
_globalModules ??= {};
_globalModuleNodes ??= {};
_forwardedModules ??= [];
_forwardedModuleNodes ??= {};
var forwardedVariableNames =
forwarded.expand((module) => module.variables.keys).toSet();
@ -308,6 +354,7 @@ class AsyncEnvironment {
if (shadowed != null) {
_globalModules.remove(module);
_globalModules.add(shadowed);
_globalModuleNodes[shadowed] = _globalModuleNodes.remove(module);
}
}
for (var i = 0; i < _forwardedModules.length; i++) {
@ -316,11 +363,17 @@ class AsyncEnvironment {
variables: forwardedVariableNames,
mixins: forwardedMixinNames,
functions: forwardedFunctionNames);
if (shadowed != null) _forwardedModules[i] = shadowed;
if (shadowed != null) {
_forwardedModules[i] = shadowed;
_forwardedModuleNodes[shadowed] =
_forwardedModuleNodes.remove(module);
}
}
_globalModules.addAll(forwarded);
_globalModuleNodes.addAll(module._environment._forwardedModuleNodes);
_forwardedModules.addAll(forwarded);
_forwardedModuleNodes.addAll(module._environment._forwardedModuleNodes);
} else {
_nestedForwardedModules ??=
List.generate(_variables.length - 1, (_) => []);
@ -746,7 +799,7 @@ class AsyncEnvironment {
configuration[name] = ConfiguredValue(values[name], null, nodes[name]);
}
}
return Configuration(configuration, isImplicit: true);
return Configuration.implicit(configuration);
}
/// Returns a module that represents the top-level members defined in [this],
@ -795,11 +848,12 @@ class AsyncEnvironment {
if (valueInModule == null) continue;
if (value != null) {
throw SassScriptException(
'This $type is available from multiple global modules:\n' +
bulletedList(_globalModules
.where((module) => callback(module) != null)
.map((module) => p.prettyUri(module.url))));
throw MultiSpanSassScriptException(
'This $type is available from multiple global modules.',
'$type use', {
for (var entry in _globalModuleNodes.entries)
if (callback(entry.key) != null) entry.value.span: 'includes $type'
});
}
value = valueInModule;

View File

@ -15,8 +15,8 @@ export 'callable/built_in.dart';
export 'callable/plain_css.dart';
export 'callable/user_defined.dart';
/// An interface functions and mixins that can be invoked from Sass by passing
/// in arguments.
/// An interface for functions and mixins that can be invoked from Sass by
/// passing in arguments.
///
/// This extends [AsyncCallable] because all synchronous callables are also
/// usable in asynchronous contexts. [Callable]s are usable with both the
@ -65,7 +65,12 @@ export 'callable/user_defined.dart';
/// access a string's length in code points.
@sealed
abstract class Callable extends AsyncCallable {
/// Creates a callable with the given [name] and [arguments] that runs
@Deprecated('Use `Callable.function` instead.')
factory Callable(String name, String arguments,
ext.Value callback(List<ext.Value> arguments)) =>
Callable.function(name, arguments, callback);
/// Creates a function with the given [name] and [arguments] that runs
/// [callback] when called.
///
/// The argument declaration is parsed from [arguments], which uses the same
@ -80,7 +85,8 @@ abstract class Callable extends AsyncCallable {
/// For example:
///
/// ```dart
/// new Callable("str-split", r'$string, $divider: " "', (arguments) {
/// new Callable.function("str-split", r'$string, $divider: " "',
/// (arguments) {
/// var string = arguments[0].assertString("string");
/// var divider = arguments[1].assertString("divider");
/// return new SassList(
@ -90,12 +96,12 @@ abstract class Callable extends AsyncCallable {
/// });
/// ```
///
/// Callables may also take variable length argument lists. These are declared
/// Functions may also take variable length argument lists. These are declared
/// the same way as in Sass, and are passed as the final argument to the
/// callback. For example:
///
/// ```dart
/// new Callable("str-join", r'$strings...', (arguments) {
/// new Callable.function("str-join", r'$strings...', (arguments) {
/// var args = arguments.first as SassArgumentList;
/// var strings = args.map((arg) => arg.assertString()).toList();
/// return new SassString(strings.map((string) => string.text).join(),
@ -106,8 +112,8 @@ abstract class Callable extends AsyncCallable {
/// Note that the argument list is always an instance of [SassArgumentList],
/// which provides access to keyword arguments using
/// [SassArgumentList.keywords].
factory Callable(String name, String arguments,
factory Callable.function(String name, String arguments,
ext.Value callback(List<ext.Value> arguments)) =>
BuiltInCallable(
BuiltInCallable.function(
name, arguments, (arguments) => callback(arguments) as Value);
}

View File

@ -10,8 +10,8 @@ import '../value.dart';
import '../value/external/value.dart' as ext;
import 'async_built_in.dart';
/// An interface functions and mixins that can be invoked from Sass by passing
/// in arguments.
/// An interface for functions and mixins that can be invoked from Sass by
/// passing in arguments.
///
/// This class represents callables that *need* to do asynchronous work. It's
/// only compatible with the asynchonous `compile()` methods. If a callback can
@ -23,6 +23,11 @@ abstract class AsyncCallable {
/// The callable's name.
String get name;
@Deprecated('Use `AsyncCallable.function` instead.')
factory AsyncCallable(String name, String arguments,
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
AsyncCallable.function(name, arguments, callback);
/// Creates a callable with the given [name] and [arguments] that runs
/// [callback] when called.
///
@ -30,9 +35,9 @@ abstract class AsyncCallable {
/// include parentheses. Throws a [SassFormatException] if parsing fails.
///
/// See [new Callable] for more details.
factory AsyncCallable(String name, String arguments,
factory AsyncCallable.function(String name, String arguments,
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
AsyncBuiltInCallable(name, arguments, (arguments) {
AsyncBuiltInCallable.function(name, arguments, (arguments) {
var result = callback(arguments);
if (result is ext.Value) return result as Value;
return (result as Future<ext.Value>).then((value) => value as Value);

View File

@ -22,36 +22,49 @@ typedef _Callback = FutureOr<Value> Function(List<Value> arguments);
class AsyncBuiltInCallable implements AsyncCallable {
final String name;
/// The overloads declared for this callable.
final _overloads = <Tuple2<ArgumentDeclaration, _Callback>>[];
/// This callable's arguments.
final ArgumentDeclaration _arguments;
/// Creates a callable with a single [arguments] declaration and a single
/// The callback to run when executing this callable.
final _Callback _callback;
/// Creates a function with a single [arguments] declaration and a single
/// [callback].
///
/// The argument declaration is parsed from [arguments], which should not
/// include parentheses. Throws a [SassFormatException] if parsing fails.
AsyncBuiltInCallable(String name, String arguments,
FutureOr<Value> callback(List<Value> arguments))
: this.parsed(name, ArgumentDeclaration.parse(arguments), callback);
///
/// If passed, [url] is the URL of the module in which the function is
/// defined.
AsyncBuiltInCallable.function(String name, String arguments,
FutureOr<Value> callback(List<Value> arguments), {Object url})
: this.parsed(
name,
ArgumentDeclaration.parse('@function $name($arguments) {',
url: url),
callback);
/// Creates a mixin with a single [arguments] declaration and a single
/// [callback].
///
/// The argument declaration is parsed from [arguments], which should not
/// include parentheses. Throws a [SassFormatException] if parsing fails.
///
/// If passed, [url] is the URL of the module in which the mixin is
/// defined.
AsyncBuiltInCallable.mixin(String name, String arguments,
FutureOr<void> callback(List<Value> arguments),
{Object url})
: this.parsed(name,
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
(arguments) async {
await callback(arguments);
return null;
});
/// Creates a callable with a single [arguments] declaration and a single
/// [callback].
AsyncBuiltInCallable.parsed(this.name, ArgumentDeclaration arguments,
FutureOr<Value> callback(List<Value> arguments)) {
_overloads.add(Tuple2(arguments, callback));
}
/// Creates a callable with multiple implementations.
///
/// Each key/value pair in [overloads] defines the argument declaration for
/// the overload (which should not include parentheses), and the callback to
/// execute if that argument declaration matches. Throws a
/// [SassFormatException] if parsing fails.
AsyncBuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads) {
overloads.forEach((arguments, callback) {
_overloads.add(Tuple2(ArgumentDeclaration.parse(arguments), callback));
});
}
AsyncBuiltInCallable.parsed(this.name, this._arguments, this._callback);
/// Returns the argument declaration and Dart callback for the given
/// positional and named arguments.
@ -60,28 +73,6 @@ class AsyncBuiltInCallable implements AsyncCallable {
/// doesn't guarantee that [positional] and [names] are valid for the returned
/// [ArgumentDeclaration].
Tuple2<ArgumentDeclaration, _Callback> callbackFor(
int positional, Set<String> names) {
Tuple2<ArgumentDeclaration, _Callback> fuzzyMatch;
int minMismatchDistance;
for (var overload in _overloads) {
// Ideally, find an exact match.
if (overload.item1.matches(positional, names)) return overload;
var mismatchDistance = overload.item1.arguments.length - positional;
if (minMismatchDistance != null) {
if (mismatchDistance.abs() > minMismatchDistance.abs()) continue;
// If two overloads have the same mismatch distance, favor the overload
// that has more arguments.
if (mismatchDistance.abs() == minMismatchDistance.abs() &&
mismatchDistance < 0) continue;
}
minMismatchDistance = mismatchDistance;
fuzzyMatch = overload;
}
return fuzzyMatch;
}
int positional, Set<String> names) =>
Tuple2(_arguments, _callback);
}

View File

@ -23,14 +23,40 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
/// The overloads declared for this callable.
final List<Tuple2<ArgumentDeclaration, _Callback>> _overloads;
/// Creates a callable with a single [arguments] declaration and a single
/// Creates a function with a single [arguments] declaration and a single
/// [callback].
///
/// The argument declaration is parsed from [arguments], which should not
/// include parentheses. Throws a [SassFormatException] if parsing fails.
BuiltInCallable(
String name, String arguments, Value callback(List<Value> arguments))
: this.parsed(name, ArgumentDeclaration.parse(arguments), callback);
///
/// If passed, [url] is the URL of the module in which the function is
/// defined.
BuiltInCallable.function(
String name, String arguments, Value callback(List<Value> arguments),
{Object url})
: this.parsed(
name,
ArgumentDeclaration.parse('@function $name($arguments) {',
url: url),
callback);
/// Creates a mixin with a single [arguments] declaration and a single
/// [callback].
///
/// The argument declaration is parsed from [arguments], which should not
/// include parentheses. Throws a [SassFormatException] if parsing fails.
///
/// If passed, [url] is the URL of the module in which the mixin is
/// defined.
BuiltInCallable.mixin(
String name, String arguments, void callback(List<Value> arguments),
{Object url})
: this.parsed(name,
ArgumentDeclaration.parse('@mixin $name($arguments) {', url: url),
(arguments) {
callback(arguments);
return null;
});
/// Creates a callable with a single [arguments] declaration and a single
/// [callback].
@ -38,16 +64,24 @@ class BuiltInCallable implements Callable, AsyncBuiltInCallable {
Value callback(List<Value> arguments))
: _overloads = [Tuple2(arguments, callback)];
/// Creates a callable with multiple implementations.
/// Creates a function with multiple implementations.
///
/// Each key/value pair in [overloads] defines the argument declaration for
/// the overload (which should not include parentheses), and the callback to
/// execute if that argument declaration matches. Throws a
/// [SassFormatException] if parsing fails.
BuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads)
///
/// If passed, [url] is the URL of the module in which the function is
/// defined.
BuiltInCallable.overloadedFunction(
this.name, Map<String, _Callback> overloads,
{Object url})
: _overloads = [
for (var entry in overloads.entries)
Tuple2(ArgumentDeclaration.parse(entry.key), entry.value)
Tuple2(
ArgumentDeclaration.parse('@function $name(${entry.key}) {',
url: url),
entry.value)
];
BuiltInCallable._(this.name, this._overloads);

View File

@ -4,6 +4,7 @@
import 'dart:collection';
import 'ast/node.dart';
import 'ast/sass.dart';
import 'configured_value.dart';
import 'util/limited_map_view.dart';
@ -19,6 +20,11 @@ class Configuration {
Map<String, ConfiguredValue> get values => UnmodifiableMapView(_values);
final Map<String, ConfiguredValue> _values;
/// The node whose span indicates where the configuration was declared.
///
/// This is `null` for implicit configurations.
final AstNode nodeWithSpan;
/// Whether or not this configuration is implicit.
///
/// Implicit configurations are created when a file containing a `@forward`
@ -31,8 +37,18 @@ class Configuration {
/// silently ignored in this case.
final bool isImplicit;
Configuration(Map<String, ConfiguredValue> values, {this.isImplicit = false})
: _values = values;
/// Creates an explicit configuration with the given [values].
Configuration(Map<String, ConfiguredValue> values, this.nodeWithSpan)
: _values = values,
isImplicit = false;
/// Creates an implicit configuration with the given [values].
///
/// See [isImplicit] for details.
Configuration.implicit(Map<String, ConfiguredValue> values)
: _values = values,
nodeWithSpan = null,
isImplicit = true;
/// The empty configuration, which indicates that the module has not been
/// configured.
@ -41,6 +57,7 @@ class Configuration {
/// ignored if the module has already been loaded.
const Configuration.empty()
: _values = const {},
nodeWithSpan = null,
isImplicit = true;
bool get isEmpty => values.isEmpty;
@ -67,6 +84,8 @@ class Configuration {
} else if (forward.hiddenVariables?.isNotEmpty ?? false) {
newValues = LimitedMapView.blocklist(newValues, forward.hiddenVariables);
}
return Configuration(newValues, isImplicit: isImplicit);
return isImplicit
? Configuration.implicit(newValues)
: Configuration(newValues, nodeWithSpan);
}
}

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: 7da67a8956ec74db270764e941b674dcc315a488
// Checksum: b497eab76eb15ba7bfc4f1cecf71ff9f9c1fb2a5
//
// ignore_for_file: unused_import
@ -41,16 +41,32 @@ class Environment {
Map<String, Module<Callable>> get modules => UnmodifiableMapView(_modules);
final Map<String, Module<Callable>> _modules;
/// A map from module namespaces to the nodes whose spans indicate where those
/// modules were originally loaded.
final Map<String, AstNode> _namespaceNodes;
/// The namespaceless modules used in the current scope.
///
/// This is `null` if there are no namespaceless modules.
Set<Module<Callable>> _globalModules;
/// A map from modules in [_globalModules] to the nodes whose spans
/// indicate where those modules were originally loaded.
///
/// This is `null` if there are no namespaceless modules.
Map<Module<Callable>, AstNode> _globalModuleNodes;
/// The modules forwarded by this module.
///
/// This is `null` if there are no forwarded modules.
List<Module<Callable>> _forwardedModules;
/// A map from modules in [_forwardedModules] to the nodes whose spans
/// indicate where those modules were originally forwarded.
///
/// This is `null` if there are no forwarded modules.
Map<Module<Callable>, AstNode> _forwardedModuleNodes;
/// Modules forwarded by nested imports at each lexical scope level *beneath
/// the global scope*.
///
@ -142,8 +158,11 @@ class Environment {
/// If [sourceMap] is `true`, this tracks variables' source locations
Environment({bool sourceMap = false})
: _modules = {},
_namespaceNodes = {},
_globalModules = null,
_globalModuleNodes = null,
_forwardedModules = null,
_forwardedModuleNodes = null,
_nestedForwardedModules = null,
_allModules = [],
_variables = [{}],
@ -156,8 +175,11 @@ class Environment {
Environment._(
this._modules,
this._namespaceNodes,
this._globalModules,
this._globalModuleNodes,
this._forwardedModules,
this._forwardedModuleNodes,
this._nestedForwardedModules,
this._allModules,
this._variables,
@ -180,8 +202,11 @@ class Environment {
/// when the closure was created will be reflected.
Environment closure() => Environment._(
_modules,
_namespaceNodes,
_globalModules,
_globalModuleNodes,
_forwardedModules,
_forwardedModuleNodes,
_nestedForwardedModules,
_allModules,
_variables.toList(),
@ -196,6 +221,9 @@ class Environment {
/// functions, and mixins, but not its modules.
Environment global() => Environment._(
{},
{},
null,
null,
null,
null,
null,
@ -208,16 +236,21 @@ class Environment {
/// Adds [module] to the set of modules visible in this environment.
///
/// [nodeWithSpan]'s span is used to report any errors with the module.
///
/// If [namespace] is passed, the module is made available under that
/// namespace.
///
/// Throws a [SassScriptException] if there's already a module with the given
/// [namespace], or if [namespace] is `null` and [module] defines a variable
/// with the same name as a variable defined in this environment.
void addModule(Module<Callable> module, {String namespace}) {
void addModule(Module<Callable> module, AstNode nodeWithSpan,
{String namespace}) {
if (namespace == null) {
_globalModules ??= {};
_globalModuleNodes ??= {};
_globalModules.add(module);
_globalModuleNodes[module] = nodeWithSpan;
_allModules.add(module);
for (var name in _variables.first.keys) {
@ -229,11 +262,14 @@ class Environment {
}
} else {
if (_modules.containsKey(namespace)) {
throw SassScriptException(
"There's already a module with namespace \"$namespace\".");
throw MultiSpanSassScriptException(
"There's already a module with namespace \"$namespace\".",
"new @use",
{_namespaceNodes[namespace].span: "original @use"});
}
_modules[namespace] = module;
_namespaceNodes[namespace] = nodeWithSpan;
_allModules.add(module);
}
}
@ -242,12 +278,15 @@ class Environment {
/// defined in this module, according to the modifications defined by [rule].
void forwardModule(Module<Callable> module, ForwardRule rule) {
_forwardedModules ??= [];
_forwardedModuleNodes ??= {};
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);
_assertNoConflicts(
view.variables, other.variables, "variable", other, rule);
_assertNoConflicts(
view.functions, other.functions, "function", other, rule);
_assertNoConflicts(view.mixins, other.mixins, "mixin", other, rule);
}
// Add the original module to [_allModules] (rather than the
@ -256,14 +295,19 @@ class Environment {
// CSS, not for the members they expose.
_allModules.add(module);
_forwardedModules.add(view);
_forwardedModuleNodes[view] = rule;
}
/// Throws a [SassScriptException] if [newMembers] has any keys that overlap
/// with [oldMembers].
///
/// The [type] and [oldModule] is used for error reporting.
void _assertNoConflicts(Map<String, Object> newMembers,
Map<String, Object> oldMembers, String type, Module<Callable> oldModule) {
/// The [type], [other], [newModuleNodeWithSpan] are used for error reporting.
void _assertNoConflicts(
Map<String, Object> newMembers,
Map<String, Object> oldMembers,
String type,
Module<Callable> other,
AstNode newModuleNodeWithSpan) {
Map<String, Object> smaller;
Map<String, Object> larger;
if (newMembers.length < oldMembers.length) {
@ -277,9 +321,10 @@ class Environment {
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.');
throw MultiSpanSassScriptException(
'Two forwarded modules both define a $type named $name.',
"new @forward",
{_forwardedModuleNodes[other].span: "original @forward"});
}
}
}
@ -294,7 +339,9 @@ class Environment {
if (forwarded == null) return;
_globalModules ??= {};
_globalModuleNodes ??= {};
_forwardedModules ??= [];
_forwardedModuleNodes ??= {};
var forwardedVariableNames =
forwarded.expand((module) => module.variables.keys).toSet();
@ -314,6 +361,7 @@ class Environment {
if (shadowed != null) {
_globalModules.remove(module);
_globalModules.add(shadowed);
_globalModuleNodes[shadowed] = _globalModuleNodes.remove(module);
}
}
for (var i = 0; i < _forwardedModules.length; i++) {
@ -322,11 +370,17 @@ class Environment {
variables: forwardedVariableNames,
mixins: forwardedMixinNames,
functions: forwardedFunctionNames);
if (shadowed != null) _forwardedModules[i] = shadowed;
if (shadowed != null) {
_forwardedModules[i] = shadowed;
_forwardedModuleNodes[shadowed] =
_forwardedModuleNodes.remove(module);
}
}
_globalModules.addAll(forwarded);
_globalModuleNodes.addAll(module._environment._forwardedModuleNodes);
_forwardedModules.addAll(forwarded);
_forwardedModuleNodes.addAll(module._environment._forwardedModuleNodes);
} else {
_nestedForwardedModules ??=
List.generate(_variables.length - 1, (_) => []);
@ -750,7 +804,7 @@ class Environment {
configuration[name] = ConfiguredValue(values[name], null, nodes[name]);
}
}
return Configuration(configuration, isImplicit: true);
return Configuration.implicit(configuration);
}
/// Returns a module that represents the top-level members defined in [this],
@ -799,11 +853,12 @@ class Environment {
if (valueInModule == null) continue;
if (value != null) {
throw SassScriptException(
'This $type is available from multiple global modules:\n' +
bulletedList(_globalModules
.where((module) => callback(module) != null)
.map((module) => p.prettyUri(module.url))));
throw MultiSpanSassScriptException(
'This $type is available from multiple global modules.',
'$type use', {
for (var entry in _globalModuleNodes.entries)
if (callback(entry.key) != null) entry.value.span: 'includes $type'
});
}
value = valueInModule;

View File

@ -77,6 +77,43 @@ body::before {
}
}
/// A [SassException] that's also a [MultiSpanSassException].
class MultiSpanSassException extends SassException
implements MultiSourceSpanException {
final String primaryLabel;
final Map<FileSpan, String> secondarySpans;
MultiSpanSassException(String message, FileSpan span, this.primaryLabel,
Map<FileSpan, String> secondarySpans)
: secondarySpans = Map.unmodifiable(secondarySpans),
super(message, span);
String toString({Object color, String secondaryColor}) {
var useColor = false;
String primaryColor;
if (color is String) {
useColor = true;
primaryColor = color;
} else if (color == true) {
useColor = true;
}
var buffer = StringBuffer()
..writeln("Error: $message")
..write(span.highlightMultiple(primaryLabel, secondarySpans,
color: useColor,
primaryColor: primaryColor,
secondaryColor: secondaryColor));
for (var frame in trace.toString().split("\n")) {
if (frame.isEmpty) continue;
buffer.writeln();
buffer.write(" $frame");
}
return buffer.toString();
}
}
/// An exception thrown by Sass while evaluating a stylesheet.
class SassRuntimeException extends SassException {
final Trace trace;
@ -85,6 +122,16 @@ class SassRuntimeException extends SassException {
: super(message, span);
}
/// A [SassRuntimeException] that's also a [MultiSpanSassException].
class MultiSpanSassRuntimeException extends MultiSpanSassException
implements SassRuntimeException {
final Trace trace;
MultiSpanSassRuntimeException(String message, FileSpan span,
String primaryLabel, Map<FileSpan, String> secondarySpans, this.trace)
: super(message, span, primaryLabel, secondarySpans);
}
/// An exception thrown when Sass parsing has failed.
class SassFormatException extends SassException
implements SourceSpanFormatException {
@ -108,3 +155,18 @@ class SassScriptException {
String toString() => "$message\n\nBUG: This should include a source span!";
}
/// A [SassScriptException] that contains one or more additional spans to
/// display as points of reference.
class MultiSpanSassScriptException extends SassScriptException {
/// See [MultiSourceSpanException.primaryLabel].
final String primaryLabel;
/// See [MultiSourceSpanException.secondarySpans].
final Map<FileSpan, String> secondarySpans;
MultiSpanSassScriptException(
String message, this.primaryLabel, Map<FileSpan, String> secondarySpans)
: secondarySpans = Map.unmodifiable(secondarySpans),
super(message);
}

View File

@ -30,7 +30,7 @@ final List<BuiltInCallable> globalFunctions = UnmodifiableListView([
// This is only invoked using `call()`. Hand-authored `if()`s are parsed as
// [IfExpression]s.
BuiltInCallable("if", r"$condition, $if-true, $if-false",
BuiltInCallable.function("if", r"$condition, $if-true, $if-false",
(arguments) => arguments[0].isTruthy ? arguments[1] : arguments[2]),
]);

View File

@ -23,7 +23,7 @@ final global = UnmodifiableListView([
// ### RGB
_red, _green, _blue, _mix,
BuiltInCallable.overloaded("rgb", {
BuiltInCallable.overloadedFunction("rgb", {
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgb", arguments),
r"$red, $green, $blue": (arguments) => _rgb("rgb", arguments),
r"$color, $alpha": (arguments) => _rgbTwoArg("rgb", arguments),
@ -34,7 +34,7 @@ final global = UnmodifiableListView([
}
}),
BuiltInCallable.overloaded("rgba", {
BuiltInCallable.overloadedFunction("rgba", {
r"$red, $green, $blue, $alpha": (arguments) => _rgb("rgba", arguments),
r"$red, $green, $blue": (arguments) => _rgb("rgba", arguments),
r"$color, $alpha": (arguments) => _rgbTwoArg("rgba", arguments),
@ -47,7 +47,7 @@ final global = UnmodifiableListView([
}
}),
BuiltInCallable("invert", r"$color, $weight: 100%", (arguments) {
_function("invert", r"$color, $weight: 100%", (arguments) {
var weight = arguments[1].assertNumber("weight");
if (arguments[0] is SassNumber) {
if (weight.value != 100 || !weight.hasUnit("%")) {
@ -68,7 +68,7 @@ final global = UnmodifiableListView([
// ### HSL
_hue, _saturation, _lightness, _complement,
BuiltInCallable.overloaded("hsl", {
BuiltInCallable.overloadedFunction("hsl", {
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
_hsl("hsl", arguments),
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsl", arguments),
@ -88,7 +88,7 @@ final global = UnmodifiableListView([
}
}),
BuiltInCallable.overloaded("hsla", {
BuiltInCallable.overloadedFunction("hsla", {
r"$hue, $saturation, $lightness, $alpha": (arguments) =>
_hsl("hsla", arguments),
r"$hue, $saturation, $lightness": (arguments) => _hsl("hsla", arguments),
@ -108,7 +108,7 @@ final global = UnmodifiableListView([
}
}),
BuiltInCallable("grayscale", r"$color", (arguments) {
_function("grayscale", r"$color", (arguments) {
if (arguments[0] is SassNumber) {
return _functionString('grayscale', arguments);
}
@ -117,13 +117,13 @@ final global = UnmodifiableListView([
return color.changeHsl(saturation: 0);
}),
BuiltInCallable("adjust-hue", r"$color, $degrees", (arguments) {
_function("adjust-hue", r"$color, $degrees", (arguments) {
var color = arguments[0].assertColor("color");
var degrees = arguments[1].assertNumber("degrees");
return color.changeHsl(hue: color.hue + degrees.value);
}),
BuiltInCallable("lighten", r"$color, $amount", (arguments) {
_function("lighten", r"$color, $amount", (arguments) {
var color = arguments[0].assertColor("color");
var amount = arguments[1].assertNumber("amount");
return color.changeHsl(
@ -131,7 +131,7 @@ final global = UnmodifiableListView([
.clamp(0, 100));
}),
BuiltInCallable("darken", r"$color, $amount", (arguments) {
_function("darken", r"$color, $amount", (arguments) {
var color = arguments[0].assertColor("color");
var amount = arguments[1].assertNumber("amount");
return color.changeHsl(
@ -139,7 +139,7 @@ final global = UnmodifiableListView([
.clamp(0, 100));
}),
BuiltInCallable.overloaded("saturate", {
BuiltInCallable.overloadedFunction("saturate", {
r"$amount": (arguments) {
var number = arguments[0].assertNumber("amount");
return SassString("saturate(${number.toCssString()})", quotes: false);
@ -153,7 +153,7 @@ final global = UnmodifiableListView([
}
}),
BuiltInCallable("desaturate", r"$color, $amount", (arguments) {
_function("desaturate", r"$color, $amount", (arguments) {
var color = arguments[0].assertColor("color");
var amount = arguments[1].assertNumber("amount");
return color.changeHsl(
@ -162,12 +162,12 @@ final global = UnmodifiableListView([
}),
// ### Opacity
BuiltInCallable("opacify", r"$color, $amount", _opacify),
BuiltInCallable("fade-in", r"$color, $amount", _opacify),
BuiltInCallable("transparentize", r"$color, $amount", _transparentize),
BuiltInCallable("fade-out", r"$color, $amount", _transparentize),
_function("opacify", r"$color, $amount", _opacify),
_function("fade-in", r"$color, $amount", _opacify),
_function("transparentize", r"$color, $amount", _transparentize),
_function("fade-out", r"$color, $amount", _transparentize),
BuiltInCallable.overloaded("alpha", {
BuiltInCallable.overloadedFunction("alpha", {
r"$color": (arguments) {
var argument = arguments[0];
if (argument is SassString &&
@ -201,7 +201,7 @@ final global = UnmodifiableListView([
}
}),
BuiltInCallable("opacity", r"$color", (arguments) {
_function("opacity", r"$color", (arguments) {
if (arguments[0] is SassNumber) {
return _functionString("opacity", arguments);
}
@ -222,7 +222,7 @@ final module = BuiltInModule("color", functions: [
// ### RGB
_red, _green, _blue, _mix,
BuiltInCallable("invert", r"$color, $weight: 100%", (arguments) {
_function("invert", r"$color, $weight: 100%", (arguments) {
var weight = arguments[1].assertNumber("weight");
if (arguments[0] is SassNumber) {
if (weight.value != 100 || !weight.hasUnit("%")) {
@ -252,7 +252,7 @@ final module = BuiltInModule("color", functions: [
_removedColorFunction("saturate", "saturation"),
_removedColorFunction("desaturate", "saturation", negative: true),
BuiltInCallable("grayscale", r"$color", (arguments) {
_function("grayscale", r"$color", (arguments) {
if (arguments[0] is SassNumber) {
var result = _functionString("grayscale", arguments.take(1));
warn("Passing a number to color.grayscale() is deprecated.\n"
@ -271,7 +271,7 @@ final module = BuiltInModule("color", functions: [
_removedColorFunction("transparentize", "alpha", negative: true),
_removedColorFunction("fade-out", "alpha", negative: true),
BuiltInCallable.overloaded("alpha", {
BuiltInCallable.overloadedFunction("alpha", {
r"$color": (arguments) {
var argument = arguments[0];
if (argument is SassString &&
@ -306,7 +306,7 @@ final module = BuiltInModule("color", functions: [
}
}),
BuiltInCallable("opacity", r"$color", (arguments) {
_function("opacity", r"$color", (arguments) {
if (arguments[0] is SassNumber) {
var result = _functionString("opacity", arguments);
warn("Passing a number to color.opacity() is deprecated.\n"
@ -325,20 +325,19 @@ final module = BuiltInModule("color", functions: [
// ### RGB
final _red = BuiltInCallable("red", r"$color", (arguments) {
final _red = _function("red", r"$color", (arguments) {
return SassNumber(arguments.first.assertColor("color").red);
});
final _green = BuiltInCallable("green", r"$color", (arguments) {
final _green = _function("green", r"$color", (arguments) {
return SassNumber(arguments.first.assertColor("color").green);
});
final _blue = BuiltInCallable("blue", r"$color", (arguments) {
final _blue = _function("blue", r"$color", (arguments) {
return SassNumber(arguments.first.assertColor("color").blue);
});
final _mix =
BuiltInCallable("mix", r"$color1, $color2, $weight: 50%", (arguments) {
final _mix = _function("mix", r"$color1, $color2, $weight: 50%", (arguments) {
var color1 = arguments[0].assertColor("color1");
var color2 = arguments[1].assertColor("color2");
var weight = arguments[2].assertNumber("weight");
@ -347,29 +346,29 @@ final _mix =
// ### HSL
final _hue = BuiltInCallable("hue", r"$color",
final _hue = _function("hue", r"$color",
(arguments) => SassNumber(arguments.first.assertColor("color").hue, "deg"));
final _saturation = BuiltInCallable(
final _saturation = _function(
"saturation",
r"$color",
(arguments) =>
SassNumber(arguments.first.assertColor("color").saturation, "%"));
final _lightness = BuiltInCallable(
final _lightness = _function(
"lightness",
r"$color",
(arguments) =>
SassNumber(arguments.first.assertColor("color").lightness, "%"));
final _complement = BuiltInCallable("complement", r"$color", (arguments) {
final _complement = _function("complement", r"$color", (arguments) {
var color = arguments[0].assertColor("color");
return color.changeHsl(hue: color.hue + 180);
});
// Miscellaneous
final _adjust = BuiltInCallable("adjust", r"$color, $kwargs...", (arguments) {
final _adjust = _function("adjust", r"$color, $kwargs...", (arguments) {
var color = arguments[0].assertColor("color");
var argumentList = arguments[1] as SassArgumentList;
if (argumentList.asList.isNotEmpty) {
@ -422,7 +421,7 @@ final _adjust = BuiltInCallable("adjust", r"$color, $kwargs...", (arguments) {
}
});
final _scale = BuiltInCallable("scale", r"$color, $kwargs...", (arguments) {
final _scale = _function("scale", r"$color, $kwargs...", (arguments) {
var color = arguments[0].assertColor("color");
var argumentList = arguments[1] as SassArgumentList;
if (argumentList.asList.isNotEmpty) {
@ -483,7 +482,7 @@ final _scale = BuiltInCallable("scale", r"$color, $kwargs...", (arguments) {
}
});
final _change = BuiltInCallable("change", r"$color, $kwargs...", (arguments) {
final _change = _function("change", r"$color, $kwargs...", (arguments) {
var color = arguments[0].assertColor("color");
var argumentList = arguments[1] as SassArgumentList;
if (argumentList.asList.isNotEmpty) {
@ -529,7 +528,7 @@ final _change = BuiltInCallable("change", r"$color, $kwargs...", (arguments) {
}
});
final _ieHexStr = BuiltInCallable("ie-hex-str", r"$color", (arguments) {
final _ieHexStr = _function("ie-hex-str", r"$color", (arguments) {
var color = arguments[0].assertColor("color");
String hexString(int component) =>
component.toRadixString(16).padLeft(2, '0').toUpperCase();
@ -548,14 +547,14 @@ SassString _functionString(String name, Iterable<Value> arguments) =>
")",
quotes: false);
/// Returns a [BuiltInCallable] that throws an error indicating that
/// Returns a [_function] that throws an error indicating that
/// `color.adjust()` should be used instead.
///
/// This prints a suggested `color.adjust()` call that passes the adjustment
/// value to [argument], with a leading minus sign if [negative] is `true`.
BuiltInCallable _removedColorFunction(String name, String argument,
{bool negative = false}) =>
BuiltInCallable(name, r"$color, $amount", (arguments) {
_function(name, r"$color, $amount", (arguments) {
throw SassScriptException(
"The function $name() isn't in the sass:color module.\n"
"\n"
@ -776,3 +775,9 @@ SassColor _transparentize(List<Value> arguments) {
/// Like [fuzzyRound], but returns `null` if [number] is `null`.
int _fuzzyRoundOrNull(num number) => number == null ? null : fuzzyRound(number);
/// Like [new BuiltInCallable.function], but always sets the URL to
/// `sass:color`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:color");

View File

@ -23,16 +23,16 @@ final module = BuiltInModule("list", functions: [
_separator
]);
final _length = BuiltInCallable(
final _length = _function(
"length", r"$list", (arguments) => SassNumber(arguments[0].asList.length));
final _nth = BuiltInCallable("nth", r"$list, $n", (arguments) {
final _nth = _function("nth", r"$list, $n", (arguments) {
var list = arguments[0];
var index = arguments[1];
return list.asList[list.sassIndexToListIndex(index, "n")];
});
final _setNth = BuiltInCallable("set-nth", r"$list, $n, $value", (arguments) {
final _setNth = _function("set-nth", r"$list, $n, $value", (arguments) {
var list = arguments[0];
var index = arguments[1];
var value = arguments[2];
@ -41,7 +41,7 @@ final _setNth = BuiltInCallable("set-nth", r"$list, $n, $value", (arguments) {
return arguments[0].changeListContents(newList);
});
final _join = BuiltInCallable(
final _join = _function(
"join", r"$list1, $list2, $separator: auto, $bracketed: auto", (arguments) {
var list1 = arguments[0];
var list2 = arguments[1];
@ -75,7 +75,7 @@ final _join = BuiltInCallable(
});
final _append =
BuiltInCallable("append", r"$list, $val, $separator: auto", (arguments) {
_function("append", r"$list, $val, $separator: auto", (arguments) {
var list = arguments[0];
var value = arguments[1];
var separatorParam = arguments[2].assertString("separator");
@ -98,7 +98,7 @@ final _append =
return list.changeListContents(newList, separator: separator);
});
final _zip = BuiltInCallable("zip", r"$lists...", (arguments) {
final _zip = _function("zip", r"$lists...", (arguments) {
var lists = arguments[0].asList.map((list) => list.asList).toList();
if (lists.isEmpty) {
return const SassList.empty(separator: ListSeparator.comma);
@ -113,7 +113,7 @@ final _zip = BuiltInCallable("zip", r"$lists...", (arguments) {
return SassList(results, ListSeparator.comma);
});
final _index = BuiltInCallable("index", r"$list, $value", (arguments) {
final _index = _function("index", r"$list, $value", (arguments) {
var list = arguments[0].asList;
var value = arguments[1];
@ -121,12 +121,17 @@ final _index = BuiltInCallable("index", r"$list, $value", (arguments) {
return index == -1 ? sassNull : SassNumber(index + 1);
});
final _separator = BuiltInCallable(
final _separator = _function(
"separator",
r"$list",
(arguments) => arguments[0].separator == ListSeparator.comma
? SassString("comma", quotes: false)
: SassString("space", quotes: false));
final _isBracketed = BuiltInCallable("is-bracketed", r"$list",
final _isBracketed = _function("is-bracketed", r"$list",
(arguments) => SassBoolean(arguments[0].hasBrackets));
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:list`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:list");

View File

@ -24,19 +24,19 @@ final global = UnmodifiableListView([
final module = BuiltInModule("map",
functions: [_get, _merge, _remove, _keys, _values, _hasKey]);
final _get = BuiltInCallable("get", r"$map, $key", (arguments) {
final _get = _function("get", r"$map, $key", (arguments) {
var map = arguments[0].assertMap("map");
var key = arguments[1];
return map.contents[key] ?? sassNull;
});
final _merge = BuiltInCallable("merge", r"$map1, $map2", (arguments) {
final _merge = _function("merge", r"$map1, $map2", (arguments) {
var map1 = arguments[0].assertMap("map1");
var map2 = arguments[1].assertMap("map2");
return SassMap({...map1.contents, ...map2.contents});
});
final _remove = BuiltInCallable.overloaded("remove", {
final _remove = BuiltInCallable.overloadedFunction("remove", {
// Because the signature below has an explicit `$key` argument, it doesn't
// allow zero keys to be passed. We want to allow that case, so we add an
// explicit overload for it.
@ -55,20 +55,25 @@ final _remove = BuiltInCallable.overloaded("remove", {
}
});
final _keys = BuiltInCallable(
final _keys = _function(
"keys",
r"$map",
(arguments) => SassList(
arguments[0].assertMap("map").contents.keys, ListSeparator.comma));
final _values = BuiltInCallable(
final _values = _function(
"values",
r"$map",
(arguments) => SassList(
arguments[0].assertMap("map").contents.values, ListSeparator.comma));
final _hasKey = BuiltInCallable("has-key", r"$map, $key", (arguments) {
final _hasKey = _function("has-key", r"$map, $key", (arguments) {
var map = arguments[0].assertMap("map");
var key = arguments[1];
return SassBoolean(map.contents.containsKey(key));
});
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:map`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:map");

View File

@ -31,24 +31,13 @@ final module = BuiltInModule("math", functions: [
"pi": SassNumber(math.pi),
});
/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, num transform(num value)) {
return BuiltInCallable(name, r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassNumber.withUnits(transform(number.value),
numeratorUnits: number.numeratorUnits,
denominatorUnits: number.denominatorUnits);
});
}
///
/// Bounding functions
///
final _ceil = _numberFunction("ceil", (value) => value.ceil());
final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
final _clamp = _function("clamp", r"$min, $number, $max", (arguments) {
var min = arguments[0].assertNumber("min");
var number = arguments[1].assertNumber("number");
var max = arguments[2].assertNumber("max");
@ -70,7 +59,7 @@ final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
final _floor = _numberFunction("floor", (value) => value.floor());
final _max = BuiltInCallable("max", r"$numbers...", (arguments) {
final _max = _function("max", r"$numbers...", (arguments) {
SassNumber max;
for (var value in arguments[0].asList) {
var number = value.assertNumber();
@ -80,7 +69,7 @@ final _max = BuiltInCallable("max", r"$numbers...", (arguments) {
throw SassScriptException("At least one argument must be passed.");
});
final _min = BuiltInCallable("min", r"$numbers...", (arguments) {
final _min = _function("min", r"$numbers...", (arguments) {
SassNumber min;
for (var value in arguments[0].asList) {
var number = value.assertNumber();
@ -98,7 +87,7 @@ final _round = _numberFunction("round", fuzzyRound);
final _abs = _numberFunction("abs", (value) => value.abs());
final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
final _hypot = _function("hypot", r"$numbers...", (arguments) {
var numbers =
arguments[0].asList.map((argument) => argument.assertNumber()).toList();
if (numbers.isEmpty) {
@ -132,13 +121,13 @@ final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
/// Exponential functions
///
final _log = BuiltInCallable("log", r"$number, $base: null", (arguments) {
final _log = _function("log", r"$number, $base: null", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
}
var numberValue = fuzzyRoundIfZero(number.value);
var numberValue = _fuzzyRoundIfZero(number.value);
if (arguments[1] == sassNull) return SassNumber(math.log(numberValue));
var base = arguments[1].assertNumber("base");
@ -148,11 +137,11 @@ final _log = BuiltInCallable("log", r"$number, $base: null", (arguments) {
var baseValue = fuzzyEquals(base.value, 1)
? fuzzyRound(base.value)
: fuzzyRoundIfZero(base.value);
: _fuzzyRoundIfZero(base.value);
return SassNumber(math.log(numberValue) / math.log(baseValue));
});
final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
final _pow = _function("pow", r"$base, $exponent", (arguments) {
var base = arguments[0].assertNumber("base");
var exponent = arguments[1].assertNumber("exponent");
if (base.hasUnits) {
@ -164,8 +153,8 @@ final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
// Exponentiating certain real numbers leads to special behaviors. Ensure that
// these behaviors are consistent for numbers within the precision limit.
var baseValue = fuzzyRoundIfZero(base.value);
var exponentValue = fuzzyRoundIfZero(exponent.value);
var baseValue = _fuzzyRoundIfZero(base.value);
var exponentValue = _fuzzyRoundIfZero(exponent.value);
if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
return SassNumber(double.nan);
} else if (fuzzyEquals(baseValue, 0)) {
@ -189,26 +178,21 @@ final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
return SassNumber(math.pow(baseValue, exponentValue));
});
final _sqrt = BuiltInCallable("sqrt", r"$number", (arguments) {
final _sqrt = _function("sqrt", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
}
var numberValue = fuzzyRoundIfZero(number.value);
var numberValue = _fuzzyRoundIfZero(number.value);
return SassNumber(math.sqrt(numberValue));
});
num fuzzyRoundIfZero(num number) {
if (!fuzzyEquals(number, 0)) return number;
return number.isNegative ? -0.0 : 0;
}
///
/// Trigonometric functions
///
final _acos = BuiltInCallable("acos", r"$number", (arguments) {
final _acos = _function("acos", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
@ -221,7 +205,7 @@ final _acos = BuiltInCallable("acos", r"$number", (arguments) {
return SassNumber.withUnits(acos, numeratorUnits: ['deg']);
});
final _asin = BuiltInCallable("asin", r"$number", (arguments) {
final _asin = _function("asin", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
@ -229,23 +213,23 @@ final _asin = BuiltInCallable("asin", r"$number", (arguments) {
var numberValue = fuzzyEquals(number.value.abs(), 1)
? fuzzyRound(number.value)
: fuzzyRoundIfZero(number.value);
: _fuzzyRoundIfZero(number.value);
var asin = math.asin(numberValue) * 180 / math.pi;
return SassNumber.withUnits(asin, numeratorUnits: ['deg']);
});
final _atan = BuiltInCallable("atan", r"$number", (arguments) {
final _atan = _function("atan", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
}
var numberValue = fuzzyRoundIfZero(number.value);
var numberValue = _fuzzyRoundIfZero(number.value);
var atan = math.atan(numberValue) * 180 / math.pi;
return SassNumber.withUnits(atan, numeratorUnits: ['deg']);
});
final _atan2 = BuiltInCallable("atan2", r"$y, $x", (arguments) {
final _atan2 = _function("atan2", r"$y, $x", (arguments) {
var y = arguments[0].assertNumber("y");
var x = arguments[1].assertNumber("x");
if (y.hasUnits != x.hasUnits) {
@ -257,24 +241,24 @@ final _atan2 = BuiltInCallable("atan2", r"$y, $x", (arguments) {
}
x = x.coerce(y.numeratorUnits, y.denominatorUnits);
var xValue = fuzzyRoundIfZero(x.value);
var yValue = fuzzyRoundIfZero(y.value);
var xValue = _fuzzyRoundIfZero(x.value);
var yValue = _fuzzyRoundIfZero(y.value);
var atan2 = math.atan2(yValue, xValue) * 180 / math.pi;
return SassNumber.withUnits(atan2, numeratorUnits: ['deg']);
});
final _cos = BuiltInCallable("cos", r"$number", (arguments) {
final _cos = _function("cos", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
return SassNumber(math.cos(number.value));
});
final _sin = BuiltInCallable("sin", r"$number", (arguments) {
final _sin = _function("sin", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
var numberValue = fuzzyRoundIfZero(number.value);
var numberValue = _fuzzyRoundIfZero(number.value);
return SassNumber(math.sin(numberValue));
});
final _tan = BuiltInCallable("tan", r"$number", (arguments) {
final _tan = _function("tan", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
var asymptoteInterval = 0.5 * math.pi;
var tanPeriod = 2 * math.pi;
@ -283,11 +267,61 @@ final _tan = BuiltInCallable("tan", r"$number", (arguments) {
} else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) {
return SassNumber(double.negativeInfinity);
} else {
var numberValue = fuzzyRoundIfZero(number.value);
var numberValue = _fuzzyRoundIfZero(number.value);
return SassNumber(math.tan(numberValue));
}
});
///
/// Unit functions
///
final _compatible = _function("compatible", r"$number1, $number2", (arguments) {
var number1 = arguments[0].assertNumber("number1");
var number2 = arguments[1].assertNumber("number2");
return SassBoolean(number1.isComparableTo(number2));
});
final _isUnitless = _function("is-unitless", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassBoolean(!number.hasUnits);
});
final _unit = _function("unit", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassString(number.unitString, quotes: true);
});
///
/// Other functions
///
final _percentage = _function("percentage", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
number.assertNoUnits("number");
return SassNumber(number.value * 100, '%');
});
final _random = math.Random();
final _randomFunction = _function("random", r"$limit: null", (arguments) {
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
var limit = arguments[0].assertNumber("limit").assertInt("limit");
if (limit < 1) {
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
}
return SassNumber(_random.nextInt(limit) + 1);
});
///
/// Helpers
///
num _fuzzyRoundIfZero(num number) {
if (!fuzzyEquals(number, 0)) return number;
return number.isNegative ? -0.0 : 0;
}
SassNumber _coerceToRad(SassNumber number) {
try {
return number.coerce(['rad'], []);
@ -297,44 +331,18 @@ SassNumber _coerceToRad(SassNumber number) {
}
}
///
/// Unit functions
///
/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, num transform(num value)) {
return _function(name, r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassNumber.withUnits(transform(number.value),
numeratorUnits: number.numeratorUnits,
denominatorUnits: number.denominatorUnits);
});
}
final _compatible =
BuiltInCallable("compatible", r"$number1, $number2", (arguments) {
var number1 = arguments[0].assertNumber("number1");
var number2 = arguments[1].assertNumber("number2");
return SassBoolean(number1.isComparableTo(number2));
});
final _isUnitless = BuiltInCallable("is-unitless", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassBoolean(!number.hasUnits);
});
final _unit = BuiltInCallable("unit", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
return SassString(number.unitString, quotes: true);
});
///
/// Other functions
///
final _percentage = BuiltInCallable("percentage", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
number.assertNoUnits("number");
return SassNumber(number.value * 100, '%');
});
final _random = math.Random();
final _randomFunction = BuiltInCallable("random", r"$limit: null", (arguments) {
if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
var limit = arguments[0].assertNumber("limit").assertInt("limit");
if (limit < 1) {
throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
}
return SassNumber(_random.nextInt(limit) + 1);
});
/// Like [new _function.function], but always sets the URL to `sass:math`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:math");

View File

@ -23,15 +23,15 @@ final global = UnmodifiableListView([
// This is only a partial list of meta functions. The rest are defined in the
// evaluator, because they need access to context that's only available at
// runtime.
BuiltInCallable("feature-exists", r"$feature", (arguments) {
_function("feature-exists", r"$feature", (arguments) {
var feature = arguments[0].assertString("feature");
return SassBoolean(_features.contains(feature.text));
}),
BuiltInCallable("inspect", r"$value",
_function("inspect", r"$value",
(arguments) => SassString(arguments.first.toString(), quotes: false)),
BuiltInCallable("type-of", r"$value", (arguments) {
_function("type-of", r"$value", (arguments) {
var value = arguments[0];
if (value is SassArgumentList) {
return SassString("arglist", quotes: false);
@ -47,7 +47,7 @@ final global = UnmodifiableListView([
return SassString("string", quotes: false);
}),
BuiltInCallable("keywords", r"$args", (arguments) {
_function("keywords", r"$args", (arguments) {
var argumentList = arguments[0];
if (argumentList is SassArgumentList) {
return SassMap(mapMap(argumentList.keywords,
@ -57,3 +57,8 @@ final global = UnmodifiableListView([
}
})
]);
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:meta`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:meta");

View File

@ -37,7 +37,7 @@ final module = BuiltInModule("selector", functions: [
_unify
]);
final _nest = BuiltInCallable("nest", r"$selectors...", (arguments) {
final _nest = _function("nest", r"$selectors...", (arguments) {
var selectors = arguments[0].asList;
if (selectors.isEmpty) {
throw SassScriptException(
@ -50,7 +50,7 @@ final _nest = BuiltInCallable("nest", r"$selectors...", (arguments) {
.asSassList;
});
final _append = BuiltInCallable("append", r"$selectors...", (arguments) {
final _append = _function("append", r"$selectors...", (arguments) {
var selectors = arguments[0].asList;
if (selectors.isEmpty) {
throw SassScriptException(
@ -77,7 +77,7 @@ final _append = BuiltInCallable("append", r"$selectors...", (arguments) {
});
final _extend =
BuiltInCallable("extend", r"$selector, $extendee, $extender", (arguments) {
_function("extend", r"$selector, $extendee, $extender", (arguments) {
var selector = arguments[0].assertSelector(name: "selector");
var target = arguments[1].assertSelector(name: "extendee");
var source = arguments[2].assertSelector(name: "extender");
@ -85,8 +85,8 @@ final _extend =
return Extender.extend(selector, source, target).asSassList;
});
final _replace = BuiltInCallable(
"replace", r"$selector, $original, $replacement", (arguments) {
final _replace =
_function("replace", r"$selector, $original, $replacement", (arguments) {
var selector = arguments[0].assertSelector(name: "selector");
var target = arguments[1].assertSelector(name: "original");
var source = arguments[2].assertSelector(name: "replacement");
@ -94,7 +94,7 @@ final _replace = BuiltInCallable(
return Extender.replace(selector, source, target).asSassList;
});
final _unify = BuiltInCallable("unify", r"$selector1, $selector2", (arguments) {
final _unify = _function("unify", r"$selector1, $selector2", (arguments) {
var selector1 = arguments[0].assertSelector(name: "selector1");
var selector2 = arguments[1].assertSelector(name: "selector2");
@ -103,7 +103,7 @@ final _unify = BuiltInCallable("unify", r"$selector1, $selector2", (arguments) {
});
final _isSuperselector =
BuiltInCallable("is-superselector", r"$super, $sub", (arguments) {
_function("is-superselector", r"$super, $sub", (arguments) {
var selector1 = arguments[0].assertSelector(name: "super");
var selector2 = arguments[1].assertSelector(name: "sub");
@ -111,7 +111,7 @@ final _isSuperselector =
});
final _simpleSelectors =
BuiltInCallable("simple-selectors", r"$selector", (arguments) {
_function("simple-selectors", r"$selector", (arguments) {
var selector = arguments[0].assertCompoundSelector(name: "selector");
return SassList(
@ -120,7 +120,7 @@ final _simpleSelectors =
ListSeparator.comma);
});
final _parse = BuiltInCallable("parse", r"$selector",
final _parse = _function("parse", r"$selector",
(arguments) => arguments[0].assertSelector(name: "selector").asSassList);
/// Adds a [ParentSelector] to the beginning of [compound], or returns `null` if
@ -138,3 +138,9 @@ CompoundSelector _prependParent(CompoundSelector compound) {
return CompoundSelector([ParentSelector(), ...compound.components]);
}
}
/// Like [new BuiltInCallable.function], but always sets the URL to
/// `sass:selector`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:selector");

View File

@ -34,25 +34,24 @@ final module = BuiltInModule("string", functions: [
_slice, _uniqueId,
]);
final _unquote = BuiltInCallable("unquote", r"$string", (arguments) {
final _unquote = _function("unquote", r"$string", (arguments) {
var string = arguments[0].assertString("string");
if (!string.hasQuotes) return string;
return SassString(string.text, quotes: false);
});
final _quote = BuiltInCallable("quote", r"$string", (arguments) {
final _quote = _function("quote", r"$string", (arguments) {
var string = arguments[0].assertString("string");
if (string.hasQuotes) return string;
return SassString(string.text, quotes: true);
});
final _length = BuiltInCallable("length", r"$string", (arguments) {
final _length = _function("length", r"$string", (arguments) {
var string = arguments[0].assertString("string");
return SassNumber(string.sassLength);
});
final _insert =
BuiltInCallable("insert", r"$string, $insert, $index", (arguments) {
final _insert = _function("insert", r"$string, $insert, $index", (arguments) {
var string = arguments[0].assertString("string");
var insert = arguments[1].assertString("insert");
var index = arguments[2].assertNumber("index");
@ -78,7 +77,7 @@ final _insert =
quotes: string.hasQuotes);
});
final _index = BuiltInCallable("index", r"$string, $substring", (arguments) {
final _index = _function("index", r"$string, $substring", (arguments) {
var string = arguments[0].assertString("string");
var substring = arguments[1].assertString("substring");
@ -90,7 +89,7 @@ final _index = BuiltInCallable("index", r"$string, $substring", (arguments) {
});
final _slice =
BuiltInCallable("slice", r"$string, $start-at, $end-at: -1", (arguments) {
_function("slice", r"$string, $start-at, $end-at: -1", (arguments) {
var string = arguments[0].assertString("string");
var start = arguments[1].assertNumber("start-at");
var end = arguments[2].assertNumber("end-at");
@ -120,7 +119,7 @@ final _slice =
quotes: string.hasQuotes);
});
final _toUpperCase = BuiltInCallable("to-upper-case", r"$string", (arguments) {
final _toUpperCase = _function("to-upper-case", r"$string", (arguments) {
var string = arguments[0].assertString("string");
var buffer = StringBuffer();
for (var i = 0; i < string.text.length; i++) {
@ -129,7 +128,7 @@ final _toUpperCase = BuiltInCallable("to-upper-case", r"$string", (arguments) {
return SassString(buffer.toString(), quotes: string.hasQuotes);
});
final _toLowerCase = BuiltInCallable("to-lower-case", r"$string", (arguments) {
final _toLowerCase = _function("to-lower-case", r"$string", (arguments) {
var string = arguments[0].assertString("string");
var buffer = StringBuffer();
for (var i = 0; i < string.text.length; i++) {
@ -138,7 +137,7 @@ final _toLowerCase = BuiltInCallable("to-lower-case", r"$string", (arguments) {
return SassString(buffer.toString(), quotes: string.hasQuotes);
});
final _uniqueId = BuiltInCallable("unique-id", "", (arguments) {
final _uniqueId = _function("unique-id", "", (arguments) {
// Make it difficult to guess the next ID by randomizing the increase.
_previousUniqueId += _random.nextInt(36) + 1;
if (_previousUniqueId > math.pow(36, 6)) {
@ -168,3 +167,9 @@ int _codepointForIndex(int index, int lengthInCodepoints,
if (result < 0 && !allowNegative) return 0;
return result;
}
/// Like [new BuiltInCallable.function], but always sets the URL to
/// `sass:string`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:string");

View File

@ -101,8 +101,16 @@ abstract class StylesheetParser extends Parser {
});
}
ArgumentDeclaration parseArgumentDeclaration() =>
_parseSingleProduction(_argumentDeclaration);
ArgumentDeclaration parseArgumentDeclaration() => _parseSingleProduction(() {
scanner.expectChar($at, name: "@-rule");
identifier();
whitespace();
identifier();
var arguments = _argumentDeclaration();
whitespace();
scanner.expectChar($lbrace);
return arguments;
});
Expression parseExpression() => _parseSingleProduction(expression);
@ -1177,14 +1185,12 @@ abstract class StylesheetParser extends Parser {
ContentBlock content;
if (contentArguments != null || lookingAtChildren()) {
contentArguments ??= ArgumentDeclaration.empty(span: scanner.emptySpan);
var wasInContentBlock = _inContentBlock;
_inContentBlock = true;
content = _withChildren(_statement, start, (children, span) {
return ContentBlock(
contentArguments ??
ArgumentDeclaration.empty(span: scanner.emptySpan),
children,
span);
return ContentBlock(contentArguments, children, span);
});
_inContentBlock = wasInContentBlock;
} else {

View File

@ -411,3 +411,24 @@ Map<K1, Map<K2, V>> copyMapOfMap<K1, K2, V>(Map<K1, Map<K2, V>> map) =>
/// Returns a deep copy of a map that contains lists.
Map<K, List<E>> copyMapOfList<K, E>(Map<K, List<E>> map) =>
mapMap<K, List<E>, K, List<E>>(map, value: (_, list) => list.toList());
extension SpanExtensions on FileSpan {
/// Returns this span with all whitespace trimmed from both sides.
FileSpan trim() {
var text = this.text;
var start = 0;
while (isWhitespace(text.codeUnitAt(start))) {
start++;
}
var end = text.length - 1;
while (isWhitespace(text.codeUnitAt(end))) {
end--;
}
return start == 0 && end == text.length - 1
? this
: file.span(this.start.offset + start, this.start.offset + end + 1);
}
}

View File

@ -139,6 +139,13 @@ class _EvaluateVisitor
/// All modules that have been loaded and evaluated so far.
final _modules = <Uri, Module>{};
/// A map from canonical module URLs to the nodes whose spans indicate where
/// those modules were originally loaded.
///
/// This is not guaranteed to have a node for every module in [_modules]. For
/// example, the entrypoint module was not loaded by a node.
final _moduleNodes = <Uri, AstNode>{};
/// The logger to use to print warnings.
final Logger _logger;
@ -199,11 +206,16 @@ class _EvaluateVisitor
/// imports, it contains the URL passed to the `@import`.
final _includedFiles = <String>{};
/// The set of canonical URLs for modules (or imported files) that are
/// currently being evaluated.
/// A map from canonical URLs for modules (or imported files) that are
/// currently being evaluated to AST nodes whose spans indicate the original
/// loads for those modules.
///
/// Map values may be `null`, which indicates an active module that doesn't
/// have a source span associated with its original load (such as the
/// entrypoint module).
///
/// This is used to ensure that we don't get into an infinite load loop.
final _activeModules = <Uri>{};
final _activeModules = <Uri, AstNode>{};
/// The dynamic call stack representing function invocations, mixin
/// invocations, and imports surrounding the current context.
@ -276,47 +288,49 @@ class _EvaluateVisitor
var metaFunctions = [
// These functions are defined in the context of the evaluator because
// they need access to the [_environment] or other local state.
BuiltInCallable("global-variable-exists", r"$name, $module: null",
(arguments) {
BuiltInCallable.function(
"global-variable-exists", r"$name, $module: null", (arguments) {
var variable = arguments[0].assertString("name");
var module = arguments[1].realNull?.assertString("module");
return SassBoolean(_environment.globalVariableExists(
variable.text.replaceAll("_", "-"),
namespace: module?.text));
}),
}, url: "sass:meta"),
BuiltInCallable("variable-exists", r"$name", (arguments) {
BuiltInCallable.function("variable-exists", r"$name", (arguments) {
var variable = arguments[0].assertString("name");
return SassBoolean(
_environment.variableExists(variable.text.replaceAll("_", "-")));
}),
}, url: "sass:meta"),
BuiltInCallable("function-exists", r"$name, $module: null", (arguments) {
BuiltInCallable.function("function-exists", r"$name, $module: null",
(arguments) {
var variable = arguments[0].assertString("name");
var module = arguments[1].realNull?.assertString("module");
return SassBoolean(_environment.functionExists(
variable.text.replaceAll("_", "-"),
namespace: module?.text) ||
_builtInFunctions.containsKey(variable.text));
}),
}, url: "sass:meta"),
BuiltInCallable("mixin-exists", r"$name, $module: null", (arguments) {
BuiltInCallable.function("mixin-exists", r"$name, $module: null",
(arguments) {
var variable = arguments[0].assertString("name");
var module = arguments[1].realNull?.assertString("module");
return SassBoolean(_environment.mixinExists(
variable.text.replaceAll("_", "-"),
namespace: module?.text));
}),
}, url: "sass:meta"),
BuiltInCallable("content-exists", "", (arguments) {
BuiltInCallable.function("content-exists", "", (arguments) {
if (!_environment.inMixin) {
throw SassScriptException(
"content-exists() may only be called within a mixin.");
}
return SassBoolean(_environment.content != null);
}),
}, url: "sass:meta"),
BuiltInCallable("module-variables", r"$module", (arguments) {
BuiltInCallable.function("module-variables", r"$module", (arguments) {
var namespace = arguments[0].assertString("module");
var module = _environment.modules[namespace.text];
if (module == null) {
@ -327,9 +341,9 @@ class _EvaluateVisitor
for (var entry in module.variables.entries)
SassString(entry.key): entry.value
});
}),
}, url: "sass:meta"),
BuiltInCallable("module-functions", r"$module", (arguments) {
BuiltInCallable.function("module-functions", r"$module", (arguments) {
var namespace = arguments[0].assertString("module");
var module = _environment.modules[namespace.text];
if (module == null) {
@ -340,10 +354,10 @@ class _EvaluateVisitor
for (var entry in module.functions.entries)
SassString(entry.key): SassFunction(entry.value)
});
}),
}, url: "sass:meta"),
BuiltInCallable("get-function", r"$name, $css: false, $module: null",
(arguments) {
BuiltInCallable.function(
"get-function", r"$name, $css: false, $module: null", (arguments) {
var name = arguments[0].assertString("name");
var css = arguments[1].isTruthy;
var module = arguments[2].realNull?.assertString("module");
@ -361,9 +375,10 @@ class _EvaluateVisitor
if (callable != null) return SassFunction(callable);
throw "Function not found: $name";
}),
}, url: "sass:meta"),
AsyncBuiltInCallable("call", r"$function, $args...", (arguments) async {
AsyncBuiltInCallable.function("call", r"$function, $args...",
(arguments) async {
var function = arguments[0];
var args = arguments[1] as SassArgumentList;
@ -400,11 +415,11 @@ class _EvaluateVisitor
"The function ${callable.name} is asynchronous.\n"
"This is probably caused by a bug in a Sass plugin.");
}
})
}, url: "sass:meta")
];
var metaMixins = [
AsyncBuiltInCallable("load-css", r"$module, $with: null",
AsyncBuiltInCallable.mixin("load-css", r"$module, $with: null",
(arguments) async {
var url = Uri.parse(arguments[0].assertString("module").text);
var withMap = arguments[1].realNull?.assertMap("with")?.contents;
@ -422,7 +437,7 @@ class _EvaluateVisitor
values[name] = ConfiguredValue(value, span);
});
configuration = Configuration(values);
configuration = Configuration(values, _callableNode);
}
await _loadModule(url, "load-css()", _callableNode,
@ -433,7 +448,7 @@ class _EvaluateVisitor
_assertConfigurationIsEmpty(configuration, nameInError: true);
return null;
})
}, url: "sass:meta")
];
var metaModule = BuiltInModule("meta",
@ -453,7 +468,7 @@ class _EvaluateVisitor
return _withWarnCallback(() async {
var url = node.span?.sourceUrl;
if (url != null) {
_activeModules.add(url);
_activeModules[url] = null;
if (_asNodeSass) {
if (url.scheme == 'file') {
_includedFiles.add(p.fromUri(url));
@ -487,13 +502,13 @@ class _EvaluateVisitor
}
/// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet]
/// with [nodeForSpan]'s source span.
Future<T> _withFakeStylesheet<T>(AsyncImporter importer, AstNode nodeForSpan,
/// with [nodeWithSpan]'s source span.
Future<T> _withFakeStylesheet<T>(AsyncImporter importer, AstNode nodeWithSpan,
FutureOr<T> callback()) async {
var oldImporter = _importer;
_importer = importer;
var oldStylesheet = _stylesheet;
_stylesheet = Stylesheet(const [], nodeForSpan.span);
_stylesheet = Stylesheet(const [], nodeWithSpan.span);
try {
return await callback();
@ -517,9 +532,9 @@ class _EvaluateVisitor
/// configured variables in errors relating to them. This should only be
/// `true` if the names won't be obvious from the source span.
///
/// The [stackFrame] and [nodeForSpan] are used for the name and location of
/// The [stackFrame] and [nodeWithSpan] are used for the name and location of
/// the stack frame for the duration of the [callback].
Future<void> _loadModule(Uri url, String stackFrame, AstNode nodeForSpan,
Future<void> _loadModule(Uri url, String stackFrame, AstNode nodeWithSpan,
void callback(Module module),
{Uri baseUrl,
Configuration configuration,
@ -531,32 +546,39 @@ class _EvaluateVisitor
namesInErrors
? "Built-in module $url can't be configured."
: "Built-in modules can't be configured.",
nodeForSpan.span);
nodeWithSpan.span);
}
callback(builtInModule);
return;
}
await _withStackFrame(stackFrame, nodeForSpan, () async {
var result = await _loadStylesheet(url.toString(), nodeForSpan.span,
await _withStackFrame(stackFrame, nodeWithSpan, () async {
var result = await _loadStylesheet(url.toString(), nodeWithSpan.span,
baseUrl: baseUrl);
var importer = result.item1;
var stylesheet = result.item2;
var canonicalUrl = stylesheet.span.sourceUrl;
if (_activeModules.contains(canonicalUrl)) {
throw _exception(namesInErrors
if (_activeModules.containsKey(canonicalUrl)) {
var message = namesInErrors
? "Module loop: ${p.prettyUri(canonicalUrl)} is already being "
"loaded."
: "Module loop: this module is already being loaded.");
: "Module loop: this module is already being loaded.";
var previousLoad = _activeModules[canonicalUrl];
throw previousLoad == null
? _exception(message)
: _multiSpanException(
message, "new load", {previousLoad.span: "original load"});
}
_activeModules.add(canonicalUrl);
_activeModules[canonicalUrl] = nodeWithSpan;
Module module;
try {
module = await _execute(importer, stylesheet,
configuration: configuration, namesInErrors: namesInErrors);
configuration: configuration,
nodeWithSpan: nodeWithSpan,
namesInErrors: namesInErrors);
} finally {
_activeModules.remove(canonicalUrl);
}
@ -565,8 +587,14 @@ class _EvaluateVisitor
await callback(module);
} on SassRuntimeException {
rethrow;
} on MultiSpanSassException catch (error) {
throw MultiSpanSassRuntimeException(error.message, error.span,
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
} on SassException catch (error) {
throw _exception(error.message, error.span);
} on MultiSpanSassScriptException catch (error) {
throw _multiSpanException(
error.message, error.primaryLabel, error.secondarySpans);
} on SassScriptException catch (error) {
throw _exception(error.message);
}
@ -582,17 +610,30 @@ class _EvaluateVisitor
/// 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,
{Configuration configuration, bool namesInErrors = false}) async {
{Configuration configuration,
AstNode nodeWithSpan,
bool namesInErrors = false}) async {
var url = stylesheet.span.sourceUrl;
var alreadyLoaded = _modules[url];
if (alreadyLoaded != null) {
if (!(configuration ?? _configuration).isImplicit) {
throw _exception(namesInErrors
var message = namesInErrors
? "${p.prettyUri(url)} was already loaded, so it can't be "
"configured using \"with\"."
: "This module was already loaded, so it can't be configured using "
"\"with\".");
"\"with\".";
var existingNode = _moduleNodes[url];
var secondarySpans = {
if (existingNode != null) existingNode.span: "original load",
if (configuration == null)
_configuration.nodeWithSpan.span: "configuration"
};
throw secondarySpans.isEmpty
? _exception(message)
: _multiSpanException(message, "new load", secondarySpans);
}
return alreadyLoaded;
@ -654,6 +695,7 @@ class _EvaluateVisitor
var module = environment.toModule(css, extender);
_modules[url] = module;
_moduleNodes[url] = nodeWithSpan;
return module;
}
@ -1022,12 +1064,12 @@ class _EvaluateVisitor
Future<Value> visitEachRule(EachRule node) async {
var list = await node.list.accept(this);
var nodeForSpan = _expressionNode(node.list);
var nodeWithSpan = _expressionNode(node.list);
var setVariables = node.variables.length == 1
? (Value value) => _environment.setLocalVariable(
node.variables.first, value.withoutSlash(), nodeForSpan)
node.variables.first, value.withoutSlash(), nodeWithSpan)
: (Value value) =>
_setMultipleVariables(node.variables, value, nodeForSpan);
_setMultipleVariables(node.variables, value, nodeWithSpan);
return _environment.scope(() {
return _handleReturn<Value>(list.asList, (element) {
setVariables(element);
@ -1040,15 +1082,15 @@ class _EvaluateVisitor
/// Destructures [value] and assigns it to [variables], as in an `@each`
/// statement.
void _setMultipleVariables(
List<String> variables, Value value, AstNode nodeForSpan) {
List<String> variables, Value value, AstNode nodeWithSpan) {
var list = value.asList;
var minLength = math.min(variables.length, list.length);
for (var i = 0; i < minLength; i++) {
_environment.setLocalVariable(
variables[i], list[i].withoutSlash(), nodeForSpan);
variables[i], list[i].withoutSlash(), nodeWithSpan);
}
for (var i = minLength; i < variables.length; i++) {
_environment.setLocalVariable(variables[i], sassNull, nodeForSpan);
_environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
}
}
@ -1172,10 +1214,10 @@ class _EvaluateVisitor
if (from == to) return null;
return _environment.scope(() async {
var nodeForSpan = _expressionNode(node.from);
var nodeWithSpan = _expressionNode(node.from);
for (var i = from; i != to; i += direction) {
_environment.setLocalVariable(
node.variable, SassNumber(i), nodeForSpan);
node.variable, SassNumber(i), nodeWithSpan);
var result = await _handleReturn<Statement>(
node.children, (child) => child.accept(this));
if (result != null) return result;
@ -1237,7 +1279,7 @@ class _EvaluateVisitor
_expressionNode(variable.expression));
}
return Configuration(newValues);
return Configuration(newValues, node);
}
/// Remove configured values from [upstream] that have been removed from
@ -1315,11 +1357,14 @@ class _EvaluateVisitor
var stylesheet = result.item2;
var url = stylesheet.span.sourceUrl;
if (!_activeModules.add(url)) {
throw _exception("This file is already being loaded.");
if (_activeModules.containsKey(url)) {
var previousLoad = _activeModules[url];
throw previousLoad == null
? _exception("This file is already being loaded.")
: _multiSpanException("This file is already being loaded.",
"new load", {previousLoad.span: "original load"});
}
_activeModules.add(url);
_activeModules[url] = import;
// If the imported stylesheet doesn't use any modules, we can inject its
// CSS directly into the current stylesheet. If it does use modules, we
@ -1499,27 +1544,34 @@ class _EvaluateVisitor
throw _exception("Undefined mixin.", node.span);
}
var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent);
if (mixin is AsyncBuiltInCallable) {
if (node.content != null) {
throw _exception("Mixin doesn't accept a content block.", node.span);
}
await _runBuiltInCallable(node.arguments, mixin, node);
await _runBuiltInCallable(node.arguments, mixin, nodeWithSpan);
} else if (mixin is UserDefinedCallable<AsyncEnvironment>) {
if (node.content != null &&
!(mixin.declaration as MixinRule).hasContent) {
throw _exception("Mixin doesn't accept a content block.", node.span);
throw MultiSpanSassRuntimeException(
"Mixin doesn't accept a content block.",
node.spanWithoutContent,
"invocation",
{mixin.declaration.arguments.spanWithName: "declaration"},
_stackTrace(node.spanWithoutContent));
}
var contentCallable = node.content == null
? null
: UserDefinedCallable(node.content, _environment.closure());
await _runUserDefinedCallable(node.arguments, mixin, node, () async {
await _runUserDefinedCallable(node.arguments, mixin, nodeWithSpan,
() async {
await _environment.withContent(contentCallable, () async {
await _environment.asMixin(() async {
for (var statement in mixin.declaration.children) {
await _addErrorSpan(node, () => statement.accept(this));
await _addErrorSpan(nodeWithSpan, () => statement.accept(this));
}
});
return null;
@ -1829,10 +1881,10 @@ class _EvaluateVisitor
(await variable.expression.accept(this)).withoutSlash(),
variable.span,
_expressionNode(variable.expression))
});
}, node);
await _loadModule(node.url, "@use", node, (module) {
_environment.addModule(module, namespace: node.namespace);
_environment.addModule(module, node, namespace: node.namespace);
}, configuration: configuration);
_assertConfigurationIsEmpty(configuration);
@ -1994,13 +2046,20 @@ class _EvaluateVisitor
Future<SassMap> visitMapExpression(MapExpression node) async {
var map = <Value, Value>{};
var keyNodes = <Value, AstNode>{};
for (var pair in node.pairs) {
var keyValue = await pair.item1.accept(this);
var valueValue = await pair.item2.accept(this);
if (map.containsKey(keyValue)) {
throw _exception('Duplicate key.', pair.item1.span);
throw MultiSpanSassRuntimeException(
'Duplicate key.',
pair.item1.span,
'second key',
{keyNodes[keyValue].span: 'first key'},
_stackTrace(pair.item1.span));
}
map[keyValue] = valueValue;
keyNodes[keyValue] = pair.item1;
}
return SassMap(map);
}
@ -2113,8 +2172,12 @@ class _EvaluateVisitor
var argumentWord = pluralize('argument', evaluated.named.keys.length);
var argumentNames =
toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or');
throw _exception(
"No $argumentWord named $argumentNames.", nodeWithSpan.span);
throw MultiSpanSassRuntimeException(
"No $argumentWord named $argumentNames.",
nodeWithSpan.span,
"invocation",
{callable.declaration.arguments.spanWithName: "declaration"},
_stackTrace(nodeWithSpan.span));
});
});
});
@ -2222,6 +2285,16 @@ class _EvaluateVisitor
result = await callback(evaluated.positional);
} on SassRuntimeException {
rethrow;
} on MultiSpanSassScriptException catch (error) {
throw MultiSpanSassRuntimeException(
error.message,
nodeWithSpan.span,
error.primaryLabel,
error.secondarySpans,
_stackTrace(nodeWithSpan.span));
} on MultiSpanSassException catch (error) {
throw MultiSpanSassRuntimeException(error.message, error.span,
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
} catch (error) {
String message;
try {
@ -2236,10 +2309,13 @@ class _EvaluateVisitor
if (argumentList == null) return result;
if (evaluated.named.isEmpty) return result;
if (argumentList.wereKeywordsAccessed) return result;
throw _exception(
throw MultiSpanSassRuntimeException(
"No ${pluralize('argument', evaluated.named.keys.length)} named "
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
nodeWithSpan.span);
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
nodeWithSpan.span,
"invocation",
{overload.spanWithName: "declaration"},
_stackTrace(nodeWithSpan.span));
}
/// Returns the evaluated values of the given [arguments].
@ -2367,7 +2443,7 @@ class _EvaluateVisitor
/// Adds the values in [map] to [values].
///
/// Throws a [SassRuntimeException] associated with [nodeForSpan]'s source
/// Throws a [SassRuntimeException] associated with [nodeWithSpan]'s source
/// span if any [map] keys aren't strings.
///
/// If [convert] is passed, that's used to convert the map values to the value
@ -2376,7 +2452,7 @@ class _EvaluateVisitor
/// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
/// [AstNode.span] if the span isn't required, since some nodes need to do
/// real work to manufacture a source span.
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeForSpan,
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeWithSpan,
[T convert(Value value)]) {
convert ??= (value) => value as T;
map.contents.forEach((key, value) {
@ -2386,7 +2462,7 @@ class _EvaluateVisitor
throw _exception(
"Variable keyword argument map must have string keys.\n"
"$key is not a string in $map.",
nodeForSpan.span);
nodeWithSpan.span);
}
});
}
@ -2843,13 +2919,22 @@ class _EvaluateVisitor
_logger.warn(message,
span: span, trace: _stackTrace(span), deprecation: deprecation);
/// Throws a [SassRuntimeException] with the given [message].
/// Returns a [SassRuntimeException] with the given [message].
///
/// If [span] is passed, it's used for the innermost stack frame.
SassRuntimeException _exception(String message, [FileSpan span]) =>
SassRuntimeException(
message, span ?? _stack.last.item2.span, _stackTrace(span));
/// Returns a [MultiSpanSassRuntimeException] with the given [message],
/// [primaryLabel], and [secondaryLabels].
///
/// The primary span is taken from the current stack trace span.
SassRuntimeException _multiSpanException(String message, String primaryLabel,
Map<FileSpan, String> secondaryLabels) =>
MultiSpanSassRuntimeException(message, _stack.last.item2.span,
primaryLabel, secondaryLabels, _stackTrace());
/// Runs [callback], and adjusts any [SassFormatException] to be within
/// [nodeWithSpan]'s source span.
///
@ -2887,6 +2972,13 @@ class _EvaluateVisitor
T _addExceptionSpan<T>(AstNode nodeWithSpan, T callback()) {
try {
return callback();
} on MultiSpanSassScriptException catch (error) {
throw MultiSpanSassRuntimeException(
error.message,
nodeWithSpan.span,
error.primaryLabel,
error.secondarySpans,
_stackTrace(nodeWithSpan.span));
} on SassScriptException catch (error) {
throw _exception(error.message, nodeWithSpan.span);
}
@ -2897,6 +2989,13 @@ class _EvaluateVisitor
AstNode nodeWithSpan, Future<T> callback()) async {
try {
return await callback();
} on MultiSpanSassScriptException catch (error) {
throw MultiSpanSassRuntimeException(
error.message,
nodeWithSpan.span,
error.primaryLabel,
error.secondarySpans,
_stackTrace(nodeWithSpan.span));
} on SassScriptException catch (error) {
throw _exception(error.message, nodeWithSpan.span);
}

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: 98bd4418d21d4f005485e343869b458b44841450
// Checksum: eb095e782e2983223945d189caadc649b081a676
//
// ignore_for_file: unused_import
@ -147,6 +147,13 @@ class _EvaluateVisitor
/// All modules that have been loaded and evaluated so far.
final _modules = <Uri, Module<Callable>>{};
/// A map from canonical module URLs to the nodes whose spans indicate where
/// those modules were originally loaded.
///
/// This is not guaranteed to have a node for every module in [_modules]. For
/// example, the entrypoint module was not loaded by a node.
final _moduleNodes = <Uri, AstNode>{};
/// The logger to use to print warnings.
final Logger _logger;
@ -207,11 +214,16 @@ class _EvaluateVisitor
/// imports, it contains the URL passed to the `@import`.
final _includedFiles = <String>{};
/// The set of canonical URLs for modules (or imported files) that are
/// currently being evaluated.
/// A map from canonical URLs for modules (or imported files) that are
/// currently being evaluated to AST nodes whose spans indicate the original
/// loads for those modules.
///
/// Map values may be `null`, which indicates an active module that doesn't
/// have a source span associated with its original load (such as the
/// entrypoint module).
///
/// This is used to ensure that we don't get into an infinite load loop.
final _activeModules = <Uri>{};
final _activeModules = <Uri, AstNode>{};
/// The dynamic call stack representing function invocations, mixin
/// invocations, and imports surrounding the current context.
@ -284,47 +296,49 @@ class _EvaluateVisitor
var metaFunctions = [
// These functions are defined in the context of the evaluator because
// they need access to the [_environment] or other local state.
BuiltInCallable("global-variable-exists", r"$name, $module: null",
(arguments) {
BuiltInCallable.function(
"global-variable-exists", r"$name, $module: null", (arguments) {
var variable = arguments[0].assertString("name");
var module = arguments[1].realNull?.assertString("module");
return SassBoolean(_environment.globalVariableExists(
variable.text.replaceAll("_", "-"),
namespace: module?.text));
}),
}, url: "sass:meta"),
BuiltInCallable("variable-exists", r"$name", (arguments) {
BuiltInCallable.function("variable-exists", r"$name", (arguments) {
var variable = arguments[0].assertString("name");
return SassBoolean(
_environment.variableExists(variable.text.replaceAll("_", "-")));
}),
}, url: "sass:meta"),
BuiltInCallable("function-exists", r"$name, $module: null", (arguments) {
BuiltInCallable.function("function-exists", r"$name, $module: null",
(arguments) {
var variable = arguments[0].assertString("name");
var module = arguments[1].realNull?.assertString("module");
return SassBoolean(_environment.functionExists(
variable.text.replaceAll("_", "-"),
namespace: module?.text) ||
_builtInFunctions.containsKey(variable.text));
}),
}, url: "sass:meta"),
BuiltInCallable("mixin-exists", r"$name, $module: null", (arguments) {
BuiltInCallable.function("mixin-exists", r"$name, $module: null",
(arguments) {
var variable = arguments[0].assertString("name");
var module = arguments[1].realNull?.assertString("module");
return SassBoolean(_environment.mixinExists(
variable.text.replaceAll("_", "-"),
namespace: module?.text));
}),
}, url: "sass:meta"),
BuiltInCallable("content-exists", "", (arguments) {
BuiltInCallable.function("content-exists", "", (arguments) {
if (!_environment.inMixin) {
throw SassScriptException(
"content-exists() may only be called within a mixin.");
}
return SassBoolean(_environment.content != null);
}),
}, url: "sass:meta"),
BuiltInCallable("module-variables", r"$module", (arguments) {
BuiltInCallable.function("module-variables", r"$module", (arguments) {
var namespace = arguments[0].assertString("module");
var module = _environment.modules[namespace.text];
if (module == null) {
@ -335,9 +349,9 @@ class _EvaluateVisitor
for (var entry in module.variables.entries)
SassString(entry.key): entry.value
});
}),
}, url: "sass:meta"),
BuiltInCallable("module-functions", r"$module", (arguments) {
BuiltInCallable.function("module-functions", r"$module", (arguments) {
var namespace = arguments[0].assertString("module");
var module = _environment.modules[namespace.text];
if (module == null) {
@ -348,10 +362,10 @@ class _EvaluateVisitor
for (var entry in module.functions.entries)
SassString(entry.key): SassFunction(entry.value)
});
}),
}, url: "sass:meta"),
BuiltInCallable("get-function", r"$name, $css: false, $module: null",
(arguments) {
BuiltInCallable.function(
"get-function", r"$name, $css: false, $module: null", (arguments) {
var name = arguments[0].assertString("name");
var css = arguments[1].isTruthy;
var module = arguments[2].realNull?.assertString("module");
@ -369,9 +383,9 @@ class _EvaluateVisitor
if (callable != null) return SassFunction(callable);
throw "Function not found: $name";
}),
}, url: "sass:meta"),
BuiltInCallable("call", r"$function, $args...", (arguments) {
BuiltInCallable.function("call", r"$function, $args...", (arguments) {
var function = arguments[0];
var args = arguments[1] as SassArgumentList;
@ -407,11 +421,11 @@ class _EvaluateVisitor
"The function ${callable.name} is asynchronous.\n"
"This is probably caused by a bug in a Sass plugin.");
}
})
}, url: "sass:meta")
];
var metaMixins = [
BuiltInCallable("load-css", r"$module, $with: null", (arguments) {
BuiltInCallable.mixin("load-css", r"$module, $with: null", (arguments) {
var url = Uri.parse(arguments[0].assertString("module").text);
var withMap = arguments[1].realNull?.assertMap("with")?.contents;
@ -428,7 +442,7 @@ class _EvaluateVisitor
values[name] = ConfiguredValue(value, span);
});
configuration = Configuration(values);
configuration = Configuration(values, _callableNode);
}
_loadModule(url, "load-css()", _callableNode,
@ -439,7 +453,7 @@ class _EvaluateVisitor
_assertConfigurationIsEmpty(configuration, nameInError: true);
return null;
})
}, url: "sass:meta")
];
var metaModule = BuiltInModule("meta",
@ -459,7 +473,7 @@ class _EvaluateVisitor
return _withWarnCallback(() {
var url = node.span?.sourceUrl;
if (url != null) {
_activeModules.add(url);
_activeModules[url] = null;
if (_asNodeSass) {
if (url.scheme == 'file') {
_includedFiles.add(p.fromUri(url));
@ -493,13 +507,13 @@ class _EvaluateVisitor
}
/// Runs [callback] with [importer] as [_importer] and a fake [_stylesheet]
/// with [nodeForSpan]'s source span.
/// with [nodeWithSpan]'s source span.
T _withFakeStylesheet<T>(
Importer importer, AstNode nodeForSpan, T callback()) {
Importer importer, AstNode nodeWithSpan, T callback()) {
var oldImporter = _importer;
_importer = importer;
var oldStylesheet = _stylesheet;
_stylesheet = Stylesheet(const [], nodeForSpan.span);
_stylesheet = Stylesheet(const [], nodeWithSpan.span);
try {
return callback();
@ -523,9 +537,9 @@ class _EvaluateVisitor
/// configured variables in errors relating to them. This should only be
/// `true` if the names won't be obvious from the source span.
///
/// The [stackFrame] and [nodeForSpan] are used for the name and location of
/// The [stackFrame] and [nodeWithSpan] are used for the name and location of
/// the stack frame for the duration of the [callback].
void _loadModule(Uri url, String stackFrame, AstNode nodeForSpan,
void _loadModule(Uri url, String stackFrame, AstNode nodeWithSpan,
void callback(Module<Callable> module),
{Uri baseUrl, Configuration configuration, bool namesInErrors = false}) {
var builtInModule = _builtInModules[url];
@ -535,32 +549,39 @@ class _EvaluateVisitor
namesInErrors
? "Built-in module $url can't be configured."
: "Built-in modules can't be configured.",
nodeForSpan.span);
nodeWithSpan.span);
}
callback(builtInModule);
return;
}
_withStackFrame(stackFrame, nodeForSpan, () {
_withStackFrame(stackFrame, nodeWithSpan, () {
var result =
_loadStylesheet(url.toString(), nodeForSpan.span, baseUrl: baseUrl);
_loadStylesheet(url.toString(), nodeWithSpan.span, baseUrl: baseUrl);
var importer = result.item1;
var stylesheet = result.item2;
var canonicalUrl = stylesheet.span.sourceUrl;
if (_activeModules.contains(canonicalUrl)) {
throw _exception(namesInErrors
if (_activeModules.containsKey(canonicalUrl)) {
var message = namesInErrors
? "Module loop: ${p.prettyUri(canonicalUrl)} is already being "
"loaded."
: "Module loop: this module is already being loaded.");
: "Module loop: this module is already being loaded.";
var previousLoad = _activeModules[canonicalUrl];
throw previousLoad == null
? _exception(message)
: _multiSpanException(
message, "new load", {previousLoad.span: "original load"});
}
_activeModules.add(canonicalUrl);
_activeModules[canonicalUrl] = nodeWithSpan;
Module<Callable> module;
try {
module = _execute(importer, stylesheet,
configuration: configuration, namesInErrors: namesInErrors);
configuration: configuration,
nodeWithSpan: nodeWithSpan,
namesInErrors: namesInErrors);
} finally {
_activeModules.remove(canonicalUrl);
}
@ -569,8 +590,14 @@ class _EvaluateVisitor
callback(module);
} on SassRuntimeException {
rethrow;
} on MultiSpanSassException catch (error) {
throw MultiSpanSassRuntimeException(error.message, error.span,
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
} on SassException catch (error) {
throw _exception(error.message, error.span);
} on MultiSpanSassScriptException catch (error) {
throw _multiSpanException(
error.message, error.primaryLabel, error.secondarySpans);
} on SassScriptException catch (error) {
throw _exception(error.message);
}
@ -586,17 +613,30 @@ class _EvaluateVisitor
/// 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,
{Configuration configuration, bool namesInErrors = false}) {
{Configuration configuration,
AstNode nodeWithSpan,
bool namesInErrors = false}) {
var url = stylesheet.span.sourceUrl;
var alreadyLoaded = _modules[url];
if (alreadyLoaded != null) {
if (!(configuration ?? _configuration).isImplicit) {
throw _exception(namesInErrors
var message = namesInErrors
? "${p.prettyUri(url)} was already loaded, so it can't be "
"configured using \"with\"."
: "This module was already loaded, so it can't be configured using "
"\"with\".");
"\"with\".";
var existingNode = _moduleNodes[url];
var secondarySpans = {
if (existingNode != null) existingNode.span: "original load",
if (configuration == null)
_configuration.nodeWithSpan.span: "configuration"
};
throw secondarySpans.isEmpty
? _exception(message)
: _multiSpanException(message, "new load", secondarySpans);
}
return alreadyLoaded;
@ -658,6 +698,7 @@ class _EvaluateVisitor
var module = environment.toModule(css, extender);
_modules[url] = module;
_moduleNodes[url] = nodeWithSpan;
return module;
}
@ -1025,12 +1066,12 @@ class _EvaluateVisitor
Value visitEachRule(EachRule node) {
var list = node.list.accept(this);
var nodeForSpan = _expressionNode(node.list);
var nodeWithSpan = _expressionNode(node.list);
var setVariables = node.variables.length == 1
? (Value value) => _environment.setLocalVariable(
node.variables.first, value.withoutSlash(), nodeForSpan)
node.variables.first, value.withoutSlash(), nodeWithSpan)
: (Value value) =>
_setMultipleVariables(node.variables, value, nodeForSpan);
_setMultipleVariables(node.variables, value, nodeWithSpan);
return _environment.scope(() {
return _handleReturn<Value>(list.asList, (element) {
setVariables(element);
@ -1043,15 +1084,15 @@ class _EvaluateVisitor
/// Destructures [value] and assigns it to [variables], as in an `@each`
/// statement.
void _setMultipleVariables(
List<String> variables, Value value, AstNode nodeForSpan) {
List<String> variables, Value value, AstNode nodeWithSpan) {
var list = value.asList;
var minLength = math.min(variables.length, list.length);
for (var i = 0; i < minLength; i++) {
_environment.setLocalVariable(
variables[i], list[i].withoutSlash(), nodeForSpan);
variables[i], list[i].withoutSlash(), nodeWithSpan);
}
for (var i = minLength; i < variables.length; i++) {
_environment.setLocalVariable(variables[i], sassNull, nodeForSpan);
_environment.setLocalVariable(variables[i], sassNull, nodeWithSpan);
}
}
@ -1171,10 +1212,10 @@ class _EvaluateVisitor
if (from == to) return null;
return _environment.scope(() {
var nodeForSpan = _expressionNode(node.from);
var nodeWithSpan = _expressionNode(node.from);
for (var i = from; i != to; i += direction) {
_environment.setLocalVariable(
node.variable, SassNumber(i), nodeForSpan);
node.variable, SassNumber(i), nodeWithSpan);
var result = _handleReturn<Statement>(
node.children, (child) => child.accept(this));
if (result != null) return result;
@ -1236,7 +1277,7 @@ class _EvaluateVisitor
_expressionNode(variable.expression));
}
return Configuration(newValues);
return Configuration(newValues, node);
}
/// Remove configured values from [upstream] that have been removed from
@ -1313,11 +1354,14 @@ class _EvaluateVisitor
var stylesheet = result.item2;
var url = stylesheet.span.sourceUrl;
if (!_activeModules.add(url)) {
throw _exception("This file is already being loaded.");
if (_activeModules.containsKey(url)) {
var previousLoad = _activeModules[url];
throw previousLoad == null
? _exception("This file is already being loaded.")
: _multiSpanException("This file is already being loaded.",
"new load", {previousLoad.span: "original load"});
}
_activeModules.add(url);
_activeModules[url] = import;
// If the imported stylesheet doesn't use any modules, we can inject its
// CSS directly into the current stylesheet. If it does use modules, we
@ -1496,27 +1540,33 @@ class _EvaluateVisitor
throw _exception("Undefined mixin.", node.span);
}
var nodeWithSpan = AstNode.fake(() => node.spanWithoutContent);
if (mixin is BuiltInCallable) {
if (node.content != null) {
throw _exception("Mixin doesn't accept a content block.", node.span);
}
_runBuiltInCallable(node.arguments, mixin, node);
_runBuiltInCallable(node.arguments, mixin, nodeWithSpan);
} else if (mixin is UserDefinedCallable<Environment>) {
if (node.content != null &&
!(mixin.declaration as MixinRule).hasContent) {
throw _exception("Mixin doesn't accept a content block.", node.span);
throw MultiSpanSassRuntimeException(
"Mixin doesn't accept a content block.",
node.spanWithoutContent,
"invocation",
{mixin.declaration.arguments.spanWithName: "declaration"},
_stackTrace(node.spanWithoutContent));
}
var contentCallable = node.content == null
? null
: UserDefinedCallable(node.content, _environment.closure());
_runUserDefinedCallable(node.arguments, mixin, node, () {
_runUserDefinedCallable(node.arguments, mixin, nodeWithSpan, () {
_environment.withContent(contentCallable, () {
_environment.asMixin(() {
for (var statement in mixin.declaration.children) {
_addErrorSpan(node, () => statement.accept(this));
_addErrorSpan(nodeWithSpan, () => statement.accept(this));
}
});
return null;
@ -1821,10 +1871,10 @@ class _EvaluateVisitor
variable.expression.accept(this).withoutSlash(),
variable.span,
_expressionNode(variable.expression))
});
}, node);
_loadModule(node.url, "@use", node, (module) {
_environment.addModule(module, namespace: node.namespace);
_environment.addModule(module, node, namespace: node.namespace);
}, configuration: configuration);
_assertConfigurationIsEmpty(configuration);
@ -1981,13 +2031,20 @@ class _EvaluateVisitor
SassMap visitMapExpression(MapExpression node) {
var map = <Value, Value>{};
var keyNodes = <Value, AstNode>{};
for (var pair in node.pairs) {
var keyValue = pair.item1.accept(this);
var valueValue = pair.item2.accept(this);
if (map.containsKey(keyValue)) {
throw _exception('Duplicate key.', pair.item1.span);
throw MultiSpanSassRuntimeException(
'Duplicate key.',
pair.item1.span,
'second key',
{keyNodes[keyValue].span: 'first key'},
_stackTrace(pair.item1.span));
}
map[keyValue] = valueValue;
keyNodes[keyValue] = pair.item1;
}
return SassMap(map);
}
@ -2100,8 +2157,12 @@ class _EvaluateVisitor
var argumentWord = pluralize('argument', evaluated.named.keys.length);
var argumentNames =
toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or');
throw _exception(
"No $argumentWord named $argumentNames.", nodeWithSpan.span);
throw MultiSpanSassRuntimeException(
"No $argumentWord named $argumentNames.",
nodeWithSpan.span,
"invocation",
{callable.declaration.arguments.spanWithName: "declaration"},
_stackTrace(nodeWithSpan.span));
});
});
});
@ -2207,6 +2268,16 @@ class _EvaluateVisitor
result = callback(evaluated.positional);
} on SassRuntimeException {
rethrow;
} on MultiSpanSassScriptException catch (error) {
throw MultiSpanSassRuntimeException(
error.message,
nodeWithSpan.span,
error.primaryLabel,
error.secondarySpans,
_stackTrace(nodeWithSpan.span));
} on MultiSpanSassException catch (error) {
throw MultiSpanSassRuntimeException(error.message, error.span,
error.primaryLabel, error.secondarySpans, _stackTrace(error.span));
} catch (error) {
String message;
try {
@ -2221,10 +2292,13 @@ class _EvaluateVisitor
if (argumentList == null) return result;
if (evaluated.named.isEmpty) return result;
if (argumentList.wereKeywordsAccessed) return result;
throw _exception(
throw MultiSpanSassRuntimeException(
"No ${pluralize('argument', evaluated.named.keys.length)} named "
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
nodeWithSpan.span);
"${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.",
nodeWithSpan.span,
"invocation",
{overload.spanWithName: "declaration"},
_stackTrace(nodeWithSpan.span));
}
/// Returns the evaluated values of the given [arguments].
@ -2351,7 +2425,7 @@ class _EvaluateVisitor
/// Adds the values in [map] to [values].
///
/// Throws a [SassRuntimeException] associated with [nodeForSpan]'s source
/// Throws a [SassRuntimeException] associated with [nodeWithSpan]'s source
/// span if any [map] keys aren't strings.
///
/// If [convert] is passed, that's used to convert the map values to the value
@ -2360,7 +2434,7 @@ class _EvaluateVisitor
/// This takes an [AstNode] rather than a [FileSpan] so it can avoid calling
/// [AstNode.span] if the span isn't required, since some nodes need to do
/// real work to manufacture a source span.
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeForSpan,
void _addRestMap<T>(Map<String, T> values, SassMap map, AstNode nodeWithSpan,
[T convert(Value value)]) {
convert ??= (value) => value as T;
map.contents.forEach((key, value) {
@ -2370,7 +2444,7 @@ class _EvaluateVisitor
throw _exception(
"Variable keyword argument map must have string keys.\n"
"$key is not a string in $map.",
nodeForSpan.span);
nodeWithSpan.span);
}
});
}
@ -2816,13 +2890,22 @@ class _EvaluateVisitor
_logger.warn(message,
span: span, trace: _stackTrace(span), deprecation: deprecation);
/// Throws a [SassRuntimeException] with the given [message].
/// Returns a [SassRuntimeException] with the given [message].
///
/// If [span] is passed, it's used for the innermost stack frame.
SassRuntimeException _exception(String message, [FileSpan span]) =>
SassRuntimeException(
message, span ?? _stack.last.item2.span, _stackTrace(span));
/// Returns a [MultiSpanSassRuntimeException] with the given [message],
/// [primaryLabel], and [secondaryLabels].
///
/// The primary span is taken from the current stack trace span.
SassRuntimeException _multiSpanException(String message, String primaryLabel,
Map<FileSpan, String> secondaryLabels) =>
MultiSpanSassRuntimeException(message, _stack.last.item2.span,
primaryLabel, secondaryLabels, _stackTrace());
/// Runs [callback], and adjusts any [SassFormatException] to be within
/// [nodeWithSpan]'s source span.
///
@ -2860,6 +2943,13 @@ class _EvaluateVisitor
T _addExceptionSpan<T>(AstNode nodeWithSpan, T callback()) {
try {
return callback();
} on MultiSpanSassScriptException catch (error) {
throw MultiSpanSassRuntimeException(
error.message,
nodeWithSpan.span,
error.primaryLabel,
error.secondarySpans,
_stackTrace(nodeWithSpan.span));
} on SassScriptException catch (error) {
throw _exception(error.message, nodeWithSpan.span);
}

View File

@ -346,6 +346,9 @@ class _SerializeVisitor
try {
_buffer.forSpan(
node.valueSpanForMap, () => node.value.value.accept(this));
} on MultiSpanSassScriptException catch (error) {
throw MultiSpanSassException(error.message, node.value.span,
error.primaryLabel, error.secondarySpans);
} on SassScriptException catch (error) {
throw SassException(error.message, node.value.span);
}

View File

@ -1,5 +1,5 @@
name: sass
version: 1.25.0-dev
version: 1.25.0
description: A Sass implementation in Dart.
author: Sass Team
homepage: https://github.com/sass/dart-sass
@ -21,9 +21,7 @@ dependencies:
package_resolver: "^1.0.0"
path: "^1.6.0"
source_maps: "^0.10.5"
# Temporarily limit this until #926 lands.
source_span: ">=1.4.0 <1.6.0"
source_span: "^1.6.0"
stack_trace: ">=0.9.0 <2.0.0"
stream_transform: ">=0.0.20 <2.0.0"
string_scanner: ">=0.1.5 <2.0.0"

View File

@ -132,7 +132,7 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
"Error: Expected expression.",
"\u001b[34m ,\u001b[0m",
"\u001b[34m1 |\u001b[0m a {b: \u001b[31m\u001b[0m}",
"\u001b[34m |\u001b[0m \u001b[31m^\u001b[0m",
"\u001b[34m |\u001b[0m \u001b[31m ^\u001b[0m",
"\u001b[34m '\u001b[0m",
" test.scss 1:7 root stylesheet",
]));

View File

@ -0,0 +1,54 @@
// Copyright 2017 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.
@TestOn('vm')
import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
import '../tool/grind/synchronize.dart' as synchronize;
/// Tests that double-check that everything in the repo looks sensible.
void main() {
group("synchronized file is up-to-date:", () {
/// The pattern of a checksum in a generated file.
var checksumPattern = RegExp(r"^// Checksum: (.*)$", multiLine: true);
synchronize.sources.forEach((sourcePath, targetPath) {
test(targetPath, () {
var target = File(targetPath).readAsStringSync();
var actualHash = checksumPattern.firstMatch(target)[1];
var source = File(sourcePath).readAsBytesSync();
var expectedHash = sha1.convert(source).toString();
expect(actualHash, equals(expectedHash),
reason: "$targetPath is out-of-date.\n"
"Run pub run grinder to update it.");
});
});
},
// Windows sees different bytes than other OSes, possibly because of
// newline normalization issues.
testOn: "!windows");
test("pubspec version matches CHANGELOG version", () {
var firstLine = const LineSplitter()
.convert(File("CHANGELOG.md").readAsStringSync())
.first;
expect(firstLine, startsWith("## "));
var changelogVersion = firstLine.substring(3);
var pubspec = loadYaml(File("pubspec.yaml").readAsStringSync(),
sourceUrl: "pubspec.yaml") as Map<Object, Object>;
expect(pubspec, containsPair("version", isA<String>()));
var pubspecVersion = pubspec["version"] as String;
expect(pubspecVersion,
anyOf(equals(changelogVersion), equals("$changelogVersion-dev")));
});
}

View File

@ -1,32 +0,0 @@
// Copyright 2017 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.
// Windows sees different bytes than other OSes, possibly because of newline
// normalization issues.
@TestOn('vm && !windows')
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:test/test.dart';
import '../tool/grind/synchronize.dart' as synchronize;
/// The pattern of a checksum in a generated file.
final _checksumPattern = RegExp(r"^// Checksum: (.*)$", multiLine: true);
void main() {
synchronize.sources.forEach((sourcePath, targetPath) {
test("synchronized file $targetPath is up-to-date", () {
var target = File(targetPath).readAsStringSync();
var actualHash = _checksumPattern.firstMatch(target)[1];
var source = File(sourcePath).readAsBytesSync();
var expectedHash = sha1.convert(source).toString();
expect(actualHash, equals(expectedHash),
reason: "$targetPath is out-of-date.\n"
"Run pub run grinder to update it.");
});
});
}