Skip to main content

jsonvet/
problem.rs

1use std::fmt;
2
3use crate::unicode::{is_bidi_control, is_surrogate};
4use crate::{Dialect, Rule, Severity};
5
6/// One problem in a file: what is wrong, where, and how much it matters.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Problem {
9    pub(crate) offset: usize,
10    pub(crate) severity: Severity,
11    pub(crate) detail: Detail,
12}
13
14impl Problem {
15    /// The byte offset in the text where the problem is. Always on a character boundary.
16    pub fn offset(&self) -> usize {
17        self.offset
18    }
19
20    /// The problem's severity under the config it was checked with. Never `Off`.
21    pub fn severity(&self) -> Severity {
22        self.severity
23    }
24
25    /// The rule the problem belongs to.
26    pub fn rule(&self) -> Rule {
27        match self.detail {
28            Detail::Syntax(_) => Rule::Syntax,
29            Detail::DuplicateKey { .. } => Rule::DuplicateKey,
30            Detail::InvalidNumber { .. } => Rule::InvalidNumber,
31            Detail::InvalidCodePoint { .. } => Rule::InvalidCodePoint,
32            Detail::ByteOrderMark => Rule::ByteOrderMark,
33            Detail::MissingFinalNewline => Rule::MissingFinalNewline,
34        }
35    }
36
37    /// A second place the problem refers to: the first occurrence of a duplicate key, or where
38    /// an unterminated string or comment starts.
39    pub fn related(&self) -> Option<Related> {
40        let (offset, kind) = match &self.detail {
41            Detail::DuplicateKey { first, .. } => (*first, RelatedKind::FirstOccurrence),
42            Detail::Syntax(s) => (s.started?, RelatedKind::Start),
43            _ => return None,
44        };
45        Some(Related { offset, kind })
46    }
47
48    /// The JSONPath of a duplicate key, such as `$.server.port`, or `None` for other problems.
49    /// A path longer than 256 bytes keeps its ends, with `…` in place of the middle, so a path is
50    /// at most 259 bytes: 256 and the three of `…`.
51    ///
52    /// ```
53    /// use jsonvet::{check, Config, Dialect};
54    ///
55    /// let report = check(r#"{"server": {"port": 80, "port": 8080}}"#, &Config::new(Dialect::Json));
56    /// assert_eq!(report.problems()[0].path(), Some("$.server.port"));
57    /// assert_eq!(check("[1 2]", &Config::new(Dialect::Json)).problems()[0].path(), None);
58    /// ```
59    pub fn path(&self) -> Option<&str> {
60        match &self.detail {
61            Detail::DuplicateKey { path, .. } => Some(path),
62            _ => None,
63        }
64    }
65
66    /// The offset of [`related`](Self::related).
67    #[deprecated(since = "3.0.0", note = "use related().map(|r| r.offset)")]
68    pub fn related_offset(&self) -> Option<usize> {
69        self.related().map(|r| r.offset)
70    }
71}
72
73/// A second place a [`Problem`] refers to, and what it is.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75#[non_exhaustive]
76pub struct Related {
77    /// The byte offset in the text. Always on a character boundary.
78    pub offset: usize,
79    /// What sits at the offset.
80    pub kind: RelatedKind,
81}
82
83/// What a [`Related`] offset points at.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85#[non_exhaustive]
86pub enum RelatedKind {
87    /// The first occurrence of a duplicate key.
88    FirstOccurrence,
89    /// The start of the unterminated string or comment a syntax error is about.
90    Start,
91}
92
93/// The message, without its position or rule id.
94impl fmt::Display for Problem {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        self.detail.fmt(f)
97    }
98}
99
100/// What a problem is about.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub(crate) enum Detail {
103    /// A syntax error.
104    Syntax(SyntaxError),
105    /// A key repeated in the same object.
106    DuplicateKey {
107        /// The offset of the key's first occurrence.
108        first: usize,
109        /// The JSONPath of the key, such as `$.server.port`.
110        path: String,
111    },
112    /// A number I-JSON does not allow.
113    InvalidNumber {
114        /// True when the number overflows a double; false for an integer past ±(2⁵³ − 1).
115        overflows: bool,
116    },
117    /// A lone surrogate or a noncharacter in a string.
118    InvalidCodePoint {
119        /// The code point.
120        code_point: u32,
121    },
122    /// A byte order mark at the start of the file.
123    ByteOrderMark,
124    /// A JSON Lines file that does not end with a newline.
125    MissingFinalNewline,
126}
127
128impl fmt::Display for Detail {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Detail::Syntax(s) => s.fmt(f),
132            Detail::DuplicateKey { path, .. } => write!(f, "duplicate key {path}"),
133            Detail::InvalidNumber { overflows: true } => f.write_str("number overflows a double"),
134            Detail::InvalidNumber { overflows: false } => {
135                f.write_str("integer beyond ±(2^53 − 1) cannot be represented exactly")
136            }
137            Detail::InvalidCodePoint { code_point } if is_surrogate(*code_point) => {
138                write!(f, "lone surrogate U+{code_point:04X}")
139            }
140            Detail::InvalidCodePoint { code_point } => {
141                write!(f, "noncharacter U+{code_point:04X}")
142            }
143            Detail::ByteOrderMark => f.write_str("byte order mark at the start of the file"),
144            Detail::MissingFinalNewline => f.write_str("no newline at the end of the file"),
145        }
146    }
147}
148
149/// A syntax error: the first place where the text stops being valid in its dialect.
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub(crate) struct SyntaxError {
152    /// What went wrong.
153    pub(crate) kind: SyntaxKind,
154    /// Where the unterminated string or comment the error is about starts, if it is about one.
155    pub(crate) started: Option<usize>,
156}
157
158/// The kinds of syntax error.
159#[derive(Clone, Debug, PartialEq, Eq)]
160pub(crate) enum SyntaxKind {
161    /// The text ended where something else was needed.
162    UnexpectedEnd {
163        /// What was needed, as the message names it: "a value", "`:`".
164        expected: &'static str,
165    },
166    /// A JSON Lines line ended where something else was needed.
167    UnexpectedLineEnd {
168        /// What was needed, as for `UnexpectedEnd`.
169        expected: &'static str,
170    },
171    /// A character that cannot continue the document here.
172    Unexpected {
173        /// The character found.
174        found: char,
175        /// What was needed instead, as for `UnexpectedEnd`.
176        expected: &'static str,
177    },
178    /// Syntax that a larger dialect allows but this one does not.
179    NotAllowed {
180        /// The syntax as the message names it: "comments are".
181        what: &'static str,
182        /// The dialect being checked.
183        dialect: Dialect,
184    },
185    /// A trailing comma before `]` or `}` in a dialect without them.
186    TrailingComma {
187        /// The closing bracket.
188        close: char,
189        /// The dialect being checked.
190        dialect: Dialect,
191    },
192    /// A string that runs to the end of the text.
193    UnterminatedString,
194    /// A raw line break inside a string.
195    LineBreakInString,
196    /// A raw control character inside a string, which this dialect requires to be escaped.
197    ControlCharacter {
198        /// The character.
199        found: char,
200    },
201    /// A backslash escape this dialect does not define.
202    InvalidEscape {
203        /// The character after the backslash.
204        found: char,
205    },
206    /// `\u` without four hex digits after it.
207    InvalidUnicodeEscape,
208    /// `\x` without two hex digits after it.
209    InvalidHexEscape,
210    /// A block comment without its closing `*/`.
211    UnterminatedComment,
212    /// An empty line in a JSON Lines file.
213    BlankLine,
214    /// An object or array opened one level past the config's maximum depth.
215    TooDeep {
216        /// The limit that was passed.
217        limit: usize,
218    },
219}
220
221impl fmt::Display for SyntaxError {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match &self.kind {
224            SyntaxKind::UnexpectedEnd { expected } => {
225                write!(f, "expected {expected}, found the end of the file")
226            }
227            SyntaxKind::UnexpectedLineEnd { expected } => {
228                write!(f, "expected {expected}, found the end of the line")
229            }
230            SyntaxKind::Unexpected { found, expected } => {
231                write!(f, "expected {expected}, found {}", Shown(*found))
232            }
233            SyntaxKind::NotAllowed { what, dialect } => {
234                write!(f, "{what} not allowed in {dialect}")
235            }
236            SyntaxKind::TrailingComma { close, dialect } => {
237                write!(
238                    f,
239                    "trailing comma before `{close}` is not allowed in {dialect}"
240                )
241            }
242            SyntaxKind::UnterminatedString => f.write_str("unterminated string"),
243            SyntaxKind::LineBreakInString => f.write_str("line break inside a string"),
244            SyntaxKind::ControlCharacter { found } => {
245                write!(
246                    f,
247                    "control character {} inside a string must be escaped",
248                    Shown(*found)
249                )
250            }
251            SyntaxKind::InvalidEscape { found } => {
252                write!(f, "invalid escape `\\{}`", found.escape_debug())
253            }
254            SyntaxKind::InvalidUnicodeEscape => f.write_str("`\\u` needs four hex digits"),
255            SyntaxKind::InvalidHexEscape => f.write_str("`\\x` needs two hex digits"),
256            SyntaxKind::UnterminatedComment => f.write_str("unterminated block comment"),
257            SyntaxKind::BlankLine => {
258                f.write_str("blank line; JSON Lines needs a value on every line")
259            }
260            SyntaxKind::TooDeep { limit } => write!(f, "nested deeper than {limit} levels"),
261        }
262    }
263}
264
265/// A character as a message shows it: in backticks when printable, as U+XXXX otherwise. Bidi
266/// controls show as U+XXXX too, since printed they would reorder the rest of the line.
267struct Shown(char);
268
269impl fmt::Display for Shown {
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        let c = self.0;
272        if c.is_control() || c.is_whitespace() || c == '\u{FEFF}' || is_bidi_control(c) {
273            write!(f, "U+{:04X}", u32::from(c))
274        } else {
275            write!(f, "`{c}`")
276        }
277    }
278}