diff --git a/kaptan-spec/src/lib.rs b/kaptan-spec/src/lib.rs index 3869fe3..0911393 100644 --- a/kaptan-spec/src/lib.rs +++ b/kaptan-spec/src/lib.rs @@ -1,9 +1,10 @@ -use proc_macro::{Literal, Punct, Spacing, TokenStream, TokenTree}; +use proc_macro::{Literal, TokenStream, TokenTree}; #[proc_macro_attribute] pub fn spec(attr: TokenStream, item: TokenStream) -> TokenStream { match parse_spec_path(attr) { - Ok(path) => expand_spec_guard(&path, item), + Ok(path) if is_module_item(&item) => expand_spec_guard(&path, item), + Ok(_) => compile_error("`#[spec]` may only be applied to a module"), Err(message) => compile_error(&message), } } @@ -12,13 +13,12 @@ fn parse_spec_path(attr: TokenStream) -> Result { let tokens: Vec = attr.into_iter().collect(); match tokens.as_slice() { - [TokenTree::Literal(path)] if is_string_literal(path) => Ok(path.to_string()), - [ - TokenTree::Ident(name), - TokenTree::Punct(eq), - TokenTree::Literal(path), - ] if name.to_string() == "path" && is_equals(eq) && is_string_literal(path) => { - Ok(path.to_string()) + [TokenTree::Literal(path)] if is_string_literal(path) => { + if has_spec_suffix(path) { + Ok(path.to_string()) + } else { + Err("spec path must end in `.spec.md`; use #[spec(\"file.spec.md\")]".into()) + } } [] => Err("missing spec path; use #[spec(\"file.spec.md\")]".into()), _ => Err("invalid spec attribute; use #[spec(\"file.spec.md\")]".into()), @@ -28,11 +28,20 @@ fn parse_spec_path(attr: TokenStream) -> Result { fn is_string_literal(literal: &Literal) -> bool { let value = literal.to_string(); - value.starts_with('"') || value.starts_with("r\"") || value.starts_with("r#") + value.starts_with('"') && value.ends_with('"') } -fn is_equals(punct: &Punct) -> bool { - punct.as_char() == '=' && punct.spacing() == Spacing::Alone +fn has_spec_suffix(literal: &Literal) -> bool { + literal + .to_string() + .strip_suffix('"') + .is_some_and(|path| path.ends_with(".spec.md")) +} + +fn is_module_item(item: &TokenStream) -> bool { + item.clone().into_iter().any( + |token| matches!(token, TokenTree::Ident(identifier) if identifier.to_string() == "mod"), + ) } fn expand_spec_guard(path: &str, item: TokenStream) -> TokenStream { diff --git a/kaptan-spec/tests/spec_macro.rs b/kaptan-spec/tests/spec_macro.rs index 14a3505..a62807c 100644 --- a/kaptan-spec/tests/spec_macro.rs +++ b/kaptan-spec/tests/spec_macro.rs @@ -5,7 +5,13 @@ fn accepts_existing_sidecar_spec() { } #[test] -fn rejects_missing_sidecar_spec() { +fn rejects_invalid_spec_uses() { let tests = trybuild::TestCases::new(); + + tests.compile_fail("tests/ui/fail_missing_argument.rs"); + tests.compile_fail("tests/ui/fail_legacy_named_argument.rs"); + tests.compile_fail("tests/ui/fail_non_string_argument.rs"); + tests.compile_fail("tests/ui/fail_invalid_suffix.rs"); tests.compile_fail("tests/ui/fail_missing_sidecar.rs"); + tests.compile_fail("tests/ui/fail_non_module_item.rs"); } diff --git a/kaptan-spec/tests/ui/fail_invalid_suffix.rs b/kaptan-spec/tests/ui/fail_invalid_suffix.rs new file mode 100644 index 0000000..293e9cd --- /dev/null +++ b/kaptan-spec/tests/ui/fail_invalid_suffix.rs @@ -0,0 +1,6 @@ +use kaptan_spec::spec; + +#[spec("flight_core_contract.md")] +struct InvalidSuffix; + +fn main() {} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_invalid_suffix.stderr b/kaptan-spec/tests/ui/fail_invalid_suffix.stderr new file mode 100644 index 0000000..512489b --- /dev/null +++ b/kaptan-spec/tests/ui/fail_invalid_suffix.stderr @@ -0,0 +1,7 @@ +error: spec path must end in `.spec.md`; use #[spec("file.spec.md")] + --> tests/ui/fail_invalid_suffix.rs:3:1 + | +3 | #[spec("flight_core_contract.md")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `spec` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/kaptan-spec/tests/ui/fail_legacy_named_argument.rs b/kaptan-spec/tests/ui/fail_legacy_named_argument.rs new file mode 100644 index 0000000..c5c539c --- /dev/null +++ b/kaptan-spec/tests/ui/fail_legacy_named_argument.rs @@ -0,0 +1,6 @@ +use kaptan_spec::spec; + +#[spec(path = "flight_core_contract.spec.md")] +struct LegacyNamedArgument; + +fn main() {} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_legacy_named_argument.stderr b/kaptan-spec/tests/ui/fail_legacy_named_argument.stderr new file mode 100644 index 0000000..8e45974 --- /dev/null +++ b/kaptan-spec/tests/ui/fail_legacy_named_argument.stderr @@ -0,0 +1,7 @@ +error: invalid spec attribute; use #[spec("file.spec.md")] + --> tests/ui/fail_legacy_named_argument.rs:3:1 + | +3 | #[spec(path = "flight_core_contract.spec.md")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `spec` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/kaptan-spec/tests/ui/fail_missing_argument.rs b/kaptan-spec/tests/ui/fail_missing_argument.rs new file mode 100644 index 0000000..4b62f4f --- /dev/null +++ b/kaptan-spec/tests/ui/fail_missing_argument.rs @@ -0,0 +1,6 @@ +use kaptan_spec::spec; + +#[spec] +struct MissingArgument; + +fn main() {} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_missing_argument.stderr b/kaptan-spec/tests/ui/fail_missing_argument.stderr new file mode 100644 index 0000000..d2c56d3 --- /dev/null +++ b/kaptan-spec/tests/ui/fail_missing_argument.stderr @@ -0,0 +1,7 @@ +error: missing spec path; use #[spec("file.spec.md")] + --> tests/ui/fail_missing_argument.rs:3:1 + | +3 | #[spec] + | ^^^^^^^ + | + = note: this error originates in the attribute macro `spec` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/kaptan-spec/tests/ui/fail_missing_sidecar.rs b/kaptan-spec/tests/ui/fail_missing_sidecar.rs index 1cddb67..9057817 100644 --- a/kaptan-spec/tests/ui/fail_missing_sidecar.rs +++ b/kaptan-spec/tests/ui/fail_missing_sidecar.rs @@ -1,8 +1,7 @@ use kaptan_spec::spec; #[spec("fail_missing_sidecar.spec.md")] -struct MissingSpec; +mod missing_spec {} fn main() { - let _ = core::mem::size_of::(); } \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_non_module_item.rs b/kaptan-spec/tests/ui/fail_non_module_item.rs new file mode 100644 index 0000000..16bd7bb --- /dev/null +++ b/kaptan-spec/tests/ui/fail_non_module_item.rs @@ -0,0 +1,6 @@ +use kaptan_spec::spec; + +#[spec("flight_core_contract.spec.md")] +struct FlightCore; + +fn main() {} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_non_module_item.stderr b/kaptan-spec/tests/ui/fail_non_module_item.stderr new file mode 100644 index 0000000..7825be9 --- /dev/null +++ b/kaptan-spec/tests/ui/fail_non_module_item.stderr @@ -0,0 +1,7 @@ +error: `#[spec]` may only be applied to a module + --> tests/ui/fail_non_module_item.rs:3:1 + | +3 | #[spec("flight_core_contract.spec.md")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `spec` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/kaptan-spec/tests/ui/fail_non_string_argument.rs b/kaptan-spec/tests/ui/fail_non_string_argument.rs new file mode 100644 index 0000000..7a61a0d --- /dev/null +++ b/kaptan-spec/tests/ui/fail_non_string_argument.rs @@ -0,0 +1,6 @@ +use kaptan_spec::spec; + +#[spec(42)] +struct NonStringArgument; + +fn main() {} \ No newline at end of file diff --git a/kaptan-spec/tests/ui/fail_non_string_argument.stderr b/kaptan-spec/tests/ui/fail_non_string_argument.stderr new file mode 100644 index 0000000..ff595e6 --- /dev/null +++ b/kaptan-spec/tests/ui/fail_non_string_argument.stderr @@ -0,0 +1,7 @@ +error: invalid spec attribute; use #[spec("file.spec.md")] + --> tests/ui/fail_non_string_argument.rs:3:1 + | +3 | #[spec(42)] + | ^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `spec` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/kaptan-spec/tests/ui/flight_core_contract.rs b/kaptan-spec/tests/ui/flight_core_contract.rs index 399782f..705ebd9 100644 --- a/kaptan-spec/tests/ui/flight_core_contract.rs +++ b/kaptan-spec/tests/ui/flight_core_contract.rs @@ -1,9 +1,8 @@ use kaptan_spec::spec; #[spec("flight_core_contract.spec.md")] -struct FlightCore; +mod flight_core {} fn main() { - let size = core::mem::size_of::(); - assert_eq!(size, 0); + assert_eq!(core::mem::size_of::<()>(), 0); } \ No newline at end of file