mirror of https://github.com/zkat/miette.git
feat(serde): Add `serde` support (#264)
Fixes: https://github.com/zkat/miette/issues/260
This commit is contained in:
parent
024145dbdd
commit
c25676cb1f
|
|
@ -27,6 +27,7 @@ supports-unicode = { version = "2.0.0", optional = true }
|
||||||
backtrace = { version = "0.3.61", optional = true }
|
backtrace = { version = "0.3.61", optional = true }
|
||||||
terminal_size = { version = "0.1.17", optional = true }
|
terminal_size = { version = "0.1.17", optional = true }
|
||||||
backtrace-ext = { version = "0.2.1", optional = true }
|
backtrace-ext = { version = "0.2.1", optional = true }
|
||||||
|
serde = { version = "1.0.162", features = ["derive"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
semver = "1.0.4"
|
semver = "1.0.4"
|
||||||
|
|
@ -40,6 +41,8 @@ syn = { version = "2.0", features = ["full"] }
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
|
||||||
|
serde_json = "1.0.64"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
no-format-args-capture = []
|
no-format-args-capture = []
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,10 @@ macro_rules! miette {
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! diagnostic {
|
macro_rules! diagnostic {
|
||||||
($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {{
|
($fmt:literal $($arg:tt)*) => {{
|
||||||
|
$crate::MietteDiagnostic::new(format!($fmt $($arg)*))
|
||||||
|
}};
|
||||||
|
($($key:ident = $value:expr,)+ $fmt:literal $($arg:tt)*) => {{
|
||||||
let mut diag = $crate::MietteDiagnostic::new(format!($fmt $($arg)*));
|
let mut diag = $crate::MietteDiagnostic::new(format!($fmt $($arg)*));
|
||||||
$(diag.$key = Some($value.into());)*
|
$(diag.$key = Some($value.into());)*
|
||||||
diag
|
diag
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ use std::{
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{Diagnostic, LabeledSpan, Severity};
|
use crate::{Diagnostic, LabeledSpan, Severity};
|
||||||
|
|
||||||
/// Diagnostic that can be created at runtime.
|
/// Diagnostic that can be created at runtime.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct MietteDiagnostic {
|
pub struct MietteDiagnostic {
|
||||||
/// Displayed diagnostic message
|
/// Displayed diagnostic message
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
|
@ -15,17 +19,22 @@ pub struct MietteDiagnostic {
|
||||||
/// in the toplevel crate's documentation for easy searching.
|
/// in the toplevel crate's documentation for easy searching.
|
||||||
/// Rust path format (`foo::bar::baz`) is recommended, but more classic
|
/// Rust path format (`foo::bar::baz`) is recommended, but more classic
|
||||||
/// codes like `E0123` will work just fine
|
/// codes like `E0123` will work just fine
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub code: Option<String>,
|
pub code: Option<String>,
|
||||||
/// [`Diagnostic`] severity. Intended to be used by
|
/// [`Diagnostic`] severity. Intended to be used by
|
||||||
/// [`ReportHandler`](crate::ReportHandler)s to change the way different
|
/// [`ReportHandler`](crate::ReportHandler)s to change the way different
|
||||||
/// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]
|
/// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub severity: Option<Severity>,
|
pub severity: Option<Severity>,
|
||||||
/// Additional help text related to this Diagnostic
|
/// Additional help text related to this Diagnostic
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub help: Option<String>,
|
pub help: Option<String>,
|
||||||
/// URL to visit for a more detailed explanation/help about this
|
/// URL to visit for a more detailed explanation/help about this
|
||||||
/// [`Diagnostic`].
|
/// [`Diagnostic`].
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
/// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
|
/// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub labels: Option<Vec<LabeledSpan>>,
|
pub labels: Option<Vec<LabeledSpan>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,3 +257,109 @@ impl MietteDiagnostic {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_miette_diagnostic() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::diagnostic;
|
||||||
|
|
||||||
|
let diag = diagnostic!("message");
|
||||||
|
let json = json!({ "message": "message" });
|
||||||
|
assert_eq!(json!(diag), json);
|
||||||
|
|
||||||
|
let diag = diagnostic!(
|
||||||
|
code = "code",
|
||||||
|
help = "help",
|
||||||
|
url = "url",
|
||||||
|
labels = [
|
||||||
|
LabeledSpan::at_offset(0, "label1"),
|
||||||
|
LabeledSpan::at(1..3, "label2")
|
||||||
|
],
|
||||||
|
severity = Severity::Warning,
|
||||||
|
"message"
|
||||||
|
);
|
||||||
|
let json = json!({
|
||||||
|
"message": "message",
|
||||||
|
"code": "code",
|
||||||
|
"help": "help",
|
||||||
|
"url": "url",
|
||||||
|
"severity": "Warning",
|
||||||
|
"labels": [
|
||||||
|
{
|
||||||
|
"span": {
|
||||||
|
"offset": 0,
|
||||||
|
"length": 0
|
||||||
|
},
|
||||||
|
"label": "label1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"span": {
|
||||||
|
"offset": 1,
|
||||||
|
"length": 2
|
||||||
|
},
|
||||||
|
"label": "label2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
assert_eq!(json!(diag), json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_miette_diagnostic() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::diagnostic;
|
||||||
|
|
||||||
|
let json = json!({ "message": "message" });
|
||||||
|
let diag = diagnostic!("message");
|
||||||
|
assert_eq!(diag, serde_json::from_value(json).unwrap());
|
||||||
|
|
||||||
|
let json = json!({
|
||||||
|
"message": "message",
|
||||||
|
"help": null,
|
||||||
|
"code": null,
|
||||||
|
"severity": null,
|
||||||
|
"url": null,
|
||||||
|
"labels": null
|
||||||
|
});
|
||||||
|
assert_eq!(diag, serde_json::from_value(json).unwrap());
|
||||||
|
|
||||||
|
let diag = diagnostic!(
|
||||||
|
code = "code",
|
||||||
|
help = "help",
|
||||||
|
url = "url",
|
||||||
|
labels = [
|
||||||
|
LabeledSpan::at_offset(0, "label1"),
|
||||||
|
LabeledSpan::at(1..3, "label2")
|
||||||
|
],
|
||||||
|
severity = Severity::Warning,
|
||||||
|
"message"
|
||||||
|
);
|
||||||
|
let json = json!({
|
||||||
|
"message": "message",
|
||||||
|
"code": "code",
|
||||||
|
"help": "help",
|
||||||
|
"url": "url",
|
||||||
|
"severity": "Warning",
|
||||||
|
"labels": [
|
||||||
|
{
|
||||||
|
"span": {
|
||||||
|
"offset": 0,
|
||||||
|
"length": 0
|
||||||
|
},
|
||||||
|
"label": "label1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"span": {
|
||||||
|
"offset": 1,
|
||||||
|
"length": 2
|
||||||
|
},
|
||||||
|
"label": "label2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
assert_eq!(diag, serde_json::from_value(json).unwrap());
|
||||||
|
}
|
||||||
|
|
|
||||||
151
src/protocol.rs
151
src/protocol.rs
|
|
@ -9,6 +9,9 @@ use std::{
|
||||||
panic::Location,
|
panic::Location,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::MietteError;
|
use crate::MietteError;
|
||||||
|
|
||||||
/// Adds rich metadata to your Error that can be used by
|
/// Adds rich metadata to your Error that can be used by
|
||||||
|
|
@ -163,6 +166,7 @@ impl From<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Sen
|
||||||
[`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
|
[`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
|
||||||
*/
|
*/
|
||||||
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub enum Severity {
|
pub enum Severity {
|
||||||
/// Just some help. Here's how you could be doing it better.
|
/// Just some help. Here's how you could be doing it better.
|
||||||
Advice,
|
Advice,
|
||||||
|
|
@ -179,6 +183,31 @@ impl Default for Severity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_severity() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
assert_eq!(json!(Severity::Advice), json!("Advice"));
|
||||||
|
assert_eq!(json!(Severity::Warning), json!("Warning"));
|
||||||
|
assert_eq!(json!(Severity::Error), json!("Error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_severity() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
let severity: Severity = serde_json::from_value(json!("Advice")).unwrap();
|
||||||
|
assert_eq!(severity, Severity::Advice);
|
||||||
|
|
||||||
|
let severity: Severity = serde_json::from_value(json!("Warning")).unwrap();
|
||||||
|
assert_eq!(severity, Severity::Warning);
|
||||||
|
|
||||||
|
let severity: Severity = serde_json::from_value(json!("Error")).unwrap();
|
||||||
|
assert_eq!(severity, Severity::Error);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Represents readable source code of some sort.
|
Represents readable source code of some sort.
|
||||||
|
|
||||||
|
|
@ -203,14 +232,16 @@ pub trait SourceCode: Send + Sync {
|
||||||
|
|
||||||
/// A labeled [`SourceSpan`].
|
/// A labeled [`SourceSpan`].
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct LabeledSpan {
|
pub struct LabeledSpan {
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
label: Option<String>,
|
label: Option<String>,
|
||||||
span: SourceSpan,
|
span: SourceSpan,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LabeledSpan {
|
impl LabeledSpan {
|
||||||
/// Makes a new labeled span.
|
/// Makes a new labeled span.
|
||||||
pub fn new(label: Option<String>, offset: ByteOffset, len: ByteOffset) -> Self {
|
pub fn new(label: Option<String>, offset: ByteOffset, len: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
label,
|
label,
|
||||||
span: (offset, len).into(),
|
span: (offset, len).into(),
|
||||||
|
|
@ -299,6 +330,53 @@ impl LabeledSpan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_labeled_span() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
json!(LabeledSpan::new(None, 0, 0)),
|
||||||
|
json!({
|
||||||
|
"span": { "offset": 0, "length": 0 }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
|
||||||
|
json!({
|
||||||
|
"label": "label",
|
||||||
|
"span": { "offset": 0, "length": 0 }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_labeled_span() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
let span: LabeledSpan = serde_json::from_value(json!({
|
||||||
|
"label": null,
|
||||||
|
"span": { "offset": 0, "length": 0 }
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(span, LabeledSpan::new(None, 0, 0));
|
||||||
|
|
||||||
|
let span: LabeledSpan = serde_json::from_value(json!({
|
||||||
|
"span": { "offset": 0, "length": 0 }
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(span, LabeledSpan::new(None, 0, 0));
|
||||||
|
|
||||||
|
let span: LabeledSpan = serde_json::from_value(json!({
|
||||||
|
"label": "label",
|
||||||
|
"span": { "offset": 0, "length": 0 }
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Contents of a [`SourceCode`] covered by [`SourceSpan`].
|
Contents of a [`SourceCode`] covered by [`SourceSpan`].
|
||||||
|
|
||||||
|
|
@ -402,15 +480,14 @@ impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/// Span within a [`SourceCode`]
|
||||||
Span within a [`SourceCode`] with an associated message.
|
|
||||||
*/
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct SourceSpan {
|
pub struct SourceSpan {
|
||||||
/// The start of the span.
|
/// The start of the span.
|
||||||
offset: SourceOffset,
|
offset: SourceOffset,
|
||||||
/// The total length of the span. Think of this as an offset from `start`.
|
/// The total length of the span
|
||||||
length: SourceOffset,
|
length: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceSpan {
|
impl SourceSpan {
|
||||||
|
|
@ -418,7 +495,7 @@ impl SourceSpan {
|
||||||
pub fn new(start: SourceOffset, length: SourceOffset) -> Self {
|
pub fn new(start: SourceOffset, length: SourceOffset) -> Self {
|
||||||
Self {
|
Self {
|
||||||
offset: start,
|
offset: start,
|
||||||
length,
|
length: length.offset(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,31 +506,28 @@ impl SourceSpan {
|
||||||
|
|
||||||
/// Total length of the [`SourceSpan`], in bytes.
|
/// Total length of the [`SourceSpan`], in bytes.
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.length.offset()
|
self.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this [`SourceSpan`] has a length of zero. It may still be useful
|
/// Whether this [`SourceSpan`] has a length of zero. It may still be useful
|
||||||
/// to point to a specific point.
|
/// to point to a specific point.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.length.offset() == 0
|
self.length == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(ByteOffset, ByteOffset)> for SourceSpan {
|
impl From<(ByteOffset, usize)> for SourceSpan {
|
||||||
fn from((start, len): (ByteOffset, ByteOffset)) -> Self {
|
fn from((start, len): (ByteOffset, usize)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
offset: start.into(),
|
offset: start.into(),
|
||||||
length: len.into(),
|
length: len,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(SourceOffset, SourceOffset)> for SourceSpan {
|
impl From<(SourceOffset, SourceOffset)> for SourceSpan {
|
||||||
fn from((start, len): (SourceOffset, SourceOffset)) -> Self {
|
fn from((start, len): (SourceOffset, SourceOffset)) -> Self {
|
||||||
Self {
|
Self::new(start, len)
|
||||||
offset: start,
|
|
||||||
length: len,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,17 +535,14 @@ impl From<std::ops::Range<ByteOffset>> for SourceSpan {
|
||||||
fn from(range: std::ops::Range<ByteOffset>) -> Self {
|
fn from(range: std::ops::Range<ByteOffset>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
offset: range.start.into(),
|
offset: range.start.into(),
|
||||||
length: range.len().into(),
|
length: range.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SourceOffset> for SourceSpan {
|
impl From<SourceOffset> for SourceSpan {
|
||||||
fn from(offset: SourceOffset) -> Self {
|
fn from(offset: SourceOffset) -> Self {
|
||||||
Self {
|
Self { offset, length: 0 }
|
||||||
offset,
|
|
||||||
length: 0.into(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -479,11 +550,31 @@ impl From<ByteOffset> for SourceSpan {
|
||||||
fn from(offset: ByteOffset) -> Self {
|
fn from(offset: ByteOffset) -> Self {
|
||||||
Self {
|
Self {
|
||||||
offset: offset.into(),
|
offset: offset.into(),
|
||||||
length: 0.into(),
|
length: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_source_span() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
json!(SourceSpan::from(0)),
|
||||||
|
json!({ "offset": 0, "length": 0})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_source_span() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
|
||||||
|
assert_eq!(span, SourceSpan::from(0))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
"Raw" type for the byte offset from the beginning of a [`SourceCode`].
|
"Raw" type for the byte offset from the beginning of a [`SourceCode`].
|
||||||
*/
|
*/
|
||||||
|
|
@ -493,6 +584,7 @@ pub type ByteOffset = usize;
|
||||||
Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`]
|
Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`]
|
||||||
*/
|
*/
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct SourceOffset(ByteOffset);
|
pub struct SourceOffset(ByteOffset);
|
||||||
|
|
||||||
impl SourceOffset {
|
impl SourceOffset {
|
||||||
|
|
@ -576,3 +668,18 @@ fn test_source_offset_from_location() {
|
||||||
source.len()
|
source.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_source_offset() {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
assert_eq!(json!(SourceOffset::from(0)), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_source_offset() {
|
||||||
|
let offset: SourceOffset = serde_json::from_str("0").unwrap();
|
||||||
|
assert_eq!(offset, SourceOffset::from(0))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue