make spec parser stricter
Signed-off-by: Onur Özkan <work@onurozkan.dev>
This commit is contained in:
1 parent
4c6ee111e8
commit
b0a7a9bb03
14 files changed
+96
-18
No files matched your search
+21
-12
@@ -1,9 +1,10 @@
|
|||||||
use proc_macro::{Literal, Punct, Spacing, TokenStream, TokenTree};
|
use proc_macro::{Literal, TokenStream, TokenTree};
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn spec(attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn spec(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
match parse_spec_path(attr) {
|
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),
|
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();
|
let tokens: Vec<TokenTree> = attr.into_iter().collect();
|
||||||
|
|
||||||
match tokens.as_slice() {
|
match tokens.as_slice() {
|
||||||
[TokenTree::Literal(path)] if is_string_literal(path) => Ok(path.to_string()),
|
[TokenTree::Literal(path)] if is_string_literal(path) => {
|
||||||
[
|
if has_spec_suffix(path) {
|
||||||
TokenTree::Ident(name),
|
Ok(path.to_string())
|
||||||
TokenTree::Punct(eq),
|
} else {
|
||||||
TokenTree::Literal(path),
|
Err("spec path must end in `.spec.md`; use #[spec(\"file.spec.md\")]".into())
|
||||||
] if name.to_string() == "path" && is_equals(eq) && is_string_literal(path) => {
|
}
|
||||||
Ok(path.to_string())
|
|
||||||
}
|
}
|
||||||
[] => Err("missing spec path; 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()),
|
_ => 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 {
|
fn is_string_literal(literal: &Literal) -> bool {
|
||||||
let value = literal.to_string();
|
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 {
|
fn has_spec_suffix(literal: &Literal) -> bool {
|
||||||
punct.as_char() == '=' && punct.spacing() == Spacing::Alone
|
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 {
|
fn expand_spec_guard(path: &str, item: TokenStream) -> TokenStream {
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ fn accepts_existing_sidecar_spec() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_missing_sidecar_spec() {
|
fn rejects_invalid_spec_uses() {
|
||||||
let tests = trybuild::TestCases::new();
|
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_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,8 +1,7 @@
|
|||||||
use kaptan_spec::spec;
|
use kaptan_spec::spec;
|
||||||
|
|
||||||
#[spec("fail_missing_sidecar.spec.md")]
|
#[spec("fail_missing_sidecar.spec.md")]
|
||||||
struct MissingSpec;
|
mod missing_spec {}
|
||||||
|
|
||||||
fn main() {
|
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)
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use kaptan_spec::spec;
|
use kaptan_spec::spec;
|
||||||
|
|
||||||
#[spec("flight_core_contract.spec.md")]
|
#[spec("flight_core_contract.spec.md")]
|
||||||
struct FlightCore;
|
mod flight_core {}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let size = core::mem::size_of::<FlightCore>();
|
assert_eq!(core::mem::size_of::<()>(), 0);
|
||||||
assert_eq!(size, 0);
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user