Introduce pmp-macro-core crate: implement attribute parsing, macro registration, and boilerplate generation for PmpMacro trait.

This commit is contained in:
2026-03-05 08:22:28 +01:00
parent 431fe30e34
commit 36b065ddb2
9 changed files with 271 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
use darling::{ FromMeta }; use darling::FromMeta;
/// Implemented by any item that carries a `Vec<syn::Attribute>`. /// Implemented by any item that carries a `Vec<syn::Attribute>`.
/// ///

View File

@@ -65,3 +65,21 @@ impl FromMeta for StrictPath
Path::from_expr(expr).map(StrictPath) Path::from_expr(expr).map(StrictPath)
} }
} }
/// A Rust expression that must be written as `= <expr>`
#[derive(Debug, Clone)]
pub struct StrictExpr(pub syn::Expr);
impl FromMeta for StrictExpr
{
fn from_word() -> Result<Self>
{
Err(Error::custom(
"expected `= <expr>`, provide an expression, e.g. `target = Target::CLASS | Target::INTERFACE`"
))
}
fn from_expr(expr: &syn::Expr) -> Result<Self> {
Ok(StrictExpr(expr.clone()))
}
}

View File

@@ -0,0 +1,11 @@
[package]
name = "pmp-macro"
version = "1.0.0"
edition.workspace = true
authors.workspace = true
license-file.workspace = true
[dependencies]
pmp-macro-core = { path = "crates/pmp-macro-core" }
pmp-macro-misc = { path = "crates/pmp-macro-misc" }

View File

@@ -0,0 +1,19 @@
[package]
name = "pmp-macro-core"
version = "1.0.0"
edition.workspace = true
authors.workspace = true
license-file.workspace = true
[lib]
proc-macro = true
[dependencies]
pmp-core = { path = "../../../pmp-core" }
pmp-macro-misc = { path = "../pmp-macro-misc" }
syn = { version = "^2", features = ["full"] }
quote = "^1"
darling = "^0"
proc-macro2 = "^1"

View File

@@ -0,0 +1,49 @@
use pmp_core::{ StrictBool, StrictExpr, StrictString };
use darling::{ ast, FromField, FromDeriveInput };
use syn::{ Type, Ident };
/// - `name = "..."` - PHP attribute name (default: struct name)
/// - `target = <expr>` - which PHP constructs may use this attribute (default: Target::ALL)
/// - `docs = "..."` - description for diagnostics and generated docs
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(pmp_macro), forward_attrs(allow, doc, cfg))]
pub struct MacroInput {
pub ident: Ident,
pub data: ast::Data<darling::util::Ignored, FieldInput>,
#[darling(default)]
pub name: Option<StrictString>,
#[darling(default)]
pub target: Option<StrictExpr>,
#[darling(default)]
pub docs: Option<StrictString>,
}
/// - `php_type = "..."` - PHP type hint, e.g. `"class-string[]"` (required)
/// - `default = "..."` - PHP default value, e.g. `"[]"`
/// - `required = bool` - overrides required inference
#[derive(Debug, FromField)]
#[darling(attributes(param))]
pub struct FieldInput {
pub ident: Option<Ident>,
#[allow(dead_code)]
pub ty: Type,
#[darling(default)]
pub php_type: Option<StrictString>,
#[darling(default)]
pub default: Option<StrictString>,
#[darling(default)]
pub required: Option<StrictBool>,
}
impl FieldInput
{
pub fn is_param(&self) -> bool {
self.php_type.is_some()
}
pub fn is_required(&self) -> bool {
self.required.map(|b| b.0).unwrap_or(self.default.is_none())
}
}

View File

