blob: f516984f04ec0114f3d1ebcd3072e9fd389f9334 [file] [log] [blame] [edit]
/*!
This module defines the core of the miette protocol: a series of types and
traits that you can implement to get access to miette's (and related library's)
full reporting and such features.
*/
use std::{
fmt::{self, Display},
fs,
panic::Location,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::MietteError;
/// Adds rich metadata to your Error that can be used by
/// [`Report`](crate::Report) to print really nice and human-friendly error
/// messages.
pub trait Diagnostic: std::error::Error {
/// Unique diagnostic code that can be used to look up more information
/// about this `Diagnostic`. Ideally also globally unique, and documented
/// in the toplevel crate's documentation for easy searching. Rust path
/// format (`foo::bar::baz`) is recommended, but more classic codes like
/// `E0123` or enums will work just fine.
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
None
}
/// Diagnostic severity. This may be used by
/// [`ReportHandler`](crate::ReportHandler)s to change the display format
/// of this diagnostic.
///
/// If `None`, reporters should treat this as [`Severity::Error`].
fn severity(&self) -> Option<Severity> {
None
}
/// Additional help text related to this `Diagnostic`. Do you have any
/// advice for the poor soul who's just run into this issue?
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
None
}
/// URL to visit for a more detailed explanation/help about this
/// `Diagnostic`.
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
None
}
/// Source code to apply this `Diagnostic`'s [`Diagnostic::labels`] to.
fn source_code(&self) -> Option<&dyn SourceCode> {
None
}
/// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
None
}
/// Additional related `Diagnostic`s.
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
None
}
/// The cause of the error.
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
None
}
}
macro_rules! box_impls {
($($box_type:ty),*) => {
$(
impl std::error::Error for $box_type {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
(**self).source()
}
fn cause(&self) -> Option<&dyn std::error::Error> {
self.source()
}
}
)*
}
}
box_impls! {
Box<dyn Diagnostic>,
Box<dyn Diagnostic + Send>,
Box<dyn Diagnostic + Send + Sync>
}
impl<T: Diagnostic + Send + Sync + 'static> From<T>
for Box<dyn Diagnostic + Send + Sync + 'static>
{
fn from(diag: T) -> Self {
Box::new(diag)
}
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + Send + 'static> {
fn from(diag: T) -> Self {
Box::<dyn Diagnostic + Send + Sync>::from(diag)
}
}
impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'static> {
fn from(diag: T) -> Self {
Box::<dyn Diagnostic + Send + Sync>::from(diag)
}
}
impl From<&str> for Box<dyn Diagnostic> {
fn from(s: &str) -> Self {
From::from(String::from(s))
}
}
impl<'a> From<&str> for Box<dyn Diagnostic + Send + Sync + 'a> {
fn from(s: &str) -> Self {
From::from(String::from(s))
}
}
impl From<String> for Box<dyn Diagnostic> {
fn from(s: String) -> Self {
let err1: Box<dyn Diagnostic + Send + Sync> = From::from(s);
let err2: Box<dyn Diagnostic> = err1;
err2
}
}
impl From<String> for Box<dyn Diagnostic + Send + Sync> {
fn from(s: String) -> Self {
struct StringError(String);
impl std::error::Error for StringError {}
impl Diagnostic for StringError {}
impl Display for StringError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
// Purposefully skip printing "StringError(..)"
impl fmt::Debug for StringError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
Box::new(StringError(s))
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Send + Sync> {
fn from(s: Box<dyn std::error::Error + Send + Sync>) -> Self {
#[derive(thiserror::Error)]
#[error(transparent)]
struct BoxedDiagnostic(Box<dyn std::error::Error + Send + Sync>);
impl fmt::Debug for BoxedDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl Diagnostic for BoxedDiagnostic {}
Box::new(BoxedDiagnostic(s))
}
}
/**
[`Diagnostic`] severity. Intended to be used by
[`ReportHandler`](crate::ReportHandler)s to change the way different
[`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
*/
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Severity {
/// Just some help. Here's how you could be doing it better.
Advice,
/// Warning. Please take note.
Warning,
/// Critical failure. The program cannot continue.
/// This is the default severity, if you don't specify another one.
Error,
}
impl Default for Severity {
fn default() -> Self {
Severity::Error
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serialize_severity() {
use serde_json::json;
assert_eq!(json!(Severity::Advice), json!("Advice"));
assert_eq!(json!(Severity::Warning), json!("Warning"));
assert_eq!(json!(Severity::Error), json!("Error"));
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialize_severity() {
use serde_json::json;
let severity: Severity = serde_json::from_value(json!("Advice")).unwrap();
assert_eq!(severity, Severity::Advice);
let severity: Severity = serde_json::from_value(json!("Warning")).unwrap();
assert_eq!(severity, Severity::Warning);
let severity: Severity = serde_json::from_value(json!("Error")).unwrap();
assert_eq!(severity, Severity::Error);
}
/**
Represents readable source code of some sort.
This trait is able to support simple `SourceCode` types like [`String`]s, as
well as more involved types like indexes into centralized `SourceMap`-like
types, file handles, and even network streams.
If you can read it, you can source it, and it's not necessary to read the
whole thing--meaning you should be able to support `SourceCode`s which are
gigabytes or larger in size.
*/
pub trait SourceCode: Send + Sync {
/// Read the bytes for a specific span from this SourceCode, keeping a
/// certain number of lines before and after the span as context.
fn read_span<'a>(
&'a self,
span: &SourceSpan,
context_lines_before: usize,
context_lines_after: usize,
) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError>;
}
/// A labeled [`SourceSpan`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LabeledSpan {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
label: Option<String>,
span: SourceSpan,
}
impl LabeledSpan {
/// Makes a new labeled span.
pub const fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
Self {
label,
span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
}
}
/// Makes a new labeled span using an existing span.
pub fn new_with_span(label: Option<String>, span: impl Into<SourceSpan>) -> Self {
Self {
label,
span: span.into(),
}
}
/// Makes a new label at specified span
///
/// # Examples
/// ```
/// use miette::LabeledSpan;
///
/// let source = "Cpp is the best";
/// let label = LabeledSpan::at(0..3, "should be Rust");
/// assert_eq!(
/// label,
/// LabeledSpan::new(Some("should be Rust".to_string()), 0, 3)
/// )
/// ```
pub fn at(span: impl Into<SourceSpan>, label: impl Into<String>) -> Self {
Self::new_with_span(Some(label.into()), span)
}
/// Makes a new label that points at a specific offset.
///
/// # Examples
/// ```
/// use miette::LabeledSpan;
///
/// let source = "(2 + 2";
/// let label = LabeledSpan::at_offset(4, "expected a closing parenthesis");
/// assert_eq!(
/// label,
/// LabeledSpan::new(Some("expected a closing parenthesis".to_string()), 4, 0)
/// )
/// ```
pub fn at_offset(offset: ByteOffset, label: impl Into<String>) -> Self {
Self::new(Some(label.into()), offset, 0)
}
/// Makes a new label without text, that underlines a specific span.
///
/// # Examples
/// ```
/// use miette::LabeledSpan;
///
/// let source = "You have an eror here";
/// let label = LabeledSpan::underline(12..16);
/// assert_eq!(label, LabeledSpan::new(None, 12, 4))
/// ```
pub fn underline(span: impl Into<SourceSpan>) -> Self {
Self::new_with_span(None, span)
}
/// Gets the (optional) label string for this `LabeledSpan`.
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
/// Returns a reference to the inner [`SourceSpan`].
pub const fn inner(&self) -> &SourceSpan {
&self.span
}
/// Returns the 0-based starting byte offset.
pub const fn offset(&self) -> usize {
self.span.offset()
}
/// Returns the number of bytes this `LabeledSpan` spans.
pub const fn len(&self) -> usize {
self.span.len()
}
/// True if this `LabeledSpan` is empty.
pub const fn is_empty(&self) -> bool {
self.span.is_empty()
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serialize_labeled_span() {
use serde_json::json;
assert_eq!(
json!(LabeledSpan::new(None, 0, 0)),
json!({
"span": { "offset": 0, "length": 0 }
})
);
assert_eq!(
json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
json!({
"label": "label",
"span": { "offset": 0, "length": 0 }
})
)
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialize_labeled_span() {
use serde_json::json;
let span: LabeledSpan = serde_json::from_value(json!({
"label": null,
"span": { "offset": 0, "length": 0 }
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({
"span": { "offset": 0, "length": 0 }
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({
"label": "label",
"span": { "offset": 0, "length": 0 }
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))
}
/**
Contents of a [`SourceCode`] covered by [`SourceSpan`].
Includes line and column information to optimize highlight calculations.
*/
pub trait SpanContents<'a> {
/// Reference to the data inside the associated span, in bytes.
fn data(&self) -> &'a [u8];
/// [`SourceSpan`] representing the span covered by this `SpanContents`.
fn span(&self) -> &SourceSpan;
/// An optional (file?) name for the container of this `SpanContents`.
fn name(&self) -> Option<&str> {
None
}
/// The 0-indexed line in the associated [`SourceCode`] where the data
/// begins.
fn line(&self) -> usize;
/// The 0-indexed column in the associated [`SourceCode`] where the data
/// begins, relative to `line`.
fn column(&self) -> usize;
/// Total number of lines covered by this `SpanContents`.
fn line_count(&self) -> usize;
}
/**
Basic implementation of the [`SpanContents`] trait, for convenience.
*/
#[derive(Clone, Debug)]
pub struct MietteSpanContents<'a> {
// Data from a [`SourceCode`], in bytes.
data: &'a [u8],
// span actually covered by this SpanContents.
span: SourceSpan,
// The 0-indexed line where the associated [`SourceSpan`] _starts_.
line: usize,
// The 0-indexed column where the associated [`SourceSpan`] _starts_.
column: usize,
// Number of line in this snippet.
line_count: usize,
// Optional filename
name: Option<String>,
}
impl<'a> MietteSpanContents<'a> {
/// Make a new [`MietteSpanContents`] object.
pub const fn new(
data: &'a [u8],
span: SourceSpan,
line: usize,
column: usize,
line_count: usize,
) -> MietteSpanContents<'a> {
MietteSpanContents {
data,
span,
line,
column,
line_count,
name: None,
}
}
/// Make a new [`MietteSpanContents`] object, with a name for its 'file'.
pub const fn new_named(
name: String,
data: &'a [u8],
span: SourceSpan,
line: usize,
column: usize,
line_count: usize,
) -> MietteSpanContents<'a> {
MietteSpanContents {
data,
span,
line,
column,
line_count,
name: Some(name),
}
}
}
impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
fn data(&self) -> &'a [u8] {
self.data
}
fn span(&self) -> &SourceSpan {
&self.span
}
fn line(&self) -> usize {
self.line
}
fn column(&self) -> usize {
self.column
}
fn line_count(&self) -> usize {
self.line_count
}
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
}
/// Span within a [`SourceCode`]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SourceSpan {
/// The start of the span.
offset: SourceOffset,
/// The total length of the span
length: usize,
}
impl SourceSpan {
/// Create a new [`SourceSpan`].
pub const fn new(start: SourceOffset, length: SourceOffset) -> Self {
Self {
offset: start,
length: length.offset(),
}
}
/// The absolute offset, in bytes, from the beginning of a [`SourceCode`].
pub const fn offset(&self) -> usize {
self.offset.offset()
}
/// Total length of the [`SourceSpan`], in bytes.
pub const fn len(&self) -> usize {
self.length
}
/// Whether this [`SourceSpan`] has a length of zero. It may still be useful
/// to point to a specific point.
pub const fn is_empty(&self) -> bool {
self.length == 0
}
}
impl From<(ByteOffset, usize)> for SourceSpan {
fn from((start, len): (ByteOffset, usize)) -> Self {
Self {
offset: start.into(),
length: len,
}
}
}
impl From<(SourceOffset, SourceOffset)> for SourceSpan {
fn from((start, len): (SourceOffset, SourceOffset)) -> Self {
Self::new(start, len)
}
}
impl From<std::ops::Range<ByteOffset>> for SourceSpan {
fn from(range: std::ops::Range<ByteOffset>) -> Self {
Self {
offset: range.start.into(),
length: range.len(),
}
}
}
impl From<SourceOffset> for SourceSpan {
fn from(offset: SourceOffset) -> Self {
Self { offset, length: 0 }
}
}
impl From<ByteOffset> for SourceSpan {
fn from(offset: ByteOffset) -> Self {
Self {
offset: offset.into(),
length: 0,
}
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serialize_source_span() {
use serde_json::json;
assert_eq!(
json!(SourceSpan::from(0)),
json!({ "offset": 0, "length": 0})
)
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialize_source_span() {
use serde_json::json;
let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
assert_eq!(span, SourceSpan::from(0))
}
/**
"Raw" type for the byte offset from the beginning of a [`SourceCode`].
*/
pub type ByteOffset = usize;
/**
Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`]
*/
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SourceOffset(ByteOffset);
impl SourceOffset {
/// Actual byte offset.
pub const fn offset(&self) -> ByteOffset {
self.0
}
/// Little utility to help convert 1-based line/column locations into
/// miette-compatible Spans
///
/// This function is infallible: Giving an out-of-range line/column pair
/// will return the offset of the last byte in the source.
pub fn from_location(source: impl AsRef<str>, loc_line: usize, loc_col: usize) -> Self {
let mut line = 0usize;
let mut col = 0usize;
let mut offset = 0usize;
for char in source.as_ref().chars() {
if line + 1 >= loc_line && col + 1 >= loc_col {
break;
}
if char == '\n' {
col = 0;
line += 1;
} else {
col += 1;
}
offset += char.len_utf8();
}
SourceOffset(offset)
}
/// Returns an offset for the _file_ location of wherever this function is
/// called. If you want to get _that_ caller's location, mark this
/// function's caller with `#[track_caller]` (and so on and so forth).
///
/// Returns both the filename that was given and the offset of the caller
/// as a [`SourceOffset`].
///
/// Keep in mind that this fill only work if the file your Rust source
/// file was compiled from is actually available at that location. If
/// you're shipping binaries for your application, you'll want to ignore
/// the Err case or otherwise report it.
#[track_caller]
pub fn from_current_location() -> Result<(String, Self), MietteError> {
let loc = Location::caller();
Ok((
loc.file().into(),
fs::read_to_string(loc.file())
.map(|txt| Self::from_location(txt, loc.line() as usize, loc.column() as usize))?,
))
}
}
impl From<ByteOffset> for SourceOffset {
fn from(bytes: ByteOffset) -> Self {
SourceOffset(bytes)
}
}
#[test]
fn test_source_offset_from_location() {
let source = "f\n\noo\r\nbar";
assert_eq!(SourceOffset::from_location(source, 1, 1).offset(), 0);
assert_eq!(SourceOffset::from_location(source, 1, 2).offset(), 1);
assert_eq!(SourceOffset::from_location(source, 2, 1).offset(), 2);
assert_eq!(SourceOffset::from_location(source, 3, 1).offset(), 3);
assert_eq!(SourceOffset::from_location(source, 3, 2).offset(), 4);
assert_eq!(SourceOffset::from_location(source, 3, 3).offset(), 5);
assert_eq!(SourceOffset::from_location(source, 3, 4).offset(), 6);
assert_eq!(SourceOffset::from_location(source, 4, 1).offset(), 7);
assert_eq!(SourceOffset::from_location(source, 4, 2).offset(), 8);
assert_eq!(SourceOffset::from_location(source, 4, 3).offset(), 9);
assert_eq!(SourceOffset::from_location(source, 4, 4).offset(), 10);
// Out-of-range
assert_eq!(
SourceOffset::from_location(source, 5, 1).offset(),
source.len()
);
}
#[cfg(feature = "serde")]
#[test]
fn test_serialize_source_offset() {
use serde_json::json;
assert_eq!(json!(SourceOffset::from(0)), 0)
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialize_source_offset() {
let offset: SourceOffset = serde_json::from_str("0").unwrap();
assert_eq!(offset, SourceOffset::from(0))
}