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:
方法本身就具备了异常反馈。: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
解析,因此其中传入的span
是list
。
也就是整个each
的位置路径。
仔细观察一下报错信息
error: expected `builder(each = "...")`
--> tests/08-unrecognized-attribute.rs:22:7
|
22 | #[builder(eac = "arg")]
| ^^^^^^^^^^^^^^^^^^^^
上面的^
符号提示的限定位置,如果是attr
本身的话就必须包括外面的括号。
尤其是,你可以将报错信息更加具体的指向eac
,只要把span
传入修改成kv
的key
的位置即可。
整体
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 = "e::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 协议》,转载必须注明作者和本文链接