proc-macro-workshop: builder-8

审题

// Ensure that your macro reports a reasonable error message when the caller
// mistypes the inert attribute in various ways. This is a compile_fail test.
//
// The preferred way to report an error from a procedural macro is by including
// an invocation of the standard library's compile_error macro in the code
// emitted by the procedural macro.
//
//
// Resources:
//
//   - The compile_error macro for emitting basic custom errors:
//     https://doc.rust-lang.org/std/macro.compile_error.html
//
//   - Lowering a syn::Error into an invocation of compile_error:
//     https://docs.rs/syn/1.0/syn/struct.Error.html#method.to_compile_error

use derive_builder::Builder;

#[derive(Builder)]
pub struct Command {
    executable: String,
    #[builder(eac = "arg")]
    args: Vec<String>,
    env: Vec<String>,
    current_dir: Option<String>,
}

fn main() {}

这道题主要的就是如何报错。
相关的参考链接如下

但是比起直接的to_compile_error,其中更加关键的是报错的位置。
我们平时不关注的span,也就是代码所处的位置,在这里就凸显的比较重要。
毕竟,错误相关的几点

  • 错误信息
  • 错误类型
  • 错误位置

能够精准的锁定错误位置,对我们的帮助不言而喻。

题解

// solution8.rs
pub(super) fn solution(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> syn::Result<proc_macro2::TokenStream> {
    let mut token_stream = proc_macro2::TokenStream::new();

    // solution2
    let solution2_stream = solution2(fields, builder_ident, origin_ident)?;
    token_stream.extend(solution2_stream);

    // solution3
    let solution3_stream = solution3(fields, builder_ident, origin_ident)?;
    token_stream.extend(solution3_stream);

    // solution45
    let solution45_stream = solution45(fields, builder_ident, origin_ident)?;
    token_stream.extend(solution45_stream);

    syn::Result::Ok(token_stream)
}

fn solution2(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> syn::Result<proc_macro2::TokenStream> {
    let struct_field_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Option")
            {
                quote::quote! {
                    pub #ident: #ty
                }
            } else if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Vec")
            {
                quote::quote! {
                    pub #ident: #ty
                }
            } else {
                quote::quote! {
                    pub #ident: std::option::Option<#ty>
                }
            }
        })
        .collect();

    let construct_field_stream_vec: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            if let std::option::Option::Some(_) = crate::common::option_type_with_ident(ty, "Vec") {
                quote::quote! {
                    #ident: vec![]
                }
            } else {
                quote::quote! {
                    #ident: std::option::Option::None
                }
            }
        })
        .collect();
    syn::Result::Ok(quote::quote! {
        pub struct #builder_ident {
            #(
                #struct_field_stream_vec
            ),*
        }

        impl #origin_ident {
            pub fn builder() -> #builder_ident {
                #builder_ident {
                    #(
                        #construct_field_stream_vec
                    ),*
                }
            }
        }
    })
}

fn solution3(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    _: &syn::Ident,
) -> syn::Result<proc_macro2::TokenStream> {
    let setter_stream_vec_result: syn::Result<Vec<proc_macro2::TokenStream>> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            if let std::option::Option::Some(inner_type) =
                crate::common::option_type_with_ident(ty, "Option")
            {
                syn::Result::Ok(quote::quote! {
                    pub fn #ident(&mut self, #ident: #inner_type) -> &mut Self {
                        self.#ident = std::option::Option::Some(#ident);
                        self
                    }
                })
            } else if let std::option::Option::Some(inner_type) =
                crate::common::option_type_with_ident(ty, "Vec")
            {
                if let std::option::Option::Some(ref each_method_ident) =
                    crate::common::each_method_result(f)?
                {
                    let mut each_method_stream = quote::quote! {
                        pub fn #each_method_ident(&mut self, #ident: #inner_type) -> &mut Self {
                            self.#ident.push(#ident);
                            self
                        }
                    };
                    if ident.as_ref().unwrap() != each_method_ident {
                        let origin_setter_stream = quote::quote! {
                            pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                                self.#ident = #ident;
                                self
                            }
                        };
                        each_method_stream.extend(origin_setter_stream);
                    }
                    syn::Result::Ok(each_method_stream)
                } else {
                    syn::Result::Ok(quote::quote! {
                        pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                            self.#ident = #ident;
                            self
                        }
                    })
                }
            } else {
                syn::Result::Ok(quote::quote! {
                    pub fn #ident(&mut self, #ident: #ty) -> &mut Self {
                        self.#ident = std::option::Option::Some(#ident);
                        self
                    }
                })
            }
        })
        .collect();
    let setter_stream_vec = setter_stream_vec_result?;
    syn::Result::Ok(quote::quote! {
        impl #builder_ident {
            #(
                #setter_stream_vec
            )*
        }
    })
}

