Skip to content

Commit 408e95f

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

File tree

7 files changed

+483
-0
lines changed

7 files changed

+483
-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 theme($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: 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 '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+
}

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 null if none of the given values are error strings, or is returns
16+
/// an error string if a 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|null} The error string if any value is an error, or null
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+
}

sass/ext/_type.scss

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// Copyright 2025 Google LLC
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
// Utilities for Sass type checking.
7+
8+
// go/keep-sorted start by_regex='(.+) prefix_order=sass:
9+
@use 'sass:meta';
10+
@use 'sass:string';
11+
@use 'throw';
12+
// go/keep-sorted end
13+
14+
/// Returns true if the given value matches the provided type string.
15+
///
16+
/// The type string supports multiple types separated by `|`, such as
17+
/// `'string|null'`. Type options are any values returned by `meta.type-of()`.
18+
///
19+
/// @example scss
20+
/// @function is-empty($value) {
21+
/// @if type.matches($value, 'list|map') {
22+
/// @return list.length($value) == 0;
23+
/// }
24+
/// @if type.matches($value, 'string') {
25+
/// @return $value == '';
26+
/// }
27+
/// @return type.matches($value, 'null');
28+
/// }
29+
///
30+
/// @param {*} $value - The value to check the type of.
31+
/// @param {string} $type-string - The type to check. May be multiple types
32+
/// separated by `|`.
33+
/// @return {boolean} True if the value matches the type string.
34+
@function matches($value, $type-string) {
35+
@if meta.type-of($type-string) != 'string' or $type-string == '' {
36+
@return throw.error(
37+
'$type-string must be a non-empty string',
38+
$source: 'type.matches'
39+
);
40+
}
41+
@if string.index($type-string, ' ') {
42+
@return throw.error(
43+
'$type-string may not contain spaces',
44+
$source: 'type.matches'
45+
);
46+
}
47+
@if string.index($type-string, 'boolean') {
48+
@return throw.error(
49+
'Use "bool" instead of "boolean"',
50+
$source: 'type.matches'
51+
);
52+
}
53+
54+
$value-type: meta.type-of($value);
55+
@if $value-type == $type-string {
56+
@return true;
57+
}
58+
59+
@each $type in string.split($type-string, '|') {
60+
@if $value-type == $type {
61+
@return true;
62+
}
63+
}
64+
65+
@return false;
66+
}

0 commit comments

Comments
 (0)