proc_macro2_diagnostics/lib.rs
1#![cfg_attr(nightly_diagnostics, feature(proc_macro_diagnostic, proc_macro_span))]
2
3//! Diagnostic emulation on stable and nightly.
4//!
5//! # Usage
6//!
7//! 1. Depend on the library in your proc-macro.
8//!
9//! ```toml
10//! [dependencies]
11//! proc_macro2_diagnostics = "0.10"
12//! ```
13//!
14//! 2. Import [`SpanDiagnosticExt`] and use its methods on a
15//! [`proc_macro2::Span`] to create [`Diagnostic`]s:
16//!
17//! ```rust
18//! use syn::spanned::Spanned;
19//! use proc_macro2::TokenStream;
20//! use proc_macro2_diagnostics::{SpanDiagnosticExt, Diagnostic};
21//!
22//! fn my_macro(input: TokenStream) -> Result<TokenStream, Diagnostic> {
23//! Err(input.span().error("there's a problem here..."))
24//! }
25//! ```
26//!
27//! 3. If there's an error, emit the diagnostic as tokens:
28//!
29//! ```rust
30//! extern crate proc_macro;
31//!
32//! # use proc_macro2::TokenStream;
33//! # use proc_macro2_diagnostics::{SpanDiagnosticExt, Diagnostic};
34//! # use syn::spanned::Spanned;
35//! # fn my_macro(input: TokenStream) -> Result<TokenStream, Diagnostic> {
36//! # Err(input.span().error("there's a problem here..."))
37//! # }
38//! # /*
39//! #[proc_macro]
40//! # */
41//! pub fn real_macro(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
42//! match my_macro(tokens.into()) {
43//! Ok(tokens) => tokens.into(),
44//! Err(diag) => diag.emit_as_expr_tokens().into()
45//! }
46//! }
47//! ```
48//!
49//! This does the right thing on nightly _or_ stable.
50//!
51//! # Caveats
52//!
53//! On stable, due to limitations, any top-level, non-error diagnostics are
54//! emitted as errors. This will abort compilation. To avoid this, you may want
55//! to `cfg`-gate emitting non-error diagnostics to nightly.
56//!
57//! # Colors
58//!
59//! By default, error messages are colored on stable. To disable, disable
60//! default features:
61//!
62//! ```toml
63//! [dependencies]
64//! proc_macro2_diagnostics = { version = "0.10", default-features = false }
65//! ```
66//!
67//! The compiler always colors diagnostics on nightly.
68
69extern crate proc_macro;
70
71mod ext;
72mod diagnostic;
73mod line;
74
75pub use diagnostic::{Diagnostic, Level};
76pub use ext::SpanDiagnosticExt;
77
78// We stole this from proc_macro2. Checks whether nightly proc_macro things
79// _actually_ work by checking if calls to proc_macro::Span panic.
80#[cfg(nightly_diagnostics)]
81fn nightly_works() -> bool {
82 use std::panic::{self, PanicInfo};
83 use std::sync::atomic::*;
84 use std::sync::Once;
85
86 static WORKS: AtomicUsize = AtomicUsize::new(0);
87 static INIT: Once = Once::new();
88
89 match WORKS.load(Ordering::SeqCst) {
90 1 => return false,
91 2 => return true,
92 _ => {}
93 }
94
95 // Swap in a null panic hook to avoid printing "thread panicked" to stderr,
96 // then use catch_unwind to determine whether the compiler's proc_macro is
97 // working. When proc-macro2 is used from outside of a procedural macro all
98 // of the proc_macro crate's APIs currently panic.
99 //
100 // The Once is to prevent the possibility of this ordering:
101 //
102 // thread 1 calls take_hook, gets the user's original hook
103 // thread 1 calls set_hook with the null hook
104 // thread 2 calls take_hook, thinks null hook is the original hook
105 // thread 2 calls set_hook with the null hook
106 // thread 1 calls set_hook with the actual original hook
107 // thread 2 calls set_hook with what it thinks is the original hook
108 //
109 // in which the user's hook has been lost.
110 //
111 // There is still a race condition where a panic in a different thread can
112 // happen during the interval that the user's original panic hook is
113 // unregistered such that their hook is incorrectly not called. This is
114 // sufficiently unlikely and less bad than printing panic messages to stderr
115 // on correct use of this crate. Maybe there is a libstd feature request
116 // here. For now, if a user needs to guarantee that this failure mode does
117 // not occur, they need to call e.g. `proc_macro2::Span::call_site()` from
118 // the main thread before launching any other threads.
119 INIT.call_once(|| {
120 type PanicHook = dyn Fn(&PanicInfo) + Sync + Send + 'static;
121
122 let null_hook: Box<PanicHook> = Box::new(|_panic_info| { /* ignore */ });
123 let sanity_check = &*null_hook as *const PanicHook;
124 let original_hook = panic::take_hook();
125 panic::set_hook(null_hook);
126
127 let works = panic::catch_unwind(|| proc_macro::Span::call_site()).is_ok();
128 WORKS.store(works as usize + 1, Ordering::SeqCst);
129
130 let hopefully_null_hook = panic::take_hook();
131 panic::set_hook(original_hook);
132 if sanity_check != &*hopefully_null_hook {
133 panic!("observed race condition in proc_macro2::nightly_works");
134 }
135 });
136
137 nightly_works()
138}