1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! The `#[smart_display::private_metadata]` attribute.

use std::iter;

use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Paren, Pub};
use syn::{
    Error, ItemStruct, Path, PathArguments, PathSegment, Result, Token, VisRestricted, Visibility,
};

/// Implementation of the `#[smart_display::private_metadata]` attribute.
pub(crate) fn implementation(attr: TokenStream, item: TokenStream) -> Result<TokenStream> {
    if !attr.is_empty() {
        return Err(Error::new_spanned(
            attr,
            "`delegate_display` does not take any arguments",
        ));
    }

    let mut input = syn::parse2::<ItemStruct>(item)?;

    // Public items are not supported by the macro.
    if matches!(input.vis, Visibility::Public(_)) {
        return Err(Error::new_spanned(
            input.vis.into_token_stream(),
            "`metadata` can only be used on non-pubic items",
        ));
    }

    // Preserve the original visibility for the re-export.
    let original_vis = input.vis.clone();
    // Change the visibility of the struct to public, avoiding any compiler errors.
    input.vis = Visibility::Public(Token![pub](input.vis.span()));

    // Change the visibility of fields as necessary.
    for field in &mut input.fields {
        vis_prepend_super(&mut field.vis)?;
    }

    let mod_name = format_ident!(
        "__powerfmt_private_{}",
        input.ident,
        span = Span::call_site() // Avoid a warning about the module not being snake case.
    );

    Ok(quote! {
        mod #mod_name {
            use super::*;

            #[non_exhaustive]
            #input
        }

        #original_vis use #mod_name::*;
    })
}

fn vis_prepend_super(vis: &mut Visibility) -> Result<()> {
    match vis {
        Visibility::Public(_) => {
            // If the visibility is public, there is nothing to do.
            Ok(())
        }
        Visibility::Restricted(VisRestricted { path, in_token, .. }) => {
            let segments = &path.segments;
            let ident = &segments
                .first()
                .or_else(|| segments.last())
                .expect("path should have at least one segment")
                .ident;

            match ident.to_string().as_ref() {
                "crate" => {
                    // If the path is simply `crate`, there is nothing to do. Otherwise, we need to
                    // remove the final segment.
                    if path.get_ident().is_none() {
                        path.segments.pop();
                    }

                    Ok(())
                }
                "super" => {
                    *in_token = in_token.or_else(|| Some(Token![in](path.span())));

                    // Any path starting with `super` needs another `super` prepended.
                    path.segments = iter::once(PathSegment {
                        ident: Token![super](path.span()).into(),
                        arguments: PathArguments::default(),
                    })
                    .chain(path.segments.iter().cloned())
                    .collect();

                    Ok(())
                }
                "self" => {
                    // `pub(self)` -> `pub(super)`
                    //
                    // There is absolutely no reason this should ever be written, but it is valid
                    // syntax.
                    let super_span = path.get_ident().map_or_else(Span::call_site, Ident::span);
                    path.segments = Punctuated::from_iter([PathSegment {
                        ident: Token![super](super_span).into(),
                        arguments: PathArguments::None,
                    }]);

                    Ok(())
                }
                _ => Err(Error::new_spanned(path, "unexpected visibility path")),
            }
        }
        Visibility::Inherited => {
            // inherited visibility -> `pub(super)`
            *vis = Visibility::Restricted(VisRestricted {
                pub_token: Pub::default(),
                paren_token: Paren::default(),
                in_token: None,
                path: Box::new(Path {
                    leading_colon: None,
                    segments: Punctuated::from_iter([PathSegment {
                        ident: format_ident!("super"),
                        arguments: PathArguments::None,
                    }]),
                }),
            });

            Ok(())
        }
    }
}