From 577c02c9a45a30f9f5fbf14c5b4a536d528f3a80 Mon Sep 17 00:00:00 2001 From: Overlord Date: Wed, 4 Mar 2026 18:59:50 +0100 Subject: [PATCH] Introduce `pmp-macro-misc` crate: define shared types, traits, and metadata for macro registration and handling --- .../crates/pmp-macro-misc/Cargo.toml | 11 ++ .../crates/pmp-macro-misc/src/context.rs | 45 ++++++ .../crates/pmp-macro-misc/src/descriptor.rs | 36 +++++ .../crates/pmp-macro-misc/src/lib.rs | 20 +++ .../crates/pmp-macro-misc/src/target.rs | 39 +++++ .../crates/pmp-macro-misc/src/traits.rs | 143 ++++++++++++++++++ 6 files changed, 294 insertions(+) create mode 100644 crates/pmp-macro/crates/pmp-macro-misc/Cargo.toml create mode 100644 crates/pmp-macro/crates/pmp-macro-misc/src/context.rs create mode 100644 crates/pmp-macro/crates/pmp-macro-misc/src/descriptor.rs create mode 100644 crates/pmp-macro/crates/pmp-macro-misc/src/lib.rs create mode 100644 crates/pmp-macro/crates/pmp-macro-misc/src/target.rs create mode 100644 crates/pmp-macro/crates/pmp-macro-misc/src/traits.rs diff --git a/crates/pmp-macro/crates/pmp-macro-misc/Cargo.toml b/crates/pmp-macro/crates/pmp-macro-misc/Cargo.toml new file mode 100644 index 0000000..d5fdc2c --- /dev/null +++ b/crates/pmp-macro/crates/pmp-macro-misc/Cargo.toml @@ -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" diff --git a/crates/pmp-macro/crates/pmp-macro-misc/src/context.rs b/crates/pmp-macro/crates/pmp-macro-misc/src/context.rs new file mode 100644 index 0000000..12b61c9 --- /dev/null +++ b/crates/pmp-macro/crates/pmp-macro-misc/src/context.rs @@ -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; diff --git a/crates/pmp-macro/crates/pmp-macro-misc/src/descriptor.rs b/crates/pmp-macro/crates/pmp-macro-misc/src/descriptor.rs new file mode 100644 index 0000000..b84ae8c --- /dev/null +++ b/crates/pmp-macro/crates/pmp-macro-misc/src/descriptor.rs @@ -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], +} diff --git a/crates/pmp-macro/crates/pmp-macro-misc/src/lib.rs b/crates/pmp-macro/crates/pmp-macro-misc/src/lib.rs new file mode 100644 index 0000000..0a3cca8 --- /dev/null +++ b/crates/pmp-macro/crates/pmp-macro-misc/src/lib.rs @@ -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 }; diff --git a/crates/pmp-macro/crates/pmp-macro-misc/src/target.rs b/crates/pmp-macro/crates/pmp-macro-misc/src/target.rs new file mode 100644 index 0000000..134513a --- /dev/null +++ b/crates/pmp-macro/crates/pmp-macro-misc/src/target.rs @@ -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; + } +} diff --git a/crates/pmp-macro/crates/pmp-macro-misc/src/traits.rs b/crates/pmp-macro/crates/pmp-macro-misc/src/traits.rs new file mode 100644 index 0000000..437a6e4 --- /dev/null +++ b/crates/pmp-macro/crates/pmp-macro-misc/src/traits.rs @@ -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` 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 { + 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 { + 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 = 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::` 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:: + { + 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 { + 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() + } +}