Skip to content

Commit be01246

Browse files
asynclizcopybara-github
authored andcommitted
chore: add errors and type assertions to sass-ext
PiperOrigin-RevId: 826549583
1 parent 0a1f511 commit be01246

File tree

7 files changed

+537
-0
lines changed

7 files changed

+537
-0
lines changed

sass/ext/_assert.scss

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// Copyright 2025 Google LLC
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
// Utility assert functions that throw errors.
7+
8+
// go/keep-sorted start by_regex='(.+) prefix_order=sass:
9+
@use 'sass:meta';
10+
@use 'throw';
11+
@use 'type';
12+
// go/keep-sorted end
13+
14+
/// Asserts that the argument is a specific type. If it is, the argument is
15+
/// returned, otherwise an error is thrown.
16+
///
17+
/// @example scss
18+
/// @mixin multiply($a, $b) {
19+
/// $a: assert.is-type($a, 'number');
20+
/// $b: assert.is-type($b, 'number');
21+
/// @return $a * $b;
22+
/// }
23+
///
24+
/// @function is-empty($value) {
25+
/// $value: assert.is-type(
26+
/// $value,
27+
/// 'list|map|null',
28+
/// $message: '$value must be a list, map, or null',
29+
/// $source: 'is-empty'
30+
/// );
31+
/// @return $value and list.length($value) == 0;
32+
/// }
33+
///
34+
/// @param {*} $arg - The argument to check.
35+
/// @param {string} $type - The string type to assert the argument matches.
36+
/// Multiple types may be separated by '|'.
37+
/// @param {string} $message - Optional custom error message.
38+
/// @param {string} $source - Optional source of the error message.
39+
/// @return {*} The argument if it matches the type string.
40+
/// @throw Error if the argument does not match the type string.
41+
@function is-type(
42+
$arg,
43+
$type,
44+
$message: 'Argument must be type #{meta.inspect($type)}. $arg: #{meta.inspect($arg)}',
45+
$source: 'assert.is-type'
46+
) {
47+
@if type.matches($arg, $type) {
48+
@return $arg;
49+
}
50+
@return throw.error($message, $source);
51+
}
52+
53+
/// Asserts that the argument is a specific type. If it is, the argument is
54+
/// returned, otherwise an error is thrown.
55+
///
56+
/// @example scss
57+
/// @function get-or-throw($map, $key) {
58+
/// @return assert.not-type(
59+
/// map.get($map, $key),
60+
/// 'null',
61+
/// $message: 'Key must be in the map'
62+
/// );
63+
/// }
64+
///
65+
/// @param {*} $arg - The argument to check.
66+
/// @param {string} $type - The string type to assert the argument does not
67+
/// match. Multiple types may be separated by '|'.
68+
/// @param {string} $message - Optional custom error message.
69+
/// @param {string} $source - Optional source of the error message.
70+
/// @return {*} The argument if it does not match the type string.
71+
/// @throw Error if the argument matches the type string.
72+
@function not-type(
73+
$arg,
74+
$type,
75+
$message: 'Argument may not be type #{meta.inspect($type)}. $arg: #{meta.inspect($arg)}',
76+
$source: 'assert.not-type'
77+
) {
78+
@if type.matches($arg, $type) {
79+
@return throw.error($message, $source);
80+
}
81+
@return $arg;
82+
}

sass/ext/_assert_test.scss

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//
2+
// Copyright 2025 Google LLC
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
@use 'true' as test;
7+
8+
// go/keep-sorted start by_regex='(.+) prefix_order=sass:
9+
@use 'sass:string';
10+
@use 'assert';
11+
@use 'throw';
12+
// go/keep-sorted end
13+
14+
@include test.describe('assert') {
15+
// Value types
16+
$number: 1;
17+
$string: 'a-string';
18+
$color: red;
19+
$bool: true;
20+
$null: null;
21+
$list: ('list', 'of', 'values');
22+
$map: (
23+
'map': 'value',
24+
);
25+
26+
@include test.describe('is-type()') {
27+
@include test.it('returns the argument when it matches a single type') {
28+
@include test.assert-equal(assert.is-type($number, 'number'), $number);
29+
@include test.assert-equal(assert.is-type($string, 'string'), $string);
30+
@include test.assert-equal(assert.is-type($bool, 'bool'), $bool);
31+
@include test.assert-equal(assert.is-type($null, 'null'), $null);
32+
@include test.assert-equal(assert.is-type($list, 'list'), $list);
33+
@include test.assert-equal(assert.is-type($map, 'map'), $map);
34+
}
35+
36+
@include test.it(
37+
'returns the argument when it matches one of multiple types'
38+
) {
39+
@include test.assert-equal(
40+
assert.is-type($number, 'number|string'),
41+
$number
42+
);
43+
@include test.assert-equal(
44+
assert.is-type($string, 'number|string'),
45+
$string
46+
);
47+
@include test.assert-equal(assert.is-type($null, 'list|map|null'), $null);
48+
@include test.assert-equal(assert.is-type($list, 'list|map|null'), $list);
49+
@include test.assert-equal(assert.is-type($map, 'list|map|null'), $map);
50+
}
51+
52+
@include test.it('throws an error when it does not match the type') {
53+
@include test.assert-true(
54+
throw.get-error(assert.is-type($number, 'string')),
55+
'number should not match "string" type'
56+
);
57+
@include test.assert-true(
58+
throw.get-error(assert.is-type($string, 'number')),
59+
'string should not match "number" type'
60+
);
61+
@include test.assert-true(
62+
throw.get-error(assert.is-type($null, 'list|map')),
63+
'null should not match "list|map" type'
64+
);
65+
}
66+
}
67+
68+
@include test.describe('not-type()') {
69+
@include test.it(
70+
'returns the argument when it does not match a single type'
71+
) {
72+
@include test.assert-equal(assert.not-type($number, 'string'), $number);
73+
@include test.assert-equal(assert.not-type($string, 'number'), $string);
74+
@include test.assert-equal(assert.not-type($bool, 'string'), $bool);
75+
@include test.assert-equal(assert.not-type($null, 'string'), $null);
76+
@include test.assert-equal(assert.not-type($list, 'string'), $list);
77+
@include test.assert-equal(assert.not-type($map, 'string'), $map);
78+
}
79+
80+
@include test.it(
81+
'returns the argument when it does not match one of multiple types'
82+
) {
83+
@include test.assert-equal(
84+
assert.not-type($number, 'string|map'),
85+
$number
86+
);
87+
@include test.assert-equal(
88+
assert.not-type($string, 'number|map'),
89+
$string
90+
);
91+
@include test.assert-equal(assert.not-type($null, 'list|map'), $null);
92+
}
93+
94+
@include test.it('throws an error when it matches the type') {
95+
@include test.assert-true(
96+
throw.get-error(assert.not-type($number, 'number')),
97+
'number should match "number" type and throw'
98+
);
99+
@include test.assert-true(
100+
throw.get-error(assert.not-type($string, 'string')),
101+
'string should match "string" type and throw'
102+
);
103+
@include test.assert-true(
104+
throw.get-error(assert.not-type($null, 'null')),
105+
'null should match "null" type and throw'
106+
);
107+
@include test.assert-true(
108+
throw.get-error(assert.not-type($number, 'number|string')),
109+
'number should match "number|string" type and throw'
110+
);
111+
@include test.assert-true(
112+
throw.get-error(assert.not-type($string, 'number|string')),
113+
'string should match "number|string" type and throw'
114+
);
115+
@include test.assert-true(
116+
throw.get-error(assert.not-type($null, 'list|map|null')),
117+
'null should match "list|map|null" type and throw'
118+
);
119+
}
120+
}
121+
}

