Introduce pmp-macro-core crate: implement attribute parsing, macro registration, and boilerplate generation for PmpMacro trait.
This commit is contained in:
11
crates/pmp-macro/Cargo.toml
Normal file
11
crates/pmp-macro/Cargo.toml
Normal 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" }
|
||||
19
crates/pmp-macro/crates/pmp-macro-core/Cargo.toml
Normal file
19
crates/pmp-macro/crates/pmp-macro-core/Cargo.toml
Normal 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"
|
||||
49
crates/pmp-macro/crates/pmp-macro-core/src/attrs.rs
Normal file
49
crates/pmp-macro/crates/pmp-macro-core/src/attrs.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
112
crates/pmp-macro/crates/pmp-macro-core/src/expand.rs
Normal file
112
crates/pmp-macro/crates/pmp-macro-core/src/expand.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
55
crates/pmp-macro/crates/pmp-macro-core/src/lib.rs
Normal file
55
crates/pmp-macro/crates/pmp-macro-core/src/lib.rs
Normal 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()
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use crate::{
|
||||
descriptor::MacroDescriptor,
|
||||
context::{ Rewrite, ResolvedAttr, Diagnostic, MacroContext, AttributedNode }
|
||||
context::{ Rewrite, Diagnostic, ResolvedAttr, MacroContext, AttributedNode }
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------------------------- //
|
||||
|
||||
5
crates/pmp-macro/src/lib.rs
Normal file
5
crates/pmp-macro/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[doc(hidden)]
|
||||
pub extern crate pmp_macro_misc;
|
||||
|
||||
pub use pmp_macro_misc::*;
|
||||
pub use pmp_macro_core::*;
|
||||
Reference in New Issue
Block a user