make spec parser stricter

Signed-off-by: Onur Özkan <work@onurozkan.dev>
This commit is contained in:
Onur Özkan committed 2026-07-18 13:16:42 +03:00
1 parent 4c6ee111e8
commit b0a7a9bb03
14 files changed
+96 -18

No files matched your search

+21 -12
View File
@@ -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<String, String> {
let tokens: Vec<TokenTree> = 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<String, String> {
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 {
+7 -1
View File
@@ -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");
}
@@ -0,0 +1,6 @@
use kaptan_spec::spec;
#[spec("flight_core_contract.md")]
struct InvalidSuffix;
fn main() {}
@@ -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)
@@ -0,0 +1,6 @@
use kaptan_spec::spec;
#[spec(path = "flight_core_contract.spec.md")]
struct LegacyNamedArgument;
fn main() {}
@@ -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)
@@ -0,0 +1,6 @@
use kaptan_spec::spec;
#[spec]
struct MissingArgument;
fn main() {}
@@ -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)
+1 -2
View File
@@ -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::<MissingSpec>();
}
@@ -0,0 +1,6 @@
use kaptan_spec::spec;
#[spec("flight_core_contract.spec.md")]
struct FlightCore;
fn main() {}
@@ -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)
@@ -0,0 +1,6 @@
use kaptan_spec::spec;
#[spec(42)]
struct NonStringArgument;
fn main() {}
@@ -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)
+2 -3
View File
@@ -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::<FlightCore>();
assert_eq!(size, 0);
assert_eq!(core::mem::size_of::<()>(), 0);
}