no_panic/lib.rs
1//! [![github]](https://github.com/dtolnay/no-panic) [![crates-io]](https://crates.io/crates/no-panic) [![docs-rs]](https://docs.rs/no-panic)
2//!
3//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github
4//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust
5//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs
6//!
7//! <br>
8//!
9//! A Rust attribute macro to require that the compiler prove a function can't
10//! ever panic.
11//!
12//! ```toml
13//! [dependencies]
14//! no-panic = "0.1"
15//! ```
16//!
17//! ```
18//! use no_panic::no_panic;
19//!
20//! #[no_panic]
21//! fn demo(s: &str) -> &str {
22//! &s[1..]
23//! }
24//!
25//! fn main() {
26//! # fn demo(s: &str) -> &str {
27//! # &s[1..]
28//! # }
29//! #
30//! println!("{}", demo("input string"));
31//! }
32//! ```
33//!
34//! If the function does panic (or the compiler fails to prove that the function
35//! cannot panic), the program fails to compile with a linker error that
36//! identifies the function name. Let's trigger that by passing a string that
37//! cannot be sliced at the first byte:
38//!
39//! ```should_panic
40//! # fn demo(s: &str) -> &str {
41//! # &s[1..]
42//! # }
43//! #
44//! fn main() {
45//! println!("{}", demo("\u{1f980}input string"));
46//! }
47//! ```
48//!
49//! ```console
50//! Compiling no-panic-demo v0.0.1
51//! error: linking with `cc` failed: exit code: 1
52//! |
53//! = note: /no-panic-demo/target/release/deps/no_panic_demo-7170785b672ae322.no_p
54//! anic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs.rcgu.o: In function `_$LT$no_pani
55//! c_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Drop$GT$::drop::h72f8f423002
56//! b8d9f':
57//! no_panic_demo1-cba7f4b666ccdbcbbf02b7348e5df1b2.rs:(.text._ZN72_$LT$no
58//! _panic_demo..demo..__NoPanic$u20$as$u20$core..ops..drop..Drop$GT$4drop17h72f8f42
59//! 3002b8d9fE+0x2): undefined reference to `
60//!
61//! ERROR[no-panic]: detected panic in function `demo`
62//! '
63//! collect2: error: ld returned 1 exit status
64//! ```
65//!
66//! The error is not stellar but notice the ERROR\[no-panic\] part at the end
67//! that provides the name of the offending function.
68//!
69//! <br>
70//!
71//! ## Caveats
72//!
73//! - Functions that require some amount of optimization to prove that they do
74//! not panic may no longer compile in debug mode after being marked
75//! `#[no_panic]`.
76//!
77//! - Panic detection happens at link time across the entire dependency graph,
78//! so any Cargo commands that do not invoke a linker will not trigger panic
79//! detection. This includes `cargo build` of library crates and `cargo check`
80//! of binary and library crates.
81//!
82//! - The attribute is useless in code built with `panic = "abort"`. Code must
83//! be built with `panic = "unwind"` (the default) in order for any panics to
84//! be detected. After confirming absence of panics, you can of course still
85//! ship your software as a `panic = "abort"` build.
86//!
87//! - Const functions are not supported. The attribute will fail to compile if
88//! placed on a `const fn`.
89//!
90//! If you find that code requires optimization to pass `#[no_panic]`, either
91//! make no-panic an optional dependency that you only enable in release builds,
92//! or add a section like the following to your Cargo.toml or .cargo/config.toml
93//! to enable very basic optimization in debug builds.
94//!
95//! ```toml
96//! [profile.dev]
97//! opt-level = 1
98//! ```
99//!
100//! If the code that you need to prove isn't panicking makes function calls to
101//! non-generic non-inline functions from a different crate, you may need thin
102//! LTO enabled for the linker to deduce those do not panic.
103//!
104//! ```toml
105//! [profile.release]
106//! lto = "thin"
107//! ```
108//!
109//! If thin LTO isn't cutting it, the next thing to try would be fat LTO with a
110//! single codegen unit:
111//!
112//! ```toml
113//! [profile.release]
114//! lto = "fat"
115//! codegen-units = 1
116//! ```
117//!
118//! If you want no_panic to just assume that some function you call doesn't
119//! panic, and get Undefined Behavior if it does at runtime, see
120//! [dtolnay/no-panic#16]; try wrapping that call in an `unsafe extern "C"`
121//! wrapper.
122//!
123//! [dtolnay/no-panic#16]: https://github.com/dtolnay/no-panic/issues/16
124//!
125//! <br>
126//!
127//! ## Acknowledgments
128//!
129//! The linker error technique is based on [Kixunil]'s crate [`dont_panic`].
130//! Check out that crate for other convenient ways to require absence of panics.
131//!
132//! [Kixunil]: https://github.com/Kixunil
133//! [`dont_panic`]: https://github.com/Kixunil/dont_panic
134
135#![doc(html_root_url = "https://docs.rs/no-panic/0.1.35")]
136#![allow(
137 clippy::doc_markdown,
138 clippy::match_same_arms,
139 clippy::missing_panics_doc
140)]
141#![cfg_attr(all(test, exhaustive), feature(non_exhaustive_omitted_patterns_lint))]
142
143extern crate proc_macro;
144
145use proc_macro::TokenStream;
146use proc_macro2::{Span, TokenStream as TokenStream2};
147use quote::quote;
148use syn::parse::{Error, Nothing, Result};
149use syn::{
150 parse_quote, FnArg, GenericArgument, Ident, ItemFn, Pat, PatType, Path, PathArguments,
151 ReturnType, Token, Type, TypeInfer, TypeParamBound,
152};
153
154#[proc_macro_attribute]
155pub fn no_panic(args: TokenStream, input: TokenStream) -> TokenStream {
156 let args = TokenStream2::from(args);
157 let input = TokenStream2::from(input);
158 TokenStream::from(match parse(args, input.clone()) {
159 Ok(function) => {
160 let expanded = expand_no_panic(function);
161 quote! {
162 #[cfg(not(doc))]
163 #expanded
164 // Keep generated parameter names out of doc builds.
165 #[cfg(doc)]
166 #input
167 }
168 }
169 Err(parse_error) => {
170 let compile_error = parse_error.to_compile_error();
171 quote! {
172 #compile_error
173 #input
174 }
175 }
176 })
177}
178
179fn parse(args: TokenStream2, input: TokenStream2) -> Result<ItemFn> {
180 let function: ItemFn = syn::parse2(input)?;
181 let _: Nothing = syn::parse2::<Nothing>(args)?;
182 if function.sig.constness.is_some() {
183 return Err(Error::new(
184 Span::call_site(),
185 "no_panic attribute on const fn is not supported",
186 ));
187 }
188 if function.sig.asyncness.is_some() {
189 return Err(Error::new(
190 Span::call_site(),
191 "no_panic attribute on async fn is not supported",
192 ));
193 }
194 Ok(function)
195}
196
197// Convert `Path<impl Trait>` to `Path<_>`
198fn make_impl_trait_wild(ret: &mut Type) {
199 match ret {
200 #![cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))]
201 Type::ImplTrait(impl_trait) => {
202 *ret = Type::Infer(TypeInfer {
203 underscore_token: Token,
204 });
205 }
206 Type::Array(ret) => make_impl_trait_wild(&mut ret.elem),
207 Type::Group(ret) => make_impl_trait_wild(&mut ret.elem),
208 Type::Paren(ret) => make_impl_trait_wild(&mut ret.elem),
209 Type::Path(ret) => make_impl_trait_wild_in_path(&mut ret.path),
210 Type::Ptr(ret) => make_impl_trait_wild(&mut ret.elem),
211 Type::Reference(ret) => make_impl_trait_wild(&mut ret.elem),
212 Type::Slice(ret) => make_impl_trait_wild(&mut ret.elem),
213 Type::TraitObject(ret) => {
214 for bound in &mut ret.bounds {
215 if let TypeParamBound::Trait(bound) = bound {
216 make_impl_trait_wild_in_path(&mut bound.path);
217 }
218 }
219 }
220 Type::Tuple(ret) => ret.elems.iter_mut().for_each(make_impl_trait_wild),
221 Type::BareFn(_) | Type::Infer(_) | Type::Macro(_) | Type::Never(_) | Type::Verbatim(_) => {}
222 _ => {}
223 }
224}
225
226fn make_impl_trait_wild_in_path(path: &mut Path) {
227 for segment in &mut path.segments {
228 if let PathArguments::AngleBracketed(bracketed) = &mut segment.arguments {
229 for arg in &mut bracketed.args {
230 if let GenericArgument::Type(arg) = arg {
231 make_impl_trait_wild(arg);
232 }
233 }
234 }
235 }
236}
237
238fn expand_no_panic(mut function: ItemFn) -> TokenStream2 {
239 let mut move_self = None;
240 let mut arg_pat = Vec::new();
241 let mut arg_val = Vec::new();
242 for (i, input) in function.sig.inputs.iter_mut().enumerate() {
243 match input {
244 FnArg::Typed(PatType { pat, .. })
245 if match pat.as_ref() {
246 Pat::Ident(pat) => pat.ident != "self",
247 _ => true,
248 } =>
249 {
250 let arg_name = if let Pat::Ident(original_name) = &**pat {
251 original_name.ident.clone()
252 } else {
253 Ident::new(&format!("__arg{}", i), Span::call_site())
254 };
255 arg_pat.push(quote!(#pat));
256 arg_val.push(quote!(#arg_name));
257 *pat = parse_quote!(mut #arg_name);
258 }
259 FnArg::Typed(_) | FnArg::Receiver(_) => {
260 move_self = Some(quote! {
261 if false {
262 loop {}
263 #[allow(unreachable_code)]
264 {
265 let __self = self;
266 }
267 }
268 });
269 }
270 }
271 }
272
273 let has_inline = function
274 .attrs
275 .iter()
276 .any(|attr| attr.path().is_ident("inline"));
277 if !has_inline {
278 function.attrs.push(parse_quote!(#[inline]));
279 }
280
281 let ret = match &function.sig.output {
282 ReturnType::Default => quote!(-> ()),
283 ReturnType::Type(arrow, output) => {
284 let mut output = output.clone();
285 make_impl_trait_wild(&mut output);
286 quote!(#arrow #output)
287 }
288 };
289 let stmts = function.block.stmts;
290 let message = format!(
291 "\n\nERROR[no-panic]: detected panic in function `{}`\n",
292 function.sig.ident,
293 );
294 let unsafe_extern = if cfg!(no_unsafe_extern_blocks) {
295 None
296 } else {
297 Some(Token))
298 };
299 function.block = Box::new(parse_quote!({
300 struct __NoPanic;
301 #unsafe_extern extern "C" {
302 #[link_name = #message]
303 fn trigger() -> !;
304 }
305 impl ::core::ops::Drop for __NoPanic {
306 fn drop(&mut self) {
307 unsafe {
308 trigger();
309 }
310 }
311 }
312 let __guard = __NoPanic;
313 let __result = (move || #ret {
314 #move_self
315 #(
316 let #arg_pat = #arg_val;
317 )*
318 #(#stmts)*
319 })();
320 ::core::mem::forget(__guard);
321 __result
322 }));
323
324 quote!(#function)
325}