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
7 changes: 5 additions & 2 deletions crates/macros/cgp-macro-core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ and the crate hierarchy.
- **[src/types/](src/types/)** — the bulk of the crate. One submodule per user-facing macro
(`cgp_component`, `cgp_impl`, `cgp_provider`, `cgp_fn`, `cgp_getter`, `cgp_auto_getter`,
`cgp_type`, `cgp_data`, `delegate_component`, `check_components`,
`delegate_and_check_components`, `namespace`, `product`, `sum`), plus shared building-block types:
`delegate_and_check_components`, `namespace`, `product`, `sum`, and `shape` for `Struct!`/`Enum!`),
plus shared building-block types:
`attributes/` (parsing of `#[uses]`, `#[use_type]`, `#[use_provider]`, `#[derive_delegate]`,
`#[default_impl]`), `generics/`, `field/`, `getter/`, `implicits/`, `ident/`, `path/`, `keyword`.
- **[src/functions/](src/functions/)** — free helper functions: identifier case conversion
(`camel_case`/`snake_case`), `parse_internal`, generics merging, delegated-impl synthesis,
field/getter/implicit-argument parsing, `strip`.
field/getter/implicit-argument parsing, `strip`, the `Struct!`/`Enum!` body parsing and
validation in `shape/`, and `reject_non_empty_attributes`, shared by the macros that accept no
attributes.
- **[src/visitors/](src/visitors/)** — `syn` `VisitMut` passes that rewrite ASTs. `replace_self`
(receiver/type/value) is the heart of `#[cgp_impl]`: it rewrites `self`/`Self` into the explicit
`context`/`Context`. Also `replace_provider`, `remove_self_path`, `self_assoc_type`, and
Expand Down
4 changes: 4 additions & 0 deletions crates/macros/cgp-macro-core/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod implicits;
mod is_provider_params;
mod override_span;
mod parse_internal;
mod reject_attributes;
mod shape;
mod snake_case;
mod strip;

Expand All @@ -19,5 +21,7 @@ pub use implicits::*;
pub use is_provider_params::*;
pub use override_span::*;
pub use parse_internal::*;
pub use reject_attributes::*;
pub use shape::*;
pub use snake_case::*;
pub use strip::*;
19 changes: 19 additions & 0 deletions crates/macros/cgp-macro-core/src/functions/reject_attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{Attribute, Error};

/// Error with a spanned "unsupported attribute" message if any attribute is
/// present, pointing at the first one.
pub fn reject_non_empty_attributes(attributes: &[Attribute]) -> syn::Result<()> {
if let Some(attribute) = attributes.first() {
Err(Error::new(
attribute.span(),
format!(
"unsupported attribute: {}",
attribute.path().to_token_stream()
),
))
} else {
Ok(())
}
}
5 changes: 5 additions & 0 deletions crates/macros/cgp-macro-core/src/functions/shape/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod parse_fields;
mod validate_fields;

pub use parse_fields::*;
pub use validate_fields::*;
99 changes: 99 additions & 0 deletions crates/macros/cgp-macro-core/src/functions/shape/parse_fields.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use syn::ext::IdentExt;
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{
Attribute, Error, Field, Fields, FieldsNamed, FieldsUnnamed, Ident, Lit, Token, Visibility,
};

use crate::functions::validate_shape_fields;

/// Parse the body of a `Struct!` into the `syn::Fields` a struct declaration would hold.
///
/// A proc macro never sees the delimiter it was invoked with, so the form is read from the
/// entries: `name: Type` entries make a named body, and bare types make a tuple body. An empty
/// body is `Fields::Unit`. The fields are validated with [`validate_shape_fields`].
pub fn parse_shape_fields(input: ParseStream) -> syn::Result<Fields> {
let mut named_form = None;
let mut named = Punctuated::<Field, Comma>::new();
let mut unnamed = Punctuated::<Field, Comma>::new();

while !input.is_empty() {
let entry_is_named = peek_named_entry(input)?;

match named_form {
None => named_form = Some(entry_is_named),
Some(form) if form != entry_is_named => {
return Err(Error::new(
input.span(),
"cannot mix named and positional fields: write every entry as \
`name: Type`, or every entry as a bare type",
));
}
Some(_) => {}
}

if entry_is_named {
named.push_value(Field::parse_named(input)?);
} else {
unnamed.push_value(Field::parse_unnamed(input)?);
}

if input.is_empty() {
break;
}

let comma: Comma = input.parse()?;

if entry_is_named {
named.push_punct(comma);
} else {
unnamed.push_punct(comma);
}
}

let fields = match named_form {
None => Fields::Unit,
Some(true) => Fields::Named(FieldsNamed {
brace_token: Default::default(),
named,
}),
Some(false) => Fields::Unnamed(FieldsUnnamed {
paren_token: Default::default(),
unnamed,
}),
};

validate_shape_fields(&fields)?;

Ok(fields)
}

