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}