Skip to main content

jjpwrgem_parse/
diagnostics.rs

1use core::range::Range;
2use std::{borrow::Cow, path::Path};
3
4use crate::{
5    Error, ErrorKind,
6    tokens::{ErrorToken, JsonCharOption, Token, TokenOption, TokenWithContext, lexical::JsonChar},
7};
8pub(crate) const EXPECTED_COMMA_OR_CLOSED_CURLY_MESSAGE: &str = "the preceding key/value pair";
9pub(crate) const INSERT_MISSING_CLOSED_BRACE_HELP: &str = "insert the missing closed brace";
10pub(crate) const REMOVE_EXPONENT_HELP: &str = "remove the exponent";
11pub(crate) const INSERT_EXPONENT_PLACEHOLDER_DIGIT_HELP: &str = "insert a placeholder digit";
12pub(crate) const EXPONENT_PLACEHOLDER_DIGIT: &str = "5";
13
14#[derive(Debug, PartialEq, Eq, Clone)]
15pub struct Context<'a> {
16    pub message: Cow<'a, str>,
17    pub span: Range<usize>,
18    pub source: Source<'a>,
19}
20
21impl<'a> Context<'a> {
22    fn new(message: impl Into<Cow<'a, str>>, span: Range<usize>, source: Source<'a>) -> Self {
23        Self {
24            message: message.into(),
25            span,
26            source,
27        }
28    }
29}
30#[derive(Debug, PartialEq, Eq, Clone)]
31pub struct Patch<'a> {
32    pub message: Cow<'a, str>,
33    pub span: Range<usize>,
34    pub source: Source<'a>,
35    pub replacement: Cow<'a, str>,
36}
37
38impl<'a> Patch<'a> {
39    fn new(
40        message: impl Into<Cow<'a, str>>,
41        span: Range<usize>,
42        source: Source<'a>,
43        replacement: impl Into<Cow<'a, str>>,
44    ) -> Self {
45        Self {
46            message: message.into(),
47            span,
48            source,
49            replacement: replacement.into(),
50        }
51    }
52}
53
54#[derive(Debug, PartialEq, Eq, Clone, Copy)]
55pub enum Source<'a> {
56    Stdin(&'a str),
57    File { source: &'a str, path: &'a Path },
58}
59#[derive(Debug)]
60pub struct Diagnostic<'a> {
61    pub message: String,
62    pub range: Option<Range<usize>>,
63    pub context: Vec<Context<'a>>,
64    pub patches: Vec<Patch<'a>>,
65    pub source: Source<'a>,
66}
67
68fn error_source(error: &Error) -> Source<'_> {
69    if error.source_name == "stdin" {
70        Source::Stdin(error.source_text.as_str())
71    } else {
72        Source::File {
73            source: error.source_text.as_str(),
74            path: Path::new(error.source_name.as_str()),
75        }
76    }
77}
78
79fn exponent_patch_suggestions(exponent_range: Range<usize>, source: Source<'_>) -> Vec<Patch<'_>> {
80    vec![
81        Patch::new(REMOVE_EXPONENT_HELP, exponent_range, source, ""),
82        Patch::new(
83            INSERT_EXPONENT_PLACEHOLDER_DIGIT_HELP,
84            exponent_range.end..exponent_range.end,
85            source,
86            EXPONENT_PLACEHOLDER_DIGIT,
87        ),
88    ]
89}
90
91impl<'a> From<&'a Error> for Vec<Patch<'a>> {
92    fn from(error: &'a Error) -> Self {
93        let source = error_source(error);
94        match &error.kind {
95            ErrorKind::ExpectedKey(
96                TokenWithContext {
97                    token: Token::Comma,
98                    range: comma_range,
99                },
100                TokenOption(Some(_)),
101            )
102            | ErrorKind::ExpectedValue(
103                Some(TokenWithContext {
104                    token: Token::Comma,
105                    range: comma_range,
106                }),
107                TokenOption(Some(ErrorToken {
108                    tag: Token::ClosedSquareBracket,
109                    ..
110                })),
111            ) => vec![Patch::new(
112                "consider removing the trailing comma",
113                *comma_range,
114                source,
115                "",
116            )],
117            ErrorKind::ExpectedKey(
118                TokenWithContext {
119                    token: Token::Comma,
120                    range: comma_range,
121                },
122                TokenOption(None),
123            ) => vec![Patch::new(
124                "consider replacing the trailing comma with a closed curly brace",
125                *comma_range,
126                source,
127                "}",
128            )],
129            ErrorKind::ExpectedColon(ctx, TokenOption(None)) => {
130                let r = ctx.range;
131                vec![Patch::new(
132                    "insert colon, placeholder value, and closing curly brace",
133                    r.end..r.end,
134                    source,
135                    r#": "garlic bread" }"#,
136                )]
137            }
138            ErrorKind::ExpectedColon(
139                ctx,
140                TokenOption(Some(ErrorToken {
141                    tag: Token::Comma | Token::ClosedCurlyBrace,
142                    ..
143                })),
144            ) => {
145                let r = ctx.range;
146                vec![Patch::new(
147                    "insert colon and placeholder value",
148                    r.end..r.end,
149                    source,
150                    r#": "🐟🛹""#,
151                )]
152            }
153            ErrorKind::ExpectedColon(ctx, _) => {
154                let r = ctx.range;
155                vec![Patch::new(
156                    "insert the missing colon",
157                    r.end..r.end,
158                    source,
159                    ": ",
160                )]
161            }
162            ErrorKind::ExpectedEntryOrClosedDelimiter {
163                expected,
164                found: TokenOption(None),
165                ..
166            } => vec![Patch::new(
167                Cow::Owned(format!("insert the missing closed delimiter `{expected}`")),
168                error.range.end..error.range.end,
169                source,
170                expected.to_string(),
171            )],
172            ErrorKind::ExpectedEntryOrClosedDelimiter {
173                found: TokenOption(Some(_)),
174                ..
175            } => Vec::new(),
176            ErrorKind::ExpectedCommaOrClosedCurlyBrace {
177                range,
178                found:
179                    TokenOption(Some(
180                        t @ ErrorToken {
181                            tag: Token::String, ..
182                        },
183                    )),
184                ..
185            } => {
186                let s = t.content();
187                vec![Patch::new(
188                    Cow::Owned(format!("is {s} a key? consider adding a comma")),
189                    range.end..range.end,
190                    source,
191                    ",",
192                )]
193            }
194            ErrorKind::ExpectedCommaOrClosedCurlyBrace {
195                range,
196                found: TokenOption(None),
197                ..
198            } => vec![Patch::new(
199                INSERT_MISSING_CLOSED_BRACE_HELP,
200                range.end..range.end,
201                source,
202                "}",
203            )],
204            ErrorKind::ExpectedCommaOrClosedCurlyBrace { .. } => Vec::new(),
205            ErrorKind::ExpectedValue(_, TokenOption(None)) => vec![Patch::new(
206                "insert a placeholder value",
207                error.range.end..error.range.end,
208                source,
209                " \"rust is a must\"",
210            )],
211            ErrorKind::ExpectedValue(
212                _,
213                TokenOption(Some(ErrorToken {
214                    tag: Token::ClosedCurlyBrace,
215                    ..
216                })),
217            ) => vec![Patch::new(
218                "consider adding the missing open curly brace",
219                error.range.end - 1..error.range.end,
220                source,
221                "{}",
222            )],
223            ErrorKind::ExpectedValue(_, _) => Vec::new(),
224            ErrorKind::UnexpectedControlCharacterInString(escaped) => vec![Patch::new(
225                "replace the control character with its escaped form",
226                error.range,
227                source,
228                escaped.to_string(),
229            )],
230            ErrorKind::TokenAfterEnd(token) => vec![Patch::new(
231                format!("consider removing the trailing content (starting with {token})"),
232                error.range.start..error.source_text.len(),
233                source,
234                "",
235            )],
236            ErrorKind::ExpectedDigitFollowingMinus(range, found) => {
237                let patch_info = match found.0 {
238                    None => ("insert placeholder digits after the minus sign", "194"),
239                    Some(JsonChar('.')) => (
240                        "did you mean to add a fraction? consider adding a 0 before the period",
241                        "0",
242                    ),
243                    _ => return vec![],
244                };
245                let (message, replacement) = patch_info;
246                {
247                    vec![Patch::new(
248                        message,
249                        range.end..range.end,
250                        source,
251                        replacement,
252                    )]
253                }
254            }
255            ErrorKind::UnexpectedLeadingZero { extra, .. } => {
256                vec![Patch::new("remove the leading zeros", *extra, source, "")]
257            }
258            ErrorKind::ExpectedDigitAfterDot {
259                maybe_c: JsonCharOption(None),
260                number_range,
261                ..
262            } => vec![Patch::new(
263                "insert placeholder digit after the decimal point",
264                number_range.end..number_range.end,
265                source,
266                "0",
267            )],
268            ErrorKind::ExpectedPlusOrMinusOrDigitAfterE {
269                e_range, maybe_c, ..
270            } => {
271                if maybe_c.0.is_none_or(|c| c.is_structural()) {
272                    exponent_patch_suggestions(*e_range, source)
273                } else {
274                    Vec::new()
275                }
276            }
277            ErrorKind::ExpectedDigitAfterE {
278                exponent_range,
279                maybe_c,
280                ..
281            } => {
282                if maybe_c.0.is_none_or(|c| c.is_structural()) {
283                    exponent_patch_suggestions(*exponent_range, source)
284                } else {
285                    Vec::new()
286                }
287            }
288            ErrorKind::ExpectedQuote { string_range, .. } => vec![Patch::new(
289                "insert the missing closing quote",
290                string_range.end..string_range.end,
291                source,
292                "\"",
293            )],
294            ErrorKind::ExpectedEscape {
295                maybe_c,
296                slash_range,
297                ..
298            } => match maybe_c.0.as_ref() {
299                Some(c) if c.is_control() => {
300                    vec![Patch::new(
301                        "escape the control character",
302                        slash_range.start..error.range.end,
303                        source,
304                        c.escape(),
305                    )]
306                }
307                _ => {
308                    vec![Patch::new(
309                        "remove unnecessary escape slash",
310                        *slash_range,
311                        source,
312                        "",
313                    )]
314                }
315            },
316
317            ErrorKind::ExpectedDigitAfterDot { .. } => Vec::new(),
318            ErrorKind::UnexpectedCharacter(_) => Vec::new(),
319            ErrorKind::ExpectedHexDigit { .. } => Vec::new(),
320            ErrorKind::InvalidEncoding(_) => Vec::new(),
321            ErrorKind::ExpectedOpenBrace { .. } => Vec::new(),
322            ErrorKind::ExpectedMinusOrDigit(_) => Vec::new(),
323            ErrorKind::ExpectedKey(_, _) => Vec::new(),
324            ErrorKind::NestingTooDeep(_) => Vec::new(),
325        }
326    }
327}
328
329impl<'a> From<&'a Error> for Vec<Context<'a>> {
330    fn from(error: &'a Error) -> Self {
331        let source = error_source(error);
332        match &error.kind {
333            ErrorKind::ExpectedKey(ctx, _)
334            | ErrorKind::ExpectedColon(ctx, _)
335            | ErrorKind::ExpectedEntryOrClosedDelimiter { open_ctx: ctx, .. }
336            | ErrorKind::ExpectedValue(Some(ctx), _)
337            | ErrorKind::ExpectedOpenBrace {
338                context: Some(ctx), ..
339            } => vec![Context::new(
340                format!(
341                    "expected due to `{}`",
342                    &error.source_text[ctx.range.start..ctx.range.end]
343                ),
344                ctx.range,
345                source,
346            )],
347            ErrorKind::ExpectedCommaOrClosedCurlyBrace {
348                range, open_ctx, ..
349            } => vec![
350                Context::new(
351                    format!("expected due to {EXPECTED_COMMA_OR_CLOSED_CURLY_MESSAGE}"),
352                    *range,
353                    source,
354                ),
355                Context::new(
356                    format!(
357                        "object opened here by `{}`",
358                        &error.source_text[open_ctx.range.start..open_ctx.range.end]
359                    ),
360                    open_ctx.range,
361                    source,
362                ),
363            ],
364            ErrorKind::ExpectedDigitFollowingMinus(range, _) => {
365                vec![Context::new("minus sign found here", *range, source)]
366            }
367            ErrorKind::UnexpectedLeadingZero { initial, .. } => {
368                vec![Context::new("first zero found here", *initial, source)]
369            }
370            ErrorKind::ExpectedDigitAfterDot {
371                dot_range,
372                number_range,
373                ..
374            } => vec![
375                Context::new("decimal point found here", *dot_range, source),
376                Context::new("number found here", *number_range, source),
377            ],
378            ErrorKind::ExpectedDigitAfterE { exponent_range, .. } => {
379                vec![Context::new("exponent found here", *exponent_range, source)]
380            }
381            ErrorKind::ExpectedPlusOrMinusOrDigitAfterE { e_range, .. } => {
382                vec![Context::new("exponent found here", *e_range, source)]
383            }
384            ErrorKind::ExpectedQuote { open_range, .. } => vec![Context::new(
385                "opening quote found here",
386                *open_range,
387                source,
388            )],
389            ErrorKind::ExpectedEscape {
390                slash_range,
391                quote_range,
392                ..
393            } => vec![
394                Context::new("escape slash found here", *slash_range, source),
395                Context::new("opening quote found here", *quote_range, source),
396            ],
397
398            ErrorKind::ExpectedHexDigit {
399                quote_range,
400                slash_range,
401                u_range,
402                ..
403            } => vec![
404                Context::new("opening quote found here", *quote_range, source),
405                Context::new(
406                    "\\u escape started here",
407                    slash_range.start..u_range.end,
408                    source,
409                ),
410            ],
411            ErrorKind::ExpectedValue(None, _) => Vec::new(),
412            ErrorKind::UnexpectedCharacter(_) => Vec::new(),
413            ErrorKind::UnexpectedControlCharacterInString(_) => Vec::new(),
414            ErrorKind::TokenAfterEnd(_) => Vec::new(),
415            ErrorKind::InvalidEncoding(_) => Vec::new(),
416            ErrorKind::ExpectedMinusOrDigit(_) => Vec::new(),
417            ErrorKind::ExpectedOpenBrace { context: None, .. } => Vec::new(),
418            ErrorKind::NestingTooDeep(_) => Vec::new(),
419        }
420    }
421}
422
423impl<'a> From<&'a Error> for Diagnostic<'a> {
424    fn from(error: &'a Error) -> Self {
425        Diagnostic {
426            message: error.message(),
427            range: Some(error.range),
428            context: error.into(),
429            patches: error.into(),
430            source: error_source(error),
431        }
432    }
433}