diff --git a/Cargo.toml b/Cargo.toml index c6c169a..b93f137 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,10 @@ edition = "2018" [dependencies] indenter = "0.3.3" thiserror = "1.0.26" +miette-derive = { version = "=0.1.0", path = "miette-derive" } [dev-dependencies] thiserror = "1.0.26" + +[workspace] +members = ["miette-derive"] diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml new file mode 100644 index 0000000..27b0c28 --- /dev/null +++ b/miette-derive/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "miette-derive" +version = "0.1.0" +authors = ["Kat Marchán "] +edition = "2018" +license = "Apache-2.0" +description = "Derive macros for miette. Like `thiserror` for Diagnostics." +repository = "https://github.com/zkat/miette" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = "1.0.45" +darling = "0.13.0" diff --git a/miette-derive/LICENSE b/miette-derive/LICENSE new file mode 100644 index 0000000..545f464 --- /dev/null +++ b/miette-derive/LICENSE @@ -0,0 +1,229 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ TERMS +AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + + + + "License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + + + + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + + + + "Legal Entity" shall mean the +union of the acting entity and all other entities that control, are controlled +by, or are under common control with that entity. For the purposes of this +definition, "control" means (i) the power, direct or indirect, to cause the +direction or management of such entity, whether by contract or otherwise, or (ii) +ownership of fifty percent (50%) or more of the outstanding shares, or (iii) +beneficial ownership of such entity. + + + + "You" (or "Your") shall mean +an individual or Legal Entity exercising permissions granted by this License. + + + + + "Source" form shall mean the preferred form for making modifications, +including but not limited to software source code, documentation source, and +configuration files. + + + + "Object" form shall mean any form resulting +from mechanical transformation or translation of a Source form, including but not +limited to compiled object code, generated documentation, and conversions to +other media types. + + + + "Work" shall mean the work of authorship, +whether in Source or Object form, made available under the License, as indicated +by a copyright notice that is included in or attached to the work (an example is +provided in the Appendix below). + + + + "Derivative Works" shall mean any +work, whether in Source or Object form, that is based on (or derived from) the +Work and for which the editorial revisions, annotations, elaborations, or other +modifications represent, as a whole, an original work of authorship. For the +purposes of this License, Derivative Works shall not include works that remain +separable from, or merely link (or bind by name) to the interfaces of, the Work +and Derivative Works thereof. + + + + "Contribution" shall mean any work +of authorship, including the original version of the Work and any modifications +or additions to that Work or Derivative Works thereof, that is intentionally +submitted to Licensor for inclusion in the Work by the copyright owner or by an +individual or Legal Entity authorized to submit on behalf of the copyright owner. +For the purposes of this definition, "submitted" means any form of electronic, +verbal, or written communication sent to the Licensor or its representatives, +including but not limited to communication on electronic mailing lists, source +code control systems, and issue tracking systems that are managed by, or on +behalf of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in +writing by the copyright owner as "Not a Contribution." + + + + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of +whom a Contribution has been received by Licensor and subsequently incorporated +within the Work. + + 2. Grant of Copyright License. Subject to the terms and +conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license +to reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or Object +form. + + 3. Grant of Patent License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, import, and +otherwise transfer the Work, where such license applies only to those patent +claims licensable by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) with the Work to +which such Contribution(s) was submitted. If You institute patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Work or a Contribution incorporated within the Work constitutes +direct or contributory patent infringement, then any patent licenses granted to +You under this License for that Work shall terminate as of the date such +litigation is filed. + + 4. Redistribution. You may reproduce and distribute +copies of the Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You meet the following +conditions: + + (a) You must give any other recipients of the Work or +Derivative Works a copy of this License; and + + (b) You must cause any +modified files to carry prominent notices stating that You changed the files; +and + + (c) You must retain, in the Source form of any Derivative Works that +You distribute, all copyright, patent, trademark, and attribution notices from +the Source form of the Work, excluding those notices that do not pertain to any +part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text +file as part of its distribution, then any Derivative Works that You distribute +must include a readable copy of the attribution notices contained within such +NOTICE file, excluding those notices that do not pertain to any part of the +Derivative Works, in at least one of the following places: within a NOTICE text +file distributed as part of the Derivative Works; within the Source form or +documentation, if provided along with the Derivative Works; or, within a display +generated by the Derivative Works, if and wherever such third-party notices +normally appear. The contents of the NOTICE file are for informational purposes +only and do not modify the License. You may add Your own attribution notices +within Derivative Works that You distribute, alongside or as an addendum to the +NOTICE text from the Work, provided that such additional attribution notices +cannot be construed as modifying the License. + + You may add Your own +copyright statement to Your modifications and may provide additional or different +license terms and conditions for use, reproduction, or distribution of Your +modifications, or for any such Derivative Works as a whole, provided Your use, +reproduction, and distribution of the Work otherwise complies with the conditions +stated in this License. + + 5. Submission of Contributions. Unless You explicitly +state otherwise, any Contribution intentionally submitted for inclusion in the +Work by You to the Licensor shall be under the terms and conditions of this +License, without any additional terms or conditions. Notwithstanding the above, +nothing herein shall supersede or modify the terms of any separate license +agreement you may have executed with Licensor regarding such Contributions. + + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless +required by applicable law or agreed to in writing, Licensor provides the Work +(and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, +without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, +MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible +for determining the appropriateness of using or redistributing the Work and +assume any risks associated with Your exercise of permissions under this +License. + + 8. Limitation of Liability. In no event and under no legal theory, +whether in tort (including negligence), contract, or otherwise, unless required +by applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any character +arising as a result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, work stoppage, +computer failure or malfunction, or any and all other commercial damages or +losses), even if such Contributor has been advised of the possibility of such +damages. + + 9. Accepting Warranty or Additional Liability. While redistributing +the Work or Derivative Works thereof, You may choose to offer, and charge a fee +for, acceptance of support, warranty, indemnity, or other liability obligations +and/or rights consistent with this License. However, in accepting such +obligations, You may act only on Your own behalf and on Your sole responsibility, +not on behalf of any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability incurred by, or +claims asserted against, such Contributor by reason of your accepting any such +warranty or additional liability. END OF TERMS AND CONDITIONS + +APPENDIX: How to +apply the Apache License to your work. + +To apply the Apache License to your work, +attach the following boilerplate notice, with the fields enclosed by brackets +"[]" replaced with your own identifying information. (Don't include the +brackets!) The text should be enclosed in the appropriate comment syntax for the +file format. We also recommend that a file or class name and description of +purpose be included on the same "printed page" as the copyright notice for easier +identification within third-party archives. + +Copyright [yyyy] Kat +Marchán + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may +not use this file except in compliance with the License. + +You may obtain a copy +of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by +applicable law or agreed to in writing, software + +distributed under the License +is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. + +See the License for the specific language +governing permissions and + +limitations under the License. \ No newline at end of file diff --git a/miette-derive/src/code.rs b/miette-derive/src/code.rs new file mode 100644 index 0000000..ebed1ae --- /dev/null +++ b/miette-derive/src/code.rs @@ -0,0 +1,80 @@ +use std::fmt::Display; + +use darling::{ast::Fields, error::Error as DarlingError, FromMeta}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Lit, Meta, NestedMeta}; + +use crate::{Diagnostic, DiagnosticField, DiagnosticVariant}; + +#[derive(Debug)] +pub struct Code(String); + +impl Display for Code { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromMeta for Code { + fn from_string(arg: &str) -> Result { + Ok(Code(arg.into())) + } + + fn from_list(items: &[NestedMeta]) -> Result { + match &items[0] { + NestedMeta::Meta(Meta::Path(p)) => Ok(Code( + p.segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"), + )), + NestedMeta::Lit(Lit::Str(code)) => Ok(Code(code.value())), + _ => Err(DarlingError::custom( + "invalid code format. Only path::style and string literals are accepted", + )), + } + } +} + +impl Code { + pub(crate) fn gen_enum( + _diag: &Diagnostic, + variants: &[&DiagnosticVariant], + ) -> Option { + let code_pairs = variants.iter().map( + |DiagnosticVariant { + ref ident, + ref code, + .. + }| { + let code = code.to_string(); + quote! { Self::#ident => std::boxed::Box::new(#code), } + }, + ); + Some(quote! { + fn code<'a>(&'a self) -> std::boxed::Box { + match self { + #(#code_pairs)* + } + } + }) + } + + pub(crate) fn gen_struct( + diag: &Diagnostic, + _fields: &Fields<&DiagnosticField>, + ) -> Option { + let code = diag + .code + .as_ref() + .expect("`code` attribute is required for diagnostics.") + .to_string(); + Some(quote! { + fn code<'a>(&'a self) -> std::boxed::Box { + std::boxed::Box::new(#code) + } + }) + } +} diff --git a/miette-derive/src/help.rs b/miette-derive/src/help.rs new file mode 100644 index 0000000..c84b053 --- /dev/null +++ b/miette-derive/src/help.rs @@ -0,0 +1,93 @@ +use darling::{ast::Fields, error::Error as DarlingError, FromMeta}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Lit, NestedMeta}; + +use crate::{Diagnostic, DiagnosticField, DiagnosticVariant}; + +#[derive(Debug)] +pub struct Help { + pub fmt: String, + pub args: Vec, +} + +impl FromMeta for Help { + fn from_string(arg: &str) -> Result { + Ok(Help { + fmt: arg.into(), + args: Vec::new(), + }) + } + + fn from_list(items: &[NestedMeta]) -> Result { + match &items.get(0) { + Some(NestedMeta::Lit(Lit::Str(fmt))) => Ok(Help { + fmt: fmt.value(), + args: items[1..] + .iter() + .map(|item| match item { + NestedMeta::Meta(_) => Err(DarlingError::custom( + "Only literals are supported for now. Sorry :(" + )), + NestedMeta::Lit(_) => Ok(item.clone()), + }) + .collect::, DarlingError>>()?, + }), + None => Err(DarlingError::custom("Help format string is required")), + _ => Err(DarlingError::custom( + "First argument must be a literal format string", + )), + } + } +} + +impl Help { + pub(crate) fn gen_enum( + _diag: &Diagnostic, + variants: &[&DiagnosticVariant], + ) -> Option { + let help_pairs = variants + .iter() + .filter(|v| v.help.is_some()) + .map( + |DiagnosticVariant { + ref ident, + ref help, + .. + }| { + let help = &help.as_ref().unwrap(); + let fmt = &help.fmt; + let args = help.args.iter().map(|arg| quote! { #arg, }); + quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))), } + }, + ) + .collect::>(); + if help_pairs.is_empty() { + None + } else { + Some(quote! { + fn help<'a>(&'a self) -> std::option::Option> { + match self { + #(#help_pairs)* + _ => None, + } + } + }) + } + } + + pub(crate) fn gen_struct( + diag: &Diagnostic, + _fields: &Fields<&DiagnosticField>, + ) -> Option { + diag.help.as_ref().map(|h| { + let fmt = &h.fmt; + let args = &h.args; + quote! { + fn help<'a>(&'a self) -> std::option::Option> { + std::option::Option::Some(std::boxed::Box::new(format!(#fmt, #(#args),*))) + } + } + }) + } +} diff --git a/miette-derive/src/lib.rs b/miette-derive/src/lib.rs new file mode 100644 index 0000000..665ab53 --- /dev/null +++ b/miette-derive/src/lib.rs @@ -0,0 +1,101 @@ +use darling::{ + ast::{self, Fields}, + FromDeriveInput, FromField, FromVariant, ToTokens, +}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +use code::Code; +use help::Help; +use severity::Severity; + +mod code; +mod help; +mod severity; + +#[proc_macro_derive(Diagnostic, attributes(diagnostic))] +pub fn derive_diagnostic(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let cmd = match Diagnostic::from_derive_input(&input) { + Ok(cmd) => cmd, + Err(err) => return err.write_errors().into(), + }; + // panic!("{:#}", cmd.to_token_stream()); + quote!(#cmd).into() +} + +#[derive(Debug, FromDeriveInput)] +#[darling(supports(any), attributes(diagnostic))] +struct Diagnostic { + ident: syn::Ident, + data: ast::Data, + generics: syn::Generics, + #[darling(default)] + code: Option, + #[darling(default)] + severity: Option, + #[darling(default)] + help: Option, +} + +#[derive(Debug, FromField)] +struct DiagnosticField { + ident: Option, + ty: syn::Type, +} + +#[derive(Debug, FromVariant)] +#[darling(attributes(diagnostic))] +struct DiagnosticVariant { + ident: syn::Ident, + code: Code, + #[darling(default)] + severity: Option, + #[darling(default)] + help: Option, +} + +impl ToTokens for Diagnostic { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ts = match self.data.as_ref() { + ast::Data::Enum(variants) => self.gen_enum(variants), + ast::Data::Struct(fields) => self.gen_struct(fields), + }; + tokens.extend(ts); + } +} + +impl Diagnostic { + fn gen_enum(&self, variants: Vec<&DiagnosticVariant>) -> TokenStream { + let ident = &self.ident; + let (impl_generics, ty_generics, where_clause) = &self.generics.split_for_impl(); + let code_body = Code::gen_enum(self, &variants); + let help_body = Help::gen_enum(self, &variants); + let sev_body = Severity::gen_enum(self, &variants); + + quote! { + impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { + #code_body + #help_body + #sev_body + } + } + } + + fn gen_struct(&self, fields: Fields<&DiagnosticField>) -> TokenStream { + let ident= &self.ident; + let (impl_generics, ty_generics, where_clause) = &self.generics.split_for_impl(); + let code_body = Code::gen_struct(self, &fields); + let help_body = Help::gen_struct(self, &fields); + let sev_body = Severity::gen_struct(self, &fields); + + quote! { + impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause { + #code_body + #help_body + #sev_body + } + } + } +} diff --git a/miette-derive/src/severity.rs b/miette-derive/src/severity.rs new file mode 100644 index 0000000..754425e --- /dev/null +++ b/miette-derive/src/severity.rs @@ -0,0 +1,68 @@ +use darling::{ast::Fields, error::Error as DarlingError, FromMeta}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{Lit, LitStr, Meta, NestedMeta, Path}; + +use crate::{Diagnostic, DiagnosticField, DiagnosticVariant}; + +#[derive(Debug)] +pub struct Severity(pub Path); + +impl FromMeta for Severity { + fn from_string(arg: &str) -> Result { + Ok(Severity(LitStr::new(arg, Span::call_site()).parse()?)) + } + + fn from_list(items: &[NestedMeta]) -> Result { + match &items[0] { + NestedMeta::Meta(Meta::Path(p)) => Ok(Severity(p.clone())), + NestedMeta::Lit(Lit::Str(sev)) => Ok(Severity(sev.parse()?)), + _ => Err(DarlingError::custom( + "invalid severity format. Only literal names and string literals are accepted", + )), + } + } +} + +impl Severity { + pub(crate) fn gen_enum( + _diag: &Diagnostic, + variants: &[&DiagnosticVariant], + ) -> Option { + let sev_pairs = variants + .iter() + .filter(|v| v.severity.is_some()) + .map( + |DiagnosticVariant { + ident, severity, .. + }| { + let severity = &severity.as_ref().unwrap().0; + quote! { Self::#ident => std::option::Option::Some(miette::Severity::#severity), } + }, + ) + .collect::>(); + if sev_pairs.is_empty() { + None + } else { + Some(quote! { + fn severity(&self) -> std::option::Option { + match self { + #(#sev_pairs)* + _ => None, + } + } + }) + } + } + + pub(crate) fn gen_struct(diag: &Diagnostic, _fields: &Fields<&DiagnosticField>) -> Option { + diag.severity.as_ref().map(|sev| { + let sev = &sev.0; + quote! { + fn severity(&self) -> std::option::Option { + Some(miette::Severity::#sev) + } + } + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2e9ef49..1400894 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ #![doc = include_str!("../README.md")] +pub use miette_derive::*; + pub use error::*; pub use protocol::*; pub use reporter::*; diff --git a/src/protocol.rs b/src/protocol.rs index 2a5f88c..77747f1 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -91,7 +91,7 @@ pub trait DiagnosticReporter: core::any::Any + Send + Sync { [Diagnostic] severity. Intended to be used by [DiagnosticReporter] to change the way different Diagnostics are displayed. */ -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Severity { /// Critical failure. The program cannot continue. Error, diff --git a/tests/derive.rs b/tests/derive.rs new file mode 100644 index 0000000..1a15c10 --- /dev/null +++ b/tests/derive.rs @@ -0,0 +1,156 @@ +use miette::{Diagnostic, Severity}; +use thiserror::Error; + +#[test] +fn basic_struct() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic( + code = "foo::bar::baz", + severity = "Error", + help = "try doing it better" + )] + struct Foo; + + assert_eq!("foo::bar::baz".to_string(), Foo.code().to_string()); + + assert_eq!(Some(Severity::Error), Foo.severity()); + + assert_eq!( + "try doing it better".to_string(), + Foo.help().unwrap().to_string() + ); +} + +#[test] +fn basic_enum() { + + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + enum Foo { + #[diagnostic( + code = "foo::x", + severity = "Warning", + help = "Try using Foo::Y instead" + )] + X, + #[diagnostic(code = "foo::y")] + Y, + } + + assert_eq!("foo::x".to_string(), Foo::X.code().to_string()); + assert_eq!("foo::y".to_string(), Foo::Y.code().to_string()); + + assert_eq!(Some(Severity::Warning), Foo::X.severity()); + assert_eq!(None, Foo::Y.severity()); +} + +#[test] +fn path_code() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic(code(foo::bar::baz))] + struct FooStruct; + + assert_eq!("foo::bar::baz".to_string(), FooStruct.code().to_string()); + + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + enum FooEnum { + #[diagnostic(code(foo::x))] + X, + } + + assert_eq!("foo::x".to_string(), FooEnum::X.code().to_string()); +} + +#[test] +fn path_severity() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic( + code(foo::bar::baz), + severity(Warning) + )] + struct FooStruct; + + assert_eq!(Some(Severity::Warning), FooStruct.severity()); + + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + enum FooEnum { + #[diagnostic( + code(foo::x), + severity(Warning), + )] + X, + } + + assert_eq!(Some(Severity::Warning), FooEnum::X.severity()); +} + +#[test] +fn list_help() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic( + code(foo::bar::baz), + help("try doing it better"), + )] + struct FooStruct; + + assert_eq!( + "try doing it better".to_string(), + FooStruct.help().unwrap().to_string() + ); + + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + enum FooEnum { + #[diagnostic( + code(foo::x), + help("try doing it better"), + )] + X, + } + + assert_eq!( + "try doing it better".to_string(), + FooEnum::X.help().unwrap().to_string() + ); +} + +// TODO: Darling doesn't support this, apparently: +// https://github.com/TedDriggs/darling/issues/145 +/* +#[test] +fn fmt_help() { + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + #[diagnostic( + code(foo::bar::baz), + help("{} {}", 1, "bar"), + )] + struct FooStruct; + + assert_eq!( + "1 bar".to_string(), + FooStruct.help().unwrap().to_string() + ); + + #[derive(Debug, Diagnostic, Error)] + #[error("welp")] + enum FooEnum { + #[diagnostic( + code(foo::x), + help("{} {}", 1, "bar"), + )] + X, + } + + assert_eq!( + "1 bar".to_string(), + FooEnum::X.help().unwrap().to_string() + ); +} +*/