@@ -0,0 +1,112 @@
use quote::{ format_ident, quote, quote_spanned };
use proc_macro2::TokenStream as TokenStream2;
use crate::attrs::MacroInput;
pub fn expand(parsed: MacroInput) -> syn::Result<TokenStream2>
{
let struct_ident = &parsed.ident;
let fields = parsed.data.take_struct().ok_or_else(|| {
syn::Error::new_spanned(struct_ident, "PmpMacro can only be derived for structs")
})?;
let attr_name: String = parsed.name
.map(|s| s.0)
.unwrap_or_else(|| struct_ident.to_string());
let target_expr: syn::Expr = parsed.target
.map(|e| e.0)
.unwrap_or_else(|| syn::parse_quote! { ::pmp_macro_misc::Target::ALL });
let docs: String = parsed.docs.map(|s| s.0).unwrap_or_default();
let mut param_tokens: Vec<TokenStream2> = Vec::new();
let mut errors: Option<syn::Error> = None;
for field in fields.iter().filter(|f| f.is_param())
{
let span = field.ident.as_ref().map(|i| i.span()).unwrap_or_else(|| struct_ident.span());
let field_name = field.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
let php_type = match &field.php_type
{
Some(s) => s.0.clone(),
None =>
{
let e = syn::Error::new(
span,
format!("field `{field_name}` has #[param] but is missing `php_type = \"...\"`"),
);
match &mut errors {
None => errors = Some(e),
Some(existing) => existing.combine(e),
}
continue;
}
};
let required = field.is_required();
let default_tokens = match field.default.as_ref().map(|s| s.0.as_str()) {
Some(d) => quote! { Some(#d) },
None => quote! { None },
};
param_tokens.push(quote_spanned! { span =>
::pmp_macro_misc::ParamDescriptor {
name: #field_name,
php_type: #php_type,
required: #required,
default: #default_tokens,
}
});
}
if let Some(e) = errors { return Err(e); }
let mod_ident = format_ident!("__pmp_macro_impl_{}", struct_ident);
let handler = quote! { super::#struct_ident };
Ok(quote!
{
#[doc(hidden)]
mod #mod_ident
{
use ::pmp_macro_misc::{
AnalyzeFn, MacroDescriptor, MacroRegistration, ParamDescriptor,
TransformFn, PmpMacro,
};
static PARAMS: &[ParamDescriptor] = &[ #(#param_tokens),* ];
static DESCRIPTOR: MacroDescriptor = MacroDescriptor {
name: #attr_name,
target: #target_expr,
docs: #docs,
params: PARAMS,
};
fn analyze(
ctx: &::pmp_macro_misc::MacroContext,
attr: &::pmp_macro_misc::ResolvedAttr,
node: &::pmp_macro_misc::AttributedNode,
) -> ::std::vec::Vec<::pmp_macro_misc::Diagnostic>
{
<#handler as PmpMacro>::analyze(&::std::default::Default::default(), ctx, attr, node)
}
fn transform(
ctx: &::pmp_macro_misc::MacroContext,
attr: &::pmp_macro_misc::ResolvedAttr,
node: &::pmp_macro_misc::AttributedNode,
) -> ::std::vec::Vec<::pmp_macro_misc::Rewrite>
{
<#handler as PmpMacro>::transform(&::std::default::Default::default(), ctx, attr, node)
}
::inventory::submit! {
MacroRegistration::new(&DESCRIPTOR, analyze, transform)
}
}
})
}

View File

@@ -0,0 +1,55 @@
use darling::FromDeriveInput;
use proc_macro::TokenStream;
use syn::DeriveInput;
mod attrs;
mod expand;
use attrs::MacroInput;
/// Derives registration boilerplate for a PHP macro attribute handler.
///
/// #### Struct-level attributes (`#[pmp_macro(...)]`)
///
/// | Key | Type | Default | Description |
/// |------------|---------------|------------------|----------------------------------------------|
/// | `name` | `= "..."` | Struct name | PHP attribute name to match against |
/// | `target` | `= <expr>` | `Target::ALL` | Which PHP constructs may use this attribute |
/// | `docs` | `= "..."` | `""` | Description for diagnostics and generated docs |
///
/// #### Field-level attributes (`#[param(...)]`)
///
/// Fields without `#[param(...)]` are ignored - they are treated as handler
/// implementation details, not PHP constructor parameters.
///
/// | Key | Type | Default | Description |
/// |------------|-----------|-------------------|------------------------------------------|
/// | `php_type` | `= "..."` | *(required)* | PHP type hint, e.g. `"class-string[]"` |
/// | `default` | `= "..."` | `None` | PHP default value, e.g. `"[]"` |
/// | `required` | `= bool` | inferred | Overrides required inference |
///
/// #### What is generated
///
/// - A hidden module containing a `static MacroDescriptor` and `static [ParamDescriptor]`
/// - Two free functions delegating to the handler's `PmpMacro::analyze` / `::transform`
/// - An `inventory::submit!` registration wiring it all together
///
/// #### Requirements
///
/// - The struct must implement [`pmp_macro_misc::PmpMacro`] (user-written)
/// - The struct must implement [`Default`] (needed to construct a handler instance
/// inside the generated delegation functions)
#[proc_macro_derive(PmpMacro, attributes(pmp_macro, param))]
pub fn pmp_macro_derive(item: TokenStream) -> TokenStream
{
let input = syn::parse_macro_input!(item as DeriveInput);
let parsed = match MacroInput::from_derive_input(&input) {
Ok(p) => p,
Err(e) => return e.write_errors().into(),
};
expand::expand(parsed)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}

View File

@@ -5,7 +5,7 @@ use std::{
use crate::{ use crate::{
descriptor::MacroDescriptor, descriptor::MacroDescriptor,
context::{ Rewrite, ResolvedAttr, Diagnostic, MacroContext, AttributedNode } context::{ Rewrite, Diagnostic, ResolvedAttr, MacroContext, AttributedNode }
}; };
// --------------------------------------------------------------------------------------------- // // --------------------------------------------------------------------------------------------- //

View File

@@ -0,0 +1,5 @@
#[doc(hidden)]
pub extern crate pmp_macro_misc;
pub use pmp_macro_misc::*;
pub use pmp_macro_core::*;