Introduce pmp-macro-misc crate: define shared types, traits, and metadata for macro registration and handling

This commit is contained in:
2026-03-04 18:59:50 +01:00
parent 6863d7377b
commit 577c02c9a4
6 changed files with 294 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "pmp-macro-misc"
version = "1.0.0"
edition.workspace = true
authors.workspace = true
license-file.workspace = true
[dependencies]
bitflags = "^2"
inventory = "^0"

View File

@@ -0,0 +1,45 @@
// ---------------------------------------------------------------------------------------------- //
// Mago-facing placeholders //
// //
// These types will wrap or re-export mago's AST / symbol / source types once the //
// integration surface is understood. For now they are empty structs so the trait //
// and derive can be written against concrete type names without a mago dependency. //
// ---------------------------------------------------------------------------------------------- //
/// Global preprocessor state passed to every handler invocation.
///
/// Will hold references to mago's parsed file set, resolved symbol table,
/// and any cross-file index needed for macros like `#[Sealed]` that must
/// scan the entire project.
///
/// TODO: replace with real mago types.
#[non_exhaustive]
pub struct MacroContext;
/// The PHP AST node that carries the attribute being processed, together
/// with the specific attribute instance (which macro it is, its position).
///
/// TODO: replace with real mago node types.
#[non_exhaustive]
pub struct AttributedNode;
/// The parsed and type-resolved arguments supplied to the attribute.
///
/// e.g. for `#[Sealed([Success::class, Failure::class])]` this would
/// expose the two resolved class names as typed Rust values.
///
/// TODO: replace with real resolved argument types.
#[non_exhaustive]
pub struct ResolvedAttr;
/// A single source-level diagnostic emitted by a macro handler.
///
/// TODO: decide between a custom type or wrapping mago's / miette's diagnostic type.
#[non_exhaustive]
pub struct Diagnostic;
/// A rewrite instruction produced by `PmpMacro::transform`.
///
/// TODO: decide between text-span replacement or typed AST mutation depending on what mago exposes.
#[non_exhaustive]
pub struct Rewrite;

View File

@@ -0,0 +1,36 @@
use crate::target::Target;
/// Describes a single constructor parameter of a PHP macro attribute.
///
/// All fields are `&'static str` so the descriptor can be embedded as a
/// `static` and referenced from the inventory without allocation.
#[derive(Debug, Clone, Copy)]
pub struct ParamDescriptor {
/// The parameter name as it appears in the PHP constructor.
pub name: &'static str,
/// A human-readable PHP type hint, e.g. `"class-string[]"` or `"bool"`.
pub php_type: &'static str,
/// Whether the parameter must be supplied by the caller.
pub required: bool,
/// The PHP source representation of the default value, e.g. `"[]"` or `"false"`.
/// `None` when `required` is `true`.
pub default: Option<&'static str>,
}
/// Static metadata for a registered PHP macro attribute.
///
/// Generated by `#[derive(PmpMacro)]` and stored as a `&'static MacroDescriptor`
/// in the inventory. Entirely allocation-free.
#[derive(Debug)]
pub struct MacroDescriptor {
/// The PHP attribute name, e.g. `"Sealed"`. Used to match against
/// attribute usages in parsed PHP files.
pub name: &'static str,
/// Which PHP constructs this attribute is permitted to annotate.
pub target: Target,
/// Human-readable description of what this macro does.
/// Used for error messages and generated documentation.
pub docs: &'static str,
/// The constructor parameters this attribute accepts.
pub params: &'static [ParamDescriptor],
}

View File

@@ -0,0 +1,20 @@
//! Shared types, traits, and registration machinery for `pmp-macro`.
//!
//! # Crate layout
//!
//! | Module | Contents |
//! |--------------|-----------------------------------------------------------------|
//! | `target` | [`Target`] bitflags for PHP attribute target sites |
//! | `descriptor` | [`MacroDescriptor`] and [`ParamDescriptor`] - static metadata |
//! | `context` | [`MacroContext`], [`AttributedNode`], [`ResolvedAttr`], etc. |
//! | `traits` | [`PmpMacro`] trait, [`MacroRegistration`], [`validate_registry`]|
mod target;
mod traits;
mod context;
mod descriptor;
pub use target::Target;
pub use descriptor::{ MacroDescriptor, ParamDescriptor };
pub use traits::{ PmpMacro, MacroRegistry, MacroRegistration, };
pub use context::{ Rewrite, ResolvedAttr, Diagnostic, MacroContext, AttributedNode };

View File

