Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::BTreeSet;
use cgp_macro_core::functions::to_camel_case_str;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::ext::IdentExt;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
Expand Down Expand Up @@ -64,10 +65,7 @@ fn derive_blanket_impl(item_trait: &ItemTrait) -> syn::Result<TokenStream> {
let method_ident = &signature.ident;
let mut hrtbs: BTreeSet<Ident> = BTreeSet::new();

let computer_ident = Ident::new(
&format!("Compute{}", to_camel_case_str(&method_ident.to_string())),
method_ident.span(),
);
let computer_ident = derive_computer_ident(method_ident);

for generic_param in signature.generics.params.iter() {
match generic_param {
Expand Down Expand Up @@ -396,10 +394,7 @@ fn derive_method_computer(
TokenStream::new()
};

let computer_ident = Ident::new(
&format!("Compute{}", to_camel_case_str(&method_ident.to_string())),
method_ident.span(),
);
let computer_ident = derive_computer_ident(method_ident);

let method_generics = {
let method_generics = method
Expand All @@ -419,9 +414,16 @@ fn derive_method_computer(

let (impl_generics, _, where_clause) = generics.split_for_impl();

// The helper function takes a reserved name rather than the method's own, so it
// cannot collide with an item of the same name already in the caller's module.
let helper_ident = Ident::new(
&format!("__compute_{}__", method_ident.unraw()),
method_ident.span(),
);

Ok(quote! {
#[cgp_computer( #computer_ident )]
#async_token fn #method_ident #impl_generics (
#async_token fn #helper_ident #impl_generics (
#context_ident: #context_type,
#arg_params
) #return_type
Expand All @@ -431,3 +433,16 @@ fn derive_method_computer(
}
})
}

/// The per-variant computer's name, `Compute` plus the method name in PascalCase. The
/// method name is unrawed first, so `r#type` yields `ComputeType` rather than an
/// invalid identifier.
fn derive_computer_ident(method_ident: &Ident) -> Ident {
Ident::new(
&format!(
"Compute{}",
to_camel_case_str(&method_ident.unraw().to_string())
),
method_ident.span(),
)
}
49 changes: 48 additions & 1 deletion crates/macros/cgp-macro-core/src/types/cgp_component/item.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use syn::ItemTrait;
use syn::{Error, GenericParam, ItemTrait};

use crate::types::attributes::CgpComponentAttributes;
use crate::types::cgp_component::{CgpComponentArgs, PreprocessedCgpComponent};
use crate::types::ident::TypeGenericParam;

/// Raw input stage: the parsed attribute args and trait, before CGP attributes
/// are stripped. First stage of the `#[cgp_component]` pipeline.
Expand All @@ -13,6 +14,8 @@ pub struct ItemCgpComponent {
impl ItemCgpComponent {
/// Split the CGP modifier attributes off the trait, yielding the next stage.
pub fn preprocess(&self) -> syn::Result<PreprocessedCgpComponent> {
self.check_component_name_params()?;

let (attributes, item_trait) = CgpComponentAttributes::preprocess(&self.item_trait)?;

Ok(PreprocessedCgpComponent {
Expand All @@ -21,4 +24,48 @@ impl ItemCgpComponent {
attributes,
})
}

/// Reject a `name:` parameter the trait does not declare. The marker struct is
/// declared with the name's parameters and the generated impls name it with the
/// trait's, so an undeclared one would otherwise fail downstream with `E0425`.
fn check_component_name_params(&self) -> syn::Result<()> {
let trait_generics = &self.item_trait.generics;

for param in self.args.component_name.type_generics.params.iter() {
let (declared, name) = match param {
TypeGenericParam::Lifetime(life) => (
trait_generics.params.iter().any(
|p| matches!(p, GenericParam::Lifetime(l) if l.lifetime.ident == life.ident),
),
life.to_string(),
),
TypeGenericParam::Type(ident) => (
trait_generics
.params
.iter()
.any(|p| matches!(p, GenericParam::Type(t) if &t.ident == ident)),
ident.to_string(),
),
TypeGenericParam::Const(param) => (
trait_generics
.params
.iter()
.any(|p| matches!(p, GenericParam::Const(c) if c.ident == param.ident)),
param.ident.to_string(),
),
};

if !declared {
return Err(Error::new_spanned(
param,
format!(
"the component name's parameter `{name}` is not a generic parameter of the trait `{}`",
self.item_trait.ident,
),
));
}
}

Ok(())
}
}
17 changes: 14 additions & 3 deletions crates/macros/cgp-macro-core/src/types/check_components/table.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use quote::ToTokens;
use quote::{ToTokens, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
Expand Down Expand Up @@ -40,9 +40,13 @@ impl CheckComponentsTable {
let impl_generics = &self.impl_generics;
let where_clause = &self.where_clause;

// The `#[check_providers]` form takes the context as a trait parameter rather
// than naming it in the supertrait, so a generic table's parameters (`<T>
// Wrapper<T>`) stay on the impls that declare them instead of appearing,
// undeclared, in the trait header.
let item_trait: ItemTrait = if self.check_providers.is_some() {
parse_internal! {
trait #trait_name <__Component__, __Params__: ?Sized>: #IsProviderFor<__Component__, #context_type, __Params__> {}
trait #trait_name <__Component__, __Context__, __Params__: ?Sized>: #IsProviderFor<__Component__, __Context__, __Params__> {}
}
} else {
parse_internal! {
Expand Down Expand Up @@ -78,9 +82,16 @@ impl CheckComponentsTable {
let impl_generics = generics.split_for_impl().0;

for self_type in self_types {
// Only the `#[check_providers]` trait takes the context as an argument.
let trait_args = if self.check_providers.is_some() {
quote! { #component_type, #context_type, #component_param }
} else {
quote! { #component_type, #component_param }
};

let item_impl: ItemImpl = parse_internal! {
impl #impl_generics
#trait_name < #component_type, #component_param >
#trait_name < #trait_args >
for #self_type
#where_clause
{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ impl EvaluatedDelegateEntry {

/// Emit the forwarding `IsProviderFor<Key, __Context__, __Params__>` impl,
/// bounded on the value being a provider for the same key so a missing
/// transitive dependency stays diagnosable.
/// transitive dependency stays diagnosable. `__Params__` is `?Sized`, as on
/// the trait, so a component used at an unsized parameter (`str`, making the
/// params tuple `(Life<'a>, str)` unsized) still resolves through the table.
pub fn build_is_provider_for_impl(&self, outer_generics: &Generics) -> syn::Result<ItemImpl> {
let table_type = &self.table_type;

Expand All @@ -68,7 +70,7 @@ impl EvaluatedDelegateEntry {
let value = &self.value;

generics.params.push(parse_internal!(__Context__));
generics.params.push(parse_internal!(__Params__));
generics.params.push(parse_internal!(__Params__: ?Sized));

generics
.make_where_clause()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::{Attribute, ItemImpl, Type, braced};
use syn::{Attribute, GenericParam, ItemImpl, Type, braced};

use crate::functions::parse_internal;
use crate::traits::ParseOptionalKeyword;
Expand Down Expand Up @@ -67,9 +67,33 @@ impl DelegateTable {
if self.new.is_some() {
let struct_type: IdentWithTypeGenerics =
parse_internal(self.table_type.to_token_stream())?;

// The target's type arguments name each parameter without its kind, so a
// parameter the table's generic list declares `const` is restored as a
// const parameter of the struct rather than read as a type parameter.
let mut generics = struct_type.type_generics.to_generics();
for param in generics.params.iter_mut() {
if let GenericParam::Type(type_param) = param
&& let Some(const_param) =
self.impl_generics
.generics
.params
.iter()
.find_map(|p| match p {
GenericParam::Const(c) if c.ident == type_param.ident => Some(c),
_ => None,
})
{
let mut const_param = const_param.clone();
const_param.eq_token = None;
const_param.default = None;
*param = GenericParam::Const(const_param);
}
}

item_structs.push(EmptyStruct {
ident: struct_type.ident,
generics: struct_type.type_generics.to_generics(),
generics,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ impl Parse for DelegateValueWithInnerTable {
impl EvalDelegateValue for DelegateValueWithInnerTable {
fn eval(&self) -> syn::Result<Type> {
let wrapper_ident = &self.wrapper_ident;
let struct_ident = &self.inner_table.table_ident;
let struct_generics = &self.inner_table.table_generics;
// The table's type arguments, not its definition list, so a
// `const N: usize` parameter is written as `N` in the value.
let table_type = self.inner_table.build_table_type()?;

let ty = parse_internal!( #wrapper_ident < #struct_ident #struct_generics > );
let ty = parse_internal!( #wrapper_ident < #table_type > );
Ok(ty)
}
}
Expand Down
19 changes: 15 additions & 4 deletions crates/macros/cgp-macro-core/src/types/generics/type_generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use core::ops::{Deref, DerefMut};
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::{Error, Generics};
use syn::{Error, GenericParam, Generics};

use crate::functions::parse_internal;

Expand Down Expand Up @@ -39,14 +39,25 @@ impl DerefMut for TypeGenerics {
}

impl Parse for TypeGenerics {
/// Accept a definition-site list whose parameters carry no bounds and no
/// defaults: bare lifetimes, bare type parameters, and `const N: T` parameters,
/// whose kind is kept so a declared table struct can be const-generic.
fn parse(input: ParseStream) -> syn::Result<Self> {
let generics: Generics = input.parse()?;

let (_, type_generics, _) = generics.split_for_impl();
for param in generics.params.iter() {
let valid = match param {
GenericParam::Lifetime(life) => life.colon_token.is_none(),
GenericParam::Type(ty) => ty.colon_token.is_none() && ty.default.is_none(),
GenericParam::Const(konst) => konst.default.is_none(),
};

let generics2: Generics = parse_internal(type_generics.to_token_stream())?;
if !valid {
return Err(Error::new_spanned(param, "invalid type generics syntax"));
}
}

if generics != generics2 {
if generics.where_clause.is_some() {
return Err(Error::new_spanned(generics, "invalid type generics syntax"));
}

Expand Down
8 changes: 4 additions & 4 deletions crates/macros/cgp-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,16 +709,16 @@ pub fn cgp_type(attrs: TokenStream, body: TokenStream) -> TokenStream {
Given the following:

```rust,ignore
#[trait_alias]
pub trait HasErrorType: Async + HasErrorType<Error: Async> {}
#[blanket_trait]
pub trait HasSendError: HasErrorType<Error: Send + Sync> {}
```

automatically generates the following blanket implementation:

```rust,ignore
impl<Context> HasErrorType for Context
impl<__Context__> HasSendError for __Context__
where
Context: Async + HasErrorType<Error: Async> {}
__Context__: HasErrorType<Error: Send + Sync> {}
```
*/
#[proc_macro_attribute]
Expand Down
1 change: 1 addition & 0 deletions crates/tests/cgp-macro-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ cgp-macro-test-util = { workspace = true }
insta = { version = "1.48.0" }

[dev-dependencies]
cgp-extra-macro-lib = { workspace = true }
cgp-macro-core = { workspace = true }
cgp-macro-lib = { workspace = true }
cgp-macro-test-util-lib = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! `#[blanket_trait]` rejects trait items it cannot forward into the blanket
//! impl: a method without a default body, a constant without a default
//! expression, and an item that is not a type, method, or constant.
//!
//! See cgp-knowledge-base/cgp/implementation/entrypoints/blanket_trait.md (Tests) for these
//! failure cases, and cgp-knowledge-base/cgp/reference/macros/blanket_trait.md for the
//! user-facing semantics.

use quote::quote;

use super::assert_macro_rejects;

#[test]
fn rejects_method_without_default() {
assert_macro_rejects("blanket_trait with a bodiless method", || {
cgp_macro_lib::blanket_trait(
quote!(),
quote!(
pub trait FooBar: Foo {
fn foo_bar(&self);
}
),
)
});
}

#[test]
fn rejects_const_without_default() {
assert_macro_rejects("blanket_trait with a valueless const", || {
cgp_macro_lib::blanket_trait(
quote!(),
quote!(
pub trait HasLimit: Foo {
const LIMIT: u32;
}
),
)
});
}

#[test]
fn rejects_macro_item() {
assert_macro_rejects("blanket_trait with a macro item", || {
cgp_macro_lib::blanket_trait(
quote!(),
quote!(
pub trait FooBar: Foo {
some_macro!();
}
),
)
});
}
Loading
Loading