Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: macros checks of attribute macros usages #252

Merged
merged 13 commits into from
May 20, 2024
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-Clink-arg=/force:unresolved", "-Ctarget-feature=+crt-static"]
2 changes: 2 additions & 0 deletions macros/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@

//! Implemntation of the procedural macros exposed via the `sails-macros` crate.

pub use program::__gprogram_internal;
pub use program::gprogram;
pub use route::groute;
pub use service::__gservice_internal;
pub use service::gservice;

mod program;
Expand Down
35 changes: 31 additions & 4 deletions macros/core/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,46 @@ use proc_macro_error::abort;
use quote::quote;
use std::collections::BTreeMap;
use syn::{
parse_quote, Ident, ImplItem, ImplItemFn, ItemImpl, Receiver, ReturnType, Type, TypePath,
Visibility,
parse_quote, spanned::Spanned, Ident, ImplItem, ImplItemFn, ItemImpl, Receiver, ReturnType,
Type, TypePath, Visibility,
};

/// Static Span of Program `impl` block
static mut PROGRAM_SPAN: Option<Span> = None;

pub fn gprogram(program_impl_tokens: TokenStream2) -> TokenStream2 {
let program_impl = syn::parse2(program_impl_tokens).unwrap_or_else(|err| {
let program_impl = parse_gprogram_impl(program_impl_tokens);
ensure_single_gprogram(&program_impl);
gen_gprogram_impl(program_impl)
}

#[doc(hidden)]
pub fn __gprogram_internal(program_impl_tokens: TokenStream2) -> TokenStream2 {
let program_impl = parse_gprogram_impl(program_impl_tokens);
gen_gprogram_impl(program_impl)
}

fn parse_gprogram_impl(program_impl_tokens: TokenStream2) -> ItemImpl {
syn::parse2(program_impl_tokens).unwrap_or_else(|err| {
abort!(
err.span(),
"`gprogram` attribute can be applied to impls only: {}",
err
)
});
})
}

fn ensure_single_gprogram(program_impl: &ItemImpl) {
if unsafe { PROGRAM_SPAN }.is_some() {
abort!(
program_impl.span(),
vobradovich marked this conversation as resolved.
Show resolved Hide resolved
"multiple `gprogram` attributes are not allowed"
)
}
unsafe { PROGRAM_SPAN = Some(program_impl.span()) };
}

fn gen_gprogram_impl(program_impl: ItemImpl) -> TokenStream2 {
let services_ctors = discover_services_ctors(&program_impl);

let mut program_impl = program_impl.clone();
Expand Down
58 changes: 54 additions & 4 deletions macros/core/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,69 @@ use proc_macro_error::abort;
use quote::quote;
use std::collections::BTreeMap;
use syn::{
GenericArgument, Ident, ImplItemFn, ItemImpl, Path, PathArguments, Type, TypeParamBound,
Visibility, WhereClause, WherePredicate,
spanned::Spanned, GenericArgument, Ident, ImplItemFn, ItemImpl, Path, PathArguments, Type,
TypeParamBound, Visibility, WhereClause, WherePredicate,
};

static mut SERVICE_SPANS: BTreeMap<String, Span> = BTreeMap::new();

pub fn gservice(service_impl_tokens: TokenStream2) -> TokenStream2 {
let service_impl = syn::parse2(service_impl_tokens).unwrap_or_else(|err| {
let service_impl = parse_gservice_impl(service_impl_tokens);
ensure_single_gservice_on_impl(&service_impl);
ensure_single_gservice_by_name(&service_impl);
gen_gservice_impl(service_impl)
}

#[doc(hidden)]
pub fn __gservice_internal(service_impl_tokens: TokenStream2) -> TokenStream2 {
let service_impl = parse_gservice_impl(service_impl_tokens);
gen_gservice_impl(service_impl)
}

fn parse_gservice_impl(service_impl_tokens: TokenStream2) -> ItemImpl {
syn::parse2(service_impl_tokens).unwrap_or_else(|err| {
abort!(
err.span(),
"`gservice` attribute can be applied to impls only: {}",
err
)
});
})
}

fn ensure_single_gservice_on_impl(service_impl: &ItemImpl) {
let attrs_gservice: Vec<_> = service_impl
.attrs
.iter()
.filter(|attr| {
attr.meta
.path()
.segments
.last()
.map(|s| s.ident == "gservice")
.unwrap_or(false)
})
.collect();
if !attrs_gservice.is_empty() {
abort!(
service_impl.span(),
"multiple `gservice` attributes on the same impl are not allowed",
)
}
}
DennisInSky marked this conversation as resolved.
Show resolved Hide resolved

fn ensure_single_gservice_by_name(service_impl: &ItemImpl) {
let path = shared::impl_type_path(service_impl);
let type_ident = path.path.segments.last().unwrap().ident.to_string();
if unsafe { SERVICE_SPANS.get(&type_ident) }.is_some() {
abort!(
service_impl.span(),
"multiple `gservice` attributes on a type with the same name are not allowed"
)
}
unsafe { SERVICE_SPANS.insert(type_ident, service_impl.span()) };
}

fn gen_gservice_impl(service_impl: ItemImpl) -> TokenStream2 {
let (service_type_path, service_type_args, service_type_constraints) = {
let service_type = ImplType::new(&service_impl);
(
Expand Down
26 changes: 14 additions & 12 deletions macros/core/src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,7 @@ pub(crate) struct ImplType<'a> {

impl<'a> ImplType<'a> {
pub(crate) fn new(item_impl: &'a ItemImpl) -> Self {
let path = {
let item_impl_type = item_impl.self_ty.as_ref();
if let Type::Path(type_path) = item_impl_type {
type_path
} else {
abort!(
item_impl_type.span(),
"failed to parse impl type: {}",
item_impl_type.to_token_stream()
)
}
};
let path = impl_type_path(item_impl);
let args = &path.path.segments.last().unwrap().arguments;
let constraints = item_impl.generics.where_clause.as_ref();
Self {
Expand All @@ -51,6 +40,19 @@ impl<'a> ImplType<'a> {
}
}

pub(crate) fn impl_type_path(item_impl: &ItemImpl) -> &TypePath {
let item_impl_type = item_impl.self_ty.as_ref();
if let Type::Path(type_path) = item_impl_type {
type_path
} else {
abort!(
item_impl_type.span(),
vobradovich marked this conversation as resolved.
Show resolved Hide resolved
"failed to parse impl type: {}",
item_impl_type.to_token_stream()
)
}
}

/// Represents parts of a handler function.
#[derive(Clone)]
pub(crate) struct Func<'a> {
Expand Down
12 changes: 6 additions & 6 deletions macros/core/tests/program.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use quote::quote;
use sails_macros_core::gprogram;
use sails_macros_core::__gprogram_internal;
vobradovich marked this conversation as resolved.
Show resolved Hide resolved

#[test]
fn gprogram_generates_init_for_single_ctor() {
Expand All @@ -11,7 +11,7 @@ fn gprogram_generates_init_for_single_ctor() {
}
};

let result = gprogram(input).to_string();
let result = __gprogram_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand All @@ -31,7 +31,7 @@ fn gprogram_generates_init_for_multiple_ctors() {
}
};

let result = gprogram(input).to_string();
let result = __gprogram_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand All @@ -44,7 +44,7 @@ fn gprogram_generates_init_for_no_ctor() {
}
};

let result = gprogram(input).to_string();
let result = __gprogram_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand All @@ -60,7 +60,7 @@ fn gprogram_generates_handle_for_single_service_with_non_empty_route() {
}
};

let result = gprogram(input).to_string();
let result = __gprogram_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand All @@ -81,7 +81,7 @@ fn gprogram_generates_handle_for_multiple_services_with_non_empty_routes() {
}
};

let result = gprogram(input).to_string();
let result = __gprogram_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand Down
6 changes: 3 additions & 3 deletions macros/core/tests/service.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use quote::quote;
use sails_macros_core::gservice;
use sails_macros_core::__gservice_internal;

#[test]
fn gservice_works() {
Expand All @@ -15,7 +15,7 @@ fn gservice_works() {
}
};

let result = gservice(input).to_string();
let result = __gservice_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand All @@ -33,7 +33,7 @@ fn gservice_works_for_lifetimes_and_generics() {
}
};

let result = gservice(input).to_string();
let result = __gservice_internal(input).to_string();
let result = prettyplease::unparse(&syn::parse_str(&result).unwrap());

insta::assert_snapshot!(result);
Expand Down
10 changes: 5 additions & 5 deletions macros/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@
#[test]
fn gservice_works() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/gservice_works.rs");
t.pass("tests/ui/gservice_works*.rs");
}

