From 5bd4dae0311f2b0fe45d4f04319556215634b71d Mon Sep 17 00:00:00 2001 From: Kaya Gokalp <20915464+kayagokalp@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:57:33 -0700 Subject: [PATCH 1/6] feat: fuzz attribute parsing --- forc-pkg/src/pkg.rs | 69 +++--- sway-ast/src/attribute.rs | 10 + sway-core/src/language/ty/ast_node.rs | 2 +- sway-core/src/transform/attribute.rs | 66 ++++++ sway-parse/src/attribute.rs | 221 ++++++++++++++++++ .../attributes_fuzz_invalid_args/Forc.lock | 0 .../attributes_fuzz_invalid_args/Forc.toml | 7 + .../attributes_fuzz_invalid_args/src/main.sw | 15 ++ .../attributes_fuzz_invalid_args/test.toml | 3 + .../Forc.lock | 0 .../Forc.toml | 7 + .../src/main.sw | 11 + .../test.toml | 3 + .../attributes_fuzz_invalid_target/Forc.lock | 0 .../attributes_fuzz_invalid_target/Forc.toml | 7 + .../src/main.sw | 9 + .../attributes_fuzz_invalid_target/test.toml | 4 + .../language/attributes_fuzz_valid/Forc.lock | 0 .../language/attributes_fuzz_valid/Forc.toml | 7 + .../attributes_fuzz_valid/src/main.sw | 22 ++ .../language/attributes_fuzz_valid/test.toml | 3 + 21 files changed, 438 insertions(+), 28 deletions(-) create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/test.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/test.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/test.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw create mode 100644 test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 11203579b5d..cfe1b1e8b15 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -2041,36 +2041,51 @@ impl PkgTestEntry { let span = decl_ref.span(); let test_function_decl = engines.de().get_function(decl_ref); - let Some(test_attr) = test_function_decl.attributes.test() else { - unreachable!("`test_function_decl` is guaranteed to be a test function and it must have a `#[test]` attribute"); - }; + let test_attr = test_function_decl.attributes.test(); + let fuzz_attr = test_function_decl.attributes.fuzz(); + + match (test_attr, fuzz_attr) { + (Some(_), Some(_)) => { + bail!("Function \"{}\" cannot have both #[test] and #[fuzz] attributes", test_function_decl.name); + } + (None, None) => { + unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` or `#[fuzz]` attribute"); + } + _ => {} // Valid: exactly one attribute present + } - let pass_condition = match test_attr - .args - .iter() - // Last "should_revert" argument wins ;-) - .rfind(|arg| arg.is_test_should_revert()) - { - Some(should_revert_arg) => { - match should_revert_arg.get_string_opt(&Handler::default()) { - Ok(should_revert_arg_value) => TestPassCondition::ShouldRevert( - should_revert_arg_value - .map(|val| val.parse::()) - .transpose() - .map_err(|_| { - anyhow!(get_invalid_revert_code_error_msg( - &test_function_decl.name, - should_revert_arg - )) - })?, - ), - Err(_) => bail!(get_invalid_revert_code_error_msg( - &test_function_decl.name, - should_revert_arg - )), + let pass_condition = if let Some(test_attr) = test_attr { + // Handle #[test] attributes + match test_attr + .args + .iter() + // Last "should_revert" argument wins ;-) + .rfind(|arg| arg.is_test_should_revert()) + { + Some(should_revert_arg) => { + match should_revert_arg.get_string_opt(&Handler::default()) { + Ok(should_revert_arg_value) => TestPassCondition::ShouldRevert( + should_revert_arg_value + .map(|val| val.parse::()) + .transpose() + .map_err(|_| { + anyhow!(get_invalid_revert_code_error_msg( + &test_function_decl.name, + should_revert_arg + )) + })?, + ), + Err(_) => bail!(get_invalid_revert_code_error_msg( + &test_function_decl.name, + should_revert_arg + )), + } } + None => TestPassCondition::ShouldNotRevert, } - None => TestPassCondition::ShouldNotRevert, + } else { + // Handle #[fuzz] attributes - fuzz tests shouldn't revert by default + TestPassCondition::ShouldNotRevert }; let file_path = diff --git a/sway-ast/src/attribute.rs b/sway-ast/src/attribute.rs index 9f6fbfa61b5..fddf942a9c8 100644 --- a/sway-ast/src/attribute.rs +++ b/sway-ast/src/attribute.rs @@ -33,6 +33,14 @@ pub const DOC_COMMENT_ATTRIBUTE_NAME: &str = "doc-comment"; pub const TEST_ATTRIBUTE_NAME: &str = "test"; pub const TEST_SHOULD_REVERT_ARG_NAME: &str = "should_revert"; +// In-language fuzz testing. +pub const FUZZ_ATTRIBUTE_NAME: &str = "fuzz"; +pub const FUZZ_PARAM_ATTRIBUTE_NAME: &str = "fuzz_param"; +pub const FUZZ_PARAM_NAME_ARG_NAME: &str = "name"; +pub const FUZZ_PARAM_ITERATION_ARG_NAME: &str = "iteration"; +pub const FUZZ_PARAM_MIN_VAL_ARG_NAME: &str = "min_val"; +pub const FUZZ_PARAM_MAX_VAL_ARG_NAME: &str = "max_val"; + // Allow warnings. pub const ALLOW_ATTRIBUTE_NAME: &str = "allow"; pub const ALLOW_DEAD_CODE_ARG_NAME: &str = "dead_code"; @@ -72,6 +80,8 @@ pub const KNOWN_ATTRIBUTE_NAMES: &[&str] = &[ DEPRECATED_ATTRIBUTE_NAME, FALLBACK_ATTRIBUTE_NAME, ABI_NAME_ATTRIBUTE_NAME, + FUZZ_ATTRIBUTE_NAME, + FUZZ_PARAM_ATTRIBUTE_NAME, ]; /// An attribute declaration. Attribute declaration diff --git a/sway-core/src/language/ty/ast_node.rs b/sway-core/src/language/ty/ast_node.rs index 4f029ee8577..b40eef3f8a0 100644 --- a/sway-core/src/language/ty/ast_node.rs +++ b/sway-core/src/language/ty/ast_node.rs @@ -220,7 +220,7 @@ impl TyAstNode { } => { let fn_decl = decl_engine.get_function(decl_id); let TyFunctionDecl { attributes, .. } = &*fn_decl; - attributes.has_any_of_kind(AttributeKind::Test) + attributes.has_any_of_kind(AttributeKind::Test) || attributes.has_any_of_kind(AttributeKind::Fuzz) } _ => false, } diff --git a/sway-core/src/transform/attribute.rs b/sway-core/src/transform/attribute.rs index ab6d8769f0e..612e5023682 100644 --- a/sway-core/src/transform/attribute.rs +++ b/sway-core/src/transform/attribute.rs @@ -179,6 +179,22 @@ impl AttributeArg { self.name.as_str() == TEST_SHOULD_REVERT_ARG_NAME } + pub fn is_fuzz_param_name(&self) -> bool { + self.name.as_str() == FUZZ_PARAM_NAME_ARG_NAME + } + + pub fn is_fuzz_param_iteration(&self) -> bool { + self.name.as_str() == FUZZ_PARAM_ITERATION_ARG_NAME + } + + pub fn is_fuzz_param_min_val(&self) -> bool { + self.name.as_str() == FUZZ_PARAM_MIN_VAL_ARG_NAME + } + + pub fn is_fuzz_param_max_val(&self) -> bool { + self.name.as_str() == FUZZ_PARAM_MAX_VAL_ARG_NAME + } + pub fn is_error_message(&self) -> bool { self.name.as_str() == ERROR_M_ARG_NAME } @@ -356,6 +372,8 @@ pub enum AttributeKind { Error, Trace, AbiName, + Fuzz, + FuzzParam, } /// Denotes if an [ItemTraitItem] belongs to an ABI or to a trait. @@ -388,6 +406,8 @@ impl AttributeKind { ERROR_ATTRIBUTE_NAME => AttributeKind::Error, TRACE_ATTRIBUTE_NAME => AttributeKind::Trace, ABI_NAME_ATTRIBUTE_NAME => AttributeKind::AbiName, + FUZZ_ATTRIBUTE_NAME => AttributeKind::Fuzz, + FUZZ_PARAM_ATTRIBUTE_NAME => AttributeKind::FuzzParam, _ => AttributeKind::Unknown, } } @@ -418,6 +438,8 @@ impl AttributeKind { Error => false, Trace => false, AbiName => false, + Fuzz => false, + FuzzParam => true, } } } @@ -461,6 +483,10 @@ impl Attribute { // `trace(never)` or `trace(always)`. Trace => Multiplicity::exactly(1), AbiName => Multiplicity::exactly(1), + // `fuzz` takes no arguments. + Fuzz => Multiplicity::zero(), + // `fuzz_param(name = "foo", iteration = 100)`. + FuzzParam => Multiplicity::at_least(1), } } @@ -518,6 +544,13 @@ impl Attribute { Error => MustBeIn(vec![ERROR_M_ARG_NAME]), Trace => MustBeIn(vec![TRACE_ALWAYS_ARG_NAME, TRACE_NEVER_ARG_NAME]), AbiName => MustBeIn(vec![ABI_NAME_NAME_ARG_NAME]), + Fuzz => None, + FuzzParam => MustBeIn(vec![ + FUZZ_PARAM_NAME_ARG_NAME, + FUZZ_PARAM_ITERATION_ARG_NAME, + FUZZ_PARAM_MIN_VAL_ARG_NAME, + FUZZ_PARAM_MAX_VAL_ARG_NAME, + ]), } } @@ -543,6 +576,9 @@ impl Attribute { Error => Yes, Trace => No, AbiName => Yes, + Fuzz => No, + // `fuzz_param(name = "foo", iteration = 100)`. + FuzzParam => Yes, } } @@ -565,6 +601,8 @@ impl Attribute { Error => false, Trace => false, AbiName => false, + Fuzz => false, + FuzzParam => false, } } @@ -620,6 +658,8 @@ impl Attribute { Error => false, Trace => matches!(item_kind, ItemKind::Fn(_)), AbiName => matches!(item_kind, ItemKind::Struct(_) | ItemKind::Enum(_)), + Fuzz => matches!(item_kind, ItemKind::Fn(_)), + FuzzParam => matches!(item_kind, ItemKind::Fn(_)), } } @@ -647,6 +687,8 @@ impl Attribute { Error => struct_or_enum_field == StructOrEnumField::EnumField, Trace => false, AbiName => false, + Fuzz => false, + FuzzParam => false, } } @@ -676,6 +718,8 @@ impl Attribute { // because they don't have implementation. Trace => false, AbiName => false, + Fuzz => false, + FuzzParam => false, } } @@ -700,6 +744,8 @@ impl Attribute { Error => false, Trace => matches!(item, ItemImplItem::Fn(..)), AbiName => false, + Fuzz => matches!(item, ItemImplItem::Fn(..)), + FuzzParam => matches!(item, ItemImplItem::Fn(..)), } } @@ -723,6 +769,8 @@ impl Attribute { Error => false, Trace => true, AbiName => false, + Fuzz => false, + FuzzParam => false, } } @@ -744,6 +792,8 @@ impl Attribute { Error => false, Trace => false, AbiName => false, + Fuzz => false, + FuzzParam => false, } } @@ -764,6 +814,8 @@ impl Attribute { Error => false, Trace => false, AbiName => false, + Fuzz => false, + FuzzParam => false, } } @@ -819,6 +871,8 @@ impl Attribute { AbiName => vec![ "\"abi_name\" attribute can only annotate structs and enums.", ], + Fuzz => vec!["\"fuzz\" attribute can only annotate module functions."], + FuzzParam => vec!["\"fuzz_param\" attribute can only annotate module functions."], }; if help.is_empty() && target_friendly_name.starts_with("module kind") { @@ -1060,6 +1114,18 @@ impl Attributes { self.of_kind(AttributeKind::Test).last() } + /// Returns the `#[fuzz]` [Attribute], or `None` if the + /// [Attributes] does not contain any `#[fuzz]` attributes. + pub fn fuzz(&self) -> Option<&Attribute> { + // Last-wins approach. + self.of_kind(AttributeKind::Fuzz).last() + } + + /// Returns all `#[fuzz_param]` [Attribute]s. + pub fn fuzz_params(&self) -> impl Iterator { + self.of_kind(AttributeKind::FuzzParam) + } + /// Returns the `#[error]` [Attribute], or `None` if the /// [Attributes] does not contain any `#[error]` attributes. pub fn error(&self) -> Option<&Attribute> { diff --git a/sway-parse/src/attribute.rs b/sway-parse/src/attribute.rs index 6c6ccac9d17..7a52182c186 100644 --- a/sway-parse/src/attribute.rs +++ b/sway-parse/src/attribute.rs @@ -543,4 +543,225 @@ mod tests { ) "#); } + + #[test] + fn parse_fuzz_attribute() { + assert_ron_snapshot!(parse::(r#" + fuzz + "#,), @r#" + Attribute( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz\n ", + start: 13, + end: 17, + source_id: None, + ), + is_raw_ident: false, + ), + args: None, + ) + "#); + } + + #[test] + fn parse_fuzz_param_attribute() { + assert_ron_snapshot!(parse::(r#" + fuzz_param(name = "input1", iteration = 100) + "#,), @r#" + Attribute( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 13, + end: 23, + source_id: None, + ), + is_raw_ident: false, + ), + args: Some(Parens( + inner: Punctuated( + value_separator_pairs: [ + (AttributeArg( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 24, + end: 28, + source_id: None, + ), + is_raw_ident: false, + ), + value: Some(String(LitString( + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 31, + end: 39, + source_id: None, + ), + parsed: "input1", + ))), + ), CommaToken( + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 39, + end: 40, + source_id: None, + ), + )), + ], + final_value_opt: Some(AttributeArg( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 41, + end: 50, + source_id: None, + ), + is_raw_ident: false, + ), + value: Some(Int(LitInt( + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 53, + end: 56, + source_id: None, + ), + parsed: [ + 100, + ], + ty_opt: None, + is_generated_b256: false, + ))), + )), + ), + span: Span( + src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", + start: 23, + end: 57, + source_id: None, + ), + )), + ) + "#); + } + + #[test] + fn parse_fuzz_param_min_max_attribute() { + assert_ron_snapshot!(parse::(r#" + fuzz_param(name = "input2", min_val = 0, max_val = 255) + "#,), @r#" + Attribute( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 13, + end: 23, + source_id: None, + ), + is_raw_ident: false, + ), + args: Some(Parens( + inner: Punctuated( + value_separator_pairs: [ + (AttributeArg( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 24, + end: 28, + source_id: None, + ), + is_raw_ident: false, + ), + value: Some(String(LitString( + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 31, + end: 39, + source_id: None, + ), + parsed: "input2", + ))), + ), CommaToken( + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 39, + end: 40, + source_id: None, + ), + )), + (AttributeArg( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 41, + end: 48, + source_id: None, + ), + is_raw_ident: false, + ), + value: Some(Int(LitInt( + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 51, + end: 52, + source_id: None, + ), + parsed: [], + ty_opt: None, + is_generated_b256: false, + ))), + ), CommaToken( + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 52, + end: 53, + source_id: None, + ), + )), + ], + final_value_opt: Some(AttributeArg( + name: BaseIdent( + name_override_opt: None, + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 54, + end: 61, + source_id: None, + ), + is_raw_ident: false, + ), + value: Some(Int(LitInt( + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 64, + end: 67, + source_id: None, + ), + parsed: [ + 255, + ], + ty_opt: None, + is_generated_b256: false, + ))), + )), + ), + span: Span( + src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", + start: 23, + end: 68, + source_id: None, + ), + )), + ) + "#); + } } diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml new file mode 100644 index 00000000000..a144622af57 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "attributes_fuzz_invalid_args" + +[dependencies] \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw new file mode 100644 index 00000000000..7b8da26af41 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw @@ -0,0 +1,15 @@ +library; + +#[fuzz(invalid_arg)] +fn invalid_fuzz_with_args() { +} + +#[fuzz] +#[fuzz_param] +fn invalid_fuzz_param_no_args() { +} + +#[fuzz] +#[fuzz_param(unknown_arg = "value")] +fn invalid_fuzz_param_unknown_arg() { +} \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/test.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/test.toml new file mode 100644 index 00000000000..6c8e88395e0 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/test.toml @@ -0,0 +1,3 @@ +category = "fail" + +# check: $() \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml new file mode 100644 index 00000000000..450be36eb17 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "attributes_fuzz_invalid_multiplicity" + +[dependencies] \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw new file mode 100644 index 00000000000..ec5bed3881f --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw @@ -0,0 +1,11 @@ +library; + +#[fuzz] +#[fuzz] +fn multiple_fuzz_attributes() { +} + +#[test] +#[fuzz] +fn both_test_and_fuzz_attributes() { +} \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/test.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/test.toml new file mode 100644 index 00000000000..6c8e88395e0 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/test.toml @@ -0,0 +1,3 @@ +category = "fail" + +# check: $() \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml new file mode 100644 index 00000000000..5f562e66e4f --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "attributes_fuzz_invalid_target" + +[dependencies] \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw new file mode 100644 index 00000000000..4545909be26 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw @@ -0,0 +1,9 @@ +library; + +#[fuzz] +struct InvalidStruct { + field: u64, +} + +#[fuzz_param(name = "input", iteration = 100)] +const INVALID_CONST: u64 = 42; \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/test.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/test.toml new file mode 100644 index 00000000000..fe296733d2a --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/test.toml @@ -0,0 +1,4 @@ +category = "fail" + +# check: $() +# check: $() \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml new file mode 100644 index 00000000000..da239786a6a --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml @@ -0,0 +1,7 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "attributes_fuzz_valid" + +[dependencies] \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw new file mode 100644 index 00000000000..b927d939ced --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw @@ -0,0 +1,22 @@ +library; + +#[fuzz] +fn fuzz_simple() { +} + +#[fuzz] +#[fuzz_param(name = "input1", iteration = 100)] +fn fuzz_with_single_param(input1: u64) { +} + +#[fuzz] +#[fuzz_param(name = "input1", iteration = 50)] +#[fuzz_param(name = "input2", min_val = 0, max_val = 255)] +fn fuzz_with_multiple_params(input1: u64, input2: u8) { +} + +#[fuzz] +#[fuzz_param(name = "x", min_val = 1, max_val = 100)] +#[fuzz_param(name = "y", iteration = 25)] +fn fuzz_with_mixed_params(x: u32, y: u64) { +} \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml new file mode 100644 index 00000000000..27d9c7d4fc4 --- /dev/null +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml @@ -0,0 +1,3 @@ +category = "compile" + +# check: $() \ No newline at end of file From f473527b5b88540fb6b4e48705d5a3cba1167107 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp <20915464+kayagokalp@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:14:56 -0700 Subject: [PATCH 2/6] std dependency --- .../should_fail/attributes_fuzz_invalid_args/Forc.lock | 3 +++ .../should_fail/attributes_fuzz_invalid_args/Forc.toml | 3 +-- .../should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock | 3 +++ .../should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml | 3 +-- .../should_fail/attributes_fuzz_invalid_target/Forc.lock | 3 +++ .../should_fail/attributes_fuzz_invalid_target/Forc.toml | 3 +-- .../should_pass/language/attributes_fuzz_valid/Forc.lock | 3 +++ .../should_pass/language/attributes_fuzz_valid/Forc.toml | 3 +-- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock index e69de29bb2d..8355521d5a2 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.lock @@ -0,0 +1,3 @@ +[[package]] +name = "attributes_fuzz_invalid_args" +source = "member" diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml index a144622af57..4feb90450cf 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/Forc.toml @@ -3,5 +3,4 @@ authors = ["Fuel Labs "] entry = "main.sw" license = "Apache-2.0" name = "attributes_fuzz_invalid_args" - -[dependencies] \ No newline at end of file +implicit-std = false \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock index e69de29bb2d..5e912ce0b6d 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.lock @@ -0,0 +1,3 @@ +[[package]] +name = "attributes_fuzz_invalid_multiplicity" +source = "member" diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml index 450be36eb17..a607148532e 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/Forc.toml @@ -3,5 +3,4 @@ authors = ["Fuel Labs "] entry = "main.sw" license = "Apache-2.0" name = "attributes_fuzz_invalid_multiplicity" - -[dependencies] \ No newline at end of file +implicit-std = false \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock index e69de29bb2d..fec1acbe6f1 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.lock @@ -0,0 +1,3 @@ +[[package]] +name = "attributes_fuzz_invalid_target" +source = "member" diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml index 5f562e66e4f..4715847a82b 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/Forc.toml @@ -3,5 +3,4 @@ authors = ["Fuel Labs "] entry = "main.sw" license = "Apache-2.0" name = "attributes_fuzz_invalid_target" - -[dependencies] \ No newline at end of file +implicit-std = false \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock index e69de29bb2d..c1812a9344f 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.lock @@ -0,0 +1,3 @@ +[[package]] +name = "attributes_fuzz_valid" +source = "member" diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml index da239786a6a..f86b1d5af2a 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/Forc.toml @@ -3,5 +3,4 @@ authors = ["Fuel Labs "] entry = "main.sw" license = "Apache-2.0" name = "attributes_fuzz_valid" - -[dependencies] \ No newline at end of file +implicit-std = false \ No newline at end of file From f21366a4cf17a358262aa551dffd07393ebe3bf7 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp <20915464+kayagokalp@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:08:00 -0700 Subject: [PATCH 3/6] fmt --- forc-pkg/src/pkg.rs | 7 +++++-- sway-core/src/language/ty/ast_node.rs | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index cfe1b1e8b15..8fd0418fefb 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -2043,10 +2043,13 @@ impl PkgTestEntry { let test_attr = test_function_decl.attributes.test(); let fuzz_attr = test_function_decl.attributes.fuzz(); - + match (test_attr, fuzz_attr) { (Some(_), Some(_)) => { - bail!("Function \"{}\" cannot have both #[test] and #[fuzz] attributes", test_function_decl.name); + bail!( + "Function \"{}\" cannot have both #[test] and #[fuzz] attributes", + test_function_decl.name + ); } (None, None) => { unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` or `#[fuzz]` attribute"); diff --git a/sway-core/src/language/ty/ast_node.rs b/sway-core/src/language/ty/ast_node.rs index b40eef3f8a0..075d339b613 100644 --- a/sway-core/src/language/ty/ast_node.rs +++ b/sway-core/src/language/ty/ast_node.rs @@ -220,7 +220,8 @@ impl TyAstNode { } => { let fn_decl = decl_engine.get_function(decl_id); let TyFunctionDecl { attributes, .. } = &*fn_decl; - attributes.has_any_of_kind(AttributeKind::Test) || attributes.has_any_of_kind(AttributeKind::Fuzz) + attributes.has_any_of_kind(AttributeKind::Test) + || attributes.has_any_of_kind(AttributeKind::Fuzz) } _ => false, } From 96903a3dd79d2ee8ce7fce6e8a2f2c28c68772f6 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp <20915464+kayagokalp@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:09:00 -0700 Subject: [PATCH 4/6] test expected warnings --- .../should_pass/language/attributes_fuzz_valid/test.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml index 27d9c7d4fc4..3361f4e48c4 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/test.toml @@ -1,3 +1,4 @@ category = "compile" +expected_warnings = 9 # check: $() \ No newline at end of file From efba9e1ce1e5cda09b0ed83f2870ffad795ceb92 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp <20915464+kayagokalp@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:59:08 -0700 Subject: [PATCH 5/6] fixture --- forc-pkg/src/pkg.rs | 14 +- sway-ast/src/attribute.rs | 10 +- sway-core/src/lib.rs | 23 ++ sway-core/src/transform/attribute.rs | 75 +++--- sway-error/src/convert_parse_tree_error.rs | 6 + sway-parse/src/attribute.rs | 218 +----------------- .../attributes_fuzz_invalid_args/src/main.sw | 22 +- .../src/main.sw | 19 +- .../src/main.sw | 14 +- .../attributes_fuzz_valid/src/main.sw | 26 ++- 10 files changed, 134 insertions(+), 293 deletions(-) diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 8fd0418fefb..1218a8a6438 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -2044,17 +2044,19 @@ impl PkgTestEntry { let test_attr = test_function_decl.attributes.test(); let fuzz_attr = test_function_decl.attributes.fuzz(); - match (test_attr, fuzz_attr) { - (Some(_), Some(_)) => { + // With fixture-based testing, #[test] is mandatory and can coexist with #[fuzz] and #[case] + let has_case = test_function_decl.attributes.cases().next().is_some(); + match (test_attr, fuzz_attr, has_case) { + (None, Some(_), _) | (None, None, true) => { bail!( - "Function \"{}\" cannot have both #[test] and #[fuzz] attributes", + "Function \"{}\" has parameterization attributes (#[fuzz] or #[case]) but is missing the required #[test] attribute", test_function_decl.name ); } - (None, None) => { - unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` or `#[fuzz]` attribute"); + (None, None, false) => { + unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` attribute or parameterization attributes"); } - _ => {} // Valid: exactly one attribute present + _ => {} // Valid: #[test] is present or only #[test] is present } let pass_condition = if let Some(test_attr) = test_attr { diff --git a/sway-ast/src/attribute.rs b/sway-ast/src/attribute.rs index fddf942a9c8..6e94e8e8ed1 100644 --- a/sway-ast/src/attribute.rs +++ b/sway-ast/src/attribute.rs @@ -33,13 +33,11 @@ pub const DOC_COMMENT_ATTRIBUTE_NAME: &str = "doc-comment"; pub const TEST_ATTRIBUTE_NAME: &str = "test"; pub const TEST_SHOULD_REVERT_ARG_NAME: &str = "should_revert"; +// In-language parameterized testing. +pub const CASE_ATTRIBUTE_NAME: &str = "case"; + // In-language fuzz testing. pub const FUZZ_ATTRIBUTE_NAME: &str = "fuzz"; -pub const FUZZ_PARAM_ATTRIBUTE_NAME: &str = "fuzz_param"; -pub const FUZZ_PARAM_NAME_ARG_NAME: &str = "name"; -pub const FUZZ_PARAM_ITERATION_ARG_NAME: &str = "iteration"; -pub const FUZZ_PARAM_MIN_VAL_ARG_NAME: &str = "min_val"; -pub const FUZZ_PARAM_MAX_VAL_ARG_NAME: &str = "max_val"; // Allow warnings. pub const ALLOW_ATTRIBUTE_NAME: &str = "allow"; @@ -73,6 +71,7 @@ pub const KNOWN_ATTRIBUTE_NAMES: &[&str] = &[ STORAGE_ATTRIBUTE_NAME, DOC_COMMENT_ATTRIBUTE_NAME, TEST_ATTRIBUTE_NAME, + CASE_ATTRIBUTE_NAME, INLINE_ATTRIBUTE_NAME, PAYABLE_ATTRIBUTE_NAME, ALLOW_ATTRIBUTE_NAME, @@ -81,7 +80,6 @@ pub const KNOWN_ATTRIBUTE_NAMES: &[&str] = &[ FALLBACK_ATTRIBUTE_NAME, ABI_NAME_ATTRIBUTE_NAME, FUZZ_ATTRIBUTE_NAME, - FUZZ_PARAM_ATTRIBUTE_NAME, ]; /// An attribute declaration. Attribute declaration diff --git a/sway-core/src/lib.rs b/sway-core/src/lib.rs index f732530ae2f..f82d3cc6159 100644 --- a/sway-core/src/lib.rs +++ b/sway-core/src/lib.rs @@ -374,6 +374,29 @@ pub(crate) fn attr_decls_to_attributes( } } + // Check fixture-based testing requirements: #[case] or #[fuzz] require #[test] + let has_test = attributes.of_kind(AttributeKind::Test).any(|_| true); + let has_case = attributes.of_kind(AttributeKind::Case).any(|_| true); + let has_fuzz = attributes.of_kind(AttributeKind::Fuzz).any(|_| true); + + if (has_case || has_fuzz) && !has_test { + let parameterization_attr = if has_case { + attributes.of_kind(AttributeKind::Case).next() + } else { + attributes.of_kind(AttributeKind::Fuzz).next() + }; + + if let Some(attr) = parameterization_attr { + handler.emit_err( + ConvertParseTreeError::ParameterizedTestRequiresTestAttribute { + span: attr.span.clone(), + attribute: attr.name.clone(), + } + .into(), + ); + } + } + (handler, attributes) } diff --git a/sway-core/src/transform/attribute.rs b/sway-core/src/transform/attribute.rs index 612e5023682..0b708492899 100644 --- a/sway-core/src/transform/attribute.rs +++ b/sway-core/src/transform/attribute.rs @@ -179,21 +179,6 @@ impl AttributeArg { self.name.as_str() == TEST_SHOULD_REVERT_ARG_NAME } - pub fn is_fuzz_param_name(&self) -> bool { - self.name.as_str() == FUZZ_PARAM_NAME_ARG_NAME - } - - pub fn is_fuzz_param_iteration(&self) -> bool { - self.name.as_str() == FUZZ_PARAM_ITERATION_ARG_NAME - } - - pub fn is_fuzz_param_min_val(&self) -> bool { - self.name.as_str() == FUZZ_PARAM_MIN_VAL_ARG_NAME - } - - pub fn is_fuzz_param_max_val(&self) -> bool { - self.name.as_str() == FUZZ_PARAM_MAX_VAL_ARG_NAME - } pub fn is_error_message(&self) -> bool { self.name.as_str() == ERROR_M_ARG_NAME @@ -363,6 +348,7 @@ pub enum AttributeKind { Storage, Inline, Test, + Case, Payable, Allow, Cfg, @@ -373,7 +359,6 @@ pub enum AttributeKind { Trace, AbiName, Fuzz, - FuzzParam, } /// Denotes if an [ItemTraitItem] belongs to an ABI or to a trait. @@ -397,6 +382,7 @@ impl AttributeKind { STORAGE_ATTRIBUTE_NAME => AttributeKind::Storage, INLINE_ATTRIBUTE_NAME => AttributeKind::Inline, TEST_ATTRIBUTE_NAME => AttributeKind::Test, + CASE_ATTRIBUTE_NAME => AttributeKind::Case, PAYABLE_ATTRIBUTE_NAME => AttributeKind::Payable, ALLOW_ATTRIBUTE_NAME => AttributeKind::Allow, CFG_ATTRIBUTE_NAME => AttributeKind::Cfg, @@ -407,7 +393,6 @@ impl AttributeKind { TRACE_ATTRIBUTE_NAME => AttributeKind::Trace, ABI_NAME_ATTRIBUTE_NAME => AttributeKind::AbiName, FUZZ_ATTRIBUTE_NAME => AttributeKind::Fuzz, - FUZZ_PARAM_ATTRIBUTE_NAME => AttributeKind::FuzzParam, _ => AttributeKind::Unknown, } } @@ -429,6 +414,7 @@ impl AttributeKind { Storage => false, Inline => false, Test => false, + Case => true, Payable => false, Allow => true, Cfg => true, @@ -439,7 +425,6 @@ impl AttributeKind { Trace => false, AbiName => false, Fuzz => false, - FuzzParam => true, } } } @@ -472,6 +457,8 @@ impl Attribute { Inline => Multiplicity::exactly(1), // `test`, `test(should_revert)`. Test => Multiplicity::at_most(1), + // `case(value1, value2, ...)`. + Case => Multiplicity::at_least(1), Payable => Multiplicity::zero(), Allow => Multiplicity::at_least(1), Cfg => Multiplicity::exactly(1), @@ -483,10 +470,8 @@ impl Attribute { // `trace(never)` or `trace(always)`. Trace => Multiplicity::exactly(1), AbiName => Multiplicity::exactly(1), - // `fuzz` takes no arguments. - Fuzz => Multiplicity::zero(), - // `fuzz_param(name = "foo", iteration = 100)`. - FuzzParam => Multiplicity::at_least(1), + // `fuzz(param_iterations = 10, param_min = 1, param_max = 100)`. + Fuzz => Multiplicity::at_least(1), } } @@ -527,6 +512,7 @@ impl Attribute { Storage => MustBeIn(vec![STORAGE_READ_ARG_NAME, STORAGE_WRITE_ARG_NAME]), Inline => MustBeIn(vec![INLINE_ALWAYS_ARG_NAME, INLINE_NEVER_ARG_NAME]), Test => MustBeIn(vec![TEST_SHOULD_REVERT_ARG_NAME]), + Case => Any, // Case accepts any values as arguments Payable => None, Allow => ShouldBeIn(vec![ALLOW_DEAD_CODE_ARG_NAME, ALLOW_DEPRECATED_ARG_NAME]), Cfg => { @@ -544,13 +530,7 @@ impl Attribute { Error => MustBeIn(vec![ERROR_M_ARG_NAME]), Trace => MustBeIn(vec![TRACE_ALWAYS_ARG_NAME, TRACE_NEVER_ARG_NAME]), AbiName => MustBeIn(vec![ABI_NAME_NAME_ARG_NAME]), - Fuzz => None, - FuzzParam => MustBeIn(vec![ - FUZZ_PARAM_NAME_ARG_NAME, - FUZZ_PARAM_ITERATION_ARG_NAME, - FUZZ_PARAM_MIN_VAL_ARG_NAME, - FUZZ_PARAM_MAX_VAL_ARG_NAME, - ]), + Fuzz => Any, // Fuzz will accept parameter-specific arguments } } @@ -565,6 +545,8 @@ impl Attribute { Inline => No, // `test(should_revert)`, `test(should_revert = "18446744073709486084")`. Test => Maybe, + // `case(value1, value2, ...)` - case arguments are values, not key-value pairs. + Case => No, Payable => No, Allow => No, Cfg => Yes, @@ -576,9 +558,8 @@ impl Attribute { Error => Yes, Trace => No, AbiName => Yes, - Fuzz => No, - // `fuzz_param(name = "foo", iteration = 100)`. - FuzzParam => Yes, + // `fuzz(param_iterations = 10, param_min = 1, param_max = 100)`. + Fuzz => Yes, } } @@ -590,6 +571,7 @@ impl Attribute { Storage => false, Inline => false, Test => false, + Case => false, Payable => false, Allow => false, Cfg => false, @@ -602,7 +584,6 @@ impl Attribute { Trace => false, AbiName => false, Fuzz => false, - FuzzParam => false, } } @@ -632,6 +613,7 @@ impl Attribute { Storage => matches!(item_kind, ItemKind::Fn(_)), Inline => matches!(item_kind, ItemKind::Fn(_)), Test => matches!(item_kind, ItemKind::Fn(_)), + Case => matches!(item_kind, ItemKind::Fn(_)), Payable => false, Allow => !matches!(item_kind, ItemKind::Submodule(_)), Cfg => !matches!(item_kind, ItemKind::Submodule(_)), @@ -659,7 +641,6 @@ impl Attribute { Trace => matches!(item_kind, ItemKind::Fn(_)), AbiName => matches!(item_kind, ItemKind::Struct(_) | ItemKind::Enum(_)), Fuzz => matches!(item_kind, ItemKind::Fn(_)), - FuzzParam => matches!(item_kind, ItemKind::Fn(_)), } } @@ -678,6 +659,7 @@ impl Attribute { Storage => false, Inline => false, Test => false, + Case => false, Payable => false, Allow => true, Cfg => true, @@ -688,7 +670,6 @@ impl Attribute { Trace => false, AbiName => false, Fuzz => false, - FuzzParam => false, } } @@ -706,6 +687,7 @@ impl Attribute { // because they don't have implementation. Inline => false, Test => false, + Case => false, Payable => parent == TraitItemParent::Abi && matches!(item, ItemTraitItem::Fn(..)), Allow => true, Cfg => true, @@ -719,7 +701,6 @@ impl Attribute { Trace => false, AbiName => false, Fuzz => false, - FuzzParam => false, } } @@ -735,6 +716,7 @@ impl Attribute { Storage => matches!(item, ItemImplItem::Fn(..)), Inline => matches!(item, ItemImplItem::Fn(..)), Test => false, + Case => matches!(item, ItemImplItem::Fn(..)), Payable => parent == ImplItemParent::Contract, Allow => true, Cfg => true, @@ -745,7 +727,6 @@ impl Attribute { Trace => matches!(item, ItemImplItem::Fn(..)), AbiName => false, Fuzz => matches!(item, ItemImplItem::Fn(..)), - FuzzParam => matches!(item, ItemImplItem::Fn(..)), } } @@ -760,6 +741,7 @@ impl Attribute { Storage => true, Inline => true, Test => false, + Case => false, Payable => abi_or_trait_item == TraitItemParent::Abi, Allow => true, Cfg => true, @@ -770,7 +752,6 @@ impl Attribute { Trace => true, AbiName => false, Fuzz => false, - FuzzParam => false, } } @@ -782,6 +763,7 @@ impl Attribute { Storage => false, Inline => false, Test => false, + Case => false, Payable => false, Allow => true, Cfg => true, @@ -793,7 +775,6 @@ impl Attribute { Trace => false, AbiName => false, Fuzz => false, - FuzzParam => false, } } @@ -805,6 +786,7 @@ impl Attribute { Storage => false, Inline => false, Test => false, + Case => false, Payable => false, Allow => true, Cfg => true, @@ -815,7 +797,6 @@ impl Attribute { Trace => false, AbiName => false, Fuzz => false, - FuzzParam => false, } } @@ -871,8 +852,8 @@ impl Attribute { AbiName => vec![ "\"abi_name\" attribute can only annotate structs and enums.", ], - Fuzz => vec!["\"fuzz\" attribute can only annotate module functions."], - FuzzParam => vec!["\"fuzz_param\" attribute can only annotate module functions."], + Case => vec!["\"case\" attribute can only annotate test functions (functions with #[test])."], + Fuzz => vec!["\"fuzz\" attribute can only annotate test functions (functions with #[test])."], }; if help.is_empty() && target_friendly_name.starts_with("module kind") { @@ -1114,6 +1095,11 @@ impl Attributes { self.of_kind(AttributeKind::Test).last() } + /// Returns all `#[case]` [Attribute]s. + pub fn cases(&self) -> impl Iterator { + self.of_kind(AttributeKind::Case) + } + /// Returns the `#[fuzz]` [Attribute], or `None` if the /// [Attributes] does not contain any `#[fuzz]` attributes. pub fn fuzz(&self) -> Option<&Attribute> { @@ -1121,11 +1107,6 @@ impl Attributes { self.of_kind(AttributeKind::Fuzz).last() } - /// Returns all `#[fuzz_param]` [Attribute]s. - pub fn fuzz_params(&self) -> impl Iterator { - self.of_kind(AttributeKind::FuzzParam) - } - /// Returns the `#[error]` [Attribute], or `None` if the /// [Attributes] does not contain any `#[error]` attributes. pub fn error(&self) -> Option<&Attribute> { diff --git a/sway-error/src/convert_parse_tree_error.rs b/sway-error/src/convert_parse_tree_error.rs index b38d825249e..45fe71f9c41 100644 --- a/sway-error/src/convert_parse_tree_error.rs +++ b/sway-error/src/convert_parse_tree_error.rs @@ -163,6 +163,11 @@ pub enum ConvertParseTreeError { arg: Ident, expected_values: Vec<&'static str>, }, + #[error("Parameterized test attribute \"{attribute}\" requires a #[test] attribute on the same function.")] + ParameterizedTestRequiresTestAttribute { + span: Span, + attribute: Ident, + }, } pub(crate) enum AttributeType { @@ -268,6 +273,7 @@ impl Spanned for ConvertParseTreeError { ConvertParseTreeError::InvalidAttributeArgExpectsValue { arg, .. } => arg.span(), ConvertParseTreeError::InvalidAttributeArgValueType { span, .. } => span.clone(), ConvertParseTreeError::InvalidAttributeArgValue { span, .. } => span.clone(), + ConvertParseTreeError::ParameterizedTestRequiresTestAttribute { span, .. } => span.clone(), } } } diff --git a/sway-parse/src/attribute.rs b/sway-parse/src/attribute.rs index 7a52182c186..5ace26a2a90 100644 --- a/sway-parse/src/attribute.rs +++ b/sway-parse/src/attribute.rs @@ -545,223 +545,23 @@ mod tests { } #[test] - fn parse_fuzz_attribute() { + fn parse_fuzz_attribute_no_args() { assert_ron_snapshot!(parse::(r#" - fuzz - "#,), @r#" - Attribute( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz\n ", - start: 13, - end: 17, - source_id: None, - ), - is_raw_ident: false, - ), - args: None, - ) - "#); + fuzz() + "#,), @"Attribute(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n fuzz()\\n \",start:13,end:17,source_id:None,),is_raw_ident:false,),args:Some(Parens(inner:Punctuated(value_separator_pairs:[],final_value_opt:None,),span:Span(src:\"\\n fuzz()\\n \",start:17,end:19,source_id:None,),)),)"); } #[test] - fn parse_fuzz_param_attribute() { + fn parse_case_attribute() { assert_ron_snapshot!(parse::(r#" - fuzz_param(name = "input1", iteration = 100) - "#,), @r#" - Attribute( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 13, - end: 23, - source_id: None, - ), - is_raw_ident: false, - ), - args: Some(Parens( - inner: Punctuated( - value_separator_pairs: [ - (AttributeArg( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 24, - end: 28, - source_id: None, - ), - is_raw_ident: false, - ), - value: Some(String(LitString( - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 31, - end: 39, - source_id: None, - ), - parsed: "input1", - ))), - ), CommaToken( - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 39, - end: 40, - source_id: None, - ), - )), - ], - final_value_opt: Some(AttributeArg( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 41, - end: 50, - source_id: None, - ), - is_raw_ident: false, - ), - value: Some(Int(LitInt( - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 53, - end: 56, - source_id: None, - ), - parsed: [ - 100, - ], - ty_opt: None, - is_generated_b256: false, - ))), - )), - ), - span: Span( - src: "\n fuzz_param(name = \"input1\", iteration = 100)\n ", - start: 23, - end: 57, - source_id: None, - ), - )), - ) - "#); + case(zero, one, is_true) + "#,), @"Attribute(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:13,end:17,source_id:None,),is_raw_ident:false,),args:Some(Parens(inner:Punctuated(value_separator_pairs:[(AttributeArg(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:18,end:22,source_id:None,),is_raw_ident:false,),value:None,),CommaToken(span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:22,end:23,source_id:None,),)),(AttributeArg(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:24,end:27,source_id:None,),is_raw_ident:false,),value:None,),CommaToken(span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:27,end:28,source_id:None,),)),],final_value_opt:Some(AttributeArg(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:29,end:36,source_id:None,),is_raw_ident:false,),value:None,)),),span:Span(src:\"\\n case(zero, one, is_true)\\n \",start:17,end:37,source_id:None,),)),)"); } #[test] - fn parse_fuzz_param_min_max_attribute() { + fn parse_fuzz_parameterized_attribute() { assert_ron_snapshot!(parse::(r#" - fuzz_param(name = "input2", min_val = 0, max_val = 255) - "#,), @r#" - Attribute( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 13, - end: 23, - source_id: None, - ), - is_raw_ident: false, - ), - args: Some(Parens( - inner: Punctuated( - value_separator_pairs: [ - (AttributeArg( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 24, - end: 28, - source_id: None, - ), - is_raw_ident: false, - ), - value: Some(String(LitString( - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 31, - end: 39, - source_id: None, - ), - parsed: "input2", - ))), - ), CommaToken( - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 39, - end: 40, - source_id: None, - ), - )), - (AttributeArg( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 41, - end: 48, - source_id: None, - ), - is_raw_ident: false, - ), - value: Some(Int(LitInt( - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 51, - end: 52, - source_id: None, - ), - parsed: [], - ty_opt: None, - is_generated_b256: false, - ))), - ), CommaToken( - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 52, - end: 53, - source_id: None, - ), - )), - ], - final_value_opt: Some(AttributeArg( - name: BaseIdent( - name_override_opt: None, - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 54, - end: 61, - source_id: None, - ), - is_raw_ident: false, - ), - value: Some(Int(LitInt( - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 64, - end: 67, - source_id: None, - ), - parsed: [ - 255, - ], - ty_opt: None, - is_generated_b256: false, - ))), - )), - ), - span: Span( - src: "\n fuzz_param(name = \"input2\", min_val = 0, max_val = 255)\n ", - start: 23, - end: 68, - source_id: None, - ), - )), - ) - "#); + fuzz(x_iterations = 10, y_min = 0, y_max = 255) + "#,), @"Attribute(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:13,end:17,source_id:None,),is_raw_ident:false,),args:Some(Parens(inner:Punctuated(value_separator_pairs:[(AttributeArg(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:18,end:30,source_id:None,),is_raw_ident:false,),value:Some(Int(LitInt(span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:33,end:35,source_id:None,),parsed:[10,],ty_opt:None,is_generated_b256:false,))),),CommaToken(span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:35,end:36,source_id:None,),)),(AttributeArg(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:37,end:42,source_id:None,),is_raw_ident:false,),value:Some(Int(LitInt(span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:45,end:46,source_id:None,),parsed:[],ty_opt:None,is_generated_b256:false,))),),CommaToken(span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:46,end:47,source_id:None,),)),],final_value_opt:Some(AttributeArg(name:BaseIdent(name_override_opt:None,span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:48,end:53,source_id:None,),is_raw_ident:false,),value:Some(Int(LitInt(span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:56,end:59,source_id:None,),parsed:[255,],ty_opt:None,is_generated_b256:false,))),)),),span:Span(src:\"\\n fuzz(x_iterations = 10, y_min = 0, y_max = 255)\\n \",start:17,end:60,source_id:None,),)),)"); } } diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw index 7b8da26af41..da99d3ea7c7 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_args/src/main.sw @@ -1,15 +1,23 @@ library; +// Invalid: fuzz argument without value assignment +#[test] #[fuzz(invalid_arg)] -fn invalid_fuzz_with_args() { +fn invalid_fuzz_with_unassigned_arg() { } -#[fuzz] -#[fuzz_param] -fn invalid_fuzz_param_no_args() { +// Invalid: case attribute requires arguments +#[test] +#[case()] +fn invalid_case_no_args() { } -#[fuzz] -#[fuzz_param(unknown_arg = "value")] -fn invalid_fuzz_param_unknown_arg() { +// Invalid: case attribute used without #[test] +#[case(some_value)] +fn invalid_case_without_test() { +} + +// Invalid: fuzz attribute used without #[test] +#[fuzz(param_iterations = 10)] +fn invalid_fuzz_without_test() { } \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw index ec5bed3881f..aee2ed56f77 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_multiplicity/src/main.sw @@ -1,11 +1,22 @@ library; -#[fuzz] -#[fuzz] +// Invalid: multiple fuzz attributes (only one allowed per function) +#[test] +#[fuzz(param1_iterations = 10)] +#[fuzz(param2_iterations = 20)] fn multiple_fuzz_attributes() { } +// Valid: This should now be allowed - test with both case and fuzz +#[test] +#[case(first_case)] +#[fuzz(param_iterations = 10)] +fn test_with_case_and_fuzz() { +} + +// Invalid: multiple test attributes (only one allowed per function) +#[test] #[test] -#[fuzz] -fn both_test_and_fuzz_attributes() { +#[case(some_case)] +fn multiple_test_attributes() { } \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw index 4545909be26..54a564f635d 100644 --- a/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw +++ b/test/src/e2e_vm_tests/test_programs/should_fail/attributes_fuzz_invalid_target/src/main.sw @@ -1,9 +1,17 @@ library; -#[fuzz] +// Invalid: fuzz attribute on struct +#[fuzz(param_iterations = 10)] struct InvalidStruct { field: u64, } -#[fuzz_param(name = "input", iteration = 100)] -const INVALID_CONST: u64 = 42; \ No newline at end of file +// Invalid: case attribute on const +#[case(some_value)] +const INVALID_CONST: u64 = 42; + +// Invalid: case attribute on struct +#[case(field_value)] +struct AnotherInvalidStruct { + field: u64, +} \ No newline at end of file diff --git a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw index b927d939ced..ea6a6bf73ed 100644 --- a/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw +++ b/test/src/e2e_vm_tests/test_programs/should_pass/language/attributes_fuzz_valid/src/main.sw @@ -1,22 +1,26 @@ library; -#[fuzz] +// Example 1: Simple fuzz test without specific parameters +#[test] +#[fuzz()] fn fuzz_simple() { } -#[fuzz] -#[fuzz_param(name = "input1", iteration = 100)] +// Example 2: Fuzz test with specific iterations for one parameter +#[test] +#[fuzz(input1_iterations = 100)] fn fuzz_with_single_param(input1: u64) { } -#[fuzz] -#[fuzz_param(name = "input1", iteration = 50)] -#[fuzz_param(name = "input2", min_val = 0, max_val = 255)] -fn fuzz_with_multiple_params(input1: u64, input2: u8) { +// Example 3: Mixed case and fuzz testing +#[test] +#[case(zero_input, small_input)] +#[fuzz(input1_iterations = 50, input2_min = 0, input2_max = 255)] +fn fuzz_with_mixed_fixtures(input1: u64, input2: u8) { } -#[fuzz] -#[fuzz_param(name = "x", min_val = 1, max_val = 100)] -#[fuzz_param(name = "y", iteration = 25)] -fn fuzz_with_mixed_params(x: u32, y: u64) { +// Example 4: Multiple fuzz parameter configurations +#[test] +#[fuzz(x_min = 1, x_max = 100, y_iterations = 25)] +fn fuzz_with_param_ranges(x: u32, y: u64) { } \ No newline at end of file From 1722998ba05f213515b90c658d4033240fdf142d Mon Sep 17 00:00:00 2001 From: Kaya Gokalp <20915464+kayagokalp@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:23:05 -0700 Subject: [PATCH 6/6] case by case --- forc-pkg/src/pkg.rs | 22 +++---- sway-core/src/lib.rs | 89 ++++++++++++++-------------- sway-core/src/transform/attribute.rs | 52 +++++++--------- 3 files changed, 78 insertions(+), 85 deletions(-) diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 1218a8a6438..625ae7f1322 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -2046,17 +2046,17 @@ impl PkgTestEntry { // With fixture-based testing, #[test] is mandatory and can coexist with #[fuzz] and #[case] let has_case = test_function_decl.attributes.cases().next().is_some(); - match (test_attr, fuzz_attr, has_case) { - (None, Some(_), _) | (None, None, true) => { - bail!( - "Function \"{}\" has parameterization attributes (#[fuzz] or #[case]) but is missing the required #[test] attribute", - test_function_decl.name - ); - } - (None, None, false) => { - unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` attribute or parameterization attributes"); - } - _ => {} // Valid: #[test] is present or only #[test] is present + let has_parameterization = fuzz_attr.is_some() || has_case; + + if test_attr.is_none() && has_parameterization { + bail!( + "Function \"{}\" has parameterization attributes (#[fuzz] or #[case]) but is missing the required #[test] attribute", + test_function_decl.name + ); + } + + if test_attr.is_none() && !has_parameterization { + unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` attribute or parameterization attributes"); } let pass_condition = if let Some(test_attr) = test_attr { diff --git a/sway-core/src/lib.rs b/sway-core/src/lib.rs index f82d3cc6159..edf09dcd283 100644 --- a/sway-core/src/lib.rs +++ b/sway-core/src/lib.rs @@ -218,42 +218,42 @@ pub(crate) fn attr_decls_to_attributes( let first_doc_line = attributes .next() .expect("`chunk_by` guarantees existence of at least one element in the chunk"); - if !can_annotate(first_doc_line) { - let last_doc_line = match attributes.last() { - Some(last_attr) => last_attr, - // There is only one doc line in the complete doc comment. - None => first_doc_line, - }; + if can_annotate(first_doc_line) { + continue; + } + + let last_doc_line = attributes.last().unwrap_or(first_doc_line); + handler.emit_err( + ConvertParseTreeError::InvalidAttributeTarget { + span: Span::join( + first_doc_line.span.clone(), + &last_doc_line.span.start_span(), + ), + attribute: first_doc_line.name.clone(), + target_friendly_name, + can_only_annotate_help: first_doc_line + .can_only_annotate_help(target_friendly_name), + } + .into(), + ); + } else { + // For other attributes, the error is shown for every individual attribute. + for attribute in attributes { + if can_annotate(attribute) { + continue; + } + handler.emit_err( ConvertParseTreeError::InvalidAttributeTarget { - span: Span::join( - first_doc_line.span.clone(), - &last_doc_line.span.start_span(), - ), - attribute: first_doc_line.name.clone(), + span: attribute.name.span(), + attribute: attribute.name.clone(), target_friendly_name, - can_only_annotate_help: first_doc_line + can_only_annotate_help: attribute .can_only_annotate_help(target_friendly_name), } .into(), ); } - } else { - // For other attributes, the error is shown for every individual attribute. - for attribute in attributes { - if !can_annotate(attribute) { - handler.emit_err( - ConvertParseTreeError::InvalidAttributeTarget { - span: attribute.name.span(), - attribute: attribute.name.clone(), - target_friendly_name, - can_only_annotate_help: attribute - .can_only_annotate_help(target_friendly_name), - } - .into(), - ); - } - } } } @@ -376,25 +376,24 @@ pub(crate) fn attr_decls_to_attributes( // Check fixture-based testing requirements: #[case] or #[fuzz] require #[test] let has_test = attributes.of_kind(AttributeKind::Test).any(|_| true); - let has_case = attributes.of_kind(AttributeKind::Case).any(|_| true); - let has_fuzz = attributes.of_kind(AttributeKind::Fuzz).any(|_| true); + let has_parameterization = attributes.of_kind(AttributeKind::Case).any(|_| true) + || attributes.of_kind(AttributeKind::Fuzz).any(|_| true); + + if !has_parameterization || has_test { + return (handler, attributes); + } - if (has_case || has_fuzz) && !has_test { - let parameterization_attr = if has_case { - attributes.of_kind(AttributeKind::Case).next() - } else { - attributes.of_kind(AttributeKind::Fuzz).next() - }; + let first_param_attr = attributes.of_kind(AttributeKind::Case).next() + .or_else(|| attributes.of_kind(AttributeKind::Fuzz).next()); - if let Some(attr) = parameterization_attr { - handler.emit_err( - ConvertParseTreeError::ParameterizedTestRequiresTestAttribute { - span: attr.span.clone(), - attribute: attr.name.clone(), - } - .into(), - ); - } + if let Some(attr) = first_param_attr { + handler.emit_err( + ConvertParseTreeError::ParameterizedTestRequiresTestAttribute { + span: attr.span.clone(), + attribute: attr.name.clone(), + } + .into(), + ); } (handler, attributes) diff --git a/sway-core/src/transform/attribute.rs b/sway-core/src/transform/attribute.rs index 0b708492899..9793713eada 100644 --- a/sway-core/src/transform/attribute.rs +++ b/sway-core/src/transform/attribute.rs @@ -618,23 +618,7 @@ impl Attribute { Allow => !matches!(item_kind, ItemKind::Submodule(_)), Cfg => !matches!(item_kind, ItemKind::Submodule(_)), // TODO: Adapt once https://github.com/FuelLabs/sway/issues/6942 is implemented. - Deprecated => match item_kind { - ItemKind::Submodule(_) => false, - ItemKind::Use(_) => false, - ItemKind::Struct(_) => true, - ItemKind::Enum(_) => true, - ItemKind::Fn(_) => true, - ItemKind::Trait(_) => false, - ItemKind::Impl(_) => false, - ItemKind::Abi(_) => false, - ItemKind::Const(_) => true, - ItemKind::Storage(_) => false, - // TODO: Currently, only single configurables can be deprecated. - // Change to true once https://github.com/FuelLabs/sway/issues/6942 is implemented. - ItemKind::Configurable(_) => false, - ItemKind::TypeAlias(_) => false, - ItemKind::Error(_, _) => true, - }, + Deprecated => Self::can_deprecate_item_kind(item_kind), Fallback => matches!(item_kind, ItemKind::Fn(_)), ErrorType => matches!(item_kind, ItemKind::Enum(_)), Error => false, @@ -644,6 +628,27 @@ impl Attribute { } } + fn can_deprecate_item_kind(item_kind: &ItemKind) -> bool { + matches!(item_kind, + ItemKind::Struct(_) | + ItemKind::Enum(_) | + ItemKind::Fn(_) | + ItemKind::Const(_) | + ItemKind::Error(_, _) + ) + } + + fn storage_help_text(target_friendly_name: &str) -> Vec<&'static str> { + if target_friendly_name == "function signature" { + vec![ + "\"storage\" attribute can only annotate functions that have an implementation.", + "Function signatures in ABI and trait declarations do not have implementations.", + ] + } else { + vec!["\"storage\" attribute can only annotate functions."] + } + } + // TODO: Add `can_annotated_nested_item_kind`, once we properly support nested items. // E.g., the `#[test]` attribute can annotate module functions (`ItemKind::Fn`), // but will not be allowed on nested functions. @@ -820,18 +825,7 @@ impl Attribute { vec![] }, }, - Storage => { - if target_friendly_name == "function signature" { - vec![ - "\"storage\" attribute can only annotate functions that have an implementation.", - "Function signatures in ABI and trait declarations do not have implementations.", - ] - } else { - vec![ - "\"storage\" attribute can only annotate functions.", - ] - } - }, + Storage => Self::storage_help_text(target_friendly_name), Inline => vec!["\"inline\" attribute can only annotate functions."], Test => vec!["\"test\" attribute can only annotate module functions."], Payable => vec![