log_termination/
lib.rs

1//! This attribute proc macro should be applied to main functions of the form
2//! `main() -> Result<(), Box<dyn Error>>`; it wraps the returned value in a
3//! newtype which implements [`Termination`] and displays the returned error via
4//! [`error!`].  This macro exists purely for convenience.
5//!
6//! Your create needs the [`try_trait`] and [`termination_trait_lib`] features.
7//!
8//! For example:
9//!
10//! ```
11//! #![feature(try_trait)]
12//! #![feature(termination_trait_lib)]
13//!
14//! use std::error::Error;
15//! use std::io::ErrorKind;
16//!
17//! #[macro_use]
18//! extern crate log;
19//! extern crate fern;
20//!
21//! extern crate log_termination;
22//! use log_termination::log_termination;
23//!
24//! #[log_termination]
25//! fn main() -> Result<(), Box<dyn Error>> {
26//!   fern::Dispatch::new()
27//!     .format(|o, m, r| { o.finish(format_args!(
28//!       "[{}:{}] {} {}: {}",
29//!       r.file().unwrap(),
30//!       r.line().unwrap(),
31//!       chrono::Local::now().format("%F %T"),
32//!       r.level(),
33//!       m
34//!     ))})
35//!     .level(log::LevelFilter::Error)
36//!     .chain(std::io::stderr())
37//!     .apply()?;
38//!
39//!   return Err(Box::new(std::io::Error::new(ErrorKind::Other, "SNAFU")));
40//!
41//!   // This code is valid, but unreachable:
42//!   //Ok(())
43//! }
44//! ```
45//!
46//! [`Termination`]: std::process::Termination
47//! [`error!`]: https://docs.rs/log/latest/log/macro.error.html
48//! [`try_trait`]: std::ops::Try
49//! [`termination_trait_lib`]: std::process::Termination
50
51
52use std::string::ToString;
53use std::iter::FromIterator;
54
55extern crate proc_macro;
56use proc_macro::TokenStream;
57
58use proc_macro2::Span;
59use proc_macro2::Ident;
60
61use quote::{quote, quote_spanned};
62
63use syn::{Expr, ExprCall, ExprPath, ExprReturn};
64use syn::{Type, TypeTuple};
65use syn::punctuated::Punctuated;
66use syn::token::Paren;
67use syn::spanned::Spanned;
68use syn::visit_mut::VisitMut;
69
70use proc_macro_error::*;
71
72
73struct ReturnTypeMapper(syn::Path, bool);
74
75impl ReturnTypeMapper {
76  fn new(p: syn::Path) -> Self {
77    Self(p, false)
78  }
79
80  fn wrap_expr_with_type(&self, e: &Expr) -> Expr {
81    Expr::Call(ExprCall {
82      attrs:       vec![],
83      paren_token: Paren { span: Span::call_site() },
84      func:        Box::new(Expr::Path(ExprPath {
85        attrs: vec![],
86        qself: None,
87        path:  self.0.clone(),
88      })),
89      args:        Punctuated::from_iter(vec![e.clone()]),
90    })
91  }
92}
93
94impl syn::visit_mut::VisitMut for ReturnTypeMapper {
95  fn visit_signature_mut(&mut self, node: &mut syn::Signature) {
96    node.output = syn::ReturnType::Type(
97      syn::Token![->](Span::call_site()),
98      Box::new(syn::Type::Path(syn::TypePath {
99        qself: None,
100        path:  self.0.clone(),
101      })),
102    );
103  }
104
105  fn visit_expr_return_mut(&mut self, expr: &mut ExprReturn) {
106    expr.expr = Some(Box::new(
107      self.wrap_expr_with_type(&*(expr.expr.as_ref().unwrap())),
108    ));
109  }
110
111
112  fn visit_expr_mut(&mut self, node: &mut Expr) {
113    if let Expr::Closure(_) = &node {
114      // We don't want to descend into other syntactic scopes which have their
115      // own return statements; this means nested function items and closures.
116      return;
117    }
118
119    syn::visit_mut::visit_expr_mut(self, node);
120  }
121
122  fn visit_item_fn_mut(&mut self, node: &mut syn::ItemFn) {
123    if self.1 {
124      // See visit_expr_mut above.
125      return;
126    }
127
128    // If the last statement is an expression, it's an implicit return and must
129    // be wrapped.
130    let stmts_len = node.block.stmts.len();
131    if let syn::Stmt::Expr(e) = node.block.stmts[stmts_len-1].clone() {
132      node.block.stmts[stmts_len-1] = syn::Stmt::Expr(self.wrap_expr_with_type(&e));
133    }
134
135    self.1 = true;
136    syn::visit_mut::visit_item_fn_mut(self, node);
137  }
138}
139
140
141fn gensym(s: &str, u: &uuid::Uuid) -> Ident {
142  Ident::new(
143    &*format!("{}_mygensym_{}", s, u.to_simple()),
144    Span::call_site(),
145  )
146}
147
148
149#[proc_macro_attribute]
150#[proc_macro_error]
151pub fn log_termination(_attr: TokenStream, item: TokenStream) -> TokenStream {
152  let input_i = syn::parse_macro_input!(item as syn::Item);
153
154  let input_i = match input_i {
155    syn::Item::Fn(i) => i,
156    _ => abort!(Span::call_site(),
157      "#[log_termination] item is not a function")
158  };
159
160  if !input_i.sig.inputs.is_empty() {
161    emit_warning!(input_i.sig.inputs.span(),
162      "#[log_termination] function has parameters");
163  }
164
165  let return_tp = match &input_i.sig.output {
166    syn::ReturnType::Default => Type::Tuple(TypeTuple {
167      paren_token: syn::token::Paren(Span::call_site()),
168      elems: Punctuated::default()
169    }),
170    syn::ReturnType::Type(_, tp) => *tp.clone()
171  };
172
173  let return_tp_str = &quote!(#return_tp).to_string();
174  let return_tp_str_escaped = {
175    let not_alnum = regex::Regex::new(r"[^A-Za-z0-9]").unwrap();
176    let underscore = regex::Regex::new(r"_+").unwrap();
177    let no_dangerous_chars = &*not_alnum.replace_all(return_tp_str, "_").into_owned();
178
179    underscore.replace_all(no_dangerous_chars, "_").into_owned()
180  };
181  let quit_ident = gensym(&*format!("_log_termination_{}", return_tp_str_escaped), &uuid::Uuid::new_v4());
182
183  let mut main = input_i;
184
185  let new_return_tp = syn::Path {
186    leading_colon: None,
187    segments:      Punctuated::from_iter(
188      std::vec![
189        syn::PathSegment {
190          arguments: syn::PathArguments::AngleBracketed(
191            syn::AngleBracketedGenericArguments {
192              colon2_token: Some(syn::Token![::](Span::call_site())),
193              lt_token: syn::Token![<](Span::call_site()),
194              args: Punctuated::from_iter(vec![
195                syn::GenericArgument::Type(return_tp)
196              ]),
197              gt_token: syn::Token![>](Span::call_site())
198            },
199          ),
200          ident: quit_ident.clone()
201        }
202      ]
203    )
204  };
205  let mut visitor = ReturnTypeMapper::new(new_return_tp);
206  visitor.visit_item_fn_mut(&mut main);
207
208
209  let expanded = quote_spanned! {main.span()=>
210    struct #quit_ident<T: std::ops::Try + std::process::Termination>
211    ( T );
212
213    impl<T: std::ops::Try + std::process::Termination> std::convert::From<T> for #quit_ident<T>
214    where
215      T : std::ops::Try + std::process::Termination
216    {
217      fn from(from: T) -> Self {
218        Self(from)
219      }
220    }
221
222    impl<T: std::ops::Try + std::process::Termination> std::ops::Try for #quit_ident<T>
223    where
224      T : std::ops::Try + std::process::Termination
225    {
226      type Ok    = <T as std::ops::Try>::Ok;
227      type Error = <T as std::ops::Try>::Error;
228
229      fn into_result(self) -> std::result::Result<Self::Ok, Self::Error> {
230        self.0.into_result()
231      }
232
233      fn from_error(e: Self::Error) -> Self {
234        Self(<T as std::ops::Try>::from_error(e))
235      }
236
237      fn from_ok(o: Self::Ok) -> Self {
238        Self(<T as std::ops::Try>::from_ok(o))
239      }
240    }
241
242    impl<T: std::ops::Try + std::process::Termination> std::process::Termination for #quit_ident<T>
243    where
244      T : std::ops::Try + std::process::Termination,
245      <T as std::ops::Try>::Error: std::fmt::Display
246    {
247      fn report(self) -> i32 {
248        if let std::result::Result::Err(e) = self.0.into_result() {
249          log::error!("{}", e);
250          return 1;
251        }
252
253        0
254      }
255    }
256
257    #main
258  };
259
260
261  proc_macro::TokenStream::from(expanded)
262}