use std::fmt;
use crate::unicode::{is_bidi_control, is_surrogate};
use crate::{Dialect, Rule, Severity};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Problem {
pub(crate) offset: usize,
pub(crate) severity: Severity,
pub(crate) detail: Detail,
}
impl Problem {
pub fn offset(&self) -> usize {
self.offset
}
pub fn severity(&self) -> Severity {
self.severity
}
pub fn rule(&self) -> Rule {
match self.detail {
Detail::Syntax(_) => Rule::Syntax,
Detail::DuplicateKey { .. } => Rule::DuplicateKey,
Detail::InvalidNumber { .. } => Rule::InvalidNumber,
Detail::InvalidCodePoint { .. } => Rule::InvalidCodePoint,
Detail::ByteOrderMark => Rule::ByteOrderMark,
Detail::MissingFinalNewline => Rule::MissingFinalNewline,
}
}
pub fn related(&self) -> Option<Related> {
let (offset, kind) = match &self.detail {
Detail::DuplicateKey { first, .. } => (*first, RelatedKind::FirstOccurrence),
Detail::Syntax(s) => {
let kind = match s.kind {
SyntaxKind::Unclosed { .. } => RelatedKind::Opened,
_ => RelatedKind::Start,
};
(s.started?, kind)
}
_ => return None,
};
Some(Related { offset, kind })
}
pub fn path(&self) -> Option<&str> {
match &self.detail {
Detail::DuplicateKey { path, .. } => Some(path),
_ => None,
}
}
#[deprecated(since = "3.0.0", note = "use related().map(|r| r.offset)")]
pub fn related_offset(&self) -> Option<usize> {
self.related().map(|r| r.offset)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Related {
pub offset: usize,
pub kind: RelatedKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RelatedKind {
FirstOccurrence,
Start,
Opened,
}
impl fmt::Display for Problem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.detail.fmt(f)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Detail {
Syntax(SyntaxError),
DuplicateKey {
first: usize,
path: String,
},
InvalidNumber {
overflows: bool,
},
InvalidCodePoint {
code_point: u32,
},
ByteOrderMark,
MissingFinalNewline,
}
impl fmt::Display for Detail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Detail::Syntax(s) => s.fmt(f),
Detail::DuplicateKey { path, .. } => write!(f, "duplicate key {path}"),
Detail::InvalidNumber { overflows: true } => f.write_str("number overflows a double"),
Detail::InvalidNumber { overflows: false } => {
f.write_str("integer beyond ±(2^53 − 1) cannot be represented exactly")
}
Detail::InvalidCodePoint { code_point } if is_surrogate(*code_point) => {
write!(f, "lone surrogate U+{code_point:04X}")
}
Detail::InvalidCodePoint { code_point } => {
write!(f, "noncharacter U+{code_point:04X}")
}
Detail::ByteOrderMark => f.write_str("byte order mark at the start of the file"),
Detail::MissingFinalNewline => f.write_str("no newline at the end of the file"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SyntaxError {
pub(crate) kind: SyntaxKind,
pub(crate) started: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SyntaxKind {
UnexpectedEnd {
expected: &'static str,
},
UnexpectedLineEnd {
expected: &'static str,
},
Unclosed {
open: char,
expected: &'static str,
},
MissingComma,
UnexpectedWord {
word: String,
expected: &'static str,
},
LeadingZero,
Unexpected {
found: char,
expected: &'static str,
},
NotAllowed {
what: &'static str,
dialect: Dialect,
hint: bool,
},
TrailingComma {
close: char,
dialect: Dialect,
},
UnterminatedString,
LineBreakInString,
ControlCharacter {
found: char,
},
InvalidEscape {
found: char,
},
InvalidUnicodeEscape,
InvalidHexEscape,
UnterminatedComment,
BlankLine,
TooDeep {
limit: usize,
},
}
impl fmt::Display for SyntaxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
SyntaxKind::UnexpectedEnd { expected } => {
write!(f, "expected {expected}, found the end of the file")
}
SyntaxKind::UnexpectedLineEnd { expected } => {
write!(f, "expected {expected}, found the end of the line")
}
SyntaxKind::Unclosed { open, expected } => {
write!(
f,
"unclosed `{open}`: expected {expected}, found the end of the file"
)
}
SyntaxKind::MissingComma => f.write_str("missing `,`"),
SyntaxKind::UnexpectedWord { word, expected } => {
write!(f, "expected {expected}, found `{word}`")?;
match literal_for(word).filter(|_| *expected == "a value") {
Some(literal) => write!(f, "; did you mean `{literal}`?"),
None => Ok(()),
}
}
SyntaxKind::LeadingZero => f.write_str("leading zeros are not allowed"),
SyntaxKind::Unexpected { found, expected } => {
write!(f, "expected {expected}, found {}", Shown(*found))
}
SyntaxKind::NotAllowed {
what,
dialect,
hint,
} => {
write!(f, "{what} not allowed in {dialect}")?;
let allows = if *what == "comments are" {
"jsonc"
} else {
"json5"
};
if *hint {
write!(f, "; the {allows} dialect allows this")?;
}
Ok(())
}
SyntaxKind::TrailingComma { close, dialect } => {
write!(
f,
"trailing comma before `{close}` is not allowed in {dialect}"
)?;
match dialect {
Dialect::JsonLines => Ok(()),
_ => f.write_str("; the jwcc and json5 dialects allow this"),
}
}
SyntaxKind::UnterminatedString => f.write_str("unterminated string"),
SyntaxKind::LineBreakInString => f.write_str("line break inside a string"),
SyntaxKind::ControlCharacter { found } => {
write!(
f,
"control character {} inside a string must be escaped",
Shown(*found)
)
}
SyntaxKind::InvalidEscape { found } => {
write!(f, "invalid escape `\\{}`", found.escape_debug())
}
SyntaxKind::InvalidUnicodeEscape => f.write_str("`\\u` needs four hex digits"),
SyntaxKind::InvalidHexEscape => f.write_str("`\\x` needs two hex digits"),
SyntaxKind::UnterminatedComment => f.write_str("unterminated block comment"),
SyntaxKind::BlankLine => {
f.write_str("blank line; JSON Lines needs a value on every line")
}
SyntaxKind::TooDeep { limit } => write!(f, "nested deeper than {limit} levels"),
}
}
}
fn literal_for(word: &str) -> Option<&'static str> {
let literal = match word.to_ascii_lowercase().as_str() {
"true" => "true",
"false" => "false",
"null" | "none" | "nil" | "undefined" => "null",
_ => return None,
};
(literal != word).then_some(literal)
}
struct Shown(char);
impl fmt::Display for Shown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let c = self.0;
if c.is_control() || c.is_whitespace() || c == '\u{FEFF}' || is_bidi_control(c) {
write!(f, "U+{:04X}", u32::from(c))
} else {
write!(f, "`{c}`")
}
}
}