use std::fmt;
use oneyaml_core::code::{self, Code, CodeRange};
use oneyaml_core::{ColumnUnit, Diagnostic, Label, LineIndex, Path, PathSegment, Severity, Span};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ErrorKind {
Syntax,
Schema,
Limit,
Data,
Io,
Internal,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Location {
line: usize,
column: usize,
index: usize,
}
impl Location {
#[must_use]
pub const fn line(&self) -> usize {
self.line
}
#[must_use]
pub const fn column(&self) -> usize {
self.column
}
#[must_use]
pub const fn index(&self) -> usize {
self.index
}
pub(crate) fn of(src: &str, index: usize) -> Option<Self> {
Self::indexed(&LineIndex::new(src), index)
}
pub(crate) fn indexed(lines: &LineIndex, index: usize) -> Option<Self> {
let at = lines.line_col(index, ColumnUnit::Char)?;
Some(Self {
line: at.line.saturating_add(1),
column: at.column.saturating_add(1),
index,
})
}
}
pub struct Error(Box<Inner>);
struct Inner {
kind: ErrorKind,
diagnostic: Diagnostic,
has_span: bool,
rev_path: Vec<PathSegment>,
location: Option<Location>,
warnings: Vec<Diagnostic>,
io: Option<std::io::Error>,
}
pub(crate) const MAX_SEGMENT_CHARS: usize = 64;
pub(crate) const MAX_MESSAGE_BYTES: usize = 3072;
pub(crate) const MAX_PATH_BYTES: usize = 8192;
pub(crate) const TRUNCATED: &str = "… [message truncated]";
struct Bounded {
out: String,
left: usize,
bytes: bool,
cut: bool,
}
impl fmt::Write for Bounded {
fn write_str(&mut self, s: &str) -> fmt::Result {
for (i, c) in s.char_indices() {
let cost = if self.bytes { c.len_utf8() } else { 1 };
if cost > self.left {
self.out.push_str(s.get(..i).unwrap_or(""));
self.cut = true;
return Err(fmt::Error);
}
self.left = self.left.saturating_sub(cost);
}
self.out.push_str(s);
Ok(())
}
}
fn bounded(args: fmt::Arguments<'_>, left: usize, bytes: bool) -> (String, bool) {
let mut w = Bounded {
out: String::new(),
left,
bytes,
cut: false,
};
let _ = fmt::write(&mut w, args);
(w.out, w.cut)
}
pub(crate) fn short_chars(args: fmt::Arguments<'_>, max: usize) -> String {
let (mut s, cut) = bounded(args, max, false);
if cut {
s.push('…');
}
s
}
pub(crate) fn segment(key: &str) -> String {
short_chars(format_args!("{key}"), MAX_SEGMENT_CHARS)
}
pub(crate) fn message(args: fmt::Arguments<'_>) -> String {
let room = MAX_MESSAGE_BYTES.saturating_sub(TRUNCATED.len());
let (mut s, cut) = bounded(args, room, true);
if cut {
s.push_str(TRUNCATED);
}
s
}
pub(crate) fn shown_path(path: &Path) -> String {
let (mut s, cut) = bounded(format_args!("{path}"), MAX_PATH_BYTES, true);
if cut {
s.push('…');
}
s
}
fn cap_message(text: &mut String) {
if text.len() > MAX_MESSAGE_BYTES {
let room = MAX_MESSAGE_BYTES.saturating_sub(TRUNCATED.len());
let mut at = room.min(text.len());
while !text.is_char_boundary(at) {
at = at.saturating_sub(1);
}
text.truncate(at);
text.push_str(TRUNCATED);
}
}
fn kind_of(code: Code) -> ErrorKind {
if code == code::IO_ERROR || code == code::WRITE_ERROR {
return ErrorKind::Io;
}
match code.range() {
CodeRange::Syntax => ErrorKind::Syntax,
CodeRange::Schema => ErrorKind::Schema,
CodeRange::Limits => ErrorKind::Limit,
CodeRange::Serde => ErrorKind::Data,
_ => ErrorKind::Internal,
}
}
impl Error {
pub(crate) fn new(code: Code, message: impl Into<String>, span: Option<Span>) -> Self {
let label = Label::new(span.unwrap_or(Span::empty(0)), "");
Self::build(Diagnostic::error(code, message, label), span.is_some())
}
pub(crate) fn from_diagnostic(diagnostic: Diagnostic) -> Self {
Self::build(diagnostic, true)
}
fn build(mut diagnostic: Diagnostic, has_span: bool) -> Self {
cap_message(&mut diagnostic.message);
Self(Box::new(Inner {
kind: kind_of(diagnostic.code),
diagnostic,
has_span,
rev_path: Vec::new(),
location: None,
warnings: Vec::new(),
io: None,
}))
}
pub(crate) fn internal(what: &str, span: Option<Span>) -> Self {
Self::new(
code::INTERNAL_ERROR,
format!("internal error: {what} (a bug in oneyaml; please report it)"),
span,
)
}
pub(crate) fn io(err: std::io::Error) -> Self {
let mut e = Self::new(code::IO_ERROR, format!("I/O error: {err}"), None);
e.0.io = Some(err);
e
}
pub(crate) fn write(err: std::io::Error) -> Self {
let mut e = Self::new(
code::WRITE_ERROR,
format!("I/O error while writing: {err}"),
None,
);
e.0.io = Some(err);
e
}
pub(crate) fn with_help(mut self, help: impl Into<String>) -> Self {
let mut help = help.into();
cap_message(&mut help);
self.0.diagnostic.help = Some(help);
self
}
pub(crate) fn with_secondary(mut self, label: Label) -> Self {
self.0.diagnostic.secondary.push(label);
self
}
pub(crate) fn at(mut self, span: Span) -> Self {
if !self.0.has_span {
self.0.diagnostic.primary.span = span;
self.0.has_span = true;
}
self
}
pub(crate) fn without_span(mut self) -> Self {
let inner = &mut *self.0;
inner.has_span = false;
inner.diagnostic.primary.span = Span::empty(0);
inner.location = None;
self
}
pub(crate) fn in_key(mut self, key: &str) -> Self {
self.0.rev_path.push(PathSegment::Key(segment(key)));
self
}
pub(crate) fn in_index(mut self, index: usize) -> Self {
self.0.rev_path.push(PathSegment::Index(index));
self
}
pub(crate) fn locate(mut self, lines: &LineIndex) -> Self {
let inner = &mut *self.0;
if inner.location.is_none() && inner.has_span {
inner.location = Location::indexed(lines, inner.diagnostic.primary.span.start);
}
self
}
pub(crate) fn finish(mut self, src: Option<&str>, warnings: Vec<Diagnostic>) -> Self {
let inner = &mut *self.0;
if inner.diagnostic.path.is_none() && !inner.rev_path.is_empty() {
let mut path = Path::new();
for seg in inner.rev_path.drain(..).rev() {
match seg {
PathSegment::Key(k) => path.push_key(k),
PathSegment::Index(i) => path.push_index(i),
_ => {}
}
}
inner.diagnostic.path = Some(path);
}
if inner.location.is_none() && inner.has_span {
inner.location = src.and_then(|s| Location::of(s, inner.diagnostic.primary.span.start));
}
if inner.warnings.is_empty() {
inner.warnings = warnings;
}
self
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.0.kind
}
#[must_use]
pub fn code(&self) -> Code {
self.0.diagnostic.code
}
#[must_use]
pub fn diagnostic(&self) -> &Diagnostic {
&self.0.diagnostic
}
#[must_use]
pub fn warnings(&self) -> &[Diagnostic] {
&self.0.warnings
}
#[must_use]
pub fn path(&self) -> Option<&Path> {
self.0.diagnostic.path.as_ref()
}
#[must_use]
pub fn span(&self) -> Option<Span> {
self.0.has_span.then_some(self.0.diagnostic.primary.span)
}
#[must_use]
pub fn location(&self) -> Option<Location> {
self.0.location
}
#[must_use]
pub fn render(&self, src: &str) -> String {
let inner = &*self.0;
match inner.location {
Some(l) => {
let numbers = crate::render::Numbers::Anchor {
at: l.index,
line: l.line.saturating_sub(1),
col: l.column.saturating_sub(1),
};
crate::render::render(&inner.diagnostic, src, numbers, inner.has_span)
}
None => self.render_indexed(src, &LineIndex::new(src)),
}
}
#[must_use]
pub fn render_indexed(&self, src: &str, index: &LineIndex) -> String {
let numbers = crate::render::Numbers::Index(index);
crate::render::render(&self.0.diagnostic, src, numbers, self.0.has_span)
}
}
impl crate::Render for Error {
fn render(&self, src: &str) -> String {
Error::render(self, src)
}
fn render_indexed(&self, src: &str, index: &LineIndex) -> String {
Error::render_indexed(self, src, index)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0.diagnostic.message)?;
if let Some(path) = self.path().filter(|p| !p.is_empty()) {
write!(f, " at {}", shown_path(path))?;
}
if let Some(l) = self.0.location {
write!(f, " ({}:{})", l.line, l.column)?;
}
Ok(())
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error({}: {self})", self.code())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0
.io
.as_ref()
.map(|e| e as &(dyn std::error::Error + 'static))
}
}
pub(crate) fn warning(code: Code, message: impl Into<String>, span: Span) -> Diagnostic {
Diagnostic::new(code, Severity::Warning, message, Label::new(span, ""))
}
impl serde::de::Error for Error {
fn custom<T: fmt::Display>(msg: T) -> Self {
Self::new(code::CUSTOM, message(format_args!("{msg}")), None)
}
fn invalid_type(unexp: serde::de::Unexpected<'_>, exp: &dyn serde::de::Expected) -> Self {
Self::new(
code::TYPE_MISMATCH,
message(format_args!("invalid type: {unexp}, expected {exp}")),
None,
)
}
fn invalid_value(unexp: serde::de::Unexpected<'_>, exp: &dyn serde::de::Expected) -> Self {
Self::new(
code::INVALID_VALUE,
message(format_args!("invalid value: {unexp}, expected {exp}")),
None,
)
}
fn invalid_length(len: usize, exp: &dyn serde::de::Expected) -> Self {
Self::new(
code::INVALID_LENGTH,
message(format_args!("invalid length {len}, expected {exp}")),
None,
)
}
fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
let variant = segment(variant);
Self::new(
code::UNKNOWN_VARIANT,
message(format_args!(
"unknown variant `{variant}`, {}",
one_of(expected)
)),
None,
)
}
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
let field = segment(field);
Self::new(
code::UNKNOWN_FIELD,
message(format_args!(
"unknown field `{field}`, {}",
one_of(expected)
)),
None,
)
}
fn missing_field(field: &'static str) -> Self {
Self::new(
code::MISSING_FIELD,
format!("missing field `{field}`"),
None,
)
}
fn duplicate_field(field: &'static str) -> Self {
Self::new(
code::DUPLICATE_FIELD,
format!("duplicate field `{field}`"),
None,
)
}
}
impl serde::ser::Error for Error {
fn custom<T: fmt::Display>(msg: T) -> Self {
if crate::value::is_tls_gone::<T>() {
return crate::value::tls_gone_ser();
}
Self::new(code::SERIALIZE_CUSTOM, message(format_args!("{msg}")), None)
}
}
fn one_of(names: &[&str]) -> String {
match names {
[] => "there are none".to_owned(),
[one] => format!("expected `{one}`"),
[a, b] => format!("expected `{a}` or `{b}`"),
_ => {
let list: Vec<String> = names.iter().map(|n| format!("`{n}`")).collect();
format!("expected one of {}", list.join(", "))
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
use super::*;
use serde::de::Error as _;
#[test]
fn location_counts_all_breaks() {
let src = "a\r\nb\rc\nd\u{e9}x";
let l = Location::of(src, src.find('x').unwrap()).unwrap();
assert_eq!((l.line(), l.column()), (4, 3));
assert_eq!(Location::of(src, 0).unwrap().line(), 1);
assert_eq!(Location::of(src, 99), None);
assert_eq!(Location::of("\u{e9}", 1), None);
}
#[test]
fn display_is_compact_and_kind_follows_code() {
let e = Error::custom("invalid port: 99999")
.at(Span::new(4, 9))
.in_key("port")
.in_index(2)
.in_key("ports")
.in_key("spec")
.at(Span::new(0, 1))
.finish(Some("a:\n 99999"), Vec::new());
assert_eq!(
e.to_string(),
"invalid port: 99999 at spec.ports[2].port (2:2)"
);
assert_eq!(e.kind(), ErrorKind::Data);
assert_eq!(e.code(), code::CUSTOM);
assert_eq!(e.span(), Some(Span::new(4, 9)));
assert_eq!(e.path().unwrap().to_string(), "spec.ports[2].port");
let no_span = Error::missing_field("x").finish(None, Vec::new());
assert_eq!(no_span.to_string(), "missing field `x`");
assert_eq!(no_span.span(), None);
assert_eq!(Error::internal("x", None).kind(), ErrorKind::Internal);
let io = Error::io(std::io::Error::other("boom"));
assert_eq!(io.kind(), ErrorKind::Io);
assert!(std::error::Error::source(&io).is_some());
assert!(format!("{io:?}").contains("Y3010"));
assert_eq!(one_of(&[]), "there are none");
assert_eq!(one_of(&["a", "b"]), "expected `a` or `b`");
}
}