Skip to content

Commit

Permalink
Add support for CSS round() with one unitless argument (#2436)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nex3 authored Nov 15, 2024
1 parent e400bdd commit 57a6853
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 54 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## 1.81.0-dev
## 1.81.0

* No user-visible changes.
* 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

Expand Down
2 changes: 1 addition & 1 deletion lib/src/ast/css/style_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions lib/src/js/value/calculation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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;
}();
Expand Down
95 changes: 74 additions & 21 deletions lib/src/value/calculation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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))) {
return operator == CalculationOperator.plus
? left.plus(right)
: left.minus(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]);
Expand Down
35 changes: 25 additions & 10 deletions lib/src/visitor/async_evaluate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand Down Expand Up @@ -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<Value> _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);
Expand Down Expand Up @@ -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}".')
Expand Down Expand Up @@ -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<Object> _visitCalculationExpression(Expression node,
{required bool inLegacySassFunction}) async {
{required String? inLegacySassFunction}) async {
switch (node) {
case ParenthesizedExpression(expression: var inner):
var result = await _visitCalculationExpression(inner,
Expand Down Expand Up @@ -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() ||
Expand Down
37 changes: 26 additions & 11 deletions lib/src/visitor/evaluate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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" ||
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}".')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() ||
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## 0.4.5-dev
## 0.4.5

* Add support for parsing the `@forward` rule.

Expand Down
2 changes: 1 addition & 1 deletion pkg/sass-parser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sass-parser",
"version": "0.4.5-dev",
"version": "0.4.5",
"description": "A PostCSS-compatible wrapper of the official Sass parser",
"repository": "sass/sass",
"author": "Google Inc.",
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## 14.2.0-dev
## 14.2.0

* No user-visible changes.

Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.0-dev
version: 14.2.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: sass
version: 1.81.0-dev
version: 1.81.0
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass

Expand Down
Loading

0 comments on commit 57a6853

Please sign in to comment.