fn_error_context/
lib.rs

1//! This crate provides the [`context`](attr.context.html) macro for adding extra error
2//! information to a function.
3//!
4//! Works with [`anyhow`], [`failure`] and any other error type which
5//! provides a `context` method taking a string.
6//!
7//! ```
8//! # use std::fs::read_to_string;
9//! # use std::path::Path;
10//! # use std::io;
11//! #
12//! use fn_error_context::context;
13//!
14//! #[context("failed to parse config at `{}`", path.as_ref().display())]
15//! pub fn parse_config(path: impl AsRef<Path>) -> anyhow::Result<u32> {
16//!     let text = read_to_string(path.as_ref())?;
17//!     Ok(text.parse()?)
18//! }
19//!
20//! let error = parse_config("not-found").unwrap_err();
21//! assert_eq!(
22//!     error.to_string(),
23//!     "failed to parse config at `not-found`",
24//! );
25//! assert_eq!(
26//!     error.source().unwrap().downcast_ref::<io::Error>().unwrap().kind(),
27//!     io::ErrorKind::NotFound,
28//! );
29//! ```
30//!
31//! [`anyhow`]: https://crates.io/crates/anyhow
32//! [`failure`]: https://crates.io/crates/failure
33
34#![doc(html_root_url = "https://docs.rs/fn-error-context/0.2.1")]
35#![deny(missing_docs)]
36
37extern crate proc_macro;
38
39use proc_macro::TokenStream;
40use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
41use quote::{quote, ToTokens};
42use syn::parse::{self, Parse, ParseStream};
43use syn::Token;
44
45/// Add context to errors from a function.
46///
47/// The arguments to this macro are a format string with arguments using
48/// the standard `std::fmt` syntax. Arguments to the function can be used
49/// as long as they are not consumed in the function body.
50///
51/// This macro desugars to something like
52/// ```
53/// # fn function_body() -> anyhow::Result<()> { unimplemented!() }
54/// #
55/// pub fn function() -> anyhow::Result<()> {
56///     (|| -> anyhow::Result<()> {
57///         function_body()
58///     })().map_err(|err| err.context("context"))
59/// }
60/// ```
61///
62/// Sometimes you will receive borrowck errors, especially when returning references. These can
63/// often be fixed by setting the `move` option of the attribute macro. For example:
64///
65/// ```
66/// use fn_error_context::context;
67///
68/// #[context(move, "context")]
69/// fn returns_reference(val: &mut u32) -> anyhow::Result<&mut u32> {
70///     Ok(&mut *val)
71/// }
72/// ```
73#[proc_macro_attribute]
74pub fn context(args: TokenStream, input: TokenStream) -> TokenStream {
75    let Args(move_token, format_args) = syn::parse_macro_input!(args);
76    let mut input = syn::parse_macro_input!(input as syn::ItemFn);
77
78    let body = &input.block;
79    let return_ty = &input.sig.output;
80    let err = Ident::new("err", Span::mixed_site());
81    let new_body = if input.sig.asyncness.is_some() {
82        let return_ty = match return_ty {
83            syn::ReturnType::Default => {
84                return syn::Error::new_spanned(input, "function should return Result")
85                    .to_compile_error()
86                    .into();
87            }
88            syn::ReturnType::Type(_, return_ty) => return_ty,
89        };
90        let result = Ident::new("result", Span::mixed_site());
91        quote! {
92            let #result: #return_ty = async #move_token { #body }.await;
93            #result.map_err(|#err| #err.context(format!(#format_args)).into())
94        }
95    } else {
96        let force_fn_once = Ident::new("force_fn_once", Span::mixed_site());
97        quote! {
98            // Moving a non-`Copy` value into the closure tells borrowck to always treat the closure
99            // as a `FnOnce`, preventing some borrowing errors.
100            let #force_fn_once = ::core::iter::empty::<()>();
101            (#move_token || #return_ty {
102                ::core::mem::drop(#force_fn_once);
103                #body
104            })().map_err(|#err| #err.context(format!(#format_args)).into())
105        }
106    };
107    input.block.stmts = vec![syn::Stmt::Expr(syn::Expr::Verbatim(new_body), None)];
108
109    input.into_token_stream().into()
110}
111
112struct Args(Option<Token![move]>, TokenStream2);
113impl Parse for Args {
114    fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
115        let move_token = if input.peek(Token![move]) {
116            let token = input.parse()?;
117            input.parse::<Token![,]>()?;
118            Some(token)
119        } else {
120            None
121        };
122        Ok(Self(move_token, input.parse()?))
123    }
124}