/// Whether the next entry is a `name: Type` field, looking past any attributes and visibility
/// so that those reach their own rejection. A single `:` decides it: `syn` peeks `Token![:]` on
/// the first half of a `::` too, so a path type such as `core::marker::PhantomData<u8>` is ruled
/// out explicitly. Also rejects a literal where a type belongs, the likely mistake of reading
/// `Struct! { a: 1 }` as a struct literal.
fn peek_named_entry(input: ParseStream) -> syn::Result<bool> {
let fork = input.fork();
fork.call(Attribute::parse_outer)?;
fork.parse::<Visibility>()?;

let is_named = (fork.peek(Ident) || fork.peek(Token![_]))
&& fork.peek2(Token![:])
&& !fork.peek2(Token![::]);

if is_named {
fork.call(Ident::parse_any)?;
fork.parse::<Token![:]>()?;
}

if fork.peek(Lit) {
return Err(Error::new(
fork.span(),
"expected a type: a type-level shape lists field types, not values",
));
}

Ok(is_named)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use syn::ext::IdentExt;
use syn::{Error, Fields, Visibility};

use crate::functions::reject_non_empty_attributes;

/// Reject the parts of a struct or variant body that a type-level shape has no use for: field
/// attributes, field visibility, the `_` field name, and a field name given twice.
///
/// `syn` parses all of these as part of an ordinary struct body, so a `Struct!` or `Enum!` body
/// gets through its parser and must be checked here. Names are compared with any `r#` removed,
/// since `r#a` and `a` produce the same `Symbol!("a")` tag.
pub fn validate_shape_fields(fields: &Fields) -> syn::Result<()> {
let mut seen = Vec::new();

for field in fields {
reject_non_empty_attributes(&field.attrs)?;

if !matches!(field.vis, Visibility::Inherited) {
return Err(Error::new_spanned(
&field.vis,
"a field of a type-level shape has no visibility; remove it",
));
}

if let Some(ident) = &field.ident {
let name = ident.unraw().to_string();

if name == "_" {
return Err(Error::new(
ident.span(),
"a field of a type-level shape must be named; `_` is not allowed",
));
}

if seen.contains(&name) {
return Err(Error::new(
ident.span(),
format!("duplicate field `{name}`"),
));
}

seen.push(name);
}
}

Ok(())
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{Attribute, Error};

use crate::functions::reject_non_empty_attributes;
use crate::types::delegate_component::{
DelegateEntries, DelegateKey, DelegateMapping, DelegateStatement, DelegateTable, DelegateValue,
DelegateValueWithInnerTable, ForDelegateStatement, MultiDelegateKey, PathDelegateKey,
Expand All @@ -17,23 +14,6 @@ pub trait ValidateAttributes {
fn validate_attributes(&self) -> syn::Result<()>;
}

/// Error with a spanned "unsupported attribute" message if any attribute is
/// present, pointing at the first one.
pub fn reject_non_empty_attributes(attributes: &[Attribute]) -> syn::Result<()> {
if !attributes.is_empty() {
let attribute = &attributes[0];
Err(Error::new(
attribute.span(),
format!(
"unsupported attribute: {}",
attribute.path().to_token_stream()
),
))
} else {
Ok(())
}
}

impl ValidateAttributes for SingleDelegateKey {
fn validate_attributes(&self) -> syn::Result<()> {
reject_non_empty_attributes(&self.attributes)
Expand Down
1 change: 1 addition & 0 deletions crates/macros/cgp-macro-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ pub mod namespace;
pub mod path;
pub mod product;
pub mod provider_impl;
pub mod shape;
pub mod sum;
99 changes: 99 additions & 0 deletions crates/macros/cgp-macro-core/src/types/shape/enum_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use proc_macro2::TokenStream;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::{Brace, Comma, Paren};
use syn::{Attribute, Error, Fields, Ident, Token, Type, Variant, Visibility};

use crate::functions::{reject_non_empty_attributes, validate_shape_fields};
use crate::types::cgp_data::variants_to_sum_type;

/// The `Enum!` macro: the body of an enum, held as the `syn::Variant` list the enum
/// declaration would have.
///
/// As with [`StructType`](super::StructType), holding the derive's input form lets
/// [`eval`](Self::eval) run the `#[derive(HasFields)]` encoder unchanged.
pub struct EnumType {
pub variants: Punctuated<Variant, Comma>,
}

impl EnumType {
/// Encode the variants as the derive does: a sum of `Field<Symbol!("Variant"), Payload>`
/// entries ending in `Void`, each payload encoded by the `Struct!` rules.
pub fn eval(&self) -> syn::Result<Type> {
variants_to_sum_type(&self.variants, &TokenStream::new())
}
}

impl Parse for EnumType {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut variants = Punctuated::new();
let mut seen = Vec::new();

while !input.is_empty() {
let variant = parse_variant(input)?;

let name = variant.ident.unraw().to_string();
if seen.contains(&name) {
return Err(Error::new(
variant.ident.span(),
format!("duplicate variant `{name}`"),
));
}
seen.push(name);

variants.push_value(variant);

if input.is_empty() {
break;
}

variants.push_punct(input.parse()?);
}

Ok(Self { variants })
}
}

/// Parse one variant. `syn`'s own `Variant` parser silently discards a visibility and accepts
/// a discriminant, so the variant is read here, rejecting both, and its fields are parsed with
/// `syn`'s struct-body parsers. Unlike the `Struct!` body, a variant's delimiter is visible, so
/// braces hold named fields and parentheses positional ones, as in a Rust enum.
fn parse_variant(input: ParseStream) -> syn::Result<Variant> {
let attrs = input.call(Attribute::parse_outer)?;
reject_non_empty_attributes(&attrs)?;

if input.peek(Token![pub]) {
let visibility: Visibility = input.parse()?;
return Err(Error::new_spanned(
visibility,
"an `Enum!` variant has no visibility; remove it",
));
}

let ident: Ident = input.parse()?;

let fields = if input.peek(Brace) {
Fields::Named(input.parse()?)
} else if input.peek(Paren) {
Fields::Unnamed(input.parse()?)
} else {
Fields::Unit
};

validate_shape_fields(&fields)?;

if input.peek(Token![=]) {
return Err(Error::new(
input.span(),
"an `Enum!` variant has no discriminant; remove the `= …`",
));
}

Ok(Variant {
attrs: Vec::new(),
ident,
fields,
discriminant: None,
})
}
5 changes: 5 additions & 0 deletions crates/macros/cgp-macro-core/src/types/shape/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod enum_type;
mod struct_type;

pub use enum_type::*;
pub use struct_type::*;
32 changes: 32 additions & 0 deletions crates/macros/cgp-macro-core/src/types/shape/struct_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use proc_macro2::TokenStream;
use syn::parse::{Parse, ParseStream};
use syn::{Fields, Type};

use crate::functions::parse_shape_fields;
use crate::types::cgp_data::item_fields_to_product_type;

/// The `Struct!` macro: the body of a named or tuple struct, held as the `syn::Fields` the
/// struct declaration would have.
///
/// Holding the derive's own input form lets [`eval`](Self::eval) run the `#[derive(HasFields)]`
/// encoder unchanged, so `Struct! { … }` is by construction the type a derived `Fields` is.
pub struct StructType {
pub fields: Fields,
}

impl StructType {
/// Encode the fields as the derive does: a product of `Field` entries, keyed by `Symbol!`
/// for named fields and by `Index<N>` for positional ones, with a one-field tuple body
/// encoding to its bare type and an empty body to `Nil`.
pub fn eval(&self) -> syn::Result<Type> {
item_fields_to_product_type(&self.fields, &TokenStream::new())
}
}

impl Parse for StructType {
fn parse(input: ParseStream) -> syn::Result<Self> {
let fields = parse_shape_fields(input)?;

Ok(Self { fields })
}
}
Loading
Loading