auto_context/
lib.rs

1//! Auto-add context to anyhow errors (without nightly).
2//!
3//! # Example
4//!
5//! ```no_run
6//! use anyhow::{Context, Result};
7//!
8//! struct Test {}
9//!
10//! #[auto_context::auto_context]
11//! impl Test {
12//!   fn some_method(self) -> Result<()> {
13//!     anyhow::bail!("some error")
14//!   }
15//!
16//!   fn some_function(_: i32) -> Result<()> {
17//!     Test {}.some_method()?;
18//!
19//!     Ok(())
20//!   }
21//! }
22//!
23//! #[auto_context::auto_context]
24//! fn main() -> Result<()> {
25//!   Test::some_function(123)?;
26//!
27//!   Ok(())
28//! }
29//! ```
30//! fails with
31//! ```text
32//! Error: Test::some_function(..) @ auto-context/src/lib.rs::23
33//!
34//! Caused by:
35//!     0: .some_method() @ auto-context/src/lib.rs::15
36//!     1: some error
37//! ```
38//!
39//! # Details
40//!
41//! The [`macro@auto_context`] proc macro can be used to annotate any item. This
42//! includes functions, methods, and struct/trait impls.
43//!
44//! Context is added to every [try expression] (every use of a `?`). Different
45//! kinds of expressions result in different context formats:
46//!
47//! - method calls: `.method_name(args)`
48//! - function calls: `some::func(args)`
49//! - identifiers: `xyz`
50//! - expression calls: `(.. some expr ..)`
51//!
52//! where `args` is `..` if arguments are present and is empty otherwise.
53//!
54//! [try expression]: syn::ExprTry
55
56use proc_macro::TokenStream;
57use quote::quote;
58use syn::{parse_macro_input, visit_mut::VisitMut, Expr, Item, Path};
59
60struct AutoContext;
61
62impl VisitMut for AutoContext {
63  fn visit_expr_mut(&mut self, expr: &mut Expr) {
64    if let Expr::Try(expr_try) = expr {
65      let context = anyhow_context(&expr_try.expr);
66      let inner = &expr_try.expr;
67      let span = expr_try.question_token.spans[0];
68
69      // thank you @t6 for thinking of `quote_spanned!`.
70      *expr = syn::parse_quote_spanned! { span=>
71          (#inner).context(format!("{} @ {}::{}", #context, file!(), line!()))?
72      };
73    }
74    syn::visit_mut::visit_expr_mut(self, expr);
75  }
76}
77
78// Converts a path to a string roughly representing the source it came from.
79fn path_to_string(path: &Path) -> String {
80  path
81    .segments
82    .iter()
83    .map(|s| format!("{}", s.ident))
84    .collect::<Vec<String>>()
85    .join("::")
86}
87
88/// Returns a best-effort context for an expression.
89fn anyhow_context(expr: &Expr) -> String {
90  match expr {
91    Expr::MethodCall(method_call) => {
92      let name = method_call.method.to_string();
93      let args = if method_call.args.is_empty() { "" } else { ".." };
94
95      format!(".{name}({args})")
96    }
97
98    Expr::Call(call) => {
99      if let Expr::Path(path) = &*call.func {
100        let path = path_to_string(&path.path);
101        let args = if call.args.is_empty() { "" } else { ".." };
102
103        format!("{path}({args})")
104      } else {
105        "(.. some expression ..)".to_string()
106      }
107    }
108
109    Expr::Path(path) => path_to_string(&path.path),
110
111    _ => "(.. some expr ..)".to_string(),
112  }
113}
114
115/// Auto-adds sensible context to [anyhow] errors.
116///
117/// Different expressions result in different context formats:
118///
119/// See the [module documentation] for examples.
120///
121/// [anyhow]: https://docs.rs/anyhow
122/// [module documentation]: self
123#[proc_macro_attribute]
124pub fn auto_context(_attr: TokenStream, item: TokenStream) -> TokenStream {
125  let mut item = parse_macro_input!(item as Item);
126
127  AutoContext.visit_item_mut(&mut item);
128
129  TokenStream::from(quote! { #item })
130}