facet_macros_impl/
plugin.rs

1//! Plugin system for facet derive macro.
2//!
3//! This module implements the plugin chain pattern that allows external crates
4//! to hook into `#[derive(Facet)]` and generate additional trait implementations.
5//!
6//! ## How it works
7//!
8//! 1. User writes `#[derive(Facet)]` with `#[facet(derive(Error))]`
9//! 2. `facet_macros` detects the `derive(...)` attribute
10//! 3. It chains to the first plugin: `::facet_error::__facet_derive!`
11//! 4. Each plugin adds itself to the chain and forwards to the next (or finalize)
12//! 5. `__facet_finalize!` parses ONCE and generates all code
13//!
14//! ## Plugin naming convention
15//!
16//! `#[facet(derive(Foo))]` maps to `::facet_foo::__facet_derive!`
17//! (lowercase the trait name, prefix with `facet_`)
18
19use crate::{Attribute, AttributeInner, FacetInner, IParse, Ident, ToTokenIter, TokenStream};
20use quote::quote;
21
22/// A plugin reference - either a simple name or a full path.
23///
24/// - `Error` → convention-based lookup (`::facet_error`)
25/// - `facet_miette::Diagnostic` → explicit path (`::facet_miette`)
26#[derive(Debug, Clone)]
27pub enum PluginRef {
28    /// Simple name like `Error` - uses convention `::facet_{snake_case}`
29    Simple(String),
30    /// Explicit path like `facet_miette::Diagnostic` - uses the crate part directly
31    Path {
32        /// The crate name (e.g., `facet_miette`)
33        crate_name: String,
34        /// The plugin/trait name (e.g., `Diagnostic`)
35        plugin_name: String,
36    },
37}
38
39impl PluginRef {
40    /// Get the crate path for this plugin reference.
41    pub fn crate_path(&self) -> TokenStream {
42        match self {
43            PluginRef::Simple(name) => {
44                let snake_case = to_snake_case(name);
45                let crate_name = format!("facet_{snake_case}");
46                let crate_ident = quote::format_ident!("{}", crate_name);
47                quote! { ::#crate_ident }
48            }
49            PluginRef::Path { crate_name, .. } => {
50                let crate_ident = quote::format_ident!("{}", crate_name);
51                quote! { ::#crate_ident }
52            }
53        }
54    }
55}
56
57/// Extract plugin references from `#[facet(derive(Plugin1, Plugin2, ...))]` attributes.
58///
59/// Supports both simple names and explicit paths:
60/// - `#[facet(derive(Error))]` → `PluginRef::Simple("Error")`
61/// - `#[facet(derive(facet_miette::Diagnostic))]` → `PluginRef::Path { crate_name: "facet_miette", plugin_name: "Diagnostic" }`
62pub fn extract_derive_plugins(attrs: &[Attribute]) -> Vec<PluginRef> {
63    let mut plugins = Vec::new();
64
65    for attr in attrs {
66        if let AttributeInner::Facet(facet_attr) = &attr.body.content {
67            for inner in facet_attr.inner.content.iter().map(|d| &d.value) {
68                if let FacetInner::Simple(simple) = inner
69                    && simple.key == "derive"
70                {
71                    // Parse the args to get plugin names
72                    if let Some(ref args) = simple.args {
73                        match args {
74                            crate::AttrArgs::Parens(parens) => {
75                                // Parse comma-separated items (either idents or paths)
76                                plugins.extend(parse_plugin_list(&parens.content));
77                            }
78                            crate::AttrArgs::Equals(_) => {
79                                // derive = Something syntax (unusual but handle it)
80                            }
81                        }
82                    }
83                }
84            }
85        }
86    }
87
88    plugins
89}
90
91/// Parse a comma-separated list of plugin references (idents or paths).
92fn parse_plugin_list(tokens: &[crate::TokenTree]) -> Vec<PluginRef> {
93    let mut plugins = Vec::new();
94    let mut iter = tokens.iter().cloned().peekable();
95
96    while iter.peek().is_some() {
97        // Collect tokens until comma or end
98        let mut item_tokens = Vec::new();
99        while let Some(tt) = iter.peek() {
100            if let proc_macro2::TokenTree::Punct(p) = tt
101                && p.as_char() == ','
102            {
103                iter.next(); // consume comma
104                break;
105            }
106            item_tokens.push(iter.next().unwrap());
107        }
108
109        // Parse the collected tokens as either a simple ident or a path
110        if let Some(plugin_ref) = parse_plugin_ref(&item_tokens) {
111            plugins.push(plugin_ref);
112        }
113    }
114
115    plugins
116}
117
118/// Parse a single plugin reference from a sequence of tokens.
119fn parse_plugin_ref(tokens: &[proc_macro2::TokenTree]) -> Option<PluginRef> {
120    if tokens.is_empty() {
121        return None;
122    }
123
124    // Check if it's a path (contains ::)
125    let has_path_sep = tokens.windows(2).any(|w| {
126        matches!((&w[0], &w[1]),
127            (proc_macro2::TokenTree::Punct(p1), proc_macro2::TokenTree::Punct(p2))
128            if p1.as_char() == ':' && p2.as_char() == ':')
129    });
130
131    if has_path_sep {
132        // Parse as path: crate_name::PluginName
133        // For now, just support single-segment crate name
134        let mut iter = tokens.iter();
135
136        // First ident is crate name
137        let crate_name = match iter.next() {
138            Some(proc_macro2::TokenTree::Ident(id)) => id.to_string(),
139            _ => return None,
140        };
141
142        // Skip ::
143        match (iter.next(), iter.next()) {
144            (Some(proc_macro2::TokenTree::Punct(p1)), Some(proc_macro2::TokenTree::Punct(p2)))
145                if p1.as_char() == ':' && p2.as_char() == ':' => {}
146            _ => return None,
147        }
148
149        // Last ident is plugin name
150        let plugin_name = match iter.next() {
151            Some(proc_macro2::TokenTree::Ident(id)) => id.to_string(),
152            _ => return None,
153        };
154
155        Some(PluginRef::Path {
156            crate_name,
157            plugin_name,
158        })
159    } else {
160        // Simple ident
161        match tokens.first() {
162            Some(proc_macro2::TokenTree::Ident(id)) => Some(PluginRef::Simple(id.to_string())),
163            _ => None,
164        }
165    }
166}
167
168/// Convert a plugin name to its crate path.
169///
170/// `Error` → `::facet_error`
171/// `Display` → `::facet_display`
172pub fn plugin_to_crate_path(plugin_name: &str) -> TokenStream {
173    // Convert PascalCase to snake_case and prefix with facet_
174    let snake_case = to_snake_case(plugin_name);
175    let crate_name = format!("facet_{snake_case}");
176    let crate_ident = quote::format_ident!("{}", crate_name);
177    quote! { ::#crate_ident }
178}
179
180/// Convert PascalCase to snake_case.
181fn to_snake_case(s: &str) -> String {
182    let mut result = String::new();
183    for (i, c) in s.chars().enumerate() {
184        if c.is_uppercase() {
185            if i > 0 {
186                result.push('_');
187            }
188            result.push(c.to_ascii_lowercase());
189        } else {
190            result.push(c);
191        }
192    }
193    result
194}
195
196/// Strip `#[facet(derive(...))]` and plugin-specific attributes from a token stream.
197///
198/// This filters out the plugin-system-specific attributes before passing
199/// the tokens to the normal Facet processing, which would otherwise reject
200/// "derive" as an unknown attribute.
201///
202/// Currently strips:
203/// - `#[facet(derive(...))]` - plugin registration
204/// - `#[facet(error::from)]` - facet-error plugin attribute
205/// - `#[facet(error::source)]` - facet-error plugin attribute
206fn strip_derive_attrs(tokens: TokenStream) -> TokenStream {
207    let mut result = TokenStream::new();
208    let mut iter = tokens.into_iter().peekable();
209
210    while let Some(tt) = iter.next() {
211        // Check for # followed by [...]
212        if let proc_macro2::TokenTree::Punct(p) = &tt
213            && p.as_char() == '#'
214            && let Some(proc_macro2::TokenTree::Group(g)) = iter.peek()
215            && g.delimiter() == proc_macro2::Delimiter::Bracket
216        {
217            // This is an attribute - check if it's a plugin attribute
218            let inner = g.stream();
219            if is_plugin_attr(&inner) {
220                // Skip the # and the [...]
221                iter.next(); // consume the group
222                continue;
223            }
224        }
225        result.extend(std::iter::once(tt));
226    }
227
228    result
229}
230
231/// Check if an attribute is a plugin-specific attribute that should be stripped.
232///
233/// Returns true for:
234/// - `facet(derive(...))`
235/// - `facet(error::from)`
236/// - `facet(error::source)`
237/// - Any other `facet(namespace::key)` pattern (for future plugins)
238fn is_plugin_attr(inner: &TokenStream) -> bool {
239    let mut iter = inner.clone().into_iter();
240
241    // Check for "facet"
242    if let Some(proc_macro2::TokenTree::Ident(id)) = iter.next() {
243        if id != "facet" {
244            return false;
245        }
246    } else {
247        return false;
248    }
249
250    // Check for (...) containing plugin-specific attributes
251    if let Some(proc_macro2::TokenTree::Group(g)) = iter.next() {
252        if g.delimiter() != proc_macro2::Delimiter::Parenthesis {
253            return false;
254        }
255
256        let content = g.stream();
257        let mut content_iter = content.into_iter();
258
259        // Check the first identifier
260        if let Some(proc_macro2::TokenTree::Ident(id)) = content_iter.next() {
261            let first = id.to_string();
262
263            // Check for derive(...)
264            if first == "derive" {
265                return true;
266            }
267
268            // Check for namespace::key pattern (e.g., error::from, error::source)
269            if let Some(proc_macro2::TokenTree::Punct(p)) = content_iter.next()
270                && p.as_char() == ':'
271                && let Some(proc_macro2::TokenTree::Punct(p2)) = content_iter.next()
272                && p2.as_char() == ':'
273            {
274                // This is a namespace::key pattern - strip it
275                return true;
276            }
277        }
278    }
279
280    false
281}
282
283/// Check if an attribute's inner content is `facet(derive(...))`.
284#[deprecated(note = "use is_plugin_attr instead")]
285#[allow(dead_code)]
286fn is_facet_derive_attr(inner: &TokenStream) -> bool {
287    is_plugin_attr(inner)
288}
289
290/// Generate the plugin chain invocation.
291///
292/// If there are plugins, emits a chain starting with the first plugin.
293/// If no plugins, returns None (caller should proceed with normal codegen).
294pub fn generate_plugin_chain(
295    input_tokens: &TokenStream,
296    plugins: &[PluginRef],
297    facet_crate: &TokenStream,
298) -> Option<TokenStream> {
299    if plugins.is_empty() {
300        return None;
301    }
302
303    // Build the chain from right to left
304    // First plugin gets called with remaining plugins
305    let plugin_paths: Vec<TokenStream> = plugins
306        .iter()
307        .map(|p| {
308            let crate_path = p.crate_path();
309            quote! { #crate_path::__facet_invoke }
310        })
311        .collect();
312
313    let first = &plugin_paths[0];
314    let rest: Vec<_> = plugin_paths[1..].iter().collect();
315
316    let remaining = if rest.is_empty() {
317        quote! {}
318    } else {
319        quote! { #(#rest),* }
320    };
321
322    Some(quote! {
323        #first! {
324            @tokens { #input_tokens }
325            @remaining { #remaining }
326            @plugins { }
327            @facet_crate { #facet_crate }
328        }
329    })
330}
331
332/// Implementation of `__facet_finalize!` proc macro.
333///
334/// This is called at the end of the plugin chain. It:
335/// 1. Parses the type definition ONCE
336/// 2. Generates the base Facet impl
337/// 3. Evaluates each plugin's template against the parsed type
338pub fn facet_finalize(input: TokenStream) -> TokenStream {
339    // Parse the finalize invocation format:
340    // @tokens { ... }
341    // @plugins { @plugin { @name {...} @template {...} } ... }
342    // @facet_crate { ::facet }
343
344    let mut iter = input.to_token_iter();
345
346    let mut tokens: Option<TokenStream> = None;
347    let mut plugins_section: Option<TokenStream> = None;
348    let mut facet_crate: Option<TokenStream> = None;
349
350    // Parse sections
351    while let Ok(section) = iter.parse::<FinalizeSection>() {
352        match section.marker.name.to_string().as_str() {
353            "tokens" => {
354                tokens = Some(section.content.content);
355            }
356            "plugins" => {
357                plugins_section = Some(section.content.content);
358            }
359            "facet_crate" => {
360                facet_crate = Some(section.content.content);
361            }
362            other => {
363                let msg = format!("unknown section in __facet_finalize: @{other}");
364                return quote! { compile_error!(#msg); };
365            }
366        }
367    }
368
369    let tokens = match tokens {
370        Some(t) => t,
371        None => {
372            return quote! { compile_error!("__facet_finalize: missing @tokens section"); };
373        }
374    };
375
376    let facet_crate = facet_crate.unwrap_or_else(|| quote! { ::facet });
377
378    // Strip #[facet(derive(...))] attributes before processing
379    let filtered_tokens = strip_derive_attrs(tokens.clone());
380
381    // Parse the type and generate Facet impl
382    let mut type_iter = filtered_tokens.clone().to_token_iter();
383    let facet_impl = match type_iter.parse::<crate::Cons<crate::AdtDecl, crate::EndOfStream>>() {
384        Ok(it) => match it.first {
385            crate::AdtDecl::Struct(parsed) => crate::process_struct::process_struct(parsed),
386            crate::AdtDecl::Enum(parsed) => crate::process_enum::process_enum(parsed),
387        },
388        Err(err) => {
389            let msg = format!("__facet_finalize: could not parse type: {err}");
390            return quote! { compile_error!(#msg); };
391        }
392    };
393
394    // Extract and evaluate plugin templates
395    let plugin_impls = if let Some(plugins_tokens) = plugins_section {
396        // For now, just extract the templates - evaluation will come next
397        extract_plugin_templates(plugins_tokens, &filtered_tokens, &facet_crate)
398    } else {
399        vec![]
400    };
401
402    quote! {
403        #facet_impl
404        #(#plugin_impls)*
405    }
406}
407
408/// Represents a parsed plugin with its template
409struct PluginTemplate {
410    #[allow(dead_code)] // Will be used for debugging/diagnostics
411    name: String,
412    template: TokenStream,
413}
414
415/// Extract plugin templates from the @plugins section
416fn extract_plugin_templates(
417    plugins_tokens: TokenStream,
418    type_tokens: &TokenStream,
419    facet_crate: &TokenStream,
420) -> Vec<TokenStream> {
421    // Parse plugin sections
422    let plugins = parse_plugin_sections(plugins_tokens);
423
424    // Parse the type once for all plugins
425    let parsed_type = match facet_macro_parse::parse_type(type_tokens.clone()) {
426        Ok(ty) => ty,
427        Err(e) => {
428            let msg = format!("failed to parse type for plugin templates: {e}");
429            return vec![quote! { compile_error!(#msg); }];
430        }
431    };
432
433    // Evaluate each plugin's template
434    plugins
435        .into_iter()
436        .map(|plugin| evaluate_template(plugin.template, &parsed_type, facet_crate))
437        .collect()
438}
439
440/// Parse @plugin { @name {...} @template {...} } sections
441fn parse_plugin_sections(tokens: TokenStream) -> Vec<PluginTemplate> {
442    let mut plugins = Vec::new();
443    let mut iter = tokens.into_iter().peekable();
444
445    while let Some(tt) = iter.next() {
446        // Look for @plugin marker
447        if let proc_macro2::TokenTree::Punct(p) = &tt
448            && p.as_char() == '@'
449        {
450            // Next should be 'plugin' identifier
451            if let Some(proc_macro2::TokenTree::Ident(id)) = iter.peek()
452                && *id == "plugin"
453            {
454                iter.next(); // consume 'plugin'
455
456                // Next should be { ... } containing @name and @template
457                if let Some(proc_macro2::TokenTree::Group(g)) = iter.next()
458                    && g.delimiter() == proc_macro2::Delimiter::Brace
459                    && let Some(plugin) = parse_plugin_content(g.stream())
460                {
461                    plugins.push(plugin);
462                }
463            }
464        }
465    }
466
467    plugins
468}
469
470/// Parse the content of a @plugin { ... } section
471fn parse_plugin_content(tokens: TokenStream) -> Option<PluginTemplate> {
472    let mut name: Option<String> = None;
473    let mut template: Option<TokenStream> = None;
474    let mut iter = tokens.into_iter().peekable();
475
476    while let Some(tt) = iter.next() {
477        if let proc_macro2::TokenTree::Punct(p) = &tt
478            && p.as_char() == '@'
479            && let Some(proc_macro2::TokenTree::Ident(id)) = iter.peek()
480        {
481            let key = id.to_string();
482            iter.next(); // consume identifier
483
484            // Next should be { ... }
485            if let Some(proc_macro2::TokenTree::Group(g)) = iter.next()
486                && g.delimiter() == proc_macro2::Delimiter::Brace
487            {
488                match key.as_str() {
489                    "name" => {
490                        // Extract string literal from group
491                        let content = g.stream().into_iter().collect::<Vec<_>>();
492                        if let Some(proc_macro2::TokenTree::Literal(lit)) = content.first() {
493                            let s = lit.to_string();
494                            name = Some(s.trim_matches('"').to_string());
495                        }
496                    }
497                    "template" => {
498                        template = Some(g.stream());
499                    }
500                    _ => {}
501                }
502            }
503        }
504    }
505
506    match (name, template) {
507        (Some(n), Some(t)) => Some(PluginTemplate {
508            name: n,
509            template: t,
510        }),
511        _ => None,
512    }
513}
514
515/// Evaluate a template against a parsed type
516fn evaluate_template(
517    template: TokenStream,
518    parsed_type: &facet_macro_parse::PType,
519    _facet_crate: &TokenStream,
520) -> TokenStream {
521    let mut ctx = EvalContext::new(parsed_type);
522    evaluate_with_context(template, &mut ctx)
523}
524
525// ============================================================================
526// CONTEXT STACK
527// ============================================================================
528
529/// The evaluation context - a stack of nested scopes.
530///
531/// As we enter `@for_variant`, `@for_field`, `@if_attr`, etc., we push
532/// context frames. Directives like `@field_name` look up the stack to find
533/// the relevant context.
534struct EvalContext<'a> {
535    /// The parsed type we're generating code for
536    parsed_type: &'a facet_macro_parse::PType,
537
538    /// Stack of context frames (innermost last)
539    stack: Vec<ContextFrame<'a>>,
540}
541
542/// A single frame in the context stack
543enum ContextFrame<'a> {
544    /// We're inside a `@for_variant { ... }` loop
545    Variant {
546        variant: &'a facet_macro_parse::PVariant,
547    },
548
549    /// We're inside a `@for_field { ... }` loop
550    Field {
551        field: &'a facet_macro_parse::PStructField,
552        /// Index of this field (for tuple patterns like `__v0`, `__v1`)
553        index: usize,
554    },
555
556    /// We're inside a `@if_attr(ns::key) { ... }` block
557    Attr {
558        /// The matched attribute
559        attr: &'a facet_macro_parse::PFacetAttr,
560    },
561}
562
563impl<'a> EvalContext<'a> {
564    fn new(parsed_type: &'a facet_macro_parse::PType) -> Self {
565        Self {
566            parsed_type,
567            stack: Vec::new(),
568        }
569    }
570
571    fn push(&mut self, frame: ContextFrame<'a>) {
572        self.stack.push(frame);
573    }
574
575    fn pop(&mut self) {
576        self.stack.pop();
577    }
578
579    /// Find the current variant context (if any)
580    fn current_variant(&self) -> Option<&'a facet_macro_parse::PVariant> {
581        self.stack.iter().rev().find_map(|f| match f {
582            ContextFrame::Variant { variant } => Some(*variant),
583            _ => None,
584        })
585    }
586
587    /// Find the current field context (if any)
588    fn current_field(&self) -> Option<(&'a facet_macro_parse::PStructField, usize)> {
589        self.stack.iter().rev().find_map(|f| match f {
590            ContextFrame::Field { field, index } => Some((*field, *index)),
591            _ => None,
592        })
593    }
594
595    /// Find the current attr context (if any)
596    fn current_attr(&self) -> Option<&'a facet_macro_parse::PFacetAttr> {
597        self.stack.iter().rev().find_map(|f| match f {
598            ContextFrame::Attr { attr } => Some(*attr),
599            _ => None,
600        })
601    }
602
603    /// Get the fields of the current context (variant's fields or struct's fields)
604    fn current_fields(&self) -> Option<&'a [facet_macro_parse::PStructField]> {
605        // First check if we're in a variant
606        if let Some(variant) = self.current_variant() {
607            return match &variant.kind {
608                facet_macro_parse::PVariantKind::Tuple { fields } => Some(fields),
609                facet_macro_parse::PVariantKind::Struct { fields } => Some(fields),
610                facet_macro_parse::PVariantKind::Unit => None,
611            };
612        }
613
614        // Otherwise, check if we're in a struct
615        if let facet_macro_parse::PType::Struct(s) = self.parsed_type {
616            return match &s.kind {
617                facet_macro_parse::PStructKind::Struct { fields } => Some(fields),
618                facet_macro_parse::PStructKind::TupleStruct { fields } => Some(fields),
619                facet_macro_parse::PStructKind::UnitStruct => None,
620            };
621        }
622
623        None
624    }
625
626    /// Get the attrs of the current context (field, variant, or container)
627    fn current_attrs(&self) -> &'a facet_macro_parse::PAttrs {
628        // Check field first (most specific)
629        if let Some((field, _)) = self.current_field() {
630            return &field.attrs;
631        }
632
633        // Then variant
634        if let Some(variant) = self.current_variant() {
635            return &variant.attrs;
636        }
637
638        // Finally container
639        match self.parsed_type {
640            facet_macro_parse::PType::Struct(s) => &s.container.attrs,
641            facet_macro_parse::PType::Enum(e) => &e.container.attrs,
642        }
643    }
644}
645
646// ============================================================================
647// ATTRIBUTE QUERY
648// ============================================================================
649
650/// Parsed attribute query like `error::source` or `diagnostic::label`
651struct AttrQuery {
652    ns: String,
653    key: String,
654}
655
656impl AttrQuery {
657    /// Parse from tokens like `error::source` or `diagnostic::label`
658    fn parse(tokens: TokenStream) -> Option<Self> {
659        let mut iter = tokens.into_iter();
660
661        // First: namespace ident
662        let ns = match iter.next() {
663            Some(proc_macro2::TokenTree::Ident(id)) => id.to_string(),
664            _ => return None,
665        };
666
667        // Then: ::
668        match (iter.next(), iter.next()) {
669            (Some(proc_macro2::TokenTree::Punct(p1)), Some(proc_macro2::TokenTree::Punct(p2)))
670                if p1.as_char() == ':' && p2.as_char() == ':' => {}
671            _ => return None,
672        }
673
674        // Then: key ident
675        let key = match iter.next() {
676            Some(proc_macro2::TokenTree::Ident(id)) => id.to_string(),
677            _ => return None,
678        };
679
680        Some(AttrQuery { ns, key })
681    }
682
683    /// Check if an attribute matches this query
684    fn matches(&self, attr: &facet_macro_parse::PFacetAttr) -> bool {
685        if let Some(ref ns) = attr.ns {
686            *ns == self.ns && attr.key == self.key
687        } else {
688            false
689        }
690    }
691
692    /// Find matching attribute in a list
693    fn find_in<'a>(
694        &self,
695        attrs: &'a [facet_macro_parse::PFacetAttr],
696    ) -> Option<&'a facet_macro_parse::PFacetAttr> {
697        attrs.iter().find(|a| self.matches(a))
698    }
699}
700
701// ============================================================================
702// TEMPLATE EVALUATION
703// ============================================================================
704
705/// Evaluate a template with the given context
706fn evaluate_with_context(template: TokenStream, ctx: &mut EvalContext<'_>) -> TokenStream {
707    let mut output = TokenStream::new();
708    let mut iter = template.into_iter().peekable();
709
710    while let Some(tt) = iter.next() {
711        match &tt {
712            proc_macro2::TokenTree::Punct(p) if p.as_char() == '@' => {
713                handle_directive(&mut iter, ctx, &mut output);
714            }
715            proc_macro2::TokenTree::Group(g) => {
716                // Recursively evaluate groups
717                let inner = evaluate_with_context(g.stream(), ctx);
718                let new_group = proc_macro2::Group::new(g.delimiter(), inner);
719                output.extend(std::iter::once(proc_macro2::TokenTree::Group(new_group)));
720            }
721            _ => {
722                output.extend(std::iter::once(tt));
723            }
724        }
725    }
726
727    output
728}
729
730/// Handle a directive after seeing `@`
731fn handle_directive(
732    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
733    ctx: &mut EvalContext<'_>,
734    output: &mut TokenStream,
735) {
736    let Some(next) = iter.next() else {
737        // @ at end of stream - emit it
738        output.extend(quote! { @ });
739        return;
740    };
741
742    let proc_macro2::TokenTree::Ident(directive_ident) = &next else {
743        // Not an ident - emit @ and the token
744        output.extend(quote! { @ });
745        output.extend(std::iter::once(next));
746        return;
747    };
748
749    let directive = directive_ident.to_string();
750
751    match directive.as_str() {
752        // === Type-level directives ===
753        "Self" => emit_self_type(ctx, output),
754
755        // === Looping directives ===
756        "for_variant" => handle_for_variant(iter, ctx, output),
757        "for_field" => handle_for_field(iter, ctx, output),
758
759        // === Conditional directives ===
760        "if_attr" => handle_if_attr(iter, ctx, output),
761        "if_field_attr" => handle_if_field_attr(iter, ctx, output),
762        "if_any_field_attr" => handle_if_any_field_attr(iter, ctx, output),
763        "if_struct" => handle_if_struct(iter, ctx, output),
764        "if_enum" => handle_if_enum(iter, ctx, output),
765
766        // === Context accessors ===
767        "variant_name" => emit_variant_name(ctx, output),
768        "variant_pattern" => emit_variant_pattern(ctx, output),
769        "field_name" => emit_field_name(ctx, output),
770        "field_type" => emit_field_type(ctx, output),
771        "field_expr" => emit_field_expr(ctx, output),
772        "attr_args" => emit_attr_args(ctx, output),
773        "doc" => emit_doc(ctx, output),
774
775        // === Default-related directives ===
776        "field_default_expr" => emit_field_default_expr(ctx, output),
777        "variant_default_construction" => emit_variant_default_construction(ctx, output),
778
779        // === Legacy directives (for backwards compatibility with facet-error) ===
780        "format_doc_comment" => emit_format_doc_comment(ctx, output),
781        "if_has_source_field" => handle_legacy_if_has_source_field(iter, ctx, output),
782        "if_has_from_field" => handle_legacy_if_has_from_field(iter, ctx, output),
783        "source_pattern" => emit_legacy_source_pattern(ctx, output),
784        "source_expr" => emit_legacy_source_expr(output),
785        "from_field_type" => emit_legacy_from_field_type(ctx, output),
786
787        // === Unknown directive ===
788        _ => {
789            // Emit as-is (might be user code with @ symbol)
790            output.extend(quote! { @ });
791            output.extend(std::iter::once(next.clone()));
792        }
793    }
794}
795
796// ============================================================================
797// TYPE-LEVEL DIRECTIVES
798// ============================================================================
799
800fn emit_self_type(ctx: &EvalContext<'_>, output: &mut TokenStream) {
801    let name = ctx.parsed_type.name();
802    output.extend(quote! { #name });
803}
804
805// ============================================================================
806// LOOPING DIRECTIVES
807// ============================================================================
808
809/// `@for_variant { ... }` - loop over enum variants
810fn handle_for_variant(
811    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
812    ctx: &mut EvalContext<'_>,
813    output: &mut TokenStream,
814) {
815    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
816        return; // Malformed - no body
817    };
818
819    let body = body_group.stream();
820
821    // Only works for enums
822    let facet_macro_parse::PType::Enum(e) = ctx.parsed_type else {
823        return;
824    };
825
826    for variant in &e.variants {
827        ctx.push(ContextFrame::Variant { variant });
828        let expanded = evaluate_with_context(body.clone(), ctx);
829        output.extend(expanded);
830        ctx.pop();
831    }
832}
833
834/// `@for_field { ... }` - loop over fields of current context
835fn handle_for_field(
836    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
837    ctx: &mut EvalContext<'_>,
838    output: &mut TokenStream,
839) {
840    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
841        return;
842    };
843
844    let body = body_group.stream();
845
846    let Some(fields) = ctx.current_fields() else {
847        return;
848    };
849
850    // Need to collect to avoid borrow issues
851    let fields: Vec<_> = fields.iter().enumerate().collect();
852
853    for (index, field) in fields {
854        ctx.push(ContextFrame::Field { field, index });
855        let expanded = evaluate_with_context(body.clone(), ctx);
856        output.extend(expanded);
857        ctx.pop();
858    }
859}
860
861// ============================================================================
862// CONDITIONAL DIRECTIVES
863// ============================================================================
864
865/// `@if_attr(ns::key) { ... }` - conditional on current context having attr
866fn handle_if_attr(
867    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
868    ctx: &mut EvalContext<'_>,
869    output: &mut TokenStream,
870) {
871    // Parse (ns::key)
872    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
873        return;
874    };
875
876    // Parse { body }
877    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
878        return;
879    };
880
881    let Some(query) = AttrQuery::parse(query_group.stream()) else {
882        return;
883    };
884
885    let attrs = ctx.current_attrs();
886
887    if let Some(matched_attr) = query.find_in(&attrs.facet) {
888        ctx.push(ContextFrame::Attr { attr: matched_attr });
889        let expanded = evaluate_with_context(body_group.stream(), ctx);
890        output.extend(expanded);
891        ctx.pop();
892    }
893}
894
895/// `@if_field_attr(ns::key) { ... }` - find field with attr, bind field context
896///
897/// This is a combined "find + bind" - it searches all fields in current context
898/// for one with the given attribute, and if found, enters both field and attr context.
899fn handle_if_field_attr(
900    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
901    ctx: &mut EvalContext<'_>,
902    output: &mut TokenStream,
903) {
904    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
905        return;
906    };
907
908    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
909        return;
910    };
911
912    let Some(query) = AttrQuery::parse(query_group.stream()) else {
913        return;
914    };
915
916    let Some(fields) = ctx.current_fields() else {
917        return;
918    };
919
920    // Need to collect to avoid borrow issues
921    let fields: Vec<_> = fields.iter().enumerate().collect();
922
923    // Find first field with matching attr
924    for (index, field) in fields {
925        if let Some(matched_attr) = query.find_in(&field.attrs.facet) {
926            ctx.push(ContextFrame::Field { field, index });
927            ctx.push(ContextFrame::Attr { attr: matched_attr });
928            let expanded = evaluate_with_context(body_group.stream(), ctx);
929            output.extend(expanded);
930            ctx.pop(); // attr
931            ctx.pop(); // field
932            return; // Only emit once for first match
933        }
934    }
935}
936
937/// `@if_any_field_attr(ns::key) { ... }` - conditional if ANY field has attr
938///
939/// Unlike `@if_field_attr`, this doesn't bind a specific field - it just checks
940/// if any field has the attribute. Useful for wrapping a `@for_field` that will
941/// check each field individually.
942fn handle_if_any_field_attr(
943    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
944    ctx: &mut EvalContext<'_>,
945    output: &mut TokenStream,
946) {
947    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
948        return;
949    };
950
951    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
952        return;
953    };
954
955    let Some(query) = AttrQuery::parse(query_group.stream()) else {
956        return;
957    };
958
959    let Some(fields) = ctx.current_fields() else {
960        return;
961    };
962
963    // Check if any field has the attr
964    let has_any = fields
965        .iter()
966        .any(|f| query.find_in(&f.attrs.facet).is_some());
967
968    if has_any {
969        let expanded = evaluate_with_context(body_group.stream(), ctx);
970        output.extend(expanded);
971    }
972}
973
974/// `@if_struct { ... }` - emit body only for struct types
975fn handle_if_struct(
976    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
977    ctx: &mut EvalContext<'_>,
978    output: &mut TokenStream,
979) {
980    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
981        return;
982    };
983
984    if matches!(ctx.parsed_type, facet_macro_parse::PType::Struct(_)) {
985        let expanded = evaluate_with_context(body_group.stream(), ctx);
986        output.extend(expanded);
987    }
988}
989
990/// `@if_enum { ... }` - emit body only for enum types
991fn handle_if_enum(
992    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
993    ctx: &mut EvalContext<'_>,
994    output: &mut TokenStream,
995) {
996    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
997        return;
998    };
999
1000    if matches!(ctx.parsed_type, facet_macro_parse::PType::Enum(_)) {
1001        let expanded = evaluate_with_context(body_group.stream(), ctx);
1002        output.extend(expanded);
1003    }
1004}
1005
1006// ============================================================================
1007// CONTEXT ACCESSORS
1008// ============================================================================
1009
1010fn emit_variant_name(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1011    if let Some(variant) = ctx.current_variant()
1012        && let facet_macro_parse::IdentOrLiteral::Ident(name) = &variant.name.raw
1013    {
1014        output.extend(quote! { #name });
1015    }
1016}
1017
1018fn emit_variant_pattern(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1019    let Some(variant) = ctx.current_variant() else {
1020        return;
1021    };
1022
1023    use facet_macro_parse::{IdentOrLiteral, PVariantKind};
1024
1025    match &variant.kind {
1026        PVariantKind::Unit => {
1027            // No pattern needed for unit variants
1028        }
1029        PVariantKind::Tuple { fields } => {
1030            // Use v0, v1, etc. for legacy compatibility with @format_doc_comment
1031            // (which uses {0}, {1} placeholders that expect v0, v1, etc.)
1032            let names: Vec<_> = (0..fields.len())
1033                .map(|i| quote::format_ident!("v{}", i))
1034                .collect();
1035            output.extend(quote! { ( #(#names),* ) });
1036        }
1037        PVariantKind::Struct { fields } => {
1038            let names: Vec<_> = fields
1039                .iter()
1040                .filter_map(|f| {
1041                    if let IdentOrLiteral::Ident(id) = &f.name.raw {
1042                        Some(id.clone())
1043                    } else {
1044                        None
1045                    }
1046                })
1047                .collect();
1048            output.extend(quote! { { #(#names),* } });
1049        }
1050    }
1051}
1052
1053fn emit_field_name(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1054    let Some((field, index)) = ctx.current_field() else {
1055        return;
1056    };
1057
1058    use facet_macro_parse::IdentOrLiteral;
1059
1060    match &field.name.raw {
1061        IdentOrLiteral::Ident(id) => {
1062            output.extend(quote! { #id });
1063        }
1064        IdentOrLiteral::Literal(_) => {
1065            // Tuple field - use generated name matching @variant_pattern (v0, v1, etc.)
1066            let name = quote::format_ident!("v{}", index);
1067            output.extend(quote! { #name });
1068        }
1069    }
1070}
1071
1072fn emit_field_type(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1073    if let Some((field, _)) = ctx.current_field() {
1074        let ty = &field.ty;
1075        output.extend(quote! { #ty });
1076    }
1077}
1078
1079fn emit_field_expr(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1080    // Same as field_name for now, but could be different for self.field access
1081    emit_field_name(ctx, output);
1082}
1083
1084fn emit_attr_args(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1085    if let Some(attr) = ctx.current_attr() {
1086        let args = &attr.args;
1087        output.extend(args.clone());
1088    }
1089}
1090
1091fn emit_doc(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1092    let attrs = ctx.current_attrs();
1093    let doc = attrs.doc.join(" ").trim().to_string();
1094    if !doc.is_empty() {
1095        output.extend(quote! { #doc });
1096    }
1097}
1098
1099// ============================================================================
1100// DEFAULT-RELATED DIRECTIVES
1101// ============================================================================
1102
1103/// `@field_default_expr` - emit the default expression for the current field
1104///
1105/// Checks for:
1106/// - `#[facet(default::value = literal)]` → `literal.into()`
1107/// - `#[facet(default::func = "path")]` → `path()`
1108/// - No attribute → `::core::default::Default::default()`
1109fn emit_field_default_expr(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1110    let Some((field, _)) = ctx.current_field() else {
1111        return;
1112    };
1113
1114    // Check for default::value attribute
1115    let value_query = AttrQuery {
1116        ns: "default".to_string(),
1117        key: "value".to_string(),
1118    };
1119
1120    if let Some(attr) = value_query.find_in(&field.attrs.facet) {
1121        // Has default::value = something
1122        let args = &attr.args;
1123        output.extend(quote! { (#args).into() });
1124        return;
1125    }
1126
1127    // Check for default::func attribute
1128    let func_query = AttrQuery {
1129        ns: "default".to_string(),
1130        key: "func".to_string(),
1131    };
1132
1133    if let Some(attr) = func_query.find_in(&field.attrs.facet) {
1134        // Has default::func = "some_fn" - parse the string and emit as path
1135        let func_path = parse_func_path(&attr.args);
1136        output.extend(quote! { #func_path() });
1137        return;
1138    }
1139
1140    // No default attribute - use Default::default()
1141    output.extend(quote! { ::core::default::Default::default() });
1142}
1143
1144/// `@variant_default_construction` - emit the construction for a default variant
1145///
1146/// - Unit variant → nothing
1147/// - Tuple variant → (Default::default(), ...)
1148/// - Struct variant → { field: Default::default(), ... }
1149fn emit_variant_default_construction(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1150    use facet_macro_parse::{IdentOrLiteral, PVariantKind};
1151
1152    let Some(variant) = ctx.current_variant() else {
1153        return;
1154    };
1155
1156    match &variant.kind {
1157        PVariantKind::Unit => {
1158            // Nothing to emit
1159        }
1160        PVariantKind::Tuple { fields } => {
1161            let defaults: Vec<_> = fields.iter().map(field_default_tokens).collect();
1162            output.extend(quote! { ( #(#defaults),* ) });
1163        }
1164        PVariantKind::Struct { fields } => {
1165            let field_inits: Vec<_> = fields
1166                .iter()
1167                .filter_map(|f| {
1168                    if let IdentOrLiteral::Ident(name) = &f.name.raw {
1169                        let default_expr = field_default_tokens(f);
1170                        Some(quote! { #name: #default_expr })
1171                    } else {
1172                        None
1173                    }
1174                })
1175                .collect();
1176            output.extend(quote! { { #(#field_inits),* } });
1177        }
1178    }
1179}
1180
1181/// Helper to generate the default expression tokens for a field
1182fn field_default_tokens(field: &facet_macro_parse::PStructField) -> TokenStream {
1183    // Check for default::value attribute
1184    let value_query = AttrQuery {
1185        ns: "default".to_string(),
1186        key: "value".to_string(),
1187    };
1188
1189    if let Some(attr) = value_query.find_in(&field.attrs.facet) {
1190        let args = &attr.args;
1191        return quote! { (#args).into() };
1192    }
1193
1194    // Check for default::func attribute
1195    let func_query = AttrQuery {
1196        ns: "default".to_string(),
1197        key: "func".to_string(),
1198    };
1199
1200    if let Some(attr) = func_query.find_in(&field.attrs.facet) {
1201        let func_path = parse_func_path(&attr.args);
1202        return quote! { #func_path() };
1203    }
1204
1205    // No default attribute - use Default::default()
1206    quote! { ::core::default::Default::default() }
1207}
1208
1209/// Parse a function path from a string literal in attribute args.
1210///
1211/// Takes tokens like `"my_fn"` or `"my_mod::my_fn"` and returns tokens for the path.
1212fn parse_func_path(args: &TokenStream) -> TokenStream {
1213    // Extract the string literal
1214    let args_str = args.to_string();
1215    let trimmed = args_str.trim();
1216
1217    // Remove quotes from string literal
1218    if trimmed.starts_with('"') && trimmed.ends_with('"') {
1219        let path_str = &trimmed[1..trimmed.len() - 1];
1220
1221        // Parse path segments (handles :: separators)
1222        let segments: Vec<_> = path_str.split("::").collect();
1223
1224        if segments.len() == 1 {
1225            // Simple identifier
1226            let ident = quote::format_ident!("{}", segments[0]);
1227            quote! { #ident }
1228        } else {
1229            // Path with multiple segments
1230            let idents: Vec<_> = segments
1231                .iter()
1232                .map(|s| quote::format_ident!("{}", s))
1233                .collect();
1234            quote! { #(#idents)::* }
1235        }
1236    } else {
1237        // Not a string literal - use as-is (shouldn't happen with proper usage)
1238        args.clone()
1239    }
1240}
1241
1242// ============================================================================
1243// LEGACY DIRECTIVES (for backwards compatibility with existing facet-error)
1244// ============================================================================
1245
1246/// Legacy `@format_doc_comment` - emits doc comment as format string with args
1247fn emit_format_doc_comment(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1248    use facet_macro_parse::PVariantKind;
1249
1250    let Some(variant) = ctx.current_variant() else {
1251        return;
1252    };
1253
1254    let doc = variant.attrs.doc.join(" ").trim().to_string();
1255    let format_str = if doc.is_empty() {
1256        variant.name.effective.clone()
1257    } else {
1258        doc
1259    };
1260
1261    // Check if format string uses positional args like {0}
1262    match &variant.kind {
1263        PVariantKind::Unit => {
1264            output.extend(quote! { #format_str });
1265        }
1266        PVariantKind::Tuple { fields } => {
1267            if format_str.contains("{0}") {
1268                // Use v0, v1, etc. to match legacy @variant_pattern
1269                let field_names: Vec<_> = (0..fields.len())
1270                    .map(|i| quote::format_ident!("v{}", i))
1271                    .collect();
1272                output.extend(quote! { #format_str, #(#field_names),* });
1273            } else {
1274                output.extend(quote! { #format_str });
1275            }
1276        }
1277        PVariantKind::Struct { fields: _ } => {
1278            // For struct variants, Rust will resolve field names in format string
1279            output.extend(quote! { #format_str });
1280        }
1281    }
1282}
1283
1284/// Legacy `@if_has_source_field { ... }` - checks for error::source or error::from
1285fn handle_legacy_if_has_source_field(
1286    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
1287    ctx: &mut EvalContext<'_>,
1288    output: &mut TokenStream,
1289) {
1290    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
1291        return;
1292    };
1293
1294    let Some(variant) = ctx.current_variant() else {
1295        return;
1296    };
1297
1298    // Check for error::source or error::from on variant attrs
1299    let has_source = variant.attrs.facet.iter().any(|attr| {
1300        if let Some(ns) = &attr.ns {
1301            *ns == "error" && (attr.key == "source" || attr.key == "from")
1302        } else {
1303            false
1304        }
1305    });
1306
1307    // Only emit for single-field tuple variants (legacy behavior)
1308    let is_single_tuple = matches!(&variant.kind, facet_macro_parse::PVariantKind::Tuple { fields } if fields.len() == 1);
1309
1310    if has_source && is_single_tuple {
1311        let expanded = evaluate_with_context(body_group.stream(), ctx);
1312        output.extend(expanded);
1313    }
1314}
1315
1316/// Legacy `@if_has_from_field { ... }` - checks for error::from specifically
1317fn handle_legacy_if_has_from_field(
1318    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
1319    ctx: &mut EvalContext<'_>,
1320    output: &mut TokenStream,
1321) {
1322    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
1323        return;
1324    };
1325
1326    let Some(variant) = ctx.current_variant() else {
1327        return;
1328    };
1329
1330    // Check for error::from specifically
1331    let has_from = variant.attrs.facet.iter().any(|attr| {
1332        if let Some(ns) = &attr.ns {
1333            *ns == "error" && attr.key == "from"
1334        } else {
1335            false
1336        }
1337    });
1338
1339    // Only for single-field tuple variants
1340    let is_single_tuple = matches!(&variant.kind, facet_macro_parse::PVariantKind::Tuple { fields } if fields.len() == 1);
1341
1342    if has_from && is_single_tuple {
1343        let expanded = evaluate_with_context(body_group.stream(), ctx);
1344        output.extend(expanded);
1345    }
1346}
1347
1348/// Legacy `@source_pattern` - emits (e) for tuple variants
1349fn emit_legacy_source_pattern(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1350    use facet_macro_parse::PVariantKind;
1351
1352    let Some(variant) = ctx.current_variant() else {
1353        return;
1354    };
1355
1356    match &variant.kind {
1357        PVariantKind::Tuple { .. } => {
1358            output.extend(quote! { (e) });
1359        }
1360        PVariantKind::Struct { fields } => {
1361            // Find the field with error::source or error::from
1362            for field in fields {
1363                if field.attrs.facet.iter().any(|attr| {
1364                    if let Some(ns) = &attr.ns {
1365                        *ns == "error" && (attr.key == "source" || attr.key == "from")
1366                    } else {
1367                        false
1368                    }
1369                }) && let facet_macro_parse::IdentOrLiteral::Ident(name) = &field.name.raw
1370                {
1371                    output.extend(quote! { { #name, .. } });
1372                    return;
1373                }
1374            }
1375            output.extend(quote! { { .. } });
1376        }
1377        _ => {}
1378    }
1379}
1380
1381/// Legacy `@source_expr` - emits `e`
1382fn emit_legacy_source_expr(output: &mut TokenStream) {
1383    output.extend(quote! { e });
1384}
1385
1386/// Legacy `@from_field_type` - emits type of first tuple field
1387fn emit_legacy_from_field_type(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1388    use facet_macro_parse::PVariantKind;
1389
1390    let Some(variant) = ctx.current_variant() else {
1391        return;
1392    };
1393
1394    if let PVariantKind::Tuple { fields } = &variant.kind
1395        && let Some(field) = fields.first()
1396    {
1397        let ty = &field.ty;
1398        output.extend(quote! { #ty });
1399    }
1400}
1401
1402// Grammar for parsing finalize sections
1403crate::unsynn! {
1404    /// Section marker like `@tokens`, `@plugins`
1405    struct FinalizeSectionMarker {
1406        _at: crate::At,
1407        name: Ident,
1408    }
1409
1410    /// A braced section like `@tokens { ... }`
1411    struct FinalizeSection {
1412        marker: FinalizeSectionMarker,
1413        content: crate::BraceGroupContaining<TokenStream>,
1414    }
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419    use super::*;
1420    use crate::IParse;
1421    use quote::quote;
1422
1423    #[test]
1424    fn test_to_snake_case() {
1425        assert_eq!(to_snake_case("Error"), "error");
1426        assert_eq!(to_snake_case("Display"), "display");
1427        assert_eq!(to_snake_case("PartialEq"), "partial_eq");
1428        assert_eq!(to_snake_case("FromStr"), "from_str");
1429    }
1430
1431    #[test]
1432    fn test_extract_derive_plugins() {
1433        let input = quote! {
1434            #[derive(Facet, Debug)]
1435            #[facet(derive(Error))]
1436            #[repr(u8)]
1437            pub enum MyError {
1438                Disconnect(u32),
1439            }
1440        };
1441
1442        let mut iter = input.to_token_iter();
1443        let parsed = iter.parse::<crate::Enum>().expect("Failed to parse enum");
1444
1445        let plugins = extract_derive_plugins(&parsed.attributes);
1446        assert_eq!(plugins.len(), 1);
1447        assert!(matches!(&plugins[0], PluginRef::Simple(name) if name == "Error"));
1448    }
1449
1450    #[test]
1451    fn test_extract_multiple_plugins() {
1452        let input = quote! {
1453            #[facet(derive(Error, Display))]
1454            pub enum MyError {
1455                Unknown,
1456            }
1457        };
1458
1459        let mut iter = input.to_token_iter();
1460        let parsed = iter.parse::<crate::Enum>().expect("Failed to parse enum");
1461
1462        let plugins = extract_derive_plugins(&parsed.attributes);
1463        assert_eq!(plugins.len(), 2);
1464        assert!(matches!(&plugins[0], PluginRef::Simple(name) if name == "Error"));
1465        assert!(matches!(&plugins[1], PluginRef::Simple(name) if name == "Display"));
1466    }
1467
1468    #[test]
1469    fn test_extract_path_plugins() {
1470        let input = quote! {
1471            #[facet(derive(Error, facet_miette::Diagnostic))]
1472            pub enum MyError {
1473                Unknown,
1474            }
1475        };
1476
1477        let mut iter = input.to_token_iter();
1478        let parsed = iter.parse::<crate::Enum>().expect("Failed to parse enum");
1479
1480        let plugins = extract_derive_plugins(&parsed.attributes);
1481        assert_eq!(plugins.len(), 2);
1482        assert!(matches!(&plugins[0], PluginRef::Simple(name) if name == "Error"));
1483        assert!(
1484            matches!(&plugins[1], PluginRef::Path { crate_name, plugin_name } if crate_name == "facet_miette" && plugin_name == "Diagnostic")
1485        );
1486    }
1487
1488    #[test]
1489    fn test_plugin_ref_crate_path() {
1490        let simple = PluginRef::Simple("Error".to_string());
1491        assert_eq!(simple.crate_path().to_string(), ":: facet_error");
1492
1493        let path = PluginRef::Path {
1494            crate_name: "facet_miette".to_string(),
1495            plugin_name: "Diagnostic".to_string(),
1496        };
1497        assert_eq!(path.crate_path().to_string(), ":: facet_miette");
1498    }
1499}