From 75707bf050a81fcd859b93186f18f49427a70722 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 2 Sep 2025 18:12:59 -0700 Subject: [PATCH] [Slash Separator] Parse `/` as a separator rather than a division operator Closes #663 --- CHANGELOG.md | 4 + .../ast/sass/expression/binary_operation.dart | 23 +- lib/src/deprecation.dart | 11 +- lib/src/functions/color.dart | 26 -- lib/src/functions/list.dart | 10 + lib/src/functions/math.dart | 12 +- lib/src/parse/stylesheet.dart | 281 ++++++------ lib/src/value.dart | 2 +- lib/src/value/color.dart | 9 - lib/src/value/number.dart | 22 +- lib/src/value/number/complex.dart | 14 +- lib/src/value/number/single_unit.dart | 9 +- lib/src/value/number/unitless.dart | 5 +- lib/src/visitor/async_evaluate.dart | 411 ++++++++++------- lib/src/visitor/evaluate.dart | 413 +++++++++++------- lib/src/visitor/is_calculation_safe.dart | 3 +- lib/src/visitor/serialize.dart | 85 +++- pkg/sass-parser/CHANGELOG.md | 2 +- pkg/sass_api/CHANGELOG.md | 2 + test/cli/shared.dart | 61 +-- test/deprecations_test.dart | 5 - test/embedded/function_test.dart | 4 +- 22 files changed, 786 insertions(+), 628 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f79402c24..5de2d93f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 2.0.0 +* **Breaking change:** `/` is now used for slash-separated lists rather than + division in SassScript. You can still use it for division within `calc()` and + other calculation expressions, or use `math.div()` instead. + * **Breaking change:** `@elseif` is no longer treated as equivalent to `@else if`, and is now treated like any other unknown plain CSS at-rule. diff --git a/lib/src/ast/sass/expression/binary_operation.dart b/lib/src/ast/sass/expression/binary_operation.dart index 9aeb05a33..871d8d618 100644 --- a/lib/src/ast/sass/expression/binary_operation.dart +++ b/lib/src/ast/sass/expression/binary_operation.dart @@ -24,13 +24,6 @@ final class BinaryOperationExpression extends Expression { /// The right-hand operand. final Expression right; - /// Whether this is a [BinaryOperator.dividedBy] operation that may be - /// interpreted as slash-separated numbers. - /// - /// @nodoc - @internal - final bool allowsSlash; - 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. @@ -57,17 +50,7 @@ final class BinaryOperationExpression extends Expression { .trim() : span; - BinaryOperationExpression(this.operator, this.left, this.right) - : allowsSlash = false; - - /// Creates a [BinaryOperator.dividedBy] operation that may be interpreted as - /// slash-separated numbers. - /// - /// @nodoc - @internal - BinaryOperationExpression.slash(this.left, this.right) - : operator = BinaryOperator.dividedBy, - allowsSlash = true; + BinaryOperationExpression(this.operator, this.left, this.right); T accept(ExpressionVisitor visitor) => visitor.visitBinaryOperationExpression(this); @@ -151,6 +134,10 @@ enum BinaryOperator { times('times', '*', 6, associative: true), /// The division operator, `/`. + /// + /// **Note:** This is never directly parsed by the Sass stylesheet. It's only + /// ever generated at runtime by reorienting the precedence of slash-separated + /// lists. dividedBy('divided by', '/', 6), /// The modulo operator, `%`. diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 64b3b54d7..5e3f6b812 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -15,7 +15,7 @@ enum Deprecation { // DO NOT EDIT. This section was generated from the language repo. // See tool/grind/generate_deprecations.dart for details. // - // Checksum: 0be67f32391e119205f8e98cd15fa8e7382b6dd2 + // Checksum: 0bd4067eac410d6d9ab05aab30a1f5bf0e602144 /// Deprecation for passing a string directly to meta.call(). callString('call-string', @@ -40,7 +40,6 @@ enum Deprecation { /// Deprecation for declaring new variables with !global. newGlobal('new-global', deprecatedIn: '1.17.2', - obsoleteIn: '2.0.0', description: 'Declaring new variables with !global.'), /// Deprecation for using color module functions in place of plain CSS functions. @@ -51,7 +50,9 @@ enum Deprecation { /// Deprecation for / operator for division. slashDiv('slash-div', - deprecatedIn: '1.33.0', description: '/ operator for division.'), + deprecatedIn: '1.33.0', + obsoleteIn: '2.0.0', + description: '/ operator for division.'), /// Deprecation for leading, trailing, and repeated combinators. bogusCombinators('bogus-combinators', @@ -145,6 +146,10 @@ enum Deprecation { deprecatedIn: '1.91.0', description: 'A rest parameter before a positional or named parameter.'), + /// Deprecation for the list.slash() function. + listSlash('list-slash', + deprecatedIn: '2.0.0', description: 'The list.slash() function.'), + // END AUTOGENERATED CODE /// Used for deprecations coming from user-authored code. diff --git a/lib/src/functions/color.dart b/lib/src/functions/color.dart index afffa341d..80d61293a 100644 --- a/lib/src/functions/color.dart +++ b/lib/src/functions/color.dart @@ -11,7 +11,6 @@ import '../deprecation.dart'; import '../evaluation_context.dart'; import '../exception.dart'; import '../module/built_in.dart'; -import '../parse/scss.dart'; import '../util/map.dart'; import '../util/nullable.dart'; import '../util/number.dart'; @@ -1806,34 +1805,9 @@ Value _parseChannels( "${pluralize('was', inputList.length, plural: 'were')} passed.", name, ), - [...var initial, SassString(hasQuotes: false, :var text)] => switch ( - text.split('/')) { - [_] => (input, null), - [var channel3, var alpha] => ( - SassList([ - ...initial, - _parseNumberOrString(channel3), - ], ListSeparator.space), - _parseNumberOrString(alpha), - ), - _ => null, - }, - [...var initial, SassNumber(asSlash: (var before, var after))] => ( - SassList([...initial, before], ListSeparator.space), - after, - ), _ => (input, null), }; -/// Parses [text] as either a Sass number or an unquoted Sass string. -Value _parseNumberOrString(String text) { - try { - return ScssParser(text).parseNumber(); - } on SassFormatException { - return SassString(text, quotes: false); - } -} - /// Creates a [SassColor] for the given [space] from the given channel values, /// or throws a [SassScriptException] if the channel values are invalid. /// diff --git a/lib/src/functions/list.dart b/lib/src/functions/list.dart index d8003c098..1530138b4 100644 --- a/lib/src/functions/list.dart +++ b/lib/src/functions/list.dart @@ -7,6 +7,8 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import '../callable.dart'; +import '../deprecation.dart'; +import '../evaluation_context.dart'; import '../exception.dart'; import '../module/built_in.dart'; import '../value.dart'; @@ -152,6 +154,14 @@ final _isBracketed = _function( ); final _slash = _function("slash", r"$elements...", (arguments) { + warnForDeprecation( + 'list.slash() is no longer necessary. Sass now supports slash-separated ' + 'list literals.\n' + 'This function is deprecated and will be removed in Dart 3.0.0.\n' + 'More info and automated migrator: https://sass-lang.com/d/slash-div', + Deprecation.listSlash, + ); + var list = arguments[0].asList; if (list.length < 2) { throw SassScriptException("At least two elements are required."); diff --git a/lib/src/functions/math.dart b/lib/src/functions/math.dart index 6cb1540ef..47a767fd2 100644 --- a/lib/src/functions/math.dart +++ b/lib/src/functions/math.dart @@ -257,16 +257,8 @@ final _randomFunction = _function("random", r"$limit: null", (arguments) { }); final _div = _function("div", r"$number1, $number2", (arguments) { - var number1 = arguments[0]; - var number2 = arguments[1]; - - if (number1 is! SassNumber || number2 is! SassNumber) { - warn( - "math.div() will only support number arguments in a future release.\n" - "Use list.slash() instead for a slash separator.", - ); - } - + var number1 = arguments[0].assertNumber('number1'); + var number2 = arguments[1].assertNumber('number2'); return number1.dividedBy(number2); }); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index df9104310..d35f75ce8 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -53,9 +53,6 @@ abstract class StylesheetParser extends Parser { /// Whether the parser is currently parsing a style rule. var _inStyleRule = false; - /// Whether the parser is currently within a parenthesized expression. - var _inParentheses = false; - /// Whether the parser is currently within an expression. @protected bool get inExpression => _inExpression; @@ -1850,7 +1847,6 @@ abstract class StylesheetParser extends Parser { var start = scanner.state; var wasInExpression = _inExpression; - var wasInParentheses = _inParentheses; _inExpression = true; // We use the convention below of referring to nullable variables that are @@ -1861,6 +1857,8 @@ abstract class StylesheetParser extends Parser { List? commaExpressions_; + List? slashExpressions_; + List? spaceExpressions_; // Operators whose right-hand operands_ are not fully parsed yet, in order of @@ -1873,10 +1871,6 @@ abstract class StylesheetParser extends Parser { // of `operators_[n]`. List? operands_; - /// Whether the single expression parsed so far may be interpreted as - /// slash-separated numbers. - var allowSlash = true; - /// The leftmost expression that's been fully-parsed. This can be null in /// special cases where the expression begins with a sub-expression but has /// a later character that indicates that the outer expression isn't done, @@ -1886,18 +1880,6 @@ abstract class StylesheetParser extends Parser { /// ^ Expression? singleExpression_ = _singleExpression(); - // Resets the scanner state to the state it was at at the beginning of the - // expression, except for [_inParentheses]. - void resetState() { - commaExpressions_ = null; - spaceExpressions_ = null; - operators_ = null; - operands_ = null; - scanner.state = start; - allowSlash = true; - singleExpression_ = _singleExpression(); - } - void resolveOneOperation() { var operator = operators_!.removeLast(); var operands = operands_!; @@ -1912,44 +1894,35 @@ abstract class StylesheetParser extends Parser { ); } - if (allowSlash && - !_inParentheses && - operator == BinaryOperator.dividedBy && - _isSlashOperand(left) && - _isSlashOperand(right)) { - singleExpression_ = BinaryOperationExpression.slash(left, right); - } else { - singleExpression_ = BinaryOperationExpression(operator, left, right); - allowSlash = false; - - if (operator case BinaryOperator.plus || BinaryOperator.minus) { - if (scanner.string.substring( - right.span.start.offset - 1, - right.span.start.offset, - ) == - operator.operator && - scanner.string.codeUnitAt(left.span.end.offset).isWhitespace) { - warnings.add(( - deprecation: Deprecation.strictUnary, - message: "This operation is parsed as:\n" - "\n" - " $left ${operator.operator} $right\n" - "\n" - "but you may have intended it to mean:\n" - "\n" - " $left (${operator.operator}$right)\n" - "\n" - "Add a space after ${operator.operator} to clarify that it's " - "meant to be a binary operation, or wrap\n" - "it in parentheses to make it a unary operation. This will be " - "an error in future\n" - "versions of Sass.\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/strict-unary", - span: singleExpression_!.span, - )); - } + singleExpression_ = BinaryOperationExpression(operator, left, right); + + if (operator case BinaryOperator.plus || BinaryOperator.minus) { + if (scanner.string.substring( + right.span.start.offset - 1, + right.span.start.offset, + ) == + operator.operator && + scanner.string.codeUnitAt(left.span.end.offset).isWhitespace) { + warnings.add(( + deprecation: Deprecation.strictUnary, + message: "This operation is parsed as:\n" + "\n" + " $left ${operator.operator} $right\n" + "\n" + "but you may have intended it to mean:\n" + "\n" + " $left (${operator.operator}$right)\n" + "\n" + "Add a space after ${operator.operator} to clarify that it's " + "meant to be a binary operation, or wrap\n" + "it in parentheses to make it a unary operation. This will be " + "an error in future\n" + "versions of Sass.\n" + "\n" + "More info and automated migrator: " + "https://sass-lang.com/d/strict-unary", + span: singleExpression_!.span, + )); } } } @@ -1964,25 +1937,12 @@ abstract class StylesheetParser extends Parser { void addSingleExpression(Expression expression) { if (singleExpression_ != null) { - // If we discover we're parsing a list whose first element is a division - // operation, and we're in parentheses, reparse outside of a paren - // context. This ensures that `(1/2 1)` doesn't perform division on its - // first element. - if (_inParentheses) { - _inParentheses = false; - if (allowSlash) { - resetState(); - return; - } - } - var spaceExpressions = spaceExpressions_ ??= []; resolveOperations(); // [singleExpression_] was non-null before, and [resolveOperations] // can't make it null, it can only change it. spaceExpressions.add(singleExpression_!); - allowSlash = true; } singleExpression_ = expression; @@ -1995,8 +1955,7 @@ abstract class StylesheetParser extends Parser { // evaluation time. operator != BinaryOperator.plus && operator != BinaryOperator.minus && - operator != BinaryOperator.times && - operator != BinaryOperator.dividedBy) { + operator != BinaryOperator.times) { scanner.error( "Operators aren't allowed in plain CSS.", position: scanner.position - operator.operator.length, @@ -2004,8 +1963,6 @@ abstract class StylesheetParser extends Parser { ); } - allowSlash = allowSlash && operator == BinaryOperator.dividedBy; - var operators = operators_ ??= []; var operands = operands_ ??= []; while (operators.isNotEmpty && @@ -2037,10 +1994,14 @@ abstract class StylesheetParser extends Parser { resolveOperations(); var spaceExpressions = spaceExpressions_; - if (spaceExpressions == null) return; + + // This allows a list of the form `X,` but forbids a list of the form + // `X/`. + if (spaceExpressions == null && slashExpressions_ == null) return; var singleExpression = singleExpression_; if (singleExpression == null) scanner.error("Expected expression."); + if (spaceExpressions == null) return; spaceExpressions.add(singleExpression); singleExpression_ = ListExpression( @@ -2051,6 +2012,24 @@ abstract class StylesheetParser extends Parser { spaceExpressions_ = null; } + void resolveSlashExpressions() { + resolveSpaceExpressions(); + + var slashExpressions = slashExpressions_; + if (slashExpressions == null) return; + + var singleExpression = singleExpression_; + if (singleExpression == null) scanner.error("Expected expression."); + + slashExpressions.add(singleExpression); + singleExpression_ = ListExpression( + slashExpressions, + ListSeparator.slash, + slashExpressions.first.span.expand(singleExpression.span), + ); + slashExpressions_ = null; + } + loop: while (true) { whitespace(consumeNewlines: consumeNewlines || bracketList); @@ -2142,13 +2121,6 @@ abstract class StylesheetParser extends Parser { addOperator(BinaryOperator.minus); } - case $slash when singleExpression_ == null: - addSingleExpression(_unaryOperation()); - - case $slash: - scanner.readChar(); - addOperator(BinaryOperator.dividedBy); - case $percent: scanner.readChar(); addOperator(BinaryOperator.modulo); @@ -2184,30 +2156,39 @@ abstract class StylesheetParser extends Parser { >= 0x80: addSingleExpression(identifierLike()); - case $comma: - // If we discover we're parsing a list whose first element is a - // division operation, and we're in parentheses, reparse outside of a - // paren context. This ensures that `(1/2, 1)` doesn't perform division - // on its first element. - if (_inParentheses) { - _inParentheses = false; - if (allowSlash) { - resetState(); - break; - } + case $slash when singleExpression_ == null: + if (slashExpressions_ case var slashExpressions?) { + slashExpressions + .add(StringExpression.plain(' ', scanner.location.pointSpan())); + scanner.readChar(); + } else { + addSingleExpression(_unaryOperation()); } + case $slash: + var slashExpressions = slashExpressions_ ??= []; + + resolveSpaceExpressions(); + + // [resolveSpaceExpressions] can modify [singleExpression_], but it + // can't set it to null`. We know it's not null here because of the + // previous `$slash when singleExpression_ == null` case. + slashExpressions.add(singleExpression_!); + + scanner.readChar(); + singleExpression_ = null; + + case $comma: var commaExpressions = commaExpressions_ ??= []; if (singleExpression_ == null) scanner.error("Expected expression."); - resolveSpaceExpressions(); + resolveSlashExpressions(); - // [resolveSpaceExpressions can modify [singleExpression_], but it + // [resolveSlashExpressions] can modify [singleExpression_], but it // can't set it to null`. commaExpressions.add(singleExpression_!); scanner.readChar(); - allowSlash = true; singleExpression_ = null; case _: @@ -2215,14 +2196,13 @@ abstract class StylesheetParser extends Parser { } } - if (bracketList) scanner.expectChar($rbracket); - // TODO(dart-lang/sdk#52756): Use patterns to null-check these values. var commaExpressions = commaExpressions_; + var slashExpressions = slashExpressions_; var spaceExpressions = spaceExpressions_; if (commaExpressions != null) { - resolveSpaceExpressions(); - _inParentheses = wasInParentheses; + resolveSlashExpressions(); + if (bracketList) scanner.expectChar($rbracket); var singleExpression = singleExpression_; if (singleExpression != null) commaExpressions.add(singleExpression); _inExpression = wasInExpression; @@ -2232,8 +2212,19 @@ abstract class StylesheetParser extends Parser { scanner.spanFrom(beforeBracket ?? start), brackets: bracketList, ); + } else if (bracketList && slashExpressions != null) { + resolveSpaceExpressions(); + scanner.expectChar($rbracket); + _inExpression = wasInExpression; + return ListExpression( + slashExpressions..add(singleExpression_!), + ListSeparator.slash, + scanner.spanFrom(beforeBracket!), + brackets: true, + ); } else if (bracketList && spaceExpressions != null) { resolveOperations(); + scanner.expectChar($rbracket); _inExpression = wasInExpression; return ListExpression( spaceExpressions..add(singleExpression_!), @@ -2242,8 +2233,9 @@ abstract class StylesheetParser extends Parser { brackets: true, ); } else { - resolveSpaceExpressions(); + resolveSlashExpressions(); if (bracketList) { + scanner.expectChar($rbracket); singleExpression_ = ListExpression( [singleExpression_!], ListSeparator.undecided, @@ -2269,13 +2261,6 @@ abstract class StylesheetParser extends Parser { ); } - /// Whether [expression] is allowed as an operand of a `/` expression that - /// produces a potentially slash-separated number. - bool _isSlashOperand(Expression expression) => - expression is NumberExpression || - expression is FunctionExpression || - (expression is BinaryOperationExpression && expression.allowsSlash); - /// Consumes an expression that doesn't contain any top-level whitespace. Expression _singleExpression() => switch (scanner.peekChar()) { // Note: when adding a new case, make sure it's reflected in @@ -2312,52 +2297,46 @@ abstract class StylesheetParser extends Parser { /// Consumes a parenthesized expression. @protected Expression parentheses() { - var wasInParentheses = _inParentheses; - _inParentheses = true; - try { - var start = scanner.state; - scanner.expectChar($lparen); - whitespace(consumeNewlines: true); - var inside = scanner.state; - if (!_lookingAtExpression()) { - scanner.expectChar($rparen); - return ListExpression( - [], - ListSeparator.undecided, - scanner.spanFrom(start), - ); - } - - var first = expressionUntilComma(); - if (scanner.scanChar($colon)) { - whitespace(consumeNewlines: true); - return _map(first, start); - } + var start = scanner.state; + scanner.expectChar($lparen); + whitespace(consumeNewlines: true); + var inside = scanner.state; + if (!_lookingAtExpression()) { + scanner.expectChar($rparen); + return ListExpression( + [], + ListSeparator.undecided, + scanner.spanFrom(start), + ); + } - if (!scanner.scanChar($comma)) { - scanner.expectChar($rparen); - return ParenthesizedExpression(first, scanner.spanFrom(start)); - } + var first = expressionUntilComma(); + if (scanner.scanChar($colon)) { whitespace(consumeNewlines: true); + return _map(first, start); + } - var expressions = [first]; - while (true) { - if (!_lookingAtExpression()) break; - expressions.add(expressionUntilComma()); - if (!scanner.scanChar($comma)) break; - whitespace(consumeNewlines: true); - } - - var list = ListExpression( - expressions, - ListSeparator.comma, - scanner.spanFrom(inside), - ); + if (!scanner.scanChar($comma)) { scanner.expectChar($rparen); - return ParenthesizedExpression(list, scanner.spanFrom(start)); - } finally { - _inParentheses = wasInParentheses; + return ParenthesizedExpression(first, scanner.spanFrom(start)); + } + whitespace(consumeNewlines: true); + + var expressions = [first]; + while (true) { + if (!_lookingAtExpression()) break; + expressions.add(expressionUntilComma()); + if (!scanner.scanChar($comma)) break; + whitespace(consumeNewlines: true); } + + var list = ListExpression( + expressions, + ListSeparator.comma, + scanner.spanFrom(inside), + ); + scanner.expectChar($rparen); + return ParenthesizedExpression(list, scanner.spanFrom(start)); } /// Consumes a map expression. @@ -3596,13 +3575,11 @@ abstract class StylesheetParser extends Parser { // a slower backtracking case. Expression name; var nameStart = scanner.state; - var wasInParentheses = _inParentheses; try { name = _expression(consumeNewlines: true); scanner.expectChar($colon); } on FormatException catch (_) { scanner.state = nameStart; - _inParentheses = wasInParentheses; var identifier = interpolatedIdentifier(); if (_trySupportsOperation(identifier, nameStart) case var operation?) { diff --git a/lib/src/value.dart b/lib/src/value.dart index 7154c9dd3..3fed27ded 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -385,7 +385,7 @@ abstract class Value { /// @nodoc @internal Value dividedBy(Value other) => - SassString("${toCssString()}/${other.toCssString()}", quotes: false); + throw SassScriptException('Undefined operation "$this / $other".'); /// The SassScript unary `+` operation. /// diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index ea839ac2f..908d3bffc 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -1124,15 +1124,6 @@ class SassColor extends Value { throw SassScriptException('Undefined operation "$this - $other".'); } - /// @nodoc - @internal - Value dividedBy(Value other) { - if (other is! SassNumber && other is! SassColor) { - return super.dividedBy(other); - } - throw SassScriptException('Undefined operation "$this / $other".'); - } - operator ==(Object other) { if (other is! SassColor) return false; diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 7fa5d94e5..a376bda9d 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -184,13 +184,6 @@ abstract class SassNumber extends Value { /// lengths. bool get hasComplexUnits; - /// The representation of this number as two slash-separated numbers, if it - /// has one. - /// - /// @nodoc - @internal - final (SassNumber, SassNumber)? asSlash; - /// Whether `this` is an integer, according to [fuzzyEquals]. /// /// The [int] value can be accessed using [asInt] or [assertInt]. Note that @@ -274,7 +267,7 @@ abstract class SassNumber extends Value { /// @nodoc @protected - SassNumber.protected(this._value, this.asSlash); + SassNumber.protected(this._value); T accept(ValueVisitor visitor) => visitor.visitNumber(this); @@ -285,19 +278,6 @@ abstract class SassNumber extends Value { @protected SassNumber withValue(num value); - /// Returns a copy of `this` without [asSlash] set. - /// - /// @nodoc - @internal - SassNumber withoutSlash() => asSlash == null ? this : withValue(value); - - /// Returns a copy of `this` with [asSlash] set to a pair containing - /// [numerator] and [denominator]. - /// - /// @nodoc - @internal - SassNumber withSlash(SassNumber numerator, SassNumber denominator); - SassNumber assertNumber([String? name]) => this; /// Returns [value] as an [int], if it's an integer value according to diff --git a/lib/src/value/number/complex.dart b/lib/src/value/number/complex.dart index 391ebf158..8e7db7e30 100644 --- a/lib/src/value/number/complex.dart +++ b/lib/src/value/number/complex.dart @@ -31,12 +31,8 @@ class ComplexSassNumber extends SassNumber { List denominatorUnits, ) : this._(value, numeratorUnits, denominatorUnits); - ComplexSassNumber._( - double value, - this._numeratorUnits, - this._denominatorUnits, [ - (SassNumber, SassNumber)? asSlash, - ]) : super.protected(value, asSlash) { + ComplexSassNumber._(super.value, this._numeratorUnits, this._denominatorUnits) + : super.protected() { assert(numeratorUnits.length > 1 || denominatorUnits.isNotEmpty); } @@ -55,10 +51,4 @@ class ComplexSassNumber extends SassNumber { SassNumber withValue(num value) => ComplexSassNumber._(value.toDouble(), numeratorUnits, denominatorUnits); - - SassNumber withSlash(SassNumber numerator, SassNumber denominator) => - ComplexSassNumber._(value, numeratorUnits, denominatorUnits, ( - numerator, - denominator, - )); } diff --git a/lib/src/value/number/single_unit.dart b/lib/src/value/number/single_unit.dart index 94a05a9b9..79e99fa6c 100644 --- a/lib/src/value/number/single_unit.dart +++ b/lib/src/value/number/single_unit.dart @@ -50,18 +50,11 @@ class SingleUnitSassNumber extends SassNumber { bool get hasUnits => true; bool get hasComplexUnits => false; - SingleUnitSassNumber( - double value, - this._unit, [ - (SassNumber, SassNumber)? asSlash, - ]) : super.protected(value, asSlash); + SingleUnitSassNumber(super.value, this._unit) : super.protected(); SassNumber withValue(num value) => SingleUnitSassNumber(value.toDouble(), _unit); - SassNumber withSlash(SassNumber numerator, SassNumber denominator) => - SingleUnitSassNumber(value, _unit, (numerator, denominator)); - bool hasUnit(String unit) => unit == _unit; bool hasCompatibleUnits(SassNumber other) => diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index 31e5a1353..5b2c43a62 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -19,13 +19,10 @@ class UnitlessSassNumber extends SassNumber { bool get hasUnits => false; bool get hasComplexUnits => false; - UnitlessSassNumber(super.value, [super.asSlash]) : super.protected(); + UnitlessSassNumber(super.value) : super.protected(); SassNumber withValue(num value) => UnitlessSassNumber(value.toDouble()); - SassNumber withSlash(SassNumber numerator, SassNumber denominator) => - UnitlessSassNumber(value, (numerator, denominator)); - bool hasUnit(String unit) => false; bool hasCompatibleUnits(SassNumber other) => other is UnitlessSassNumber; diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index b85a20343..0a13b44c9 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -45,7 +45,6 @@ import '../util/map.dart'; import '../util/nullable.dart'; import '../util/span.dart'; import '../value.dart'; -import 'expression_to_calc.dart'; import 'interface/css.dart'; import 'interface/expression.dart'; import 'interface/modifiable_css.dart'; @@ -226,6 +225,9 @@ final class _EvaluateVisitor /// This is used to produce warnings for importers. FileSpan? _importSpan; + /// Whether we're currently executing a mixin. + var _inMixin = false; + /// Whether we're currently executing a function. var _inFunction = false; @@ -333,6 +335,13 @@ final class _EvaluateVisitor /// If this is empty, that indicates that the current module is not configured. var _configuration = const Configuration.empty(); + /// A cache mapping slash-separated list expressions to the binary operation + /// expressions they're rewritten as when executing calculations. + /// + /// These are only cached in function and mixin contexts, where the same + /// calculations are likely to be evaluated multiple times. + final _calcSlashListCache = HashMap.identity(); + /// Creates a new visitor. /// /// Most arguments are the same as those to [evaluateAsync]. @@ -1394,11 +1403,8 @@ final class _EvaluateVisitor var list = await node.list.accept(this); var nodeWithSpan = _expressionNode(node.list); var setVariables = switch (node.variables) { - [var variable] => (Value value) => _environment.setLocalVariable( - variable, - _withoutSlash(value, nodeWithSpan), - nodeWithSpan, - ), + [var variable] => (Value value) => + _environment.setLocalVariable(variable, value, nodeWithSpan), var variables => (Value value) => _setMultipleVariables(variables, value, nodeWithSpan), }; @@ -1423,11 +1429,7 @@ final class _EvaluateVisitor var list = value.asList; var minLength = math.min(variables.length, list.length); for (var i = 0; i < minLength; i++) { - _environment.setLocalVariable( - variables[i], - _withoutSlash(list[i], nodeWithSpan), - nodeWithSpan, - ); + _environment.setLocalVariable(variables[i], list[i], nodeWithSpan); } for (var i = minLength; i < variables.length; i++) { _environment.setLocalVariable(variables[i], sassNull, nodeWithSpan); @@ -1681,10 +1683,7 @@ final class _EvaluateVisitor var variableNodeWithSpan = _expressionNode(variable.expression); newValues[variable.name] = ConfiguredValue.explicit( - _withoutSlash( - await variable.expression.accept(this), - variableNodeWithSpan, - ), + await variable.expression.accept(this), variable.span, variableNodeWithSpan, ); @@ -2156,13 +2155,18 @@ final class _EvaluateVisitor () => node.spanWithoutContent, ); - await _applyMixin( - mixin, - contentCallable, - node.arguments, - node, - nodeWithSpanWithoutContent, - ); + _inMixin = true; + try { + await _applyMixin( + mixin, + contentCallable, + node.arguments, + node, + nodeWithSpanWithoutContent, + ); + } finally { + _inMixin = false; + } return null; } @@ -2296,7 +2300,7 @@ final class _EvaluateVisitor } Future visitReturnRule(ReturnRule node) async => - _withoutSlash(await node.expression.accept(this), node.expression); + await node.expression.accept(this); Future visitSilentComment(SilentComment node) async => null; @@ -2581,10 +2585,7 @@ final class _EvaluateVisitor ); } - var value = _withoutSlash( - await node.expression.accept(this), - node.expression, - ); + var value = await node.expression.accept(this); _addExceptionSpan(node, () { _environment.setVariable( node.name, @@ -2604,10 +2605,7 @@ final class _EvaluateVisitor for (var variable in node.configuration) { var variableNodeWithSpan = _expressionNode(variable.expression); values[variable.name] = ConfiguredValue.explicit( - _withoutSlash( - await variable.expression.accept(this), - variableNodeWithSpan, - ), + await variable.expression.accept(this), variable.span, variableNodeWithSpan, ); @@ -2655,9 +2653,7 @@ final class _EvaluateVisitor // ## Expressions Future visitBinaryOperationExpression(BinaryOperationExpression node) { - if (_stylesheet.plainCss && - node.operator != BinaryOperator.singleEquals && - node.operator != BinaryOperator.dividedBy) { + if (_stylesheet.plainCss && node.operator != BinaryOperator.singleEquals) { throw _exception( "Operators aren't allowed in plain CSS.", node.operatorSpan, @@ -2693,73 +2689,17 @@ final class _EvaluateVisitor BinaryOperator.plus => left.plus(await node.right.accept(this)), BinaryOperator.minus => left.minus(await node.right.accept(this)), BinaryOperator.times => left.times(await node.right.accept(this)), - BinaryOperator.dividedBy => _slash( - left, - await node.right.accept(this), - node, - ), BinaryOperator.modulo => left.modulo(await node.right.accept(this)), + + /// This can't be generated by the actual Sass parser, but it's still + /// supported by the AST for calculation purposes. Might as well support + /// it here too. + BinaryOperator.dividedBy => + left.dividedBy(await node.right.accept(this)), }; }); } - /// Returns the result of the SassScript `/` operation between [left] and - /// [right] in [node]. - Value _slash(Value left, Value right, BinaryOperationExpression node) { - var result = left.dividedBy(right); - switch ((left, right)) { - case (SassNumber left, SassNumber right) - when node.allowsSlash && - _operandAllowsSlash(node.left) && - _operandAllowsSlash(node.right): - return (result as SassNumber).withSlash(left, right); - - case (SassNumber(), SassNumber()): - String recommendation(Expression expression) => switch (expression) { - BinaryOperationExpression( - operator: BinaryOperator.dividedBy, - :var left, - :var right, - ) => - "math.div(${recommendation(left)}, ${recommendation(right)})", - ParenthesizedExpression() => expression.expression.toString(), - _ => expression.toString(), - }; - - _warn( - "Using / for division outside of calc() is deprecated " - "and will be removed in Dart Sass 2.0.0.\n" - "\n" - "Recommendation: ${recommendation(node)} or " - "${expressionToCalc(node)}\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/slash-div", - node.span, - Deprecation.slashDiv, - ); - return result; - - case _: - return result; - } - } - - /// Returns whether [node] can be used as a component of a slash-separated - /// number. - /// - /// Although this logic is mostly resolved at parse-time, we can't tell - /// whether operands will be evaluated as calculations until evaluation-time. - bool _operandAllowsSlash(Expression node) => - node is! FunctionExpression || - (node.namespace == null && - const { - "calc", "clamp", "hypot", "sin", "cos", "tan", "asin", "acos", // - "atan", "sqrt", "exp", "sign", "mod", "rem", "atan2", "pow", // - "log", "calc-size", - }.contains(node.name.toLowerCase()) && - _environment.getFunction(node.name) == null); - Future visitValueExpression(ValueExpression node) async => node.value; Future visitVariableExpression(VariableExpression node) async { @@ -2799,7 +2739,7 @@ final class _EvaluateVisitor var ifFalse = positional.elementAtOrNull(2) ?? named["if-false"]!; var result = (await condition.accept(this)).isTruthy ? ifTrue : ifFalse; - return _withoutSlash(await result.accept(this), _expressionNode(result)); + return await result.accept(this); } Future visitNullExpression(NullExpression node) async => sassNull; @@ -3211,6 +3151,25 @@ final class _EvaluateVisitor return SassString(elements.join(' '), quotes: false); + // Correctly handling slashes is complicated, because they're parsed as + // list separators and so have lower precedence than any mathematical + // operators. They *should* have higher precedence than addition or + // subtraction and equivalent precedence to multiplication. The spec + // handles this by reconstructing the AST once it's determined that it's + // evaluating a calculation, but we want to avoid the extra allocations so + // we handle it as we evaluate instead. + case ListExpression( + hasBrackets: false, + separator: ListSeparator.slash, + contents: var contents, + ): + var adjusted = _inFunction || _inMixin + ? _calcSlashListCache.putIfAbsent( + node, () => _adjustSlashPrecedence(contents)) + : _adjustSlashPrecedence(contents); + return await _visitCalculationExpression(adjusted, + inLegacySassFunction: inLegacySassFunction); + case _: assert(!node.isCalculationSafe); throw _exception( @@ -3220,6 +3179,198 @@ final class _EvaluateVisitor } } + /// Converts the slash-separated list of [contents] into an expression that + /// matches `calc()` precedence. + /// + /// This is necessary because slashes are parsed as list separators and so + /// have lower precedence than any mathematical operators. They *should* have + /// higher precedence than addition or subtraction and equivalent precedence + /// to multiplication. + Expression _adjustSlashPrecedence(List contents) { + var left = contents.first; + for (var right in contents.skip(1)) { + if (right + case StringExpression( + text: Interpolation(asPlain: " "), + hasQuotes: false + )) { + throw _exception( + "This expression can't be used in a calculation.", + right.span, + ); + } + + left = switch (( + _splitCalculationSumTail(left), + _splitCalculationSumHead(right) + )) { + // Example: 1 + 2 / 3 + 4 + ( + // 1 + 2 + (var leftRemainder, var leftOperator, var leftOperand), + // 3 + 4 + (var rightOperand, var rightOperator, var rightRemainder) + ) => + // (1 + (2 / 3)) + 4 + BinaryOperationExpression( + rightOperator, + // 1 + (2 / 3) + BinaryOperationExpression( + leftOperator, + leftRemainder, + // 2 / 3 + BinaryOperationExpression( + BinaryOperator.dividedBy, leftOperand, rightOperand), + ), + rightRemainder, + ), + // Example: 1 + 2 / 3 + // 1 + 2 + ((var leftRemainder, var leftOperator, var leftOperand), _) => + // 1 + (2 / 3) + BinaryOperationExpression( + leftOperator, + leftRemainder, + // 2 / 3 + BinaryOperationExpression( + BinaryOperator.dividedBy, leftOperand, right), + ), + // Example: 1 / 2 + 3 + // 2 + 3 + (_, (var rightOperand, var rightOperator, var rightRemainder)) => + // (1 / 2) + 3 + BinaryOperationExpression( + rightOperator, + // 1 / 2 + BinaryOperationExpression( + BinaryOperator.dividedBy, left, rightOperand), + rightRemainder, + ), + _ => switch (right) { + // Example: 1 / 2 * 3 + BinaryOperationExpression( + operator: BinaryOperator.times, + left: var rightOperand, // 2 + right: var rightRemainder, // 3 + ) => + // (1 / 2) * 3 + BinaryOperationExpression( + BinaryOperator.times, + // 1 / 2 + BinaryOperationExpression( + BinaryOperator.dividedBy, left, rightOperand), + rightRemainder, + ), + _ => + BinaryOperationExpression(BinaryOperator.dividedBy, left, right), + }, + }; + } + + return left; + } + + /// If [expression] is a sequence of `+` or `-` operations, returns the + /// leftmost operator and its operand, as well as the remainder of the + /// expression (which may be rewritten according to the associative property). + /// + /// For example, if [expression] is `1 + 2 - 3 + 4`, this returns `(1, +, 2 + /// - 3 + 4)`. This works regardless of how [expression] has been parsed. + /// + /// If [expression] is not such a sequence, returns `null`. + (Expression, BinaryOperator, Expression)? _splitCalculationSumHead( + Expression expression) { + switch (expression) { + // Example: ((1 + 2) + 3) - 4 + case BinaryOperationExpression( + // - + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + left: BinaryOperationExpression( + operator: BinaryOperator.plus || BinaryOperator.minus + ) && + var left, // ((1 + 2) + 3) + :var right // 4 + ): + // (1, +, 2 + 3) + var (head, leftOperator, leftRemainder) = + _splitCalculationSumHead(left)!; + // (1, +, (2 + 3) - 4) + return ( + head, + leftOperator, + // (2 + 3) - 4 + BinaryOperationExpression(operator, leftRemainder, right) + ); + + // Example: 1 + 2 + case BinaryOperationExpression( + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + :var left, // 1 + :var right // 2 + ): + // (1, +, 2) + return (left, operator, right); + + case _: + return null; + } + } + + /// If [expression] is a sequence of `+` or `-` operations, returns the + /// rightmost operator and its operand, as well as the remainder of the + /// expression (which may be rewritten according to the associative property). + /// + /// For example, if [expression] is `1 + 2 - 3 + 4`, this returns `(1 + 2 - 3, + /// +, 4)`. This works regardless of how [expression] has been parsed. + /// + /// If [expression] is not such a sequence, returns `null`. + (Expression, BinaryOperator, Expression)? _splitCalculationSumTail( + Expression expression) { + // Example: 1 - (2 + (3 + 4)) + // + // Note: Sass parses operations left-associatively, so this parse won't + // actually appear naturally. We still handle it defensively in case it + // comes up from some other source. + switch (expression) { + case BinaryOperationExpression( + // - + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + :var left, // 1 + right: BinaryOperationExpression( + operator: BinaryOperator.plus || BinaryOperator.minus + ) && + var right // 2 + (3 + 4) + ): + // (2 + 3, +, 4) + var (rightRemainder, rightOperator, tail) = + _splitCalculationSumTail(right)!; + // (1 - (2 + 3), + 4) + return ( + // 1 - (2 + 3) + BinaryOperationExpression(operator, left, rightRemainder), + rightOperator, // + + tail // 4 + ); + + // Example: 1 + 2 + case BinaryOperationExpression( + // + + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + :var left, // 1 + :var right // 2 + ): + // (1, +, 2) + return (left, operator, right); + + case _: + return null; + } + } + /// Throws an error if [node] requires whitespace around its operator in a /// calculation but doesn't have it. void _checkWhitespaceAroundCalculationOperator( @@ -3364,10 +3515,7 @@ final class _EvaluateVisitor i++) { var parameter = parameters[i]; var value = evaluated.named.remove(parameter.name) ?? - _withoutSlash( - await parameter.defaultValue!.accept>(this), - _expressionNode(parameter.defaultValue!), - ); + await parameter.defaultValue!.accept>(this); _environment.setLocalVariable( parameter.name, value, @@ -3431,10 +3579,7 @@ final class _EvaluateVisitor AstNode nodeWithSpan, ) async { if (callable is AsyncBuiltInCallable) { - return _withoutSlash( - await _runBuiltInCallable(arguments, callable, nodeWithSpan), - nodeWithSpan, - ); + return await _runBuiltInCallable(arguments, callable, nodeWithSpan); } else if (callable is UserDefinedCallable) { return await _runUserDefinedCallable( arguments, @@ -3523,10 +3668,7 @@ final class _EvaluateVisitor var parameter = parameters[i]; evaluated.positional.add( evaluated.named.remove(parameter.name) ?? - _withoutSlash( - await parameter.defaultValue!.accept(this), - parameter.defaultValue!, - ), + await parameter.defaultValue!.accept(this), ); } @@ -3595,7 +3737,7 @@ final class _EvaluateVisitor var positionalNodes = []; for (var expression in arguments.positional) { var nodeForSpan = _expressionNode(expression); - positional.add(_withoutSlash(await expression.accept(this), nodeForSpan)); + positional.add(await expression.accept(this)); positionalNodes.add(nodeForSpan); } @@ -3603,7 +3745,7 @@ final class _EvaluateVisitor var namedNodes = {}; for (var (name, value) in arguments.named.pairs) { var nodeForSpan = _expressionNode(value); - named[name] = _withoutSlash(await value.accept(this), nodeForSpan); + named[name] = await value.accept(this); namedNodes[name] = nodeForSpan; } @@ -3628,20 +3770,18 @@ final class _EvaluateVisitor (key as SassString).text: restNodeForSpan, }); } else if (rest is SassList) { - positional.addAll( - rest.asList.map((value) => _withoutSlash(value, restNodeForSpan)), - ); + positional.addAll(rest.asList.map((value) => value)); positionalNodes.addAll(List.filled(rest.lengthAsList, restNodeForSpan)); separator = rest.separator; if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { - named[key] = _withoutSlash(value, restNodeForSpan); + named[key] = value; namedNodes[key] = restNodeForSpan; }); } } else { - positional.add(_withoutSlash(rest, restNodeForSpan)); + positional.add(rest); positionalNodes.add(restNodeForSpan); } @@ -3695,7 +3835,6 @@ final class _EvaluateVisitor var positional = invocation.arguments.positional.toList(); var named = Map.of(invocation.arguments.named); var rest = await restArgs.accept(this); - var restNodeForSpan = _expressionNode(restArgs); if (rest is SassMap) { _addRestMap( named, @@ -3707,7 +3846,7 @@ final class _EvaluateVisitor positional.addAll( rest.asList.map( (value) => ValueExpression( - _withoutSlash(value, restNodeForSpan), + value, restArgs.span, ), ), @@ -3715,14 +3854,14 @@ final class _EvaluateVisitor if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { named[key] = ValueExpression( - _withoutSlash(value, restNodeForSpan), + value, restArgs.span, ); }); } } else { positional.add( - ValueExpression(_withoutSlash(rest, restNodeForSpan), restArgs.span), + ValueExpression(rest, restArgs.span), ); } @@ -3731,14 +3870,13 @@ final class _EvaluateVisitor var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348 var keywordRest = await keywordRestArgs.accept(this); - var keywordRestNodeForSpan = _expressionNode(keywordRestArgs); if (keywordRest is SassMap) { _addRestMap( named, keywordRest, invocation, (value) => ValueExpression( - _withoutSlash(value, keywordRestNodeForSpan), + value, keywordRestArgs.span, ), ); @@ -3768,10 +3906,9 @@ final class _EvaluateVisitor AstNode nodeWithSpan, T convert(Value value), ) { - var expressionNode = _expressionNode(nodeWithSpan); map.contents.forEach((key, value) { if (key is SassString) { - values[key.text] = convert(_withoutSlash(value, expressionNode)); + values[key.text] = convert(value); } else { throw _exception( "Variable keyword argument map must have string keys.\n" @@ -4418,32 +4555,6 @@ final class _EvaluateVisitor return result; } - /// Like [Value.withoutSlash], but produces a deprecation warning if [value] - /// was a slash-separated number. - Value _withoutSlash(Value value, AstNode nodeForSpan) { - if (value case SassNumber(asSlash: _?)) { - String recommendation(SassNumber number) => switch (number.asSlash) { - (var before, var after) => - "math.div(${recommendation(before)}, ${recommendation(after)})", - _ => number.toString(), - }; - - _warn( - "Using / for division is deprecated and will be removed in Dart Sass " - "2.0.0.\n" - "\n" - "Recommendation: ${recommendation(value)}\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/slash-div", - nodeForSpan.span, - Deprecation.slashDiv, - ); - } - - return value.withoutSlash(); - } - /// Creates a new stack frame with location information from [member] and /// [span]. Frame _stackFrame(String member, FileSpan span) => frameForSpan( diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 71537fdc3..5966a6819 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: faf491d48ccd341abf6a301773bfce08af22b113 +// Checksum: e348cd8084c79bfb0e7b3b34f73e9783bd7752c2 // // ignore_for_file: unused_import @@ -54,7 +54,6 @@ import '../util/map.dart'; import '../util/nullable.dart'; import '../util/span.dart'; import '../value.dart'; -import 'expression_to_calc.dart'; import 'interface/css.dart'; import 'interface/expression.dart'; import 'interface/modifiable_css.dart'; @@ -234,6 +233,9 @@ final class _EvaluateVisitor /// This is used to produce warnings for importers. FileSpan? _importSpan; + /// Whether we're currently executing a mixin. + var _inMixin = false; + /// Whether we're currently executing a function. var _inFunction = false; @@ -341,6 +343,13 @@ final class _EvaluateVisitor /// If this is empty, that indicates that the current module is not configured. var _configuration = const Configuration.empty(); + /// A cache mapping slash-separated list expressions to the binary operation + /// expressions they're rewritten as when executing calculations. + /// + /// These are only cached in function and mixin contexts, where the same + /// calculations are likely to be evaluated multiple times. + final _calcSlashListCache = HashMap.identity(); + /// Creates a new visitor. /// /// Most arguments are the same as those to [evaluate]. @@ -1402,11 +1411,8 @@ final class _EvaluateVisitor var list = node.list.accept(this); var nodeWithSpan = _expressionNode(node.list); var setVariables = switch (node.variables) { - [var variable] => (Value value) => _environment.setLocalVariable( - variable, - _withoutSlash(value, nodeWithSpan), - nodeWithSpan, - ), + [var variable] => (Value value) => + _environment.setLocalVariable(variable, value, nodeWithSpan), var variables => (Value value) => _setMultipleVariables(variables, value, nodeWithSpan), }; @@ -1431,11 +1437,7 @@ final class _EvaluateVisitor var list = value.asList; var minLength = math.min(variables.length, list.length); for (var i = 0; i < minLength; i++) { - _environment.setLocalVariable( - variables[i], - _withoutSlash(list[i], nodeWithSpan), - nodeWithSpan, - ); + _environment.setLocalVariable(variables[i], list[i], nodeWithSpan); } for (var i = minLength; i < variables.length; i++) { _environment.setLocalVariable(variables[i], sassNull, nodeWithSpan); @@ -1689,10 +1691,7 @@ final class _EvaluateVisitor var variableNodeWithSpan = _expressionNode(variable.expression); newValues[variable.name] = ConfiguredValue.explicit( - _withoutSlash( - variable.expression.accept(this), - variableNodeWithSpan, - ), + variable.expression.accept(this), variable.span, variableNodeWithSpan, ); @@ -2163,13 +2162,18 @@ final class _EvaluateVisitor () => node.spanWithoutContent, ); - _applyMixin( - mixin, - contentCallable, - node.arguments, - node, - nodeWithSpanWithoutContent, - ); + _inMixin = true; + try { + _applyMixin( + mixin, + contentCallable, + node.arguments, + node, + nodeWithSpanWithoutContent, + ); + } finally { + _inMixin = false; + } return null; } @@ -2302,8 +2306,7 @@ final class _EvaluateVisitor return queries; } - Value visitReturnRule(ReturnRule node) => - _withoutSlash(node.expression.accept(this), node.expression); + Value visitReturnRule(ReturnRule node) => node.expression.accept(this); Value? visitSilentComment(SilentComment node) => null; @@ -2586,10 +2589,7 @@ final class _EvaluateVisitor ); } - var value = _withoutSlash( - node.expression.accept(this), - node.expression, - ); + var value = node.expression.accept(this); _addExceptionSpan(node, () { _environment.setVariable( node.name, @@ -2609,10 +2609,7 @@ final class _EvaluateVisitor for (var variable in node.configuration) { var variableNodeWithSpan = _expressionNode(variable.expression); values[variable.name] = ConfiguredValue.explicit( - _withoutSlash( - variable.expression.accept(this), - variableNodeWithSpan, - ), + variable.expression.accept(this), variable.span, variableNodeWithSpan, ); @@ -2660,9 +2657,7 @@ final class _EvaluateVisitor // ## Expressions Value visitBinaryOperationExpression(BinaryOperationExpression node) { - if (_stylesheet.plainCss && - node.operator != BinaryOperator.singleEquals && - node.operator != BinaryOperator.dividedBy) { + if (_stylesheet.plainCss && node.operator != BinaryOperator.singleEquals) { throw _exception( "Operators aren't allowed in plain CSS.", node.operatorSpan, @@ -2696,73 +2691,16 @@ final class _EvaluateVisitor BinaryOperator.plus => left.plus(node.right.accept(this)), BinaryOperator.minus => left.minus(node.right.accept(this)), BinaryOperator.times => left.times(node.right.accept(this)), - BinaryOperator.dividedBy => _slash( - left, - node.right.accept(this), - node, - ), BinaryOperator.modulo => left.modulo(node.right.accept(this)), + + /// This can't be generated by the actual Sass parser, but it's still + /// supported by the AST for calculation purposes. Might as well support + /// it here too. + BinaryOperator.dividedBy => left.dividedBy(node.right.accept(this)), }; }); } - /// Returns the result of the SassScript `/` operation between [left] and - /// [right] in [node]. - Value _slash(Value left, Value right, BinaryOperationExpression node) { - var result = left.dividedBy(right); - switch ((left, right)) { - case (SassNumber left, SassNumber right) - when node.allowsSlash && - _operandAllowsSlash(node.left) && - _operandAllowsSlash(node.right): - return (result as SassNumber).withSlash(left, right); - - case (SassNumber(), SassNumber()): - String recommendation(Expression expression) => switch (expression) { - BinaryOperationExpression( - operator: BinaryOperator.dividedBy, - :var left, - :var right, - ) => - "math.div(${recommendation(left)}, ${recommendation(right)})", - ParenthesizedExpression() => expression.expression.toString(), - _ => expression.toString(), - }; - - _warn( - "Using / for division outside of calc() is deprecated " - "and will be removed in Dart Sass 2.0.0.\n" - "\n" - "Recommendation: ${recommendation(node)} or " - "${expressionToCalc(node)}\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/slash-div", - node.span, - Deprecation.slashDiv, - ); - return result; - - case _: - return result; - } - } - - /// Returns whether [node] can be used as a component of a slash-separated - /// number. - /// - /// Although this logic is mostly resolved at parse-time, we can't tell - /// whether operands will be evaluated as calculations until evaluation-time. - bool _operandAllowsSlash(Expression node) => - node is! FunctionExpression || - (node.namespace == null && - const { - "calc", "clamp", "hypot", "sin", "cos", "tan", "asin", "acos", // - "atan", "sqrt", "exp", "sign", "mod", "rem", "atan2", "pow", // - "log", "calc-size", - }.contains(node.name.toLowerCase()) && - _environment.getFunction(node.name) == null); - Value visitValueExpression(ValueExpression node) => node.value; Value visitVariableExpression(VariableExpression node) { @@ -2802,7 +2740,7 @@ final class _EvaluateVisitor var ifFalse = positional.elementAtOrNull(2) ?? named["if-false"]!; var result = condition.accept(this).isTruthy ? ifTrue : ifFalse; - return _withoutSlash(result.accept(this), _expressionNode(result)); + return result.accept(this); } Value visitNullExpression(NullExpression node) => sassNull; @@ -3212,6 +3150,25 @@ final class _EvaluateVisitor return SassString(elements.join(' '), quotes: false); + // Correctly handling slashes is complicated, because they're parsed as + // list separators and so have lower precedence than any mathematical + // operators. They *should* have higher precedence than addition or + // subtraction and equivalent precedence to multiplication. The spec + // handles this by reconstructing the AST once it's determined that it's + // evaluating a calculation, but we want to avoid the extra allocations so + // we handle it as we evaluate instead. + case ListExpression( + hasBrackets: false, + separator: ListSeparator.slash, + contents: var contents, + ): + var adjusted = _inFunction || _inMixin + ? _calcSlashListCache.putIfAbsent( + node, () => _adjustSlashPrecedence(contents)) + : _adjustSlashPrecedence(contents); + return _visitCalculationExpression(adjusted, + inLegacySassFunction: inLegacySassFunction); + case _: assert(!node.isCalculationSafe); throw _exception( @@ -3221,6 +3178,198 @@ final class _EvaluateVisitor } } + /// Converts the slash-separated list of [contents] into an expression that + /// matches `calc()` precedence. + /// + /// This is necessary because slashes are parsed as list separators and so + /// have lower precedence than any mathematical operators. They *should* have + /// higher precedence than addition or subtraction and equivalent precedence + /// to multiplication. + Expression _adjustSlashPrecedence(List contents) { + var left = contents.first; + for (var right in contents.skip(1)) { + if (right + case StringExpression( + text: Interpolation(asPlain: " "), + hasQuotes: false + )) { + throw _exception( + "This expression can't be used in a calculation.", + right.span, + ); + } + + left = switch (( + _splitCalculationSumTail(left), + _splitCalculationSumHead(right) + )) { + // Example: 1 + 2 / 3 + 4 + ( + // 1 + 2 + (var leftRemainder, var leftOperator, var leftOperand), + // 3 + 4 + (var rightOperand, var rightOperator, var rightRemainder) + ) => + // (1 + (2 / 3)) + 4 + BinaryOperationExpression( + rightOperator, + // 1 + (2 / 3) + BinaryOperationExpression( + leftOperator, + leftRemainder, + // 2 / 3 + BinaryOperationExpression( + BinaryOperator.dividedBy, leftOperand, rightOperand), + ), + rightRemainder, + ), + // Example: 1 + 2 / 3 + // 1 + 2 + ((var leftRemainder, var leftOperator, var leftOperand), _) => + // 1 + (2 / 3) + BinaryOperationExpression( + leftOperator, + leftRemainder, + // 2 / 3 + BinaryOperationExpression( + BinaryOperator.dividedBy, leftOperand, right), + ), + // Example: 1 / 2 + 3 + // 2 + 3 + (_, (var rightOperand, var rightOperator, var rightRemainder)) => + // (1 / 2) + 3 + BinaryOperationExpression( + rightOperator, + // 1 / 2 + BinaryOperationExpression( + BinaryOperator.dividedBy, left, rightOperand), + rightRemainder, + ), + _ => switch (right) { + // Example: 1 / 2 * 3 + BinaryOperationExpression( + operator: BinaryOperator.times, + left: var rightOperand, // 2 + right: var rightRemainder, // 3 + ) => + // (1 / 2) * 3 + BinaryOperationExpression( + BinaryOperator.times, + // 1 / 2 + BinaryOperationExpression( + BinaryOperator.dividedBy, left, rightOperand), + rightRemainder, + ), + _ => + BinaryOperationExpression(BinaryOperator.dividedBy, left, right), + }, + }; + } + + return left; + } + + /// If [expression] is a sequence of `+` or `-` operations, returns the + /// leftmost operator and its operand, as well as the remainder of the + /// expression (which may be rewritten according to the associative property). + /// + /// For example, if [expression] is `1 + 2 - 3 + 4`, this returns `(1, +, 2 + /// - 3 + 4)`. This works regardless of how [expression] has been parsed. + /// + /// If [expression] is not such a sequence, returns `null`. + (Expression, BinaryOperator, Expression)? _splitCalculationSumHead( + Expression expression) { + switch (expression) { + // Example: ((1 + 2) + 3) - 4 + case BinaryOperationExpression( + // - + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + left: BinaryOperationExpression( + operator: BinaryOperator.plus || BinaryOperator.minus + ) && + var left, // ((1 + 2) + 3) + :var right // 4 + ): + // (1, +, 2 + 3) + var (head, leftOperator, leftRemainder) = + _splitCalculationSumHead(left)!; + // (1, +, (2 + 3) - 4) + return ( + head, + leftOperator, + // (2 + 3) - 4 + BinaryOperationExpression(operator, leftRemainder, right) + ); + + // Example: 1 + 2 + case BinaryOperationExpression( + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + :var left, // 1 + :var right // 2 + ): + // (1, +, 2) + return (left, operator, right); + + case _: + return null; + } + } + + /// If [expression] is a sequence of `+` or `-` operations, returns the + /// rightmost operator and its operand, as well as the remainder of the + /// expression (which may be rewritten according to the associative property). + /// + /// For example, if [expression] is `1 + 2 - 3 + 4`, this returns `(1 + 2 - 3, + /// +, 4)`. This works regardless of how [expression] has been parsed. + /// + /// If [expression] is not such a sequence, returns `null`. + (Expression, BinaryOperator, Expression)? _splitCalculationSumTail( + Expression expression) { + // Example: 1 - (2 + (3 + 4)) + // + // Note: Sass parses operations left-associatively, so this parse won't + // actually appear naturally. We still handle it defensively in case it + // comes up from some other source. + switch (expression) { + case BinaryOperationExpression( + // - + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + :var left, // 1 + right: BinaryOperationExpression( + operator: BinaryOperator.plus || BinaryOperator.minus + ) && + var right // 2 + (3 + 4) + ): + // (2 + 3, +, 4) + var (rightRemainder, rightOperator, tail) = + _splitCalculationSumTail(right)!; + // (1 - (2 + 3), + 4) + return ( + // 1 - (2 + 3) + BinaryOperationExpression(operator, left, rightRemainder), + rightOperator, // + + tail // 4 + ); + + // Example: 1 + 2 + case BinaryOperationExpression( + // + + operator: + (BinaryOperator.plus || BinaryOperator.minus) && var operator, + :var left, // 1 + :var right // 2 + ): + // (1, +, 2) + return (left, operator, right); + + case _: + return null; + } + } + /// Throws an error if [node] requires whitespace around its operator in a /// calculation but doesn't have it. void _checkWhitespaceAroundCalculationOperator( @@ -3365,10 +3514,7 @@ final class _EvaluateVisitor i++) { var parameter = parameters[i]; var value = evaluated.named.remove(parameter.name) ?? - _withoutSlash( - parameter.defaultValue!.accept(this), - _expressionNode(parameter.defaultValue!), - ); + parameter.defaultValue!.accept(this); _environment.setLocalVariable( parameter.name, value, @@ -3432,10 +3578,7 @@ final class _EvaluateVisitor AstNode nodeWithSpan, ) { if (callable is BuiltInCallable) { - return _withoutSlash( - _runBuiltInCallable(arguments, callable, nodeWithSpan), - nodeWithSpan, - ); + return _runBuiltInCallable(arguments, callable, nodeWithSpan); } else if (callable is UserDefinedCallable) { return _runUserDefinedCallable( arguments, @@ -3524,10 +3667,7 @@ final class _EvaluateVisitor var parameter = parameters[i]; evaluated.positional.add( evaluated.named.remove(parameter.name) ?? - _withoutSlash( - parameter.defaultValue!.accept(this), - parameter.defaultValue!, - ), + parameter.defaultValue!.accept(this), ); } @@ -3596,7 +3736,7 @@ final class _EvaluateVisitor var positionalNodes = []; for (var expression in arguments.positional) { var nodeForSpan = _expressionNode(expression); - positional.add(_withoutSlash(expression.accept(this), nodeForSpan)); + positional.add(expression.accept(this)); positionalNodes.add(nodeForSpan); } @@ -3604,7 +3744,7 @@ final class _EvaluateVisitor var namedNodes = {}; for (var (name, value) in arguments.named.pairs) { var nodeForSpan = _expressionNode(value); - named[name] = _withoutSlash(value.accept(this), nodeForSpan); + named[name] = value.accept(this); namedNodes[name] = nodeForSpan; } @@ -3629,20 +3769,18 @@ final class _EvaluateVisitor (key as SassString).text: restNodeForSpan, }); } else if (rest is SassList) { - positional.addAll( - rest.asList.map((value) => _withoutSlash(value, restNodeForSpan)), - ); + positional.addAll(rest.asList.map((value) => value)); positionalNodes.addAll(List.filled(rest.lengthAsList, restNodeForSpan)); separator = rest.separator; if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { - named[key] = _withoutSlash(value, restNodeForSpan); + named[key] = value; namedNodes[key] = restNodeForSpan; }); } } else { - positional.add(_withoutSlash(rest, restNodeForSpan)); + positional.add(rest); positionalNodes.add(restNodeForSpan); } @@ -3696,7 +3834,6 @@ final class _EvaluateVisitor var positional = invocation.arguments.positional.toList(); var named = Map.of(invocation.arguments.named); var rest = restArgs.accept(this); - var restNodeForSpan = _expressionNode(restArgs); if (rest is SassMap) { _addRestMap( named, @@ -3708,7 +3845,7 @@ final class _EvaluateVisitor positional.addAll( rest.asList.map( (value) => ValueExpression( - _withoutSlash(value, restNodeForSpan), + value, restArgs.span, ), ), @@ -3716,14 +3853,14 @@ final class _EvaluateVisitor if (rest is SassArgumentList) { rest.keywords.forEach((key, value) { named[key] = ValueExpression( - _withoutSlash(value, restNodeForSpan), + value, restArgs.span, ); }); } } else { positional.add( - ValueExpression(_withoutSlash(rest, restNodeForSpan), restArgs.span), + ValueExpression(rest, restArgs.span), ); } @@ -3732,14 +3869,13 @@ final class _EvaluateVisitor var keywordRestArgs = keywordRestArgs_; // dart-lang/sdk#45348 var keywordRest = keywordRestArgs.accept(this); - var keywordRestNodeForSpan = _expressionNode(keywordRestArgs); if (keywordRest is SassMap) { _addRestMap( named, keywordRest, invocation, (value) => ValueExpression( - _withoutSlash(value, keywordRestNodeForSpan), + value, keywordRestArgs.span, ), ); @@ -3769,10 +3905,9 @@ final class _EvaluateVisitor AstNode nodeWithSpan, T convert(Value value), ) { - var expressionNode = _expressionNode(nodeWithSpan); map.contents.forEach((key, value) { if (key is SassString) { - values[key.text] = convert(_withoutSlash(value, expressionNode)); + values[key.text] = convert(value); } else { throw _exception( "Variable keyword argument map must have string keys.\n" @@ -4419,32 +4554,6 @@ final class _EvaluateVisitor return result; } - /// Like [Value.withoutSlash], but produces a deprecation warning if [value] - /// was a slash-separated number. - Value _withoutSlash(Value value, AstNode nodeForSpan) { - if (value case SassNumber(asSlash: _?)) { - String recommendation(SassNumber number) => switch (number.asSlash) { - (var before, var after) => - "math.div(${recommendation(before)}, ${recommendation(after)})", - _ => number.toString(), - }; - - _warn( - "Using / for division is deprecated and will be removed in Dart Sass " - "2.0.0.\n" - "\n" - "Recommendation: ${recommendation(value)}\n" - "\n" - "More info and automated migrator: " - "https://sass-lang.com/d/slash-div", - nodeForSpan.span, - Deprecation.slashDiv, - ); - } - - return value.withoutSlash(); - } - /// Creates a new stack frame with location information from [member] and /// [span]. Frame _stackFrame(String member, FileSpan span) => frameForSpan( diff --git a/lib/src/visitor/is_calculation_safe.dart b/lib/src/visitor/is_calculation_safe.dart index f2501050e..31245d332 100644 --- a/lib/src/visitor/is_calculation_safe.dart +++ b/lib/src/visitor/is_calculation_safe.dart @@ -44,7 +44,8 @@ class IsCalculationSafeVisitor implements ExpressionVisitor { bool visitIfExpression(IfExpression node) => true; bool visitListExpression(ListExpression node) => - node.separator == ListSeparator.space && + (node.separator == ListSeparator.space || + node.separator == ListSeparator.slash) && !node.hasBrackets && node.contents.length > 1 && node.contents.every((expression) => expression.accept(this)); diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 01ee24a24..592200662 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -1030,22 +1030,26 @@ final class _SerializeVisitor value.separator == ListSeparator.slash); if (singleton && !value.hasBrackets) _buffer.writeCharCode($lparen); - _writeBetween( - _inspect - ? value.asList - : value.asList.where((element) => !element.isBlank), - _separatorString(value.separator), - _inspect - ? (element) { - var needsParens = _elementNeedsParens(value.separator, element); - if (needsParens) _buffer.writeCharCode($lparen); - element.accept(this); - if (needsParens) _buffer.writeCharCode($rparen); - } - : (element) { - element.accept(this); - }, - ); + if (!_isCompressed && value.separator == ListSeparator.slash) { + _writeUncompressedSlashListContents(value); + } else { + _writeBetween( + _inspect + ? value.asList + : value.asList.where((element) => !element.isBlank), + _separatorString(value.separator), + _inspect + ? (element) { + var needsParens = _elementNeedsParens(value.separator, element); + if (needsParens) _buffer.writeCharCode($lparen); + element.accept(this); + if (needsParens) _buffer.writeCharCode($rparen); + } + : (element) { + element.accept(this); + }, + ); + } if (singleton) { _buffer.write(value.separator.separator); @@ -1055,6 +1059,48 @@ final class _SerializeVisitor if (value.hasBrackets) _buffer.writeCharCode($rbracket); } + /// Writes the contents of a slash-separated list in uncompressed mode. + /// + /// This uses a different code-path because it's more complex and potentially + /// more expensive in order to produce elegant output for empty elements like + /// those allowed by the `border-image` shorthand syntax. + void _writeUncompressedSlashListContents(SassList value) { + var contents = _inspect + ? value.asList + : [ + for (var element in value.asList) + if (!element.isBlank) element + ]; + var lastElementEmpty = false; + for (var i = 0; i < contents.length; i++) { + var element = contents[i]; + if (i != 0) { + if (!lastElementEmpty) { + _buffer.writeCharCode($space); + lastElementEmpty = false; + } + + _buffer.writeCharCode($slash); + if (element case SassString(text: ' ', hasQuotes: false)) { + lastElementEmpty = true; + } else { + _buffer.writeCharCode($space); + } + } else if (element case SassString(text: ' ', hasQuotes: false)) { + lastElementEmpty = true; + } + + if (_inspect) { + var needsParens = _elementNeedsParens(value.separator, element); + if (needsParens) _buffer.writeCharCode($lparen); + element.accept(this); + if (needsParens) _buffer.writeCharCode($rparen); + } else { + element.accept(this); + } + } + } + /// Returns the string to use to separate list items for lists with the given [separator]. String _separatorString(ListSeparator separator) => switch (separator) { ListSeparator.comma => _commaSeparator, @@ -1108,13 +1154,6 @@ final class _SerializeVisitor } void visitNumber(SassNumber value) { - if (value.asSlash case (var before, var after)) { - visitNumber(before); - _buffer.writeCharCode($slash); - visitNumber(after); - return; - } - if (!value.value.isFinite) { visitCalculation(SassCalculation.unsimplified('calc', [value])); return; diff --git a/pkg/sass-parser/CHANGELOG.md b/pkg/sass-parser/CHANGELOG.md index 557b75911..b22b089b1 100644 --- a/pkg/sass-parser/CHANGELOG.md +++ b/pkg/sass-parser/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.5.0-dev -* No user-visible changes. +* `/` is now parsed as slash-separated lists rather than division in SassScript. ## 0.4.27-dev diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 6c9aa7dde..ea816591c 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,5 +1,7 @@ ## 16.0.0 +* `/` is now parsed as slash-separated lists rather than division in SassScript. + ### Bogus Selectors * Drop support for bogus selectors that can never become valid CSS through diff --git a/test/cli/shared.dart b/test/cli/shared.dart index 46ae07844..421d16232 100644 --- a/test/cli/shared.dart +++ b/test/cli/shared.dart @@ -683,39 +683,39 @@ void sharedTests( group("with a bunch of deprecation warnings", () { setUp(() async { + await d.file("_other.scss", "").create(); await d.file("test.scss", r""" - @use "sass:list"; - @use "sass:meta"; - - $_: meta.call("inspect", null); - $_: meta.call("rgb", 0, 0, 0); - $_: meta.call("nth", null, 1); - $_: meta.call("join", null, null); - $_: meta.call("if", true, 1, 2); - $_: meta.call("hsl", 0, 100%, 100%); - - $_: 1/2; - $_: 1/3; - $_: 1/4; - $_: 1/5; - $_: 1/6; - $_: 1/7; + @import "other"; + @import "./other"; + @import "././other"; + @import "./././other"; + @import "././././other"; + @import "./././././other"; + + $_: nth(a b c d e f g, 1); + $_: nth(a b c d e f g, 2); + $_: nth(a b c d e f g, 3); + $_: nth(a b c d e f g, 4); + $_: nth(a b c d e f g, 5); + $_: nth(a b c d e f g, 6); """).create(); }); test("without --verbose, only prints five", () async { var sass = await runSass(["test.scss"]); + expect( sass.stderr, - emitsInOrder(List.filled(5, emitsThrough(contains("call()")))), + emitsInOrder(List.filled(5, emitsThrough(contains("[import]")))), ); - expect(sass.stderr, neverEmits(contains("call()"))); + expect(sass.stderr, neverEmits(contains("[import]"))); expect( sass.stderr, - emitsInOrder(List.filled(5, emitsThrough(contains("math.div")))), + emitsInOrder( + List.filled(5, emitsThrough(contains("[global-builtin]")))), ); - expect(sass.stderr, neverEmits(contains("math.div()"))); + expect(sass.stderr, neverEmits(contains("[global-builtin]"))); expect( sass.stderr, @@ -732,12 +732,12 @@ void sharedTests( expect( sass.stderr, - emitsInOrder(List.filled(6, emitsThrough(contains("call()")))), + emitsInOrder(List.filled(6, emitsThrough(contains("import")))), ); expect( sass.stderr, - emitsInOrder(List.filled(6, emitsThrough(contains("math.div")))), + emitsInOrder(List.filled(6, emitsThrough(contains("global-builtin")))), ); }); }); @@ -867,29 +867,30 @@ void sharedTests( group("with --fatal-deprecation", () { test("set to a specific deprecation, errors as intended", () async { - await d.file("test.scss", "a {b: (4/2)}").create(); - var sass = await runSass(["--fatal-deprecation=slash-div", "test.scss"]); + await d.file("test.scss", "a {b: nth(1 2 3, 1)}").create(); + var sass = + await runSass(["--fatal-deprecation=global-builtin", "test.scss"]); expect(sass.stdout, emitsDone); await sass.shouldExit(65); }); test("set to version, errors as intended", () async { - await d.file("test.scss", "a {b: (4/2)}").create(); - var sass = await runSass(["--fatal-deprecation=1.33.0", "test.scss"]); + await d.file("test.scss", "a {b: nth(1 2 3, 1)}").create(); + var sass = await runSass(["--fatal-deprecation=1.80.0", "test.scss"]); expect(sass.stdout, emitsDone); await sass.shouldExit(65); }); test("set to lower version, only warns", () async { - await d.file("test.scss", "a {b: (4/2)}").create(); - var sass = await runSass(["--fatal-deprecation=1.32.0", "test.scss"]); - expect(sass.stdout, emitsInOrder(["a {", " b: 2;", "}"])); + await d.file("test.scss", "a {b: nth(1 2 3, 1)}").create(); + var sass = await runSass(["--fatal-deprecation=1.79.0", "test.scss"]); + expect(sass.stdout, emitsInOrder(["a {", " b: 1;", "}"])); expect(sass.stderr, emitsThrough(contains("DEPRECATION WARNING"))); await sass.shouldExit(0); }); test("set to future version, usage error", () async { - await d.file("test.scss", "a {b: (4/2)}").create(); + await d.file("test.scss", "a {b: nth(1 2 3, 1)}").create(); var sass = await runSass(["--fatal-deprecation=1000.0.0", "test.scss"]); expect(sass.stdout, emitsThrough(contains("Invalid version 1000.0.0"))); await sass.shouldExit(64); diff --git a/test/deprecations_test.dart b/test/deprecations_test.dart index 91c618451..8de05a632 100644 --- a/test/deprecations_test.dart +++ b/test/deprecations_test.dart @@ -39,11 +39,6 @@ void main() { }); }); - // Deprecated in 1.33.0 - test("slashDiv is violated by using / for division", () { - _expectDeprecation(r"a {b: (4/2)}", Deprecation.slashDiv); - }); - // Deprecated in 1.55.0 group("strictUnary is violated by", () { test("an ambiguous + operator", () { diff --git a/test/embedded/function_test.dart b/test/embedded/function_test.dart index bd0761cc6..bec4aeaf5 100644 --- a/test/embedded/function_test.dart +++ b/test/embedded/function_test.dart @@ -768,7 +768,7 @@ void main() { }); test("with a slash separator", () async { - var list = (await _protofy(r"list.slash(true, null, false)")).list; + var list = (await _protofy(r"true / null / false")).list; expect(list.contents, equals([_true, _null, _false])); expect(list.hasBrackets, isFalse); expect(list.separator, equals(ListSeparator.SLASH)); @@ -803,7 +803,7 @@ void main() { test("with a slash separator", () async { var list = (await _protofy( - r"capture-args(list.slash(true, null, false)...)", + r"capture-args(true / null / false...)", )) .argumentList; expect(list.contents, [_true, _null, _false]);