feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+349
View File
@@ -0,0 +1,349 @@
use crate::{abort_now, check_correctness, sealed::Sealed, SpanRange};
use proc_macro2::Span;
use proc_macro2::TokenStream;
use quote::{quote_spanned, ToTokens};
/// Represents a diagnostic level
///
/// # Warnings
///
/// Warnings are ignored on stable/beta
#[derive(Debug, PartialEq)]
pub enum Level {
Error,
Warning,
#[doc(hidden)]
NonExhaustive,
}
/// Represents a single diagnostic message
#[derive(Debug)]
pub struct Diagnostic {
pub(crate) level: Level,
pub(crate) span_range: SpanRange,
pub(crate) msg: String,
pub(crate) suggestions: Vec<(SuggestionKind, String, Option<SpanRange>)>,
pub(crate) children: Vec<(SpanRange, String)>,
}
/// A collection of methods that do not exist in `proc_macro::Diagnostic`
/// but still useful to have around.
///
/// This trait is sealed and cannot be implemented outside of `proc_macro_error`.
pub trait DiagnosticExt: Sealed {
/// Create a new diagnostic message that points to the `span_range`.
///
/// This function is the same as `Diagnostic::spanned` but produces considerably
/// better error messages for multi-token spans on stable.
fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self;
/// Add another error message to self such that it will be emitted right after
/// the main message.
///
/// This function is the same as `Diagnostic::span_error` but produces considerably
/// better error messages for multi-token spans on stable.
fn span_range_error(self, span_range: SpanRange, msg: String) -> Self;
/// Attach a "help" note to your main message, the note will have it's own span on nightly.
///
/// This function is the same as `Diagnostic::span_help` but produces considerably
/// better error messages for multi-token spans on stable.
///
/// # Span
///
/// The span is ignored on stable, the note effectively inherits its parent's (main message) span
fn span_range_help(self, span_range: SpanRange, msg: String) -> Self;
/// Attach a note to your main message, the note will have it's own span on nightly.
///
/// This function is the same as `Diagnostic::span_note` but produces considerably
/// better error messages for multi-token spans on stable.
///
/// # Span
///
/// The span is ignored on stable, the note effectively inherits its parent's (main message) span
fn span_range_note(self, span_range: SpanRange, msg: String) -> Self;
}
impl DiagnosticExt for Diagnostic {
fn spanned_range(span_range: SpanRange, level: Level, message: String) -> Self {
Diagnostic {
level,
span_range,
msg: message,
suggestions: vec![],
children: vec![],
}
}
fn span_range_error(mut self, span_range: SpanRange, msg: String) -> Self {
self.children.push((span_range, msg));
self
}
fn span_range_help(mut self, span_range: SpanRange, msg: String) -> Self {
self.suggestions
.push((SuggestionKind::Help, msg, Some(span_range)));
self
}
fn span_range_note(mut self, span_range: SpanRange, msg: String) -> Self {
self.suggestions
.push((SuggestionKind::Note, msg, Some(span_range)));
self
}
}
impl Diagnostic {
/// Create a new diagnostic message that points to `Span::call_site()`
pub fn new(level: Level, message: String) -> Self {
Diagnostic::spanned(Span::call_site(), level, message)
}
/// Create a new diagnostic message that points to the `span`
pub fn spanned(span: Span, level: Level, message: String) -> Self {
Diagnostic::spanned_range(
SpanRange {
first: span,
last: span,
},
level,
message,
)
}
/// Add another error message to self such that it will be emitted right after
/// the main message.
pub fn span_error(self, span: Span, msg: String) -> Self {
self.span_range_error(
SpanRange {
first: span,
last: span,
},
msg,
)
}
/// Attach a "help" note to your main message, the note will have it's own span on nightly.
///
/// # Span
///
/// The span is ignored on stable, the note effectively inherits its parent's (main message) span
pub fn span_help(self, span: Span, msg: String) -> Self {
self.span_range_help(
SpanRange {
first: span,
last: span,
},
msg,
)
}
/// Attach a "help" note to your main message.
pub fn help(mut self, msg: String) -> Self {
self.suggestions.push((SuggestionKind::Help, msg, None));
self
}
/// Attach a note to your main message, the note will have it's own span on nightly.
///
/// # Span
///
/// The span is ignored on stable, the note effectively inherits its parent's (main message) span
pub fn span_note(self, span: Span, msg: String) -> Self {
self.span_range_note(
SpanRange {
first: span,
last: span,
},
msg,
)
}
/// Attach a note to your main message
pub fn note(mut self, msg: String) -> Self {
self.suggestions.push((SuggestionKind::Note, msg, None));
self
}
/// The message of main warning/error (no notes attached)
pub fn message(&self) -> &str {
&self.msg
}
/// Abort the proc-macro's execution and display the diagnostic.
///
/// # Warnings
///
/// Warnings are not emitted on stable and beta, but this function will abort anyway.
pub fn abort(self) -> ! {
self.emit();
abort_now()
}
/// Display the diagnostic while not aborting macro execution.
///
/// # Warnings
///
/// Warnings are ignored on stable/beta
pub fn emit(self) {
check_correctness();
crate::imp::emit_diagnostic(self);
}
}
/// **NOT PUBLIC API! NOTHING TO SEE HERE!!!**
#[doc(hidden)]
impl Diagnostic {
pub fn span_suggestion(self, span: Span, suggestion: &str, msg: String) -> Self {
match suggestion {
"help" | "hint" => self.span_help(span, msg),
_ => self.span_note(span, msg),
}
}
pub fn suggestion(self, suggestion: &str, msg: String) -> Self {
match suggestion {
"help" | "hint" => self.help(msg),
_ => self.note(msg),
}
}
}
impl ToTokens for Diagnostic {
fn to_tokens(&self, ts: &mut TokenStream) {
use std::borrow::Cow;
fn ensure_lf(buf: &mut String, s: &str) {
if s.ends_with('\n') {
buf.push_str(s);
} else {
buf.push_str(s);
buf.push('\n');
}
}
fn diag_to_tokens(
span_range: SpanRange,
level: &Level,
msg: &str,
suggestions: &[(SuggestionKind, String, Option<SpanRange>)],
) -> TokenStream {
if *level == Level::Warning {
return TokenStream::new();
}
let message = if suggestions.is_empty() {
Cow::Borrowed(msg)
} else {
let mut message = String::new();
ensure_lf(&mut message, msg);
message.push('\n');
for (kind, note, _span) in suggestions {
message.push_str(" = ");
message.push_str(kind.name());
message.push_str(": ");
ensure_lf(&mut message, note);
}
message.push('\n');
Cow::Owned(message)
};
let mut msg = proc_macro2::Literal::string(&message);
msg.set_span(span_range.last);
let group = quote_spanned!(span_range.last=> { #msg } );
quote_spanned!(span_range.first=> compile_error!#group)
}
ts.extend(diag_to_tokens(
self.span_range,
&self.level,
&self.msg,
&self.suggestions,
));
ts.extend(
self.children
.iter()
.map(|(span_range, msg)| diag_to_tokens(*span_range, &Level::Error, &msg, &[])),
);
}
}
#[derive(Debug)]
pub(crate) enum SuggestionKind {
Help,
Note,
}
impl SuggestionKind {
fn name(&self) -> &'static str {
match self {
SuggestionKind::Note => "note",
SuggestionKind::Help => "help",
}
}
}
#[cfg(feature = "syn-error")]
impl From<syn::Error> for Diagnostic {
fn from(err: syn::Error) -> Self {
use proc_macro2::{Delimiter, TokenTree};
fn gut_error(ts: &mut impl Iterator<Item = TokenTree>) -> Option<(SpanRange, String)> {
let first = match ts.next() {
// compile_error
None => return None,
Some(tt) => tt.span(),
};
ts.next().unwrap(); // !
let lit = match ts.next().unwrap() {
TokenTree::Group(group) => {
// Currently `syn` builds `compile_error!` invocations
// exclusively in `ident{"..."}` (braced) form which is not
// followed by `;` (semicolon).
//
// But if it changes to `ident("...");` (parenthesized)
// or `ident["..."];` (bracketed) form,
// we will need to skip the `;` as well.
// Highly unlikely, but better safe than sorry.
if group.delimiter() == Delimiter::Parenthesis
|| group.delimiter() == Delimiter::Bracket
{
ts.next().unwrap(); // ;
}
match group.stream().into_iter().next().unwrap() {
TokenTree::Literal(lit) => lit,
_ => unreachable!(),
}
}
_ => unreachable!(),
};
let last = lit.span();
let mut msg = lit.to_string();
// "abc" => abc
msg.pop();
msg.remove(0);
Some((SpanRange { first, last }, msg))
}
let mut ts = err.to_compile_error().into_iter();
let (span_range, msg) = gut_error(&mut ts).unwrap();
let mut res = Diagnostic::spanned_range(span_range, Level::Error, msg);
while let Some((span_range, msg)) = gut_error(&mut ts) {
res = res.span_range_error(span_range, msg);
}
res
}
}
+150
View File
@@ -0,0 +1,150 @@
//! Facility to emit dummy implementations (or whatever) in case
//! an error happen.
//!
//! `compile_error!` does not abort a compilation right away. This means
//! `rustc` doesn't just show you the error and abort, it carries on the
//! compilation process looking for other errors to report.
//!
//! Let's consider an example:
//!
//! ```rust,ignore
//! use proc_macro::TokenStream;
//! use proc_macro_error::*;
//!
//! trait MyTrait {
//! fn do_thing();
//! }
//!
//! // this proc macro is supposed to generate MyTrait impl
//! #[proc_macro_derive(MyTrait)]
//! #[proc_macro_error]
//! fn example(input: TokenStream) -> TokenStream {
//! // somewhere deep inside
//! abort!(span, "something's wrong");
//!
//! // this implementation will be generated if no error happened
//! quote! {
//! impl MyTrait for #name {
//! fn do_thing() {/* whatever */}
//! }
//! }
//! }
//!
//! // ================
//! // in main.rs
//!
//! // this derive triggers an error
//! #[derive(MyTrait)] // first BOOM!
//! struct Foo;
//!
//! fn main() {
//! Foo::do_thing(); // second BOOM!
//! }
//! ```
//!
//! The problem is: the generated token stream contains only `compile_error!`
//! invocation, the impl was not generated. That means user will see two compilation
//! errors:
//!
//! ```text
//! error: something's wrong
//! --> $DIR/probe.rs:9:10
//! |
//! 9 |#[proc_macro_derive(MyTrait)]
//! | ^^^^^^^
//!
//! error[E0599]: no function or associated item named `do_thing` found for type `Foo` in the current scope
//! --> src\main.rs:3:10
//! |
//! 1 | struct Foo;
//! | ----------- function or associated item `do_thing` not found for this
//! 2 | fn main() {
//! 3 | Foo::do_thing(); // second BOOM!
//! | ^^^^^^^^ function or associated item not found in `Foo`
//! ```
//!
//! But the second error is meaningless! We definitely need to fix this.
//!
//! Most used approach in cases like this is "dummy implementation" -
//! omit `impl MyTrait for #name` and fill functions bodies with `unimplemented!()`.
//!
//! This is how you do it:
//!
//! ```rust,ignore
//! use proc_macro::TokenStream;
//! use proc_macro_error::*;
//!
//! trait MyTrait {
//! fn do_thing();
//! }
//!
//! // this proc macro is supposed to generate MyTrait impl
//! #[proc_macro_derive(MyTrait)]
//! #[proc_macro_error]
//! fn example(input: TokenStream) -> TokenStream {
//! // first of all - we set a dummy impl which will be appended to
//! // `compile_error!` invocations in case a trigger does happen
//! set_dummy(quote! {
//! impl MyTrait for #name {
//! fn do_thing() { unimplemented!() }
//! }
//! });
//!
//! // somewhere deep inside
//! abort!(span, "something's wrong");
//!
//! // this implementation will be generated if no error happened
//! quote! {
//! impl MyTrait for #name {
//! fn do_thing() {/* whatever */}
//! }
//! }
//! }
//!
//! // ================
//! // in main.rs
//!
//! // this derive triggers an error
//! #[derive(MyTrait)] // first BOOM!
//! struct Foo;
//!
//! fn main() {
//! Foo::do_thing(); // no more errors!
//! }
//! ```
use proc_macro2::TokenStream;
use std::cell::RefCell;
use crate::check_correctness;
thread_local! {
static DUMMY_IMPL: RefCell<Option<TokenStream>> = RefCell::new(None);
}
/// Sets dummy token stream which will be appended to `compile_error!(msg);...`
/// invocations in case you'll emit any errors.
///
/// See [guide](../index.html#guide).
pub fn set_dummy(dummy: TokenStream) -> Option<TokenStream> {
check_correctness();
DUMMY_IMPL.with(|old_dummy| old_dummy.replace(Some(dummy)))
}
/// Same as [`set_dummy`] but, instead of resetting, appends tokens to the
/// existing dummy (if any). Behaves as `set_dummy` if no dummy is present.
pub fn append_dummy(dummy: TokenStream) {
check_correctness();
DUMMY_IMPL.with(|old_dummy| {
let mut cell = old_dummy.borrow_mut();
if let Some(ts) = cell.as_mut() {
ts.extend(dummy);
} else {
*cell = Some(dummy);
}
});
}
pub(crate) fn cleanup() -> Option<TokenStream> {
DUMMY_IMPL.with(|old_dummy| old_dummy.replace(None))
}
+69
View File
@@ -0,0 +1,69 @@
//! This implementation uses [`proc_macro::Diagnostic`], nightly only.
use std::cell::Cell;
use proc_macro::{Diagnostic as PDiag, Level as PLevel};
use crate::{
abort_now, check_correctness,
diagnostic::{Diagnostic, Level, SuggestionKind},
};
pub fn abort_if_dirty() {
check_correctness();
if IS_DIRTY.with(|c| c.get()) {
abort_now()
}
}
pub(crate) fn cleanup() -> Vec<Diagnostic> {
IS_DIRTY.with(|c| c.set(false));
vec![]
}
pub(crate) fn emit_diagnostic(diag: Diagnostic) {
let Diagnostic {
level,
span_range,
msg,
suggestions,
children,
} = diag;
let span = span_range.collapse().unwrap();
let level = match level {
Level::Warning => PLevel::Warning,
Level::Error => {
IS_DIRTY.with(|c| c.set(true));
PLevel::Error
}
_ => unreachable!(),
};
let mut res = PDiag::spanned(span, level, msg);
for (kind, msg, span) in suggestions {
res = match (kind, span) {
(SuggestionKind::Note, Some(span_range)) => {
res.span_note(span_range.collapse().unwrap(), msg)
}
(SuggestionKind::Help, Some(span_range)) => {
res.span_help(span_range.collapse().unwrap(), msg)
}
(SuggestionKind::Note, None) => res.note(msg),
(SuggestionKind::Help, None) => res.help(msg),
}
}
for (span_range, msg) in children {
let span = span_range.collapse().unwrap();
res = res.span_error(span, msg);
}
res.emit()
}
thread_local! {
static IS_DIRTY: Cell<bool> = Cell::new(false);
}
+30
View File
@@ -0,0 +1,30 @@
//! This implementation uses self-written stable facilities.
use crate::{
abort_now, check_correctness,
diagnostic::{Diagnostic, Level},
};
use std::cell::RefCell;
pub fn abort_if_dirty() {
check_correctness();
ERR_STORAGE.with(|storage| {
if !storage.borrow().is_empty() {
abort_now()
}
});
}
pub(crate) fn cleanup() -> Vec<Diagnostic> {
ERR_STORAGE.with(|storage| storage.replace(Vec::new()))
}
pub(crate) fn emit_diagnostic(diag: Diagnostic) {
if diag.level == Level::Error {
ERR_STORAGE.with(|storage| storage.borrow_mut().push(diag));
}
}
thread_local! {
static ERR_STORAGE: RefCell<Vec<Diagnostic>> = RefCell::new(Vec::new());
}
+560
View File
@@ -0,0 +1,560 @@
//! # proc-macro-error
//!
//! This crate aims to make error reporting in proc-macros simple and easy to use.
//! Migrate from `panic!`-based errors for as little effort as possible!
//!
//! (Also, you can explicitly [append a dummy token stream](dummy/index.html) to your errors).
//!
//! To achieve his, this crate serves as a tiny shim around `proc_macro::Diagnostic` and
//! `compile_error!`. It detects the best way of emitting available based on compiler's version.
//! When the underlying diagnostic type is finally stabilized, this crate will simply be
//! delegating to it requiring no changes in your code!
//!
//! So you can just use this crate and have *both* some of `proc_macro::Diagnostic` functionality
//! available on stable ahead of time *and* your error-reporting code future-proof.
//!
//! ## Cargo features
//!
//! This crate provides *enabled by default* `syn-error` feature that gates
//! `impl From<syn::Error> for Diagnostic` conversion. If you don't use `syn` and want
//! to cut off some of compilation time, you can disable it via
//!
//! ```toml
//! [dependencies]
//! proc-macro-error = { version = "1", default-features = false }
//! ```
//!
//! ***Please note that disabling this feature makes sense only if you don't depend on `syn`
//! directly or indirectly, and you very likely do.**
//!
//! ## Real world examples
//!
//! * [`structopt-derive`](https://github.com/TeXitoi/structopt/tree/master/structopt-derive)
//! (abort-like usage)
//! * [`auto-impl`](https://github.com/auto-impl-rs/auto_impl/) (emit-like usage)
//!
//! ## Limitations
//!
//! - Warnings are emitted only on nightly, they are ignored on stable.
//! - "help" suggestions can't have their own span info on stable,
//! (essentially inheriting the parent span).
//! - If a panic occurs somewhere in your macro no errors will be displayed. This is not a
//! technical limitation but rather intentional design. `panic` is not for error reporting.
//!
//! ### `#[proc_macro_error]` attribute
//!
//! **This attribute MUST be present on the top level of your macro** (the function
//! annotated with any of `#[proc_macro]`, `#[proc_macro_derive]`, `#[proc_macro_attribute]`).
//!
//! This attribute performs the setup and cleanup necessary to make things work.
//!
//! In most cases you'll need the simple `#[proc_macro_error]` form without any
//! additional settings. Feel free to [skip the "Syntax" section](#macros).
//!
//! #### Syntax
//!
//! `#[proc_macro_error]` or `#[proc_macro_error(settings...)]`, where `settings...`
//! is a comma-separated list of:
//!
//! - `proc_macro_hack`:
//!
//! In order to correctly cooperate with `#[proc_macro_hack]`, `#[proc_macro_error]`
//! attribute must be placed *before* (above) it, like this:
//!
//! ```no_run
//! # use proc_macro2::TokenStream;
//! # const IGNORE: &str = "
//! #[proc_macro_error]
//! #[proc_macro_hack]
//! #[proc_macro]
//! # ";
//! fn my_macro(input: TokenStream) -> TokenStream {
//! unimplemented!()
//! }
//! ```
//!
//! If, for some reason, you can't place it like that you can use
//! `#[proc_macro_error(proc_macro_hack)]` instead.
//!
//! # Note
//!
//! If `proc-macro-hack` was detected (by any means) `allow_not_macro`
//! and `assert_unwind_safe` will be applied automatically.
//!
//! - `allow_not_macro`:
//!
//! By default, the attribute checks that it's applied to a proc-macro.
//! If none of `#[proc_macro]`, `#[proc_macro_derive]` nor `#[proc_macro_attribute]` are
//! present it will panic. It's the intention - this crate is supposed to be used only with
//! proc-macros.
//!
//! This setting is made to bypass the check, useful in certain circumstances.
//!
//! Pay attention: the function this attribute is applied to must return
//! `proc_macro::TokenStream`.
//!
//! This setting is implied if `proc-macro-hack` was detected.
//!
//! - `assert_unwind_safe`:
//!
//! By default, your code must be [unwind safe]. If your code is not unwind safe,
//! but you believe it's correct, you can use this setting to bypass the check.
//! You would need this for code that uses `lazy_static` or `thread_local` with
//! `Cell/RefCell` inside (and the like).
//!
//! This setting is implied if `#[proc_macro_error]` is applied to a function
//! marked as `#[proc_macro]`, `#[proc_macro_derive]` or `#[proc_macro_attribute]`.
//!
//! This setting is also implied if `proc-macro-hack` was detected.
//!
//! ## Macros
//!
//! Most of the time you want to use the macros. Syntax is described in the next section below.
//!
//! You'll need to decide how you want to emit errors:
//!
//! * Emit the error and abort. Very much panic-like usage. Served by [`abort!`] and
//! [`abort_call_site!`].
//! * Emit the error but do not abort right away, looking for other errors to report.
//! Served by [`emit_error!`] and [`emit_call_site_error!`].
//!
//! You **can** mix these usages.
//!
//! `abort` and `emit_error` take a "source span" as the first argument. This source
//! will be used to highlight the place the error originates from. It must be one of:
//!
//! * *Something* that implements [`ToTokens`] (most types in `syn` and `proc-macro2` do).
//! This source is the preferable one since it doesn't lose span information on multi-token
//! spans, see [this issue](https://gitlab.com/CreepySkeleton/proc-macro-error/-/issues/6)
//! for details.
//! * [`proc_macro::Span`]
//! * [`proc-macro2::Span`]
//!
//! The rest is your message in format-like style.
//!
//! See [the next section](#syntax-1) for detailed syntax.
//!
//! - [`abort!`]:
//!
//! Very much panic-like usage - abort right away and show the error.
//! Expands to [`!`] (never type).
//!
//! - [`abort_call_site!`]:
//!
//! Shortcut for `abort!(Span::call_site(), ...)`. Expands to [`!`] (never type).
//!
//! - [`emit_error!`]:
//!
//! [`proc_macro::Diagnostic`]-like usage - emit the error but keep going,
//! looking for other errors to report.
//! The compilation will fail nonetheless. Expands to [`()`] (unit type).
//!
//! - [`emit_call_site_error!`]:
//!
//! Shortcut for `emit_error!(Span::call_site(), ...)`. Expands to [`()`] (unit type).
//!
//! - [`emit_warning!`]:
//!
//! Like `emit_error!` but emit a warning instead of error. The compilation won't fail
//! because of warnings.
//! Expands to [`()`] (unit type).
//!
//! **Beware**: warnings are nightly only, they are completely ignored on stable.
//!
//! - [`emit_call_site_warning!`]:
//!
//! Shortcut for `emit_warning!(Span::call_site(), ...)`. Expands to [`()`] (unit type).
//!
//! - [`diagnostic`]:
//!
//! Build an instance of `Diagnostic` in format-like style.
//!
//! #### Syntax
//!
//! All the macros have pretty much the same syntax:
//!
//! 1. ```ignore
//! abort!(single_expr)
//! ```
//! Shortcut for `Diagnostic::from(expr).abort()`.
//!
//! 2. ```ignore
//! abort!(span, message)
//! ```
//! The first argument is an expression the span info should be taken from.
//!
//! The second argument is the error message, it must implement [`ToString`].
//!
//! 3. ```ignore
//! abort!(span, format_literal, format_args...)
//! ```
//!
//! This form is pretty much the same as 2, except `format!(format_literal, format_args...)`
//! will be used to for the message instead of [`ToString`].
//!
//! That's it. `abort!`, `emit_warning`, `emit_error` share this exact syntax.
//!
//! `abort_call_site!`, `emit_call_site_warning`, `emit_call_site_error` lack 1 form
//! and do not take span in 2'th and 3'th forms. Those are essentially shortcuts for
//! `macro!(Span::call_site(), args...)`.
//!
//! `diagnostic!` requires a [`Level`] instance between `span` and second argument
//! (1'th form is the same).
//!
//! > **Important!**
//! >
//! > If you have some type from `proc_macro` or `syn` to point to, do not call `.span()`
//! > on it but rather use it directly:
//! > ```no_run
//! > # use proc_macro_error::abort;
//! > # let input = proc_macro2::TokenStream::new();
//! > let ty: syn::Type = syn::parse2(input).unwrap();
//! > abort!(ty, "BOOM");
//! > // ^^ <-- avoid .span()
//! > ```
//! >
//! > `.span()` calls work too, but you may experience regressions in message quality.
//!
//! #### Note attachments
//!
//! 3. Every macro can have "note" attachments (only 2 and 3 form).
//! ```ignore
//! let opt_help = if have_some_info { Some("did you mean `this`?") } else { None };
//!
//! abort!(
//! span, message; // <--- attachments start with `;` (semicolon)
//!
//! help = "format {} {}", "arg1", "arg2"; // <--- every attachment ends with `;`,
//! // maybe except the last one
//!
//! note = "to_string"; // <--- one arg uses `.to_string()` instead of `format!()`
//!
//! yay = "I see what {} did here", "you"; // <--- "help =" and "hint =" are mapped
//! // to Diagnostic::help,
//! // anything else is Diagnostic::note
//!
//! wow = note_span => "custom span"; // <--- attachments can have their own span
//! // it takes effect only on nightly though
//!
//! hint =? opt_help; // <-- "optional" attachment, get displayed only if `Some`
//! // must be single `Option` expression
//!
//! note =? note_span => opt_help // <-- optional attachments can have custom spans too
//! );
//! ```
//!
//! ### Diagnostic type
//!
//! [`Diagnostic`] type is intentionally designed to be API compatible with [`proc_macro::Diagnostic`].
//! Not all API is implemented, only the part that can be reasonably implemented on stable.
//!
//!
//! [`abort!`]: macro.abort.html
//! [`abort_call_site!`]: macro.abort_call_site.html
//! [`emit_warning!`]: macro.emit_warning.html
//! [`emit_error!`]: macro.emit_error.html
//! [`emit_call_site_warning!`]: macro.emit_call_site_error.html
//! [`emit_call_site_error!`]: macro.emit_call_site_warning.html
//! [`diagnostic!`]: macro.diagnostic.html
//! [`Diagnostic`]: struct.Diagnostic.html
//!
//! [`proc_macro::Span`]: https://doc.rust-lang.org/proc_macro/struct.Span.html
//! [`proc_macro::Diagnostic`]: https://doc.rust-lang.org/proc_macro/struct.Diagnostic.html
//!
//! [unwind safe]: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html#what-is-unwind-safety
//! [`!`]: https://doc.rust-lang.org/std/primitive.never.html
//! [`()`]: https://doc.rust-lang.org/std/primitive.unit.html
//! [`ToString`]: https://doc.rust-lang.org/std/string/trait.ToString.html
//!
//! [`proc-macro2::Span`]: https://docs.rs/proc-macro2/1.0.10/proc_macro2/struct.Span.html
//! [`ToTokens`]: https://docs.rs/quote/1.0.3/quote/trait.ToTokens.html
//!
#![cfg_attr(not(use_fallback), feature(proc_macro_diagnostic))]
#![forbid(unsafe_code)]
#![allow(clippy::needless_doctest_main)]
extern crate proc_macro;
pub use crate::{
diagnostic::{Diagnostic, DiagnosticExt, Level},
dummy::{append_dummy, set_dummy},
};
pub use proc_macro_error_attr::proc_macro_error;
use proc_macro2::Span;
use quote::{quote, ToTokens};
use std::cell::Cell;
use std::panic::{catch_unwind, resume_unwind, UnwindSafe};
pub mod dummy;
mod diagnostic;
mod macros;
mod sealed;
#[cfg(use_fallback)]
#[path = "imp/fallback.rs"]
mod imp;
#[cfg(not(use_fallback))]
#[path = "imp/delegate.rs"]
mod imp;
#[derive(Debug, Clone, Copy)]
pub struct SpanRange {
pub first: Span,
pub last: Span,
}
impl SpanRange {
/// Create a range with the `first` and `last` spans being the same.
pub fn single_span(span: Span) -> Self {
SpanRange {
first: span,
last: span,
}
}
/// Create a `SpanRange` resolving at call site.
pub fn call_site() -> Self {
SpanRange::single_span(Span::call_site())
}
/// Construct span range from a `TokenStream`. This method always preserves all the
/// range.
///
/// ### Note
///
/// If the stream is empty, the result is `SpanRange::call_site()`. If the stream
/// consists of only one `TokenTree`, the result is `SpanRange::single_span(tt.span())`
/// that doesn't lose anything.
pub fn from_tokens(ts: &dyn ToTokens) -> Self {
let mut spans = ts.to_token_stream().into_iter().map(|tt| tt.span());
let first = spans.next().unwrap_or_else(|| Span::call_site());
let last = spans.last().unwrap_or(first);
SpanRange { first, last }
}
/// Join two span ranges. The resulting range will start at `self.first` and end at
/// `other.last`.
pub fn join_range(self, other: SpanRange) -> Self {
SpanRange {
first: self.first,
last: other.last,
}
}
/// Collapse the range into single span, preserving as much information as possible.
pub fn collapse(self) -> Span {
self.first.join(self.last).unwrap_or(self.first)
}
}
/// This traits expands `Result<T, Into<Diagnostic>>` with some handy shortcuts.
pub trait ResultExt {
type Ok;
/// Behaves like `Result::unwrap`: if self is `Ok` yield the contained value,
/// otherwise abort macro execution via `abort!`.
fn unwrap_or_abort(self) -> Self::Ok;
/// Behaves like `Result::expect`: if self is `Ok` yield the contained value,
/// otherwise abort macro execution via `abort!`.
/// If it aborts then resulting error message will be preceded with `message`.
fn expect_or_abort(self, msg: &str) -> Self::Ok;
}
/// This traits expands `Option` with some handy shortcuts.
pub trait OptionExt {
type Some;
/// Behaves like `Option::expect`: if self is `Some` yield the contained value,
/// otherwise abort macro execution via `abort_call_site!`.
/// If it aborts the `message` will be used for [`compile_error!`][compl_err] invocation.
///
/// [compl_err]: https://doc.rust-lang.org/std/macro.compile_error.html
fn expect_or_abort(self, msg: &str) -> Self::Some;
}
/// Abort macro execution and display all the emitted errors, if any.
///
/// Does nothing if no errors were emitted (warnings do not count).
pub fn abort_if_dirty() {
imp::abort_if_dirty();
}
impl<T, E: Into<Diagnostic>> ResultExt for Result<T, E> {
type Ok = T;
fn unwrap_or_abort(self) -> T {
match self {
Ok(res) => res,
Err(e) => e.into().abort(),
}
}
fn expect_or_abort(self, message: &str) -> T {
match self {
Ok(res) => res,
Err(e) => {
let mut e = e.into();
e.msg = format!("{}: {}", message, e.msg);
e.abort()
}
}
}
}
impl<T> OptionExt for Option<T> {
type Some = T;
fn expect_or_abort(self, message: &str) -> T {
match self {
Some(res) => res,
None => abort_call_site!(message),
}
}
}
/// This is the entry point for a proc-macro.
///
/// **NOT PUBLIC API, SUBJECT TO CHANGE WITHOUT ANY NOTICE**
#[doc(hidden)]
pub fn entry_point<F>(f: F, proc_macro_hack: bool) -> proc_macro::TokenStream
where
F: FnOnce() -> proc_macro::TokenStream + UnwindSafe,
{
ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() + 1));
let caught = catch_unwind(f);
let dummy = dummy::cleanup();
let err_storage = imp::cleanup();
ENTERED_ENTRY_POINT.with(|flag| flag.set(flag.get() - 1));
let gen_error = || {
if proc_macro_hack {
quote! {{
macro_rules! proc_macro_call {
() => ( unimplemented!() )
}
#(#err_storage)*
#dummy
unimplemented!()
}}
} else {
quote!( #(#err_storage)* #dummy )
}
};
match caught {
Ok(ts) => {
if err_storage.is_empty() {
ts
} else {
gen_error().into()
}
}
Err(boxed) => match boxed.downcast::<AbortNow>() {
Ok(_) => gen_error().into(),
Err(boxed) => resume_unwind(boxed),
},
}
}
fn abort_now() -> ! {
check_correctness();
panic!(AbortNow)
}
thread_local! {
static ENTERED_ENTRY_POINT: Cell<usize> = Cell::new(0);
}
struct AbortNow;
fn check_correctness() {
if ENTERED_ENTRY_POINT.with(|flag| flag.get()) == 0 {
panic!(
"proc-macro-error API cannot be used outside of `entry_point` invocation, \
perhaps you forgot to annotate your #[proc_macro] function with `#[proc_macro_error]"
);
}
}
/// **ALL THE STUFF INSIDE IS NOT PUBLIC API!!!**
#[doc(hidden)]
pub mod __export {
// reexports for use in macros
pub extern crate proc_macro;
pub extern crate proc_macro2;
use proc_macro2::Span;
use quote::ToTokens;
use crate::SpanRange;
// inspired by
// https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md#simple-application
pub trait SpanAsSpanRange {
#[allow(non_snake_case)]
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange;
}
pub trait Span2AsSpanRange {
#[allow(non_snake_case)]
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange;
}
pub trait ToTokensAsSpanRange {
#[allow(non_snake_case)]
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange;
}
pub trait SpanRangeAsSpanRange {
#[allow(non_snake_case)]
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange;
}
impl<T: ToTokens> ToTokensAsSpanRange for &T {
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange {
let mut ts = self.to_token_stream().into_iter();
let first = ts
.next()
.map(|tt| tt.span())
.unwrap_or_else(Span::call_site);
let last = ts.last().map(|tt| tt.span()).unwrap_or(first);
SpanRange { first, last }
}
}
impl Span2AsSpanRange for Span {
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange {
SpanRange {
first: *self,
last: *self,
}
}
}
impl SpanAsSpanRange for proc_macro::Span {
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange {
SpanRange {
first: self.clone().into(),
last: self.clone().into(),
}
}
}
impl SpanRangeAsSpanRange for SpanRange {
fn FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange(&self) -> SpanRange {
*self
}
}
}
+288
View File
@@ -0,0 +1,288 @@
// FIXME: this can be greatly simplified via $()?
// as soon as MRSV hits 1.32
/// Build [`Diagnostic`](struct.Diagnostic.html) instance from provided arguments.
///
/// # Syntax
///
/// See [the guide](index.html#guide).
///
#[macro_export]
macro_rules! diagnostic {
// from alias
($err:expr) => { $crate::Diagnostic::from($err) };
// span, message, help
($span:expr, $level:expr, $fmt:expr, $($args:expr),+ ; $($rest:tt)+) => {{
#[allow(unused_imports)]
use $crate::__export::{
ToTokensAsSpanRange,
Span2AsSpanRange,
SpanAsSpanRange,
SpanRangeAsSpanRange
};
use $crate::DiagnosticExt;
let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange();
let diag = $crate::Diagnostic::spanned_range(
span_range,
$level,
format!($fmt, $($args),*)
);
$crate::__pme__suggestions!(diag $($rest)*);
diag
}};
($span:expr, $level:expr, $msg:expr ; $($rest:tt)+) => {{
#[allow(unused_imports)]
use $crate::__export::{
ToTokensAsSpanRange,
Span2AsSpanRange,
SpanAsSpanRange,
SpanRangeAsSpanRange
};
use $crate::DiagnosticExt;
let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange();
let diag = $crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string());
$crate::__pme__suggestions!(diag $($rest)*);
diag
}};
// span, message, no help
($span:expr, $level:expr, $fmt:expr, $($args:expr),+) => {{
#[allow(unused_imports)]
use $crate::__export::{
ToTokensAsSpanRange,
Span2AsSpanRange,
SpanAsSpanRange,
SpanRangeAsSpanRange
};
use $crate::DiagnosticExt;
let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange();
$crate::Diagnostic::spanned_range(
span_range,
$level,
format!($fmt, $($args),*)
)
}};
($span:expr, $level:expr, $msg:expr) => {{
#[allow(unused_imports)]
use $crate::__export::{
ToTokensAsSpanRange,
Span2AsSpanRange,
SpanAsSpanRange,
SpanRangeAsSpanRange
};
use $crate::DiagnosticExt;
let span_range = (&$span).FIRST_ARG_MUST_EITHER_BE_Span_OR_IMPLEMENT_ToTokens_OR_BE_SpanRange();
$crate::Diagnostic::spanned_range(span_range, $level, $msg.to_string())
}};
// trailing commas
($span:expr, $level:expr, $fmt:expr, $($args:expr),+, ; $($rest:tt)+) => {
$crate::diagnostic!($span, $level, $fmt, $($args),* ; $($rest)*)
};
($span:expr, $level:expr, $msg:expr, ; $($rest:tt)+) => {
$crate::diagnostic!($span, $level, $msg ; $($rest)*)
};
($span:expr, $level:expr, $fmt:expr, $($args:expr),+,) => {
$crate::diagnostic!($span, $level, $fmt, $($args),*)
};
($span:expr, $level:expr, $msg:expr,) => {
$crate::diagnostic!($span, $level, $msg)
};
// ($err:expr,) => { $crate::diagnostic!($err) };
}
/// Abort proc-macro execution right now and display the error.
///
/// # Syntax
///
/// See [the guide](index.html#guide).
#[macro_export]
macro_rules! abort {
($err:expr) => {
$crate::diagnostic!($err).abort()
};
($span:expr, $($tts:tt)*) => {
$crate::diagnostic!($span, $crate::Level::Error, $($tts)*).abort()
};
}
/// Shortcut for `abort!(Span::call_site(), msg...)`. This macro
/// is still preferable over plain panic, panics are not for error reporting.
///
/// # Syntax
///
/// See [the guide](index.html#guide).
///
#[macro_export]
macro_rules! abort_call_site {
($($tts:tt)*) => {
$crate::abort!($crate::__export::proc_macro2::Span::call_site(), $($tts)*)
};
}
/// Emit an error while not aborting the proc-macro right away.
///
/// # Syntax
///
/// See [the guide](index.html#guide).
///
#[macro_export]
macro_rules! emit_error {
($err:expr) => {
$crate::diagnostic!($err).emit()
};
($span:expr, $($tts:tt)*) => {{
let level = $crate::Level::Error;
$crate::diagnostic!($span, level, $($tts)*).emit()
}};
}
/// Shortcut for `emit_error!(Span::call_site(), ...)`. This macro
/// is still preferable over plain panic, panics are not for error reporting..
///
/// # Syntax
///
/// See [the guide](index.html#guide).
///
#[macro_export]
macro_rules! emit_call_site_error {
($($tts:tt)*) => {
$crate::emit_error!($crate::__export::proc_macro2::Span::call_site(), $($tts)*)
};
}
/// Emit a warning. Warnings are not errors and compilation won't fail because of them.
///
/// **Does nothing on stable**
///
/// # Syntax
///
/// See [the guide](index.html#guide).
///
#[macro_export]
macro_rules! emit_warning {
($span:expr, $($tts:tt)*) => {
$crate::diagnostic!($span, $crate::Level::Warning, $($tts)*).emit()
};
}
/// Shortcut for `emit_warning!(Span::call_site(), ...)`.
///
/// **Does nothing on stable**
///
/// # Syntax
///
/// See [the guide](index.html#guide).
///
#[macro_export]
macro_rules! emit_call_site_warning {
($($tts:tt)*) => {{
$crate::emit_warning!($crate::__export::proc_macro2::Span::call_site(), $($tts)*)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __pme__suggestions {
($var:ident) => ();
($var:ident $help:ident =? $msg:expr) => {
let $var = if let Some(msg) = $msg {
$var.suggestion(stringify!($help), msg.to_string())
} else {
$var
};
};
($var:ident $help:ident =? $span:expr => $msg:expr) => {
let $var = if let Some(msg) = $msg {
$var.span_suggestion($span.into(), stringify!($help), msg.to_string())
} else {
$var
};
};
($var:ident $help:ident =? $msg:expr ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help =? $msg);
$crate::__pme__suggestions!($var $($rest)*);
};
($var:ident $help:ident =? $span:expr => $msg:expr ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help =? $span => $msg);
$crate::__pme__suggestions!($var $($rest)*);
};
($var:ident $help:ident = $msg:expr) => {
let $var = $var.suggestion(stringify!($help), $msg.to_string());
};
($var:ident $help:ident = $fmt:expr, $($args:expr),+) => {
let $var = $var.suggestion(
stringify!($help),
format!($fmt, $($args),*)
);
};
($var:ident $help:ident = $span:expr => $msg:expr) => {
let $var = $var.span_suggestion($span.into(), stringify!($help), $msg.to_string());
};
($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+) => {
let $var = $var.span_suggestion(
$span.into(),
stringify!($help),
format!($fmt, $($args),*)
);
};
($var:ident $help:ident = $msg:expr ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $msg);
$crate::__pme__suggestions!($var $($rest)*);
};
($var:ident $help:ident = $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $fmt, $($args),*);
$crate::__pme__suggestions!($var $($rest)*);
};
($var:ident $help:ident = $span:expr => $msg:expr ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $span => $msg);
$crate::__pme__suggestions!($var $($rest)*);
};
($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+ ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*);
$crate::__pme__suggestions!($var $($rest)*);
};
// trailing commas
($var:ident $help:ident = $msg:expr,) => {
$crate::__pme__suggestions!($var $help = $msg)
};
($var:ident $help:ident = $fmt:expr, $($args:expr),+,) => {
$crate::__pme__suggestions!($var $help = $fmt, $($args)*)
};
($var:ident $help:ident = $span:expr => $msg:expr,) => {
$crate::__pme__suggestions!($var $help = $span => $msg)
};
($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),*,) => {
$crate::__pme__suggestions!($var $help = $span => $fmt, $($args)*)
};
($var:ident $help:ident = $msg:expr, ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $msg; $($rest)*)
};
($var:ident $help:ident = $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $fmt, $($args),*; $($rest)*)
};
($var:ident $help:ident = $span:expr => $msg:expr, ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $span => $msg; $($rest)*)
};
($var:ident $help:ident = $span:expr => $fmt:expr, $($args:expr),+, ; $($rest:tt)*) => {
$crate::__pme__suggestions!($var $help = $span => $fmt, $($args),*; $($rest)*)
};
}
+3
View File
@@ -0,0 +1,3 @@
pub trait Sealed {}
impl Sealed for crate::Diagnostic {}