Skip to content

Commit 5c31d1f

Browse files
authoredSep 14, 2023
Re-enable new calculation functions (#2080)
1 parent bdb145f commit 5c31d1f

31 files changed

+1677
-686
lines changed
 

‎CHANGELOG.md

+26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
## 1.67.0
22

3+
* All functions defined in CSS Values and Units 4 are now once again parsed as
4+
calculation objects: `round()`, `mod()`, `rem()`, `sin()`, `cos()`, `tan()`,
5+
`asin()`, `acos()`, `atan()`, `atan2()`, `pow()`, `sqrt()`, `hypot()`,
6+
`log()`, `exp()`, `abs()`, and `sign()`.
7+
8+
Unlike in 1.65.0, function calls are _not_ locked into being parsed as
9+
calculations or plain Sass functions at parse-time. This means that
10+
user-defined functions will take precedence over CSS calculations of the same
11+
name. Although the function names `calc()` and `clamp()` are still forbidden,
12+
users may continue to freely define functions whose names overlap with other
13+
CSS calculations (including `abs()`, `min()`, `max()`, and `round()` whose
14+
names overlap with global Sass functions).
15+
16+
* As a consequence of the change in calculation parsing described above,
17+
calculation functions containing interpolation are now parsed more strictly
18+
than before. However, all interpolations that would have produced valid CSS
19+
will continue to work, so this is not considered a breaking change.
20+
21+
* Interpolations in calculation functions that aren't used in a position that
22+
could also have a normal calculation value are now deprecated. For example,
23+
`calc(1px #{"+ 2px"})` is deprecated, but `calc(1px + #{"2px"})` is still
24+
allowed. This deprecation is named `calc-interp`. See [the Sass website] for
25+
more information.
26+
27+
[the Sass website]: https://sass-lang.com/d/calc-interp
28+
329
* **Potentially breaking bug fix**: The importer used to load a given file is no
430
longer used to load absolute URLs that appear in that file. This was
531
unintented behavior that contradicted the Sass specification. Absolute URLs

‎lib/src/ast/sass.dart

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export 'sass/dependency.dart';
1313
export 'sass/expression.dart';
1414
export 'sass/expression/binary_operation.dart';
1515
export 'sass/expression/boolean.dart';
16-
export 'sass/expression/calculation.dart';
1716
export 'sass/expression/color.dart';
1817
export 'sass/expression/function.dart';
1918
export 'sass/expression/if.dart';

‎lib/src/ast/sass/expression.dart

+86-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import 'package:charcode/charcode.dart';
56
import 'package:meta/meta.dart';
67

78
import '../../exception.dart';
89
import '../../logger.dart';
910
import '../../parse/scss.dart';
11+
import '../../util/nullable.dart';
12+
import '../../value.dart';
1013
import '../../visitor/interface/expression.dart';
11-
import 'node.dart';
14+
import '../sass.dart';
1215

1316
/// A SassScript expression in a Sass syntax tree.
1417
///
@@ -27,3 +30,85 @@ abstract interface class Expression implements SassNode {
2730
factory Expression.parse(String contents, {Object? url, Logger? logger}) =>
2831
ScssParser(contents, url: url, logger: logger).parseExpression();
2932
}
33+
34+
// Use an extension class rather than a method so we don't have to make
35+
// [Expression] a concrete base class for something we'll get rid of anyway once
36+
// we remove the global math functions that make this necessary.
37+
extension ExpressionExtensions on Expression {
38+
/// Whether this expression can be used in a calculation context.
39+
///
40+
/// @nodoc
41+
@internal
42+
bool get isCalculationSafe => accept(_IsCalculationSafeVisitor());
43+
}
44+
45+
// We could use [AstSearchVisitor] to implement this more tersely, but that
46+
// would default to returning `true` if we added a new expression type and
47+
// forgot to update this class.
48+
class _IsCalculationSafeVisitor implements ExpressionVisitor<bool> {
49+
const _IsCalculationSafeVisitor();
50+
51+
bool visitBinaryOperationExpression(BinaryOperationExpression node) =>
52+
(const {
53+
BinaryOperator.times,
54+
BinaryOperator.dividedBy,
55+
BinaryOperator.plus,
56+
BinaryOperator.minus
57+
}).contains(node.operator) &&
58+
(node.left.accept(this) || node.right.accept(this));
59+
60+
bool visitBooleanExpression(BooleanExpression node) => false;
61+
62+
bool visitColorExpression(ColorExpression node) => false;
63+
64+
bool visitFunctionExpression(FunctionExpression node) => true;
65+
66+
bool visitInterpolatedFunctionExpression(
67+
InterpolatedFunctionExpression node) =>
68+
true;
69+
70+
bool visitIfExpression(IfExpression node) => true;
71+
72+
bool visitListExpression(ListExpression node) =>
73+
node.separator == ListSeparator.space &&
74+
!node.hasBrackets &&
75+
node.contents.length > 1 &&
76+
node.contents.every((expression) => expression.accept(this));
77+
78+
bool visitMapExpression(MapExpression node) => false;
79+
80+
bool visitNullExpression(NullExpression node) => false;
81+
82+
bool visitNumberExpression(NumberExpression node) => true;
83+
84+
bool visitParenthesizedExpression(ParenthesizedExpression node) =>
85+
node.expression.accept(this);
86+
87+
bool visitSelectorExpression(SelectorExpression node) => false;
88+
89+
bool visitStringExpression(StringExpression node) {
90+
if (node.hasQuotes) return false;
91+
92+
// Exclude non-identifier constructs that are parsed as [StringExpression]s.
93+
// We could just check if they parse as valid identifiers, but this is
94+
// cheaper.
95+
var text = node.text.initialPlain;
96+
return
97+
// !important
98+
!text.startsWith("!") &&
99+
// ID-style identifiers
100+
!text.startsWith("#") &&
101+
// Unicode ranges
102+
text.codeUnitAtOrNull(1) != $plus &&
103+
// url()
104+
text.codeUnitAtOrNull(3) != $lparen;
105+
}
106+
107+
bool visitSupportsExpression(SupportsExpression node) => false;
108+
109+
bool visitUnaryOperationExpression(UnaryOperationExpression node) => false;
110+
111+
bool visitValueExpression(ValueExpression node) => false;
112+
113+
bool visitVariableExpression(VariableExpression node) => true;
114+
}

‎lib/src/ast/sass/expression/binary_operation.dart

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:charcode/charcode.dart';
66
import 'package:meta/meta.dart';
77
import 'package:source_span/source_span.dart';
88

9+
import '../../../util/span.dart';
910
import '../../../visitor/interface/expression.dart';
1011
import '../expression.dart';
1112
import 'list.dart';
@@ -45,6 +46,17 @@ final class BinaryOperationExpression implements Expression {
4546
return left.span.expand(right.span);
4647
}
4748

49+
/// Returns the span that covers only [operator].
50+
///
51+
/// @nodoc
52+
@internal
53+
FileSpan get operatorSpan => left.span.file == right.span.file &&
54+
left.span.end.offset < right.span.start.offset
55+
? left.span.file
56+
.span(left.span.end.offset, right.span.start.offset)
57+
.trim()
58+
: span;
59+
4860
BinaryOperationExpression(this.operator, this.left, this.right)
4961
: allowsSlash = false;
5062

‎lib/src/ast/sass/expression/calculation.dart

-108
This file was deleted.

‎lib/src/ast/sass/interpolation.dart

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ final class Interpolation implements SassNode {
2121

2222
final FileSpan span;
2323

24+
/// Returns whether this contains no interpolated expressions.
25+
bool get isPlain => asPlain != null;
26+
2427
/// If this contains no interpolated expressions, returns its text contents.
2528
///
2629
/// Otherwise, returns `null`.

‎lib/src/deprecation.dart

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ enum Deprecation {
6969
deprecatedIn: '1.62.3',
7070
description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'),
7171

72+
calcInterp('calc-interp',
73+
deprecatedIn: '1.67.0',
74+
description: 'Using interpolation in a calculation outside a value '
75+
'position.'),
76+
7277
/// Deprecation for `@import` rules.
7378
import.future('import', description: '@import rules.'),
7479

‎lib/src/embedded/protofier.dart

+1-3
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,6 @@ final class Protofier {
134134
..operator = _protofyCalculationOperator(value.operator)
135135
..left = _protofyCalculationValue(value.left)
136136
..right = _protofyCalculationValue(value.right);
137-
case CalculationInterpolation():
138-
result.interpolation = value.value;
139137
case _:
140138
throw "Unknown calculation value $value";
141139
}
@@ -352,7 +350,7 @@ final class Protofier {
352350
_deprotofyCalculationValue(value.operation.left),
353351
_deprotofyCalculationValue(value.operation.right)),
354352
Value_Calculation_CalculationValue_Value.interpolation =>
355-
CalculationInterpolation(value.interpolation),
353+
SassString('(${value.interpolation})', quotes: false),
356354
Value_Calculation_CalculationValue_Value.notSet =>
357355
throw mandatoryError("Value.Calculation.value")
358356
};

0 commit comments

Comments
 (0)
Please sign in to comment.