far_macros 0.2.0

Macros for the far crate
Documentation
//! Procedural macros for the `far` crate
//!
//! You should probably just be looking at the documentation for the `far` crate
//! instead.

#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]

use std::any::TypeId;

use proc_macro2::{Ident, TokenStream};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::{format_ident, quote, ToTokens};
use syn::{
    braced,
    parse::{Error, Parse, ParseStream, Result},
    parse_macro_input,
    punctuated::Punctuated,
    token, Attribute, Field, LitStr, Token, Visibility,
};

/// Ensures derive input is a struct with named fields
struct NamedFieldsStruct {
    /// Convince `syn` that it's okay if the struct has attributes on it
    #[allow(dead_code)]
    attrs: Vec<Attribute>,

    /// Convince `syn` that it's okay if the struct is marked `pub` or such
    #[allow(dead_code)]
    vis: Visibility,

    /// Inform `syn` that we're looking for a struct
    #[allow(dead_code)]
    struct_token: Token![struct],

    /// The name of the struct
    ident: Ident,

    /// Cause `syn` to enforce that this struct have braces in its definition
    #[allow(dead_code)]
    brace_token: token::Brace,

    /// The fields of the struct
    fields: Punctuated<Field, Token![,]>,
}

impl Parse for NamedFieldsStruct {
    fn parse(input: ParseStream) -> Result<Self> {
        let content;

        // clippy doesn't like it but according to syn's docs, this is the
        // idiomatic form
        #[allow(clippy::eval_order_dependence)]
        Ok(Self {
            attrs: input.call(Attribute::parse_outer)?,
            vis: input.parse()?,
            struct_token: input.parse()?,
            ident: input.parse()?,
            brace_token: braced!(content in input),
            fields: content.parse_terminated(Field::parse_named)?,
        })
    }
}

/// Parses attributes of the typical form
struct AttrNameValue<V> {
    /// The name of this attribute argument
    ///
    /// ```ignore
    /// #[far(fmt = "{}")]
    /// //    ^^^
    /// ```
    name: Ident,

    /// The token separating this argument from its value
    ///
    /// ```ignore
    /// #[far(fmt = "{}")]
    /// //        ^
    /// ```
    #[allow(dead_code)]
    eq_token: Token![=],

    /// The value of this attribute argument
    ///
    /// ```ignore
    /// #[far(fmt = "{}")]
    /// //          ^^^^
    /// ```
    ///
    /// If `V` is not a [`LitStr`](LitStr), `V` will first be parsed into a
    /// [`LitStr`](LitStr) and then into the actual type of `V`. Otherwise, it
    /// will only be parsed once into [`LitStr`](LitStr). This may sound weird
    /// but it behaves exactly how you'd expect.
    value: V,
}

impl<V> Parse for AttrNameValue<V>
where
    V: Parse + 'static,
{
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(Self {
            name: input.parse()?,
            eq_token: input.parse()?,
            value: {
                // Fine, I'll just implement my own specialization
                if TypeId::of::<V>() == TypeId::of::<LitStr>() {
                    input.parse()?
                } else {
                    let x: LitStr = input.parse()?;
                    x.parse()?
                }
            },
        })
    }
}

/// Generate the `HashMap::insert` call for a particular struct field
fn gen_insert(field: &Field) -> TokenStream {
    let field_name =
        field.ident.as_ref().expect("guaranteed to exist by NamedFieldsStruct");

    let field_name_str =
        LitStr::new(&field_name.to_string(), field_name.span());

    // Get the first user-supplied value for `fmt`, or just use `"{}"` for the
    // Display impl of the field
    let fmt: TokenStream = field
        .attrs
        .iter()
        .find_map(|attr| {
            // Only interested in our own attributes
            //
            // #[far(fmt = "{}")]
            //   ^^^
            if !attr.path.is_ident("far") {
                return None;
            }

            let fmt = attr
                .parse_args::<AttrNameValue<LitStr>>()
                .map(|attr| {
                    // Make sure the user didn't supply an argument we don't
                    // know what to do with
                    //
                    // #[far(fmt = "{}")]
                    //       ^^^
                    if attr.name != "fmt" {
                        Error::new_spanned(attr.name, "unknown argument")
                            .into_compile_error()
                    } else {
                        attr.value.to_token_stream()
                    }
                })
                .unwrap_or_else(Error::into_compile_error);

            Some(fmt)
        })
        .unwrap_or_else(|| {
            LitStr::new("{}", field_name.span()).into_token_stream()
        });

    quote! {
        replace.insert(#field_name_str, format!(#fmt, self.#field_name));
    }
}

/// Generate the `Vec::push` call for a particular struct field
fn gen_key(field: &Field) -> TokenStream {
    let field_name =
        field.ident.as_ref().expect("guaranteed to exist by NamedFieldsStruct");

    let field_name_str =
        LitStr::new(&field_name.to_string(), field_name.span());

    quote! {
        keys.push(#field_name_str);
    }
}

// Weirdness occurs when setting docs on one item in multiple places. The real
// docs live in the `far` crate, since it can actually hyperlink to all of the
// relevant places.
#[allow(missing_docs)]
#[proc_macro_derive(Render, attributes(far))]
pub fn main(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let named_fields_struct = parse_macro_input!(input as NamedFieldsStruct);

    let struct_name = named_fields_struct.ident;

    // Determine the name of the crate that the items used should come from
    let crate_token = match crate_name("far") {
        // The crate defining the types is using this macro, use `crate`
        Ok(FoundCrate::Itself) => quote! { crate },

        // Some external crate is using this macro, use the normal crate name
        Ok(FoundCrate::Name(name)) => {
            let crate_name = format_ident!("{}", name);
            quote! { ::#crate_name }
        }

        Err(e) => panic!("failed to determine crate name: {}", e),
    };

    let inserts: TokenStream =
        named_fields_struct.fields.iter().flat_map(gen_insert).collect();

    let keys: TokenStream =
        named_fields_struct.fields.iter().flat_map(gen_key).collect();

    let replace_impl = quote! {
        impl #crate_token::Render for #struct_name {
            fn render(&self) -> std::collections::HashMap<&'static str, String>
            {
                let mut replace = std::collections::HashMap::new();

                #inserts

                replace
            }

            fn keys() -> Box<dyn Iterator<Item = &'static str>> {
                let mut keys = Vec::new();

                #keys

                Box::new(keys.into_iter())
            }
        }
    };

    let tokens = quote! {
        #replace_impl
    };

    tokens.into()
}