#[test]
fn gservice_works_for_lifecycles_and_generics() {
fn gservice_fails() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/gservice_works_for_lifecycles_and_generics.rs");
t.compile_fail("tests/ui/gservice_fails*.rs");
}

#[test]
fn fails() {
fn gprogram_fails() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*fails*.rs");
t.compile_fail("tests/ui/gprogram_fails*.rs");
}
20 changes: 20 additions & 0 deletions macros/tests/ui/gprogram_fails_multiple_not_allowed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use sails_macros::gprogram;

struct MyProgram;

#[gprogram]
impl MyProgram {
pub fn new() -> Self {
Self
}
}

#[gprogram]
impl MyProgram {
pub fn default() -> Self {
Self
}
}

#[tokio::main]
async fn main() {}
5 changes: 5 additions & 0 deletions macros/tests/ui/gprogram_fails_multiple_not_allowed.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: multiple `gprogram` attributes are not allowed
--> tests/ui/gprogram_fails_multiple_not_allowed.rs:13:1
|
13 | impl MyProgram {
| ^^^^
14 changes: 14 additions & 0 deletions macros/tests/ui/gprogram_fails_multiple_not_allowed_on_one_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use sails_macros::gprogram;

struct MyProgram;

#[gprogram]
#[gprogram]
impl MyProgram {
pub fn new() -> Self {
Self
}
}

#[tokio::main]
async fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: multiple `gprogram` attributes are not allowed
--> tests/ui/gprogram_fails_multiple_not_allowed_on_one_impl.rs:7:1
|
7 | impl MyProgram {
| ^^^^
20 changes: 20 additions & 0 deletions macros/tests/ui/gservice_fails_multiple_not_allowed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use sails_macros::gservice;

struct MyService;

#[gservice]
impl MyService {
pub fn this(&self, p1: bool) -> bool {
!p1
}
}

#[gservice]
impl MyService {
pub fn this(&self, p1: bool) -> bool {
!p1
}
}

#[tokio::main]
async fn main() {}
5 changes: 5 additions & 0 deletions macros/tests/ui/gservice_fails_multiple_not_allowed.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: multiple `gservice` attributes on a type with the same name are not allowed
--> tests/ui/gservice_fails_multiple_not_allowed.rs:13:1
|
13 | impl MyService {
| ^^^^
14 changes: 14 additions & 0 deletions macros/tests/ui/gservice_fails_multiple_not_allowed_on_one_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use sails_macros::gservice;

struct MyService;

#[gservice]
#[gservice]
impl MyService {
pub fn this(&self, p1: bool) -> bool {
!p1
}
}

#[tokio::main]
async fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: multiple `gservice` attributes on the same impl are not allowed
--> tests/ui/gservice_fails_multiple_not_allowed_on_one_impl.rs:6:1
|
6 | #[gservice]
| ^