├── .gitignore ├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ ├── release.yml │ ├── zizmor.yml │ └── ci.yml ├── .editorconfig ├── rust-toolchain.toml ├── images ├── serde_json.png ├── code_linking.png └── single-line-example.png ├── .rustfmt.toml ├── tests ├── test_handler_access.rs ├── compiletest.rs ├── test_autotrait.rs ├── common │ └── mod.rs ├── test_repr.rs ├── test_source_code_name.rs ├── test_macros.rs ├── test_convert.rs ├── drop │ └── mod.rs ├── test_chain.rs ├── test_source.rs ├── test_derive_source_chain.rs ├── test_fmt.rs ├── test_emoji_underline.rs ├── test_downcast.rs ├── test_location.rs ├── test_diagnostic_source_macro.rs ├── test_context.rs ├── test_derive_attr.rs ├── color_format.rs └── test_boxed.rs ├── .typos.toml ├── release-plz.toml ├── miette-derive ├── Cargo.toml ├── src │ ├── lib.rs │ ├── diagnostic_arg.rs │ ├── diagnostic_source.rs │ ├── code.rs │ ├── related.rs │ ├── severity.rs │ ├── utils.rs │ ├── source_code.rs │ ├── url.rs │ ├── forward.rs │ ├── help.rs │ └── fmt.rs └── LICENSE ├── README.md ├── src ├── handlers │ ├── mod.rs │ ├── debug.rs │ ├── json.rs │ └── theme.rs ├── eyreish │ ├── fmt.rs │ ├── into_diagnostic.rs │ ├── kind.rs │ ├── ptr.rs │ ├── context.rs │ ├── wrapper.rs │ └── macros.rs ├── macro_helpers.rs ├── error.rs ├── diagnostic_chain.rs ├── named_source.rs ├── chain.rs ├── panic.rs └── source_impls.rs ├── cliff.toml ├── justfile ├── Cargo.toml ├── CODE_OF_CONDUCT.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Boshen, zkat] 2 | open_collective: oxc 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | trim_trailing_whitespace = true 2 | insert_final_newline = true 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.92.0" 3 | profile = "default" 4 | -------------------------------------------------------------------------------- /images/serde_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-miette/main/images/serde_json.png -------------------------------------------------------------------------------- /images/code_linking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-miette/main/images/code_linking.png -------------------------------------------------------------------------------- /images/single-line-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxc-project/oxc-miette/main/images/single-line-example.png -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | use_small_heuristics = "Max" 3 | use_field_init_shorthand = true 4 | reorder_modules = true 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>Boshen/renovate"] 4 | } 5 | -------------------------------------------------------------------------------- /tests/test_handler_access.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_handler() { 3 | use miette::{Report, miette}; 4 | 5 | let error: Report = miette!("oh no!"); 6 | let _ = error.handler(); 7 | } 8 | -------------------------------------------------------------------------------- /tests/compiletest.rs: -------------------------------------------------------------------------------- 1 | #[rustversion::attr(not(nightly), ignore)] 2 | #[cfg_attr(miri, ignore)] 3 | #[test] 4 | fn ui() { 5 | let t = trybuild::TestCases::new(); 6 | t.compile_fail("tests/ui/*.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/crate-ci/typos 2 | # cargo install typos-cli 3 | # typos 4 | 5 | [files] 6 | extend-exclude = [ 7 | "CHANGELOG.md", 8 | "tests/**" 9 | ] 10 | 11 | [default.extend-words] 12 | welp = "welp" 13 | -------------------------------------------------------------------------------- /tests/test_autotrait.rs: -------------------------------------------------------------------------------- 1 | use miette::Report; 2 | 3 | #[test] 4 | fn test_send() { 5 | fn assert_send() {} 6 | assert_send::(); 7 | } 8 | 9 | #[test] 10 | fn test_sync() { 11 | fn assert_sync() {} 12 | assert_sync::(); 13 | } 14 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use miette::{Result, bail}; 4 | 5 | pub fn bail_literal() -> Result<()> { 6 | bail!("oh no!"); 7 | } 8 | 9 | pub fn bail_fmt() -> Result<()> { 10 | bail!("{} {}!", "oh", "no"); 11 | } 12 | 13 | pub fn bail_error() -> Result<()> { 14 | bail!(io::Error::other("oh no!")); 15 | } 16 | -------------------------------------------------------------------------------- /release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | semver_check = true 3 | allow_dirty = true 4 | changelog_config = "cliff.toml" 5 | 6 | [[package]] 7 | name = "oxc-miette" 8 | version_group = "group" 9 | changelog_include = ["oxc-miette-derive"] 10 | 11 | [[package]] 12 | name = "oxc-miette-derive" 13 | version_group = "group" 14 | changelog_update = false 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | release-plz: 12 | name: Release-plz 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | id-token: write 18 | steps: 19 | - uses: oxc-project/release-plz@44b98e8dda1a7783d4ec2ef66e2f37a3e8c1c759 # v1.0.4 20 | with: 21 | PAT: ${{ secrets.OXC_BOT_PAT }} 22 | -------------------------------------------------------------------------------- /miette-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oxc-miette-derive" 3 | description = "Derive macros for miette. Like `thiserror` for Diagnostics." 4 | version = "2.6.0" 5 | authors.workspace = true 6 | categories.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | rust-version.workspace = true 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | # Relaxed version so the user can decide which version to use. 17 | proc-macro2 = "1" 18 | quote = "1" 19 | syn = "2" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `oxc_miette` 2 | 3 | Fork of https://github.com/zkat/miette 4 | 5 | ### License 6 | 7 | `miette` is released to the Rust community under the [Apache license 2.0](./LICENSE). 8 | 9 | It also includes code taken from [`eyre`](https://github.com/yaahc/eyre), 10 | and some from [`thiserror`](https://github.com/dtolnay/thiserror), also 11 | under the Apache License. Some code is taken from 12 | [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed. 13 | 14 | [`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html 15 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Reporters included with `miette`. 3 | */ 4 | 5 | #[allow(unreachable_pub)] 6 | pub use debug::*; 7 | #[allow(unreachable_pub)] 8 | #[cfg(feature = "fancy-base")] 9 | pub use graphical::*; 10 | #[allow(unreachable_pub)] 11 | pub use json::*; 12 | #[allow(unreachable_pub)] 13 | pub use narratable::*; 14 | #[allow(unreachable_pub)] 15 | #[cfg(feature = "fancy-base")] 16 | pub use theme::*; 17 | 18 | mod debug; 19 | #[cfg(feature = "fancy-base")] 20 | mod graphical; 21 | mod json; 22 | mod narratable; 23 | #[cfg(feature = "fancy-base")] 24 | mod theme; 25 | -------------------------------------------------------------------------------- /tests/test_repr.rs: -------------------------------------------------------------------------------- 1 | mod drop; 2 | 3 | use std::mem; 4 | 5 | use miette::Report; 6 | 7 | use self::drop::{DetectDrop, Flag}; 8 | 9 | #[test] 10 | fn test_error_size() { 11 | assert_eq!(mem::size_of::(), mem::size_of::()); 12 | } 13 | 14 | #[test] 15 | fn test_null_pointer_optimization() { 16 | assert_eq!(mem::size_of::>(), mem::size_of::()); 17 | } 18 | 19 | #[test] 20 | fn test_autotraits() { 21 | fn assert() {} 22 | assert::(); 23 | } 24 | 25 | #[test] 26 | fn test_drop() { 27 | let has_dropped = Flag::new(); 28 | drop(Report::new(DetectDrop::new(&has_dropped))); 29 | assert!(has_dropped.get()); 30 | } 31 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [git] 5 | commit_parsers = [ 6 | { message = "^feat", group = "Features" }, 7 | { message = "^fix", group = "Bug Fixes" }, 8 | { message = "^perf", group = "Performance" }, 9 | { message = "^doc", group = "Documentation" }, 10 | { message = "^refactor", group = "Refactor" }, 11 | { message = "^style", group = "Styling" }, 12 | { message = "^test", group = "Testing" }, 13 | { message = "^chore", group = "Chore" }, 14 | { message = "^ci", group = "CI" }, 15 | ] 16 | # protect breaking changes from being skipped due to matching a skipping commit_parser 17 | protect_breaking_commits = false 18 | -------------------------------------------------------------------------------- /miette-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use diagnostic::Diagnostic; 2 | use quote::quote; 3 | use syn::{DeriveInput, parse_macro_input}; 4 | 5 | mod code; 6 | mod diagnostic; 7 | mod diagnostic_arg; 8 | mod diagnostic_source; 9 | mod fmt; 10 | mod forward; 11 | mod help; 12 | mod label; 13 | mod related; 14 | mod severity; 15 | mod source_code; 16 | mod url; 17 | mod utils; 18 | 19 | #[proc_macro_derive( 20 | Diagnostic, 21 | attributes(diagnostic, source_code, label, related, help, diagnostic_source) 22 | )] 23 | pub fn derive_diagnostic(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 24 | let input = parse_macro_input!(input as DeriveInput); 25 | let cmd = match Diagnostic::from_derive_input(input) { 26 | Ok(cmd) => cmd.r#gen(), 27 | Err(err) => return err.to_compile_error().into(), 28 | }; 29 | // panic!("{:#}", cmd.to_token_stream()); 30 | quote!(#cmd).into() 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_source_code_name.rs: -------------------------------------------------------------------------------- 1 | use miette::{NamedSource, SourceCode}; 2 | 3 | #[test] 4 | fn test_basic_source_code_name_is_none() { 5 | let source = "Hello, world!"; 6 | assert_eq!(source.name(), None); 7 | 8 | let source = String::from("Hello, world!"); 9 | assert_eq!(source.name(), None); 10 | } 11 | 12 | #[test] 13 | fn test_named_source_returns_name() { 14 | let source = "Hello, world!"; 15 | let named = NamedSource::new("test.txt", source); 16 | // Call the trait method explicitly through SourceCode trait 17 | assert_eq!(SourceCode::name(&named), Some("test.txt")); 18 | } 19 | 20 | #[test] 21 | fn test_named_source_with_string() { 22 | let source = String::from("fn main() {}"); 23 | let named = NamedSource::new("main.rs", source); 24 | // Call the trait method explicitly through SourceCode trait 25 | assert_eq!(SourceCode::name(&named), Some("main.rs")); 26 | } 27 | -------------------------------------------------------------------------------- /src/eyreish/fmt.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use super::{error::ErrorImpl, ptr::Ref}; 4 | 5 | impl ErrorImpl<()> { 6 | pub(crate) unsafe fn display(this: Ref<'_, Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result { 7 | unsafe { 8 | this.deref() 9 | .handler 10 | .as_ref() 11 | .map(|handler| handler.display(Self::error(this), f)) 12 | .unwrap_or_else(|| core::fmt::Display::fmt(Self::diagnostic(this), f)) 13 | } 14 | } 15 | 16 | pub(crate) unsafe fn debug(this: Ref<'_, Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | unsafe { 18 | this.deref() 19 | .handler 20 | .as_ref() 21 | .map(|handler| handler.debug(Self::diagnostic(this), f)) 22 | .unwrap_or_else(|| core::fmt::Debug::fmt(Self::diagnostic(this), f)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S just --justfile 2 | 3 | set windows-shell := ["powershell"] 4 | set shell := ["bash", "-cu"] 5 | 6 | _default: 7 | @just --list -u 8 | 9 | alias r := ready 10 | 11 | # Make sure you have cargo-binstall installed. 12 | # You can download the pre-compiled binary from 13 | # or install via `cargo install cargo-binstall` 14 | # Initialize the project by installing all the necessary tools. 15 | init: 16 | cargo binstall watchexec-cli cargo-insta typos-cli cargo-shear -y 17 | 18 | # When ready, run the same CI commands 19 | ready: 20 | git diff --exit-code --quiet 21 | typos 22 | cargo shear 23 | cargo fmt 24 | cargo check 25 | cargo clippy 26 | cargo test --features fancy 27 | cargo doc 28 | git status 29 | 30 | watch *args='': 31 | watchexec {{args}} 32 | 33 | watch-check: 34 | just watch "'cargo check; cargo clippy'" 35 | -------------------------------------------------------------------------------- /tests/test_macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::eq_op)] 2 | mod common; 3 | 4 | use miette::{Result, ensure}; 5 | 6 | use self::common::*; 7 | 8 | #[test] 9 | fn test_messages() { 10 | assert_eq!("oh no!", bail_literal().unwrap_err().to_string()); 11 | assert_eq!("oh no!", bail_fmt().unwrap_err().to_string()); 12 | assert_eq!("oh no!", bail_error().unwrap_err().to_string()); 13 | } 14 | 15 | #[test] 16 | fn test_ensure() { 17 | let f = || -> Result<()> { 18 | ensure!(1 + 1 == 2, "This is correct"); 19 | Ok(()) 20 | }; 21 | assert!(f().is_ok()); 22 | 23 | let v = 1; 24 | let f = || -> Result<()> { 25 | ensure!(v + v == 2, "This is correct, v: {}", v); 26 | Ok(()) 27 | }; 28 | assert!(f().is_ok()); 29 | 30 | let f = || -> Result<()> { 31 | ensure!(v + v == 1, "This is not correct, v: {}", v); 32 | Ok(()) 33 | }; 34 | assert!(f().is_err()); 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, synchronize] 7 | paths: 8 | - ".github/workflows/**" 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - ".github/workflows/**" 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | zizmor: 19 | name: zizmor 20 | runs-on: ubuntu-latest 21 | permissions: 22 | security-events: write 23 | steps: 24 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | with: 26 | persist-credentials: false 27 | 28 | - uses: taiki-e/install-action@61e5998d108b2b55a81b9b386c18bd46e4237e4f # v2.63.1 29 | with: 30 | tool: zizmor 31 | 32 | - run: zizmor --format sarif . > results.sarif 33 | env: 34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 37 | with: 38 | sarif_file: results.sarif 39 | category: zizmor 40 | -------------------------------------------------------------------------------- /tests/test_convert.rs: -------------------------------------------------------------------------------- 1 | mod drop; 2 | 3 | use miette::{Diagnostic, Report, Result}; 4 | 5 | use self::drop::{DetectDrop, Flag}; 6 | 7 | #[test] 8 | fn test_convert() { 9 | let has_dropped = Flag::new(); 10 | let error: Report = Report::new(DetectDrop::new(&has_dropped)); 11 | let box_dyn = Box::::from(error); 12 | assert_eq!("oh no!", box_dyn.to_string()); 13 | drop(box_dyn); 14 | assert!(has_dropped.get()); 15 | } 16 | 17 | #[test] 18 | #[allow(clippy::unnecessary_wraps)] 19 | fn test_question_mark() -> Result<(), Box> { 20 | fn f() -> Result<()> { 21 | Ok(()) 22 | } 23 | f()?; 24 | Ok(()) 25 | } 26 | 27 | #[test] 28 | fn test_convert_stderr() { 29 | let has_dropped = Flag::new(); 30 | let error: Report = Report::new(DetectDrop::new(&has_dropped)); 31 | let box_dyn = Box::::from(error); 32 | assert_eq!("oh no!", box_dyn.to_string()); 33 | drop(box_dyn); 34 | assert!(has_dropped.get()); 35 | } 36 | 37 | #[test] 38 | fn test_question_mark_stderr() -> Result<(), Box> { 39 | fn f() -> Result<()> { 40 | Ok(()) 41 | } 42 | f()?; 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /tests/drop/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fmt::{self, Display}, 4 | sync::{ 5 | Arc, 6 | atomic::{AtomicBool, Ordering::SeqCst}, 7 | }, 8 | }; 9 | 10 | use miette::Diagnostic; 11 | 12 | #[derive(Debug)] 13 | pub struct Flag { 14 | atomic: Arc, 15 | } 16 | 17 | impl Flag { 18 | pub fn new() -> Self { 19 | Flag { atomic: Arc::new(AtomicBool::new(false)) } 20 | } 21 | 22 | pub fn get(&self) -> bool { 23 | self.atomic.load(SeqCst) 24 | } 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct DetectDrop { 29 | has_dropped: Flag, 30 | } 31 | 32 | impl DetectDrop { 33 | pub fn new(has_dropped: &Flag) -> Self { 34 | DetectDrop { has_dropped: Flag { atomic: Arc::clone(&has_dropped.atomic) } } 35 | } 36 | } 37 | 38 | impl StdError for DetectDrop {} 39 | impl Diagnostic for DetectDrop {} 40 | 41 | impl Display for DetectDrop { 42 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 43 | write!(f, "oh no!") 44 | } 45 | } 46 | 47 | impl Drop for DetectDrop { 48 | fn drop(&mut self) { 49 | let already_dropped = self.has_dropped.atomic.swap(true, SeqCst); 50 | assert!(!already_dropped); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /miette-derive/src/diagnostic_arg.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Parse, ParseStream}; 2 | 3 | use crate::{code::Code, forward::Forward, help::Help, severity::Severity, url::Url}; 4 | 5 | pub enum DiagnosticArg { 6 | Transparent, 7 | Code(Code), 8 | Severity(Severity), 9 | Help(Help), 10 | Url(Url), 11 | Forward(Forward), 12 | } 13 | 14 | impl Parse for DiagnosticArg { 15 | fn parse(input: ParseStream) -> syn::Result { 16 | let ident = input.fork().parse::()?; 17 | if ident == "transparent" { 18 | // consume the token 19 | let _: syn::Ident = input.parse()?; 20 | Ok(DiagnosticArg::Transparent) 21 | } else if ident == "forward" { 22 | Ok(DiagnosticArg::Forward(input.parse()?)) 23 | } else if ident == "code" { 24 | Ok(DiagnosticArg::Code(input.parse()?)) 25 | } else if ident == "severity" { 26 | Ok(DiagnosticArg::Severity(input.parse()?)) 27 | } else if ident == "help" { 28 | Ok(DiagnosticArg::Help(input.parse()?)) 29 | } else if ident == "url" { 30 | Ok(DiagnosticArg::Url(input.parse()?)) 31 | } else { 32 | Err(syn::Error::new(ident.span(), "Unrecognized diagnostic option")) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/eyreish/into_diagnostic.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::{Diagnostic, Report}; 4 | 5 | /// Convenience [`Diagnostic`] that can be used as an "anonymous" wrapper for 6 | /// Errors. This is intended to be paired with [`IntoDiagnostic`]. 7 | #[derive(Debug, Error)] 8 | #[error(transparent)] 9 | struct DiagnosticError(Box); 10 | impl Diagnostic for DiagnosticError {} 11 | 12 | /** 13 | Convenience trait that adds a [`.into_diagnostic()`](IntoDiagnostic::into_diagnostic) method that converts a type implementing 14 | [`std::error::Error`] to a [`Result`]. 15 | 16 | ## Warning 17 | 18 | Calling this on a type implementing [`Diagnostic`] will reduce it to the common denominator of 19 | [`std::error::Error`]. Meaning all extra information provided by [`Diagnostic`] will be 20 | inaccessible. If you have a type implementing [`Diagnostic`] consider simply returning it or using 21 | [`Into`] or the [`Try`](std::ops::Try) operator (`?`). 22 | */ 23 | pub trait IntoDiagnostic { 24 | /// Converts [`Result`] types that return regular [`std::error::Error`]s 25 | /// into a [`Result`] that returns a [`Diagnostic`]. 26 | fn into_diagnostic(self) -> Result; 27 | } 28 | 29 | impl IntoDiagnostic for Result { 30 | fn into_diagnostic(self) -> Result { 31 | self.map_err(|e| DiagnosticError(Box::new(e)).into()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/macro_helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::{LabeledSpan, SourceSpan}; 2 | 3 | // Huge thanks to @jam1gamer for this hack: 4 | // https://twitter.com/jam1garner/status/1515887996444323840 5 | 6 | #[doc(hidden)] 7 | pub trait IsOption {} 8 | impl IsOption for Option {} 9 | 10 | #[doc(hidden)] 11 | #[derive(Debug, Default)] 12 | pub struct OptionalWrapper(pub core::marker::PhantomData); 13 | 14 | impl OptionalWrapper { 15 | pub fn new() -> Self { 16 | Self(core::marker::PhantomData) 17 | } 18 | } 19 | 20 | #[doc(hidden)] 21 | pub trait ToOption { 22 | #[doc(hidden)] 23 | fn to_option(self, value: T) -> Option; 24 | } 25 | 26 | impl OptionalWrapper 27 | where 28 | T: IsOption, 29 | { 30 | #[doc(hidden)] 31 | pub fn to_option(self, value: &T) -> &T { 32 | value 33 | } 34 | } 35 | 36 | impl ToOption for &OptionalWrapper { 37 | fn to_option(self, value: U) -> Option { 38 | Some(value) 39 | } 40 | } 41 | 42 | #[doc(hidden)] 43 | #[derive(Debug)] 44 | pub struct ToLabelSpanWrapper {} 45 | pub trait ToLabeledSpan { 46 | #[doc(hidden)] 47 | fn to_labeled_span(span: T) -> LabeledSpan; 48 | } 49 | impl ToLabeledSpan for ToLabelSpanWrapper { 50 | fn to_labeled_span(span: LabeledSpan) -> LabeledSpan { 51 | span 52 | } 53 | } 54 | impl ToLabeledSpan for ToLabelSpanWrapper 55 | where 56 | T: Into, 57 | { 58 | fn to_labeled_span(span: T) -> LabeledSpan { 59 | LabeledSpan::new_with_span(None, span.into()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/test_chain.rs: -------------------------------------------------------------------------------- 1 | use miette::{Report, miette}; 2 | 3 | fn error() -> Report { 4 | miette!("0").wrap_err(1).wrap_err(2).wrap_err(3) 5 | } 6 | 7 | #[test] 8 | fn test_iter() { 9 | let e = error(); 10 | let mut chain = e.chain(); 11 | assert_eq!("3", chain.next().unwrap().to_string()); 12 | assert_eq!("2", chain.next().unwrap().to_string()); 13 | assert_eq!("1", chain.next().unwrap().to_string()); 14 | assert_eq!("0", chain.next().unwrap().to_string()); 15 | assert!(chain.next().is_none()); 16 | assert!(chain.next_back().is_none()); 17 | } 18 | 19 | #[test] 20 | fn test_rev() { 21 | let e = error(); 22 | let mut chain = e.chain().rev(); 23 | assert_eq!("0", chain.next().unwrap().to_string()); 24 | assert_eq!("1", chain.next().unwrap().to_string()); 25 | assert_eq!("2", chain.next().unwrap().to_string()); 26 | assert_eq!("3", chain.next().unwrap().to_string()); 27 | assert!(chain.next().is_none()); 28 | assert!(chain.next_back().is_none()); 29 | } 30 | 31 | #[test] 32 | fn test_len() { 33 | let e = error(); 34 | let mut chain = e.chain(); 35 | assert_eq!(4, chain.len()); 36 | assert_eq!("3", chain.next().unwrap().to_string()); 37 | assert_eq!(3, chain.len()); 38 | assert_eq!("0", chain.next_back().unwrap().to_string()); 39 | assert_eq!(2, chain.len()); 40 | assert_eq!("2", chain.next().unwrap().to_string()); 41 | assert_eq!(1, chain.len()); 42 | assert_eq!("1", chain.next_back().unwrap().to_string()); 43 | assert_eq!(0, chain.len()); 44 | assert!(chain.next().is_none()); 45 | } 46 | -------------------------------------------------------------------------------- /tests/test_source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error as StdError, 3 | fmt::{self, Display}, 4 | io, 5 | }; 6 | 7 | use miette::{Report, miette}; 8 | 9 | #[derive(Debug)] 10 | enum TestError { 11 | Io(io::Error), 12 | } 13 | 14 | impl Display for TestError { 15 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 16 | match self { 17 | TestError::Io(e) => Display::fmt(e, formatter), 18 | } 19 | } 20 | } 21 | 22 | impl StdError for TestError { 23 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 24 | match self { 25 | TestError::Io(io) => Some(io), 26 | } 27 | } 28 | } 29 | 30 | #[test] 31 | fn test_literal_source() { 32 | let error: Report = miette!("oh no!"); 33 | assert!(error.source().is_none()); 34 | } 35 | 36 | #[test] 37 | fn test_variable_source() { 38 | let msg = "oh no!"; 39 | let error = miette!(msg); 40 | assert!(error.source().is_none()); 41 | 42 | let msg = msg.to_owned(); 43 | let error: Report = miette!(msg); 44 | assert!(error.source().is_none()); 45 | } 46 | 47 | #[test] 48 | fn test_fmt_source() { 49 | let error: Report = miette!("{} {}!", "oh", "no"); 50 | assert!(error.source().is_none()); 51 | } 52 | 53 | #[test] 54 | #[ignore = "Again with the io::Error source issue?"] 55 | fn test_io_source() { 56 | let io = io::Error::other("oh no!"); 57 | let error: Report = miette!(TestError::Io(io)); 58 | assert_eq!("oh no!", error.source().unwrap().to_string()); 59 | } 60 | 61 | #[test] 62 | fn test_miette_from_miette() { 63 | let error: Report = miette!("oh no!").wrap_err("context"); 64 | let error = miette!(error); 65 | assert_eq!("oh no!", error.source().unwrap().to_string()); 66 | } 67 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io}; 2 | 3 | use thiserror::Error; 4 | 5 | use crate::Diagnostic; 6 | 7 | /** 8 | Error enum for miette. Used by certain operations in the protocol. 9 | */ 10 | #[derive(Debug, Error)] 11 | pub enum MietteError { 12 | /// Wrapper around [`std::io::Error`]. This is returned when something went 13 | /// wrong while reading a [`SourceCode`](crate::SourceCode). 14 | #[error(transparent)] 15 | IoError(#[from] io::Error), 16 | 17 | /// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the 18 | /// bounds of a given [`SourceCode`](crate::SourceCode). 19 | #[error("The given offset is outside the bounds of its Source")] 20 | OutOfBounds, 21 | } 22 | 23 | impl Diagnostic for MietteError { 24 | fn code<'a>(&'a self) -> Option> { 25 | match self { 26 | MietteError::IoError(_) => Some(Box::new("miette::io_error")), 27 | MietteError::OutOfBounds => Some(Box::new("miette::span_out_of_bounds")), 28 | } 29 | } 30 | 31 | fn help<'a>(&'a self) -> Option> { 32 | match self { 33 | MietteError::IoError(_) => None, 34 | MietteError::OutOfBounds => { 35 | Some(Box::new("Double-check your spans. Do you have an off-by-one error?")) 36 | } 37 | } 38 | } 39 | 40 | fn url<'a>(&'a self) -> Option> { 41 | let crate_version = env!("CARGO_PKG_VERSION"); 42 | let variant = match self { 43 | MietteError::IoError(_) => "#variant.IoError", 44 | MietteError::OutOfBounds => "#variant.OutOfBounds", 45 | }; 46 | Some(Box::new(format!( 47 | "https://docs.rs/miette/{crate_version}/miette/enum.MietteError.html{variant}", 48 | ))) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/test_derive_source_chain.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use miette::{Diagnostic, miette}; 4 | use thiserror::Error; 5 | 6 | #[test] 7 | fn test_source() { 8 | #[derive(Debug, Diagnostic, Error)] 9 | #[error("Bar")] 10 | struct Bar; 11 | 12 | #[derive(Debug, Diagnostic, Error)] 13 | #[error("Foo")] 14 | struct Foo { 15 | #[source] 16 | bar: Bar, 17 | } 18 | 19 | let e = miette!(Foo { bar: Bar }); 20 | let mut chain = e.chain(); 21 | 22 | assert_eq!("Foo", chain.next().unwrap().to_string()); 23 | assert_eq!("Bar", chain.next().unwrap().to_string()); 24 | assert!(chain.next().is_none()); 25 | } 26 | 27 | #[test] 28 | fn test_source_boxed() { 29 | #[derive(Debug, Diagnostic, Error)] 30 | #[error("Bar")] 31 | struct Bar; 32 | 33 | #[derive(Debug, Diagnostic, Error)] 34 | #[error("Foo")] 35 | struct Foo { 36 | #[source] 37 | bar: Box, 38 | } 39 | 40 | let error = miette!(Foo { bar: Box::new(Bar) }); 41 | 42 | let mut chain = error.chain(); 43 | 44 | assert_eq!("Foo", chain.next().unwrap().to_string()); 45 | assert_eq!("Bar", chain.next().unwrap().to_string()); 46 | assert!(chain.next().is_none()); 47 | } 48 | 49 | #[test] 50 | fn test_source_arc() { 51 | #[derive(Debug, Diagnostic, Error)] 52 | #[error("Bar")] 53 | struct Bar; 54 | 55 | #[derive(Debug, Diagnostic, Error)] 56 | #[error("Foo")] 57 | struct Foo { 58 | #[source] 59 | bar: Arc, 60 | } 61 | 62 | let error = miette!(Foo { bar: Arc::new(Bar) }); 63 | 64 | let mut chain = error.chain(); 65 | 66 | assert_eq!("Foo", chain.next().unwrap().to_string()); 67 | assert_eq!("Bar", chain.next().unwrap().to_string()); 68 | assert!(chain.next().is_none()); 69 | } 70 | -------------------------------------------------------------------------------- /tests/test_fmt.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use miette::{Result, WrapErr, bail}; 4 | 5 | fn f() -> Result<()> { 6 | bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!")); 7 | } 8 | 9 | fn g() -> Result<()> { 10 | f().wrap_err("f failed") 11 | } 12 | 13 | fn h() -> Result<()> { 14 | g().wrap_err("g failed") 15 | } 16 | 17 | const EXPECTED_ALTDISPLAY_F: &str = "oh no!"; 18 | 19 | const EXPECTED_ALTDISPLAY_G: &str = "f failed: oh no!"; 20 | 21 | const EXPECTED_ALTDISPLAY_H: &str = "g failed: f failed: oh no!"; 22 | 23 | const EXPECTED_DEBUG_F: &str = "oh no!"; 24 | 25 | const EXPECTED_DEBUG_G: &str = "\ 26 | f failed 27 | 28 | Caused by: 29 | oh no!\ 30 | "; 31 | 32 | const EXPECTED_DEBUG_H: &str = "\ 33 | g failed 34 | 35 | Caused by: 36 | 0: f failed 37 | 1: oh no!\ 38 | "; 39 | 40 | const EXPECTED_ALTDEBUG_F: &str = "\ 41 | Custom { 42 | kind: PermissionDenied, 43 | error: \"oh no!\", 44 | }\ 45 | "; 46 | 47 | const EXPECTED_ALTDEBUG_G: &str = "\ 48 | Error { 49 | msg: \"f failed\", 50 | source: Custom { 51 | kind: PermissionDenied, 52 | error: \"oh no!\", 53 | }, 54 | }\ 55 | "; 56 | 57 | const EXPECTED_ALTDEBUG_H: &str = "\ 58 | Error { 59 | msg: \"g failed\", 60 | source: Error { 61 | msg: \"f failed\", 62 | source: Custom { 63 | kind: PermissionDenied, 64 | error: \"oh no!\", 65 | }, 66 | }, 67 | }\ 68 | "; 69 | 70 | #[test] 71 | fn test_display() { 72 | assert_eq!("g failed", h().unwrap_err().to_string()); 73 | } 74 | 75 | #[test] 76 | fn test_altdisplay() { 77 | assert_eq!(EXPECTED_ALTDISPLAY_F, format!("{:#}", f().unwrap_err())); 78 | assert_eq!(EXPECTED_ALTDISPLAY_G, format!("{:#}", g().unwrap_err())); 79 | assert_eq!(EXPECTED_ALTDISPLAY_H, format!("{:#}", h().unwrap_err())); 80 | } 81 | 82 | #[test] 83 | #[ignore = "not really gonna work with the current printers"] 84 | fn test_debug() { 85 | assert_eq!(EXPECTED_DEBUG_F, format!("{:?}", f().unwrap_err())); 86 | assert_eq!(EXPECTED_DEBUG_G, format!("{:?}", g().unwrap_err())); 87 | assert_eq!(EXPECTED_DEBUG_H, format!("{:?}", h().unwrap_err())); 88 | } 89 | 90 | #[test] 91 | fn test_altdebug() { 92 | assert_eq!(EXPECTED_ALTDEBUG_F, format!("{:#?}", f().unwrap_err())); 93 | assert_eq!(EXPECTED_ALTDEBUG_G, format!("{:#?}", g().unwrap_err())); 94 | assert_eq!(EXPECTED_ALTDEBUG_H, format!("{:#?}", h().unwrap_err())); 95 | } 96 | -------------------------------------------------------------------------------- /tests/test_emoji_underline.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "fancy-no-backtrace")] 2 | 3 | use miette::{Diagnostic, GraphicalReportHandler, NamedSource, SourceSpan}; 4 | use thiserror::Error; 5 | 6 | #[test] 7 | fn test_emoji_sequence_underline() { 8 | #[derive(Error, Debug, Diagnostic)] 9 | #[error("emoji test")] 10 | struct TestError { 11 | #[source_code] 12 | src: NamedSource, 13 | #[label("here")] 14 | span: SourceSpan, 15 | } 16 | 17 | // Test with a ZWJ emoji sequence (family emoji) 18 | let family_emoji = "👨‍👩‍👧‍👦"; 19 | let src = format!("before {} after", family_emoji); 20 | let err = TestError { 21 | src: NamedSource::new("test.txt", src.clone()), 22 | span: (7, family_emoji.len()).into(), 23 | }; 24 | 25 | let mut output = String::new(); 26 | GraphicalReportHandler::new().render_report(&mut output, &err).unwrap(); 27 | 28 | println!("Output for family emoji:"); 29 | println!("{}", output); 30 | 31 | // Test with flag emoji (also uses ZWJ) 32 | let flag_emoji = "🏳️‍🌈"; 33 | let src2 = format!("before {} after", flag_emoji); 34 | let err2 = TestError { 35 | src: NamedSource::new("test2.txt", src2.clone()), 36 | span: (7, flag_emoji.len()).into(), 37 | }; 38 | 39 | let mut output2 = String::new(); 40 | GraphicalReportHandler::new().render_report(&mut output2, &err2).unwrap(); 41 | 42 | println!("\nOutput for rainbow flag:"); 43 | println!("{}", output2); 44 | 45 | // Test with skin tone modifier 46 | let skin_tone_emoji = "👋🏽"; 47 | let src3 = format!("before {} after", skin_tone_emoji); 48 | let err3 = TestError { 49 | src: NamedSource::new("test3.txt", src3.clone()), 50 | span: (7, skin_tone_emoji.len()).into(), 51 | }; 52 | 53 | let mut output3 = String::new(); 54 | GraphicalReportHandler::new().render_report(&mut output3, &err3).unwrap(); 55 | 56 | println!("\nOutput for waving hand with skin tone:"); 57 | println!("{}", output3); 58 | 59 | // Test ASCII fast path 60 | let ascii_text = "hello world"; 61 | let src4 = format!("before {} after", ascii_text); 62 | let err4 = TestError { 63 | src: NamedSource::new("test4.txt", src4.clone()), 64 | span: (7, ascii_text.len()).into(), 65 | }; 66 | 67 | let mut output4 = String::new(); 68 | GraphicalReportHandler::new().render_report(&mut output4, &err4).unwrap(); 69 | 70 | println!("\nOutput for ASCII text:"); 71 | println!("{}", output4); 72 | 73 | // Verify the underline matches the text length 74 | assert!(output4.contains("hello world")); 75 | } 76 | -------------------------------------------------------------------------------- /src/diagnostic_chain.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Iterate over error `.diagnostic_source()` chains. 3 | */ 4 | 5 | use crate::protocol::Diagnostic; 6 | 7 | /// Iterator of a chain of cause errors. 8 | #[derive(Clone, Default)] 9 | #[allow(missing_debug_implementations)] 10 | pub(crate) struct DiagnosticChain<'a> { 11 | state: Option>, 12 | } 13 | 14 | impl<'a> DiagnosticChain<'a> { 15 | pub(crate) fn from_diagnostic(head: &'a dyn Diagnostic) -> Self { 16 | DiagnosticChain { state: Some(ErrorKind::Diagnostic(head)) } 17 | } 18 | 19 | pub(crate) fn from_stderror(head: &'a (dyn std::error::Error + 'static)) -> Self { 20 | DiagnosticChain { state: Some(ErrorKind::StdError(head)) } 21 | } 22 | } 23 | 24 | impl<'a> Iterator for DiagnosticChain<'a> { 25 | type Item = ErrorKind<'a>; 26 | 27 | fn next(&mut self) -> Option { 28 | if let Some(err) = self.state.take() { 29 | self.state = err.get_nested(); 30 | Some(err) 31 | } else { 32 | None 33 | } 34 | } 35 | 36 | fn size_hint(&self) -> (usize, Option) { 37 | let len = self.len(); 38 | (len, Some(len)) 39 | } 40 | } 41 | 42 | impl ExactSizeIterator for DiagnosticChain<'_> { 43 | fn len(&self) -> usize { 44 | fn depth(d: Option<&ErrorKind<'_>>) -> usize { 45 | match d { 46 | Some(d) => 1 + depth(d.get_nested().as_ref()), 47 | None => 0, 48 | } 49 | } 50 | 51 | depth(self.state.as_ref()) 52 | } 53 | } 54 | 55 | #[derive(Clone)] 56 | pub(crate) enum ErrorKind<'a> { 57 | Diagnostic(&'a dyn Diagnostic), 58 | StdError(&'a (dyn std::error::Error + 'static)), 59 | } 60 | 61 | impl<'a> ErrorKind<'a> { 62 | fn get_nested(&self) -> Option> { 63 | match self { 64 | ErrorKind::Diagnostic(d) => d 65 | .diagnostic_source() 66 | .map(ErrorKind::Diagnostic) 67 | .or_else(|| d.source().map(ErrorKind::StdError)), 68 | ErrorKind::StdError(e) => e.source().map(ErrorKind::StdError), 69 | } 70 | } 71 | } 72 | 73 | impl std::fmt::Debug for ErrorKind<'_> { 74 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 75 | match self { 76 | ErrorKind::Diagnostic(d) => d.fmt(f), 77 | ErrorKind::StdError(e) => e.fmt(f), 78 | } 79 | } 80 | } 81 | 82 | impl std::fmt::Display for ErrorKind<'_> { 83 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 84 | match self { 85 | ErrorKind::Diagnostic(d) => d.fmt(f), 86 | ErrorKind::StdError(e) => e.fmt(f), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/handlers/debug.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{ReportHandler, protocol::Diagnostic}; 4 | 5 | /** 6 | [`ReportHandler`] that renders plain text and avoids extraneous graphics. 7 | It's optimized for screen readers and braille users, but is also used in any 8 | non-graphical environments, such as non-TTY output. 9 | */ 10 | #[derive(Debug, Clone)] 11 | pub struct DebugReportHandler; 12 | 13 | impl DebugReportHandler { 14 | /// Create a new [`NarratableReportHandler`](crate::NarratableReportHandler) 15 | /// There are no customization options. 16 | pub const fn new() -> Self { 17 | Self 18 | } 19 | } 20 | 21 | impl Default for DebugReportHandler { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | impl DebugReportHandler { 28 | /// Render a [`Diagnostic`]. This function is mostly internal and meant to 29 | /// be called by the toplevel [`ReportHandler`] handler, but is made public 30 | /// to make it easier (possible) to test in isolation from global state. 31 | pub fn render_report( 32 | &self, 33 | f: &mut fmt::Formatter<'_>, 34 | diagnostic: &dyn Diagnostic, 35 | ) -> fmt::Result { 36 | let mut diag = f.debug_struct("Diagnostic"); 37 | diag.field("message", &format!("{diagnostic}")); 38 | if let Some(code) = diagnostic.code() { 39 | diag.field("code", &code.to_string()); 40 | } 41 | if let Some(severity) = diagnostic.severity() { 42 | diag.field("severity", &format!("{severity:?}")); 43 | } 44 | if let Some(url) = diagnostic.url() { 45 | diag.field("url", &url.to_string()); 46 | } 47 | if let Some(help) = diagnostic.help() { 48 | diag.field("help", &help.to_string()); 49 | } 50 | if let Some(labels) = diagnostic.labels() { 51 | let labels: Vec<_> = labels.collect(); 52 | diag.field("labels", &format!("{labels:?}")); 53 | } 54 | if let Some(cause) = diagnostic.diagnostic_source() { 55 | diag.field("caused by", &format!("{cause:?}")); 56 | } 57 | diag.finish()?; 58 | writeln!(f)?; 59 | writeln!( 60 | f, 61 | "NOTE: If you're looking for the fancy error reports, install miette with the `fancy` feature, or write your own and hook it up with miette::set_hook()." 62 | ) 63 | } 64 | } 65 | 66 | impl ReportHandler for DebugReportHandler { 67 | fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | if f.alternate() { 69 | return fmt::Debug::fmt(diagnostic, f); 70 | } 71 | 72 | self.render_report(f, diagnostic) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/test_downcast.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | mod drop; 3 | 4 | use std::{ 5 | error::Error as StdError, 6 | fmt::{self, Display}, 7 | io, 8 | }; 9 | 10 | use miette::{Diagnostic, MietteDiagnostic, Report}; 11 | 12 | use self::{ 13 | common::*, 14 | drop::{DetectDrop, Flag}, 15 | }; 16 | 17 | #[test] 18 | fn test_downcast() { 19 | assert_eq!( 20 | "oh no!", 21 | bail_literal().unwrap_err().downcast::().unwrap().message, 22 | ); 23 | assert_eq!("oh no!", bail_fmt().unwrap_err().downcast::().unwrap().message,); 24 | assert_eq!("oh no!", bail_error().unwrap_err().downcast::().unwrap().to_string(),); 25 | } 26 | 27 | #[test] 28 | fn test_downcast_ref() { 29 | assert_eq!( 30 | "oh no!", 31 | bail_literal().unwrap_err().downcast_ref::().unwrap().message, 32 | ); 33 | assert_eq!( 34 | "oh no!", 35 | bail_fmt().unwrap_err().downcast_ref::().unwrap().message, 36 | ); 37 | assert_eq!( 38 | "oh no!", 39 | bail_error().unwrap_err().downcast_ref::().unwrap().to_string(), 40 | ); 41 | } 42 | 43 | #[test] 44 | fn test_downcast_mut() { 45 | assert_eq!( 46 | "oh no!", 47 | bail_literal().unwrap_err().downcast_mut::().unwrap().message, 48 | ); 49 | assert_eq!( 50 | "oh no!", 51 | bail_fmt().unwrap_err().downcast_mut::().unwrap().message, 52 | ); 53 | assert_eq!( 54 | "oh no!", 55 | bail_error().unwrap_err().downcast_mut::().unwrap().to_string(), 56 | ); 57 | } 58 | 59 | #[test] 60 | fn test_drop() { 61 | let has_dropped = Flag::new(); 62 | let error: Report = Report::new(DetectDrop::new(&has_dropped)); 63 | drop(error.downcast::().unwrap()); 64 | assert!(has_dropped.get()); 65 | } 66 | 67 | #[test] 68 | fn test_large_alignment() { 69 | #[repr(align(64))] 70 | #[derive(Debug)] 71 | struct LargeAlignedError(&'static str); 72 | 73 | impl Display for LargeAlignedError { 74 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 75 | f.write_str(self.0) 76 | } 77 | } 78 | 79 | impl StdError for LargeAlignedError {} 80 | impl Diagnostic for LargeAlignedError {} 81 | 82 | let error = Report::new(LargeAlignedError("oh no!")); 83 | assert_eq!("oh no!", error.downcast_ref::().unwrap().0); 84 | } 85 | 86 | #[test] 87 | fn test_unsuccessful_downcast() { 88 | let mut error = bail_error().unwrap_err(); 89 | assert!(error.downcast_ref::<&str>().is_none()); 90 | assert!(error.downcast_mut::<&str>().is_none()); 91 | assert!(error.downcast::<&str>().is_err()); 92 | } 93 | -------------------------------------------------------------------------------- /src/named_source.rs: -------------------------------------------------------------------------------- 1 | use crate::{MietteError, MietteSpanContents, SourceCode, SpanContents}; 2 | 3 | /// Utility struct for when you have a regular [`SourceCode`] type that doesn't 4 | /// implement `name`. For example [`String`]. Or if you want to override the 5 | /// `name` returned by the `SourceCode`. 6 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 7 | pub struct NamedSource { 8 | source: S, 9 | name: String, 10 | language: Option, 11 | } 12 | 13 | impl std::fmt::Debug for NamedSource { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | f.debug_struct("NamedSource") 16 | .field("name", &self.name) 17 | .field("source", &"") 18 | .field("language", &self.language); 19 | Ok(()) 20 | } 21 | } 22 | 23 | impl NamedSource { 24 | /// Create a new `NamedSource` using a regular [`SourceCode`] and giving 25 | /// its returned [`SpanContents`] a name. 26 | pub fn new(name: impl AsRef, source: S) -> Self 27 | where 28 | S: Send + Sync, 29 | { 30 | Self { source, name: name.as_ref().to_string(), language: None } 31 | } 32 | 33 | /// Gets the name of this `NamedSource`. 34 | pub fn name(&self) -> &str { 35 | &self.name 36 | } 37 | 38 | /// Returns a reference the inner [`SourceCode`] type for this 39 | /// `NamedSource`. 40 | pub fn inner(&self) -> &S { 41 | &self.source 42 | } 43 | 44 | /// Sets the [`language`](SpanContents::language) for this source code. 45 | #[must_use] 46 | pub fn with_language(mut self, language: impl Into) -> Self { 47 | self.language = Some(language.into()); 48 | self 49 | } 50 | } 51 | 52 | impl SourceCode for NamedSource { 53 | fn read_span<'a>( 54 | &'a self, 55 | span: &crate::SourceSpan, 56 | context_lines_before: usize, 57 | context_lines_after: usize, 58 | ) -> Result + 'a>, MietteError> { 59 | let inner_contents = 60 | self.inner().read_span(span, context_lines_before, context_lines_after)?; 61 | let mut contents = MietteSpanContents::new_named( 62 | self.name.clone(), 63 | inner_contents.data(), 64 | *inner_contents.span(), 65 | inner_contents.line(), 66 | inner_contents.column(), 67 | inner_contents.line_count(), 68 | ); 69 | if let Some(language) = &self.language { 70 | contents = contents.with_language(language); 71 | } 72 | Ok(Box::new(contents)) 73 | } 74 | 75 | fn name(&self) -> Option<&str> { 76 | Some(&self.name) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["miette-derive"] 4 | 5 | [workspace.package] 6 | authors = ["Boshen", "Kat Marchán "] 7 | categories = ["rust-patterns"] 8 | repository = "https://github.com/oxc-project/oxc-miette" 9 | license = "Apache-2.0" 10 | edition = "2024" 11 | rust-version = "1.85.0" 12 | 13 | [package] 14 | name = "oxc-miette" 15 | description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." 16 | documentation = "https://docs.rs/oxc-miette" 17 | readme = "README.md" 18 | version = "2.6.0" 19 | authors.workspace = true 20 | categories.workspace = true 21 | repository.workspace = true 22 | license.workspace = true 23 | edition.workspace = true 24 | rust-version.workspace = true 25 | exclude = ["images/", "tests/", "miette-derive/"] 26 | 27 | [lib] 28 | name = "miette" 29 | 30 | [lints.rust] 31 | absolute_paths_not_starting_with_crate = "warn" 32 | non_ascii_idents = "warn" 33 | unit-bindings = "warn" 34 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage)', 'cfg(coverage_nightly)'] } 35 | 36 | [dependencies] 37 | oxc-miette-derive = { path = "miette-derive", version = "=2.6.0", optional = true } 38 | 39 | # Relaxed version so the user can decide which version to use. 40 | thiserror = "2" 41 | serde = { version = "1", features = ["derive"], optional = true } 42 | owo-colors = { version = "4", optional = true } 43 | cfg-if = "1" 44 | 45 | unicode-width = "0.2.0" 46 | unicode-segmentation = "1.12.0" 47 | 48 | textwrap = { version = "0.16.2", optional = true } 49 | supports-hyperlinks = { version = "3.1.0", optional = true } 50 | supports-color = { version = "3.0.2", optional = true } 51 | supports-unicode = { version = "3.0.0", optional = true } 52 | backtrace = { version = "0.3.74", optional = true } 53 | terminal_size = { version = "0.4.2", optional = true } 54 | backtrace-ext = { version = "0.2.1", optional = true } 55 | 56 | [dev-dependencies] 57 | semver = "1.0.26" 58 | 59 | # Eyre devdeps 60 | futures = { version = "0.3", default-features = false } 61 | indenter = "0.3.3" 62 | rustversion = "1.0" 63 | trybuild = { version = "1.0.104", features = ["diff"] } 64 | regex = "1.11" 65 | lazy_static = "1.5" 66 | 67 | serde_json = "1.0.140" 68 | 69 | [features] 70 | default = ["derive"] 71 | derive = ["oxc-miette-derive"] 72 | no-format-args-capture = [] 73 | fancy-base = [ 74 | "owo-colors", 75 | "textwrap", 76 | ] 77 | fancy-no-syscall = [ 78 | "fancy-base", 79 | ] 80 | fancy-no-backtrace = [ 81 | "fancy-base", 82 | "terminal_size", 83 | "supports-hyperlinks", 84 | "supports-color", 85 | "supports-unicode", 86 | ] 87 | fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"] 88 | 89 | [package.metadata.docs.rs] 90 | all-features = true 91 | 92 | [package.metadata.cargo-shear] 93 | ignored = ["futures", "indenter", "semver"] 94 | -------------------------------------------------------------------------------- /miette-derive/src/diagnostic_source.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::{ 6 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 7 | forward::WhichFn, 8 | utils::{display_pat_members, gen_all_variants_with}, 9 | }; 10 | 11 | pub struct DiagnosticSource(syn::Member); 12 | 13 | impl DiagnosticSource { 14 | pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result> { 15 | match fields { 16 | syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()), 17 | syn::Fields::Unnamed(unnamed) => { 18 | Self::from_fields_vec(unnamed.unnamed.iter().collect()) 19 | } 20 | syn::Fields::Unit => Ok(None), 21 | } 22 | } 23 | 24 | fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result> { 25 | for (i, field) in fields.iter().enumerate() { 26 | for attr in &field.attrs { 27 | if attr.path().is_ident("diagnostic_source") { 28 | let diagnostic_source = if let Some(ident) = field.ident.clone() { 29 | syn::Member::Named(ident) 30 | } else { 31 | syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) 32 | }; 33 | return Ok(Some(DiagnosticSource(diagnostic_source))); 34 | } 35 | } 36 | } 37 | Ok(None) 38 | } 39 | 40 | pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 41 | gen_all_variants_with( 42 | variants, 43 | WhichFn::DiagnosticSource, 44 | |ident, fields, DiagnosticConcreteArgs { diagnostic_source, .. }| { 45 | let (display_pat, _display_members) = display_pat_members(fields); 46 | diagnostic_source.as_ref().map(|diagnostic_source| { 47 | let rel = match &diagnostic_source.0 { 48 | syn::Member::Named(ident) => ident.clone(), 49 | syn::Member::Unnamed(syn::Index { index, .. }) => { 50 | quote::format_ident!("_{}", index) 51 | } 52 | }; 53 | quote! { 54 | Self::#ident #display_pat => { 55 | std::option::Option::Some(std::borrow::Borrow::borrow(#rel)) 56 | } 57 | } 58 | }) 59 | }, 60 | ) 61 | } 62 | 63 | pub(crate) fn gen_struct(&self) -> Option { 64 | let rel = &self.0; 65 | Some(quote! { 66 | fn diagnostic_source<'a>(&'a self) -> std::option::Option<&'a dyn miette::Diagnostic> { 67 | std::option::Option::Some(std::borrow::Borrow::borrow(&self.#rel)) 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/test_location.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::print_stdout)] 2 | 3 | use std::panic::Location; 4 | 5 | use miette::{Diagnostic, IntoDiagnostic, WrapErr}; 6 | 7 | struct LocationHandler { 8 | actual: Option<&'static str>, 9 | expected: &'static str, 10 | } 11 | 12 | impl LocationHandler { 13 | fn new(expected: &'static str) -> Self { 14 | LocationHandler { actual: None, expected } 15 | } 16 | } 17 | 18 | impl miette::ReportHandler for LocationHandler { 19 | fn debug(&self, _error: &dyn Diagnostic, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | // we assume that if the compiler is new enough to support 21 | // `track_caller` that we will always have `actual` be `Some`, so we can 22 | // safely skip the assertion if the location is `None` which should only 23 | // happen in older rust versions. 24 | if let Some(actual) = self.actual { 25 | assert_eq!(self.expected, actual); 26 | } 27 | 28 | Ok(()) 29 | } 30 | 31 | fn track_caller(&mut self, location: &'static Location<'static>) { 32 | self.actual = Some(location.file()); 33 | } 34 | } 35 | 36 | #[test] 37 | fn test_wrap_err() { 38 | let _ = miette::set_hook(Box::new(|_e| { 39 | let expected_location = file!(); 40 | Box::new(LocationHandler::new(expected_location)) 41 | })); 42 | 43 | let err = std::fs::read_to_string("totally_fake_path") 44 | .into_diagnostic() 45 | .wrap_err("oopsie") 46 | .unwrap_err(); 47 | 48 | // should panic if the location isn't in our crate 49 | println!("{err:?}"); 50 | } 51 | 52 | #[test] 53 | fn test_wrap_err_with() { 54 | let _ = miette::set_hook(Box::new(|_e| { 55 | let expected_location = file!(); 56 | Box::new(LocationHandler::new(expected_location)) 57 | })); 58 | 59 | let err = std::fs::read_to_string("totally_fake_path") 60 | .into_diagnostic() 61 | .wrap_err_with(|| "oopsie") 62 | .unwrap_err(); 63 | 64 | // should panic if the location isn't in our crate 65 | println!("{err:?}"); 66 | } 67 | 68 | #[test] 69 | fn test_context() { 70 | let _ = miette::set_hook(Box::new(|_e| { 71 | let expected_location = file!(); 72 | Box::new(LocationHandler::new(expected_location)) 73 | })); 74 | 75 | let err = std::fs::read_to_string("totally_fake_path") 76 | .into_diagnostic() 77 | .context("oopsie") 78 | .unwrap_err(); 79 | 80 | // should panic if the location isn't in our crate 81 | println!("{err:?}"); 82 | } 83 | 84 | #[test] 85 | fn test_with_context() { 86 | let _ = miette::set_hook(Box::new(|_e| { 87 | let expected_location = file!(); 88 | Box::new(LocationHandler::new(expected_location)) 89 | })); 90 | 91 | let err = std::fs::read_to_string("totally_fake_path") 92 | .into_diagnostic() 93 | .with_context(|| "oopsie") 94 | .unwrap_err(); 95 | 96 | // should panic if the location isn't in our crate 97 | println!("{err:?}"); 98 | } 99 | -------------------------------------------------------------------------------- /miette-derive/src/code.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | Token, parenthesized, 5 | parse::{Parse, ParseStream}, 6 | }; 7 | 8 | use crate::{ 9 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 10 | forward::WhichFn, 11 | utils::gen_all_variants_with, 12 | }; 13 | 14 | #[derive(Debug)] 15 | pub struct Code(pub String); 16 | 17 | impl Parse for Code { 18 | fn parse(input: ParseStream) -> syn::Result { 19 | let ident = input.parse::()?; 20 | if ident == "code" { 21 | let la = input.lookahead1(); 22 | if la.peek(syn::token::Paren) { 23 | let content; 24 | parenthesized!(content in input); 25 | let la = content.lookahead1(); 26 | if la.peek(syn::LitStr) { 27 | let str = content.parse::()?; 28 | Ok(Code(str.value())) 29 | } else { 30 | let path = content.parse::()?; 31 | Ok(Code( 32 | path.segments 33 | .iter() 34 | .map(|s| s.ident.to_string()) 35 | .collect::>() 36 | .join("::"), 37 | )) 38 | } 39 | } else { 40 | input.parse::()?; 41 | Ok(Code(input.parse::()?.value())) 42 | } 43 | } else { 44 | Err(syn::Error::new( 45 | ident.span(), 46 | "diagnostic code is required. Use #[diagnostic(code = ...)] or #[diagnostic(code(...))] to define one.", 47 | )) 48 | } 49 | } 50 | } 51 | 52 | impl Code { 53 | pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 54 | gen_all_variants_with( 55 | variants, 56 | WhichFn::Code, 57 | |ident, fields, DiagnosticConcreteArgs { code, .. }| { 58 | let code = &code.as_ref()?.0; 59 | Some(match fields { 60 | syn::Fields::Named(_) => { 61 | quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), } 62 | } 63 | syn::Fields::Unnamed(_) => { 64 | quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), } 65 | } 66 | syn::Fields::Unit => { 67 | quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), } 68 | } 69 | }) 70 | }, 71 | ) 72 | } 73 | 74 | pub(crate) fn gen_struct(&self) -> Option { 75 | let code = &self.0; 76 | Some(quote! { 77 | fn code(&self) -> std::option::Option> { 78 | std::option::Option::Some(std::boxed::Box::new(#code)) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /miette-derive/src/related.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::{ 6 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 7 | forward::WhichFn, 8 | utils::{display_pat_members, gen_all_variants_with}, 9 | }; 10 | 11 | pub struct Related(syn::Member); 12 | 13 | impl Related { 14 | pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result> { 15 | match fields { 16 | syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()), 17 | syn::Fields::Unnamed(unnamed) => { 18 | Self::from_fields_vec(unnamed.unnamed.iter().collect()) 19 | } 20 | syn::Fields::Unit => Ok(None), 21 | } 22 | } 23 | 24 | fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result> { 25 | for (i, field) in fields.iter().enumerate() { 26 | for attr in &field.attrs { 27 | if attr.path().is_ident("related") { 28 | let related = if let Some(ident) = field.ident.clone() { 29 | syn::Member::Named(ident) 30 | } else { 31 | syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) 32 | }; 33 | return Ok(Some(Related(related))); 34 | } 35 | } 36 | } 37 | Ok(None) 38 | } 39 | 40 | pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 41 | gen_all_variants_with( 42 | variants, 43 | WhichFn::Related, 44 | |ident, fields, DiagnosticConcreteArgs { related, .. }| { 45 | let (display_pat, _display_members) = display_pat_members(fields); 46 | related.as_ref().map(|related| { 47 | let rel = match &related.0 { 48 | syn::Member::Named(ident) => ident.clone(), 49 | syn::Member::Unnamed(syn::Index { index, .. }) => { 50 | format_ident!("_{}", index) 51 | } 52 | }; 53 | quote! { 54 | Self::#ident #display_pat => { 55 | std::option::Option::Some(std::boxed::Box::new( 56 | #rel.iter().map(|x| -> &(dyn miette::Diagnostic) { &*x }) 57 | )) 58 | } 59 | } 60 | }) 61 | }, 62 | ) 63 | } 64 | 65 | pub(crate) fn gen_struct(&self) -> Option { 66 | let rel = &self.0; 67 | Some(quote! { 68 | fn related<'a>(&'a self) -> std::option::Option + 'a>> { 69 | use ::core::borrow::Borrow; 70 | std::option::Option::Some(std::boxed::Box::new( 71 | self.#rel.iter().map(|x| -> &(dyn miette::Diagnostic) { &*x.borrow() }) 72 | )) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/chain.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Iterate over error `.source()` chains. 3 | 4 | NOTE: This module is taken wholesale from . 5 | */ 6 | use std::{error::Error as StdError, vec}; 7 | 8 | use ChainState::*; 9 | 10 | /// Iterator of a chain of source errors. 11 | /// 12 | /// This type is the iterator returned by [`Report::chain`]. 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use miette::Report; 18 | /// use std::io; 19 | /// 20 | /// pub fn underlying_io_error_kind(error: &Report) -> Option { 21 | /// for cause in error.chain() { 22 | /// if let Some(io_error) = cause.downcast_ref::() { 23 | /// return Some(io_error.kind()); 24 | /// } 25 | /// } 26 | /// None 27 | /// } 28 | /// ``` 29 | #[derive(Clone)] 30 | #[allow(missing_debug_implementations)] 31 | pub struct Chain<'a> { 32 | state: crate::chain::ChainState<'a>, 33 | } 34 | 35 | #[derive(Clone)] 36 | pub(crate) enum ChainState<'a> { 37 | Linked { next: Option<&'a (dyn StdError + 'static)> }, 38 | Buffered { rest: vec::IntoIter<&'a (dyn StdError + 'static)> }, 39 | } 40 | 41 | impl<'a> Chain<'a> { 42 | pub(crate) fn new(head: &'a (dyn StdError + 'static)) -> Self { 43 | Chain { state: ChainState::Linked { next: Some(head) } } 44 | } 45 | } 46 | 47 | impl<'a> Iterator for Chain<'a> { 48 | type Item = &'a (dyn StdError + 'static); 49 | 50 | fn next(&mut self) -> Option { 51 | match &mut self.state { 52 | Linked { next } => { 53 | let error = (*next)?; 54 | *next = error.source(); 55 | Some(error) 56 | } 57 | Buffered { rest } => rest.next(), 58 | } 59 | } 60 | 61 | fn size_hint(&self) -> (usize, Option) { 62 | let len = self.len(); 63 | (len, Some(len)) 64 | } 65 | } 66 | 67 | impl DoubleEndedIterator for Chain<'_> { 68 | fn next_back(&mut self) -> Option { 69 | match &mut self.state { 70 | &mut Linked { mut next } => { 71 | let mut rest = Vec::new(); 72 | while let Some(cause) = next { 73 | next = cause.source(); 74 | rest.push(cause); 75 | } 76 | let mut rest = rest.into_iter(); 77 | let last = rest.next_back(); 78 | self.state = Buffered { rest }; 79 | last 80 | } 81 | Buffered { rest } => rest.next_back(), 82 | } 83 | } 84 | } 85 | 86 | impl ExactSizeIterator for Chain<'_> { 87 | fn len(&self) -> usize { 88 | match &self.state { 89 | &Linked { mut next } => { 90 | let mut len = 0; 91 | while let Some(cause) = next { 92 | next = cause.source(); 93 | len += 1; 94 | } 95 | len 96 | } 97 | Buffered { rest } => rest.len(), 98 | } 99 | } 100 | } 101 | 102 | impl Default for Chain<'_> { 103 | fn default() -> Self { 104 | Chain { state: ChainState::Buffered { rest: Vec::new().into_iter() } } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /miette-derive/src/severity.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{ 4 | Token, parenthesized, 5 | parse::{Parse, ParseStream}, 6 | }; 7 | 8 | use crate::{ 9 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 10 | forward::WhichFn, 11 | utils::gen_all_variants_with, 12 | }; 13 | 14 | pub struct Severity(pub syn::Ident); 15 | 16 | impl Parse for Severity { 17 | fn parse(input: ParseStream) -> syn::Result { 18 | let ident = input.parse::()?; 19 | if ident == "severity" { 20 | let la = input.lookahead1(); 21 | if la.peek(syn::token::Paren) { 22 | let content; 23 | parenthesized!(content in input); 24 | let la = content.lookahead1(); 25 | if la.peek(syn::LitStr) { 26 | let str = content.parse::()?; 27 | let sev = get_severity(&str.value(), str.span())?; 28 | Ok(Severity(syn::Ident::new(&sev, str.span()))) 29 | } else { 30 | let ident = content.parse::()?; 31 | let sev = get_severity(&ident.to_string(), ident.span())?; 32 | Ok(Severity(syn::Ident::new(&sev, ident.span()))) 33 | } 34 | } else { 35 | input.parse::()?; 36 | let str = input.parse::()?; 37 | let sev = get_severity(&str.value(), str.span())?; 38 | Ok(Severity(syn::Ident::new(&sev, str.span()))) 39 | } 40 | } else { 41 | Err(syn::Error::new(ident.span(), "MIETTE BUG: not a severity option")) 42 | } 43 | } 44 | } 45 | 46 | fn get_severity(input: &str, span: Span) -> syn::Result { 47 | match input.to_lowercase().as_ref() { 48 | "error" | "err" => Ok("Error".into()), 49 | "warning" | "warn" => Ok("Warning".into()), 50 | "advice" | "adv" | "info" => Ok("Advice".into()), 51 | _ => Err(syn::Error::new( 52 | span, 53 | "Invalid severity level. Only Error, Warning, and Advice are supported.", 54 | )), 55 | } 56 | } 57 | 58 | impl Severity { 59 | pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 60 | gen_all_variants_with( 61 | variants, 62 | WhichFn::Severity, 63 | |ident, fields, DiagnosticConcreteArgs { severity, .. }| { 64 | let severity = &severity.as_ref()?.0; 65 | let fields = match fields { 66 | syn::Fields::Named(_) => quote! { { .. } }, 67 | syn::Fields::Unnamed(_) => quote! { (..) }, 68 | syn::Fields::Unit => quote! {}, 69 | }; 70 | Some( 71 | quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), }, 72 | ) 73 | }, 74 | ) 75 | } 76 | 77 | pub(crate) fn gen_struct(&self) -> Option { 78 | let sev = &self.0; 79 | Some(quote! { 80 | fn severity(&self) -> std::option::Option { 81 | Some(miette::Severity::#sev) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/eyreish/kind.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_debug_implementations, missing_docs)] 2 | // Tagged dispatch mechanism for resolving the behavior of `miette!($expr)`. 3 | // 4 | // When miette! is given a single expr argument to turn into miette::Report, we 5 | // want the resulting Report to pick up the input's implementation of source() 6 | // and backtrace() if it has a std::error::Error impl, otherwise require nothing 7 | // more than Display and Debug. 8 | // 9 | // Expressed in terms of specialization, we want something like: 10 | // 11 | // trait EyreNew { 12 | // fn new(self) -> Report; 13 | // } 14 | // 15 | // impl EyreNew for T 16 | // where 17 | // T: Display + Debug + Send + Sync + 'static, 18 | // { 19 | // default fn new(self) -> Report { 20 | // /* no std error impl */ 21 | // } 22 | // } 23 | // 24 | // impl EyreNew for T 25 | // where 26 | // T: std::error::Error + Send + Sync + 'static, 27 | // { 28 | // fn new(self) -> Report { 29 | // /* use std error's source() and backtrace() */ 30 | // } 31 | // } 32 | // 33 | // Since specialization is not stable yet, instead we rely on autoref behavior 34 | // of method resolution to perform tagged dispatch. Here we have two traits 35 | // AdhocKind and TraitKind that both have an miette_kind() method. AdhocKind is 36 | // implemented whether or not the caller's type has a std error impl, while 37 | // TraitKind is implemented only when a std error impl does exist. The ambiguity 38 | // is resolved by AdhocKind requiring an extra autoref so that it has lower 39 | // precedence. 40 | // 41 | // The miette! macro will set up the call in this form: 42 | // 43 | // #[allow(unused_imports)] 44 | // use $crate::private::{AdhocKind, TraitKind}; 45 | // let error = $msg; 46 | // (&error).miette_kind().new(error) 47 | 48 | use core::fmt::{Debug, Display}; 49 | 50 | use super::Report; 51 | use crate::Diagnostic; 52 | 53 | pub struct Adhoc; 54 | 55 | pub trait AdhocKind: Sized { 56 | #[inline] 57 | fn miette_kind(&self) -> Adhoc { 58 | Adhoc 59 | } 60 | } 61 | 62 | impl AdhocKind for &T where T: ?Sized + Display + Debug + Send + Sync + 'static {} 63 | 64 | impl Adhoc { 65 | #[track_caller] 66 | pub fn new(self, message: M) -> Report 67 | where 68 | M: Display + Debug + Send + Sync + 'static, 69 | { 70 | Report::from_adhoc(message) 71 | } 72 | } 73 | 74 | pub struct Trait; 75 | 76 | pub trait TraitKind: Sized { 77 | #[inline] 78 | fn miette_kind(&self) -> Trait { 79 | Trait 80 | } 81 | } 82 | 83 | impl TraitKind for E where E: Into {} 84 | 85 | impl Trait { 86 | #[track_caller] 87 | pub fn new(self, error: E) -> Report 88 | where 89 | E: Into, 90 | { 91 | error.into() 92 | } 93 | } 94 | 95 | pub struct Boxed; 96 | 97 | pub trait BoxedKind: Sized { 98 | #[inline] 99 | fn miette_kind(&self) -> Boxed { 100 | Boxed 101 | } 102 | } 103 | 104 | impl BoxedKind for Box {} 105 | 106 | impl Boxed { 107 | #[track_caller] 108 | pub fn new(self, error: Box) -> Report { 109 | Report::from_boxed(error) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /miette-derive/src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::{ 6 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 7 | forward::WhichFn, 8 | }; 9 | 10 | pub(crate) fn gen_all_variants_with( 11 | variants: &[DiagnosticDef], 12 | which_fn: WhichFn, 13 | mut f: impl FnMut(&syn::Ident, &syn::Fields, &DiagnosticConcreteArgs) -> Option, 14 | ) -> Option { 15 | let pairs = variants 16 | .iter() 17 | .filter_map(|def| { 18 | def.args.forward_or_override_enum(&def.ident, which_fn, |concrete| { 19 | f(&def.ident, &def.fields, concrete) 20 | }) 21 | }) 22 | .collect::>(); 23 | if pairs.is_empty() { 24 | return None; 25 | } 26 | let signature = which_fn.signature(); 27 | let catchall = which_fn.catchall_arm(); 28 | Some(quote! { 29 | #signature { 30 | #[allow(unused_variables, deprecated)] 31 | match self { 32 | #(#pairs)* 33 | #catchall 34 | } 35 | } 36 | }) 37 | } 38 | 39 | use std::collections::HashSet; 40 | 41 | use crate::fmt::Display; 42 | 43 | pub(crate) fn gen_unused_pat(fields: &syn::Fields) -> TokenStream { 44 | match fields { 45 | syn::Fields::Named(_) => quote! { { .. } }, 46 | syn::Fields::Unnamed(_) => quote! { ( .. ) }, 47 | syn::Fields::Unit => quote! {}, 48 | } 49 | } 50 | 51 | /// Goes in the slot `let Self #pat = self;` or `match self { Self #pat => ... 52 | /// }`. 53 | fn gen_fields_pat(fields: &syn::Fields) -> TokenStream { 54 | let member_idents = fields 55 | .iter() 56 | .enumerate() 57 | .map(|(i, field)| field.ident.as_ref().cloned().unwrap_or_else(|| format_ident!("_{}", i))); 58 | match fields { 59 | syn::Fields::Named(_) => quote! { 60 | { #(#member_idents),* } 61 | }, 62 | syn::Fields::Unnamed(_) => quote! { 63 | ( #(#member_idents),* ) 64 | }, 65 | syn::Fields::Unit => quote! {}, 66 | } 67 | } 68 | 69 | /// The returned tokens go in the slot `let Self #pat = self;` or `match self { 70 | /// Self #pat => ... }`. The members can be passed to 71 | /// `Display::expand_shorthand[_cloned]`. 72 | pub(crate) fn display_pat_members(fields: &syn::Fields) -> (TokenStream, HashSet) { 73 | let pat = gen_fields_pat(fields); 74 | let members: HashSet = fields 75 | .iter() 76 | .enumerate() 77 | .map(|(i, field)| { 78 | if let Some(ident) = field.ident.as_ref().cloned() { 79 | syn::Member::Named(ident) 80 | } else { 81 | syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) 82 | } 83 | }) 84 | .collect(); 85 | (pat, members) 86 | } 87 | 88 | impl Display { 89 | /// Returns `(fmt, args)` which must be passed to some kind of format macro 90 | /// without tokens in between, i.e. `format!(#fmt #args)`. 91 | pub(crate) fn expand_shorthand_cloned( 92 | &self, 93 | members: &HashSet, 94 | ) -> (syn::LitStr, TokenStream) { 95 | let mut display = self.clone(); 96 | display.expand_shorthand(members); 97 | let Display { fmt, args, .. } = display; 98 | (fmt, args) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/test_diagnostic_source_macro.rs: -------------------------------------------------------------------------------- 1 | use miette::Diagnostic; 2 | 3 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 4 | #[error("A complex error happened")] 5 | struct SourceError { 6 | #[source_code] 7 | code: String, 8 | #[help] 9 | help: String, 10 | #[label("here")] 11 | label: (usize, usize), 12 | } 13 | 14 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 15 | #[error("AnErr")] 16 | struct AnErr; 17 | 18 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 19 | #[error("TestError")] 20 | struct TestStructError { 21 | #[diagnostic_source] 22 | asdf_inner_foo: SourceError, 23 | } 24 | 25 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 26 | #[error("TestError")] 27 | enum TestEnumError { 28 | Without, 29 | WithTuple(#[diagnostic_source] AnErr), 30 | WithStruct { 31 | #[diagnostic_source] 32 | inner: AnErr, 33 | }, 34 | } 35 | 36 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 37 | #[error("TestError")] 38 | struct TestTupleError(#[diagnostic_source] AnErr); 39 | 40 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 41 | #[error("TestError")] 42 | struct TestBoxedError(#[diagnostic_source] Box); 43 | 44 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 45 | #[error("TestError")] 46 | struct TestBoxedSendError(#[diagnostic_source] Box); 47 | 48 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 49 | #[error("TestError")] 50 | struct TestBoxedSendSyncError(#[diagnostic_source] Box); 51 | 52 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 53 | #[error("TestError")] 54 | struct TestArcedError(#[diagnostic_source] std::sync::Arc); 55 | 56 | #[test] 57 | fn test_diagnostic_source() { 58 | let error = TestStructError { 59 | asdf_inner_foo: SourceError { code: String::new(), help: String::new(), label: (0, 0) }, 60 | }; 61 | assert!(error.diagnostic_source().is_some()); 62 | 63 | let error = TestEnumError::Without; 64 | assert!(error.diagnostic_source().is_none()); 65 | 66 | let error = TestEnumError::WithTuple(AnErr); 67 | assert!(error.diagnostic_source().is_some()); 68 | 69 | let error = TestEnumError::WithStruct { inner: AnErr }; 70 | assert!(error.diagnostic_source().is_some()); 71 | 72 | let error = TestTupleError(AnErr); 73 | assert!(error.diagnostic_source().is_some()); 74 | 75 | let error = TestBoxedError(Box::new(AnErr)); 76 | assert!(error.diagnostic_source().is_some()); 77 | 78 | let error = TestBoxedSendError(Box::new(AnErr)); 79 | assert!(error.diagnostic_source().is_some()); 80 | 81 | let error = TestBoxedSendSyncError(Box::new(AnErr)); 82 | assert!(error.diagnostic_source().is_some()); 83 | 84 | let error = TestArcedError(std::sync::Arc::new(AnErr)); 85 | assert!(error.diagnostic_source().is_some()); 86 | } 87 | 88 | #[allow(dead_code)] 89 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 90 | #[error("A nested error happened")] 91 | struct NestedError { 92 | #[source_code] 93 | code: String, 94 | #[label("here")] 95 | label: (usize, usize), 96 | #[diagnostic_source] 97 | the_other_err: Box, 98 | } 99 | 100 | #[allow(unused)] 101 | #[derive(Debug, miette::Diagnostic, thiserror::Error)] 102 | #[error("A multi-error happened")] 103 | struct MultiError { 104 | #[related] 105 | related_errs: Vec>, 106 | } 107 | -------------------------------------------------------------------------------- /src/panic.rs: -------------------------------------------------------------------------------- 1 | use backtrace::Backtrace; 2 | use thiserror::Error; 3 | 4 | use crate::{self as miette, Context, Diagnostic, Result}; 5 | 6 | /// Tells miette to render panics using its rendering engine. 7 | pub fn set_panic_hook() { 8 | std::panic::set_hook(Box::new(move |info| { 9 | let mut message = "Something went wrong".to_string(); 10 | let payload = info.payload(); 11 | if let Some(msg) = payload.downcast_ref::<&str>() { 12 | message = msg.to_string(); 13 | } 14 | if let Some(msg) = payload.downcast_ref::() { 15 | message = msg.clone(); 16 | } 17 | let mut report: Result<()> = Err(Panic(message).into()); 18 | if let Some(loc) = info.location() { 19 | report = report 20 | .with_context(|| format!("at {}:{}:{}", loc.file(), loc.line(), loc.column())); 21 | } 22 | if let Err(err) = report.with_context(|| "Main thread panicked.".to_string()) { 23 | eprintln!("Error: {err:?}"); 24 | } 25 | })); 26 | } 27 | 28 | #[derive(Debug, Error, Diagnostic)] 29 | #[error("{0}{trace}", trace = Panic::backtrace())] 30 | #[diagnostic(help("set the `RUST_BACKTRACE=1` environment variable to display a backtrace."))] 31 | struct Panic(String); 32 | 33 | impl Panic { 34 | fn backtrace() -> String { 35 | use std::fmt::Write; 36 | if let Ok(var) = std::env::var("RUST_BACKTRACE") { 37 | if !var.is_empty() && var != "0" { 38 | const HEX_WIDTH: usize = std::mem::size_of::() + 2; 39 | // Padding for next lines after frame's address 40 | const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6; 41 | let mut backtrace = String::new(); 42 | let trace = Backtrace::new(); 43 | let frames = backtrace_ext::short_frames_strict(&trace).enumerate(); 44 | for (idx, (frame, sub_frames)) in frames { 45 | let ip = frame.ip(); 46 | let _ = write!(backtrace, "\n{idx:4}: {ip:HEX_WIDTH$?}"); 47 | 48 | let symbols = frame.symbols(); 49 | if symbols.is_empty() { 50 | let _ = write!(backtrace, " - "); 51 | continue; 52 | } 53 | 54 | for (idx, symbol) in symbols[sub_frames].iter().enumerate() { 55 | // Print symbols from this address, 56 | // if there are several addresses 57 | // we need to put it on next line 58 | if idx != 0 { 59 | let _ = write!(backtrace, "\n{:1$}", "", NEXT_SYMBOL_PADDING); 60 | } 61 | 62 | if let Some(name) = symbol.name() { 63 | let _ = write!(backtrace, " - {name}"); 64 | } else { 65 | let _ = write!(backtrace, " - "); 66 | } 67 | 68 | // See if there is debug information with file name and line 69 | if let (Some(file), Some(line)) = (symbol.filename(), symbol.lineno()) { 70 | let _ = write!( 71 | backtrace, 72 | "\n{:3$}at {}:{}", 73 | "", 74 | file.display(), 75 | line, 76 | NEXT_SYMBOL_PADDING 77 | ); 78 | } 79 | } 80 | } 81 | return backtrace; 82 | } 83 | } 84 | "".into() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/eyreish/ptr.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, ptr::NonNull}; 2 | 3 | #[repr(transparent)] 4 | /// A raw pointer that owns its pointee 5 | pub(crate) struct Own 6 | where 7 | T: ?Sized, 8 | { 9 | pub(crate) ptr: NonNull, 10 | } 11 | 12 | unsafe impl Send for Own where T: ?Sized {} 13 | unsafe impl Sync for Own where T: ?Sized {} 14 | 15 | impl Copy for Own where T: ?Sized {} 16 | 17 | impl Clone for Own 18 | where 19 | T: ?Sized, 20 | { 21 | fn clone(&self) -> Self { 22 | *self 23 | } 24 | } 25 | 26 | impl Own 27 | where 28 | T: ?Sized, 29 | { 30 | pub(crate) fn new(ptr: Box) -> Self { 31 | Own { ptr: unsafe { NonNull::new_unchecked(Box::into_raw(ptr)) } } 32 | } 33 | 34 | pub(crate) fn cast(self) -> Own { 35 | Own { ptr: self.ptr.cast() } 36 | } 37 | 38 | pub(crate) unsafe fn boxed(self) -> Box { 39 | unsafe { Box::from_raw(self.ptr.as_ptr()) } 40 | } 41 | 42 | pub(crate) const fn by_ref<'a>(&self) -> Ref<'a, T> { 43 | Ref { ptr: self.ptr, lifetime: PhantomData } 44 | } 45 | 46 | pub(crate) fn by_mut<'a>(self) -> Mut<'a, T> { 47 | Mut { ptr: self.ptr, lifetime: PhantomData } 48 | } 49 | } 50 | 51 | #[allow(explicit_outlives_requirements)] 52 | #[repr(transparent)] 53 | /// A raw pointer that represents a shared borrow of its pointee 54 | pub(crate) struct Ref<'a, T> 55 | where 56 | T: ?Sized, 57 | { 58 | pub(crate) ptr: NonNull, 59 | lifetime: PhantomData<&'a T>, 60 | } 61 | 62 | impl Copy for Ref<'_, T> where T: ?Sized {} 63 | 64 | impl Clone for Ref<'_, T> 65 | where 66 | T: ?Sized, 67 | { 68 | fn clone(&self) -> Self { 69 | *self 70 | } 71 | } 72 | 73 | impl<'a, T> Ref<'a, T> 74 | where 75 | T: ?Sized, 76 | { 77 | pub(crate) fn new(ptr: &'a T) -> Self { 78 | Ref { ptr: NonNull::from(ptr), lifetime: PhantomData } 79 | } 80 | 81 | pub(crate) const fn from_raw(ptr: NonNull) -> Self { 82 | Ref { ptr, lifetime: PhantomData } 83 | } 84 | 85 | pub(crate) fn cast(self) -> Ref<'a, U::Target> { 86 | Ref { ptr: self.ptr.cast(), lifetime: PhantomData } 87 | } 88 | 89 | pub(crate) fn by_mut(self) -> Mut<'a, T> { 90 | Mut { ptr: self.ptr, lifetime: PhantomData } 91 | } 92 | 93 | pub(crate) const fn as_ptr(self) -> *const T { 94 | self.ptr.as_ptr() as *const T 95 | } 96 | 97 | pub(crate) unsafe fn deref(self) -> &'a T { 98 | unsafe { &*self.ptr.as_ptr() } 99 | } 100 | } 101 | 102 | #[allow(explicit_outlives_requirements)] 103 | #[repr(transparent)] 104 | /// A raw pointer that represents a unique borrow of its pointee 105 | pub(crate) struct Mut<'a, T> 106 | where 107 | T: ?Sized, 108 | { 109 | pub(crate) ptr: NonNull, 110 | lifetime: PhantomData<&'a mut T>, 111 | } 112 | 113 | impl Copy for Mut<'_, T> where T: ?Sized {} 114 | 115 | impl Clone for Mut<'_, T> 116 | where 117 | T: ?Sized, 118 | { 119 | fn clone(&self) -> Self { 120 | *self 121 | } 122 | } 123 | 124 | impl<'a, T> Mut<'a, T> 125 | where 126 | T: ?Sized, 127 | { 128 | pub(crate) fn cast(self) -> Mut<'a, U::Target> { 129 | Mut { ptr: self.ptr.cast(), lifetime: PhantomData } 130 | } 131 | 132 | pub(crate) const fn by_ref(self) -> Ref<'a, T> { 133 | Ref { ptr: self.ptr, lifetime: PhantomData } 134 | } 135 | 136 | pub(crate) fn extend<'b>(self) -> Mut<'b, T> { 137 | Mut { ptr: self.ptr, lifetime: PhantomData } 138 | } 139 | 140 | pub(crate) unsafe fn deref_mut(self) -> &'a mut T { 141 | unsafe { &mut *self.ptr.as_ptr() } 142 | } 143 | } 144 | 145 | impl Mut<'_, T> { 146 | pub(crate) unsafe fn read(self) -> T { 147 | unsafe { self.ptr.as_ptr().read() } 148 | } 149 | } 150 | 151 | pub(crate) trait CastTo { 152 | type Target; 153 | } 154 | 155 | impl CastTo for T { 156 | type Target = T; 157 | } 158 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | pull_request: 8 | types: [opened, synchronize] 9 | paths-ignore: 10 | - "**/*.md" 11 | push: 12 | branches: 13 | - main 14 | paths-ignore: 15 | - "**/*.md" 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 19 | cancel-in-progress: ${{ github.ref_name != 'main' }} 20 | 21 | jobs: 22 | fmt_and_docs: 23 | name: Check fmt & build docs 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 27 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 28 | with: 29 | components: rustfmt rust-docs 30 | - run: cargo fmt --all -- --check 31 | - run: RUSTDOCFLAGS='-D warnings' cargo doc --no-deps 32 | 33 | build_and_test: 34 | name: Build & Test 35 | runs-on: ${{ matrix.os }} 36 | strategy: 37 | matrix: 38 | features: [fancy] 39 | rust: [1.70.0, stable] 40 | os: [ubuntu-latest, macOS-latest, windows-latest] 41 | 42 | steps: 43 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 44 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 45 | with: 46 | save-cache: ${{ github.ref_name == 'main' }} 47 | components: clippy 48 | - name: Clippy 49 | run: cargo clippy --all -- -D warnings 50 | - name: Run tests 51 | if: matrix.rust == 'stable' 52 | run: cargo test --all --verbose --features ${{matrix.features}} 53 | - name: Run tests 54 | if: matrix.rust == '1.70.0' 55 | run: cargo test --all --verbose --features ${{matrix.features}} no-format-args-capture 56 | 57 | wasm: 58 | name: Check Wasm build 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 62 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 63 | with: 64 | save-cache: ${{ github.ref_name == 'main' }} 65 | cache-key: wasm 66 | - name: Check wasm target 67 | run: | 68 | rustup target add wasm32-unknown-unknown 69 | cargo check --target wasm32-unknown-unknown --features fancy-no-syscall 70 | 71 | miri: 72 | name: Miri 73 | runs-on: ubuntu-latest 74 | 75 | steps: 76 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 77 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 78 | with: 79 | save-cache: ${{ github.ref_name == 'main' }} 80 | cache-key: miri 81 | - name: Install Miri 82 | run: | 83 | rustup toolchain install nightly --component miri 84 | rustup override set nightly 85 | cargo miri setup 86 | - name: Run tests with miri 87 | env: 88 | MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance 89 | run: cargo miri test --all --verbose --features fancy 90 | 91 | minimal_versions: 92 | name: Minimal versions check 93 | runs-on: ${{ matrix.os }} 94 | strategy: 95 | matrix: 96 | os: [ubuntu-latest, macOS-latest, windows-latest] 97 | 98 | steps: 99 | - uses: taiki-e/checkout-action@b13d20b7cda4e2f325ef19895128f7ff735c0b3d # v1.3.1 100 | - uses: oxc-project/setup-rust@ecabb7322a2ba5aeedb3612d2a40b86a85cee235 # v1.0.11 101 | with: 102 | save-cache: ${{ github.ref_name == 'main' }} 103 | cache-key: wasm 104 | - name: Install Nightly 105 | run: | 106 | rustup toolchain install nightly 107 | rustup override set nightly 108 | - name: Run minimal version build 109 | run: cargo build -Z direct-minimal-versions --features fancy,no-format-args-capture 110 | -------------------------------------------------------------------------------- /miette-derive/src/source_code.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::spanned::Spanned; 4 | 5 | use crate::{ 6 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 7 | forward::WhichFn, 8 | utils::{display_pat_members, gen_all_variants_with}, 9 | }; 10 | 11 | pub struct SourceCode { 12 | source_code: syn::Member, 13 | is_option: bool, 14 | } 15 | 16 | impl SourceCode { 17 | pub fn from_fields(fields: &syn::Fields) -> syn::Result> { 18 | match fields { 19 | syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()), 20 | syn::Fields::Unnamed(unnamed) => { 21 | Self::from_fields_vec(unnamed.unnamed.iter().collect()) 22 | } 23 | syn::Fields::Unit => Ok(None), 24 | } 25 | } 26 | 27 | fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result> { 28 | for (i, field) in fields.iter().enumerate() { 29 | for attr in &field.attrs { 30 | if attr.path().is_ident("source_code") { 31 | let is_option = if let syn::Type::Path(syn::TypePath { 32 | path: syn::Path { segments, .. }, 33 | .. 34 | }) = &field.ty 35 | { 36 | segments.last().map(|seg| seg.ident == "Option").unwrap_or(false) 37 | } else { 38 | false 39 | }; 40 | 41 | let source_code = if let Some(ident) = field.ident.clone() { 42 | syn::Member::Named(ident) 43 | } else { 44 | syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) 45 | }; 46 | return Ok(Some(SourceCode { source_code, is_option })); 47 | } 48 | } 49 | } 50 | Ok(None) 51 | } 52 | 53 | pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option { 54 | let (display_pat, _display_members) = display_pat_members(fields); 55 | let src = &self.source_code; 56 | let ret = if self.is_option { 57 | quote! { 58 | self.#src.as_ref().map(|s| s as _) 59 | } 60 | } else { 61 | quote! { 62 | Some(&self.#src) 63 | } 64 | }; 65 | 66 | Some(quote! { 67 | #[allow(unused_variables)] 68 | fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> { 69 | let Self #display_pat = self; 70 | #ret 71 | } 72 | }) 73 | } 74 | 75 | pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 76 | gen_all_variants_with( 77 | variants, 78 | WhichFn::SourceCode, 79 | |ident, fields, DiagnosticConcreteArgs { source_code, .. }| { 80 | let (display_pat, _display_members) = display_pat_members(fields); 81 | source_code.as_ref().and_then(|source_code| { 82 | let field = match &source_code.source_code { 83 | syn::Member::Named(ident) => ident.clone(), 84 | syn::Member::Unnamed(syn::Index { index, .. }) => { 85 | format_ident!("_{}", index) 86 | } 87 | }; 88 | let variant_name = ident.clone(); 89 | let ret = if source_code.is_option { 90 | quote! { 91 | #field.as_ref().map(|s| s as _) 92 | } 93 | } else { 94 | quote! { 95 | std::option::Option::Some(#field) 96 | } 97 | }; 98 | match &fields { 99 | syn::Fields::Unit => None, 100 | _ => Some(quote! { 101 | Self::#variant_name #display_pat => #ret, 102 | }), 103 | } 104 | }) 105 | }, 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/test_context.rs: -------------------------------------------------------------------------------- 1 | mod drop; 2 | 3 | use std::fmt::{self, Display}; 4 | 5 | use miette::{Diagnostic, IntoDiagnostic, Report, Result, WrapErr}; 6 | use thiserror::Error; 7 | 8 | use crate::drop::{DetectDrop, Flag}; 9 | 10 | // https://github.com/dtolnay/miette/issues/18 11 | #[test] 12 | fn test_inference() -> Result<()> { 13 | let x = "1"; 14 | let y: u32 = x.parse().into_diagnostic().context("...")?; 15 | assert_eq!(y, 1); 16 | Ok(()) 17 | } 18 | 19 | macro_rules! context_type { 20 | ($name:ident) => { 21 | #[derive(Debug)] 22 | struct $name { 23 | message: &'static str, 24 | #[allow(dead_code)] 25 | drop: DetectDrop, 26 | } 27 | 28 | impl Display for $name { 29 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 30 | f.write_str(self.message) 31 | } 32 | } 33 | }; 34 | } 35 | 36 | context_type!(HighLevel); 37 | context_type!(MidLevel); 38 | 39 | #[derive(Diagnostic, Error, Debug)] 40 | #[error("{message}")] 41 | #[diagnostic()] // TODO 42 | struct LowLevel { 43 | message: &'static str, 44 | drop: DetectDrop, 45 | } 46 | 47 | struct Dropped { 48 | low: Flag, 49 | mid: Flag, 50 | high: Flag, 51 | } 52 | 53 | impl Dropped { 54 | fn none(&self) -> bool { 55 | !self.low.get() && !self.mid.get() && !self.high.get() 56 | } 57 | 58 | fn all(&self) -> bool { 59 | self.low.get() && self.mid.get() && self.high.get() 60 | } 61 | } 62 | 63 | fn make_chain() -> (Report, Dropped) { 64 | let dropped = Dropped { low: Flag::new(), mid: Flag::new(), high: Flag::new() }; 65 | 66 | let low = 67 | LowLevel { message: "no such file or directory", drop: DetectDrop::new(&dropped.low) }; 68 | 69 | // impl Report for Result 70 | let mid = Err::<(), LowLevel>(low) 71 | .wrap_err(MidLevel { 72 | message: "failed to load config", 73 | drop: DetectDrop::new(&dropped.mid), 74 | }) 75 | .unwrap_err(); 76 | 77 | // impl Report for Result 78 | let high = Err::<(), Report>(mid) 79 | .wrap_err(HighLevel { 80 | message: "failed to start server", 81 | drop: DetectDrop::new(&dropped.high), 82 | }) 83 | .unwrap_err(); 84 | 85 | (high, dropped) 86 | } 87 | 88 | #[test] 89 | fn test_downcast_ref() { 90 | let (err, dropped) = make_chain(); 91 | 92 | assert!(!err.is::()); 93 | assert!(err.downcast_ref::().is_none()); 94 | 95 | assert!(err.is::()); 96 | let high = err.downcast_ref::().unwrap(); 97 | assert_eq!(high.to_string(), "failed to start server"); 98 | 99 | assert!(err.is::()); 100 | let mid = err.downcast_ref::().unwrap(); 101 | assert_eq!(mid.to_string(), "failed to load config"); 102 | 103 | assert!(err.is::()); 104 | let low = err.downcast_ref::().unwrap(); 105 | assert_eq!(low.to_string(), "no such file or directory"); 106 | 107 | assert!(dropped.none()); 108 | drop(err); 109 | assert!(dropped.all()); 110 | } 111 | 112 | #[test] 113 | fn test_downcast_high() { 114 | let (err, dropped) = make_chain(); 115 | 116 | let err = err.downcast::().unwrap(); 117 | assert!(!dropped.high.get()); 118 | assert!(dropped.low.get() && dropped.mid.get()); 119 | 120 | drop(err); 121 | assert!(dropped.all()); 122 | } 123 | 124 | #[test] 125 | fn test_downcast_mid() { 126 | let (err, dropped) = make_chain(); 127 | 128 | let err = err.downcast::().unwrap(); 129 | assert!(!dropped.mid.get()); 130 | assert!(dropped.low.get() && dropped.high.get()); 131 | 132 | drop(err); 133 | assert!(dropped.all()); 134 | } 135 | 136 | #[test] 137 | fn test_downcast_low() { 138 | let (err, dropped) = make_chain(); 139 | 140 | let err = err.downcast::().unwrap(); 141 | assert!(!dropped.low.get()); 142 | assert!(dropped.mid.get() && dropped.high.get()); 143 | 144 | drop(err); 145 | assert!(dropped.all()); 146 | } 147 | 148 | #[test] 149 | fn test_unsuccessful_downcast() { 150 | let (err, dropped) = make_chain(); 151 | 152 | let err = err.downcast::().unwrap_err(); 153 | assert!(dropped.none()); 154 | 155 | drop(err); 156 | assert!(dropped.all()); 157 | } 158 | -------------------------------------------------------------------------------- /tests/test_derive_attr.rs: -------------------------------------------------------------------------------- 1 | // Testing of the `diagnostic` attr used by derive(Diagnostic) 2 | use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan}; 3 | use thiserror::Error; 4 | 5 | #[test] 6 | fn enum_uses_base_attr() { 7 | #[derive(Debug, Diagnostic, Error)] 8 | #[error("oops!")] 9 | #[diagnostic(code(error::on::base))] 10 | enum MyBad { 11 | Only { 12 | #[source_code] 13 | src: NamedSource, 14 | #[label("this bit here")] 15 | highlight: SourceSpan, 16 | }, 17 | } 18 | 19 | let src = "source\n text\n here".to_string(); 20 | let err = MyBad::Only { src: NamedSource::new("bad_file.rs", src), highlight: (9, 4).into() }; 21 | assert_eq!(err.code().unwrap().to_string(), "error::on::base"); 22 | } 23 | 24 | #[test] 25 | fn enum_uses_variant_attr() { 26 | #[derive(Debug, Diagnostic, Error)] 27 | #[error("oops!")] 28 | enum MyBad { 29 | #[diagnostic(code(error::on::variant))] 30 | Only { 31 | #[source_code] 32 | src: NamedSource, 33 | #[label("this bit here")] 34 | highlight: SourceSpan, 35 | }, 36 | } 37 | 38 | let src = "source\n text\n here".to_string(); 39 | let err = MyBad::Only { src: NamedSource::new("bad_file.rs", src), highlight: (9, 4).into() }; 40 | assert_eq!(err.code().unwrap().to_string(), "error::on::variant"); 41 | } 42 | 43 | #[test] 44 | fn multiple_attrs_allowed_on_item() { 45 | #[derive(Debug, Diagnostic, Error)] 46 | #[error("oops!")] 47 | #[diagnostic(code(error::on::base))] 48 | #[diagnostic(help("try doing it correctly"))] 49 | enum MyBad { 50 | Only { 51 | #[source_code] 52 | src: NamedSource, 53 | #[label("this bit here")] 54 | highlight: SourceSpan, 55 | }, 56 | } 57 | 58 | let src = "source\n text\n here".to_string(); 59 | let err = MyBad::Only { src: NamedSource::new("bad_file.rs", src), highlight: (9, 4).into() }; 60 | assert_eq!(err.code().unwrap().to_string(), "error::on::base"); 61 | assert_eq!(err.help().unwrap().to_string(), "try doing it correctly"); 62 | } 63 | 64 | #[test] 65 | fn multiple_attrs_allowed_on_variant() { 66 | #[derive(Debug, Diagnostic, Error)] 67 | #[error("oops!")] 68 | enum MyBad { 69 | #[diagnostic(code(error::on::variant))] 70 | #[diagnostic(help("try doing it correctly"))] 71 | Only { 72 | #[source_code] 73 | src: NamedSource, 74 | #[label("this bit here")] 75 | highlight: SourceSpan, 76 | }, 77 | } 78 | 79 | let src = "source\n text\n here".to_string(); 80 | let err = MyBad::Only { src: NamedSource::new("bad_file.rs", src), highlight: (9, 4).into() }; 81 | assert_eq!(err.code().unwrap().to_string(), "error::on::variant"); 82 | assert_eq!(err.help().unwrap().to_string(), "try doing it correctly"); 83 | } 84 | 85 | #[test] 86 | fn attrs_can_be_split_between_item_and_variants() { 87 | #[derive(Debug, Diagnostic, Error)] 88 | #[error("oops!")] 89 | #[diagnostic(code(error::on::base))] 90 | enum MyBad { 91 | #[diagnostic(help("try doing it correctly"))] 92 | #[diagnostic(url("https://example.com/foo/bar"))] 93 | Only { 94 | #[source_code] 95 | src: NamedSource, 96 | #[label("this bit here")] 97 | highlight: SourceSpan, 98 | }, 99 | } 100 | 101 | let src = "source\n text\n here".to_string(); 102 | let err = MyBad::Only { src: NamedSource::new("bad_file.rs", src), highlight: (9, 4).into() }; 103 | assert_eq!(err.code().unwrap().to_string(), "error::on::base"); 104 | assert_eq!(err.help().unwrap().to_string(), "try doing it correctly"); 105 | assert_eq!(err.url().unwrap().to_string(), "https://example.com/foo/bar".to_string()); 106 | } 107 | 108 | #[test] 109 | fn attr_not_required() { 110 | #[derive(Debug, Diagnostic, Error)] 111 | #[error("oops!")] 112 | enum MyBad { 113 | Only { 114 | #[source_code] 115 | src: NamedSource, 116 | #[label("this bit here")] 117 | highlight: SourceSpan, 118 | }, 119 | } 120 | 121 | let src = "source\n text\n here".to_string(); 122 | let err = MyBad::Only { src: NamedSource::new("bad_file.rs", src), highlight: (9, 4).into() }; 123 | let err_span = err.labels().unwrap().next().unwrap(); 124 | let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize); 125 | assert_eq!(err_span, expectation); 126 | } 127 | -------------------------------------------------------------------------------- /miette-derive/src/url.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | Fields, Token, parenthesized, 5 | parse::{Parse, ParseStream}, 6 | }; 7 | 8 | use crate::{ 9 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 10 | fmt::{self, Display}, 11 | forward::WhichFn, 12 | utils::{display_pat_members, gen_all_variants_with, gen_unused_pat}, 13 | }; 14 | 15 | pub enum Url { 16 | Display(Display), 17 | DocsRs, 18 | } 19 | 20 | impl Parse for Url { 21 | fn parse(input: ParseStream) -> syn::Result { 22 | let ident = input.parse::()?; 23 | if ident == "url" { 24 | let la = input.lookahead1(); 25 | if la.peek(syn::token::Paren) { 26 | let content; 27 | parenthesized!(content in input); 28 | if content.peek(syn::LitStr) { 29 | let fmt = content.parse()?; 30 | let args = if content.is_empty() { 31 | TokenStream::new() 32 | } else { 33 | fmt::parse_token_expr(&content, false)? 34 | }; 35 | let display = Display { fmt, args, has_bonus_display: false }; 36 | Ok(Url::Display(display)) 37 | } else { 38 | let option = content.parse::()?; 39 | if option == "docsrs" { 40 | Ok(Url::DocsRs) 41 | } else { 42 | Err(syn::Error::new( 43 | option.span(), 44 | "Invalid argument to url() sub-attribute. It must be either a string or a plain `docsrs` identifier", 45 | )) 46 | } 47 | } 48 | } else { 49 | input.parse::()?; 50 | Ok(Url::Display(Display { 51 | fmt: input.parse()?, 52 | args: TokenStream::new(), 53 | has_bonus_display: false, 54 | })) 55 | } 56 | } else { 57 | Err(syn::Error::new(ident.span(), "not a url")) 58 | } 59 | } 60 | } 61 | 62 | impl Url { 63 | pub(crate) fn gen_enum( 64 | enum_name: &syn::Ident, 65 | variants: &[DiagnosticDef], 66 | ) -> Option { 67 | gen_all_variants_with( 68 | variants, 69 | WhichFn::Url, 70 | |ident, fields, DiagnosticConcreteArgs { url, .. }| { 71 | let (pat, fmt, args) = match url.as_ref()? { 72 | // fall through to `_ => None` below 73 | Url::Display(display) => { 74 | let (display_pat, display_members) = display_pat_members(fields); 75 | let (fmt, args) = display.expand_shorthand_cloned(&display_members); 76 | (display_pat, fmt.value(), args) 77 | } 78 | Url::DocsRs => { 79 | let pat = gen_unused_pat(fields); 80 | let fmt = 81 | "https://docs.rs/{crate_name}/{crate_version}/{mod_name}/{item_path}" 82 | .into(); 83 | let item_path = format!("enum.{enum_name}.html#variant.{ident}"); 84 | let args = quote! { 85 | , 86 | crate_name=env!("CARGO_PKG_NAME"), 87 | crate_version=env!("CARGO_PKG_VERSION"), 88 | mod_name=env!("CARGO_PKG_NAME").replace('-', "_"), 89 | item_path=#item_path 90 | }; 91 | (pat, fmt, args) 92 | } 93 | }; 94 | Some(quote! { 95 | Self::#ident #pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))), 96 | }) 97 | }, 98 | ) 99 | } 100 | 101 | pub(crate) fn gen_struct( 102 | &self, 103 | struct_name: &syn::Ident, 104 | fields: &Fields, 105 | ) -> Option { 106 | let (pat, fmt, args) = match self { 107 | Url::Display(display) => { 108 | let (display_pat, display_members) = display_pat_members(fields); 109 | let (fmt, args) = display.expand_shorthand_cloned(&display_members); 110 | (display_pat, fmt.value(), args) 111 | } 112 | Url::DocsRs => { 113 | let pat = gen_unused_pat(fields); 114 | let fmt = 115 | "https://docs.rs/{crate_name}/{crate_version}/{mod_name}/{item_path}".into(); 116 | let item_path = format!("struct.{struct_name}.html"); 117 | let args = quote! { 118 | , 119 | crate_name=env!("CARGO_PKG_NAME"), 120 | crate_version=env!("CARGO_PKG_VERSION"), 121 | mod_name=env!("CARGO_PKG_NAME").replace('-', "_"), 122 | item_path=#item_path 123 | }; 124 | (pat, fmt, args) 125 | } 126 | }; 127 | Some(quote! { 128 | fn url(&self) -> std::option::Option> { 129 | #[allow(unused_variables, deprecated)] 130 | let Self #pat = self; 131 | std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/color_format.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "fancy-no-backtrace")] 2 | 3 | use std::{ 4 | ffi::OsString, 5 | fmt::{self, Debug}, 6 | sync::Mutex, 7 | }; 8 | 9 | use lazy_static::lazy_static; 10 | use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors}; 11 | use regex::Regex; 12 | use thiserror::Error; 13 | 14 | #[derive(Eq, PartialEq, Debug)] 15 | enum ColorFormat { 16 | NoColor, 17 | Ansi, 18 | Rgb, 19 | } 20 | 21 | #[derive(Debug, Diagnostic, Error)] 22 | #[error("oops!")] 23 | #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))] 24 | struct MyBad; 25 | 26 | struct FormatTester(MietteHandler); 27 | 28 | impl Debug for FormatTester { 29 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 30 | self.0.debug(&MyBad, f) 31 | } 32 | } 33 | 34 | /// Check the color format used by a handler. 35 | fn color_format(handler: MietteHandler) -> ColorFormat { 36 | let out = format!("{:?}", FormatTester(handler)); 37 | 38 | let rgb_colors = Regex::new(r"\u{1b}\[[34]8;2;").unwrap(); 39 | let ansi_colors = Regex::new(r"\u{1b}\[(3|4|9|10)[0-7][m;]").unwrap(); 40 | if rgb_colors.is_match(&out) { 41 | ColorFormat::Rgb 42 | } else if ansi_colors.is_match(&out) { 43 | ColorFormat::Ansi 44 | } else { 45 | ColorFormat::NoColor 46 | } 47 | } 48 | 49 | /// Store the current value of an environment variable on construction, and then 50 | /// restore that value when the guard is dropped. 51 | struct EnvVarGuard<'a> { 52 | var: &'a str, 53 | old_value: Option, 54 | } 55 | 56 | impl EnvVarGuard<'_> { 57 | fn new(var: &str) -> EnvVarGuard<'_> { 58 | EnvVarGuard { var, old_value: std::env::var_os(var) } 59 | } 60 | } 61 | 62 | impl Drop for EnvVarGuard<'_> { 63 | fn drop(&mut self) { 64 | if let Some(old_value) = &self.old_value { 65 | // TODO: Audit that the environment access only happens in single-threaded code. 66 | unsafe { std::env::set_var(self.var, old_value) }; 67 | } else { 68 | // TODO: Audit that the environment access only happens in single-threaded code. 69 | unsafe { std::env::remove_var(self.var) }; 70 | } 71 | } 72 | } 73 | 74 | lazy_static! { 75 | static ref COLOR_ENV_VARS: Mutex<()> = Mutex::new(()); 76 | } 77 | 78 | /// Assert the color format used by a handler with different levels of terminal 79 | /// support. 80 | fn check_colors MietteHandlerOpts>( 81 | make_handler: F, 82 | no_support: ColorFormat, 83 | ansi_support: ColorFormat, 84 | rgb_support: ColorFormat, 85 | ) { 86 | // To simulate different levels of terminal support we're using specific 87 | // environment variables that are handled by the supports_color crate. 88 | // 89 | // Since environment variables are shared for the entire process, we need 90 | // to ensure that only one test that modifies these env vars runs at a time. 91 | let lock = COLOR_ENV_VARS.lock().unwrap(); 92 | 93 | let guards = (EnvVarGuard::new("NO_COLOR"), EnvVarGuard::new("FORCE_COLOR")); 94 | // Clear color environment variables that may be set outside of 'cargo test' 95 | // TODO: Audit that the environment access only happens in single-threaded code. 96 | unsafe { std::env::remove_var("NO_COLOR") }; 97 | // TODO: Audit that the environment access only happens in single-threaded code. 98 | unsafe { std::env::remove_var("FORCE_COLOR") }; 99 | 100 | // TODO: Audit that the environment access only happens in single-threaded code. 101 | unsafe { std::env::set_var("NO_COLOR", "1") }; 102 | let handler = make_handler(MietteHandlerOpts::new()).build(); 103 | assert_eq!(color_format(handler), no_support); 104 | // TODO: Audit that the environment access only happens in single-threaded code. 105 | unsafe { std::env::remove_var("NO_COLOR") }; 106 | 107 | // TODO: Audit that the environment access only happens in single-threaded code. 108 | unsafe { std::env::set_var("FORCE_COLOR", "1") }; 109 | let handler = make_handler(MietteHandlerOpts::new()).build(); 110 | assert_eq!(color_format(handler), ansi_support); 111 | // TODO: Audit that the environment access only happens in single-threaded code. 112 | unsafe { std::env::remove_var("FORCE_COLOR") }; 113 | 114 | // TODO: Audit that the environment access only happens in single-threaded code. 115 | unsafe { std::env::set_var("FORCE_COLOR", "3") }; 116 | let handler = make_handler(MietteHandlerOpts::new()).build(); 117 | assert_eq!(color_format(handler), rgb_support); 118 | // TODO: Audit that the environment access only happens in single-threaded code. 119 | unsafe { std::env::remove_var("FORCE_COLOR") }; 120 | 121 | drop(guards); 122 | drop(lock); 123 | } 124 | 125 | #[test] 126 | fn no_color_preference() { 127 | use ColorFormat::*; 128 | check_colors(|opts| opts, NoColor, Ansi, Ansi); 129 | } 130 | 131 | #[test] 132 | fn color_never() { 133 | use ColorFormat::*; 134 | check_colors(|opts| opts.color(false), NoColor, NoColor, NoColor); 135 | } 136 | 137 | #[test] 138 | fn color_always() { 139 | use ColorFormat::*; 140 | check_colors(|opts| opts.color(true), Ansi, Ansi, Ansi); 141 | } 142 | 143 | #[test] 144 | fn rgb_preferred() { 145 | use ColorFormat::*; 146 | check_colors(|opts| opts.rgb_colors(RgbColors::Preferred), NoColor, Ansi, Rgb); 147 | } 148 | 149 | #[test] 150 | fn rgb_always() { 151 | use ColorFormat::*; 152 | check_colors(|opts| opts.rgb_colors(RgbColors::Always), NoColor, Rgb, Rgb); 153 | } 154 | 155 | #[test] 156 | fn color_always_rgb_always() { 157 | use ColorFormat::*; 158 | check_colors(|opts| opts.color(true).rgb_colors(RgbColors::Always), Rgb, Rgb, Rgb); 159 | } 160 | -------------------------------------------------------------------------------- /miette-derive/src/forward.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{format_ident, quote}; 5 | use syn::{ 6 | parenthesized, 7 | parse::{Parse, ParseStream}, 8 | spanned::Spanned, 9 | }; 10 | 11 | pub enum Forward { 12 | Unnamed(usize), 13 | Named(syn::Ident), 14 | } 15 | 16 | impl Parse for Forward { 17 | fn parse(input: ParseStream) -> syn::Result { 18 | let forward = input.parse::()?; 19 | if forward != "forward" { 20 | return Err(syn::Error::new(forward.span(), "msg")); 21 | } 22 | let content; 23 | parenthesized!(content in input); 24 | let looky = content.lookahead1(); 25 | if looky.peek(syn::LitInt) { 26 | let int: syn::LitInt = content.parse()?; 27 | let index = int.base10_parse()?; 28 | return Ok(Forward::Unnamed(index)); 29 | } 30 | Ok(Forward::Named(content.parse()?)) 31 | } 32 | } 33 | 34 | #[derive(Copy, Clone)] 35 | pub enum WhichFn { 36 | Code, 37 | Help, 38 | Url, 39 | Severity, 40 | Labels, 41 | SourceCode, 42 | Related, 43 | DiagnosticSource, 44 | } 45 | 46 | impl WhichFn { 47 | pub fn method_call(&self) -> TokenStream { 48 | match self { 49 | Self::Code => quote! { code() }, 50 | Self::Help => quote! { help() }, 51 | Self::Url => quote! { url() }, 52 | Self::Severity => quote! { severity() }, 53 | Self::Labels => quote! { labels() }, 54 | Self::SourceCode => quote! { source_code() }, 55 | Self::Related => quote! { related() }, 56 | Self::DiagnosticSource => quote! { diagnostic_source() }, 57 | } 58 | } 59 | 60 | pub fn signature(&self) -> TokenStream { 61 | match self { 62 | Self::Code => quote! { 63 | fn code(& self) -> std::option::Option> 64 | }, 65 | Self::Help => quote! { 66 | fn help(& self) -> std::option::Option> 67 | }, 68 | Self::Url => quote! { 69 | fn url(& self) -> std::option::Option> 70 | }, 71 | Self::Severity => quote! { 72 | fn severity(&self) -> std::option::Option 73 | }, 74 | Self::Related => quote! { 75 | fn related(&self) -> std::option::Option + '_>> 76 | }, 77 | Self::Labels => quote! { 78 | fn labels(&self) -> std::option::Option + '_>> 79 | }, 80 | Self::SourceCode => quote! { 81 | fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> 82 | }, 83 | Self::DiagnosticSource => quote! { 84 | fn diagnostic_source(&self) -> std::option::Option<&dyn miette::Diagnostic> 85 | }, 86 | } 87 | } 88 | 89 | pub fn catchall_arm(&self) -> TokenStream { 90 | quote! { _ => std::option::Option::None } 91 | } 92 | } 93 | 94 | impl Forward { 95 | pub fn for_transparent_field(fields: &syn::Fields) -> syn::Result { 96 | let make_err = || { 97 | syn::Error::new( 98 | fields.span(), 99 | "you can only use #[diagnostic(transparent)] with exactly one field", 100 | ) 101 | }; 102 | match fields { 103 | syn::Fields::Named(named) => { 104 | let mut iter = named.named.iter(); 105 | let field = iter.next().ok_or_else(make_err)?; 106 | if iter.next().is_some() { 107 | return Err(make_err()); 108 | } 109 | let field_name = field.ident.clone().unwrap_or_else(|| format_ident!("unnamed")); 110 | Ok(Self::Named(field_name)) 111 | } 112 | syn::Fields::Unnamed(unnamed) => { 113 | if unnamed.unnamed.iter().len() != 1 { 114 | return Err(make_err()); 115 | } 116 | Ok(Self::Unnamed(0)) 117 | } 118 | _ => Err(syn::Error::new( 119 | fields.span(), 120 | "you cannot use #[diagnostic(transparent)] with a unit struct or a unit variant", 121 | )), 122 | } 123 | } 124 | 125 | pub fn gen_struct_method(&self, which_fn: WhichFn) -> TokenStream { 126 | let signature = which_fn.signature(); 127 | let method_call = which_fn.method_call(); 128 | 129 | let field_name = match self { 130 | Forward::Named(field_name) => quote!(#field_name), 131 | Forward::Unnamed(index) => { 132 | let index = syn::Index::from(*index); 133 | quote!(#index) 134 | } 135 | }; 136 | 137 | quote! { 138 | #[inline] 139 | #signature { 140 | self.#field_name.#method_call 141 | } 142 | } 143 | } 144 | 145 | pub fn gen_enum_match_arm(&self, variant: &syn::Ident, which_fn: WhichFn) -> TokenStream { 146 | let method_call = which_fn.method_call(); 147 | match self { 148 | Forward::Named(field_name) => quote! { 149 | Self::#variant { #field_name, .. } => #field_name.#method_call, 150 | }, 151 | Forward::Unnamed(index) => { 152 | let underscores: Vec<_> = iter::repeat_n(quote! { _, }, *index).collect(); 153 | let unnamed = format_ident!("unnamed"); 154 | quote! { 155 | Self::#variant ( #(#underscores)* #unnamed, .. ) => #unnamed.#method_call, 156 | } 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /miette-derive/src/help.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{format_ident, quote}; 3 | use syn::{ 4 | Fields, Token, parenthesized, 5 | parse::{Parse, ParseStream}, 6 | spanned::Spanned, 7 | }; 8 | 9 | use crate::{ 10 | diagnostic::{DiagnosticConcreteArgs, DiagnosticDef}, 11 | fmt::{self, Display}, 12 | forward::WhichFn, 13 | utils::{display_pat_members, gen_all_variants_with}, 14 | }; 15 | 16 | pub enum Help { 17 | Display(Display), 18 | Field(syn::Member, Box), 19 | } 20 | 21 | impl Parse for Help { 22 | fn parse(input: ParseStream) -> syn::Result { 23 | let ident = input.parse::()?; 24 | if ident == "help" { 25 | let la = input.lookahead1(); 26 | if la.peek(syn::token::Paren) { 27 | let content; 28 | parenthesized!(content in input); 29 | let fmt = content.parse()?; 30 | let args = if content.is_empty() { 31 | TokenStream::new() 32 | } else { 33 | fmt::parse_token_expr(&content, false)? 34 | }; 35 | let display = Display { fmt, args, has_bonus_display: false }; 36 | Ok(Help::Display(display)) 37 | } else { 38 | input.parse::()?; 39 | Ok(Help::Display(Display { 40 | fmt: input.parse()?, 41 | args: TokenStream::new(), 42 | has_bonus_display: false, 43 | })) 44 | } 45 | } else { 46 | Err(syn::Error::new(ident.span(), "not a help")) 47 | } 48 | } 49 | } 50 | 51 | impl Help { 52 | pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result> { 53 | match fields { 54 | syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()), 55 | syn::Fields::Unnamed(unnamed) => { 56 | Self::from_fields_vec(unnamed.unnamed.iter().collect()) 57 | } 58 | syn::Fields::Unit => Ok(None), 59 | } 60 | } 61 | 62 | fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result> { 63 | for (i, field) in fields.iter().enumerate() { 64 | for attr in &field.attrs { 65 | if attr.path().is_ident("help") { 66 | let help = if let Some(ident) = field.ident.clone() { 67 | syn::Member::Named(ident) 68 | } else { 69 | syn::Member::Unnamed(syn::Index { index: i as u32, span: field.span() }) 70 | }; 71 | return Ok(Some(Help::Field(help, Box::new(field.ty.clone())))); 72 | } 73 | } 74 | } 75 | Ok(None) 76 | } 77 | 78 | pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option { 79 | gen_all_variants_with( 80 | variants, 81 | WhichFn::Help, 82 | |ident, fields, DiagnosticConcreteArgs { help, .. }| { 83 | let (display_pat, display_members) = display_pat_members(fields); 84 | match &help.as_ref()? { 85 | Help::Display(display) => { 86 | let (fmt, args) = display.expand_shorthand_cloned(&display_members); 87 | Some(quote! { 88 | Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))), 89 | }) 90 | } 91 | Help::Field(member, ty) => { 92 | let help = match &member { 93 | syn::Member::Named(ident) => ident.clone(), 94 | syn::Member::Unnamed(syn::Index { index, .. }) => { 95 | format_ident!("_{}", index) 96 | } 97 | }; 98 | let var = quote! { __miette_internal_var }; 99 | Some(quote! { 100 | Self::#ident #display_pat => { 101 | use miette::macro_helpers::ToOption; 102 | miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&#help).as_ref().map(|#var| -> std::boxed::Box { std::boxed::Box::new(format!("{}", #var)) }) 103 | }, 104 | }) 105 | } 106 | } 107 | }, 108 | ) 109 | } 110 | 111 | pub(crate) fn gen_struct(&self, fields: &Fields) -> Option { 112 | let (display_pat, display_members) = display_pat_members(fields); 113 | match self { 114 | Help::Display(display) => { 115 | let (fmt, args) = display.expand_shorthand_cloned(&display_members); 116 | Some(quote! { 117 | fn help(&self) -> std::option::Option> { 118 | #[allow(unused_variables, deprecated)] 119 | let Self #display_pat = self; 120 | std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))) 121 | } 122 | }) 123 | } 124 | Help::Field(member, ty) => { 125 | let var = quote! { __miette_internal_var }; 126 | Some(quote! { 127 | fn help(&self) -> std::option::Option> { 128 | #[allow(unused_variables, deprecated)] 129 | let Self #display_pat = self; 130 | use miette::macro_helpers::ToOption; 131 | miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#member).as_ref().map(|#var| -> std::boxed::Box { std::boxed::Box::new(format!("{}", #var)) }) 132 | } 133 | }) 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/eyreish/context.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{self, Debug, Display, Write}; 2 | use std::error::Error as StdError; 3 | 4 | use super::{ 5 | Report, WrapErr, 6 | error::{ContextError, ErrorImpl}, 7 | }; 8 | use crate::{Diagnostic, LabeledSpan}; 9 | 10 | mod ext { 11 | use super::*; 12 | 13 | pub trait Diag { 14 | #[track_caller] 15 | fn ext_report(self, msg: D) -> Report 16 | where 17 | D: Display + Send + Sync + 'static; 18 | } 19 | 20 | impl Diag for E 21 | where 22 | E: Diagnostic + Send + Sync + 'static, 23 | { 24 | fn ext_report(self, msg: D) -> Report 25 | where 26 | D: Display + Send + Sync + 'static, 27 | { 28 | Report::from_msg(msg, self) 29 | } 30 | } 31 | 32 | impl Diag for Report { 33 | fn ext_report(self, msg: D) -> Report 34 | where 35 | D: Display + Send + Sync + 'static, 36 | { 37 | self.wrap_err(msg) 38 | } 39 | } 40 | } 41 | 42 | impl WrapErr for Result 43 | where 44 | E: ext::Diag + Send + Sync + 'static, 45 | { 46 | fn wrap_err(self, msg: D) -> Result 47 | where 48 | D: Display + Send + Sync + 'static, 49 | { 50 | match self { 51 | Ok(t) => Ok(t), 52 | Err(e) => Err(e.ext_report(msg)), 53 | } 54 | } 55 | 56 | fn wrap_err_with(self, msg: F) -> Result 57 | where 58 | D: Display + Send + Sync + 'static, 59 | F: FnOnce() -> D, 60 | { 61 | match self { 62 | Ok(t) => Ok(t), 63 | Err(e) => Err(e.ext_report(msg())), 64 | } 65 | } 66 | 67 | fn context(self, msg: D) -> Result 68 | where 69 | D: Display + Send + Sync + 'static, 70 | { 71 | self.wrap_err(msg) 72 | } 73 | 74 | fn with_context(self, msg: F) -> Result 75 | where 76 | D: Display + Send + Sync + 'static, 77 | F: FnOnce() -> D, 78 | { 79 | self.wrap_err_with(msg) 80 | } 81 | } 82 | 83 | impl Debug for ContextError 84 | where 85 | D: Display, 86 | E: Debug, 87 | { 88 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 89 | f.debug_struct("Error") 90 | .field("msg", &Quoted(&self.msg)) 91 | .field("source", &self.error) 92 | .finish() 93 | } 94 | } 95 | 96 | impl Display for ContextError 97 | where 98 | D: Display, 99 | { 100 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 101 | Display::fmt(&self.msg, f) 102 | } 103 | } 104 | 105 | impl StdError for ContextError 106 | where 107 | D: Display, 108 | E: StdError + 'static, 109 | { 110 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 111 | Some(&self.error) 112 | } 113 | } 114 | 115 | impl StdError for ContextError 116 | where 117 | D: Display, 118 | { 119 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 120 | unsafe { Some(ErrorImpl::error(self.error.inner.by_ref())) } 121 | } 122 | } 123 | 124 | impl Diagnostic for ContextError 125 | where 126 | D: Display, 127 | E: Diagnostic + 'static, 128 | { 129 | fn code<'a>(&'a self) -> Option> { 130 | self.error.code() 131 | } 132 | 133 | fn severity(&self) -> Option { 134 | self.error.severity() 135 | } 136 | 137 | fn help<'a>(&'a self) -> Option> { 138 | self.error.help() 139 | } 140 | 141 | fn url<'a>(&'a self) -> Option> { 142 | self.error.url() 143 | } 144 | 145 | fn labels<'a>(&'a self) -> Option + 'a>> { 146 | self.error.labels() 147 | } 148 | 149 | fn source_code(&self) -> Option<&dyn crate::SourceCode> { 150 | self.error.source_code() 151 | } 152 | 153 | fn related<'a>(&'a self) -> Option + 'a>> { 154 | self.error.related() 155 | } 156 | } 157 | 158 | impl Diagnostic for ContextError 159 | where 160 | D: Display, 161 | { 162 | fn code<'a>(&'a self) -> Option> { 163 | unsafe { ErrorImpl::diagnostic(self.error.inner.by_ref()).code() } 164 | } 165 | 166 | fn severity(&self) -> Option { 167 | unsafe { ErrorImpl::diagnostic(self.error.inner.by_ref()).severity() } 168 | } 169 | 170 | fn help<'a>(&'a self) -> Option> { 171 | unsafe { ErrorImpl::diagnostic(self.error.inner.by_ref()).help() } 172 | } 173 | 174 | fn url<'a>(&'a self) -> Option> { 175 | unsafe { ErrorImpl::diagnostic(self.error.inner.by_ref()).url() } 176 | } 177 | 178 | fn labels<'a>(&'a self) -> Option + 'a>> { 179 | unsafe { ErrorImpl::diagnostic(self.error.inner.by_ref()).labels() } 180 | } 181 | 182 | fn source_code(&self) -> Option<&dyn crate::SourceCode> { 183 | self.error.source_code() 184 | } 185 | 186 | fn related<'a>(&'a self) -> Option + 'a>> { 187 | self.error.related() 188 | } 189 | } 190 | 191 | struct Quoted(D); 192 | 193 | impl Debug for Quoted 194 | where 195 | D: Display, 196 | { 197 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 198 | formatter.write_char('"')?; 199 | Quoted(&mut *formatter).write_fmt(format_args!("{}", self.0))?; 200 | formatter.write_char('"')?; 201 | Ok(()) 202 | } 203 | } 204 | 205 | impl Write for Quoted<&mut fmt::Formatter<'_>> { 206 | fn write_str(&mut self, s: &str) -> fmt::Result { 207 | Display::fmt(&s.escape_debug(), self.0) 208 | } 209 | } 210 | 211 | pub(crate) mod private { 212 | use super::*; 213 | 214 | pub trait Sealed {} 215 | 216 | impl Sealed for Result where E: ext::Diag {} 217 | impl Sealed for Option {} 218 | } 219 | -------------------------------------------------------------------------------- /src/handlers/json.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Write}; 2 | 3 | use crate::{ 4 | ReportHandler, Severity, SourceCode, diagnostic_chain::DiagnosticChain, protocol::Diagnostic, 5 | }; 6 | 7 | /** 8 | [`ReportHandler`] that renders JSON output. It's a machine-readable output. 9 | */ 10 | #[derive(Debug, Clone)] 11 | pub struct JSONReportHandler; 12 | 13 | impl JSONReportHandler { 14 | /// Create a new [`JSONReportHandler`]. There are no customization 15 | /// options. 16 | pub const fn new() -> Self { 17 | Self 18 | } 19 | } 20 | 21 | impl Default for JSONReportHandler { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | struct Escape<'a>(&'a str); 28 | 29 | impl fmt::Display for Escape<'_> { 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | for c in self.0.chars() { 32 | let escape = match c { 33 | '\\' => Some(r"\\"), 34 | '"' => Some(r#"\""#), 35 | '\r' => Some(r"\r"), 36 | '\n' => Some(r"\n"), 37 | '\t' => Some(r"\t"), 38 | '\u{08}' => Some(r"\b"), 39 | '\u{0c}' => Some(r"\f"), 40 | _ => None, 41 | }; 42 | if let Some(escape) = escape { 43 | f.write_str(escape)?; 44 | } else { 45 | f.write_char(c)?; 46 | } 47 | } 48 | Ok(()) 49 | } 50 | } 51 | 52 | const fn escape(input: &'_ str) -> Escape<'_> { 53 | Escape(input) 54 | } 55 | 56 | impl JSONReportHandler { 57 | /// Render a [`Diagnostic`]. This function is mostly internal and meant to 58 | /// be called by the toplevel [`ReportHandler`] handler, but is made public 59 | /// to make it easier (possible) to test in isolation from global state. 60 | pub fn render_report( 61 | &self, 62 | f: &mut impl fmt::Write, 63 | diagnostic: &dyn Diagnostic, 64 | ) -> fmt::Result { 65 | self._render_report(f, diagnostic, None) 66 | } 67 | 68 | fn _render_report( 69 | &self, 70 | f: &mut impl fmt::Write, 71 | diagnostic: &dyn Diagnostic, 72 | parent_src: Option<&dyn SourceCode>, 73 | ) -> fmt::Result { 74 | write!(f, r#"{{"message": "{}","#, escape(&diagnostic.to_string()))?; 75 | if let Some(code) = diagnostic.code() { 76 | write!(f, r#""code": "{}","#, escape(&code.to_string()))?; 77 | } 78 | let severity = match diagnostic.severity() { 79 | Some(Severity::Error) | None => "error", 80 | Some(Severity::Warning) => "warning", 81 | Some(Severity::Advice) => "advice", 82 | }; 83 | write!(f, r#""severity": "{severity:}","#)?; 84 | if let Some(cause_iter) = diagnostic 85 | .diagnostic_source() 86 | .map(DiagnosticChain::from_diagnostic) 87 | .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) 88 | { 89 | write!(f, r#""causes": ["#)?; 90 | let mut add_comma = false; 91 | for error in cause_iter { 92 | if add_comma { 93 | write!(f, ",")?; 94 | } else { 95 | add_comma = true; 96 | } 97 | write!(f, r#""{}""#, escape(&error.to_string()))?; 98 | } 99 | write!(f, "],")?; 100 | } else { 101 | write!(f, r#""causes": [],"#)?; 102 | } 103 | if let Some(url) = diagnostic.url() { 104 | write!(f, r#""url": "{}","#, &url.to_string())?; 105 | } 106 | if let Some(help) = diagnostic.help() { 107 | write!(f, r#""help": "{}","#, escape(&help.to_string()))?; 108 | } 109 | let src = diagnostic.source_code().or(parent_src); 110 | if let Some(src) = src { 111 | self.render_snippets(f, diagnostic, src)?; 112 | } 113 | match diagnostic.labels() { 114 | Some(labels) => { 115 | write!(f, r#""labels": ["#)?; 116 | let mut add_comma = false; 117 | for label in labels { 118 | if add_comma { 119 | write!(f, ",")?; 120 | } else { 121 | add_comma = true; 122 | } 123 | write!(f, "{{")?; 124 | if let Some(label_name) = label.label() { 125 | write!(f, r#""label": "{}","#, escape(label_name))?; 126 | } 127 | write!(f, r#""span": {{"#)?; 128 | write!(f, r#""offset": {},"#, label.offset())?; 129 | write!(f, r#""length": {},"#, label.len())?; 130 | 131 | if let Some(Ok(location)) = diagnostic 132 | .source_code() 133 | .or(parent_src) 134 | .map(|src| src.read_span(label.inner(), 0, 0)) 135 | { 136 | write!(f, r#""line": {},"#, location.line() + 1)?; 137 | write!(f, r#""column": {}"#, location.column() + 1)?; 138 | } else { 139 | write!(f, r#""line": null,"column": null"#)?; 140 | } 141 | 142 | write!(f, "}}}}")?; 143 | } 144 | write!(f, "],")?; 145 | } 146 | _ => { 147 | write!(f, r#""labels": [],"#)?; 148 | } 149 | } 150 | match diagnostic.related() { 151 | Some(relates) => { 152 | write!(f, r#""related": ["#)?; 153 | let mut add_comma = false; 154 | for related in relates { 155 | if add_comma { 156 | write!(f, ",")?; 157 | } else { 158 | add_comma = true; 159 | } 160 | self._render_report(f, related, src)?; 161 | } 162 | write!(f, "]")?; 163 | } 164 | _ => { 165 | write!(f, r#""related": []"#)?; 166 | } 167 | } 168 | write!(f, "}}") 169 | } 170 | 171 | fn render_snippets( 172 | &self, 173 | f: &mut impl fmt::Write, 174 | diagnostic: &dyn Diagnostic, 175 | source: &dyn SourceCode, 176 | ) -> fmt::Result { 177 | if let Some(mut labels) = diagnostic.labels() { 178 | if let Some(label) = labels.next() { 179 | if let Ok(span_content) = source.read_span(label.inner(), 0, 0) { 180 | let filename = span_content.name().unwrap_or_default(); 181 | return write!(f, r#""filename": "{}","#, escape(filename)); 182 | } 183 | } 184 | } 185 | write!(f, r#""filename": "","#) 186 | } 187 | } 188 | 189 | impl ReportHandler for JSONReportHandler { 190 | fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result { 191 | self.render_report(f, diagnostic) 192 | } 193 | } 194 | 195 | #[test] 196 | fn test_escape() { 197 | assert_eq!(escape("a\nb").to_string(), r"a\nb"); 198 | assert_eq!(escape("C:\\Miette").to_string(), r"C:\\Miette"); 199 | } 200 | -------------------------------------------------------------------------------- /tests/test_boxed.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error as StdError, io}; 2 | 3 | use miette::{Diagnostic, LabeledSpan, Report, SourceSpan, miette}; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | #[error("outer")] 8 | struct MyError { 9 | source: io::Error, 10 | } 11 | impl Diagnostic for MyError {} 12 | 13 | #[test] 14 | fn test_boxed_str_diagnostic() { 15 | let error = Box::::from("oh no!"); 16 | let error: Report = miette!(error); 17 | assert_eq!("oh no!", error.to_string()); 18 | assert_eq!( 19 | "oh no!", 20 | error.downcast_ref::>().unwrap().to_string() 21 | ); 22 | } 23 | 24 | #[test] 25 | fn test_boxed_str_stderr() { 26 | let error = Box::::from("oh no!"); 27 | let error: Report = miette!(error); 28 | assert_eq!("oh no!", error.to_string()); 29 | assert_eq!( 30 | "oh no!", 31 | error.downcast_ref::>().unwrap().to_string() 32 | ); 33 | } 34 | 35 | #[test] 36 | fn test_boxed_thiserror() { 37 | let error = MyError { source: io::Error::other("oh no!") }; 38 | let report: Report = miette!(error); 39 | assert_eq!("oh no!", report.source().unwrap().to_string()); 40 | 41 | let error = MyError { source: io::Error::other("oh no!!!!") }; 42 | let error: Box = Box::new(error); 43 | let report = Report::new_boxed(error); 44 | assert_eq!("oh no!!!!", report.source().unwrap().to_string()); 45 | } 46 | 47 | #[test] 48 | fn test_boxed_miette() { 49 | let error: Report = miette!("oh no!").wrap_err("it failed"); 50 | let error = miette!(error); 51 | assert_eq!("oh no!", error.source().unwrap().to_string()); 52 | } 53 | 54 | #[derive(Debug)] 55 | struct CustomDiagnostic { 56 | source: Option, 57 | related: Vec>, 58 | } 59 | 60 | impl CustomDiagnostic { 61 | const CODE: &'static str = "A042"; 62 | const DESCRIPTION: &'static str = "CustomDiagnostic description"; 63 | const DISPLAY: &'static str = "CustomDiagnostic display"; 64 | const HELP: &'static str = "CustomDiagnostic help"; 65 | const LABEL: &'static str = "CustomDiagnostic label"; 66 | const SEVERITY: miette::Severity = miette::Severity::Advice; 67 | const SOURCE_CODE: &'static str = "this-is-some-source-code"; 68 | const URL: &'static str = "https://custom-diagnostic-url"; 69 | 70 | fn new() -> Self { 71 | Self { source: None, related: Vec::new() } 72 | } 73 | 74 | fn with_source(self, source: E) -> Self { 75 | let source = miette!(source); 76 | Self { source: Some(source), related: Vec::new() } 77 | } 78 | 79 | fn with_related(mut self, diagnostic: D) -> Self { 80 | self.related.push(Box::new(diagnostic)); 81 | self 82 | } 83 | } 84 | 85 | impl std::fmt::Display for CustomDiagnostic { 86 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 87 | f.write_str(Self::DISPLAY) 88 | } 89 | } 90 | 91 | impl StdError for CustomDiagnostic { 92 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 93 | self.source.as_ref().map(std::convert::AsRef::as_ref) 94 | } 95 | 96 | fn description(&self) -> &str { 97 | Self::DESCRIPTION 98 | } 99 | 100 | fn cause(&self) -> Option<&dyn StdError> { 101 | self.source.as_ref().map(std::convert::AsRef::as_ref) 102 | } 103 | } 104 | 105 | impl Diagnostic for CustomDiagnostic { 106 | fn code<'a>(&'a self) -> Option> { 107 | Some(Box::new(Self::CODE)) 108 | } 109 | 110 | fn severity(&self) -> Option { 111 | Some(miette::Severity::Advice) 112 | } 113 | 114 | fn help<'a>(&'a self) -> Option> { 115 | Some(Box::new(Self::HELP)) 116 | } 117 | 118 | fn url<'a>(&'a self) -> Option> { 119 | Some(Box::new(Self::URL)) 120 | } 121 | 122 | fn labels<'a>(&'a self) -> Option + 'a>> { 123 | let labels = miette::LabeledSpan::new(Some(Self::LABEL.to_owned()), 0, 7); 124 | Some(Box::new(std::iter::once(labels))) 125 | } 126 | 127 | fn source_code(&self) -> Option<&dyn miette::SourceCode> { 128 | Some(&Self::SOURCE_CODE) 129 | } 130 | 131 | fn related<'a>(&'a self) -> Option + 'a>> { 132 | Some(Box::new(self.related.iter().map(|d| &**d as &'a dyn Diagnostic))) 133 | } 134 | 135 | fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { 136 | self.source.as_ref().map(|source| &**source as &dyn Diagnostic) 137 | } 138 | } 139 | 140 | #[test] 141 | fn test_boxed_custom_diagnostic() { 142 | fn assert_report(report: &Report) { 143 | assert_eq!( 144 | report.source().map(std::string::ToString::to_string), 145 | Some("oh no!".to_owned()), 146 | ); 147 | assert_eq!( 148 | report.code().map(|code| code.to_string()), 149 | Some(CustomDiagnostic::CODE.to_owned()) 150 | ); 151 | assert_eq!(report.severity(), Some(CustomDiagnostic::SEVERITY)); 152 | assert_eq!( 153 | report.help().map(|help| help.to_string()), 154 | Some(CustomDiagnostic::HELP.to_owned()) 155 | ); 156 | assert_eq!(report.url().map(|url| url.to_string()), Some(CustomDiagnostic::URL.to_owned())); 157 | assert_eq!( 158 | report.labels().map(std::iter::Iterator::collect), 159 | Some(vec![LabeledSpan::new(Some(CustomDiagnostic::LABEL.to_owned()), 0, 7)]), 160 | ); 161 | let span = SourceSpan::from(0..CustomDiagnostic::SOURCE_CODE.len()); 162 | assert_eq!( 163 | report.source_code().map(|source_code| source_code 164 | .read_span(&span, 0, 0) 165 | .expect("read data from source code successfully") 166 | .data() 167 | .to_owned()), 168 | Some(CustomDiagnostic::SOURCE_CODE.to_owned().into_bytes()) 169 | ); 170 | assert_eq!( 171 | report.diagnostic_source().map(std::string::ToString::to_string), 172 | Some("oh no!".to_owned()), 173 | ); 174 | } 175 | 176 | let related = CustomDiagnostic::new(); 177 | let main_diagnostic = 178 | CustomDiagnostic::new().with_source(io::Error::other("oh no!")).with_related(related); 179 | 180 | let report = Report::new_boxed(Box::new(main_diagnostic)); 181 | assert_report(&report); 182 | 183 | let related = CustomDiagnostic::new(); 184 | let main_diagnostic = 185 | CustomDiagnostic::new().with_source(io::Error::other("oh no!")).with_related(related); 186 | let main_diagnostic = Box::new(main_diagnostic) as Box; 187 | let report = miette!(main_diagnostic); 188 | assert_report(&report); 189 | } 190 | 191 | #[test] 192 | #[ignore = "I don't know why this isn't working but it needs fixing."] 193 | fn test_boxed_sources() { 194 | let error = MyError { source: io::Error::other("oh no!") }; 195 | let error = Box::::from(error); 196 | let error: Report = miette!(error).wrap_err("it failed"); 197 | assert_eq!("it failed", error.to_string()); 198 | assert_eq!("outer", error.source().unwrap().to_string()); 199 | assert_eq!("oh no!", error.source().expect("outer").source().expect("inner").to_string()); 200 | } 201 | -------------------------------------------------------------------------------- /src/eyreish/wrapper.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{self, Debug, Display}; 2 | use std::error::Error as StdError; 3 | 4 | use crate as miette; 5 | use crate::{Diagnostic, LabeledSpan, Report, SourceCode}; 6 | 7 | #[repr(transparent)] 8 | pub(crate) struct MessageError(pub(crate) M); 9 | 10 | impl Debug for MessageError 11 | where 12 | M: Display + Debug, 13 | { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | Debug::fmt(&self.0, f) 16 | } 17 | } 18 | 19 | impl Display for MessageError 20 | where 21 | M: Display + Debug, 22 | { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | Display::fmt(&self.0, f) 25 | } 26 | } 27 | 28 | impl StdError for MessageError where M: Display + Debug + 'static {} 29 | impl Diagnostic for MessageError where M: Display + Debug + 'static {} 30 | 31 | #[repr(transparent)] 32 | pub(crate) struct BoxedError(pub(crate) Box); 33 | 34 | impl Diagnostic for BoxedError { 35 | fn code<'a>(&'a self) -> Option> { 36 | self.0.code() 37 | } 38 | 39 | fn severity(&self) -> Option { 40 | self.0.severity() 41 | } 42 | 43 | fn help<'a>(&'a self) -> Option> { 44 | self.0.help() 45 | } 46 | 47 | fn url<'a>(&'a self) -> Option> { 48 | self.0.url() 49 | } 50 | 51 | fn labels<'a>(&'a self) -> Option + 'a>> { 52 | self.0.labels() 53 | } 54 | 55 | fn source_code(&self) -> Option<&dyn miette::SourceCode> { 56 | self.0.source_code() 57 | } 58 | 59 | fn related<'a>(&'a self) -> Option + 'a>> { 60 | self.0.related() 61 | } 62 | 63 | fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { 64 | self.0.diagnostic_source() 65 | } 66 | } 67 | 68 | impl Debug for BoxedError { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | Debug::fmt(&self.0, f) 71 | } 72 | } 73 | 74 | impl Display for BoxedError { 75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | Display::fmt(&self.0, f) 77 | } 78 | } 79 | 80 | impl StdError for BoxedError { 81 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 82 | self.0.source() 83 | } 84 | 85 | fn description(&self) -> &str { 86 | #[allow(deprecated)] 87 | self.0.description() 88 | } 89 | 90 | fn cause(&self) -> Option<&dyn StdError> { 91 | #[allow(deprecated)] 92 | self.0.cause() 93 | } 94 | } 95 | 96 | pub(crate) struct WithSourceCode { 97 | pub(crate) error: E, 98 | pub(crate) source_code: C, 99 | } 100 | 101 | impl Diagnostic for WithSourceCode { 102 | fn code<'a>(&'a self) -> Option> { 103 | self.error.code() 104 | } 105 | 106 | fn severity(&self) -> Option { 107 | self.error.severity() 108 | } 109 | 110 | fn help<'a>(&'a self) -> Option> { 111 | self.error.help() 112 | } 113 | 114 | fn url<'a>(&'a self) -> Option> { 115 | self.error.url() 116 | } 117 | 118 | fn labels<'a>(&'a self) -> Option + 'a>> { 119 | self.error.labels() 120 | } 121 | 122 | fn source_code(&self) -> Option<&dyn miette::SourceCode> { 123 | self.error.source_code().or(Some(&self.source_code)) 124 | } 125 | 126 | fn related<'a>(&'a self) -> Option + 'a>> { 127 | self.error.related() 128 | } 129 | 130 | fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { 131 | self.error.diagnostic_source() 132 | } 133 | } 134 | 135 | impl Diagnostic for WithSourceCode { 136 | fn code<'a>(&'a self) -> Option> { 137 | self.error.code() 138 | } 139 | 140 | fn severity(&self) -> Option { 141 | self.error.severity() 142 | } 143 | 144 | fn help<'a>(&'a self) -> Option> { 145 | self.error.help() 146 | } 147 | 148 | fn url<'a>(&'a self) -> Option> { 149 | self.error.url() 150 | } 151 | 152 | fn labels<'a>(&'a self) -> Option + 'a>> { 153 | self.error.labels() 154 | } 155 | 156 | fn source_code(&self) -> Option<&dyn miette::SourceCode> { 157 | self.error.source_code().or(Some(&self.source_code)) 158 | } 159 | 160 | fn related<'a>(&'a self) -> Option + 'a>> { 161 | self.error.related() 162 | } 163 | 164 | fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { 165 | self.error.diagnostic_source() 166 | } 167 | } 168 | 169 | impl Debug for WithSourceCode { 170 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 171 | Debug::fmt(&self.error, f) 172 | } 173 | } 174 | 175 | impl Display for WithSourceCode { 176 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 177 | Display::fmt(&self.error, f) 178 | } 179 | } 180 | 181 | impl StdError for WithSourceCode { 182 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 183 | self.error.source() 184 | } 185 | } 186 | 187 | impl StdError for WithSourceCode { 188 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 189 | self.error.source() 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use thiserror::Error; 196 | 197 | use crate::{Diagnostic, LabeledSpan, Report, SourceCode, SourceSpan}; 198 | 199 | #[derive(Error, Debug)] 200 | #[error("inner")] 201 | struct Inner { 202 | pub(crate) at: SourceSpan, 203 | pub(crate) source_code: Option, 204 | } 205 | 206 | impl Diagnostic for Inner { 207 | fn labels(&self) -> Option + '_>> { 208 | Some(Box::new(std::iter::once(LabeledSpan::underline(self.at)))) 209 | } 210 | 211 | fn source_code(&self) -> Option<&dyn SourceCode> { 212 | self.source_code.as_ref().map(|s| s as _) 213 | } 214 | } 215 | 216 | #[derive(Error, Debug)] 217 | #[error("outer")] 218 | #[allow(unused)] 219 | struct Outer { 220 | pub(crate) errors: Vec, 221 | } 222 | 223 | impl Diagnostic for Outer { 224 | fn related<'a>(&'a self) -> Option + 'a>> { 225 | Some(Box::new(self.errors.iter().map(|e| e as _))) 226 | } 227 | } 228 | 229 | #[test] 230 | fn no_override() { 231 | let inner_source = "hello world"; 232 | let outer_source = "abc"; 233 | 234 | let report = 235 | Report::from(Inner { at: (0..5).into(), source_code: Some(inner_source.to_string()) }) 236 | .with_source_code(outer_source.to_string()); 237 | 238 | let underlined = String::from_utf8( 239 | report.source_code().unwrap().read_span(&(0..5).into(), 0, 0).unwrap().data().to_vec(), 240 | ) 241 | .unwrap(); 242 | assert_eq!(underlined, "hello"); 243 | } 244 | 245 | #[test] 246 | #[cfg(feature = "fancy")] 247 | fn two_source_codes() { 248 | let inner_source = "hello world"; 249 | let outer_source = "abc"; 250 | 251 | let report = Report::from(Outer { 252 | errors: vec![ 253 | Inner { at: (0..5).into(), source_code: Some(inner_source.to_string()) }, 254 | Inner { at: (1..2).into(), source_code: None }, 255 | ], 256 | }) 257 | .with_source_code(outer_source.to_string()); 258 | 259 | let message = format!("{report:?}"); 260 | assert!(message.contains(inner_source)); 261 | assert!(message.contains(outer_source)); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /miette-derive/src/fmt.rs: -------------------------------------------------------------------------------- 1 | // NOTE: Most code in this file is taken straight from `thiserror`. 2 | use std::{collections::HashSet as Set, iter::FromIterator}; 3 | 4 | use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; 5 | use quote::{ToTokens, format_ident, quote, quote_spanned}; 6 | use syn::{ 7 | Ident, Index, LitStr, Member, Result, Token, braced, bracketed, 8 | ext::IdentExt, 9 | parenthesized, 10 | parse::{ParseStream, Parser}, 11 | }; 12 | 13 | #[derive(Clone)] 14 | pub struct Display { 15 | pub fmt: LitStr, 16 | pub args: TokenStream, 17 | pub has_bonus_display: bool, 18 | } 19 | 20 | impl ToTokens for Display { 21 | fn to_tokens(&self, tokens: &mut TokenStream) { 22 | let fmt = &self.fmt; 23 | let args = &self.args; 24 | tokens.extend(quote! { 25 | write!(__formatter, #fmt #args) 26 | }); 27 | } 28 | } 29 | 30 | impl Display { 31 | // Transform `"error {var}"` to `"error {}", var`. 32 | pub fn expand_shorthand(&mut self, members: &Set) { 33 | let raw_args = self.args.clone(); 34 | let mut named_args = explicit_named_args.parse2(raw_args).unwrap(); 35 | 36 | let span = self.fmt.span(); 37 | let fmt = self.fmt.value(); 38 | let mut read = fmt.as_str(); 39 | let mut out = String::new(); 40 | let mut args = self.args.clone(); 41 | let mut has_bonus_display = false; 42 | 43 | let mut has_trailing_comma = false; 44 | if let Some(TokenTree::Punct(punct)) = args.clone().into_iter().last() { 45 | if punct.as_char() == ',' { 46 | has_trailing_comma = true; 47 | } 48 | } 49 | 50 | while let Some(brace) = read.find('{') { 51 | out += &read[..brace + 1]; 52 | read = &read[brace + 1..]; 53 | if read.starts_with('{') { 54 | out.push('{'); 55 | read = &read[1..]; 56 | continue; 57 | } 58 | let next = match read.chars().next() { 59 | Some(next) => next, 60 | None => return, 61 | }; 62 | let member = match next { 63 | '0'..='9' => { 64 | let int = take_int(&mut read); 65 | let member = match int.parse::() { 66 | Ok(index) => Member::Unnamed(Index { index, span }), 67 | Err(_) => return, 68 | }; 69 | if !members.contains(&member) { 70 | out += ∫ 71 | continue; 72 | } 73 | member 74 | } 75 | 'a'..='z' | 'A'..='Z' | '_' => { 76 | let mut ident = take_ident(&mut read); 77 | ident.set_span(span); 78 | Member::Named(ident) 79 | } 80 | _ => continue, 81 | }; 82 | let local = match &member { 83 | Member::Unnamed(index) => format_ident!("_{}", index), 84 | Member::Named(ident) => ident.clone(), 85 | }; 86 | let mut formatvar = local.clone(); 87 | if formatvar.to_string().starts_with("r#") { 88 | formatvar = format_ident!("r_{}", formatvar); 89 | } 90 | if formatvar.to_string().starts_with('_') { 91 | // Work around leading underscore being rejected by 1.40 and 92 | // older compilers. https://github.com/rust-lang/rust/pull/66847 93 | formatvar = format_ident!("field_{}", formatvar); 94 | } 95 | out += &formatvar.to_string(); 96 | if !named_args.insert(formatvar.clone()) { 97 | // Already specified in the format argument list. 98 | continue; 99 | } 100 | if !has_trailing_comma { 101 | args.extend(quote_spanned!(span=> ,)); 102 | } 103 | args.extend(quote_spanned!(span=> #formatvar = #local)); 104 | if read.starts_with('}') && members.contains(&member) { 105 | has_bonus_display = true; 106 | // args.extend(quote_spanned!(span=> .as_display())); 107 | } 108 | has_trailing_comma = false; 109 | } 110 | 111 | out += read; 112 | self.fmt = LitStr::new(&out, self.fmt.span()); 113 | self.args = args; 114 | self.has_bonus_display = has_bonus_display; 115 | } 116 | } 117 | 118 | fn explicit_named_args(input: ParseStream) -> Result> { 119 | let mut named_args = Set::new(); 120 | 121 | while !input.is_empty() { 122 | if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) { 123 | input.parse::()?; 124 | let ident = input.call(Ident::parse_any)?; 125 | input.parse::()?; 126 | named_args.insert(ident); 127 | } else { 128 | input.parse::()?; 129 | } 130 | } 131 | 132 | Ok(named_args) 133 | } 134 | 135 | fn take_int(read: &mut &str) -> String { 136 | let mut int = String::new(); 137 | for (i, ch) in read.char_indices() { 138 | match ch { 139 | '0'..='9' => int.push(ch), 140 | _ => { 141 | *read = &read[i..]; 142 | break; 143 | } 144 | } 145 | } 146 | int 147 | } 148 | 149 | fn take_ident(read: &mut &str) -> Ident { 150 | let mut ident = String::new(); 151 | let raw = read.starts_with("r#"); 152 | if raw { 153 | ident.push_str("r#"); 154 | *read = &read[2..]; 155 | } 156 | for (i, ch) in read.char_indices() { 157 | match ch { 158 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch), 159 | _ => { 160 | *read = &read[i..]; 161 | break; 162 | } 163 | } 164 | } 165 | Ident::parse_any.parse_str(&ident).unwrap() 166 | } 167 | 168 | pub fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result { 169 | let mut tokens = Vec::new(); 170 | while !input.is_empty() { 171 | if begin_expr && input.peek(Token![.]) { 172 | if input.peek2(Ident) { 173 | input.parse::()?; 174 | begin_expr = false; 175 | continue; 176 | } 177 | if input.peek2(syn::LitInt) { 178 | input.parse::()?; 179 | let int: Index = input.parse()?; 180 | let ident = format_ident!("_{}", int.index, span = int.span); 181 | tokens.push(TokenTree::Ident(ident)); 182 | begin_expr = false; 183 | continue; 184 | } 185 | } 186 | 187 | begin_expr = input.peek(Token![break]) 188 | || input.peek(Token![continue]) 189 | || input.peek(Token![if]) 190 | || input.peek(Token![in]) 191 | || input.peek(Token![match]) 192 | || input.peek(Token![mut]) 193 | || input.peek(Token![return]) 194 | || input.peek(Token![while]) 195 | || input.peek(Token![+]) 196 | || input.peek(Token![&]) 197 | || input.peek(Token![!]) 198 | || input.peek(Token![^]) 199 | || input.peek(Token![,]) 200 | || input.peek(Token![/]) 201 | || input.peek(Token![=]) 202 | || input.peek(Token![>]) 203 | || input.peek(Token![<]) 204 | || input.peek(Token![|]) 205 | || input.peek(Token![%]) 206 | || input.peek(Token![;]) 207 | || input.peek(Token![*]) 208 | || input.peek(Token![-]); 209 | 210 | let token: TokenTree = if input.peek(syn::token::Paren) { 211 | let content; 212 | let delimiter = parenthesized!(content in input); 213 | let nested = parse_token_expr(&content, true)?; 214 | let mut group = Group::new(Delimiter::Parenthesis, nested); 215 | group.set_span(delimiter.span.join()); 216 | TokenTree::Group(group) 217 | } else if input.peek(syn::token::Brace) { 218 | let content; 219 | let delimiter = braced!(content in input); 220 | let nested = parse_token_expr(&content, true)?; 221 | let mut group = Group::new(Delimiter::Brace, nested); 222 | group.set_span(delimiter.span.join()); 223 | TokenTree::Group(group) 224 | } else if input.peek(syn::token::Bracket) { 225 | let content; 226 | let delimiter = bracketed!(content in input); 227 | let nested = parse_token_expr(&content, true)?; 228 | let mut group = Group::new(Delimiter::Bracket, nested); 229 | group.set_span(delimiter.span.join()); 230 | TokenTree::Group(group) 231 | } else { 232 | input.parse()? 233 | }; 234 | tokens.push(token); 235 | } 236 | Ok(TokenStream::from_iter(tokens)) 237 | } 238 | -------------------------------------------------------------------------------- /src/eyreish/macros.rs: -------------------------------------------------------------------------------- 1 | /// Return early with an error. 2 | /// 3 | /// This macro is equivalent to `return Err(From::from($err))`. 4 | /// 5 | /// # Example 6 | /// 7 | /// ``` 8 | /// # use miette::{bail, Result}; 9 | /// # 10 | /// # fn has_permission(user: usize, resource: usize) -> bool { 11 | /// # true 12 | /// # } 13 | /// # 14 | /// # fn main() -> Result<()> { 15 | /// # let user = 0; 16 | /// # let resource = 0; 17 | /// # 18 | /// if !has_permission(user, resource) { 19 | #[cfg_attr( 20 | not(feature = "no-format-args-capture"), 21 | doc = r#" bail!("permission denied for accessing {resource}");"# 22 | )] 23 | #[cfg_attr( 24 | feature = "no-format-args-capture", 25 | doc = r#" bail!("permission denied for accessing {}", resource);"# 26 | )] 27 | /// } 28 | /// # Ok(()) 29 | /// # } 30 | /// ``` 31 | /// 32 | /// ``` 33 | /// # use miette::{bail, Result}; 34 | /// # use thiserror::Error; 35 | /// # 36 | /// # const MAX_DEPTH: usize = 1; 37 | /// # 38 | /// #[derive(Error, Debug)] 39 | /// enum ScienceError { 40 | /// #[error("recursion limit exceeded")] 41 | /// RecursionLimitExceeded, 42 | /// # #[error("...")] 43 | /// # More = (stringify! { 44 | /// ... 45 | /// # }, 1).1, 46 | /// } 47 | /// 48 | /// # fn main() -> Result<()> { 49 | /// # let depth = 0; 50 | /// # let err: &'static dyn std::error::Error = &ScienceError::RecursionLimitExceeded; 51 | /// # 52 | /// if depth > MAX_DEPTH { 53 | /// bail!(ScienceError::RecursionLimitExceeded); 54 | /// } 55 | /// # Ok(()) 56 | /// # } 57 | /// ``` 58 | /// 59 | /// ``` 60 | /// use miette::{bail, Result, Severity}; 61 | /// 62 | /// fn divide(x: f64, y: f64) -> Result { 63 | /// if y.abs() < 1e-3 { 64 | /// bail!( 65 | /// severity = Severity::Warning, 66 | #[cfg_attr( 67 | not(feature = "no-format-args-capture"), 68 | doc = r#" "dividing by value ({y}) close to 0""# 69 | )] 70 | #[cfg_attr( 71 | feature = "no-format-args-capture", 72 | doc = r#" "dividing by value ({}) close to 0", y"# 73 | )] 74 | /// ); 75 | /// } 76 | /// Ok(x / y) 77 | /// } 78 | /// ``` 79 | #[macro_export] 80 | macro_rules! bail { 81 | ($($key:ident = $value:expr_2021,)* $fmt:literal $($arg:tt)*) => { 82 | return $crate::private::Err( 83 | $crate::miette!($($key = $value,)* $fmt $($arg)*) 84 | ); 85 | }; 86 | ($err:expr_2021 $(,)?) => { 87 | return $crate::private::Err($crate::miette!($err)); 88 | }; 89 | } 90 | 91 | /// Return early with an error if a condition is not satisfied. 92 | /// 93 | /// This macro is equivalent to `if !$cond { return Err(From::from($err)); }`. 94 | /// 95 | /// Analogously to `assert!`, `ensure!` takes a condition and exits the function 96 | /// if the condition fails. Unlike `assert!`, `ensure!` returns an `Error` 97 | /// rather than panicking. 98 | /// 99 | /// # Example 100 | /// 101 | /// ``` 102 | /// # use miette::{ensure, Result}; 103 | /// # 104 | /// # fn main() -> Result<()> { 105 | /// # let user = 0; 106 | /// # 107 | /// ensure!(user == 0, "only user 0 is allowed"); 108 | /// # Ok(()) 109 | /// # } 110 | /// ``` 111 | /// 112 | /// ``` 113 | /// # use miette::{ensure, Result}; 114 | /// # use thiserror::Error; 115 | /// # 116 | /// # const MAX_DEPTH: usize = 1; 117 | /// # 118 | /// #[derive(Error, Debug)] 119 | /// enum ScienceError { 120 | /// #[error("recursion limit exceeded")] 121 | /// RecursionLimitExceeded, 122 | /// # #[error("...")] 123 | /// # More = (stringify! { 124 | /// ... 125 | /// # }, 1).1, 126 | /// } 127 | /// 128 | /// # fn main() -> Result<()> { 129 | /// # let depth = 0; 130 | /// # 131 | /// ensure!(depth <= MAX_DEPTH, ScienceError::RecursionLimitExceeded); 132 | /// # Ok(()) 133 | /// # } 134 | /// ``` 135 | /// 136 | /// ``` 137 | /// use miette::{ensure, Result, Severity}; 138 | /// 139 | /// fn divide(x: f64, y: f64) -> Result { 140 | /// ensure!( 141 | /// y.abs() >= 1e-3, 142 | /// severity = Severity::Warning, 143 | #[cfg_attr( 144 | not(feature = "no-format-args-capture"), 145 | doc = r#" "dividing by value ({y}) close to 0""# 146 | )] 147 | #[cfg_attr( 148 | feature = "no-format-args-capture", 149 | doc = r#" "dividing by value ({}) close to 0", y"# 150 | )] 151 | /// ); 152 | /// Ok(x / y) 153 | /// } 154 | /// ``` 155 | #[macro_export] 156 | macro_rules! ensure { 157 | ($cond:expr_2021, $($key:ident = $value:expr_2021,)* $fmt:literal $($arg:tt)*) => { 158 | if !$cond { 159 | return $crate::private::Err( 160 | $crate::miette!($($key = $value,)* $fmt $($arg)*) 161 | ); 162 | } 163 | }; 164 | ($cond:expr_2021, $err:expr_2021 $(,)?) => { 165 | if !$cond { 166 | return $crate::private::Err($crate::miette!($err)); 167 | } 168 | }; 169 | } 170 | 171 | /// Construct an ad-hoc [`Report`]. 172 | /// 173 | /// # Examples 174 | /// 175 | /// With string literal and interpolation: 176 | /// ``` 177 | /// # use miette::miette; 178 | /// let x = 1; 179 | /// let y = 2; 180 | #[cfg_attr( 181 | not(feature = "no-format-args-capture"), 182 | doc = r#"let report = miette!("{x} + {} = {z}", y, z = x + y);"# 183 | )] 184 | #[cfg_attr( 185 | feature = "no-format-args-capture", 186 | doc = r#"let report = miette!("{} + {} = {z}", x, y, z = x + y);"# 187 | )] 188 | /// 189 | /// assert_eq!(report.to_string().as_str(), "1 + 2 = 3"); 190 | /// 191 | /// let z = x + y; 192 | #[cfg_attr( 193 | not(feature = "no-format-args-capture"), 194 | doc = r#"let report = miette!("{x} + {y} = {z}");"# 195 | )] 196 | #[cfg_attr( 197 | feature = "no-format-args-capture", 198 | doc = r#"let report = miette!("{} + {} = {}", x, y, z);"# 199 | )] 200 | /// assert_eq!(report.to_string().as_str(), "1 + 2 = 3"); 201 | /// ``` 202 | /// 203 | /// With [`diagnostic!`]-like arguments: 204 | /// ``` 205 | /// use miette::{miette, LabeledSpan, Severity}; 206 | /// 207 | /// let source = "(2 + 2".to_string(); 208 | /// let report = miette!( 209 | /// // Those fields are optional 210 | /// severity = Severity::Error, 211 | /// code = "expected::rparen", 212 | /// help = "always close your parens", 213 | /// labels = vec![LabeledSpan::at_offset(6, "here")], 214 | /// url = "https://example.com", 215 | /// // Rest of the arguments are passed to `format!` 216 | /// // to form diagnostic message 217 | /// "expected closing ')'" 218 | /// ) 219 | /// .with_source_code(source); 220 | /// ``` 221 | /// 222 | /// ## `anyhow`/`eyre` Users 223 | /// 224 | /// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`. 225 | /// 226 | /// [`diagnostic!`]: crate::diagnostic! 227 | /// [`Report`]: crate::Report 228 | #[macro_export] 229 | macro_rules! miette { 230 | ($($key:ident = $value:expr_2021,)* $fmt:literal $($arg:tt)*) => { 231 | $crate::Report::from( 232 | $crate::diagnostic!($($key = $value,)* $fmt $($arg)*) 233 | ) 234 | }; 235 | ($err:expr_2021 $(,)?) => ({ 236 | use $crate::private::kind::*; 237 | let error = $err; 238 | (&error).miette_kind().new(error) 239 | }); 240 | } 241 | 242 | /// Construct a [`MietteDiagnostic`] in more user-friendly way. 243 | /// 244 | /// # Examples 245 | /// ``` 246 | /// use miette::{diagnostic, LabeledSpan, Severity}; 247 | /// 248 | /// let source = "(2 + 2".to_string(); 249 | /// let diag = diagnostic!( 250 | /// // Those fields are optional 251 | /// severity = Severity::Error, 252 | /// code = "expected::rparen", 253 | /// help = "always close your parens", 254 | /// labels = vec![LabeledSpan::at_offset(6, "here")], 255 | /// url = "https://example.com", 256 | /// // Rest of the arguments are passed to `format!` 257 | /// // to form diagnostic message 258 | /// "expected closing ')'", 259 | /// ); 260 | /// ``` 261 | /// Diagnostic without any fields: 262 | /// ``` 263 | /// # use miette::diagnostic; 264 | /// let x = 1; 265 | /// let y = 2; 266 | /// 267 | #[cfg_attr( 268 | not(feature = "no-format-args-capture"), 269 | doc = r#" let diag = diagnostic!("{x} + {} = {z}", y, z = x + y);"# 270 | )] 271 | #[cfg_attr( 272 | feature = "no-format-args-capture", 273 | doc = r#" let diag = diagnostic!("{} + {} = {z}", x, y, z = x + y);"# 274 | )] 275 | /// assert_eq!(diag.message, "1 + 2 = 3"); 276 | /// 277 | /// let z = x + y; 278 | #[cfg_attr( 279 | not(feature = "no-format-args-capture"), 280 | doc = r#"let diag = diagnostic!("{x} + {y} = {z}");"# 281 | )] 282 | #[cfg_attr( 283 | feature = "no-format-args-capture", 284 | doc = r#"let diag = diagnostic!("{} + {} = {}", x, y, z);"# 285 | )] 286 | /// assert_eq!(diag.message, "1 + 2 = 3"); 287 | /// ``` 288 | /// 289 | /// [`MietteDiagnostic`]: crate::MietteDiagnostic 290 | #[macro_export] 291 | macro_rules! diagnostic { 292 | ($fmt:literal $($arg:tt)*) => {{ 293 | $crate::MietteDiagnostic::new(format!($fmt $($arg)*)) 294 | }}; 295 | ($($key:ident = $value:expr_2021,)+ $fmt:literal $($arg:tt)*) => {{ 296 | let mut diag = $crate::MietteDiagnostic::new(format!($fmt $($arg)*)); 297 | $(diag.$key = Some($value.into());)* 298 | diag 299 | }}; 300 | } 301 | -------------------------------------------------------------------------------- /src/handlers/theme.rs: -------------------------------------------------------------------------------- 1 | use std::{env, io::IsTerminal}; 2 | 3 | use owo_colors::Style; 4 | 5 | /** 6 | Theme used by [`GraphicalReportHandler`](crate::GraphicalReportHandler) to 7 | render fancy [`Diagnostic`](crate::Diagnostic) reports. 8 | 9 | A theme consists of two things: the set of characters to be used for drawing, 10 | and the 11 | [`owo_colors::Style`](https://docs.rs/owo-colors/latest/owo_colors/struct.Style.html)s to be used to paint various items. 12 | 13 | You can create your own custom graphical theme using this type, or you can use 14 | one of the predefined ones using the methods below. 15 | */ 16 | #[derive(Debug, Clone)] 17 | pub struct GraphicalTheme { 18 | /// Characters to be used for drawing. 19 | pub characters: ThemeCharacters, 20 | /// Styles to be used for painting. 21 | pub styles: ThemeStyles, 22 | } 23 | 24 | fn force_color() -> bool { 25 | // Assume CI can always print colors. 26 | env::var("CI").is_ok() || env::var("FORCE_COLOR").is_ok_and(|env| env != "0") 27 | } 28 | 29 | impl Default for GraphicalTheme { 30 | fn default() -> Self { 31 | if force_color() { 32 | return Self::unicode(); 33 | } 34 | match std::env::var("NO_COLOR") { 35 | _ if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() => { 36 | Self::none() 37 | } 38 | Ok(string) if string != "0" => Self::unicode_nocolor(), 39 | _ => Self::unicode(), 40 | } 41 | } 42 | } 43 | 44 | impl GraphicalTheme { 45 | pub fn new(is_terminal: bool) -> Self { 46 | if force_color() { 47 | return Self::unicode(); 48 | } 49 | match std::env::var("NO_COLOR") { 50 | _ if !is_terminal => Self::none(), 51 | Ok(string) if string != "0" => Self::unicode_nocolor(), 52 | _ => Self::unicode(), 53 | } 54 | } 55 | 56 | /// ASCII-art-based graphical drawing, with ANSI styling. 57 | pub fn ascii() -> Self { 58 | Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::ansi() } 59 | } 60 | 61 | /// Graphical theme that draws using both ansi colors and unicode 62 | /// characters. 63 | /// 64 | /// Note that full rgb colors aren't enabled by default because they're 65 | /// an accessibility hazard, especially in the context of terminal themes 66 | /// that can change the background color and make hardcoded colors illegible. 67 | /// Such themes typically remap ansi codes properly, treating them more 68 | /// like CSS classes than specific colors. 69 | pub fn unicode() -> Self { 70 | Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::rgb() } 71 | } 72 | 73 | /// Graphical theme that draws in monochrome, while still using unicode 74 | /// characters. 75 | pub fn unicode_nocolor() -> Self { 76 | Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::none() } 77 | } 78 | 79 | /// A "basic" graphical theme that skips colors and unicode characters and 80 | /// just does monochrome ascii art. If you want a completely non-graphical 81 | /// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out 82 | /// [`NarratableReportHandler`](crate::NarratableReportHandler), or write 83 | /// your own [`ReportHandler`](crate::ReportHandler) 84 | pub fn none() -> Self { 85 | Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::none() } 86 | } 87 | } 88 | 89 | /** 90 | Styles for various parts of graphical rendering for the 91 | [`GraphicalReportHandler`](crate::GraphicalReportHandler). 92 | */ 93 | #[derive(Debug, Clone)] 94 | pub struct ThemeStyles { 95 | /// Style to apply to things highlighted as "error". 96 | pub error: Style, 97 | /// Style to apply to things highlighted as "warning". 98 | pub warning: Style, 99 | /// Style to apply to things highlighted as "advice". 100 | pub advice: Style, 101 | /// Style to apply to the help text. 102 | pub help: Style, 103 | /// Style to apply to filenames/links/URLs. 104 | pub link: Style, 105 | /// Style to apply to line numbers. 106 | pub linum: Style, 107 | /// Styles to cycle through (using `.iter().cycle()`), to render the lines 108 | /// and text for diagnostic highlights. 109 | pub highlights: Vec