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
use syn::{parse_quote, punctuated::Punctuated, Attribute, DeriveInput, Meta, NestedMeta, Token};

use crate::{meta_list_contains, nested_meta_to_ident};

/// Functions to make it ergonomic to work with `struct` ASTs.
pub trait DeriveInputDeriveExt {
    /// Appends derives to the list of derives.
    ///
    /// **Note:** This can only be used with [*attribute*] macros, and not [*derive*] macros.
    ///
    /// * If the `derive` attribute does not exist, one will be created.
    /// * If the `derive` attribute exists, and there are existing `derive`s that overlap with the
    ///   derives to append, this macro will panic with the overlapping derives.
    /// * If the `derive` attribute exists, and there are no overlapping `derive`s, then they will
    ///   be combined.
    ///
    /// # Panics
    ///
    /// Panics if there are existing `derive`s that overlap with the derives to append.
    ///
    /// [*attribute*]: <https://doc.rust-lang.org/reference/procedural-macros.html#attribute-macros>
    /// [*derive*]: <https://doc.rust-lang.org/reference/procedural-macros.html#derive-mode-macros>
    fn append_derives(&mut self, derives: Punctuated<NestedMeta, Token![,]>);
}

impl DeriveInputDeriveExt for DeriveInput {
    fn append_derives(&mut self, derives_to_append: Punctuated<NestedMeta, Token![,]>) {
        let attr_derives_existing = self
            .attrs
            .iter_mut()
            .filter(|attr| attr.path.is_ident("derive"))
            .filter_map(|attr| match attr.parse_meta() {
                Ok(Meta::List(meta_list)) => Some((attr, meta_list)),
                _ => None,
            })
            .next();

        if let Some((attr, mut derives_existing)) = attr_derives_existing {
            // Emit warning if the user derives any of the existing derives, as we do that for them.
            let superfluous = derives_to_append
                .iter()
                .filter(|derive_to_append| meta_list_contains(&derives_existing, derive_to_append))
                .filter_map(nested_meta_to_ident)
                .map(|ident| format!("{}", ident))
                .collect::<Vec<_>>();
            if !superfluous.is_empty() {
                // TODO: Emit warning, pending <https://github.com/rust-lang/rust/issues/54140>
                // derives_existing
                //     .span()
                //     .warning(
                //         "The following are automatically derived by this proc macro attribute.",
                //     )
                //     .emit();
                panic!(
                    "The following are automatically derived when this attribute is used:\n\
                     {:?}",
                    superfluous
                );
            } else {
                // derives_existing.nested.push_punct(<Token![,]>::default());
                derives_existing.nested.extend(derives_to_append);

                // Replace the existing `Attribute`.
                //
                // `attr.parse_meta()` returns a `Meta`, which is not referenced by the
                // `DeriveInput`, so we have to replace `attr` itself.
                *attr = parse_quote!(#[#derives_existing]);
            }
        } else {
            // Add a new `#[derive(..)]` attribute with all the derives.
            let derive_attribute: Attribute = parse_quote!(#[derive(#derives_to_append)]);
            self.attrs.push(derive_attribute);
        }
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;
    use syn::{parse_quote, DeriveInput};

    use super::DeriveInputDeriveExt;

    #[test]
    fn append_derives_creates_attr_when_attr_does_not_exist() {
        let mut ast: DeriveInput = parse_quote!(
            struct Struct;
        );
        let derives = parse_quote!(Clone, Copy);

        ast.append_derives(derives);

        let ast_expected: DeriveInput = parse_quote! {
            #[derive(Clone, Copy)]
            struct Struct;
        };
        assert_eq!(ast_expected, ast);
    }

    #[test]
    fn append_derives_appends_to_attr_when_attr_exists() {
        let mut ast: DeriveInput = parse_quote!(
            #[derive(Debug)]
            struct Struct;
        );
        let derives = parse_quote!(Clone, Copy);

        ast.append_derives(derives);

        let ast_expected: DeriveInput = parse_quote! {
            #[derive(Debug, Clone, Copy)]
            struct Struct;
        };
        assert_eq!(ast_expected, ast);
    }

    #[test]
    #[should_panic(
        expected = "The following are automatically derived when this attribute is used:\n\
                    [\"Clone\", \"Copy\"]"
    )]
    fn append_derives_panics_when_derives_exist() {
        let mut ast: DeriveInput = parse_quote!(
            #[derive(Clone, Copy, Debug)]
            struct Struct;
        );
        let derives = parse_quote!(Clone, Copy, Default);

        ast.append_derives(derives);
    }
}