#![cfg_attr(all(test, unstable_assert_matches), feature(assert_matches))]
#![cfg_attr(unstable_never_type, feature(never_type))]
#![cfg_attr(unstable_proc_macro_diagnostic, feature(proc_macro_diagnostic))]
#![cfg_attr(unstable_try_trait_v2, feature(try_trait_v2))]
#![cfg_attr(unstable_try_trait_v2_residual, feature(try_trait_v2_residual))]
#![doc = include_str!("../README.md")]
use std::fmt::{Debug, Display};
extern crate proc_macro;
use proc_macro::TokenStream as TokenStream1;
use proc_macro2::Span;
#[cfg(has_try_trait_v2)]
use crate::DiagnosticResult_::{Error, Ok as Ok_, Warning};
pub mod prelude {
pub use super::AsDiagnostic;
pub use super::DiagnosticKind;
pub use super::DiagnosticResult;
pub use super::DiagnosticStream;
pub use super::{Ok, error, error_spanned, warn_spanned};
}
pub type DiagnosticStream = DiagnosticResult<proc_macro2::TokenStream>;
#[derive(Clone, Debug)]
#[must_use = "this `DiagnosticResult` may be an error or a warning, which should be emitted"]
#[cfg(has_try_trait_v2)]
pub struct DiagnosticResult<T> {
inner: DiagnosticResult_<T>,
}
#[cfg(not(has_try_trait_v2))]
pub type DiagnosticResult<T> = Result<T, Diagnostic>;
#[derive(Clone, Debug)]
pub enum DiagnosticResultKind {
Ok,
Warning,
Error,
}
pub trait DiagnosticKind {
fn kind(&self) -> DiagnosticResultKind;
fn is_warning(&self) -> bool;
fn is_error(&self) -> bool;
}
impl<T> DiagnosticKind for DiagnosticResult<T> {
fn kind(&self) -> DiagnosticResultKind {
#[cfg(has_try_trait_v2)]
match self.inner {
DiagnosticResult_::Ok(_) => DiagnosticResultKind::Ok,
DiagnosticResult_::Warning(_, _) => DiagnosticResultKind::Warning,
DiagnosticResult_::Error(_) => DiagnosticResultKind::Error,
}
#[cfg(not(has_try_trait_v2))]
match self {
Result::Ok(_) => DiagnosticResultKind::Ok,
Result::Err(_) => DiagnosticResultKind::Error,
}
}
fn is_warning(&self) -> bool {
#[cfg(has_try_trait_v2)]
{
matches!(&self.kind(), DiagnosticResultKind::Warning)
}
#[cfg(not(has_try_trait_v2))]
{
false
}
}
fn is_error(&self) -> bool {
matches!(&self.kind(), DiagnosticResultKind::Error)
}
}
#[derive(Clone, Debug)]
#[cfg(has_try_trait_v2)]
enum DiagnosticResult_<T> {
Ok(T),
Warning(T, Diagnostic),
Error(Diagnostic),
}
#[expect(non_snake_case, reason = "same feel as a Result type alias")]
pub fn Ok<T>(val: T) -> DiagnosticResult<T> {
#[cfg(has_try_trait_v2)]
{
DiagnosticResult { inner: Ok_(val) }
}
#[cfg(not(has_try_trait_v2))]
{
Result::Ok(val)
}
}
pub fn error<T, MSG: ToString>(message: MSG) -> DiagnosticResult<T> {
let diagnostic = Diagnostic::new(Level::Error, Span::call_site(), message);
#[cfg(has_try_trait_v2)]
{
DiagnosticResult {
inner: Error(diagnostic),
}
}
#[cfg(not(has_try_trait_v2))]
{
Err(diagnostic)
}
}
pub fn error_spanned<T, MSG: ToString, SPN: MultiSpan>(
span: SPN,
message: MSG,
) -> DiagnosticResult<T> {
let diagnostic = Diagnostic::new(Level::Error, span, message);
#[cfg(has_try_trait_v2)]
{
DiagnosticResult {
inner: Error(diagnostic),
}
}
#[cfg(not(has_try_trait_v2))]
{
Err(diagnostic)
}
}
pub fn warn_spanned<T, MSG: ToString, SPN: MultiSpan>(
value: T,
#[allow(
unused_variables,
reason = "warnings ignored if try trait not available"
)]
span: SPN,
#[allow(
unused_variables,
reason = "warnings ignored if try trait not available"
)]
message: MSG,
) -> DiagnosticResult<T> {
let warning = Diagnostic::new(Level::Warning, span, message);
#[cfg(has_try_trait_v2)]
{
DiagnosticResult {
inner: Warning(value, warning),
}
}
#[cfg(not(has_try_trait_v2))]
{
warning.emit();
Result::Ok(value)
}
}
pub trait ToDiagnostic<T> {
fn or_error<MSG: ToString>(self, message: MSG) -> DiagnosticResult<T>;
fn or_error_spanned<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T>;
fn or_warn_spanned_with_default<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T>
where
T: Default;
}
impl<T> ToDiagnostic<T> for Option<T> {
fn or_error<MSG: ToString>(self, message: MSG) -> DiagnosticResult<T> {
self.or_error_spanned(Span::call_site(), message)
}
fn or_error_spanned<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T> {
match self {
Some(val) => Ok(val),
None => error_spanned(span, message),
}
}
fn or_warn_spanned_with_default<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T>
where
T: Default,
{
match self {
Some(val) => Ok(val),
None => warn_spanned(Default::default(), span, message),
}
}
}
pub trait AsDiagnostic<T> {
fn add_help<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T>;
fn add_note<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T>;
}
impl<T, E> AsDiagnostic<T> for Result<T, E>
where
Diagnostic: From<E>,
{
fn add_help<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T> {
match self {
Result::Ok(val) => Ok(val),
Result::Err(e) => {
let mut diagnostic = Diagnostic::from(e);
diagnostic.add_help(span, message);
#[cfg(not(has_try_trait_v2))]
{
Err(diagnostic)
}
#[cfg(has_try_trait_v2)]
{
DiagnosticResult {
inner: Error(diagnostic),
}
}
}
}
}
fn add_note<MSG: ToString, SPN: MultiSpan>(
self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T> {
match self {
Result::Ok(val) => Ok(val),
Result::Err(e) => {
let mut diagnostic = Diagnostic::from(e);
diagnostic.add_note(span, message);
#[cfg(not(has_try_trait_v2))]
{
Err(diagnostic)
}
#[cfg(has_try_trait_v2)]
{
DiagnosticResult {
inner: Error(diagnostic),
}
}
}
}
}
}
#[cfg(has_try_trait_v2)]
impl<T> AsDiagnostic<T> for DiagnosticResult<T> {
fn add_help<MSG: ToString, SPN: MultiSpan>(
mut self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T> {
match self.inner {
Ok_(_) => self,
Warning(_, ref mut diagnostic) | Error(ref mut diagnostic) => {
diagnostic.add_help(span, message);
self
}
}
}
fn add_note<MSG: ToString, SPN: MultiSpan>(
mut self,
span: SPN,
message: MSG,
) -> DiagnosticResult<T> {
match self.inner {
Ok_(_) => self,
Warning(_, ref mut diagnostic) | Error(ref mut diagnostic) => {
diagnostic.add_note(span, message);
self
}
}
}
}
#[cfg(has_try_trait_v2)]
impl<T> DiagnosticResult<T> {
pub fn is_ok(&self) -> bool {
matches!(&self.kind(), DiagnosticResultKind::Ok)
}
pub fn unwrap(self) -> T
where
T: Debug,
{
match self.inner {
Ok_(t) => t,
Warning(val, warning) => panic!(
"Called unwrap on value {:?} with accompanying warning: {:?}",
val, warning
),
Error(error) => panic!("Called unwrap on an error: {:?}", error),
}
}
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
level: Level,
message: String,
spans: Vec<Span>,
children: Vec<Diagnostic>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Level {
Error,
Warning,
Note,
Help,
}
impl Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Level::Error => write!(f, "error"),
Level::Warning => write!(f, "warning"),
Level::Note => write!(f, "note"),
Level::Help => write!(f, "help"),
}
}
}
impl Diagnostic {
fn new<MSG: ToString, SPN: MultiSpan>(level: Level, span: SPN, message: MSG) -> Self {
Self {
level,
message: message.to_string(),
spans: span.into_spans(),
children: vec![],
}
}
fn push(&mut self, child: Diagnostic) {
self.children.push(child);
}
fn add_child<MSG: ToString, SPN: MultiSpan>(&mut self, level: Level, span: SPN, message: MSG) {
self.children.push(Diagnostic::new(level, span, message));
}
fn add_help<MSG: ToString, SPN: MultiSpan>(&mut self, span: SPN, message: MSG) {
self.add_child(Level::Help, span, message);
}
fn add_note<MSG: ToString, SPN: MultiSpan>(&mut self, span: SPN, message: MSG) {
self.add_child(Level::Note, span, message);
}
fn spans_call_site(&self) -> bool {
let call_site = Span::call_site();
let cs_file = call_site.local_file();
let cs_start = call_site.start();
let cs_end = call_site.end();
let is_call_site = |span: &Span| {
span.local_file() == cs_file && span.start() == cs_start && span.end() == cs_end
};
self.spans.iter().any(is_call_site) || self.children.iter().any(Diagnostic::spans_call_site)
}
}
impl From<syn::Error> for Diagnostic {
fn from(error: syn::Error) -> Self {
let mut diagnostic = Diagnostic::new(Level::Error, error.span(), error.to_string());
for err in error.into_iter().skip(1) {
diagnostic.push(err.into());
}
diagnostic
}
}
impl Diagnostic {
pub fn emit(mut self) -> TokenStream1 {
if !self.spans_call_site() {
self.add_note(
Span::call_site(),
format!(
"this {} originates from the macro invocation here",
self.level
),
);
};
#[cfg(has_proc_macro_diagnostic)]
{
let spans = self.as_spans();
let mut pm_diagnostic =
proc_macro::Diagnostic::spanned(spans, self.level.into(), self.message);
for child in self.children {
pm_diagnostic = child.add_to_parent(pm_diagnostic);
}
pm_diagnostic.emit();
TokenStream1::new()
}
#[cfg(not(has_proc_macro_diagnostic))]
{
self.into_syn_err().into_compile_error().into()
}
}
#[cfg(not(has_proc_macro_diagnostic))]
fn into_syn_err(self) -> syn::Error {
let span = self.spans.into_iter().next().expect("at least one span");
let message = if matches!(self.level, Level::Error) {
self.message
} else {
format!("{}: {}", self.level, self.message)
};
let mut err = syn::Error::new(span, message);
for child in self.children {
err.combine(child.into_syn_err());
}
err
}
#[cfg(has_proc_macro_diagnostic)]
fn add_to_parent(self, parent: proc_macro::Diagnostic) -> proc_macro::Diagnostic {
let msg = self.message.clone();
match self.level {
Level::Error => parent.span_error(self.as_spans(), msg),
Level::Warning => parent.span_warning(self.as_spans(), msg),
Level::Note => parent.span_note(self.as_spans(), msg),
Level::Help => parent.span_help(self.as_spans(), msg),
}
}
#[cfg(has_proc_macro_diagnostic)]
fn as_spans(&self) -> Vec<proc_macro::Span> {
self.spans.iter().map(|span| span.unwrap()).collect()
}
}
#[cfg(has_proc_macro_diagnostic)]
impl From<Level> for proc_macro::Level {
fn from(level: Level) -> Self {
match level {
Level::Error => Self::Error,
Level::Help => Self::Help,
Level::Note => Self::Note,
Level::Warning => Self::Warning,
}
}
}
pub trait MultiSpan {
fn into_spans(self) -> Vec<Span>;
}
impl MultiSpan for Span {
fn into_spans(self) -> Vec<Span> {
vec![self]
}
}
impl MultiSpan for Vec<Span> {
fn into_spans(self) -> Vec<Span> {
self
}
}
impl MultiSpan for &[Span] {
fn into_spans(self) -> Vec<Span> {
self.to_vec()
}
}
#[cfg(all(has_never_type, has_try_trait_v2))]
impl<T> std::ops::Try for DiagnosticResult<T> {
type Output = T;
type Residual = DiagnosticResult<!>;
fn from_output(output: Self::Output) -> Self {
Self { inner: Ok_(output) }
}
fn branch(self) -> std::ops::ControlFlow<Self::Residual, Self::Output> {
match self.inner {
Ok_(t) => std::ops::ControlFlow::Continue(t),
Warning(t, d) => {
d.emit();
std::ops::ControlFlow::Continue(t)
}
Error(d) => std::ops::ControlFlow::Break(DiagnosticResult { inner: Error(d) }),
}
}
}
#[cfg(all(has_never_type, has_try_trait_v2))]
impl<T> std::ops::FromResidual<DiagnosticResult<!>> for DiagnosticResult<T> {
fn from_residual(residual: DiagnosticResult<!>) -> Self {
match residual.inner {
Error(residual) => Self {
inner: Error(residual),
},
}
}
}
#[cfg(all(has_never_type, has_try_trait_v2_residual))]
impl<T> std::ops::Residual<T> for DiagnosticResult<!> {
type TryType = DiagnosticResult<T>;
}
#[cfg(has_try_trait_v2)]
impl<T, E> std::ops::FromResidual<Result<std::convert::Infallible, E>> for DiagnosticResult<T>
where
E: Into<Diagnostic>,
{
fn from_residual(result: Result<std::convert::Infallible, E>) -> Self {
match result {
Err(e) => DiagnosticResult {
inner: DiagnosticResult_::Error(e.into()),
},
}
}
}
#[cfg(has_try_trait_v2)]
impl<T, E> From<E> for DiagnosticResult<T>
where
E: Into<Diagnostic> + std::error::Error,
{
fn from(error: E) -> Self {
Self {
inner: DiagnosticResult_::Error(error.into()),
}
}
}
pub trait ToTokens {
fn to_tokens(self) -> TokenStream1;
}
impl ToTokens for DiagnosticStream {
fn to_tokens(self) -> TokenStream1 {
#[cfg(has_try_trait_v2)]
match self.inner {
Ok_(t) => t.into(),
Warning(t, warning) => {
_ = warning.emit();
t.into()
}
Error(error) => error.emit(),
}
#[cfg(not(has_try_trait_v2))]
match self {
Self::Ok(t) => t.into(),
Self::Err(error) => error.emit(),
}
}
}
#[cfg(test)]
mod tests {
#[cfg(assert_matches_location = "root")]
use std::assert_matches;
#[cfg(assert_matches_location = "module")]
use std::assert_matches::assert_matches;
use super::*;
#[test]
fn is_ok() {
assert!(Ok(()).is_ok());
}
#[test]
#[cfg(has_try_trait_v2)] fn is_warning() {
assert!(warn_spanned((), Span::call_site(), "foo").is_warning());
}
#[test]
fn is_error() {
assert!(error::<(), &str>("foo").is_error());
}
#[test]
fn kind() {
match Ok(()).kind() {
DiagnosticResultKind::Ok => (),
DiagnosticResultKind::Warning => panic!("not a warning"),
DiagnosticResultKind::Error => panic!("not an error"),
}
}
#[test]
#[cfg(has_assert_matches)]
fn ok_with_help() {
assert_matches!(
Ok(()).add_help(Span::call_site(), "help text").kind(),
DiagnosticResultKind::Ok
)
}
#[test]
fn ok_or() {
fn five() -> DiagnosticResult<i32> {
let five = Some(5).or_error("oops!")?;
Ok(five)
}
assert_eq!(five().unwrap(), 5)
}
}