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}