use std::fmt;
use miette::{LabeledSpan, SourceCode};
use oneyaml_core::{Applicability, Code, Diagnostic, Label, Severity, Suggestion};
use crate::Error;
use crate::error::message;
fn docs_url(code: Code) -> Option<String> {
let info = code.info()?;
Some(format!(
"https://docs.rs/oneyaml-core/latest/oneyaml_core/code/constant.{}.html",
info.name
))
}
fn severity(s: Severity) -> miette::Severity {
match s {
Severity::Warning => miette::Severity::Warning,
_ => miette::Severity::Error,
}
}
fn labeled(label: &Label, primary: bool) -> LabeledSpan {
let text = (!label.message.is_empty()).then(|| message(format_args!("{}", label.message)));
let span = label.span.start..label.span.end.max(label.span.start);
if primary {
LabeledSpan::new_primary_with_span(text, span)
} else {
LabeledSpan::new_with_span(text, span)
}
}
fn help_text(d: &Diagnostic) -> Option<String> {
let mut out = d
.help
.as_ref()
.map(|h| message(format_args!("{h}")))
.unwrap_or_default();
for Suggestion {
message: text,
edits,
applicability,
..
} in &d.suggestions
{
if !out.is_empty() {
out.push('\n');
}
out.push_str(if *applicability == Applicability::MachineApplicable {
"suggestion: "
} else {
"suggestion (may be incorrect): "
});
out.push_str(&message(format_args!("{text}")));
if let [edit] = edits.as_slice() {
if !edit.text.is_empty() {
out.push_str(": write `");
out.push_str(&crate::render::excerpt(&edit.text));
out.push('`');
}
}
}
(!out.is_empty()).then_some(out)
}
fn labels(d: &Diagnostic, has_span: bool) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
let primary = has_span.then(|| labeled(&d.primary, true));
let secondary = d.secondary.iter().map(|l| labeled(l, false));
let all: Vec<LabeledSpan> = primary.into_iter().chain(secondary).collect();
(!all.is_empty()).then(|| Box::new(all.into_iter()) as Box<dyn Iterator<Item = LabeledSpan>>)
}
#[cfg_attr(docsrs, doc(cfg(feature = "miette")))]
impl miette::Diagnostic for Error {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
Some(Box::new(self.code()))
}
fn severity(&self) -> Option<miette::Severity> {
Some(severity(self.diagnostic().severity))
}
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
help_text(self.diagnostic()).map(|h| Box::new(h) as Box<dyn fmt::Display>)
}
fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
docs_url(self.code()).map(|u| Box::new(u) as Box<dyn fmt::Display>)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
labels(self.diagnostic(), self.span().is_some())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(docsrs, doc(cfg(feature = "miette")))]
pub struct DiagnosticReport {
diagnostic: Diagnostic,
source: Option<String>,
}
impl DiagnosticReport {
#[must_use]
pub fn new(diagnostic: Diagnostic) -> Self {
Self {
diagnostic,
source: None,
}
}
#[must_use]
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub fn diagnostic(&self) -> &Diagnostic {
&self.diagnostic
}
#[must_use]
pub fn into_diagnostic(self) -> Diagnostic {
self.diagnostic
}
}
impl From<Diagnostic> for DiagnosticReport {
fn from(diagnostic: Diagnostic) -> Self {
Self::new(diagnostic)
}
}
impl fmt::Display for DiagnosticReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&message(format_args!("{}", self.diagnostic.message)))?;
if let Some(path) = self.diagnostic.path.as_ref().filter(|p| !p.is_empty()) {
write!(f, " at {}", crate::error::shown_path(path))?;
}
Ok(())
}
}
impl std::error::Error for DiagnosticReport {}
#[cfg_attr(docsrs, doc(cfg(feature = "miette")))]
impl miette::Diagnostic for DiagnosticReport {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
Some(Box::new(self.diagnostic.code))
}
fn severity(&self) -> Option<miette::Severity> {
Some(severity(self.diagnostic.severity))
}
fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
help_text(&self.diagnostic).map(|h| Box::new(h) as Box<dyn fmt::Display>)
}
fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
docs_url(self.diagnostic.code).map(|u| Box::new(u) as Box<dyn fmt::Display>)
}
fn source_code(&self) -> Option<&dyn SourceCode> {
self.source.as_ref().map(|s| s as &dyn SourceCode)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
labels(&self.diagnostic, true)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use super::*;
use miette::Diagnostic as _;
#[test]
fn every_registered_code_has_a_url() {
for info in oneyaml_core::code::REGISTRY {
let url = docs_url(info.code).unwrap();
assert!(url.ends_with(&format!("constant.{}.html", info.name)));
}
assert_eq!(docs_url(Code::new(999).unwrap()), None);
}
#[test]
fn severity_and_labels_map() {
let d = Diagnostic::warning(
oneyaml_core::code::DUPLICATE_KEY_LAST_WINS,
"dup",
Label::new(oneyaml_core::Span::new(4, 2), ""),
)
.with_secondary(Label::new(oneyaml_core::Span::new(0, 1), "first"));
let r = DiagnosticReport::from(d.clone());
assert_eq!(r.severity(), Some(miette::Severity::Warning));
let labels: Vec<LabeledSpan> = r.labels().unwrap().collect();
assert_eq!(labels.len(), 2);
assert!(labels[0].primary());
assert_eq!(labels[0].len(), 0); assert_eq!(labels[1].label(), Some("first"));
assert!(r.help().is_none());
assert!(r.source_code().is_none());
assert_eq!(r.clone().into_diagnostic(), d);
assert_eq!(r.diagnostic(), &d);
}
}