Introduce pmp-macro-core crate: implement attribute parsing, macro registration, and boilerplate generation for PmpMacro trait.
This commit is contained in:
@@ -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>`.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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::{
|
use crate::{
|
||||||
descriptor::MacroDescriptor,
|
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