@@ -0,0 +1,39 @@
use bitflags::bitflags;
bitflags!
{
/// The PHP construct(s) that a macro attribute is permitted to annotate.
///
/// Mirrors PHP's `Attribute::TARGET_*` constants. A macro may target
/// multiple sites by OR-ing flags together:
///
/// ```
/// let target = Target::CLASS | Target::INTERFACE;
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Target: u16
{
const CLASS = 1 << 0;
const INTERFACE = 1 << 1;
const TRAIT = 1 << 2;
const ENUM = 1 << 3;
const METHOD = 1 << 4;
const FUNCTION = 1 << 5;
const PROPERTY = 1 << 6;
const CONSTANT = 1 << 7;
const PARAMETER = 1 << 8;
/// Convenience: any class-like construct.
const ANY_CLASS_LIKE = Self::CLASS.bits()
| Self::INTERFACE.bits()
| Self::TRAIT.bits()
| Self::ENUM.bits();
/// Convenience: any callable construct.
const ANY_CALLABLE = Self::METHOD.bits()
| Self::FUNCTION.bits();
/// Convenience: all targets.
const ALL = u16::MAX;
}
}

View File

@@ -0,0 +1,143 @@
use std::{
sync::OnceLock,
collections::HashMap
};
use crate::{
descriptor::MacroDescriptor,
context::{ Rewrite, ResolvedAttr, Diagnostic, MacroContext, AttributedNode }
};
// --------------------------------------------------------------------------------------------- //
/// The core trait every PHP macro attribute handler must implement.
///
/// The `descriptor` method is generated by `#[derive(PmpMacro)]` and should
/// not be written by hand. The two handler methods have default no-op
/// implementations so that analysis-only and transform-only macros only need
/// to override what they actually do.
///
/// The trait is object-safe (`&self`, no generics) so the registry can store
/// `Box<dyn PmpMacro>` without knowing concrete types.
pub trait PmpMacro: Send + Sync {
// --- generated by #[derive(PmpMacro)] --------------------------------------------------- //
/// Returns the static metadata for this macro.
/// Must not be implemented by hand - use `#[derive(PmpMacro)]`.
fn descriptor(&self) -> &'static MacroDescriptor;
// --- user-implemented handler hooks ----------------------------------------------------- //
/// Analyse the attributed PHP node and emit diagnostics.
///
/// Called during the analysis pass. Returning an empty `Vec` means no
/// issues were found. The default implementation is a no-op.
#[allow(unused_variables)]
fn analyze(
&self,
ctx: &MacroContext,
attr: &ResolvedAttr,
node: &AttributedNode,
) -> Vec<Diagnostic> {
vec![]
}
/// Produce rewrite instructions for the attributed PHP node.
///
/// Called during the transform pass, after all analysis is complete.
/// The default implementation is a no-op.
#[allow(unused_variables)]
fn transform(
&self,
ctx: &MacroContext,
attr: &ResolvedAttr,
node: &AttributedNode,
) -> Vec<Rewrite> {
vec![]
}
}
// --------------------------------------------------------------------------------------------- //
/// An entry in the global macro registry, submitted via `inventory::submit!`.
///
/// Wraps a `&'static dyn PmpMacro` so the inventory iterator can hand out
/// references to every registered handler without allocation.
pub struct MacroRegistration {
pub handler: &'static dyn PmpMacro,
}
impl MacroRegistration
{
pub const fn new(handler: &'static dyn PmpMacro) -> Self {
Self { handler }
}
}
inventory::collect!(MacroRegistration);
// --------------------------------------------------------------------------------------------- //
/// Validated, deduplicated view of the global macro registry.
///
/// Built once on first access and then cached for the lifetime of the process.
pub struct MacroRegistry {
/// Handlers keyed by attribute name for O(1) lookup during processing.
handlers: HashMap<&'static str, &'static dyn PmpMacro>,
}
static REGISTRY: OnceLock<MacroRegistry> = OnceLock::new();
impl MacroRegistry
{
/// Returns the global macro registry, validating and building it on first call.
///
/// Deduplication is checked here — if two macros share the same attribute name
/// this will panic with a descriptive message. Subsequent calls are free
/// (just an atomic load from the `OnceLock`).
///
/// This is the only intended way to access registered macros. Direct use of
/// `inventory::iter::<MacroRegistration>` bypasses dedup and should be avoided.
pub fn registry() -> &'static MacroRegistry {
REGISTRY.get_or_init(MacroRegistry::build)
}
fn build() -> Self
{
let mut handlers: HashMap<&'static str, &'static dyn PmpMacro> = HashMap::new();
for reg in inventory::iter::<MacroRegistration>
{
let name = reg.handler.descriptor().name;
if handlers.insert(name, reg.handler).is_some()
{
panic!(
"duplicate PmpMacro registration: attribute name \"{name}\" is \
registered more than once - check your macro definitions"
);
}
}
Self { handlers }
}
/// Look up a registered macro handler by its PHP attribute name.
pub fn get(&self, name: &str) -> Option<&'static dyn PmpMacro> {
self.handlers.get(name).copied()
}
/// Iterate over all registered handlers.
pub fn iter(&self) -> impl Iterator<Item = &'static dyn PmpMacro> {
self.handlers.values().copied()
}
/// The number of registered macros.
pub fn len(&self) -> usize {
self.handlers.len()
}
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
}