From 45e2ffd6e2b0975dcce640065b25be2017d6f4d4 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 12 Nov 2024 17:51:08 -0800 Subject: [PATCH] Add support for CSS `round()` with one unitless argument This also adds deprecation warnings for cases where a CSS calculation is determined to need to mimic a global Sass function, to match the global Sass function deprecation. See sass/sass#3803 Closes #2433 --- CHANGELOG.md | 8 +++ lib/src/ast/css/style_rule.dart | 2 +- lib/src/js/value/calculation.dart | 4 +- lib/src/value/calculation.dart | 89 +++++++++++++++++++++++------ lib/src/visitor/async_evaluate.dart | 35 ++++++++---- lib/src/visitor/evaluate.dart | 37 ++++++++---- pkg/sass-parser/CHANGELOG.md | 4 ++ pkg/sass-parser/package.json | 2 +- pkg/sass_api/CHANGELOG.md | 4 ++ pkg/sass_api/pubspec.yaml | 4 +- pubspec.yaml | 2 +- 11 files changed, 145 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a13f2ce2f..397dc612c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.81.0 + +* Fix a few cases where deprecation warnings weren't being emitted for global + built-in functions whose names overlap with CSS calculations. + +* Add support for the CSS `round()` calculation with a single argument, as long + as that argument might be a unitless number. + ## 1.80.7 ### Embedded Host diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart index 8b9da6663..58bbe5424 100644 --- a/lib/src/ast/css/style_rule.dart +++ b/lib/src/ast/css/style_rule.dart @@ -21,7 +21,7 @@ abstract interface class CssStyleRule implements CssParentNode { /// Whether this style rule was originally defined in a plain CSS stylesheet. /// - /// :nodoc: + /// @nodoc @internal bool get fromPlainCss; } diff --git a/lib/src/js/value/calculation.dart b/lib/src/js/value/calculation.dart index b8e97a4c5..96e13c5b0 100644 --- a/lib/src/js/value/calculation.dart +++ b/lib/src/js/value/calculation.dart @@ -88,7 +88,7 @@ final JSClass calculationOperationClass = () { _assertCalculationValue(left); _assertCalculationValue(right); return SassCalculation.operateInternal(operator, left, right, - inLegacySassFunction: false, simplify: false); + inLegacySassFunction: null, simplify: false, warn: null); }); jsClass.defineMethods({ @@ -104,7 +104,7 @@ final JSClass calculationOperationClass = () { getJSClass(SassCalculation.operateInternal( CalculationOperator.plus, SassNumber(1), SassNumber(1), - inLegacySassFunction: false, simplify: false)) + inLegacySassFunction: null, simplify: false, warn: null)) .injectSuperclass(jsClass); return jsClass; }(); diff --git a/lib/src/value/calculation.dart b/lib/src/value/calculation.dart index 261400dc2..8e0447a3d 100644 --- a/lib/src/value/calculation.dart +++ b/lib/src/value/calculation.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; import '../deprecation.dart'; import '../evaluation_context.dart'; @@ -482,13 +483,47 @@ final class SassCalculation extends Value { /// This may be passed fewer than two arguments, but only if one of the /// arguments is an unquoted `var()` string. static Value round(Object strategyOrNumber, - [Object? numberOrStep, Object? step]) { + [Object? numberOrStep, Object? step]) => + roundInternal(strategyOrNumber, numberOrStep, step, + span: null, inLegacySassFunction: null, warn: null); + + /// Like [round], but with the internal-only [inLegacySassFunction] and + /// [warn] parameters. + /// + /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be + /// added and subtracted with numbers with units, for backwards-compatibility + /// with the old global `min()` and `max()` functions. This emits a + /// deprecation warning using the string as the function's name. + /// + /// If [simplify] is `false`, no simplification will be done. + /// + /// The [warn] callback is used to surface deprecation warnings. + /// + /// @nodoc + @internal + static Value roundInternal( + Object strategyOrNumber, Object? numberOrStep, Object? step, + {required FileSpan? span, + required String? inLegacySassFunction, + required void Function(String message, [Deprecation? deprecation])? + warn}) { switch (( _simplify(strategyOrNumber), numberOrStep.andThen(_simplify), step.andThen(_simplify) )) { - case (SassNumber number, null, null): + case (SassNumber(hasUnits: false) && var number, null, null): + return SassNumber(number.value.round()); + + case (SassNumber number, null, null) when inLegacySassFunction != null: + warn!( + "In future versions of Sass, round() will be interpreted as a CSS " + "round() calculation. This requires an explicit modulus when " + "rounding numbers with units. If you want to use the Sass " + "function, call math.round() instead.\n" + "\n" + "See https://sass-lang.com/d/import", + Deprecation.globalBuiltin); return _matchUnits(number.value.round().toDouble(), number); case (SassNumber number, SassNumber step, null) @@ -542,12 +577,8 @@ final class SassCalculation extends Value { throw SassScriptException( "Number to round and step arguments are required."); - case (SassString rest, null, null): - return SassCalculation._("round", [rest]); - case (var number, null, null): - throw SassScriptException( - "Single argument $number expected to be simplifiable."); + return SassCalculation._("round", [number]); case (var number, var step?, null): return SassCalculation._("round", [number, step]); @@ -584,32 +615,54 @@ final class SassCalculation extends Value { static Object operate( CalculationOperator operator, Object left, Object right) => operateInternal(operator, left, right, - inLegacySassFunction: false, simplify: true); + inLegacySassFunction: null, simplify: true, warn: null); - /// Like [operate], but with the internal-only [inLegacySassFunction] parameter. + /// Like [operate], but with the internal-only [inLegacySassFunction] and + /// [warn] parameters. /// - /// If [inLegacySassFunction] is `true`, this allows unitless numbers to be added and - /// subtracted with numbers with units, for backwards-compatibility with the - /// old global `min()` and `max()` functions. + /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be + /// added and subtracted with numbers with units, for backwards-compatibility + /// with the old global `min()` and `max()` functions. This emits a + /// deprecation warning using the string as the function's name. /// /// If [simplify] is `false`, no simplification will be done. + /// + /// The [warn] callback is used to surface deprecation warnings. + /// + /// @nodoc @internal static Object operateInternal( CalculationOperator operator, Object left, Object right, - {required bool inLegacySassFunction, required bool simplify}) { + {required String? inLegacySassFunction, + required bool simplify, + required void Function(String message, [Deprecation? deprecation])? + warn}) { if (!simplify) return CalculationOperation._(operator, left, right); left = _simplify(left); right = _simplify(right); if (operator case CalculationOperator.plus || CalculationOperator.minus) { - if (left is SassNumber && - right is SassNumber && - (inLegacySassFunction - ? left.isComparableTo(right) - : left.hasCompatibleUnits(right))) { + if (left is SassNumber && right is SassNumber) { + var compatible = left.hasCompatibleUnits(right); + if (!compatible && + inLegacySassFunction != null && + left.isComparableTo(right)) { + warn!( + "In future versions of Sass, $inLegacySassFunction() will be " + "interpreted as the CSS $inLegacySassFunction() calculation. " + "This doesn't allow unitless numbers to be mixed with numbers " + "with units. If you want to use the Sass function, call " + "math.$inLegacySassFunction() instead.\n" + "\n" + "See https://sass-lang.com/d/import", + Deprecation.globalBuiltin); + compatible = true; + } + if (compatible) { return operator == CalculationOperator.plus ? left.plus(right) : left.minus(right); + } } _verifyCompatibleNumbers([left, right]); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index aba5cee7c..c06b45484 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2555,12 +2555,12 @@ final class _EvaluateVisitor // Note that the list of calculation functions is also tracked in // lib/src/visitor/is_plain_css_safe.dart. switch (node.name.toLowerCase()) { - case "min" || "max" || "round" || "abs" + case ("min" || "max" || "round" || "abs") && var name when node.arguments.named.isEmpty && node.arguments.rest == null && node.arguments.positional .every((argument) => argument.isCalculationSafe): - return await _visitCalculation(node, inLegacySassFunction: true); + return await _visitCalculation(node, inLegacySassFunction: name); case "calc" || "clamp" || @@ -2607,8 +2607,15 @@ final class _EvaluateVisitor return result; } + /// Evaluates [node] as a calculation. + /// + /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be + /// added and subtracted with numbers with units, for backwards-compatibility + /// with the old global `min()`, `max()`, `round()`, and `abs()` functions. + /// The parameter is the name of the function, which is used for reporting + /// deprecation warnings. Future _visitCalculation(FunctionExpression node, - {bool inLegacySassFunction = false}) async { + {String? inLegacySassFunction}) async { if (node.arguments.named.isNotEmpty) { throw _exception( "Keyword arguments can't be used with calculations.", node.span); @@ -2656,8 +2663,12 @@ final class _EvaluateVisitor SassCalculation.mod(arguments[0], arguments.elementAtOrNull(1)), "rem" => SassCalculation.rem(arguments[0], arguments.elementAtOrNull(1)), - "round" => SassCalculation.round(arguments[0], - arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), + "round" => SassCalculation.roundInternal(arguments[0], + arguments.elementAtOrNull(1), arguments.elementAtOrNull(2), + span: node.span, + inLegacySassFunction: inLegacySassFunction, + warn: (message, [deprecation]) => + _warn(message, node.span, deprecation)), "clamp" => SassCalculation.clamp(arguments[0], arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), _ => throw UnsupportedError('Unknown calculation name "${node.name}".') @@ -2753,11 +2764,13 @@ final class _EvaluateVisitor /// Evaluates [node] as a component of a calculation. /// - /// If [inLegacySassFunction] is `true`, this allows unitless numbers to be added and - /// subtracted with numbers with units, for backwards-compatibility with the - /// old global `min()`, `max()`, `round()`, and `abs()` functions. + /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be + /// added and subtracted with numbers with units, for backwards-compatibility + /// with the old global `min()`, `max()`, `round()`, and `abs()` functions. + /// The parameter is the name of the function, which is used for reporting + /// deprecation warnings. Future _visitCalculationExpression(Expression node, - {required bool inLegacySassFunction}) async { + {required String? inLegacySassFunction}) async { switch (node) { case ParenthesizedExpression(expression: var inner): var result = await _visitCalculationExpression(inner, @@ -2788,7 +2801,9 @@ final class _EvaluateVisitor await _visitCalculationExpression(right, inLegacySassFunction: inLegacySassFunction), inLegacySassFunction: inLegacySassFunction, - simplify: !_inSupportsDeclaration)); + simplify: !_inSupportsDeclaration, + warn: (message, [deprecation]) => + _warn(message, node.span, deprecation))); case NumberExpression() || VariableExpression() || diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index f9990c828..792326ecc 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: 3986f5db33dd220dcd971a39e8587ca4e52d9a3f +// Checksum: fbffa0dbe5a1af846dc83752457d39fb2984d280 // // ignore_for_file: unused_import @@ -2533,12 +2533,12 @@ final class _EvaluateVisitor // Note that the list of calculation functions is also tracked in // lib/src/visitor/is_plain_css_safe.dart. switch (node.name.toLowerCase()) { - case "min" || "max" || "round" || "abs" + case ("min" || "max" || "round" || "abs") && var name when node.arguments.named.isEmpty && node.arguments.rest == null && node.arguments.positional .every((argument) => argument.isCalculationSafe): - return _visitCalculation(node, inLegacySassFunction: true); + return _visitCalculation(node, inLegacySassFunction: name); case "calc" || "clamp" || @@ -2585,8 +2585,15 @@ final class _EvaluateVisitor return result; } + /// Evaluates [node] as a calculation. + /// + /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be + /// added and subtracted with numbers with units, for backwards-compatibility + /// with the old global `min()`, `max()`, `round()`, and `abs()` functions. + /// The parameter is the name of the function, which is used for reporting + /// deprecation warnings. Value _visitCalculation(FunctionExpression node, - {bool inLegacySassFunction = false}) { + {String? inLegacySassFunction}) { if (node.arguments.named.isNotEmpty) { throw _exception( "Keyword arguments can't be used with calculations.", node.span); @@ -2634,8 +2641,12 @@ final class _EvaluateVisitor SassCalculation.mod(arguments[0], arguments.elementAtOrNull(1)), "rem" => SassCalculation.rem(arguments[0], arguments.elementAtOrNull(1)), - "round" => SassCalculation.round(arguments[0], - arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), + "round" => SassCalculation.roundInternal(arguments[0], + arguments.elementAtOrNull(1), arguments.elementAtOrNull(2), + span: node.span, + inLegacySassFunction: inLegacySassFunction, + warn: (message, [deprecation]) => + _warn(message, node.span, deprecation)), "clamp" => SassCalculation.clamp(arguments[0], arguments.elementAtOrNull(1), arguments.elementAtOrNull(2)), _ => throw UnsupportedError('Unknown calculation name "${node.name}".') @@ -2731,11 +2742,13 @@ final class _EvaluateVisitor /// Evaluates [node] as a component of a calculation. /// - /// If [inLegacySassFunction] is `true`, this allows unitless numbers to be added and - /// subtracted with numbers with units, for backwards-compatibility with the - /// old global `min()`, `max()`, `round()`, and `abs()` functions. + /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be + /// added and subtracted with numbers with units, for backwards-compatibility + /// with the old global `min()`, `max()`, `round()`, and `abs()` functions. + /// The parameter is the name of the function, which is used for reporting + /// deprecation warnings. Object _visitCalculationExpression(Expression node, - {required bool inLegacySassFunction}) { + {required String? inLegacySassFunction}) { switch (node) { case ParenthesizedExpression(expression: var inner): var result = _visitCalculationExpression(inner, @@ -2766,7 +2779,9 @@ final class _EvaluateVisitor _visitCalculationExpression(right, inLegacySassFunction: inLegacySassFunction), inLegacySassFunction: inLegacySassFunction, - simplify: !_inSupportsDeclaration)); + simplify: !_inSupportsDeclaration, + warn: (message, [deprecation]) => + _warn(message, node.span, deprecation))); case NumberExpression() || VariableExpression() || diff --git a/pkg/sass-parser/CHANGELOG.md b/pkg/sass-parser/CHANGELOG.md index 2ccb58b4c..6a715bc87 100644 --- a/pkg/sass-parser/CHANGELOG.md +++ b/pkg/sass-parser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.5 + +* No user-visible changes. + ## 0.4.4 * No user-visible changes. diff --git a/pkg/sass-parser/package.json b/pkg/sass-parser/package.json index d9b596055..d3232dd2c 100644 --- a/pkg/sass-parser/package.json +++ b/pkg/sass-parser/package.json @@ -1,6 +1,6 @@ { "name": "sass-parser", - "version": "0.4.4", + "version": "0.4.5", "description": "A PostCSS-compatible wrapper of the official Sass parser", "repository": "sass/sass", "author": "Google Inc.", diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 8c61559f3..b65d12b7d 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.2.0 + +* No user-visible changes. + ## 14.1.3 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 8784f3bea..fd8ce6638 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 14.1.3 +version: 14.2.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.3.0 <4.0.0" dependencies: - sass: 1.80.7 + sass: 1.81.0 dev_dependencies: dartdoc: ^8.0.14 diff --git a/pubspec.yaml b/pubspec.yaml index 96061478f..9acd22205 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.80.7 +version: 1.81.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass