Skip to content

Commit

Permalink
add fn error context
Browse files Browse the repository at this point in the history
  • Loading branch information
kariy committed Apr 3, 2024
1 parent 4a946b2 commit f1c9066
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 25 deletions.
58 changes: 34 additions & 24 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = [ "bin", "crates/args", "crates/ops", "crates/rika", "crates/waiter" ]
members = [ "bin", "crates/args", "crates/context-macro", "crates/ops", "crates/rika", "crates/waiter" ]
resolver = "2"

[workspace.package]
Expand Down
16 changes: 16 additions & 0 deletions crates/context-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
description = "An attribute macro to add context to errors from a function."
edition = "2018"
name = "fn-error-context"
readme = "README.md"
version = "0.1.0"

[lib]
name = "fn_error_context"
proc-macro = true

[dependencies]
eyre.workspace = true
proc-macro2 = "1.0.79"
quote = "1.0.35"
syn = { version = "2.0.57", features = [ "full" ] }
124 changes: 124 additions & 0 deletions crates/context-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! This crate provides the [`context`](attr.context.html) macro for adding extra error
//! information to a function.
//!
//! Works with [`anyhow`], [`failure`] and any other error type which
//! provides a `context` method taking a string.
//!
//! ```
//! # use std::fs::read_to_string;
//! # use std::path::Path;
//! # use std::io;
//! #
//! use fn_error_context::context;
//!
//! #[context("failed to parse config at `{}`", path.as_ref().display())]
//! pub fn parse_config(path: impl AsRef<Path>) -> anyhow::Result<u32> {
//! let text = read_to_string(path.as_ref())?;
//! Ok(text.parse()?)
//! }
//!
//! let error = parse_config("not-found").unwrap_err();
//! assert_eq!(
//! error.to_string(),
//! "failed to parse config at `not-found`",
//! );
//! assert_eq!(
//! error.source().unwrap().downcast_ref::<io::Error>().unwrap().kind(),
//! io::ErrorKind::NotFound,
//! );
//! ```
//!
//! [`anyhow`]: https://crates.io/crates/anyhow
//! [`failure`]: https://crates.io/crates/failure

#![doc(html_root_url = "https://docs.rs/fn-error-context/0.2.1")]
#![deny(missing_docs)]

extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::parse::{self, Parse, ParseStream};
use syn::Token;

/// Add context to errors from a function.
///
/// The arguments to this macro are a format string with arguments using
/// the standard `std::fmt` syntax. Arguments to the function can be used
/// as long as they are not consumed in the function body.
///
/// This macro desugars to something like
/// ```
/// # fn function_body() -> anyhow::Result<()> { unimplemented!() }
/// #
/// pub fn function() -> anyhow::Result<()> {
/// (|| -> anyhow::Result<()> {
/// function_body()
/// })().map_err(|err| err.context("context"))
/// }
/// ```
///
/// Sometimes you will receive borrowck errors, especially when returning references. These can
/// often be fixed by setting the `move` option of the attribute macro. For example:
///
/// ```
/// use fn_error_context::context;
///
/// #[context(move, "context")]
/// fn returns_reference(val: &mut u32) -> anyhow::Result<&mut u32> {
/// Ok(&mut *val)
/// }
/// ```
#[proc_macro_attribute]
pub fn context(args: TokenStream, input: TokenStream) -> TokenStream {
let Args(move_token, format_args) = syn::parse_macro_input!(args);
let mut input = syn::parse_macro_input!(input as syn::ItemFn);

let body = &input.block;
let return_ty = &input.sig.output;
let err = Ident::new("err", Span::mixed_site());
let new_body = if input.sig.asyncness.is_some() {
let return_ty = match return_ty {
syn::ReturnType::Default => {
return syn::Error::new_spanned(input, "function should return Result")
.to_compile_error()
.into();
}
syn::ReturnType::Type(_, return_ty) => return_ty,
};
let result = Ident::new("result", Span::mixed_site());
quote! {
let #result: #return_ty = async #move_token { #body }.await;
#result.map_err(|#err| ::eyre::Context.context(#err, format!(#format_args)).into())
}
} else {
let force_fn_once = Ident::new("force_fn_once", Span::mixed_site());
quote! {
// Moving a non-`Copy` value into the closure tells borrowck to always treat the closure
// as a `FnOnce`, preventing some borrowing errors.
let #force_fn_once = ::core::iter::empty::<()>();
(#move_token || #return_ty {
::core::mem::drop(#force_fn_once);
#body
})().map_err(|#err| ::eyre::Context.context(#err, format!(#format_args)).into())
}
};
input.block.stmts = vec![syn::Stmt::Expr(syn::Expr::Verbatim(new_body), None)];

input.into_token_stream().into()
}

struct Args(Option<Token![move]>, TokenStream2);
impl Parse for Args {
fn parse(input: ParseStream<'_>) -> parse::Result<Self> {
let move_token = if input.peek(Token![move]) {
let token = input.parse()?;
input.parse::<Token![,]>()?;
Some(token)
} else {
None
};
Ok(Self(move_token, input.parse()?))
}
}

0 comments on commit f1c9066

Please sign in to comment.