diff --git a/CHANGELOG.md b/CHANGELOG.md index d40fad9ea..6303d867e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.16.1 + +* Fix a performance bug where stylesheet evaluation could take a very long time + when many binary operators were used in sequence. + ## 1.16.0 * `rgb()` and `hsl()` now treat unquoted strings beginning with `env()`, diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart index f0530a0b4..b1608288b 100644 --- a/lib/src/ast/sass/expression/binary_operation.dart +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -24,7 +24,20 @@ class BinaryOperationExpression implements Expression { /// interpreted as slash-separated numbers. final bool allowsSlash; - FileSpan get span => spanForList([left, right]); + FileSpan get span { + // Avoid creating a bunch of intermediate spans for multiple binary + // expressions in a row by moving to the left- and right-most expressions. + var left = this.left; + while (left is BinaryOperationExpression) { + left = (left as BinaryOperationExpression).left; + } + + var right = this.right; + while (right is BinaryOperationExpression) { + right = (right as BinaryOperationExpression).right; + } + return spanForList([left, right]); + } BinaryOperationExpression(this.operator, this.left, this.right) : allowsSlash = false; diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index 85e4d46a4..c30973cc9 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:source_span/source_span.dart'; +import 'ast/node.dart'; import 'callable.dart'; import 'functions.dart'; import 'value.dart'; @@ -26,10 +27,14 @@ class AsyncEnvironment { /// deeper in the tree. final List> _variables; - /// The spans where each variable in [_variables] was defined. + /// The nodes where each variable in [_variables] was defined. /// /// This is `null` if source mapping is disabled. - final List> _variableSpans; + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final List> _variableNodes; /// A map of variable names to their indices in [_variables]. /// @@ -104,7 +109,7 @@ class AsyncEnvironment { /// If [sourceMap] is `true`, this tracks variables' source locations AsyncEnvironment({bool sourceMap = false}) : _variables = [normalizedMap()], - _variableSpans = sourceMap ? [normalizedMap()] : null, + _variableNodes = sourceMap ? [normalizedMap()] : null, _variableIndices = normalizedMap(), _functions = [normalizedMap()], _functionIndices = normalizedMap(), @@ -113,7 +118,7 @@ class AsyncEnvironment { coreFunctions.forEach(setFunction); } - AsyncEnvironment._(this._variables, this._variableSpans, this._functions, + AsyncEnvironment._(this._variables, this._variableNodes, this._functions, this._mixins, this._content) // Lazily fill in the indices rather than eagerly copying them from the // existing environment in closure() because the copying took a lot of @@ -130,7 +135,7 @@ class AsyncEnvironment { /// when the closure was created will be reflected. AsyncEnvironment closure() => AsyncEnvironment._( _variables.toList(), - _variableSpans?.toList(), + _variableNodes?.toList(), _functions.toList(), _mixins.toList(), _content); @@ -156,18 +161,24 @@ class AsyncEnvironment { return _variables[index][name]; } - /// Returns the source span for the variable named [name], or `null` if no - /// such variable is declared. - FileSpan getVariableSpan(String name) { + /// Returns the node for the variable named [name], or `null` if no such + /// variable is declared. + /// + /// This node is intended as a proxy for the [FileSpan] indicating where the + /// variable's value originated. It's returned as an [AstNode] rather than a + /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't + /// required, since some nodes need to do real work to manufacture a source + /// span. + AstNode getVariableNode(String name) { if (_lastVariableName == name) { - return _variableSpans[_lastVariableIndex][name]; + return _variableNodes[_lastVariableIndex][name]; } var index = _variableIndices[name]; if (index != null) { _lastVariableName = name; _lastVariableIndex = index; - return _variableSpans[index][name]; + return _variableNodes[index][name]; } index = _variableIndex(name); @@ -176,7 +187,7 @@ class AsyncEnvironment { _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; - return _variableSpans[index][name]; + return _variableNodes[index][name]; } /// Returns whether a variable named [name] exists. @@ -194,12 +205,17 @@ class AsyncEnvironment { return null; } - /// Sets the variable named [name] to [value], associated with the given [span]. + /// Sets the variable named [name] to [value], associated with + /// [nodeWithSpan]'s source span. /// /// If [global] is `true`, this sets the variable at the top-level scope. /// Otherwise, if the variable was already defined, it'll set it in the /// previous scope. If it's undefined, it'll set it in the current scope. - void setVariable(String name, Value value, FileSpan span, + /// + /// 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 setVariable(String name, Value value, AstNode nodeWithSpan, {bool global = false}) { if (global || _variables.length == 1) { // Don't set the index if there's already a variable with the given name, @@ -211,7 +227,7 @@ class AsyncEnvironment { }); _variables.first[name] = value; - if (_variableSpans != null) _variableSpans.first[name] = span; + if (_variableNodes != null) _variableNodes.first[name] = nodeWithSpan; return; } @@ -227,20 +243,25 @@ class AsyncEnvironment { _lastVariableName = name; _lastVariableIndex = index; _variables[index][name] = value; - if (_variableSpans != null) _variableSpans[index][name] = span; + if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; } - /// Sets the variable named [name] to [value] in the current scope, associated with the given [span]. + /// Sets the variable named [name] to [value], associated with + /// [nodeWithSpan]'s source span. /// /// Unlike [setVariable], this will declare the variable in the current scope /// even if a declaration already exists in an outer scope. - void setLocalVariable(String name, Value value, FileSpan span) { + /// + /// 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 setLocalVariable(String name, Value value, AstNode nodeWithSpan) { var index = _variables.length - 1; _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; _variables[index][name] = value; - if (_variableSpans != null) _variableSpans[index][name] = span; + if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; } /// Returns the value of the function named [name], or `null` if no such @@ -358,7 +379,7 @@ class AsyncEnvironment { _inSemiGlobalScope = semiGlobal; _variables.add(normalizedMap()); - _variableSpans?.add(normalizedMap()); + _variableNodes?.add(normalizedMap()); _functions.add(normalizedMap()); _mixins.add(normalizedMap()); try { diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 694789441..316c7bd93 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,12 +5,13 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/synchronize.dart for details. // -// Checksum: 9e0f9274f4778b701f268bcf4fc349a1cf17a159 +// Checksum: 449ed8a8ad29fe107656a666e6e6005ef539b834 // // ignore_for_file: unused_import import 'package:source_span/source_span.dart'; +import 'ast/node.dart'; import 'callable.dart'; import 'functions.dart'; import 'value.dart'; @@ -31,10 +32,14 @@ class Environment { /// deeper in the tree. final List> _variables; - /// The spans where each variable in [_variables] was defined. + /// The nodes where each variable in [_variables] was defined. /// /// This is `null` if source mapping is disabled. - final List> _variableSpans; + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final List> _variableNodes; /// A map of variable names to their indices in [_variables]. /// @@ -109,7 +114,7 @@ class Environment { /// If [sourceMap] is `true`, this tracks variables' source locations Environment({bool sourceMap = false}) : _variables = [normalizedMap()], - _variableSpans = sourceMap ? [normalizedMap()] : null, + _variableNodes = sourceMap ? [normalizedMap()] : null, _variableIndices = normalizedMap(), _functions = [normalizedMap()], _functionIndices = normalizedMap(), @@ -118,7 +123,7 @@ class Environment { coreFunctions.forEach(setFunction); } - Environment._(this._variables, this._variableSpans, this._functions, + Environment._(this._variables, this._variableNodes, this._functions, this._mixins, this._content) // Lazily fill in the indices rather than eagerly copying them from the // existing environment in closure() because the copying took a lot of @@ -135,7 +140,7 @@ class Environment { /// when the closure was created will be reflected. Environment closure() => Environment._( _variables.toList(), - _variableSpans?.toList(), + _variableNodes?.toList(), _functions.toList(), _mixins.toList(), _content); @@ -161,18 +166,24 @@ class Environment { return _variables[index][name]; } - /// Returns the source span for the variable named [name], or `null` if no - /// such variable is declared. - FileSpan getVariableSpan(String name) { + /// Returns the node for the variable named [name], or `null` if no such + /// variable is declared. + /// + /// This node is intended as a proxy for the [FileSpan] indicating where the + /// variable's value originated. It's returned as an [AstNode] rather than a + /// [FileSpan] so we can avoid calling [AstNode.span] if the span isn't + /// required, since some nodes need to do real work to manufacture a source + /// span. + AstNode getVariableNode(String name) { if (_lastVariableName == name) { - return _variableSpans[_lastVariableIndex][name]; + return _variableNodes[_lastVariableIndex][name]; } var index = _variableIndices[name]; if (index != null) { _lastVariableName = name; _lastVariableIndex = index; - return _variableSpans[index][name]; + return _variableNodes[index][name]; } index = _variableIndex(name); @@ -181,7 +192,7 @@ class Environment { _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; - return _variableSpans[index][name]; + return _variableNodes[index][name]; } /// Returns whether a variable named [name] exists. @@ -199,12 +210,17 @@ class Environment { return null; } - /// Sets the variable named [name] to [value], associated with the given [span]. + /// Sets the variable named [name] to [value], associated with + /// [nodeWithSpan]'s source span. /// /// If [global] is `true`, this sets the variable at the top-level scope. /// Otherwise, if the variable was already defined, it'll set it in the /// previous scope. If it's undefined, it'll set it in the current scope. - void setVariable(String name, Value value, FileSpan span, + /// + /// 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 setVariable(String name, Value value, AstNode nodeWithSpan, {bool global = false}) { if (global || _variables.length == 1) { // Don't set the index if there's already a variable with the given name, @@ -216,7 +232,7 @@ class Environment { }); _variables.first[name] = value; - if (_variableSpans != null) _variableSpans.first[name] = span; + if (_variableNodes != null) _variableNodes.first[name] = nodeWithSpan; return; } @@ -232,20 +248,25 @@ class Environment { _lastVariableName = name; _lastVariableIndex = index; _variables[index][name] = value; - if (_variableSpans != null) _variableSpans[index][name] = span; + if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; } - /// Sets the variable named [name] to [value] in the current scope, associated with the given [span]. + /// Sets the variable named [name] to [value], associated with + /// [nodeWithSpan]'s source span. /// /// Unlike [setVariable], this will declare the variable in the current scope /// even if a declaration already exists in an outer scope. - void setLocalVariable(String name, Value value, FileSpan span) { + /// + /// 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 setLocalVariable(String name, Value value, AstNode nodeWithSpan) { var index = _variables.length - 1; _lastVariableName = name; _lastVariableIndex = index; _variableIndices[name] = index; _variables[index][name] = value; - if (_variableSpans != null) _variableSpans[index][name] = span; + if (_variableNodes != null) _variableNodes[index][name] = nodeWithSpan; } /// Returns the value of the function named [name], or `null` if no such @@ -361,7 +382,7 @@ class Environment { _inSemiGlobalScope = semiGlobal; _variables.add(normalizedMap()); - _variableSpans?.add(normalizedMap()); + _variableNodes?.add(normalizedMap()); _functions.add(normalizedMap()); _mixins.add(normalizedMap()); try { diff --git a/lib/src/utils.dart b/lib/src/utils.dart index e3a1b8fb7..4bbf31e95 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -174,10 +174,15 @@ Frame frameForSpan(SourceSpan span, String member, {Uri url}) => Frame( /// returns `null`. FileSpan spanForList(List nodes) { if (nodes.isEmpty) return null; + // Spans may be null for dynamically-constructed ASTs. - if (nodes.first.span == null) return null; - if (nodes.last.span == null) return null; - return nodes.first.span.expand(nodes.last.span); + var left = nodes.first.span; + if (left == null) return null; + + var right = nodes.last.span; + if (right == null) return null; + + return left.expand(right); } /// Returns [name] without a vendor prefix. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 3cf88e21d..31b7ebafc 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -13,6 +13,7 @@ import 'package:stack_trace/stack_trace.dart'; import 'package:tuple/tuple.dart'; import '../ast/css.dart'; +import '../ast/node.dart'; import '../ast/sass.dart'; import '../ast/selector.dart'; import '../async_environment.dart'; @@ -143,10 +144,13 @@ class _EvaluateVisitor /// The human-readable name of the current stack frame. var _member = "root stylesheet"; - /// The span for the innermost callable that's been invoked. + /// The node for the innermost callable that's been invoked. /// - /// This is used to provide `call()` with a span. - FileSpan _callableSpan; + /// This is used to provide `call()` with a span. It's stored as an [AstNode] + /// rather than a [FileSpan] so we can avoid calling [AstNode.span] if the + /// span isn't required, since some nodes need to do real work to manufacture + /// a source span. + AstNode _callableNode; /// Whether we're currently executing a function. var _inFunction = false; @@ -191,7 +195,11 @@ class _EvaluateVisitor /// /// Each member is a tuple of the span where the stack trace starts and the /// name of the member being invoked. - final _stack = >[]; + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final _stack = >[]; /// Whether we're running in Node Sass-compatibility mode. bool get _asNodeSass => _nodeImporter != null; @@ -262,8 +270,8 @@ class _EvaluateVisitor var function = arguments[0]; var args = arguments[1] as SassArgumentList; - var invocation = ArgumentInvocation([], {}, _callableSpan, - rest: ValueExpression(args, _callableSpan), + var invocation = ArgumentInvocation([], {}, _callableNode.span, + rest: ValueExpression(args, _callableNode.span), keywordRest: args.keywords.isEmpty ? null : ValueExpression( @@ -271,23 +279,23 @@ class _EvaluateVisitor key: (String key, Value _) => SassString(key, quotes: false), value: (String _, Value value) => value)), - _callableSpan)); + _callableNode.span)); if (function is SassString) { _warn( "Passing a string to call() is deprecated and will be illegal\n" "in Sass 4.0. Use call(get-function($function)) instead.", - _callableSpan, + _callableNode.span, deprecation: true); var expression = FunctionExpression( - Interpolation([function.text], _callableSpan), invocation); + Interpolation([function.text], _callableNode.span), invocation); return await expression.accept(this); } var callable = function.assertFunction("function").callable; if (callable is AsyncCallable) { - return await _runFunctionCallable(invocation, callable, _callableSpan); + return await _runFunctionCallable(invocation, callable, _callableNode); } else { throw SassScriptException( "The function ${callable.name} is asynchronous.\n" @@ -347,7 +355,7 @@ class _EvaluateVisitor var resolved = await _performInterpolation(node.query, warnForColor: true); query = _adjustParseError( - node.query.span, () => AtRootQuery.parse(resolved, logger: _logger)); + node.query, () => AtRootQuery.parse(resolved, logger: _logger)); } var parent = _parent; @@ -480,7 +488,7 @@ class _EvaluateVisitor var content = _environment.content; if (content == null) return null; - await _runUserDefinedCallable(node.arguments, content, node.span, () async { + await _runUserDefinedCallable(node.arguments, content, node, () async { for (var statement in content.declaration.children) { await statement.accept(this); } @@ -515,7 +523,7 @@ class _EvaluateVisitor if (cssValue != null && (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { _parent.addChild(CssDeclaration(name, cssValue, node.span, - valueSpanForMap: _expressionSpan(node.value))); + valueSpanForMap: _expressionNode(node.value)?.span)); } else if (name.value.startsWith('--')) { throw _exception( "Custom property values may not be empty.", node.value.span); @@ -540,11 +548,12 @@ class _EvaluateVisitor Future visitEachRule(EachRule node) async { var list = await node.list.accept(this); - var span = _expressionSpan(node.list); + var nodeForSpan = _expressionNode(node.list); var setVariables = node.variables.length == 1 ? (Value value) => _environment.setLocalVariable( - node.variables.first, value.withoutSlash(), span) - : (Value value) => _setMultipleVariables(node.variables, value, span); + node.variables.first, value.withoutSlash(), nodeForSpan) + : (Value value) => + _setMultipleVariables(node.variables, value, nodeForSpan); return _environment.scope(() { return _handleReturn(list.asList, (element) { setVariables(element); @@ -557,14 +566,15 @@ class _EvaluateVisitor /// Destructures [value] and assigns it to [variables], as in an `@each` /// statement. void _setMultipleVariables( - List variables, Value value, FileSpan span) { + List variables, Value value, AstNode nodeForSpan) { 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(), span); + _environment.setLocalVariable( + variables[i], list[i].withoutSlash(), nodeForSpan); } for (var i = minLength; i < variables.length; i++) { - _environment.setLocalVariable(variables[i], sassNull, span); + _environment.setLocalVariable(variables[i], sassNull, nodeForSpan); } } @@ -583,7 +593,7 @@ class _EvaluateVisitor await _interpolationToValue(node.selector, warnForColor: true); var list = _adjustParseError( - targetText.span, + targetText, () => SelectorList.parse( trimAscii(targetText.value, excludeEscape: true), logger: _logger, @@ -667,26 +677,27 @@ class _EvaluateVisitor } Future visitForRule(ForRule node) async { - var fromNumber = await _addExceptionSpanAsync(node.from.span, - () async => (await node.from.accept(this)).assertNumber()); + var fromNumber = await _addExceptionSpanAsync( + node.from, () async => (await node.from.accept(this)).assertNumber()); var toNumber = await _addExceptionSpanAsync( - node.to.span, () async => (await node.to.accept(this)).assertNumber()); + node.to, () async => (await node.to.accept(this)).assertNumber()); var from = _addExceptionSpan( - node.from.span, + node.from, () => fromNumber .coerce(toNumber.numeratorUnits, toNumber.denominatorUnits) .assertInt()); - var to = _addExceptionSpan(node.to.span, () => toNumber.assertInt()); + var to = _addExceptionSpan(node.to, () => toNumber.assertInt()); var direction = from > to ? -1 : 1; if (!node.isExclusive) to += direction; if (from == to) return null; return _environment.scope(() async { - var span = _expressionSpan(node.from); + var nodeForSpan = _expressionNode(node.from); for (var i = from; i != to; i += direction) { - _environment.setLocalVariable(node.variable, SassNumber(i), span); + _environment.setLocalVariable( + node.variable, SassNumber(i), nodeForSpan); var result = await _handleReturn( node.children, (child) => child.accept(this)); if (result != null) return result; @@ -740,7 +751,7 @@ class _EvaluateVisitor } _activeImports.add(url); - await _withStackFrame("@import", import.span, () async { + await _withStackFrame("@import", import, () async { await _withEnvironment(_environment.closure(), () async { var oldImporter = _importer; var oldStylesheet = _stylesheet; @@ -852,7 +863,7 @@ class _EvaluateVisitor var contentCallable = node.content == null ? null : UserDefinedCallable(node.content, _environment.closure()); - await _runUserDefinedCallable(node.arguments, mixin, node.span, () async { + await _runUserDefinedCallable(node.arguments, mixin, node, () async { await _environment.withContent(contentCallable, () async { await _environment.asMixin(() async { for (var statement in mixin.declaration.children) { @@ -934,7 +945,7 @@ class _EvaluateVisitor await _performInterpolation(interpolation, warnForColor: true); // TODO(nweiz): Remove this type argument when sdk#31398 is fixed. - return _adjustParseError>(interpolation.span, + return _adjustParseError>(interpolation, () => CssMediaQuery.parseList(resolved, logger: _logger)); } @@ -973,7 +984,7 @@ class _EvaluateVisitor trim: true, warnForColor: true); if (_inKeyframes) { var parsedSelector = _adjustParseError( - node.selector.span, + node.selector, () => KeyframeSelectorParser(selectorText.value, logger: _logger) .parse()); var rule = CssKeyframeBlock( @@ -990,13 +1001,13 @@ class _EvaluateVisitor } var parsedSelector = _adjustParseError( - node.selector.span, + node.selector, () => SelectorList.parse(selectorText.value, allowParent: !_stylesheet.plainCss, allowPlaceholder: !_stylesheet.plainCss, logger: _logger)); parsedSelector = _addExceptionSpan( - node.selector.span, + node.selector, () => parsedSelector.resolveParentSelectors( _styleRule?.originalSelector, implicitParent: !_atRootExcludingStyleRule)); @@ -1102,18 +1113,16 @@ class _EvaluateVisitor _environment.setVariable( node.name, (await node.expression.accept(this)).withoutSlash(), - _expressionSpan(node.expression), + _expressionNode(node.expression), global: node.isGlobal); return null; } Future visitWarnRule(WarnRule node) async { - var value = await _addExceptionSpanAsync( - node.span, () => node.expression.accept(this)); + var value = + await _addExceptionSpanAsync(node, () => node.expression.accept(this)); _logger.warn( - value is SassString - ? value.text - : _serialize(value, node.expression.span), + value is SassString ? value.text : _serialize(value, node.expression), trace: _stackTrace(node.span)); return null; } @@ -1132,7 +1141,7 @@ class _EvaluateVisitor // ## Expressions Future visitBinaryOperationExpression(BinaryOperationExpression node) { - return _addExceptionSpanAsync(node.span, () async { + return _addExceptionSpanAsync(node, () async { var left = await node.left.accept(this); switch (node.operator) { case BinaryOperator.singleEquals: @@ -1233,8 +1242,7 @@ class _EvaluateVisitor var positional = pair.item1; var named = pair.item2; - _verifyArguments( - positional.length, named, IfExpression.declaration, node.span); + _verifyArguments(positional.length, named, IfExpression.declaration, node); var condition = positional.length > 0 ? positional[0] : named["condition"]; var ifTrue = positional.length > 1 ? positional[1] : named["if-true"]; @@ -1282,8 +1290,7 @@ class _EvaluateVisitor var oldInFunction = _inFunction; _inFunction = true; - var result = - await _runFunctionCallable(node.arguments, function, node.span); + var result = await _runFunctionCallable(node.arguments, function, node); _inFunction = oldInFunction; return result; } @@ -1293,18 +1300,18 @@ class _EvaluateVisitor Future _runUserDefinedCallable( ArgumentInvocation arguments, UserDefinedCallable callable, - FileSpan span, + AstNode nodeWithSpan, Future run()) async { var evaluated = await _evaluateArguments(arguments); var name = callable.name == null ? "@content" : callable.name + "()"; - return await _withStackFrame(name, span, () { + return await _withStackFrame(name, nodeWithSpan, () { // Add an extra closure() call so that modifications to the environment // don't affect the underlying environment closure. return _withEnvironment(callable.environment.closure(), () { return _environment.scope(() async { _verifyArguments(evaluated.positional.length, evaluated.named, - callable.declaration.arguments, span); + callable.declaration.arguments, nodeWithSpan); var declaredArguments = callable.declaration.arguments.arguments; var minLength = @@ -1313,7 +1320,7 @@ class _EvaluateVisitor _environment.setLocalVariable( declaredArguments[i].name, evaluated.positional[i].withoutSlash(), - _sourceMap ? evaluated.positionalSpans[i] : null); + _sourceMap ? evaluated.positionalNodes[i] : null); } for (var i = evaluated.positional.length; @@ -1326,8 +1333,8 @@ class _EvaluateVisitor argument.name, value.withoutSlash(), _sourceMap - ? evaluated.namedSpans[argument.name] ?? - _expressionSpan(argument.defaultValue) + ? evaluated.namedNodes[argument.name] ?? + _expressionNode(argument.defaultValue) : null); } @@ -1345,7 +1352,7 @@ class _EvaluateVisitor _environment.setLocalVariable( callable.declaration.arguments.restArgument, argumentList, - span); + nodeWithSpan); } var result = await run(); @@ -1357,7 +1364,8 @@ 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.", span); + throw _exception( + "No $argumentWord named $argumentNames.", nodeWithSpan.span); }); }); }); @@ -1365,12 +1373,12 @@ class _EvaluateVisitor /// Evaluates [arguments] as applied to [callable]. Future _runFunctionCallable(ArgumentInvocation arguments, - AsyncCallable callable, FileSpan span) async { + AsyncCallable callable, AstNode nodeWithSpan) async { if (callable is AsyncBuiltInCallable) { - return (await _runBuiltInCallable(arguments, callable, span)) + return (await _runBuiltInCallable(arguments, callable, nodeWithSpan)) .withoutSlash(); } else if (callable is UserDefinedCallable) { - return (await _runUserDefinedCallable(arguments, callable, span, + return (await _runUserDefinedCallable(arguments, callable, nodeWithSpan, () async { for (var statement in callable.declaration.children) { var returnValue = await statement.accept(this); @@ -1383,8 +1391,8 @@ class _EvaluateVisitor .withoutSlash(); } else if (callable is PlainCssCallable) { if (arguments.named.isNotEmpty || arguments.keywordRest != null) { - throw _exception( - "Plain CSS functions don't support keyword arguments.", span); + throw _exception("Plain CSS functions don't support keyword arguments.", + nodeWithSpan.span); } var buffer = StringBuffer("${callable.name}("); @@ -1402,7 +1410,7 @@ class _EvaluateVisitor var rest = await arguments.rest?.accept(this); if (rest != null) { if (!first) buffer.write(", "); - buffer.write(_serialize(rest, arguments.rest.span)); + buffer.write(_serialize(rest, arguments.rest)); } buffer.writeCharCode($rparen); @@ -1415,18 +1423,18 @@ class _EvaluateVisitor /// Evaluates [invocation] as applied to [callable], and invokes [callable]'s /// body. Future _runBuiltInCallable(ArgumentInvocation arguments, - AsyncBuiltInCallable callable, FileSpan span) async { + AsyncBuiltInCallable callable, AstNode nodeWithSpan) async { var evaluated = await _evaluateArguments(arguments, trackSpans: false); - var oldCallableSpan = _callableSpan; - _callableSpan = span; + var oldCallableNode = _callableNode; + _callableNode = nodeWithSpan; var namedSet = MapKeySet(evaluated.named); var tuple = callable.callbackFor(evaluated.positional.length, namedSet); var overload = tuple.item1; var callback = tuple.item2; - _addExceptionSpan( - span, () => overload.verify(evaluated.positional.length, namedSet)); + _addExceptionSpan(nodeWithSpan, + () => overload.verify(evaluated.positional.length, namedSet)); var declaredArguments = overload.arguments; for (var i = evaluated.positional.length; @@ -1466,9 +1474,9 @@ class _EvaluateVisitor } catch (_) { message = error.toString(); } - throw _exception(message, span); + throw _exception(message, nodeWithSpan.span); } - _callableSpan = oldCallableSpan; + _callableNode = oldCallableNode; if (argumentList == null) return result; if (evaluated.named.isEmpty) return result; @@ -1476,7 +1484,7 @@ class _EvaluateVisitor throw _exception( "No ${pluralize('argument', evaluated.named.keys.length)} named " "${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.", - span); + nodeWithSpan.span); } /// Returns the evaluated values of the given [arguments]. @@ -1494,57 +1502,57 @@ class _EvaluateVisitor arguments.named, value: (_, expression) => expression.accept(this)); - var positionalSpans = - trackSpans ? arguments.positional.map(_expressionSpan).toList() : null; - var namedSpans = trackSpans - ? mapMap(arguments.named, - value: (_, expression) => _expressionSpan(expression)) + var positionalNodes = + trackSpans ? arguments.positional.map(_expressionNode).toList() : null; + var namedNodes = trackSpans + ? mapMap(arguments.named, + value: (_, expression) => _expressionNode(expression)) : null; if (arguments.rest == null) { return _ArgumentResults(positional, named, ListSeparator.undecided, - positionalSpans: positionalSpans, namedSpans: namedSpans); + positionalNodes: positionalNodes, namedNodes: namedNodes); } var rest = await arguments.rest.accept(this); - var restSpan = trackSpans ? _expressionSpan(arguments.rest) : null; + var restNodeForSpan = trackSpans ? _expressionNode(arguments.rest) : null; var separator = ListSeparator.undecided; if (rest is SassMap) { - _addRestMap(named, rest, arguments.rest.span); - namedSpans?.addAll(mapMap(rest.contents, + _addRestMap(named, rest, arguments.rest); + namedNodes?.addAll(mapMap(rest.contents, key: (key, _) => (key as SassString).text, - value: (_, __) => restSpan)); + value: (_, __) => restNodeForSpan)); } else if (rest is SassList) { positional.addAll(rest.asList); - positionalSpans?.addAll(List.filled(rest.lengthAsList, restSpan)); + positionalNodes?.addAll(List.filled(rest.lengthAsList, restNodeForSpan)); separator = rest.separator; if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { named[key] = value; - if (namedSpans != null) namedSpans[key] = restSpan; + if (namedNodes != null) namedNodes[key] = restNodeForSpan; }); } } else { positional.add(rest); - positionalSpans?.add(restSpan); + positionalNodes?.add(restNodeForSpan); } if (arguments.keywordRest == null) { return _ArgumentResults(positional, named, separator, - positionalSpans: positionalSpans, namedSpans: namedSpans); + positionalNodes: positionalNodes, namedNodes: namedNodes); } var keywordRest = await arguments.keywordRest.accept(this); - var keywordRestSpan = - trackSpans ? _expressionSpan(arguments.keywordRest) : null; + var keywordRestNodeForSpan = + trackSpans ? _expressionNode(arguments.keywordRest) : null; if (keywordRest is SassMap) { - _addRestMap(named, keywordRest, arguments.keywordRest.span); - namedSpans?.addAll(mapMap(keywordRest.contents, + _addRestMap(named, keywordRest, arguments.keywordRest); + namedNodes?.addAll(mapMap(keywordRest.contents, key: (key, _) => (key as SassString).text, - value: (_, __) => keywordRestSpan)); + value: (_, __) => keywordRestNodeForSpan)); return _ArgumentResults(positional, named, separator, - positionalSpans: positionalSpans, namedSpans: namedSpans); + positionalNodes: positionalNodes, namedNodes: namedNodes); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", @@ -1568,8 +1576,7 @@ class _EvaluateVisitor var named = normalizedMap(invocation.arguments.named); var rest = await invocation.arguments.rest.accept(this); if (rest is SassMap) { - _addRestMap( - named, rest, invocation.span, (value) => ValueExpression(value)); + _addRestMap(named, rest, invocation, (value) => ValueExpression(value)); } else if (rest is SassList) { positional.addAll(rest.asList.map((value) => ValueExpression(value))); if (rest is SassArgumentList) { @@ -1587,8 +1594,8 @@ class _EvaluateVisitor var keywordRest = await invocation.arguments.keywordRest.accept(this); if (keywordRest is SassMap) { - _addRestMap(named, keywordRest, invocation.span, - (value) => ValueExpression(value)); + _addRestMap( + named, keywordRest, invocation, (value) => ValueExpression(value)); return Tuple2(positional, named); } else { throw _exception( @@ -1599,12 +1606,16 @@ class _EvaluateVisitor /// Adds the values in [map] to [values]. /// - /// Throws a [SassRuntimeException] associated with [span] if any [map] keys - /// aren't strings. + /// Throws a [SassRuntimeException] associated with [nodeForSpan]'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 /// type for [values]. Otherwise, the [Value]s are used as-is. - void _addRestMap(Map values, SassMap map, FileSpan span, + /// + /// 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(Map values, SassMap map, AstNode nodeForSpan, [T convert(Value value)]) { convert ??= (value) => value as T; map.contents.forEach((key, value) { @@ -1614,7 +1625,7 @@ class _EvaluateVisitor throw _exception( "Variable keyword argument map must have string keys.\n" "$key is not a string in $map.", - span); + nodeForSpan.span); } }); } @@ -1622,9 +1633,9 @@ class _EvaluateVisitor /// Throws a [SassRuntimeException] if [positional] and [named] aren't valid /// when applied to [arguments]. void _verifyArguments(int positional, Map named, - ArgumentDeclaration arguments, FileSpan span) => + ArgumentDeclaration arguments, AstNode nodeWithSpan) => _addExceptionSpan( - span, () => arguments.verify(positional, MapKeySet(named))); + nodeWithSpan, () => arguments.verify(positional, MapKeySet(named))); Future visitSelectorExpression(SelectorExpression node) async { if (_styleRule == null) return sassNull; @@ -1641,7 +1652,7 @@ class _EvaluateVisitor var result = await expression.accept(this); return result is SassString ? result.text - : _serialize(result, expression.span, quote: false); + : _serialize(result, expression, quote: false); })) .join(), quotes: node.hasQuotes); @@ -1714,7 +1725,7 @@ class _EvaluateVisitor expression.span); } - return _serialize(result, expression.span, quote: false); + return _serialize(result, expression, quote: false); })) .join(); } @@ -1723,23 +1734,34 @@ class _EvaluateVisitor /// [SassScriptException] to associate it with [span]. Future _evaluateToCss(Expression expression, {bool quote = true}) async => - _serialize(await expression.accept(this), expression.span, quote: quote); + _serialize(await expression.accept(this), expression, quote: quote); /// Calls `value.toCssString()` and wraps a [SassScriptException] to associate - /// it with [span]. - String _serialize(Value value, FileSpan span, {bool quote = true}) => - _addExceptionSpan(span, () => value.toCssString(quote: quote)); + /// it with [nodeWithSpan]'s source span. + /// + /// 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. + String _serialize(Value value, AstNode nodeWithSpan, {bool quote = true}) => + _addExceptionSpan(nodeWithSpan, () => value.toCssString(quote: quote)); - /// Returns the span for [expression], or if [expression] is just a variable - /// reference for the span where it was declared. + /// Returns the [AstNode] whose span should be used for [expression]. + /// + /// If [expression] is a variable reference, [AstNode]'s span will be the span + /// where that variable was originally declared. Otherwise, this will just + /// return [expression]. /// /// Returns `null` if [_sourceMap] is `false`. - FileSpan _expressionSpan(Expression expression) { + /// + /// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling + /// [AstNode.span] if the span isn't required, since some nodes need to do + /// real work to manufacture a source span. + AstNode _expressionNode(Expression expression) { if (!_sourceMap) return null; if (expression is VariableExpression) { - return _environment.getVariableSpan(expression.name); + return _environment.getVariableNode(expression.name); } else { - return expression.span; + return expression; } } @@ -1800,13 +1822,17 @@ class _EvaluateVisitor return result; } - /// Adds a frame to the stack with the given [member] name, and [span] as the - /// site of the new frame. + /// Adds a frame to the stack with the given [member] name, and [nodeWithSpan] + /// as the site of the new frame. /// /// Runs [callback] with the new stack. + /// + /// 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. Future _withStackFrame( - String member, FileSpan span, Future callback()) async { - _stack.add(Tuple2(_member, span)); + String member, AstNode nodeWithSpan, Future callback()) async { + _stack.add(Tuple2(_member, nodeWithSpan)); var oldMember = _member; _member = member; var result = await callback(); @@ -1828,7 +1854,7 @@ class _EvaluateVisitor /// [span] is the current location, used for the bottom-most stack frame. Trace _stackTrace(FileSpan span) { var frames = _stack - .map((tuple) => _stackFrame(tuple.item1, tuple.item2)) + .map((tuple) => _stackFrame(tuple.item1, tuple.item2.span)) .toList() ..add(_stackFrame(_member, span)); return Trace(frames.reversed); @@ -1843,17 +1869,23 @@ class _EvaluateVisitor SassRuntimeException _exception(String message, FileSpan span) => SassRuntimeException(message, span, _stackTrace(span)); - /// Runs [callback], and adjusts any [SassFormatException] to be within [span]. + /// Runs [callback], and adjusts any [SassFormatException] to be within + /// [nodeWithSpan]'s source span. /// /// Specifically, this adjusts format exceptions so that the errors are /// reported as though the text being parsed were exactly in [span]. This may /// not be quite accurate if the source text contained interpolation, but /// it'll still produce a useful error. - T _adjustParseError(FileSpan span, T callback()) { + /// + /// 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. + T _adjustParseError(AstNode nodeWithSpan, T callback()) { try { return callback(); } on SassFormatException catch (error) { var errorText = error.span.file.getText(0); + var span = nodeWithSpan.span; var syntheticFile = span.file .getText(0) .replaceRange(span.start.offset, span.end.offset, errorText); @@ -1866,22 +1898,26 @@ class _EvaluateVisitor } /// Runs [callback], and converts any [SassScriptException]s it throws to - /// [SassRuntimeException]s with [span]. - T _addExceptionSpan(FileSpan span, T callback()) { + /// [SassRuntimeException]s with [nodeWithSpan]'s source span. + /// + /// 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. + T _addExceptionSpan(AstNode nodeWithSpan, T callback()) { try { return callback(); } on SassScriptException catch (error) { - throw _exception(error.message, span); + throw _exception(error.message, nodeWithSpan.span); } } /// Like [_addExceptionSpan], but for an asynchronous [callback]. Future _addExceptionSpanAsync( - FileSpan span, Future callback()) async { + AstNode nodeWithSpan, Future callback()) async { try { return await callback(); } on SassScriptException catch (error) { - throw _exception(error.message, span); + throw _exception(error.message, nodeWithSpan.span); } } } @@ -1907,20 +1943,28 @@ class _ArgumentResults { /// Arguments passed by position. final List positional; - /// The spans for each [positional] argument, or `null` if source span - /// tracking is disabled. - final List positionalSpans; + /// The [AstNode]s that hold the spans for each [positional] argument, or + /// `null` if source span tracking is disabled. + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final List positionalNodes; /// Arguments passed by name. final Map named; - /// The spans for each [named] argument, or `null` if source span tracking is - /// disabled. - final Map namedSpans; + /// The [AstNode]s that hold the spans for each [named] argument, or `null` if + /// source span tracking is disabled. + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final Map namedNodes; /// The separator used for the rest argument list, if any. final ListSeparator separator; _ArgumentResults(this.positional, this.named, this.separator, - {this.positionalSpans, this.namedSpans}); + {this.positionalNodes, this.namedNodes}); } diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index f77f3a523..af8fbeeaf 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/synchronize.dart for details. // -// Checksum: 2065f7d701646283e536c5300b0f20f38c4acb46 +// Checksum: 47ed1d2d77cf7e8f169e4e6b6c9d62d07455425a // // ignore_for_file: unused_import @@ -22,6 +22,7 @@ import 'package:stack_trace/stack_trace.dart'; import 'package:tuple/tuple.dart'; import '../ast/css.dart'; +import '../ast/node.dart'; import '../ast/sass.dart'; import '../ast/selector.dart'; import '../environment.dart'; @@ -150,10 +151,13 @@ class _EvaluateVisitor /// The human-readable name of the current stack frame. var _member = "root stylesheet"; - /// The span for the innermost callable that's been invoked. + /// The node for the innermost callable that's been invoked. /// - /// This is used to provide `call()` with a span. - FileSpan _callableSpan; + /// This is used to provide `call()` with a span. It's stored as an [AstNode] + /// rather than a [FileSpan] so we can avoid calling [AstNode.span] if the + /// span isn't required, since some nodes need to do real work to manufacture + /// a source span. + AstNode _callableNode; /// Whether we're currently executing a function. var _inFunction = false; @@ -198,7 +202,11 @@ class _EvaluateVisitor /// /// Each member is a tuple of the span where the stack trace starts and the /// name of the member being invoked. - final _stack = >[]; + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final _stack = >[]; /// Whether we're running in Node Sass-compatibility mode. bool get _asNodeSass => _nodeImporter != null; @@ -269,8 +277,8 @@ class _EvaluateVisitor var function = arguments[0]; var args = arguments[1] as SassArgumentList; - var invocation = ArgumentInvocation([], {}, _callableSpan, - rest: ValueExpression(args, _callableSpan), + var invocation = ArgumentInvocation([], {}, _callableNode.span, + rest: ValueExpression(args, _callableNode.span), keywordRest: args.keywords.isEmpty ? null : ValueExpression( @@ -278,23 +286,23 @@ class _EvaluateVisitor key: (String key, Value _) => SassString(key, quotes: false), value: (String _, Value value) => value)), - _callableSpan)); + _callableNode.span)); if (function is SassString) { _warn( "Passing a string to call() is deprecated and will be illegal\n" "in Sass 4.0. Use call(get-function($function)) instead.", - _callableSpan, + _callableNode.span, deprecation: true); var expression = FunctionExpression( - Interpolation([function.text], _callableSpan), invocation); + Interpolation([function.text], _callableNode.span), invocation); return expression.accept(this); } var callable = function.assertFunction("function").callable; if (callable is Callable) { - return _runFunctionCallable(invocation, callable, _callableSpan); + return _runFunctionCallable(invocation, callable, _callableNode); } else { throw SassScriptException( "The function ${callable.name} is asynchronous.\n" @@ -353,7 +361,7 @@ class _EvaluateVisitor if (node.query != null) { var resolved = _performInterpolation(node.query, warnForColor: true); query = _adjustParseError( - node.query.span, () => AtRootQuery.parse(resolved, logger: _logger)); + node.query, () => AtRootQuery.parse(resolved, logger: _logger)); } var parent = _parent; @@ -486,7 +494,7 @@ class _EvaluateVisitor var content = _environment.content; if (content == null) return null; - _runUserDefinedCallable(node.arguments, content, node.span, () { + _runUserDefinedCallable(node.arguments, content, node, () { for (var statement in content.declaration.children) { statement.accept(this); } @@ -521,7 +529,7 @@ class _EvaluateVisitor if (cssValue != null && (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { _parent.addChild(CssDeclaration(name, cssValue, node.span, - valueSpanForMap: _expressionSpan(node.value))); + valueSpanForMap: _expressionNode(node.value)?.span)); } else if (name.value.startsWith('--')) { throw _exception( "Custom property values may not be empty.", node.value.span); @@ -546,11 +554,12 @@ class _EvaluateVisitor Value visitEachRule(EachRule node) { var list = node.list.accept(this); - var span = _expressionSpan(node.list); + var nodeForSpan = _expressionNode(node.list); var setVariables = node.variables.length == 1 ? (Value value) => _environment.setLocalVariable( - node.variables.first, value.withoutSlash(), span) - : (Value value) => _setMultipleVariables(node.variables, value, span); + node.variables.first, value.withoutSlash(), nodeForSpan) + : (Value value) => + _setMultipleVariables(node.variables, value, nodeForSpan); return _environment.scope(() { return _handleReturn(list.asList, (element) { setVariables(element); @@ -563,14 +572,15 @@ class _EvaluateVisitor /// Destructures [value] and assigns it to [variables], as in an `@each` /// statement. void _setMultipleVariables( - List variables, Value value, FileSpan span) { + List variables, Value value, AstNode nodeForSpan) { 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(), span); + _environment.setLocalVariable( + variables[i], list[i].withoutSlash(), nodeForSpan); } for (var i = minLength; i < variables.length; i++) { - _environment.setLocalVariable(variables[i], sassNull, span); + _environment.setLocalVariable(variables[i], sassNull, nodeForSpan); } } @@ -587,7 +597,7 @@ class _EvaluateVisitor var targetText = _interpolationToValue(node.selector, warnForColor: true); var list = _adjustParseError( - targetText.span, + targetText, () => SelectorList.parse( trimAscii(targetText.value, excludeEscape: true), logger: _logger, @@ -671,25 +681,26 @@ class _EvaluateVisitor Value visitForRule(ForRule node) { var fromNumber = _addExceptionSpan( - node.from.span, () => node.from.accept(this).assertNumber()); - var toNumber = _addExceptionSpan( - node.to.span, () => node.to.accept(this).assertNumber()); + node.from, () => node.from.accept(this).assertNumber()); + var toNumber = + _addExceptionSpan(node.to, () => node.to.accept(this).assertNumber()); var from = _addExceptionSpan( - node.from.span, + node.from, () => fromNumber .coerce(toNumber.numeratorUnits, toNumber.denominatorUnits) .assertInt()); - var to = _addExceptionSpan(node.to.span, () => toNumber.assertInt()); + var to = _addExceptionSpan(node.to, () => toNumber.assertInt()); var direction = from > to ? -1 : 1; if (!node.isExclusive) to += direction; if (from == to) return null; return _environment.scope(() { - var span = _expressionSpan(node.from); + var nodeForSpan = _expressionNode(node.from); for (var i = from; i != to; i += direction) { - _environment.setLocalVariable(node.variable, SassNumber(i), span); + _environment.setLocalVariable( + node.variable, SassNumber(i), nodeForSpan); var result = _handleReturn( node.children, (child) => child.accept(this)); if (result != null) return result; @@ -743,7 +754,7 @@ class _EvaluateVisitor } _activeImports.add(url); - _withStackFrame("@import", import.span, () { + _withStackFrame("@import", import, () { _withEnvironment(_environment.closure(), () { var oldImporter = _importer; var oldStylesheet = _stylesheet; @@ -853,7 +864,7 @@ class _EvaluateVisitor var contentCallable = node.content == null ? null : UserDefinedCallable(node.content, _environment.closure()); - _runUserDefinedCallable(node.arguments, mixin, node.span, () { + _runUserDefinedCallable(node.arguments, mixin, node, () { _environment.withContent(contentCallable, () { _environment.asMixin(() { for (var statement in mixin.declaration.children) { @@ -931,7 +942,7 @@ class _EvaluateVisitor var resolved = _performInterpolation(interpolation, warnForColor: true); // TODO(nweiz): Remove this type argument when sdk#31398 is fixed. - return _adjustParseError>(interpolation.span, + return _adjustParseError>(interpolation, () => CssMediaQuery.parseList(resolved, logger: _logger)); } @@ -969,7 +980,7 @@ class _EvaluateVisitor _interpolationToValue(node.selector, trim: true, warnForColor: true); if (_inKeyframes) { var parsedSelector = _adjustParseError( - node.selector.span, + node.selector, () => KeyframeSelectorParser(selectorText.value, logger: _logger) .parse()); var rule = CssKeyframeBlock( @@ -986,13 +997,13 @@ class _EvaluateVisitor } var parsedSelector = _adjustParseError( - node.selector.span, + node.selector, () => SelectorList.parse(selectorText.value, allowParent: !_stylesheet.plainCss, allowPlaceholder: !_stylesheet.plainCss, logger: _logger)); parsedSelector = _addExceptionSpan( - node.selector.span, + node.selector, () => parsedSelector.resolveParentSelectors( _styleRule?.originalSelector, implicitParent: !_atRootExcludingStyleRule)); @@ -1097,18 +1108,15 @@ class _EvaluateVisitor _environment.setVariable( node.name, node.expression.accept(this).withoutSlash(), - _expressionSpan(node.expression), + _expressionNode(node.expression), global: node.isGlobal); return null; } Value visitWarnRule(WarnRule node) { - var value = - _addExceptionSpan(node.span, () => node.expression.accept(this)); + var value = _addExceptionSpan(node, () => node.expression.accept(this)); _logger.warn( - value is SassString - ? value.text - : _serialize(value, node.expression.span), + value is SassString ? value.text : _serialize(value, node.expression), trace: _stackTrace(node.span)); return null; } @@ -1127,7 +1135,7 @@ class _EvaluateVisitor // ## Expressions Value visitBinaryOperationExpression(BinaryOperationExpression node) { - return _addExceptionSpan(node.span, () { + return _addExceptionSpan(node, () { var left = node.left.accept(this); switch (node.operator) { case BinaryOperator.singleEquals: @@ -1227,8 +1235,7 @@ class _EvaluateVisitor var positional = pair.item1; var named = pair.item2; - _verifyArguments( - positional.length, named, IfExpression.declaration, node.span); + _verifyArguments(positional.length, named, IfExpression.declaration, node); var condition = positional.length > 0 ? positional[0] : named["condition"]; var ifTrue = positional.length > 1 ? positional[1] : named["if-true"]; @@ -1273,25 +1280,28 @@ class _EvaluateVisitor var oldInFunction = _inFunction; _inFunction = true; - var result = _runFunctionCallable(node.arguments, function, node.span); + var result = _runFunctionCallable(node.arguments, function, node); _inFunction = oldInFunction; return result; } /// Evaluates the arguments in [arguments] as applied to [callable], and /// invokes [run] in a scope with those arguments defined. - Value _runUserDefinedCallable(ArgumentInvocation arguments, - UserDefinedCallable callable, FileSpan span, Value run()) { + Value _runUserDefinedCallable( + ArgumentInvocation arguments, + UserDefinedCallable callable, + AstNode nodeWithSpan, + Value run()) { var evaluated = _evaluateArguments(arguments); var name = callable.name == null ? "@content" : callable.name + "()"; - return _withStackFrame(name, span, () { + return _withStackFrame(name, nodeWithSpan, () { // Add an extra closure() call so that modifications to the environment // don't affect the underlying environment closure. return _withEnvironment(callable.environment.closure(), () { return _environment.scope(() { _verifyArguments(evaluated.positional.length, evaluated.named, - callable.declaration.arguments, span); + callable.declaration.arguments, nodeWithSpan); var declaredArguments = callable.declaration.arguments.arguments; var minLength = @@ -1300,7 +1310,7 @@ class _EvaluateVisitor _environment.setLocalVariable( declaredArguments[i].name, evaluated.positional[i].withoutSlash(), - _sourceMap ? evaluated.positionalSpans[i] : null); + _sourceMap ? evaluated.positionalNodes[i] : null); } for (var i = evaluated.positional.length; @@ -1313,8 +1323,8 @@ class _EvaluateVisitor argument.name, value.withoutSlash(), _sourceMap - ? evaluated.namedSpans[argument.name] ?? - _expressionSpan(argument.defaultValue) + ? evaluated.namedNodes[argument.name] ?? + _expressionNode(argument.defaultValue) : null); } @@ -1332,7 +1342,7 @@ class _EvaluateVisitor _environment.setLocalVariable( callable.declaration.arguments.restArgument, argumentList, - span); + nodeWithSpan); } var result = run(); @@ -1344,7 +1354,8 @@ 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.", span); + throw _exception( + "No $argumentWord named $argumentNames.", nodeWithSpan.span); }); }); }); @@ -1352,11 +1363,12 @@ class _EvaluateVisitor /// Evaluates [arguments] as applied to [callable]. Value _runFunctionCallable( - ArgumentInvocation arguments, Callable callable, FileSpan span) { + ArgumentInvocation arguments, Callable callable, AstNode nodeWithSpan) { if (callable is BuiltInCallable) { - return _runBuiltInCallable(arguments, callable, span).withoutSlash(); + return _runBuiltInCallable(arguments, callable, nodeWithSpan) + .withoutSlash(); } else if (callable is UserDefinedCallable) { - return _runUserDefinedCallable(arguments, callable, span, () { + return _runUserDefinedCallable(arguments, callable, nodeWithSpan, () { for (var statement in callable.declaration.children) { var returnValue = statement.accept(this); if (returnValue is Value) return returnValue; @@ -1367,8 +1379,8 @@ class _EvaluateVisitor }).withoutSlash(); } else if (callable is PlainCssCallable) { if (arguments.named.isNotEmpty || arguments.keywordRest != null) { - throw _exception( - "Plain CSS functions don't support keyword arguments.", span); + throw _exception("Plain CSS functions don't support keyword arguments.", + nodeWithSpan.span); } var buffer = StringBuffer("${callable.name}("); @@ -1386,7 +1398,7 @@ class _EvaluateVisitor var rest = arguments.rest?.accept(this); if (rest != null) { if (!first) buffer.write(", "); - buffer.write(_serialize(rest, arguments.rest.span)); + buffer.write(_serialize(rest, arguments.rest)); } buffer.writeCharCode($rparen); @@ -1398,19 +1410,19 @@ class _EvaluateVisitor /// Evaluates [invocation] as applied to [callable], and invokes [callable]'s /// body. - Value _runBuiltInCallable( - ArgumentInvocation arguments, BuiltInCallable callable, FileSpan span) { + Value _runBuiltInCallable(ArgumentInvocation arguments, + BuiltInCallable callable, AstNode nodeWithSpan) { var evaluated = _evaluateArguments(arguments, trackSpans: false); - var oldCallableSpan = _callableSpan; - _callableSpan = span; + var oldCallableNode = _callableNode; + _callableNode = nodeWithSpan; var namedSet = MapKeySet(evaluated.named); var tuple = callable.callbackFor(evaluated.positional.length, namedSet); var overload = tuple.item1; var callback = tuple.item2; - _addExceptionSpan( - span, () => overload.verify(evaluated.positional.length, namedSet)); + _addExceptionSpan(nodeWithSpan, + () => overload.verify(evaluated.positional.length, namedSet)); var declaredArguments = overload.arguments; for (var i = evaluated.positional.length; @@ -1450,9 +1462,9 @@ class _EvaluateVisitor } catch (_) { message = error.toString(); } - throw _exception(message, span); + throw _exception(message, nodeWithSpan.span); } - _callableSpan = oldCallableSpan; + _callableNode = oldCallableNode; if (argumentList == null) return result; if (evaluated.named.isEmpty) return result; @@ -1460,7 +1472,7 @@ class _EvaluateVisitor throw _exception( "No ${pluralize('argument', evaluated.named.keys.length)} named " "${toSentence(evaluated.named.keys.map((name) => "\$$name"), 'or')}.", - span); + nodeWithSpan.span); } /// Returns the evaluated values of the given [arguments]. @@ -1477,57 +1489,57 @@ class _EvaluateVisitor var named = normalizedMapMap(arguments.named, value: (_, expression) => expression.accept(this)); - var positionalSpans = - trackSpans ? arguments.positional.map(_expressionSpan).toList() : null; - var namedSpans = trackSpans - ? mapMap(arguments.named, - value: (_, expression) => _expressionSpan(expression)) + var positionalNodes = + trackSpans ? arguments.positional.map(_expressionNode).toList() : null; + var namedNodes = trackSpans + ? mapMap(arguments.named, + value: (_, expression) => _expressionNode(expression)) : null; if (arguments.rest == null) { return _ArgumentResults(positional, named, ListSeparator.undecided, - positionalSpans: positionalSpans, namedSpans: namedSpans); + positionalNodes: positionalNodes, namedNodes: namedNodes); } var rest = arguments.rest.accept(this); - var restSpan = trackSpans ? _expressionSpan(arguments.rest) : null; + var restNodeForSpan = trackSpans ? _expressionNode(arguments.rest) : null; var separator = ListSeparator.undecided; if (rest is SassMap) { - _addRestMap(named, rest, arguments.rest.span); - namedSpans?.addAll(mapMap(rest.contents, + _addRestMap(named, rest, arguments.rest); + namedNodes?.addAll(mapMap(rest.contents, key: (key, _) => (key as SassString).text, - value: (_, __) => restSpan)); + value: (_, __) => restNodeForSpan)); } else if (rest is SassList) { positional.addAll(rest.asList); - positionalSpans?.addAll(List.filled(rest.lengthAsList, restSpan)); + positionalNodes?.addAll(List.filled(rest.lengthAsList, restNodeForSpan)); separator = rest.separator; if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { named[key] = value; - if (namedSpans != null) namedSpans[key] = restSpan; + if (namedNodes != null) namedNodes[key] = restNodeForSpan; }); } } else { positional.add(rest); - positionalSpans?.add(restSpan); + positionalNodes?.add(restNodeForSpan); } if (arguments.keywordRest == null) { return _ArgumentResults(positional, named, separator, - positionalSpans: positionalSpans, namedSpans: namedSpans); + positionalNodes: positionalNodes, namedNodes: namedNodes); } var keywordRest = arguments.keywordRest.accept(this); - var keywordRestSpan = - trackSpans ? _expressionSpan(arguments.keywordRest) : null; + var keywordRestNodeForSpan = + trackSpans ? _expressionNode(arguments.keywordRest) : null; if (keywordRest is SassMap) { - _addRestMap(named, keywordRest, arguments.keywordRest.span); - namedSpans?.addAll(mapMap(keywordRest.contents, + _addRestMap(named, keywordRest, arguments.keywordRest); + namedNodes?.addAll(mapMap(keywordRest.contents, key: (key, _) => (key as SassString).text, - value: (_, __) => keywordRestSpan)); + value: (_, __) => keywordRestNodeForSpan)); return _ArgumentResults(positional, named, separator, - positionalSpans: positionalSpans, namedSpans: namedSpans); + positionalNodes: positionalNodes, namedNodes: namedNodes); } else { throw _exception( "Variable keyword arguments must be a map (was $keywordRest).", @@ -1551,8 +1563,7 @@ class _EvaluateVisitor var named = normalizedMap(invocation.arguments.named); var rest = invocation.arguments.rest.accept(this); if (rest is SassMap) { - _addRestMap( - named, rest, invocation.span, (value) => ValueExpression(value)); + _addRestMap(named, rest, invocation, (value) => ValueExpression(value)); } else if (rest is SassList) { positional.addAll(rest.asList.map((value) => ValueExpression(value))); if (rest is SassArgumentList) { @@ -1570,8 +1581,8 @@ class _EvaluateVisitor var keywordRest = invocation.arguments.keywordRest.accept(this); if (keywordRest is SassMap) { - _addRestMap(named, keywordRest, invocation.span, - (value) => ValueExpression(value)); + _addRestMap( + named, keywordRest, invocation, (value) => ValueExpression(value)); return Tuple2(positional, named); } else { throw _exception( @@ -1582,12 +1593,16 @@ class _EvaluateVisitor /// Adds the values in [map] to [values]. /// - /// Throws a [SassRuntimeException] associated with [span] if any [map] keys - /// aren't strings. + /// Throws a [SassRuntimeException] associated with [nodeForSpan]'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 /// type for [values]. Otherwise, the [Value]s are used as-is. - void _addRestMap(Map values, SassMap map, FileSpan span, + /// + /// 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(Map values, SassMap map, AstNode nodeForSpan, [T convert(Value value)]) { convert ??= (value) => value as T; map.contents.forEach((key, value) { @@ -1597,7 +1612,7 @@ class _EvaluateVisitor throw _exception( "Variable keyword argument map must have string keys.\n" "$key is not a string in $map.", - span); + nodeForSpan.span); } }); } @@ -1605,9 +1620,9 @@ class _EvaluateVisitor /// Throws a [SassRuntimeException] if [positional] and [named] aren't valid /// when applied to [arguments]. void _verifyArguments(int positional, Map named, - ArgumentDeclaration arguments, FileSpan span) => + ArgumentDeclaration arguments, AstNode nodeWithSpan) => _addExceptionSpan( - span, () => arguments.verify(positional, MapKeySet(named))); + nodeWithSpan, () => arguments.verify(positional, MapKeySet(named))); Value visitSelectorExpression(SelectorExpression node) { if (_styleRule == null) return sassNull; @@ -1624,7 +1639,7 @@ class _EvaluateVisitor var result = expression.accept(this); return result is SassString ? result.text - : _serialize(result, expression.span, quote: false); + : _serialize(result, expression, quote: false); }).join(), quotes: node.hasQuotes); } @@ -1694,30 +1709,41 @@ class _EvaluateVisitor expression.span); } - return _serialize(result, expression.span, quote: false); + return _serialize(result, expression, quote: false); }).join(); } /// Evaluates [expression] and calls `toCssString()` and wraps a /// [SassScriptException] to associate it with [span]. String _evaluateToCss(Expression expression, {bool quote = true}) => - _serialize(expression.accept(this), expression.span, quote: quote); + _serialize(expression.accept(this), expression, quote: quote); /// Calls `value.toCssString()` and wraps a [SassScriptException] to associate - /// it with [span]. - String _serialize(Value value, FileSpan span, {bool quote = true}) => - _addExceptionSpan(span, () => value.toCssString(quote: quote)); + /// it with [nodeWithSpan]'s source span. + /// + /// 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. + String _serialize(Value value, AstNode nodeWithSpan, {bool quote = true}) => + _addExceptionSpan(nodeWithSpan, () => value.toCssString(quote: quote)); - /// Returns the span for [expression], or if [expression] is just a variable - /// reference for the span where it was declared. + /// Returns the [AstNode] whose span should be used for [expression]. + /// + /// If [expression] is a variable reference, [AstNode]'s span will be the span + /// where that variable was originally declared. Otherwise, this will just + /// return [expression]. /// /// Returns `null` if [_sourceMap] is `false`. - FileSpan _expressionSpan(Expression expression) { + /// + /// This returns an [AstNode] rather than a [FileSpan] so we can avoid calling + /// [AstNode.span] if the span isn't required, since some nodes need to do + /// real work to manufacture a source span. + AstNode _expressionNode(Expression expression) { if (!_sourceMap) return null; if (expression is VariableExpression) { - return _environment.getVariableSpan(expression.name); + return _environment.getVariableNode(expression.name); } else { - return expression.span; + return expression; } } @@ -1776,12 +1802,16 @@ class _EvaluateVisitor return result; } - /// Adds a frame to the stack with the given [member] name, and [span] as the - /// site of the new frame. + /// Adds a frame to the stack with the given [member] name, and [nodeWithSpan] + /// as the site of the new frame. /// /// Runs [callback] with the new stack. - T _withStackFrame(String member, FileSpan span, T callback()) { - _stack.add(Tuple2(_member, span)); + /// + /// 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. + T _withStackFrame(String member, AstNode nodeWithSpan, T callback()) { + _stack.add(Tuple2(_member, nodeWithSpan)); var oldMember = _member; _member = member; var result = callback(); @@ -1803,7 +1833,7 @@ class _EvaluateVisitor /// [span] is the current location, used for the bottom-most stack frame. Trace _stackTrace(FileSpan span) { var frames = _stack - .map((tuple) => _stackFrame(tuple.item1, tuple.item2)) + .map((tuple) => _stackFrame(tuple.item1, tuple.item2.span)) .toList() ..add(_stackFrame(_member, span)); return Trace(frames.reversed); @@ -1818,17 +1848,23 @@ class _EvaluateVisitor SassRuntimeException _exception(String message, FileSpan span) => SassRuntimeException(message, span, _stackTrace(span)); - /// Runs [callback], and adjusts any [SassFormatException] to be within [span]. + /// Runs [callback], and adjusts any [SassFormatException] to be within + /// [nodeWithSpan]'s source span. /// /// Specifically, this adjusts format exceptions so that the errors are /// reported as though the text being parsed were exactly in [span]. This may /// not be quite accurate if the source text contained interpolation, but /// it'll still produce a useful error. - T _adjustParseError(FileSpan span, T callback()) { + /// + /// 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. + T _adjustParseError(AstNode nodeWithSpan, T callback()) { try { return callback(); } on SassFormatException catch (error) { var errorText = error.span.file.getText(0); + var span = nodeWithSpan.span; var syntheticFile = span.file .getText(0) .replaceRange(span.start.offset, span.end.offset, errorText); @@ -1841,12 +1877,16 @@ class _EvaluateVisitor } /// Runs [callback], and converts any [SassScriptException]s it throws to - /// [SassRuntimeException]s with [span]. - T _addExceptionSpan(FileSpan span, T callback()) { + /// [SassRuntimeException]s with [nodeWithSpan]'s source span. + /// + /// 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. + T _addExceptionSpan(AstNode nodeWithSpan, T callback()) { try { return callback(); } on SassScriptException catch (error) { - throw _exception(error.message, span); + throw _exception(error.message, nodeWithSpan.span); } } } @@ -1856,20 +1896,28 @@ class _ArgumentResults { /// Arguments passed by position. final List positional; - /// The spans for each [positional] argument, or `null` if source span - /// tracking is disabled. - final List positionalSpans; + /// The [AstNode]s that hold the spans for each [positional] argument, or + /// `null` if source span tracking is disabled. + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final List positionalNodes; /// Arguments passed by name. final Map named; - /// The spans for each [named] argument, or `null` if source span tracking is - /// disabled. - final Map namedSpans; + /// The [AstNode]s that hold the spans for each [named] argument, or `null` if + /// source span tracking is disabled. + /// + /// This stores [AstNode]s rather than [FileSpan]s 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. + final Map namedNodes; /// The separator used for the rest argument list, if any. final ListSeparator separator; _ArgumentResults(this.positional, this.named, this.separator, - {this.positionalSpans, this.namedSpans}); + {this.positionalNodes, this.namedNodes}); } diff --git a/pubspec.yaml b/pubspec.yaml index e9af6bb46..9c3bb79bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.16.0 +version: 1.16.1 description: A Sass implementation in Dart. author: Dart Team homepage: https://github.com/sass/dart-sass