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        "if_unit_variant" => handle_if_unit_variant(iter, ctx, output),
766        "if_tuple_variant" => handle_if_tuple_variant(iter, ctx, output),
767        "if_struct_variant" => handle_if_struct_variant(iter, ctx, output),
768
769        // === Context accessors ===
770        "variant_name" => emit_variant_name(ctx, output),
771        "variant_pattern" => emit_variant_pattern(ctx, output),
772        "variant_pattern_only" => handle_variant_pattern_only(iter, ctx, output),
773        "field_name" => emit_field_name(ctx, output),
774        "field_type" => emit_field_type(ctx, output),
775        "field_expr" => emit_field_expr(ctx, output),
776        "attr_args" => emit_attr_args(ctx, output),
777        "doc" => emit_doc(ctx, output),
778
779        // === Default-related directives ===
780        "field_default_expr" => emit_field_default_expr(ctx, output),
781        "variant_default_construction" => emit_variant_default_construction(ctx, output),
782
783        // === Display-related directives ===
784        "format_doc_comment" => emit_format_doc_comment(ctx, output),
785
786        // === Unknown directive ===
787        _ => {
788            // Emit as-is (might be user code with @ symbol)
789            output.extend(quote! { @ });
790            output.extend(std::iter::once(next.clone()));
791        }
792    }
793}
794
795// ============================================================================
796// TYPE-LEVEL DIRECTIVES
797// ============================================================================
798
799fn emit_self_type(ctx: &EvalContext<'_>, output: &mut TokenStream) {
800    let name = ctx.parsed_type.name();
801    output.extend(quote! { #name });
802}
803
804// ============================================================================
805// LOOPING DIRECTIVES
806// ============================================================================
807
808/// `@for_variant { ... }` - loop over enum variants
809fn handle_for_variant(
810    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
811    ctx: &mut EvalContext<'_>,
812    output: &mut TokenStream,
813) {
814    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
815        return; // Malformed - no body
816    };
817
818    let body = body_group.stream();
819
820    // Only works for enums
821    let facet_macro_parse::PType::Enum(e) = ctx.parsed_type else {
822        return;
823    };
824
825    for variant in &e.variants {
826        ctx.push(ContextFrame::Variant { variant });
827        let expanded = evaluate_with_context(body.clone(), ctx);
828        output.extend(expanded);
829        ctx.pop();
830    }
831}
832
833/// `@for_field { ... }` - loop over fields of current context
834fn handle_for_field(
835    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
836    ctx: &mut EvalContext<'_>,
837    output: &mut TokenStream,
838) {
839    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
840        return;
841    };
842
843    let body = body_group.stream();
844
845    let Some(fields) = ctx.current_fields() else {
846        return;
847    };
848
849    // Need to collect to avoid borrow issues
850    let fields: Vec<_> = fields.iter().enumerate().collect();
851
852    for (index, field) in fields {
853        ctx.push(ContextFrame::Field { field, index });
854        let expanded = evaluate_with_context(body.clone(), ctx);
855        output.extend(expanded);
856        ctx.pop();
857    }
858}
859
860// ============================================================================
861// CONDITIONAL DIRECTIVES
862// ============================================================================
863
864/// `@if_attr(ns::key) { ... }` - conditional on current context having attr
865fn handle_if_attr(
866    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
867    ctx: &mut EvalContext<'_>,
868    output: &mut TokenStream,
869) {
870    // Parse (ns::key)
871    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
872        return;
873    };
874
875    // Parse { body }
876    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
877        return;
878    };
879
880    let Some(query) = AttrQuery::parse(query_group.stream()) else {
881        return;
882    };
883
884    let attrs = ctx.current_attrs();
885
886    if let Some(matched_attr) = query.find_in(&attrs.facet) {
887        ctx.push(ContextFrame::Attr { attr: matched_attr });
888        let expanded = evaluate_with_context(body_group.stream(), ctx);
889        output.extend(expanded);
890        ctx.pop();
891    }
892}
893
894/// `@if_field_attr(ns::key) { ... }` - find field with attr, bind field context
895///
896/// This is a combined "find + bind" - it searches all fields in current context
897/// for one with the given attribute, and if found, enters both field and attr context.
898fn handle_if_field_attr(
899    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
900    ctx: &mut EvalContext<'_>,
901    output: &mut TokenStream,
902) {
903    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
904        return;
905    };
906
907    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
908        return;
909    };
910
911    let Some(query) = AttrQuery::parse(query_group.stream()) else {
912        return;
913    };
914
915    let Some(fields) = ctx.current_fields() else {
916        return;
917    };
918
919    // Need to collect to avoid borrow issues
920    let fields: Vec<_> = fields.iter().enumerate().collect();
921
922    // Find first field with matching attr
923    for (index, field) in fields {
924        if let Some(matched_attr) = query.find_in(&field.attrs.facet) {
925            ctx.push(ContextFrame::Field { field, index });
926            ctx.push(ContextFrame::Attr { attr: matched_attr });
927            let expanded = evaluate_with_context(body_group.stream(), ctx);
928            output.extend(expanded);
929            ctx.pop(); // attr
930            ctx.pop(); // field
931            return; // Only emit once for first match
932        }
933    }
934}
935
936/// `@if_any_field_attr(ns::key) { ... }` - conditional if ANY field has attr
937///
938/// Unlike `@if_field_attr`, this doesn't bind a specific field - it just checks
939/// if any field has the attribute. Useful for wrapping a `@for_field` that will
940/// check each field individually.
941fn handle_if_any_field_attr(
942    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
943    ctx: &mut EvalContext<'_>,
944    output: &mut TokenStream,
945) {
946    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
947        return;
948    };
949
950    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
951        return;
952    };
953
954    let Some(query) = AttrQuery::parse(query_group.stream()) else {
955        return;
956    };
957
958    let Some(fields) = ctx.current_fields() else {
959        return;
960    };
961
962    // Check if any field has the attr
963    let has_any = fields
964        .iter()
965        .any(|f| query.find_in(&f.attrs.facet).is_some());
966
967    if has_any {
968        let expanded = evaluate_with_context(body_group.stream(), ctx);
969        output.extend(expanded);
970    }
971}
972
973/// `@if_struct { ... }` - emit body only for struct types
974fn handle_if_struct(
975    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
976    ctx: &mut EvalContext<'_>,
977    output: &mut TokenStream,
978) {
979    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
980        return;
981    };
982
983    if matches!(ctx.parsed_type, facet_macro_parse::PType::Struct(_)) {
984        let expanded = evaluate_with_context(body_group.stream(), ctx);
985        output.extend(expanded);
986    }
987}
988
989/// `@if_enum { ... }` - emit body only for enum types
990fn handle_if_enum(
991    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
992    ctx: &mut EvalContext<'_>,
993    output: &mut TokenStream,
994) {
995    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
996        return;
997    };
998
999    if matches!(ctx.parsed_type, facet_macro_parse::PType::Enum(_)) {
1000        let expanded = evaluate_with_context(body_group.stream(), ctx);
1001        output.extend(expanded);
1002    }
1003}
1004
1005/// `@if_unit_variant { ... }` - conditional on current variant being a unit variant
1006fn handle_if_unit_variant(
1007    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
1008    ctx: &mut EvalContext<'_>,
1009    output: &mut TokenStream,
1010) {
1011    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
1012        return;
1013    };
1014
1015    let Some(variant) = ctx.current_variant() else {
1016        return;
1017    };
1018
1019    if matches!(variant.kind, facet_macro_parse::PVariantKind::Unit) {
1020        let expanded = evaluate_with_context(body_group.stream(), ctx);
1021        output.extend(expanded);
1022    }
1023}
1024
1025/// `@if_tuple_variant { ... }` - conditional on current variant being a tuple variant
1026fn handle_if_tuple_variant(
1027    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
1028    ctx: &mut EvalContext<'_>,
1029    output: &mut TokenStream,
1030) {
1031    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
1032        return;
1033    };
1034
1035    let Some(variant) = ctx.current_variant() else {
1036        return;
1037    };
1038
1039    if matches!(variant.kind, facet_macro_parse::PVariantKind::Tuple { .. }) {
1040        let expanded = evaluate_with_context(body_group.stream(), ctx);
1041        output.extend(expanded);
1042    }
1043}
1044
1045/// `@if_struct_variant { ... }` - conditional on current variant being a struct variant
1046fn handle_if_struct_variant(
1047    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
1048    ctx: &mut EvalContext<'_>,
1049    output: &mut TokenStream,
1050) {
1051    let Some(proc_macro2::TokenTree::Group(body_group)) = iter.next() else {
1052        return;
1053    };
1054
1055    let Some(variant) = ctx.current_variant() else {
1056        return;
1057    };
1058
1059    if matches!(variant.kind, facet_macro_parse::PVariantKind::Struct { .. }) {
1060        let expanded = evaluate_with_context(body_group.stream(), ctx);
1061        output.extend(expanded);
1062    }
1063}
1064
1065// ============================================================================
1066// CONTEXT ACCESSORS
1067// ============================================================================
1068
1069fn emit_variant_name(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1070    if let Some(variant) = ctx.current_variant()
1071        && let facet_macro_parse::IdentOrLiteral::Ident(name) = &variant.name.raw
1072    {
1073        output.extend(quote! { #name });
1074    }
1075}
1076
1077fn emit_variant_pattern(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1078    let Some(variant) = ctx.current_variant() else {
1079        return;
1080    };
1081
1082    use facet_macro_parse::{IdentOrLiteral, PVariantKind};
1083
1084    match &variant.kind {
1085        PVariantKind::Unit => {
1086            // No pattern needed for unit variants
1087        }
1088        PVariantKind::Tuple { fields } => {
1089            // Use v0, v1, etc. for legacy compatibility with @format_doc_comment
1090            // (which uses {0}, {1} placeholders that expect v0, v1, etc.)
1091            let names: Vec<_> = (0..fields.len())
1092                .map(|i| quote::format_ident!("v{}", i))
1093                .collect();
1094            output.extend(quote! { ( #(#names),* ) });
1095        }
1096        PVariantKind::Struct { fields } => {
1097            let names: Vec<_> = fields
1098                .iter()
1099                .filter_map(|f| {
1100                    if let IdentOrLiteral::Ident(id) = &f.name.raw {
1101                        Some(id.clone())
1102                    } else {
1103                        None
1104                    }
1105                })
1106                .collect();
1107            output.extend(quote! { { #(#names),* } });
1108        }
1109    }
1110}
1111
1112/// `@variant_pattern_only(ns::key) { ... }` - generate pattern binding only fields with attribute
1113fn handle_variant_pattern_only(
1114    iter: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
1115    ctx: &EvalContext<'_>,
1116    output: &mut TokenStream,
1117) {
1118    let Some(proc_macro2::TokenTree::Group(query_group)) = iter.next() else {
1119        return;
1120    };
1121
1122    let Some(query) = AttrQuery::parse(query_group.stream()) else {
1123        return;
1124    };
1125
1126    let Some(variant) = ctx.current_variant() else {
1127        return;
1128    };
1129
1130    use facet_macro_parse::{IdentOrLiteral, PVariantKind};
1131
1132    match &variant.kind {
1133        PVariantKind::Unit => {
1134            // No pattern needed for unit variants
1135        }
1136        PVariantKind::Tuple { fields } => {
1137            let patterns: Vec<_> = fields
1138                .iter()
1139                .enumerate()
1140                .map(|(i, f)| {
1141                    if query.find_in(&f.attrs.facet).is_some() {
1142                        let name = quote::format_ident!("v{}", i);
1143                        quote! { #name }
1144                    } else {
1145                        quote! { _ }
1146                    }
1147                })
1148                .collect();
1149            output.extend(quote! { ( #(#patterns),* ) });
1150        }
1151        PVariantKind::Struct { fields } => {
1152            let bindings: Vec<_> = fields
1153                .iter()
1154                .filter_map(|f| {
1155                    if let IdentOrLiteral::Ident(id) = &f.name.raw {
1156                        if query.find_in(&f.attrs.facet).is_some() {
1157                            Some(quote! { #id })
1158                        } else {
1159                            Some(quote! { #id: _ })
1160                        }
1161                    } else {
1162                        None
1163                    }
1164                })
1165                .collect();
1166            output.extend(quote! { { #(#bindings),* } });
1167        }
1168    }
1169}
1170
1171fn emit_field_name(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1172    let Some((field, index)) = ctx.current_field() else {
1173        return;
1174    };
1175
1176    use facet_macro_parse::IdentOrLiteral;
1177
1178    match &field.name.raw {
1179        IdentOrLiteral::Ident(id) => {
1180            output.extend(quote! { #id });
1181        }
1182        IdentOrLiteral::Literal(_) => {
1183            // Tuple field - use generated name matching @variant_pattern (v0, v1, etc.)
1184            let name = quote::format_ident!("v{}", index);
1185            output.extend(quote! { #name });
1186        }
1187    }
1188}
1189
1190fn emit_field_type(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1191    if let Some((field, _)) = ctx.current_field() {
1192        let ty = &field.ty;
1193        output.extend(quote! { #ty });
1194    }
1195}
1196
1197fn emit_field_expr(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1198    // Same as field_name for now, but could be different for self.field access
1199    emit_field_name(ctx, output);
1200}
1201
1202fn emit_attr_args(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1203    if let Some(attr) = ctx.current_attr() {
1204        let args = &attr.args;
1205        output.extend(args.clone());
1206    }
1207}
1208
1209fn emit_doc(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1210    let attrs = ctx.current_attrs();
1211    let doc = attrs.doc.join(" ").trim().to_string();
1212    if !doc.is_empty() {
1213        output.extend(quote! { #doc });
1214    }
1215}
1216
1217// ============================================================================
1218// DEFAULT-RELATED DIRECTIVES
1219// ============================================================================
1220
1221/// `@field_default_expr` - emit the default expression for the current field
1222///
1223/// Checks for:
1224/// - `#[facet(default::value = literal)]` → `literal.into()`
1225/// - `#[facet(default::func = "path")]` → `path()`
1226/// - No attribute → `::core::default::Default::default()`
1227fn emit_field_default_expr(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1228    let Some((field, _)) = ctx.current_field() else {
1229        return;
1230    };
1231
1232    // Check for default::value attribute
1233    let value_query = AttrQuery {
1234        ns: "default".to_string(),
1235        key: "value".to_string(),
1236    };
1237
1238    if let Some(attr) = value_query.find_in(&field.attrs.facet) {
1239        // Has default::value = something
1240        let args = &attr.args;
1241        output.extend(quote! { (#args).into() });
1242        return;
1243    }
1244
1245    // Check for default::func attribute
1246    let func_query = AttrQuery {
1247        ns: "default".to_string(),
1248        key: "func".to_string(),
1249    };
1250
1251    if let Some(attr) = func_query.find_in(&field.attrs.facet) {
1252        // Has default::func = "some_fn" - parse the string and emit as path
1253        let func_path = parse_func_path(&attr.args);
1254        output.extend(quote! { #func_path() });
1255        return;
1256    }
1257
1258    // No default attribute - use Default::default()
1259    output.extend(quote! { ::core::default::Default::default() });
1260}
1261
1262/// `@variant_default_construction` - emit the construction for a default variant
1263///
1264/// - Unit variant → nothing
1265/// - Tuple variant → (Default::default(), ...)
1266/// - Struct variant → { field: Default::default(), ... }
1267fn emit_variant_default_construction(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1268    use facet_macro_parse::{IdentOrLiteral, PVariantKind};
1269
1270    let Some(variant) = ctx.current_variant() else {
1271        return;
1272    };
1273
1274    match &variant.kind {
1275        PVariantKind::Unit => {
1276            // Nothing to emit
1277        }
1278        PVariantKind::Tuple { fields } => {
1279            let defaults: Vec<_> = fields.iter().map(field_default_tokens).collect();
1280            output.extend(quote! { ( #(#defaults),* ) });
1281        }
1282        PVariantKind::Struct { fields } => {
1283            let field_inits: Vec<_> = fields
1284                .iter()
1285                .filter_map(|f| {
1286                    if let IdentOrLiteral::Ident(name) = &f.name.raw {
1287                        let default_expr = field_default_tokens(f);
1288                        Some(quote! { #name: #default_expr })
1289                    } else {
1290                        None
1291                    }
1292                })
1293                .collect();
1294            output.extend(quote! { { #(#field_inits),* } });
1295        }
1296    }
1297}
1298
1299/// Helper to generate the default expression tokens for a field
1300fn field_default_tokens(field: &facet_macro_parse::PStructField) -> TokenStream {
1301    // Check for default::value attribute
1302    let value_query = AttrQuery {
1303        ns: "default".to_string(),
1304        key: "value".to_string(),
1305    };
1306
1307    if let Some(attr) = value_query.find_in(&field.attrs.facet) {
1308        let args = &attr.args;
1309        return quote! { (#args).into() };
1310    }
1311
1312    // Check for default::func attribute
1313    let func_query = AttrQuery {
1314        ns: "default".to_string(),
1315        key: "func".to_string(),
1316    };
1317
1318    if let Some(attr) = func_query.find_in(&field.attrs.facet) {
1319        let func_path = parse_func_path(&attr.args);
1320        return quote! { #func_path() };
1321    }
1322
1323    // No default attribute - use Default::default()
1324    quote! { ::core::default::Default::default() }
1325}
1326
1327/// Parse a function path from a string literal in attribute args.
1328///
1329/// Takes tokens like `"my_fn"` or `"my_mod::my_fn"` and returns tokens for the path.
1330fn parse_func_path(args: &TokenStream) -> TokenStream {
1331    // Extract the string literal
1332    let args_str = args.to_string();
1333    let trimmed = args_str.trim();
1334
1335    // Remove quotes from string literal
1336    if trimmed.starts_with('"') && trimmed.ends_with('"') {
1337        let path_str = &trimmed[1..trimmed.len() - 1];
1338
1339        // Parse path segments (handles :: separators)
1340        let segments: Vec<_> = path_str.split("::").collect();
1341
1342        if segments.len() == 1 {
1343            // Simple identifier
1344            let ident = quote::format_ident!("{}", segments[0]);
1345            quote! { #ident }
1346        } else {
1347            // Path with multiple segments
1348            let idents: Vec<_> = segments
1349                .iter()
1350                .map(|s| quote::format_ident!("{}", s))
1351                .collect();
1352            quote! { #(#idents)::* }
1353        }
1354    } else {
1355        // Not a string literal - use as-is (shouldn't happen with proper usage)
1356        args.clone()
1357    }
1358}
1359
1360// ============================================================================
1361// LEGACY DIRECTIVES (for backwards compatibility with existing facet-error)
1362// ============================================================================
1363
1364/// Legacy `@format_doc_comment` - emits doc comment as format string with args
1365fn emit_format_doc_comment(ctx: &EvalContext<'_>, output: &mut TokenStream) {
1366    use facet_macro_parse::PVariantKind;
1367
1368    let Some(variant) = ctx.current_variant() else {
1369        return;
1370    };
1371
1372    let doc = variant.attrs.doc.join(" ").trim().to_string();
1373    let format_str = if doc.is_empty() {
1374        variant.name.effective.clone()
1375    } else {
1376        doc
1377    };
1378
1379    // Check if format string uses positional args like {0}
1380    match &variant.kind {
1381        PVariantKind::Unit => {
1382            output.extend(quote! { #format_str });
1383        }
1384        PVariantKind::Tuple { fields } => {
1385            if format_str.contains("{0}") {
1386                // Use v0, v1, etc. to match legacy @variant_pattern
1387                let field_names: Vec<_> = (0..fields.len())
1388                    .map(|i| quote::format_ident!("v{}", i))
1389                    .collect();
1390                output.extend(quote! { #format_str, #(#field_names),* });
1391            } else {
1392                output.extend(quote! { #format_str });
1393            }
1394        }
1395        PVariantKind::Struct { fields: _ } => {
1396            // For struct variants, Rust will resolve field names in format string
1397            output.extend(quote! { #format_str });
1398        }
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}