diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d920e4..1f00e73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - rust: [1.71.1, stable] + rust: [1.81, stable] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fdbc141..59b0e53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ on: jobs: # Run 'dist plan' (or host) to determine what tasks we need to do plan: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} @@ -168,7 +168,7 @@ jobs: needs: - plan - build-local-artifacts - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json @@ -218,7 +218,7 @@ jobs: if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: @@ -278,7 +278,7 @@ jobs: needs: - plan - host - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PLAN: ${{ needs.plan.outputs.val }} @@ -311,7 +311,7 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/Cargo.toml b/Cargo.toml index 446344d..333a279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" homepage = "https://kdl.dev" repository = "https://github.com/kdl-org/kdl-rs" keywords = ["kdl", "document", "serialization", "config"] -rust-version = "1.71.1" +rust-version = "1.81" edition = "2021" [features] @@ -22,17 +22,16 @@ members = ["tools/*"] [dependencies] miette.workspace = true -thiserror.workspace = true num = "0.4.2" winnow = { version = "=0.6.24", features = ["alloc", "unstable-recover"] } kdlv1 = { package = "kdl", version = "4.7.0", optional = true } [workspace.dependencies] -miette = "7.2.0" -thiserror = "1.0.40" +miette = { version = "7.6.0", default-features = false } [dev-dependencies] -miette = { workspace = true, features = ["fancy"] } +miette = { workspace = true, features = ["derive", "fancy"] } +thiserror = "2.0.12" pretty_assertions = "1.3.0" # The profile that 'dist' will build with diff --git a/README.md b/README.md index 67b2bef..9dd8b9a 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,12 @@ Error: * `span` (default) - Includes spans in the various document-related structs. * `v1` - Adds support for v1 parsing. This will pull in the entire previous - version of `kdl-rs`, and so may be fairly heavy. + version of `kdl-rs`, and so may be fairly heavy. * `v1-fallback` - Implies `v1`. Makes it so the various `*::parse()` and - `FromStr` implementations try to parse their inputs as `v2`, and, if that - fails, try again with `v1`. For `KdlDocument`, a heuristic will be applied - if both `v1` and `v2` parsers fail, to pick which error(s) to return. For - other types, only the `v2` parser's errors will be returned. + `FromStr` implementations try to parse their inputs as `v2`, and, if that + fails, try again with `v1`. For `KdlDocument`, a heuristic will be applied + if both `v1` and `v2` parsers fail, to pick which error(s) to return. For + other types, only the `v2` parser's errors will be returned. ### Quirks @@ -140,9 +140,9 @@ means a few things: representation will be thrown away and the actual value will be used when serializing. -### Minimum Supported Rust Version +### Minimum Supported Rust Version (MSRV) -You must be at least `1.71.1` tall to get on this ride. +You must be at least `1.81` tall to get on this ride. ### License diff --git a/dist-workspace.toml b/dist-workspace.toml index cf6aa93..b647281 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -17,3 +17,6 @@ install-path = "CARGO_HOME" install-updater = false # Publish jobs to run in CI publish-jobs = ["npm"] + +[dist.github-custom-runners] +global = "ubuntu-22.04" diff --git a/src/error.rs b/src/error.rs index 1c210ab..71b0671 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,6 @@ -use std::sync::Arc; +use std::{error::Error, fmt::Display, iter, sync::Arc}; -use miette::{Diagnostic, SourceSpan}; -use thiserror::Error; +use miette::{Diagnostic, LabeledSpan, Severity, SourceSpan}; #[cfg(doc)] use { @@ -34,30 +33,43 @@ use { /// ╰──── /// help: Floating point numbers must be base 10, and have numbers after the decimal point. /// ``` -#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)] -#[error("Failed to parse KDL document")] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct KdlError { /// Original input that this failure came from. - #[source_code] pub input: Arc, /// Sub-diagnostics for this failure. - #[related] pub diagnostics: Vec, } +impl Display for KdlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Failed to parse KDL document") + } +} +impl Error for KdlError {} + +impl Diagnostic for KdlError { + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + Some(&self.input) + } + + fn related<'a>(&'a self) -> Option + 'a>> { + Some(Box::new( + self.diagnostics.iter().map(|d| d as &dyn Diagnostic), + )) + } +} + /// An individual diagnostic message for a KDL parsing issue. /// /// While generally signifying errors, they can also be treated as warnings. -#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)] -#[error("{}", message.clone().unwrap_or_else(|| "Unexpected error".into()))] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct KdlDiagnostic { /// Shared source for the diagnostic. - #[source_code] pub input: Arc, /// Offset in chars of the error. - #[label("{}", label.clone().unwrap_or_else(|| "here".into()))] pub span: SourceSpan, /// Message for the error itself. @@ -67,12 +79,42 @@ pub struct KdlDiagnostic { pub label: Option, /// Suggestion for fixing the parser error. - #[help] pub help: Option, /// Severity level for the Diagnostic. - #[diagnostic(severity)] - pub severity: miette::Severity, + pub severity: Severity, +} + +impl Display for KdlDiagnostic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = self + .message + .clone() + .unwrap_or_else(|| "Unexpected error".into()); + write!(f, "{message}") + } +} +impl Error for KdlDiagnostic {} + +impl Diagnostic for KdlDiagnostic { + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + Some(&self.input) + } + + fn severity(&self) -> Option { + Some(self.severity) + } + + fn help<'a>(&'a self) -> Option> { + self.help.as_ref().map(|s| Box::new(s) as Box) + } + + fn labels(&self) -> Option + '_>> { + let label = self.label.clone().unwrap_or_else(|| "here".to_owned()); + let labeled_span = LabeledSpan::new_with_span(Some(label), self.span); + + Some(Box::new(iter::once(labeled_span))) + } } #[cfg(feature = "v1")] @@ -87,8 +129,84 @@ impl From for KdlError { message: Some(format!("{}", value.kind)), label: value.label.map(|x| x.into()), help: value.help.map(|x| x.into()), - severity: miette::Severity::Error, + severity: Severity::Error, }], } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kdl_error() { + let kdl_diagnostic = KdlDiagnostic { + input: Default::default(), + span: SourceSpan::new(0.into(), 0), + message: Default::default(), + label: Default::default(), + help: Default::default(), + severity: Default::default(), + }; + + let kdl_error = KdlError { + input: Arc::new("bark? i guess?".to_owned()), + diagnostics: vec![kdl_diagnostic.clone(), kdl_diagnostic], + }; + + // Test `Error` impl + assert_eq!(kdl_error.to_string(), "Failed to parse KDL document"); + assert!(kdl_error.source().is_none()); + + // Test `Diagnostic` impl + let related: Vec<_> = kdl_error.related().unwrap().collect(); + assert_eq!(related.len(), 2); + assert_eq!( + kdl_error + .source_code() + .unwrap() + .read_span(&SourceSpan::new(0.into(), 5), 0, 0) + .unwrap() + .data(), + b"bark?" + ); + } + + #[test] + fn kdl_diagnostic() { + let mut kdl_diagnostic = KdlDiagnostic { + input: Arc::new("Catastrophic failure!!!".to_owned()), + span: SourceSpan::new(0.into(), 3), + message: None, + label: Some("cute".to_owned()), + help: Some("try harder?".to_owned()), + severity: Severity::Error, + }; + + // Test `Error` impl + assert_eq!(kdl_diagnostic.to_string(), "Unexpected error"); + assert!(kdl_diagnostic.source().is_none()); + + kdl_diagnostic.message = Some("mega bad news, kiddo".to_owned()); + + assert_eq!(kdl_diagnostic.to_string(), "mega bad news, kiddo"); + assert!(kdl_diagnostic.source().is_none()); + + // Test `Diagnostic` impl + let labels: Vec<_> = kdl_diagnostic.labels().unwrap().collect(); + assert_eq!(labels.len(), 1); + assert_eq!(labels[0].label().unwrap(), "cute"); + assert_eq!( + kdl_diagnostic + .source_code() + .unwrap() + .read_span(labels[0].inner(), 0, 0) + .unwrap() + .data(), + b"Cat" + ); + assert_eq!(kdl_diagnostic.help().unwrap().to_string(), "try harder?"); + assert_eq!(kdl_diagnostic.severity().unwrap(), Severity::Error); + } +} diff --git a/src/lib.rs b/src/lib.rs index 953b180..c062bce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,12 +107,12 @@ //! //! * `span` (default) - Includes spans in the various document-related structs. //! * `v1` - Adds support for v1 parsing. This will pull in the entire previous -//! version of `kdl-rs`, and so may be fairly heavy. +//! version of `kdl-rs`, and so may be fairly heavy. //! * `v1-fallback` - Implies `v1`. Makes it so the various `*::parse()` and -//! `FromStr` implementations try to parse their inputs as `v2`, and, if that -//! fails, try again with `v1`. For `KdlDocument`, a heuristic will be applied -//! if both `v1` and `v2` parsers fail, to pick which error(s) to return. For -//! other types, only the `v2` parser's errors will be returned. +//! `FromStr` implementations try to parse their inputs as `v2`, and, if that +//! fails, try again with `v1`. For `KdlDocument`, a heuristic will be applied +//! if both `v1` and `v2` parsers fail, to pick which error(s) to return. For +//! other types, only the `v2` parser's errors will be returned. //! //! ## Quirks //! @@ -140,7 +140,7 @@ //! //! ## Minimum Supported Rust Version (MSRV) //! -//! You must be at least `1.71.1` tall to get on this ride. +//! You must be at least `1.81` tall to get on this ride. //! //! ## License //! diff --git a/tools/kdl-lsp/Cargo.toml b/tools/kdl-lsp/Cargo.toml index a3aed04..c70e6b4 100644 --- a/tools/kdl-lsp/Cargo.toml +++ b/tools/kdl-lsp/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" homepage = "https://kdl.dev" repository = "https://github.com/kdl-org/kdl-rs" keywords = ["kdl", "document", "config", "lsp", "language-server"] -rust-version = "1.71.1" +rust-version = "1.81" [dependencies] miette.workspace = true