sass/ext/_throw.scss

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// Copyright 2025 Google LLC
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
// Utilities for `sass-true` errors, to support testing error behavior.
7+
8+
// go/keep-sorted start by_regex='(.+) prefix_order=sass:
9+
@use 'sass:meta';
10+
@use 'sass:string';
11+
// go/keep-sorted end
12+
13+
@forward 'true' show error;
14+
15+
/// Returns false if none of the given values are error strings, or returns an
16+
/// error string if any value has an error.
17+
///
18+
/// This is used to support testing error behavior with `sass-true`, since
19+
/// `@error` messages cannot be caught at build time.
20+
///
21+
/// @example scss
22+
/// // A function that may return an "ERROR:" string in a test.
23+
/// @function get-value($map, $key) {
24+
/// @if meta.type-of($map) != 'map' {
25+
/// // Identical to `@error 'ERROR: Arg is not a map'` outside of tests.
26+
/// @return throw.error('Arg is not a map');
27+
/// }
28+
/// @return map.get($map, $key);
29+
/// }
30+
///
31+
/// // A function that needs to handle potential errors from other functions.
32+
/// @function mix-primary-on-surface($values) {
33+
/// $primary: get-value($values, 'primary');
34+
/// $surface: get-value($values, 'surface');
35+
/// $error: throw.get-error($primary, $secondary);
36+
/// @if $error {
37+
/// // Return early to guard logic against additional errors since
38+
/// // $primary or $secondary may be a string instead of a color.
39+
/// @return $error;
40+
/// }
41+
///
42+
/// @return color.mix($primary, $surface, 10%);
43+
/// }
44+
///
45+
/// Note: `throw.error()` and `throw.get-error()` are only useful when testing
46+
/// error behavior using `sass-true`. If you are not testing a function, use
47+
/// `@error` instead.
48+
///
49+
/// @example scss
50+
/// // In a `sass-true` test, `throw.get-error()` can be used to assert that
51+
/// // an error is thrown.
52+
/// @use 'true' as test with ($catch-errors: true);
53+
///
54+
/// @include test.describe('module.get-value()') {
55+
/// @include test.it('throws an error if the value is not a map') {
56+
/// $result: module.get-value('not a map', 'primary');
57+
/// @include test.assert-truthy(throw.get-error($result), '$result is an error');
58+
/// }
59+
/// }
60+
///
61+
/// @param {*} $error - The value to check.
62+
/// @param {list} $errors - Additional values to check. Useful for checking
63+
/// multiple errors at the same time.
64+
/// @return {string|boolean} The error string if any value is an error, or false
65+
/// otherwise.
66+
@function get-error($error, $errors...) {
67+
@if _is-error($error) {
68+
@return $error;
69+
}
70+
71+
@each $additional-error in $errors {
72+
@if _is-error($additional-error) {
73+
@return $additional-error;
74+
}
75+
}
76+
77+
@return false;
78+
}
79+
80+
@function _is-error($error) {
81+
@return (meta.type-of($error) == 'string') and
82+
(string.index($error, 'ERROR') == 1);
83+
}

sass/ext/_throw_test.scss

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// Copyright 2025 Google LLC
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
@use 'true' as test;
7+
8+
// go/keep-sorted start by_regex='(.+) prefix_order=sass:
9+
@use 'throw';
10+
// go/keep-sorted end
11+
12+
@include test.describe('throw') {
13+
@include test.describe('get-error()') {
14+
@include test.it('returns the string if the value is an error string') {
15+
$error: throw.error('test error message');
16+
@include test.assert-equal(throw.get-error($error), $error);
17+
}
18+
19+
@include test.it('returns null for non-error strings') {
20+
@include test.assert-false(
21+
throw.get-error('not an error'),
22+
'get-error("not an error") should return null for non-error strings'
23+
);
24+
}
25+
26+
@include test.it('returns null for other values') {
27+
@include test.assert-false(
28+
throw.get-error(1),
29+
'get-error(1) should return null'
30+
);
31+
@include test.assert-false(
32+
throw.get-error(true),
33+
'get-error(true) should return null'
34+
);
35+
@include test.assert-false(
36+
throw.get-error(null),
37+
'get-error(null) should return null'
38+
);
39+
@include test.assert-false(
40+
throw.get-error(()),
41+
'get-error(()) should return null'
42+
);
43+
}
44+
45+
@include test.it(
46+
'returns the first error if multiple values are provided'
47+
) {
48+
$error: throw.error('test error message');
49+
@include test.assert-equal(
50+
throw.get-error(
51+
'not an error',
52+
'still not an error',
53+
$error,
54+
'not an error either'
55+
),
56+
$error
57+
);
58+
}
59+
60+
@include test.it('returns null if multiple non-error values are provided') {
61+
@include test.assert-false(
62+
throw.get-error('not an error', 'still not an error'),
63+
'get-error("not an error", "still not an error") should return null'
64+
);
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)