LibWeb: Parse @function rules

This commit is contained in:
Callum Law 2026-03-07 02:07:49 +13:00 committed by Sam Atkins
parent 01394322ac
commit c7b402eff5
9 changed files with 338 additions and 93 deletions

View file

@ -259,6 +259,17 @@
},
"function": {
"spec": "https://drafts.csswg.org/css-mixins-1/#function-rule",
"descriptors": {}
"descriptors": {
"result": {
"syntax": [
"<declaration-value>?"
]
}
},
"custom-descriptors": {
"syntax": [
"<declaration-value>?"
]
}
}
}

View file

@ -16,7 +16,7 @@
#include <LibGfx/ImmutableBitmap.h>
#include <LibURL/Parser.h>
#include <LibWeb/CSS/CSSFontFeatureValuesRule.h>
#include <LibWeb/CSS/CSSFunctionDescriptors.h>
#include <LibWeb/CSS/CSSFunctionDeclarations.h>
#include <LibWeb/CSS/CSSMarginRule.h>
#include <LibWeb/CSS/CSSStyleDeclaration.h>
#include <LibWeb/CSS/CSSStyleProperties.h>
@ -1474,7 +1474,7 @@ Vector<Descriptor> Parser::parse_as_descriptor_declaration_block(AtRuleID at_rul
case AtRuleID::FontFace:
return RuleContext::AtFontFace;
case AtRuleID::Function:
TODO();
return RuleContext::AtFunction;
case AtRuleID::Page:
return RuleContext::AtPage;
case AtRuleID::Property:
@ -1536,13 +1536,17 @@ bool Parser::is_valid_in_the_current_context(Declaration const&) const
case RuleContext::AtLayer:
case RuleContext::AtMedia:
case RuleContext::AtSupports:
// Grouping rules can contain declarations if they are themselves inside a style rule
return m_rule_context.contains_slow(RuleContext::Style);
// Grouping rules can contain declarations if they are themselves inside a style or function rule
return m_rule_context.contains([](auto const& context) { return context == RuleContext::Style || context == RuleContext::AtFunction; });
case RuleContext::FontFeatureValue:
// Each feature value block accepts a list of declarations
return true;
case RuleContext::AtFunction:
// @function rules contain descriptor declarations
return true;
case RuleContext::AtCounterStyle:
case RuleContext::AtFontFace:
case RuleContext::AtFontFeatureValues:
@ -1574,6 +1578,12 @@ bool Parser::is_valid_in_the_current_context(AtRule const& at_rule) const
if (m_rule_context.contains_slow(RuleContext::Style))
return first_is_one_of(at_rule.name, "layer", "media", "supports");
if (m_rule_context.contains_slow(RuleContext::AtFunction)) {
// https://drafts.csswg.org/css-mixins-1/#function-body
// The body of a @function rule accepts conditional group rules
return first_is_one_of(at_rule.name, "media", "supports");
}
switch (m_rule_context.last()) {
case RuleContext::Unknown:
// If the context is an unknown type, we don't accept anything.
@ -1608,6 +1618,9 @@ bool Parser::is_valid_in_the_current_context(AtRule const& at_rule) const
return false;
case RuleContext::AtFontFeatureValues:
return CSSFontFeatureValuesRule::is_font_feature_value_type_at_keyword(at_rule.name);
case RuleContext::AtFunction:
// Already handled above
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
@ -1648,6 +1661,7 @@ bool Parser::is_valid_in_the_current_context(QualifiedRule const&) const
case RuleContext::AtFontFace:
case RuleContext::AtFontFeatureValues:
case RuleContext::FontFeatureValue:
case RuleContext::AtFunction:
case RuleContext::AtPage:
case RuleContext::AtProperty:
case RuleContext::Keyframe:

View file

@ -292,6 +292,13 @@ private:
Optional<FlyString> parse_layer_name(TokenStream<ComponentValue>&, AllowBlankLayerName);
Optional<Vector<FlyString>> parse_comma_separated_family_name_list(TokenStream<ComponentValue>&);
struct FunctionPrelude {
FlyString name;
Vector<FunctionParameterInternal> parameters;
NonnullOwnPtr<SyntaxNode> return_type;
};
Optional<FunctionPrelude> parse_function_prelude(TokenStream<ComponentValue>&);
bool is_valid_in_the_current_context(Declaration const&) const;
bool is_valid_in_the_current_context(AtRule const&) const;
bool is_valid_in_the_current_context(QualifiedRule const&) const;
@ -302,6 +309,7 @@ private:
GC::Ptr<CSSCounterStyleRule> convert_to_counter_style_rule(AtRule const&);
GC::Ptr<CSSFontFaceRule> convert_to_font_face_rule(AtRule const&);
GC::Ptr<CSSFontFeatureValuesRule> convert_to_font_feature_values_rule(AtRule const&);
GC::Ptr<CSSFunctionRule> convert_to_function_rule(AtRule const&);
GC::Ptr<CSSKeyframesRule> convert_to_keyframes_rule(AtRule const&);
GC::Ptr<CSSImportRule> convert_to_import_rule(AtRule const&);

View file

@ -24,9 +24,7 @@ RuleContext rule_context_type_for_rule(CSSRule::Type rule_type)
case CSSRule::Type::FontFeatureValues:
return RuleContext::AtFontFeatureValues;
case CSSRule::Type::Function:
TODO();
case CSSRule::Type::FunctionDeclarations:
TODO();
return RuleContext::AtFunction;
case CSSRule::Type::Keyframes:
return RuleContext::AtKeyframes;
case CSSRule::Type::Keyframe:
@ -47,6 +45,7 @@ RuleContext rule_context_type_for_rule(CSSRule::Type rule_type)
case CSSRule::Type::Import:
case CSSRule::Type::LayerStatement:
case CSSRule::Type::Namespace:
case CSSRule::Type::FunctionDeclarations:
break;
}
VERIFY_NOT_REACHED();
@ -64,6 +63,8 @@ RuleContext rule_context_type_for_at_rule(FlyString const& name)
return RuleContext::AtKeyframes;
if (name.equals_ignoring_ascii_case("font-feature-values"sv))
return RuleContext::AtFontFeatureValues;
if (name.equals_ignoring_ascii_case("function"sv))
return RuleContext::AtFunction;
if (CSSFontFeatureValuesRule::is_font_feature_value_type_at_keyword(name))
return RuleContext::FontFeatureValue;
if (name.equals_ignoring_ascii_case("supports"sv))

View file

@ -19,6 +19,7 @@ enum class RuleContext : u8 {
AtFontFace,
AtFontFeatureValues,
FontFeatureValue,
AtFunction,
AtKeyframes,
Keyframe,
AtSupports,

View file

@ -12,10 +12,12 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/HeapVector.h>
#include <LibWeb/CSS/CSSCounterStyleRule.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSFontFeatureValuesRule.h>
#include <LibWeb/CSS/CSSFunctionDeclarations.h>
#include <LibWeb/CSS/CSSFunctionRule.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSKeyframeRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
@ -44,6 +46,7 @@
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
namespace Web::CSS::Parser {
@ -112,6 +115,9 @@ GC::Ptr<CSSRule> Parser::convert_to_rule(Rule const& rule, Nested nested)
if (at_rule.name.equals_ignoring_ascii_case("font-feature-values"sv))
return convert_to_font_feature_values_rule(at_rule);
if (at_rule.name.equals_ignoring_ascii_case("function"sv))
return convert_to_function_rule(at_rule);
if (at_rule.name.equals_ignoring_ascii_case("import"sv))
return convert_to_import_rule(at_rule);
@ -1123,6 +1129,210 @@ GC::Ptr<CSSFontFeatureValuesRule> Parser::convert_to_font_feature_values_rule(At
return font_feature_values_rule;
}
static OwnPtr<SyntaxNode> parse_css_type(TokenStream<ComponentValue>& tokens)
{
// https://drafts.csswg.org/css-mixins-1/#function-rule
// <css-type> = <syntax-component> | <type()>
// <type()> = type( <syntax> )
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// <syntax-component>
if (auto maybe_syntax_component = parse_syntax_component(tokens)) {
transaction.commit();
return maybe_syntax_component;
}
// <type()>
auto maybe_type_function_token = tokens.consume_a_token();
if (!maybe_type_function_token.is_function("type"sv))
return nullptr;
if (auto maybe_type_function_syntax = parse_as_syntax(maybe_type_function_token.function().value)) {
transaction.commit();
return maybe_type_function_syntax;
}
return nullptr;
}
Optional<Parser::FunctionPrelude> Parser::parse_function_prelude(TokenStream<ComponentValue>& tokens)
{
// https://drafts.csswg.org/css-mixins-1/#function-rule
// <function-token> <function-parameter>#? ) [ returns <css-type> ]?
// <function-parameter> = <custom-property-name> <css-type>? [ : <default-value> ]?
// <default-value> = <declaration-value>
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& function_token = tokens.consume_a_token();
if (!function_token.is_function()) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = tokens.dump_string(),
.description = "Prelude must start with a function token."_string,
});
return {};
}
auto function_name = function_token.function().name;
// The <function-token> production must start with two dashes (U+002D HYPHEN-MINUS), similar to <dashed-ident>, or
// else the definition is invalid.
if (!function_name.starts_with_bytes("--"sv)) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = tokens.dump_string(),
.description = "Function name must start with two dashes."_string,
});
return {};
}
Vector<FunctionParameterInternal> parsed_parameters;
TokenStream parameters_tokens { function_token.function().value };
parameters_tokens.discard_whitespace();
auto parameters_component_values = parse_a_comma_separated_list_of_component_values(parameters_tokens);
// <function-parameter>#?
for (auto const& parameter_component_values : parameters_component_values) {
TokenStream parameter_tokens { parameter_component_values };
parameter_tokens.discard_whitespace();
// <custom-property-name>
auto maybe_name = parse_dashed_ident(parameter_tokens);
if (!maybe_name.has_value() || !is_a_custom_property_name_string(maybe_name.value())) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = parameter_tokens.dump_string(),
.description = "Parameter must have a name."_string,
});
return {};
}
// <css-type>?
NonnullOwnPtr<SyntaxNode> type = UniversalSyntaxNode::create();
if (auto maybe_type = parse_css_type(parameter_tokens))
type = maybe_type.release_nonnull();
parameter_tokens.discard_whitespace();
// [ : <default-value> ]?
Optional<Vector<ComponentValue>> default_value;
if (parameter_tokens.peek_token().is(Token::Type::Colon)) {
parameter_tokens.discard_a_token(); // :
parameter_tokens.discard_whitespace();
auto maybe_default_value = parse_declaration_value(parameter_tokens);
if (!maybe_default_value.has_value()) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = parameter_tokens.dump_string(),
.description = "Expected default value after ':' in parameter"_string,
});
return {};
}
// If a default value and a parameter type are both provided, then the default value must parse successfully
// according to that parameter types syntax. Otherwise, the @function rule is invalid.
TokenStream default_value_token_stream { maybe_default_value.value() };
if (!parse_according_to_syntax_node(default_value_token_stream, *type) || !default_value_token_stream.is_empty())
return {};
default_value = maybe_default_value;
}
parameter_tokens.discard_whitespace();
if (!parameter_tokens.is_empty()) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = parameter_tokens.dump_string(),
.description = "Trailing tokens after parameter"_string,
});
return {};
}
parsed_parameters.append({ maybe_name.release_value(), move(type), move(default_value) });
}
tokens.discard_whitespace();
NonnullOwnPtr<SyntaxNode> return_type = UniversalSyntaxNode::create();
if (tokens.peek_token().is_ident("returns"sv)) {
tokens.discard_a_token();
tokens.discard_whitespace();
auto maybe_return_type = parse_css_type(tokens);
if (!maybe_return_type) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = tokens.dump_string(),
.description = "Expected return type after 'returns' in prelude."_string,
});
return {};
}
return_type = maybe_return_type.release_nonnull();
}
tokens.discard_whitespace();
if (tokens.has_next_token()) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = tokens.dump_string(),
.description = "Trailing tokens in prelude."_string,
});
return {};
}
transaction.commit();
return FunctionPrelude { move(function_name), move(parsed_parameters), move(return_type) };
}
GC::Ptr<CSSFunctionRule> Parser::convert_to_function_rule(AtRule const& function_rule)
{
// https://drafts.csswg.org/css-mixins-1/#function-rule
TokenStream prelude_stream { function_rule.prelude };
if (!function_rule.is_block_rule) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@function"_fly_string,
.prelude = prelude_stream.dump_string(),
.description = "Must be a block, not a statement."_string,
});
return nullptr;
}
auto prelude = parse_function_prelude(prelude_stream);
if (!prelude.has_value())
return nullptr;
Vector<GC::Ref<CSSRule>> child_rules {};
// https://drafts.csswg.org/css-mixins-1/#function-body
for (auto const& child : function_rule.child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) {
if (auto child_rule = convert_to_rule<CSSFunctionDeclarations>(rule, Nested::Yes))
child_rules.append(*child_rule);
},
[&](Vector<Declaration> const& declarations) {
child_rules.append(CSSFunctionDeclarations::create(realm(), *this, declarations));
});
}
return CSSFunctionRule::create(realm(), CSSRuleList::create(realm(), child_rules), move(prelude->name), move(prelude->parameters), move(prelude->return_type));
}
GC::Ptr<CSSPageRule> Parser::convert_to_page_rule(AtRule const& page_rule)
{
// https://drafts.csswg.org/css-page-3/#syntax-page-selector

View file

@ -483,6 +483,7 @@ struct BackgroundLayerData;
struct CalculationContext;
struct CalculationResolutionContext;
struct CSSStyleSheetInit;
struct FunctionParameterInternal;
struct GridRepeatParams;
struct LogicalAliasMappingContext;
struct RandomCachingKey;

View file

@ -2,38 +2,38 @@ Harness status: OK
Found 34 tests
34 Fail
Fail Empty CSSFunctionRule
Fail Single CSSFunctionDeclarations
Fail CSSFunctionDescriptors (result)
Fail CSSFunctionDescriptors (result, repeated)
Fail CSSFunctionDescriptors (local variables)
Fail CSSFunctionDescriptors (local variables, repeated)
Fail CSSFunctionDescriptors (local variables and result)
Fail CSSFunctionDescriptors serialization
Fail Unknown descriptors
Fail Unknown descriptors (mutation)
Fail item()
Fail Indexed property getter
Fail @supports in body
Fail CSSFunctionRule.name
Fail CSSFunctionRule.getParameters()
Fail CSSFunctionRule.returnType
Fail CSSFunctionRule escapes
Fail CSSFunctionRule.cssText (--empty)
Fail CSSFunctionRule.cssText (--ret-length)
Fail CSSFunctionRule.cssText (--ret-length-auto)
Fail CSSFunctionRule.cssText (--param-single)
Fail CSSFunctionRule.cssText (--param-typed)
Fail CSSFunctionRule.cssText (--param-typed-default)
Fail CSSFunctionRule.cssText (--param-default)
Fail CSSFunctionRule.cssText (--param-multi)
Fail CSSFunctionRule.cssText (--param-multi-mixed)
Fail CSSFunctionRule.cssText (--body-result)
Fail CSSFunctionRule.cssText (--body-locals)
Fail CSSFunctionRule.cssText (--param-type-fn)
Fail CSSFunctionRule.cssText (--param-type-fn-uni)
Fail CSSFunctionRule.cssText (--ret-type-fn)
Fail CSSFunctionRule.cssText (--ret-type-fn-uni)
Fail CSSFunctionRule.cssText (--body-result-multi)
Fail CSSFunctionRule.cssText (--escaped-)
34 Pass
Pass Empty CSSFunctionRule
Pass Single CSSFunctionDeclarations
Pass CSSFunctionDescriptors (result)
Pass CSSFunctionDescriptors (result, repeated)
Pass CSSFunctionDescriptors (local variables)
Pass CSSFunctionDescriptors (local variables, repeated)
Pass CSSFunctionDescriptors (local variables and result)
Pass CSSFunctionDescriptors serialization
Pass Unknown descriptors
Pass Unknown descriptors (mutation)
Pass item()
Pass Indexed property getter
Pass @supports in body
Pass CSSFunctionRule.name
Pass CSSFunctionRule.getParameters()
Pass CSSFunctionRule.returnType
Pass CSSFunctionRule escapes
Pass CSSFunctionRule.cssText (--empty)
Pass CSSFunctionRule.cssText (--ret-length)
Pass CSSFunctionRule.cssText (--ret-length-auto)
Pass CSSFunctionRule.cssText (--param-single)
Pass CSSFunctionRule.cssText (--param-typed)
Pass CSSFunctionRule.cssText (--param-typed-default)
Pass CSSFunctionRule.cssText (--param-default)
Pass CSSFunctionRule.cssText (--param-multi)
Pass CSSFunctionRule.cssText (--param-multi-mixed)
Pass CSSFunctionRule.cssText (--body-result)
Pass CSSFunctionRule.cssText (--body-locals)
Pass CSSFunctionRule.cssText (--param-type-fn)
Pass CSSFunctionRule.cssText (--param-type-fn-uni)
Pass CSSFunctionRule.cssText (--ret-type-fn)
Pass CSSFunctionRule.cssText (--ret-type-fn-uni)
Pass CSSFunctionRule.cssText (--body-result-multi)
Pass CSSFunctionRule.cssText (--escaped-)

View file

@ -2,58 +2,57 @@ Harness status: OK
Found 86 tests
38 Pass
48 Fail
Fail @function --foo() is valid
Fail @function --foo( ) is valid
Fail @function --foo(--x) is valid
Fail @function --foo( --x ) is valid
86 Pass
Pass @function --foo() is valid
Pass @function --foo( ) is valid
Pass @function --foo(--x) is valid
Pass @function --foo( --x ) is valid
Pass @function --foo () is invalid
Pass @function --foo (--x) is invalid
Fail @function --foo(--x auto) is valid
Fail @function --foo(--x <angle>) is valid
Fail @function --foo(--x <color>) is valid
Fail @function --foo(--x <custom-ident>) is valid
Fail @function --foo(--x <image>) is valid
Fail @function --foo(--x <integer>) is valid
Fail @function --foo(--x <length>) is valid
Fail @function --foo(--x <length-percentage>) is valid
Fail @function --foo(--x <number>) is valid
Fail @function --foo(--x <percentage>) is valid
Fail @function --foo(--x <resolution>) is valid
Fail @function --foo(--x <string>) is valid
Fail @function --foo(--x <time>) is valid
Fail @function --foo(--x <url>) is valid
Fail @function --foo(--x <transform-function>) is valid
Fail @function --foo(--x <transform-list>) is valid
Fail @function --foo(--x type(auto)) is valid
Fail @function --foo(--x type(<length>)) is valid
Fail @function --foo(--x type(<length> | auto)) is valid
Fail @function --foo(--x type(none | auto)) is valid
Fail @function --foo(--x type(*)) is valid
Fail @function --foo(--x, --y) is valid
Fail @function --foo(--x, --y, --z) is valid
Fail @function --foo(--x <length>, --y, --z) is valid
Fail @function --foo(--x, --y <number>, --z <angle>) is valid
Fail @function --foo(--x : 10px) is valid
Fail @function --foo(--x type(*): 10px) is valid
Fail @function --foo(--x <length>: 10px) is valid
Fail @function --foo(--x <length>: 10px, --y) is valid
Fail @function --foo(--x, --y <length>: 10px) is valid
Fail @function --foo(--x type(<length> | auto): auto) is valid
Fail @function --foo(--x type(<length> | auto) : auto) is valid
Fail @function --foo(--x:1px, --y, --z:2px) is valid
Pass @function --foo(--x auto) is valid
Pass @function --foo(--x <angle>) is valid
Pass @function --foo(--x <color>) is valid
Pass @function --foo(--x <custom-ident>) is valid
Pass @function --foo(--x <image>) is valid
Pass @function --foo(--x <integer>) is valid
Pass @function --foo(--x <length>) is valid
Pass @function --foo(--x <length-percentage>) is valid
Pass @function --foo(--x <number>) is valid
Pass @function --foo(--x <percentage>) is valid
Pass @function --foo(--x <resolution>) is valid
Pass @function --foo(--x <string>) is valid
Pass @function --foo(--x <time>) is valid
Pass @function --foo(--x <url>) is valid
Pass @function --foo(--x <transform-function>) is valid
Pass @function --foo(--x <transform-list>) is valid
Pass @function --foo(--x type(auto)) is valid
Pass @function --foo(--x type(<length>)) is valid
Pass @function --foo(--x type(<length> | auto)) is valid
Pass @function --foo(--x type(none | auto)) is valid
Pass @function --foo(--x type(*)) is valid
Pass @function --foo(--x, --y) is valid
Pass @function --foo(--x, --y, --z) is valid
Pass @function --foo(--x <length>, --y, --z) is valid
Pass @function --foo(--x, --y <number>, --z <angle>) is valid
Pass @function --foo(--x : 10px) is valid
Pass @function --foo(--x type(*): 10px) is valid
Pass @function --foo(--x <length>: 10px) is valid
Pass @function --foo(--x <length>: 10px, --y) is valid
Pass @function --foo(--x, --y <length>: 10px) is valid
Pass @function --foo(--x type(<length> | auto): auto) is valid
Pass @function --foo(--x type(<length> | auto) : auto) is valid
Pass @function --foo(--x:1px, --y, --z:2px) is valid
Pass @function --foo(--x: 10px !important) is invalid
Pass @function --foo(--x <length>: 10deg) is invalid
Pass @function --foo(--x <angle>: 10px) is invalid
Pass @function --foo(--x <color>: 10px) is invalid
Pass @function --foo(--x type(<color>+): red 5) is invalid
Pass @function --foo(--x type(auto | none): thing) is invalid
Fail @function --foo(--x <length>#) is valid
Fail @function --foo(--x <length>+) is valid
Fail @function --foo(--x type(<length>+)) is valid
Fail @function --foo(--x <transform-function>#) is valid
Fail @function --foo(--x <transform-function>+) is valid
Pass @function --foo(--x <length>#) is valid
Pass @function --foo(--x <length>+) is valid
Pass @function --foo(--x type(<length>+)) is valid
Pass @function --foo(--x <transform-function>#) is valid
Pass @function --foo(--x <transform-function>+) is valid
Pass @function --foo(--x <transform-list>#) is invalid
Pass @function --foo(--x <transform-list>+) is invalid
Pass @function --foo(--x *) is invalid
@ -71,12 +70,12 @@ Pass @function --foo(--x, ;) is invalid
Pass @function --foo(;) is invalid
Pass @function --foo(]) is invalid
Pass @function --foo(, --x]) is invalid
Fail @function --foo(--x) returns type(*) is valid
Fail @function --foo(--x) returns <length> is valid
Fail @function --foo(--x) returns <length>+ is valid
Fail @function --foo(--x) returns type(<length>) is valid
Fail @function --foo(--x) returns type(<length> | auto) is valid
Fail @function --foo(--x) returns type(foo | bar) is valid
Pass @function --foo(--x) returns type(*) is valid
Pass @function --foo(--x) returns <length> is valid
Pass @function --foo(--x) returns <length>+ is valid
Pass @function --foo(--x) returns type(<length>) is valid
Pass @function --foo(--x) returns type(<length> | auto) is valid
Pass @function --foo(--x) returns type(foo | bar) is valid
Pass @function --foo(--x) ! is invalid
Pass @function --foo(--x) ! <length> is invalid
Pass @function --foo(--x) returns <length>! is invalid