fn solution45(
    fields: &crate::common::FieldsType,
    builder_ident: &syn::Ident,
    origin_ident: &syn::Ident,
) -> syn::Result<proc_macro2::TokenStream> {
    let construct_if_stream_vec: Vec<_> = fields
        .iter()
        .filter(|f| {
            let option_field = crate::common::option_type_with_ident(&f.ty, "Option");
            let vec_field = crate::common::option_type_with_ident(&f.ty, "Vec");
            option_field.is_none() && vec_field.is_none()
        })
        .map(|f| {
            let ident = &f.ident;
            quote::quote! {
                if self.#ident.is_none() {
                    let err = format!("field {} is missing", stringify!(#ident));
                    return std::result::Result::Err(err.into());
                }
            }
        })
        .collect();

    let construct_stream: Vec<_> = fields
        .iter()
        .map(|f| {
            let ident = &f.ident;
            let ty = &f.ty;
            if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Option")
            {
                quote::quote! {
                    #ident: self.#ident.clone()
                }
            } else if let std::option::Option::Some(_) =
                crate::common::option_type_with_ident(ty, "Vec")
            {
                quote::quote! {
                    #ident: self.#ident.clone()
                }
            } else {
                quote::quote! {
                    #ident: self.#ident.clone().unwrap()
                }
            }
        })
        .collect();
    syn::Result::Ok(quote::quote! {
        impl #builder_ident {
            pub fn build(&self) -> std::result::Result<#origin_ident, std::boxed::Box<dyn std::error::Error>> {
                #(
                    #construct_if_stream_vec
                )*

                let res = #origin_ident {
                    #(
                        #construct_stream
                    ),*
                };
                std::result::Result::Ok(res)
            }
        }
    })
}

整体来说,这道题主要的考察是对builder(each = "...")的精准定位和拼写检查。
和第七关比起来,不过是返回的结果变成了Result<T>
这是因为我们底层的crate::common::each_method_result方法本身就具备了异常反馈。
可以回顾一下错误提示

pub(crate) fn each_method_result(
    field: &syn::Field,
) -> syn::Result<std::option::Option<syn::Ident>> {
    for attr in &field.attrs {
        if let std::result::Result::Ok(syn::Meta::List(syn::MetaList {
            ref path,
            ref nested,
            ..
        })) = attr.parse_meta()
        {
            if let Some(p) = path.segments.first() {
                if p.ident == "builder" {
                    if let Some(syn::NestedMeta::Meta(syn::Meta::NameValue(kv))) = nested.first() {
                        if kv.path.is_ident("each") {
                            if let syn::Lit::Str(ref ident_str) = kv.lit {
                                return syn::Result::Ok(std::option::Option::Some(
                                    syn::Ident::new(ident_str.value().as_str(), attr.span()),
                                ));
                            }
                        } else {
                            if let std::result::Result::Ok(syn::Meta::List(ref list)) =
                                attr.parse_meta()
                            {
                                return syn::Result::Err(syn::Error::new_spanned(
                                    list,
                                    r#"expected `builder(each = "...")`"#,
                                ));
                            }
                        }
                    }
                }
            }
        }
    }
    syn::Result::Ok(std::option::Option::None)
}

这个提示信息需要定制,可以参考对比的提示信息。
其中关键的位置则是meta解析,因此其中传入的spanlist
也就是整个each的位置路径。
仔细观察一下报错信息

error: expected `builder(each = "...")`
  --> tests/08-unrecognized-attribute.rs:22:7
   |
22 |     #[builder(eac = "arg")]
   |       ^^^^^^^^^^^^^^^^^^^^

上面的^符号提示的限定位置,如果是attr本身的话就必须包括外面的括号。
尤其是,你可以将报错信息更加具体的指向eac,只要把span传入修改成kvkey的位置即可。

整体

mod common;
mod solution2;
mod solution35;
mod solution4;
mod solution6;
mod solution7;
mod solution8;

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    solution1(input)
}

fn solution1(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
    let fields = {
        match common::parse_fields(&ast) {
            Ok(f) => f,
            Err(_e) => std::panic!(std::stringify!(_e)),
        }
    };

    let origin_ident = &ast.ident;
    let builder_ident = &quote::format_ident!("{}Builder", origin_ident);
    let mut token_stream = proc_macro2::TokenStream::new();

    // solution2
    let solution2_stream = solution2::solution(fields, origin_ident, builder_ident);
    token_stream.extend(solution2_stream);

    // solution3
    let solution35_stream = solution35::soultion(fields, builder_ident);
    token_stream.extend(solution35_stream);

    // solution45
    let solution4_stream = solution4::solution(fields, builder_ident, origin_ident);
    token_stream.extend(solution4_stream);

    // solution6
    let _ = solution6::solution(fields, builder_ident, origin_ident);

    // solution7
    let _ = solution7::solution(fields, builder_ident, origin_ident);

    match solution8::solution(fields, builder_ident, origin_ident) {
        std::result::Result::Ok(res) => {
            token_stream = res;
        }
        std::result::Result::Err(e) => {
            return e.into_compile_error().into();
        }
    }

    proc_macro::TokenStream::from(token_stream)
}

就这样,整体代码就完成了。

悄悄剧透一下,至此builder就已经全部完成了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!