summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Maurer <mmaurer@google.com>2023-06-26 23:37:40 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-06-26 23:37:40 +0000
commitcf18a0d3a3b53ce574de2906a5f82f89a77f1888 (patch)
treec82295365488871209366131d0b6d3708991d841
parent5d5fc11f1e70fa2ef447cf14a8f6f5acae51d2b4 (diff)
parentb980cfea52a3e39940ec28c531f7e1d230f8c28a (diff)
downloadmiette-derive-cf18a0d3a3b53ce574de2906a5f82f89a77f1888.tar.gz
Initial import of miette-derive-5.9.0 am: 437d15e784 am: af7096b4ab am: 330b5b06b3 am: b980cfea52
Original change: https://android-review.googlesource.com/c/platform/external/rust/crates/miette-derive/+/2638911 Change-Id: I426625c5d97881803cd0a0fd82892ecc5f5ac5b8 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--Cargo.toml31
-rw-r--r--Cargo.toml.orig16
-rw-r--r--LICENSE229
-rw-r--r--METADATA19
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS1
-rw-r--r--src/code.rs80
-rw-r--r--src/diagnostic.rs397
-rw-r--r--src/diagnostic_arg.rs42
-rw-r--r--src/diagnostic_source.rs78
-rw-r--r--src/fmt.rs235
-rw-r--r--src/forward.rs161
-rw-r--r--src/help.rs146
-rw-r--r--src/label.rs207
-rw-r--r--src/lib.rs32
-rw-r--r--src/related.rs79
-rw-r--r--src/severity.rs89
-rw-r--r--src/source_code.rs81
-rw-r--r--src/url.rs139
-rw-r--r--src/utils.rs140
21 files changed, 2208 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..767945a
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "91e5f5b7e347b921921bac7135c56c600abb1fab"
+ },
+ "path_in_vcs": "miette-derive"
+} \ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..da6ab4c
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,31 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2018"
+name = "miette-derive"
+version = "5.9.0"
+authors = ["Kat Marchán <kzm@zkat.tech>"]
+description = "Derive macros for miette. Like `thiserror` for Diagnostics."
+license = "Apache-2.0"
+repository = "https://github.com/zkat/miette"
+
+[lib]
+proc-macro = true
+
+[dependencies.proc-macro2]
+version = "1.0"
+
+[dependencies.quote]
+version = "1.0"
+
+[dependencies.syn]
+version = "2.0.11"
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..461a328
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,16 @@
+[package]
+name = "miette-derive"
+version = "5.9.0"
+authors = ["Kat Marchán <kzm@zkat.tech>"]
+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 = "2.0.11"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..10fa5cc
--- /dev/null
+++ b/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/METADATA b/METADATA
new file mode 100644
index 0000000..c8c8ea7
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,19 @@
+name: "miette-derive"
+description: "Derive macros for miette. Like `thiserror` for Diagnostics."
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://crates.io/crates/miette-derive"
+ }
+ url {
+ type: ARCHIVE
+ value: "https://static.crates.io/crates/miette-derive/miette-derive-5.9.0.crate"
+ }
+ version: "5.9.0"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 6
+ day: 12
+ }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..45dc4dd
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:master:/OWNERS
diff --git a/src/code.rs b/src/code.rs
new file mode 100644
index 0000000..22dc795
--- /dev/null
+++ b/src/code.rs
@@ -0,0 +1,80 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{
+ parenthesized,
+ parse::{Parse, ParseStream},
+ Token,
+};
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ forward::WhichFn,
+ utils::gen_all_variants_with,
+};
+
+#[derive(Debug)]
+pub struct Code(pub String);
+
+impl Parse for Code {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let ident = input.parse::<syn::Ident>()?;
+ if ident == "code" {
+ let la = input.lookahead1();
+ if la.peek(syn::token::Paren) {
+ let content;
+ parenthesized!(content in input);
+ let la = content.lookahead1();
+ if la.peek(syn::LitStr) {
+ let str = content.parse::<syn::LitStr>()?;
+ Ok(Code(str.value()))
+ } else {
+ let path = content.parse::<syn::Path>()?;
+ Ok(Code(
+ path.segments
+ .iter()
+ .map(|s| s.ident.to_string())
+ .collect::<Vec<_>>()
+ .join("::"),
+ ))
+ }
+ } else {
+ input.parse::<Token![=]>()?;
+ Ok(Code(input.parse::<syn::LitStr>()?.value()))
+ }
+ } else {
+ Err(syn::Error::new(ident.span(), "diagnostic code is required. Use #[diagnostic(code = ...)] or #[diagnostic(code(...))] to define one."))
+ }
+ }
+}
+
+impl Code {
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::Code,
+ |ident, fields, DiagnosticConcreteArgs { code, .. }| {
+ let code = &code.as_ref()?.0;
+ Some(match fields {
+ syn::Fields::Named(_) => {
+ quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), }
+ }
+ syn::Fields::Unnamed(_) => {
+ quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), }
+ }
+ syn::Fields::Unit => {
+ quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), }
+ }
+ })
+ },
+ )
+ }
+
+ pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
+ let code = &self.0;
+ Some(quote! {
+ fn code(&self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>> {
+ std::option::Option::Some(std::boxed::Box::new(#code))
+ }
+ })
+ }
+}
diff --git a/src/diagnostic.rs b/src/diagnostic.rs
new file mode 100644
index 0000000..0173d2a
--- /dev/null
+++ b/src/diagnostic.rs
@@ -0,0 +1,397 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{punctuated::Punctuated, DeriveInput, Token};
+
+use crate::code::Code;
+use crate::diagnostic_arg::DiagnosticArg;
+use crate::diagnostic_source::DiagnosticSource;
+use crate::forward::{Forward, WhichFn};
+use crate::help::Help;
+use crate::label::Labels;
+use crate::related::Related;
+use crate::severity::Severity;
+use crate::source_code::SourceCode;
+use crate::url::Url;
+
+pub enum Diagnostic {
+ Struct {
+ generics: syn::Generics,
+ ident: syn::Ident,
+ fields: syn::Fields,
+ args: DiagnosticDefArgs,
+ },
+ Enum {
+ ident: syn::Ident,
+ generics: syn::Generics,
+ variants: Vec<DiagnosticDef>,
+ },
+}
+
+pub struct DiagnosticDef {
+ pub ident: syn::Ident,
+ pub fields: syn::Fields,
+ pub args: DiagnosticDefArgs,
+}
+
+pub enum DiagnosticDefArgs {
+ Transparent(Forward),
+ Concrete(Box<DiagnosticConcreteArgs>),
+}
+
+impl DiagnosticDefArgs {
+ pub(crate) fn forward_or_override_enum(
+ &self,
+ variant: &syn::Ident,
+ which_fn: WhichFn,
+ mut f: impl FnMut(&DiagnosticConcreteArgs) -> Option<TokenStream>,
+ ) -> Option<TokenStream> {
+ match self {
+ Self::Transparent(forward) => Some(forward.gen_enum_match_arm(variant, which_fn)),
+ Self::Concrete(concrete) => f(concrete).or_else(|| {
+ concrete
+ .forward
+ .as_ref()
+ .map(|forward| forward.gen_enum_match_arm(variant, which_fn))
+ }),
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct DiagnosticConcreteArgs {
+ pub code: Option<Code>,
+ pub severity: Option<Severity>,
+ pub help: Option<Help>,
+ pub labels: Option<Labels>,
+ pub source_code: Option<SourceCode>,
+ pub url: Option<Url>,
+ pub forward: Option<Forward>,
+ pub related: Option<Related>,
+ pub diagnostic_source: Option<DiagnosticSource>,
+}
+
+impl DiagnosticConcreteArgs {
+ fn for_fields(fields: &syn::Fields) -> Result<Self, syn::Error> {
+ let labels = Labels::from_fields(fields)?;
+ let source_code = SourceCode::from_fields(fields)?;
+ let related = Related::from_fields(fields)?;
+ let help = Help::from_fields(fields)?;
+ let diagnostic_source = DiagnosticSource::from_fields(fields)?;
+ Ok(DiagnosticConcreteArgs {
+ code: None,
+ help,
+ related,
+ severity: None,
+ labels,
+ url: None,
+ forward: None,
+ source_code,
+ diagnostic_source,
+ })
+ }
+
+ fn add_args(
+ &mut self,
+ attr: &syn::Attribute,
+ args: impl Iterator<Item = DiagnosticArg>,
+ errors: &mut Vec<syn::Error>,
+ ) {
+ for arg in args {
+ match arg {
+ DiagnosticArg::Transparent => {
+ errors.push(syn::Error::new_spanned(attr, "transparent not allowed"));
+ }
+ DiagnosticArg::Forward(to_field) => {
+ if self.forward.is_some() {
+ errors.push(syn::Error::new_spanned(
+ attr,
+ "forward has already been specified",
+ ));
+ }
+ self.forward = Some(to_field);
+ }
+ DiagnosticArg::Code(new_code) => {
+ if self.code.is_some() {
+ errors.push(syn::Error::new_spanned(
+ attr,
+ "code has already been specified",
+ ));
+ }
+ self.code = Some(new_code);
+ }
+ DiagnosticArg::Severity(sev) => {
+ if self.severity.is_some() {
+ errors.push(syn::Error::new_spanned(
+ attr,
+ "severity has already been specified",
+ ));
+ }
+ self.severity = Some(sev);
+ }
+ DiagnosticArg::Help(hl) => {
+ if self.help.is_some() {
+ errors.push(syn::Error::new_spanned(
+ attr,
+ "help has already been specified",
+ ));
+ }
+ self.help = Some(hl);
+ }
+ DiagnosticArg::Url(u) => {
+ if self.url.is_some() {
+ errors.push(syn::Error::new_spanned(
+ attr,
+ "url has already been specified",
+ ));
+ }
+ self.url = Some(u);
+ }
+ }
+ }
+ }
+}
+
+impl DiagnosticDefArgs {
+ fn parse(
+ _ident: &syn::Ident,
+ fields: &syn::Fields,
+ attrs: &[&syn::Attribute],
+ allow_transparent: bool,
+ ) -> syn::Result<Self> {
+ let mut errors = Vec::new();
+
+ // Handle the only condition where Transparent is allowed
+ if allow_transparent && attrs.len() == 1 {
+ if let Ok(args) =
+ attrs[0].parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated)
+ {
+ if matches!(args.first(), Some(DiagnosticArg::Transparent)) {
+ let forward = Forward::for_transparent_field(fields)?;
+ return Ok(Self::Transparent(forward));
+ }
+ }
+ }
+
+ // Create errors for any appearances of Transparent
+ let error_message = if allow_transparent {
+ "diagnostic(transparent) not allowed in combination with other args"
+ } else {
+ "diagnostic(transparent) not allowed here"
+ };
+ fn is_transparent(d: &DiagnosticArg) -> bool {
+ matches!(d, DiagnosticArg::Transparent)
+ }
+
+ let mut concrete = DiagnosticConcreteArgs::for_fields(fields)?;
+ for attr in attrs {
+ let args =
+ attr.parse_args_with(Punctuated::<DiagnosticArg, Token![,]>::parse_terminated);
+ let args = match args {
+ Ok(args) => args,
+ Err(error) => {
+ errors.push(error);
+ continue;
+ }
+ };
+
+ if args.iter().any(is_transparent) {
+ errors.push(syn::Error::new_spanned(attr, error_message));
+ }
+
+ let args = args
+ .into_iter()
+ .filter(|x| !matches!(x, DiagnosticArg::Transparent));
+
+ concrete.add_args(attr, args, &mut errors);
+ }
+
+ let combined_error = errors.into_iter().reduce(|mut lhs, rhs| {
+ lhs.combine(rhs);
+ lhs
+ });
+ if let Some(error) = combined_error {
+ Err(error)
+ } else {
+ Ok(DiagnosticDefArgs::Concrete(Box::new(concrete)))
+ }
+ }
+}
+
+impl Diagnostic {
+ pub fn from_derive_input(input: DeriveInput) -> Result<Self, syn::Error> {
+ let input_attrs = input
+ .attrs
+ .iter()
+ .filter(|x| x.path().is_ident("diagnostic"))
+ .collect::<Vec<&syn::Attribute>>();
+ Ok(match input.data {
+ syn::Data::Struct(data_struct) => {
+ let args = DiagnosticDefArgs::parse(
+ &input.ident,
+ &data_struct.fields,
+ &input_attrs,
+ true,
+ )?;
+
+ Diagnostic::Struct {
+ fields: data_struct.fields,
+ ident: input.ident,
+ generics: input.generics,
+ args,
+ }
+ }
+ syn::Data::Enum(syn::DataEnum { variants, .. }) => {
+ let mut vars = Vec::new();
+ for var in variants {
+ let mut variant_attrs = input_attrs.clone();
+ variant_attrs
+ .extend(var.attrs.iter().filter(|x| x.path().is_ident("diagnostic")));
+ let args =
+ DiagnosticDefArgs::parse(&var.ident, &var.fields, &variant_attrs, true)?;
+ vars.push(DiagnosticDef {
+ ident: var.ident,
+ fields: var.fields,
+ args,
+ });
+ }
+ Diagnostic::Enum {
+ ident: input.ident,
+ generics: input.generics,
+ variants: vars,
+ }
+ }
+ syn::Data::Union(_) => {
+ return Err(syn::Error::new(
+ input.ident.span(),
+ "Can't derive Diagnostic for Unions",
+ ))
+ }
+ })
+ }
+
+ pub fn gen(&self) -> TokenStream {
+ match self {
+ Self::Struct {
+ ident,
+ fields,
+ generics,
+ args,
+ } => {
+ let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
+ match args {
+ DiagnosticDefArgs::Transparent(forward) => {
+ let code_method = forward.gen_struct_method(WhichFn::Code);
+ let help_method = forward.gen_struct_method(WhichFn::Help);
+ let url_method = forward.gen_struct_method(WhichFn::Url);
+ let labels_method = forward.gen_struct_method(WhichFn::Labels);
+ let source_code_method = forward.gen_struct_method(WhichFn::SourceCode);
+ let severity_method = forward.gen_struct_method(WhichFn::Severity);
+ let related_method = forward.gen_struct_method(WhichFn::Related);
+ let diagnostic_source_method =
+ forward.gen_struct_method(WhichFn::DiagnosticSource);
+
+ quote! {
+ impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
+ #code_method
+ #help_method
+ #url_method
+ #labels_method
+ #severity_method
+ #source_code_method
+ #related_method
+ #diagnostic_source_method
+ }
+ }
+ }
+ DiagnosticDefArgs::Concrete(concrete) => {
+ let forward = |which| {
+ concrete
+ .forward
+ .as_ref()
+ .map(|fwd| fwd.gen_struct_method(which))
+ };
+ let code_body = concrete
+ .code
+ .as_ref()
+ .and_then(|x| x.gen_struct())
+ .or_else(|| forward(WhichFn::Code));
+ let help_body = concrete
+ .help
+ .as_ref()
+ .and_then(|x| x.gen_struct(fields))
+ .or_else(|| forward(WhichFn::Help));
+ let sev_body = concrete
+ .severity
+ .as_ref()
+ .and_then(|x| x.gen_struct())
+ .or_else(|| forward(WhichFn::Severity));
+ let rel_body = concrete
+ .related
+ .as_ref()
+ .and_then(|x| x.gen_struct())
+ .or_else(|| forward(WhichFn::Related));
+ let url_body = concrete
+ .url
+ .as_ref()
+ .and_then(|x| x.gen_struct(ident, fields))
+ .or_else(|| forward(WhichFn::Url));
+ let labels_body = concrete
+ .labels
+ .as_ref()
+ .and_then(|x| x.gen_struct(fields))
+ .or_else(|| forward(WhichFn::Labels));
+ let src_body = concrete
+ .source_code
+ .as_ref()
+ .and_then(|x| x.gen_struct(fields))
+ .or_else(|| forward(WhichFn::SourceCode));
+ let diagnostic_source = concrete
+ .diagnostic_source
+ .as_ref()
+ .and_then(|x| x.gen_struct())
+ .or_else(|| forward(WhichFn::DiagnosticSource));
+ quote! {
+ impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
+ #code_body
+ #help_body
+ #sev_body
+ #rel_body
+ #url_body
+ #labels_body
+ #src_body
+ #diagnostic_source
+ }
+ }
+ }
+ }
+ }
+ Self::Enum {
+ ident,
+ generics,
+ variants,
+ } => {
+ let (impl_generics, ty_generics, where_clause) = &generics.split_for_impl();
+ let code_body = Code::gen_enum(variants);
+ let help_body = Help::gen_enum(variants);
+ let sev_body = Severity::gen_enum(variants);
+ let labels_body = Labels::gen_enum(variants);
+ let src_body = SourceCode::gen_enum(variants);
+ let rel_body = Related::gen_enum(variants);
+ let url_body = Url::gen_enum(ident, variants);
+ let diagnostic_source_body = DiagnosticSource::gen_enum(variants);
+ quote! {
+ impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
+ #code_body
+ #help_body
+ #sev_body
+ #labels_body
+ #src_body
+ #rel_body
+ #url_body
+ #diagnostic_source_body
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/diagnostic_arg.rs b/src/diagnostic_arg.rs
new file mode 100644
index 0000000..bade6f0
--- /dev/null
+++ b/src/diagnostic_arg.rs
@@ -0,0 +1,42 @@
+use syn::parse::{Parse, ParseStream};
+
+use crate::code::Code;
+use crate::forward::Forward;
+use crate::help::Help;
+use crate::severity::Severity;
+use crate::url::Url;
+
+pub enum DiagnosticArg {
+ Transparent,
+ Code(Code),
+ Severity(Severity),
+ Help(Help),
+ Url(Url),
+ Forward(Forward),
+}
+
+impl Parse for DiagnosticArg {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let ident = input.fork().parse::<syn::Ident>()?;
+ if ident == "transparent" {
+ // consume the token
+ let _: syn::Ident = input.parse()?;
+ Ok(DiagnosticArg::Transparent)
+ } else if ident == "forward" {
+ Ok(DiagnosticArg::Forward(input.parse()?))
+ } else if ident == "code" {
+ Ok(DiagnosticArg::Code(input.parse()?))
+ } else if ident == "severity" {
+ Ok(DiagnosticArg::Severity(input.parse()?))
+ } else if ident == "help" {
+ Ok(DiagnosticArg::Help(input.parse()?))
+ } else if ident == "url" {
+ Ok(DiagnosticArg::Url(input.parse()?))
+ } else {
+ Err(syn::Error::new(
+ ident.span(),
+ "Unrecognized diagnostic option",
+ ))
+ }
+ }
+}
diff --git a/src/diagnostic_source.rs b/src/diagnostic_source.rs
new file mode 100644
index 0000000..1104eb7
--- /dev/null
+++ b/src/diagnostic_source.rs
@@ -0,0 +1,78 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::spanned::Spanned;
+
+use crate::forward::WhichFn;
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ utils::{display_pat_members, gen_all_variants_with},
+};
+
+pub struct DiagnosticSource(syn::Member);
+
+impl DiagnosticSource {
+ pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
+ match fields {
+ syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
+ syn::Fields::Unnamed(unnamed) => {
+ Self::from_fields_vec(unnamed.unnamed.iter().collect())
+ }
+ syn::Fields::Unit => Ok(None),
+ }
+ }
+
+ fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
+ for (i, field) in fields.iter().enumerate() {
+ for attr in &field.attrs {
+ if attr.path().is_ident("diagnostic_source") {
+ let diagnostic_source = if let Some(ident) = field.ident.clone() {
+ syn::Member::Named(ident)
+ } else {
+ syn::Member::Unnamed(syn::Index {
+ index: i as u32,
+ span: field.span(),
+ })
+ };
+ return Ok(Some(DiagnosticSource(diagnostic_source)));
+ }
+ }
+ }
+ Ok(None)
+ }
+
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::DiagnosticSource,
+ |ident,
+ fields,
+ DiagnosticConcreteArgs {
+ diagnostic_source, ..
+ }| {
+ let (display_pat, _display_members) = display_pat_members(fields);
+ diagnostic_source.as_ref().map(|diagnostic_source| {
+ let rel = match &diagnostic_source.0 {
+ syn::Member::Named(ident) => ident.clone(),
+ syn::Member::Unnamed(syn::Index { index, .. }) => {
+ quote::format_ident!("_{}", index)
+ }
+ };
+ quote! {
+ Self::#ident #display_pat => {
+ std::option::Option::Some(std::borrow::Borrow::borrow(#rel))
+ }
+ }
+ })
+ },
+ )
+ }
+
+ pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
+ let rel = &self.0;
+ Some(quote! {
+ fn diagnostic_source<'a>(&'a self) -> std::option::Option<&'a dyn miette::Diagnostic> {
+ std::option::Option::Some(std::borrow::Borrow::borrow(&self.#rel))
+ }
+ })
+ }
+}
diff --git a/src/fmt.rs b/src/fmt.rs
new file mode 100644
index 0000000..692c5ad
--- /dev/null
+++ b/src/fmt.rs
@@ -0,0 +1,235 @@
+// NOTE: Most code in this file is taken straight from `thiserror`.
+use std::collections::HashSet as Set;
+use std::iter::FromIterator;
+
+use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
+use quote::{format_ident, quote, quote_spanned, ToTokens};
+use syn::ext::IdentExt;
+use syn::parse::{ParseStream, Parser};
+use syn::{braced, bracketed, parenthesized, Ident, Index, LitStr, Member, Result, Token};
+
+#[derive(Clone)]
+pub struct Display {
+ pub fmt: LitStr,
+ pub args: TokenStream,
+ pub has_bonus_display: bool,
+}
+
+impl ToTokens for Display {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ let fmt = &self.fmt;
+ let args = &self.args;
+ tokens.extend(quote! {
+ write!(__formatter, #fmt #args)
+ });
+ }
+}
+
+impl Display {
+ // Transform `"error {var}"` to `"error {}", var`.
+ pub fn expand_shorthand(&mut self, members: &Set<Member>) {
+ let raw_args = self.args.clone();
+ let mut named_args = explicit_named_args.parse2(raw_args).unwrap();
+
+ let span = self.fmt.span();
+ let fmt = self.fmt.value();
+ let mut read = fmt.as_str();
+ let mut out = String::new();
+ let mut args = self.args.clone();
+ let mut has_bonus_display = false;
+
+ let mut has_trailing_comma = false;
+ if let Some(TokenTree::Punct(punct)) = args.clone().into_iter().last() {
+ if punct.as_char() == ',' {
+ has_trailing_comma = true;
+ }
+ }
+
+ while let Some(brace) = read.find('{') {
+ out += &read[..brace + 1];
+ read = &read[brace + 1..];
+ if read.starts_with('{') {
+ out.push('{');
+ read = &read[1..];
+ continue;
+ }
+ let next = match read.chars().next() {
+ Some(next) => next,
+ None => return,
+ };
+ let member = match next {
+ '0'..='9' => {
+ let int = take_int(&mut read);
+ let member = match int.parse::<u32>() {
+ Ok(index) => Member::Unnamed(Index { index, span }),
+ Err(_) => return,
+ };
+ if !members.contains(&member) {
+ out += &int;
+ continue;
+ }
+ member
+ }
+ 'a'..='z' | 'A'..='Z' | '_' => {
+ let mut ident = take_ident(&mut read);
+ ident.set_span(span);
+ Member::Named(ident)
+ }
+ _ => continue,
+ };
+ let local = match &member {
+ Member::Unnamed(index) => format_ident!("_{}", index),
+ Member::Named(ident) => ident.clone(),
+ };
+ let mut formatvar = local.clone();
+ if formatvar.to_string().starts_with("r#") {
+ formatvar = format_ident!("r_{}", formatvar);
+ }
+ if formatvar.to_string().starts_with('_') {
+ // Work around leading underscore being rejected by 1.40 and
+ // older compilers. https://github.com/rust-lang/rust/pull/66847
+ formatvar = format_ident!("field_{}", formatvar);
+ }
+ out += &formatvar.to_string();
+ if !named_args.insert(formatvar.clone()) {
+ // Already specified in the format argument list.
+ continue;
+ }
+ if !has_trailing_comma {
+ args.extend(quote_spanned!(span=> ,));
+ }
+ args.extend(quote_spanned!(span=> #formatvar = #local));
+ if read.starts_with('}') && members.contains(&member) {
+ has_bonus_display = true;
+ // args.extend(quote_spanned!(span=> .as_display()));
+ }
+ has_trailing_comma = false;
+ }
+
+ out += read;
+ self.fmt = LitStr::new(&out, self.fmt.span());
+ self.args = args;
+ self.has_bonus_display = has_bonus_display;
+ }
+}
+
+fn explicit_named_args(input: ParseStream) -> Result<Set<Ident>> {
+ let mut named_args = Set::new();
+
+ while !input.is_empty() {
+ if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) {
+ input.parse::<Token![,]>()?;
+ let ident = input.call(Ident::parse_any)?;
+ input.parse::<Token![=]>()?;
+ named_args.insert(ident);
+ } else {
+ input.parse::<TokenTree>()?;
+ }
+ }
+
+ Ok(named_args)
+}
+
+fn take_int(read: &mut &str) -> String {
+ let mut int = String::new();
+ for (i, ch) in read.char_indices() {
+ match ch {
+ '0'..='9' => int.push(ch),
+ _ => {
+ *read = &read[i..];
+ break;
+ }
+ }
+ }
+ int
+}
+
+fn take_ident(read: &mut &str) -> Ident {
+ let mut ident = String::new();
+ let raw = read.starts_with("r#");
+ if raw {
+ ident.push_str("r#");
+ *read = &read[2..];
+ }
+ for (i, ch) in read.char_indices() {
+ match ch {
+ 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch),
+ _ => {
+ *read = &read[i..];
+ break;
+ }
+ }
+ }
+ Ident::parse_any.parse_str(&ident).unwrap()
+}
+
+pub fn parse_token_expr(input: ParseStream, mut begin_expr: bool) -> Result<TokenStream> {
+ let mut tokens = Vec::new();
+ while !input.is_empty() {
+ if begin_expr && input.peek(Token![.]) {
+ if input.peek2(Ident) {
+ input.parse::<Token![.]>()?;
+ begin_expr = false;
+ continue;
+ }
+ if input.peek2(syn::LitInt) {
+ input.parse::<Token![.]>()?;
+ let int: Index = input.parse()?;
+ let ident = format_ident!("_{}", int.index, span = int.span);
+ tokens.push(TokenTree::Ident(ident));
+ begin_expr = false;
+ continue;
+ }
+ }
+
+ begin_expr = input.peek(Token![break])
+ || input.peek(Token![continue])
+ || input.peek(Token![if])
+ || input.peek(Token![in])
+ || input.peek(Token![match])
+ || input.peek(Token![mut])
+ || input.peek(Token![return])
+ || input.peek(Token![while])
+ || input.peek(Token![+])
+ || input.peek(Token![&])
+ || input.peek(Token![!])
+ || input.peek(Token![^])
+ || input.peek(Token![,])
+ || input.peek(Token![/])
+ || input.peek(Token![=])
+ || input.peek(Token![>])
+ || input.peek(Token![<])
+ || input.peek(Token![|])
+ || input.peek(Token![%])
+ || input.peek(Token![;])
+ || input.peek(Token![*])
+ || input.peek(Token![-]);
+
+ let token: TokenTree = if input.peek(syn::token::Paren) {
+ let content;
+ let delimiter = parenthesized!(content in input);
+ let nested = parse_token_expr(&content, true)?;
+ let mut group = Group::new(Delimiter::Parenthesis, nested);
+ group.set_span(delimiter.span.join());
+ TokenTree::Group(group)
+ } else if input.peek(syn::token::Brace) {
+ let content;
+ let delimiter = braced!(content in input);
+ let nested = parse_token_expr(&content, true)?;
+ let mut group = Group::new(Delimiter::Brace, nested);
+ group.set_span(delimiter.span.join());
+ TokenTree::Group(group)
+ } else if input.peek(syn::token::Bracket) {
+ let content;
+ let delimiter = bracketed!(content in input);
+ let nested = parse_token_expr(&content, true)?;
+ let mut group = Group::new(Delimiter::Bracket, nested);
+ group.set_span(delimiter.span.join());
+ TokenTree::Group(group)
+ } else {
+ input.parse()?
+ };
+ tokens.push(token);
+ }
+ Ok(TokenStream::from_iter(tokens))
+}
diff --git a/src/forward.rs b/src/forward.rs
new file mode 100644
index 0000000..171019a
--- /dev/null
+++ b/src/forward.rs
@@ -0,0 +1,161 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::{
+ parenthesized,
+ parse::{Parse, ParseStream},
+ spanned::Spanned,
+};
+
+pub enum Forward {
+ Unnamed(usize),
+ Named(syn::Ident),
+}
+
+impl Parse for Forward {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let forward = input.parse::<syn::Ident>()?;
+ if forward != "forward" {
+ return Err(syn::Error::new(forward.span(), "msg"));
+ }
+ let content;
+ parenthesized!(content in input);
+ let looky = content.lookahead1();
+ if looky.peek(syn::LitInt) {
+ let int: syn::LitInt = content.parse()?;
+ let index = int.base10_parse()?;
+ return Ok(Forward::Unnamed(index));
+ }
+ Ok(Forward::Named(content.parse()?))
+ }
+}
+
+#[derive(Copy, Clone)]
+pub enum WhichFn {
+ Code,
+ Help,
+ Url,
+ Severity,
+ Labels,
+ SourceCode,
+ Related,
+ DiagnosticSource,
+}
+
+impl WhichFn {
+ pub fn method_call(&self) -> TokenStream {
+ match self {
+ Self::Code => quote! { code() },
+ Self::Help => quote! { help() },
+ Self::Url => quote! { url() },
+ Self::Severity => quote! { severity() },
+ Self::Labels => quote! { labels() },
+ Self::SourceCode => quote! { source_code() },
+ Self::Related => quote! { related() },
+ Self::DiagnosticSource => quote! { diagnostic_source() },
+ }
+ }
+
+ pub fn signature(&self) -> TokenStream {
+ match self {
+ Self::Code => quote! {
+ fn code(& self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>>
+ },
+ Self::Help => quote! {
+ fn help(& self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>>
+ },
+ Self::Url => quote! {
+ fn url(& self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>>
+ },
+ Self::Severity => quote! {
+ fn severity(&self) -> std::option::Option<miette::Severity>
+ },
+ Self::Related => quote! {
+ fn related(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = &dyn miette::Diagnostic> + '_>>
+ },
+ Self::Labels => quote! {
+ fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>>
+ },
+ Self::SourceCode => quote! {
+ fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode>
+ },
+ Self::DiagnosticSource => quote! {
+ fn diagnostic_source(&self) -> std::option::Option<&dyn miette::Diagnostic>
+ },
+ }
+ }
+
+ pub fn catchall_arm(&self) -> TokenStream {
+ quote! { _ => std::option::Option::None }
+ }
+}
+
+impl Forward {
+ pub fn for_transparent_field(fields: &syn::Fields) -> syn::Result<Self> {
+ let make_err = || {
+ syn::Error::new(
+ fields.span(),
+ "you can only use #[diagnostic(transparent)] with exactly one field",
+ )
+ };
+ match fields {
+ syn::Fields::Named(named) => {
+ let mut iter = named.named.iter();
+ let field = iter.next().ok_or_else(make_err)?;
+ if iter.next().is_some() {
+ return Err(make_err());
+ }
+ let field_name = field
+ .ident
+ .clone()
+ .unwrap_or_else(|| format_ident!("unnamed"));
+ Ok(Self::Named(field_name))
+ }
+ syn::Fields::Unnamed(unnamed) => {
+ if unnamed.unnamed.iter().len() != 1 {
+ return Err(make_err());
+ }
+ Ok(Self::Unnamed(0))
+ }
+ _ => Err(syn::Error::new(
+ fields.span(),
+ "you cannot use #[diagnostic(transparent)] with a unit struct or a unit variant",
+ )),
+ }
+ }
+
+ pub fn gen_struct_method(&self, which_fn: WhichFn) -> TokenStream {
+ let signature = which_fn.signature();
+ let method_call = which_fn.method_call();
+
+ let field_name = match self {
+ Forward::Named(field_name) => quote!(#field_name),
+ Forward::Unnamed(index) => {
+ let index = syn::Index::from(*index);
+ quote!(#index)
+ }
+ };
+
+ quote! {
+ #[inline]
+ #signature {
+ self.#field_name.#method_call
+ }
+ }
+ }
+
+ pub fn gen_enum_match_arm(&self, variant: &syn::Ident, which_fn: WhichFn) -> TokenStream {
+ let method_call = which_fn.method_call();
+ match self {
+ Forward::Named(field_name) => quote! {
+ Self::#variant { #field_name, .. } => #field_name.#method_call,
+ },
+ Forward::Unnamed(index) => {
+ let underscores: Vec<_> = core::iter::repeat(quote! { _, }).take(*index).collect();
+ let unnamed = format_ident!("unnamed");
+ quote! {
+ Self::#variant ( #(#underscores)* #unnamed, .. ) => #unnamed.#method_call,
+ }
+ }
+ }
+ }
+}
diff --git a/src/help.rs b/src/help.rs
new file mode 100644
index 0000000..1c21054
--- /dev/null
+++ b/src/help.rs
@@ -0,0 +1,146 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::{
+ parenthesized,
+ parse::{Parse, ParseStream},
+ spanned::Spanned,
+ Fields, Token,
+};
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ utils::{display_pat_members, gen_all_variants_with},
+};
+use crate::{
+ fmt::{self, Display},
+ forward::WhichFn,
+};
+
+pub enum Help {
+ Display(Display),
+ Field(syn::Member, Box<syn::Type>),
+}
+
+impl Parse for Help {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let ident = input.parse::<syn::Ident>()?;
+ if ident == "help" {
+ let la = input.lookahead1();
+ if la.peek(syn::token::Paren) {
+ let content;
+ parenthesized!(content in input);
+ let fmt = content.parse()?;
+ let args = if content.is_empty() {
+ TokenStream::new()
+ } else {
+ fmt::parse_token_expr(&content, false)?
+ };
+ let display = Display {
+ fmt,
+ args,
+ has_bonus_display: false,
+ };
+ Ok(Help::Display(display))
+ } else {
+ input.parse::<Token![=]>()?;
+ Ok(Help::Display(Display {
+ fmt: input.parse()?,
+ args: TokenStream::new(),
+ has_bonus_display: false,
+ }))
+ }
+ } else {
+ Err(syn::Error::new(ident.span(), "not a help"))
+ }
+ }
+}
+
+impl Help {
+ pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
+ match fields {
+ syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
+ syn::Fields::Unnamed(unnamed) => {
+ Self::from_fields_vec(unnamed.unnamed.iter().collect())
+ }
+ syn::Fields::Unit => Ok(None),
+ }
+ }
+
+ fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
+ for (i, field) in fields.iter().enumerate() {
+ for attr in &field.attrs {
+ if attr.path().is_ident("help") {
+ let help = if let Some(ident) = field.ident.clone() {
+ syn::Member::Named(ident)
+ } else {
+ syn::Member::Unnamed(syn::Index {
+ index: i as u32,
+ span: field.span(),
+ })
+ };
+ return Ok(Some(Help::Field(help, Box::new(field.ty.clone()))));
+ }
+ }
+ }
+ Ok(None)
+ }
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::Help,
+ |ident, fields, DiagnosticConcreteArgs { help, .. }| {
+ let (display_pat, display_members) = display_pat_members(fields);
+ match &help.as_ref()? {
+ Help::Display(display) => {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ Some(quote! {
+ Self::#ident #display_pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))),
+ })
+ }
+ Help::Field(member, ty) => {
+ let help = match &member {
+ syn::Member::Named(ident) => ident.clone(),
+ syn::Member::Unnamed(syn::Index { index, .. }) => {
+ format_ident!("_{}", index)
+ }
+ };
+ let var = quote! { __miette_internal_var };
+ Some(quote! {
+ Self::#ident #display_pat => {
+ use miette::macro_helpers::ToOption;
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&#help).as_ref().map(|#var| -> std::boxed::Box<dyn std::fmt::Display + '_> { std::boxed::Box::new(format!("{}", #var)) })
+ },
+ })
+ }
+ }
+ },
+ )
+ }
+
+ pub(crate) fn gen_struct(&self, fields: &Fields) -> Option<TokenStream> {
+ let (display_pat, display_members) = display_pat_members(fields);
+ match self {
+ Help::Display(display) => {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ Some(quote! {
+ fn help(&self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>> {
+ #[allow(unused_variables, deprecated)]
+ let Self #display_pat = self;
+ std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
+ }
+ })
+ }
+ Help::Field(member, ty) => {
+ let var = quote! { __miette_internal_var };
+ Some(quote! {
+ fn help(&self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>> {
+ #[allow(unused_variables, deprecated)]
+ let Self #display_pat = self;
+ use miette::macro_helpers::ToOption;
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#member).as_ref().map(|#var| -> std::boxed::Box<dyn std::fmt::Display + '_> { std::boxed::Box::new(format!("{}", #var)) })
+ }
+ })
+ }
+ }
+ }
+}
diff --git a/src/label.rs b/src/label.rs
new file mode 100644
index 0000000..e0bc70a
--- /dev/null
+++ b/src/label.rs
@@ -0,0 +1,207 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::{
+ parenthesized,
+ parse::{Parse, ParseStream},
+ spanned::Spanned,
+ Token,
+};
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ fmt::{self, Display},
+ forward::WhichFn,
+ utils::{display_pat_members, gen_all_variants_with},
+};
+
+pub struct Labels(Vec<Label>);
+
+struct Label {
+ label: Option<Display>,
+ ty: syn::Type,
+ span: syn::Member,
+}
+
+struct LabelAttr {
+ label: Option<Display>,
+}
+
+impl Parse for LabelAttr {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ // Skip a token.
+ // This should receive one of:
+ // - label = "..."
+ // - label("...")
+ let _ = input.step(|cursor| {
+ if let Some((_, next)) = cursor.token_tree() {
+ Ok(((), next))
+ } else {
+ Err(cursor.error("unexpected empty attribute"))
+ }
+ });
+ let la = input.lookahead1();
+ let label = if la.peek(syn::token::Paren) {
+ // #[label("{}", x)]
+ let content;
+ parenthesized!(content in input);
+ if content.peek(syn::LitStr) {
+ let fmt = content.parse()?;
+ let args = if content.is_empty() {
+ TokenStream::new()
+ } else {
+ fmt::parse_token_expr(&content, false)?
+ };
+ let display = Display {
+ fmt,
+ args,
+ has_bonus_display: false,
+ };
+ Some(display)
+ } else {
+ return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string."));
+ }
+ } else if la.peek(Token![=]) {
+ // #[label = "blabla"]
+ input.parse::<Token![=]>()?;
+ Some(Display {
+ fmt: input.parse()?,
+ args: TokenStream::new(),
+ has_bonus_display: false,
+ })
+ } else {
+ None
+ };
+ Ok(LabelAttr { label })
+ }
+}
+
+impl Labels {
+ pub fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
+ match fields {
+ syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
+ syn::Fields::Unnamed(unnamed) => {
+ Self::from_fields_vec(unnamed.unnamed.iter().collect())
+ }
+ syn::Fields::Unit => Ok(None),
+ }
+ }
+
+ fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
+ let mut labels = Vec::new();
+ for (i, field) in fields.iter().enumerate() {
+ for attr in &field.attrs {
+ if attr.path().is_ident("label") {
+ let span = if let Some(ident) = field.ident.clone() {
+ syn::Member::Named(ident)
+ } else {
+ syn::Member::Unnamed(syn::Index {
+ index: i as u32,
+ span: field.span(),
+ })
+ };
+ use quote::ToTokens;
+ let LabelAttr { label } =
+ syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
+ labels.push(Label {
+ label,
+ span,
+ ty: field.ty.clone(),
+ });
+ }
+ }
+ }
+ if labels.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(Labels(labels)))
+ }
+ }
+
+ pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
+ let (display_pat, display_members) = display_pat_members(fields);
+ let labels = self.0.iter().map(|highlight| {
+ let Label { span, label, ty } = highlight;
+ let var = quote! { __miette_internal_var };
+ if let Some(display) = label {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ quote! {
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
+ .map(|#var| miette::LabeledSpan::new_with_span(
+ std::option::Option::Some(format!(#fmt #args)),
+ #var.clone(),
+ ))
+ }
+ } else {
+ quote! {
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
+ .map(|#var| miette::LabeledSpan::new_with_span(
+ std::option::Option::None,
+ #var.clone(),
+ ))
+ }
+ }
+ });
+ Some(quote! {
+ #[allow(unused_variables)]
+ fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>> {
+ use miette::macro_helpers::ToOption;
+ let Self #display_pat = self;
+ std::option::Option::Some(Box::new(vec![
+ #(#labels),*
+ ].into_iter().filter(Option::is_some).map(Option::unwrap)))
+ }
+ })
+ }
+
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::Labels,
+ |ident, fields, DiagnosticConcreteArgs { labels, .. }| {
+ let (display_pat, display_members) = display_pat_members(fields);
+ labels.as_ref().and_then(|labels| {
+ let variant_labels = labels.0.iter().map(|label| {
+ let Label { span, label, ty } = label;
+ let field = match &span {
+ syn::Member::Named(ident) => ident.clone(),
+ syn::Member::Unnamed(syn::Index { index, .. }) => {
+ format_ident!("_{}", index)
+ }
+ };
+ let var = quote! { __miette_internal_var };
+ if let Some(display) = label {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ quote! {
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
+ .map(|#var| miette::LabeledSpan::new_with_span(
+ std::option::Option::Some(format!(#fmt #args)),
+ #var.clone(),
+ ))
+ }
+ } else {
+ quote! {
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
+ .map(|#var| miette::LabeledSpan::new_with_span(
+ std::option::Option::None,
+ #var.clone(),
+ ))
+ }
+ }
+ });
+ let variant_name = ident.clone();
+ match &fields {
+ syn::Fields::Unit => None,
+ _ => Some(quote! {
+ Self::#variant_name #display_pat => {
+ use miette::macro_helpers::ToOption;
+ std::option::Option::Some(std::boxed::Box::new(vec![
+ #(#variant_labels),*
+ ].into_iter().filter(Option::is_some).map(Option::unwrap)))
+ }
+ }),
+ }
+ })
+ },
+ )
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..0f7e64e
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,32 @@
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+use diagnostic::Diagnostic;
+
+mod code;
+mod diagnostic;
+mod diagnostic_arg;
+mod diagnostic_source;
+mod fmt;
+mod forward;
+mod help;
+mod label;
+mod related;
+mod severity;
+mod source_code;
+mod url;
+mod utils;
+
+#[proc_macro_derive(
+ Diagnostic,
+ attributes(diagnostic, source_code, label, related, help, diagnostic_source)
+)]
+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.gen(),
+ Err(err) => return err.to_compile_error().into(),
+ };
+ // panic!("{:#}", cmd.to_token_stream());
+ quote!(#cmd).into()
+}
diff --git a/src/related.rs b/src/related.rs
new file mode 100644
index 0000000..9b7f9e1
--- /dev/null
+++ b/src/related.rs
@@ -0,0 +1,79 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::spanned::Spanned;
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ forward::WhichFn,
+ utils::{display_pat_members, gen_all_variants_with},
+};
+
+pub struct Related(syn::Member);
+
+impl Related {
+ pub(crate) fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
+ match fields {
+ syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
+ syn::Fields::Unnamed(unnamed) => {
+ Self::from_fields_vec(unnamed.unnamed.iter().collect())
+ }
+ syn::Fields::Unit => Ok(None),
+ }
+ }
+
+ fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
+ for (i, field) in fields.iter().enumerate() {
+ for attr in &field.attrs {
+ if attr.path().is_ident("related") {
+ let related = if let Some(ident) = field.ident.clone() {
+ syn::Member::Named(ident)
+ } else {
+ syn::Member::Unnamed(syn::Index {
+ index: i as u32,
+ span: field.span(),
+ })
+ };
+ return Ok(Some(Related(related)));
+ }
+ }
+ }
+ Ok(None)
+ }
+
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::Related,
+ |ident, fields, DiagnosticConcreteArgs { related, .. }| {
+ let (display_pat, _display_members) = display_pat_members(fields);
+ related.as_ref().map(|related| {
+ let rel = match &related.0 {
+ syn::Member::Named(ident) => ident.clone(),
+ syn::Member::Unnamed(syn::Index { index, .. }) => {
+ format_ident!("_{}", index)
+ }
+ };
+ quote! {
+ Self::#ident #display_pat => {
+ std::option::Option::Some(std::boxed::Box::new(
+ #rel.iter().map(|x| -> &(dyn miette::Diagnostic) { &*x })
+ ))
+ }
+ }
+ })
+ },
+ )
+ }
+
+ pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
+ let rel = &self.0;
+ Some(quote! {
+ fn related<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
+ use ::core::borrow::Borrow;
+ std::option::Option::Some(std::boxed::Box::new(
+ self.#rel.iter().map(|x| -> &(dyn miette::Diagnostic) { &*x.borrow() })
+ ))
+ }
+ })
+ }
+}
diff --git a/src/severity.rs b/src/severity.rs
new file mode 100644
index 0000000..76126de
--- /dev/null
+++ b/src/severity.rs
@@ -0,0 +1,89 @@
+use proc_macro2::{Span, TokenStream};
+use quote::quote;
+use syn::{
+ parenthesized,
+ parse::{Parse, ParseStream},
+ Token,
+};
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ forward::WhichFn,
+ utils::gen_all_variants_with,
+};
+
+pub struct Severity(pub syn::Ident);
+
+impl Parse for Severity {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let ident = input.parse::<syn::Ident>()?;
+ if ident == "severity" {
+ let la = input.lookahead1();
+ if la.peek(syn::token::Paren) {
+ let content;
+ parenthesized!(content in input);
+ let la = content.lookahead1();
+ if la.peek(syn::LitStr) {
+ let str = content.parse::<syn::LitStr>()?;
+ let sev = get_severity(&str.value(), str.span())?;
+ Ok(Severity(syn::Ident::new(&sev, str.span())))
+ } else {
+ let ident = content.parse::<syn::Ident>()?;
+ let sev = get_severity(&ident.to_string(), ident.span())?;
+ Ok(Severity(syn::Ident::new(&sev, ident.span())))
+ }
+ } else {
+ input.parse::<Token![=]>()?;
+ let str = input.parse::<syn::LitStr>()?;
+ let sev = get_severity(&str.value(), str.span())?;
+ Ok(Severity(syn::Ident::new(&sev, str.span())))
+ }
+ } else {
+ Err(syn::Error::new(
+ ident.span(),
+ "MIETTE BUG: not a severity option",
+ ))
+ }
+ }
+}
+
+fn get_severity(input: &str, span: Span) -> syn::Result<String> {
+ match input.to_lowercase().as_ref() {
+ "error" | "err" => Ok("Error".into()),
+ "warning" | "warn" => Ok("Warning".into()),
+ "advice" | "adv" | "info" => Ok("Advice".into()),
+ _ => Err(syn::Error::new(
+ span,
+ "Invalid severity level. Only Error, Warning, and Advice are supported.",
+ )),
+ }
+}
+
+impl Severity {
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::Severity,
+ |ident, fields, DiagnosticConcreteArgs { severity, .. }| {
+ let severity = &severity.as_ref()?.0;
+ let fields = match fields {
+ syn::Fields::Named(_) => quote! { { .. } },
+ syn::Fields::Unnamed(_) => quote! { (..) },
+ syn::Fields::Unit => quote! {},
+ };
+ Some(
+ quote! { Self::#ident #fields => std::option::Option::Some(miette::Severity::#severity), },
+ )
+ },
+ )
+ }
+
+ pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
+ let sev = &self.0;
+ Some(quote! {
+ fn severity(&self) -> std::option::Option<miette::Severity> {
+ Some(miette::Severity::#sev)
+ }
+ })
+ }
+}
diff --git a/src/source_code.rs b/src/source_code.rs
new file mode 100644
index 0000000..62f28e7
--- /dev/null
+++ b/src/source_code.rs
@@ -0,0 +1,81 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::spanned::Spanned;
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ forward::WhichFn,
+ utils::{display_pat_members, gen_all_variants_with},
+};
+
+pub struct SourceCode {
+ source_code: syn::Member,
+}
+
+impl SourceCode {
+ pub fn from_fields(fields: &syn::Fields) -> syn::Result<Option<Self>> {
+ match fields {
+ syn::Fields::Named(named) => Self::from_fields_vec(named.named.iter().collect()),
+ syn::Fields::Unnamed(unnamed) => {
+ Self::from_fields_vec(unnamed.unnamed.iter().collect())
+ }
+ syn::Fields::Unit => Ok(None),
+ }
+ }
+
+ fn from_fields_vec(fields: Vec<&syn::Field>) -> syn::Result<Option<Self>> {
+ for (i, field) in fields.iter().enumerate() {
+ for attr in &field.attrs {
+ if attr.path().is_ident("source_code") {
+ let source_code = if let Some(ident) = field.ident.clone() {
+ syn::Member::Named(ident)
+ } else {
+ syn::Member::Unnamed(syn::Index {
+ index: i as u32,
+ span: field.span(),
+ })
+ };
+ return Ok(Some(SourceCode { source_code }));
+ }
+ }
+ }
+ Ok(None)
+ }
+
+ pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
+ let (display_pat, _display_members) = display_pat_members(fields);
+ let src = &self.source_code;
+ Some(quote! {
+ #[allow(unused_variables)]
+ fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
+ let Self #display_pat = self;
+ Some(&self.#src)
+ }
+ })
+ }
+
+ pub(crate) fn gen_enum(variants: &[DiagnosticDef]) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::SourceCode,
+ |ident, fields, DiagnosticConcreteArgs { source_code, .. }| {
+ let (display_pat, _display_members) = display_pat_members(fields);
+ source_code.as_ref().and_then(|source_code| {
+ let field = match &source_code.source_code {
+ syn::Member::Named(ident) => ident.clone(),
+ syn::Member::Unnamed(syn::Index { index, .. }) => {
+ format_ident!("_{}", index)
+ }
+ };
+ let variant_name = ident.clone();
+ match &fields {
+ syn::Fields::Unit => None,
+ _ => Some(quote! {
+ Self::#variant_name #display_pat => std::option::Option::Some(#field),
+ }),
+ }
+ })
+ },
+ )
+ }
+}
diff --git a/src/url.rs b/src/url.rs
new file mode 100644
index 0000000..734d1a4
--- /dev/null
+++ b/src/url.rs
@@ -0,0 +1,139 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{
+ parenthesized,
+ parse::{Parse, ParseStream},
+ Fields, Token,
+};
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ utils::{display_pat_members, gen_all_variants_with, gen_unused_pat},
+};
+use crate::{
+ fmt::{self, Display},
+ forward::WhichFn,
+};
+
+pub enum Url {
+ Display(Display),
+ DocsRs,
+}
+
+impl Parse for Url {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let ident = input.parse::<syn::Ident>()?;
+ if ident == "url" {
+ let la = input.lookahead1();
+ if la.peek(syn::token::Paren) {
+ let content;
+ parenthesized!(content in input);
+ if content.peek(syn::LitStr) {
+ let fmt = content.parse()?;
+ let args = if content.is_empty() {
+ TokenStream::new()
+ } else {
+ fmt::parse_token_expr(&content, false)?
+ };
+ let display = Display {
+ fmt,
+ args,
+ has_bonus_display: false,
+ };
+ Ok(Url::Display(display))
+ } else {
+ let option = content.parse::<syn::Ident>()?;
+ if option == "docsrs" {
+ Ok(Url::DocsRs)
+ } else {
+ Err(syn::Error::new(option.span(), "Invalid argument to url() sub-attribute. It must be either a string or a plain `docsrs` identifier"))
+ }
+ }
+ } else {
+ input.parse::<Token![=]>()?;
+ Ok(Url::Display(Display {
+ fmt: input.parse()?,
+ args: TokenStream::new(),
+ has_bonus_display: false,
+ }))
+ }
+ } else {
+ Err(syn::Error::new(ident.span(), "not a url"))
+ }
+ }
+}
+
+impl Url {
+ pub(crate) fn gen_enum(
+ enum_name: &syn::Ident,
+ variants: &[DiagnosticDef],
+ ) -> Option<TokenStream> {
+ gen_all_variants_with(
+ variants,
+ WhichFn::Url,
+ |ident, fields, DiagnosticConcreteArgs { url, .. }| {
+ let (pat, fmt, args) = match url.as_ref()? {
+ // fall through to `_ => None` below
+ Url::Display(display) => {
+ let (display_pat, display_members) = display_pat_members(fields);
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ (display_pat, fmt.value(), args)
+ }
+ Url::DocsRs => {
+ let pat = gen_unused_pat(fields);
+ let fmt =
+ "https://docs.rs/{crate_name}/{crate_version}/{mod_name}/{item_path}"
+ .into();
+ let item_path = format!("enum.{}.html#variant.{}", enum_name, ident);
+ let args = quote! {
+ ,
+ crate_name=env!("CARGO_PKG_NAME"),
+ crate_version=env!("CARGO_PKG_VERSION"),
+ mod_name=env!("CARGO_PKG_NAME").replace('-', "_"),
+ item_path=#item_path
+ };
+ (pat, fmt, args)
+ }
+ };
+ Some(quote! {
+ Self::#ident #pat => std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args))),
+ })
+ },
+ )
+ }
+
+ pub(crate) fn gen_struct(
+ &self,
+ struct_name: &syn::Ident,
+ fields: &Fields,
+ ) -> Option<TokenStream> {
+ let (pat, fmt, args) = match self {
+ Url::Display(display) => {
+ let (display_pat, display_members) = display_pat_members(fields);
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ (display_pat, fmt.value(), args)
+ }
+ Url::DocsRs => {
+ let pat = gen_unused_pat(fields);
+ let fmt =
+ "https://docs.rs/{crate_name}/{crate_version}/{mod_name}/{item_path}".into();
+ let item_path = format!("struct.{}.html", struct_name);
+ let args = quote! {
+ ,
+ crate_name=env!("CARGO_PKG_NAME"),
+ crate_version=env!("CARGO_PKG_VERSION"),
+ mod_name=env!("CARGO_PKG_NAME").replace('-', "_"),
+ item_path=#item_path
+ };
+ (pat, fmt, args)
+ }
+ };
+ Some(quote! {
+ fn url(&self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + '_>> {
+ #[allow(unused_variables, deprecated)]
+ let Self #pat = self;
+ std::option::Option::Some(std::boxed::Box::new(format!(#fmt #args)))
+ }
+ })
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..b867849
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,140 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote, ToTokens};
+use syn::{
+ parse::{Parse, ParseStream},
+ spanned::Spanned,
+};
+
+pub(crate) enum MemberOrString {
+ Member(syn::Member),
+ String(syn::LitStr),
+}
+
+impl ToTokens for MemberOrString {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ use MemberOrString::*;
+ match self {
+ Member(member) => member.to_tokens(tokens),
+ String(string) => string.to_tokens(tokens),
+ }
+ }
+}
+
+impl Parse for MemberOrString {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let lookahead = input.lookahead1();
+ if lookahead.peek(syn::Ident) || lookahead.peek(syn::LitInt) {
+ Ok(MemberOrString::Member(input.parse()?))
+ } else if lookahead.peek(syn::LitStr) {
+ Ok(MemberOrString::String(input.parse()?))
+ } else {
+ Err(syn::Error::new(
+ input.span(),
+ "Expected a string or a field reference.",
+ ))
+ }
+ }
+}
+
+use crate::{
+ diagnostic::{DiagnosticConcreteArgs, DiagnosticDef},
+ forward::WhichFn,
+};
+
+pub(crate) fn gen_all_variants_with(
+ variants: &[DiagnosticDef],
+ which_fn: WhichFn,
+ mut f: impl FnMut(&syn::Ident, &syn::Fields, &DiagnosticConcreteArgs) -> Option<TokenStream>,
+) -> Option<TokenStream> {
+ let pairs = variants
+ .iter()
+ .filter_map(|def| {
+ def.args
+ .forward_or_override_enum(&def.ident, which_fn, |concrete| {
+ f(&def.ident, &def.fields, concrete)
+ })
+ })
+ .collect::<Vec<_>>();
+ if pairs.is_empty() {
+ return None;
+ }
+ let signature = which_fn.signature();
+ let catchall = which_fn.catchall_arm();
+ Some(quote! {
+ #signature {
+ #[allow(unused_variables, deprecated)]
+ match self {
+ #(#pairs)*
+ #catchall
+ }
+ }
+ })
+}
+
+use crate::fmt::Display;
+use std::collections::HashSet;
+
+pub(crate) fn gen_unused_pat(fields: &syn::Fields) -> TokenStream {
+ match fields {
+ syn::Fields::Named(_) => quote! { { .. } },
+ syn::Fields::Unnamed(_) => quote! { ( .. ) },
+ syn::Fields::Unit => quote! {},
+ }
+}
+
+/// Goes in the slot `let Self #pat = self;` or `match self { Self #pat => ...
+/// }`.
+fn gen_fields_pat(fields: &syn::Fields) -> TokenStream {
+ let member_idents = fields.iter().enumerate().map(|(i, field)| {
+ field
+ .ident
+ .as_ref()
+ .cloned()
+ .unwrap_or_else(|| format_ident!("_{}", i))
+ });
+ match fields {
+ syn::Fields::Named(_) => quote! {
+ { #(#member_idents),* }
+ },
+ syn::Fields::Unnamed(_) => quote! {
+ ( #(#member_idents),* )
+ },
+ syn::Fields::Unit => quote! {},
+ }
+}
+
+/// The returned tokens go in the slot `let Self #pat = self;` or `match self {
+/// Self #pat => ... }`. The members can be passed to
+/// `Display::expand_shorthand[_cloned]`.
+pub(crate) fn display_pat_members(fields: &syn::Fields) -> (TokenStream, HashSet<syn::Member>) {
+ let pat = gen_fields_pat(fields);
+ let members: HashSet<syn::Member> = fields
+ .iter()
+ .enumerate()
+ .map(|(i, field)| {
+ if let Some(ident) = field.ident.as_ref().cloned() {
+ syn::Member::Named(ident)
+ } else {
+ syn::Member::Unnamed(syn::Index {
+ index: i as u32,
+ span: field.span(),
+ })
+ }
+ })
+ .collect();
+ (pat, members)
+}
+
+impl Display {
+ /// Returns `(fmt, args)` which must be passed to some kind of format macro
+ /// without tokens in between, i.e. `format!(#fmt #args)`.
+ pub(crate) fn expand_shorthand_cloned(
+ &self,
+ members: &HashSet<syn::Member>,
+ ) -> (syn::LitStr, TokenStream) {
+ let mut display = self.clone();
+ display.expand_shorthand(members);
+ let Display { fmt, args, .. } = display;
+ (fmt, args)
+ }
+}