1use super::{ShellError, shell_error::io::IoError};
2use crate::{FromValue, IntoValue, Span, Type, Value, engine::StateWorkingSet, record};
3use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan};
4use serde::{Deserialize, Serialize};
5use std::{fmt, fs};
6
7#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
15pub struct LabeledError {
16 pub msg: String,
18 #[serde(default)]
20 pub labels: Box<Vec<ErrorLabel>>,
21 #[serde(default)]
24 pub code: Option<String>,
25 #[serde(default)]
27 pub url: Option<String>,
28 #[serde(default)]
30 pub help: Option<String>,
31 #[serde(default)]
33 pub inner: Box<Vec<ShellError>>,
34}
35
36impl LabeledError {
37 pub fn new(msg: impl Into<String>) -> Self {
50 Self {
51 msg: msg.into(),
52 ..Default::default()
53 }
54 }
55
56 pub fn with_label(mut self, text: impl Into<String>, span: Span) -> Self {
69 self.labels.push(ErrorLabel {
70 text: text.into(),
71 span,
72 });
73 self
74 }
75
76 pub fn with_code(mut self, code: impl Into<String>) -> Self {
88 self.code = Some(code.into());
89 self
90 }
91
92 pub fn with_url(mut self, url: impl Into<String>) -> Self {
103 self.url = Some(url.into());
104 self
105 }
106
107 pub fn with_help(mut self, help: impl Into<String>) -> Self {
118 self.help = Some(help.into());
119 self
120 }
121
122 pub fn with_inner(mut self, inner: impl Into<ShellError>) -> Self {
134 let inner_error: ShellError = inner.into();
135 self.inner.push(inner_error);
136 self
137 }
138
139 pub fn from_diagnostic(diag: &(impl miette::Diagnostic + ?Sized)) -> Self {
159 Self {
160 msg: diag.to_string(),
161 labels: diag
162 .labels()
163 .into_iter()
164 .flatten()
165 .map(|label| ErrorLabel {
166 text: label.label().unwrap_or("").into(),
167 span: Span::new(label.offset(), label.offset() + label.len()),
168 })
169 .collect::<Vec<_>>()
170 .into(),
171 code: diag.code().map(|s| s.to_string()),
172 url: diag.url().map(|s| s.to_string()),
173 help: diag.help().map(|s| s.to_string()),
174 inner: diag
175 .related()
176 .into_iter()
177 .flatten()
178 .map(|i| Self::from_diagnostic(i).into())
179 .collect::<Vec<_>>()
180 .into(),
181 }
182 }
183}
184
185#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ErrorLabel {
188 pub text: String,
190 pub span: Span,
192}
193
194impl From<ErrorLabel> for LabeledSpan {
195 fn from(val: ErrorLabel) -> Self {
196 LabeledSpan::new(
197 (!val.text.is_empty()).then_some(val.text),
198 val.span.start,
199 val.span.end - val.span.start,
200 )
201 }
202}
203
204impl From<ErrorLabel> for SourceSpan {
205 fn from(val: ErrorLabel) -> Self {
206 SourceSpan::new(val.span.start.into(), val.span.end - val.span.start)
207 }
208}
209
210impl FromValue for ErrorLabel {
211 fn from_value(v: Value) -> Result<Self, ShellError> {
212 let record = v.clone().into_record()?;
213 let text = String::from_value(match record.get("text") {
214 Some(val) => val.clone(),
215 None => Value::string("", v.span()),
216 })
217 .unwrap_or("originates from here".into());
218 let span = Span::from_value(match record.get("span") {
219 Some(val) => val.clone(),
220 None => Value::record(
222 record! {
223 "start" => Value::int(v.span().start as i64, v.span()),
224 "end" => Value::int(v.span().end as i64, v.span()),
225 },
226 v.span(),
227 ),
228 });
229
230 match span {
231 Ok(s) => Ok(Self { text, span: s }),
232 Err(e) => Err(e),
233 }
234 }
235 fn expected_type() -> crate::Type {
236 Type::Record(
237 vec![
238 ("text".into(), Type::String),
239 ("span".into(), Type::record()),
240 ]
241 .into(),
242 )
243 }
244}
245
246impl IntoValue for ErrorLabel {
247 fn into_value(self, span: Span) -> Value {
248 let ErrorLabel {
249 text,
250 span: label_span,
251 } = self;
252 record! {
253 "text" => Value::string(text, span),
254 "span" => label_span.into_value(span),
255 }
256 .into_value(span)
257 }
258}
259
260impl ErrorLabel {
261 fn into_value_with_resolved_span(self, span: Span, working_set: &StateWorkingSet) -> Value {
262 let ErrorLabel {
263 text,
264 span: label_span,
265 } = self;
266 let resolved_span = working_set.resolve_span(label_span);
267 record! {
268 "text" => Value::string(text, span),
269 "span" => label_span.into_value(span),
270 "location" => resolved_span.into_value(span),
271 }
272 .into_value(span)
273 }
274}
275
276#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct ErrorSource {
279 name: Option<String>,
280 text: Option<String>,
281 path: Option<String>,
282}
283
284impl ErrorSource {
285 pub fn new(name: Option<String>, text: String) -> Self {
286 Self {
287 name,
288 text: Some(text),
289 path: None,
290 }
291 }
292}
293
294impl From<ErrorSource> for NamedSource<String> {
295 fn from(value: ErrorSource) -> Self {
296 let name = value.name.unwrap_or_default();
297 match value {
298 ErrorSource {
299 text: Some(text),
300 path: None,
301 ..
302 } => NamedSource::new(name, text),
303 ErrorSource {
304 text: None,
305 path: Some(path),
306 ..
307 } => {
308 let text = fs::read_to_string(&path).unwrap_or_default();
309 NamedSource::new(path, text)
310 }
311 _ => NamedSource::new(name, "".into()),
312 }
313 }
314}
315
316impl FromValue for ErrorSource {
317 fn from_value(v: Value) -> Result<Self, ShellError> {
318 let record = v.clone().into_record()?;
319 let name = record
320 .get("name")
321 .and_then(|s| String::from_value(s.clone()).ok());
322 let text = if let Some(text) = record.get("text") {
325 String::from_value(text.clone()).ok()
326 } else {
327 None
328 };
329 let path = if let Some(path) = record.get("path") {
330 String::from_value(path.clone()).ok()
331 } else {
332 None
333 };
334
335 match (text, path) {
336 (text @ Some(_), _) => Ok(ErrorSource {
338 name,
339 text,
340 path: None,
341 }),
342 (_, path @ Some(_)) => Ok(ErrorSource {
343 name: path.clone(),
344 text: None,
345 path,
346 }),
347 _ => Err(ShellError::CantConvert {
348 to_type: Self::expected_type().to_string(),
349 from_type: v.get_type().to_string(),
350 span: v.span(),
351 help: None,
352 }),
353 }
354 }
355 fn expected_type() -> crate::Type {
356 Type::Record(
357 vec![
358 ("name".into(), Type::String),
359 ("text".into(), Type::String),
360 ("path".into(), Type::String),
361 ]
362 .into(),
363 )
364 }
365}
366
367impl IntoValue for ErrorSource {
368 fn into_value(self, span: Span) -> Value {
369 match self {
370 Self {
371 name: Some(name),
372 text: Some(text),
373 ..
374 } => record! {
375 "name" => Value::string(name, span),
376 "text" => Value::string(text, span),
377 },
378 Self {
379 text: Some(text), ..
380 } => record! {
381 "text" => Value::string(text, span)
382 },
383 Self {
384 name: Some(name),
385 path: Some(path),
386 ..
387 } => record! {
388 "name" => Value::string(name, span),
389 "path" => Value::string(path, span),
390 },
391 Self {
392 path: Some(path), ..
393 } => record! {
394 "path" => Value::string(path, span),
395 },
396 _ => record! {},
397 }
398 .into_value(span)
399 }
400}
401
402impl fmt::Display for LabeledError {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 f.write_str(&self.msg)
405 }
406}
407
408impl std::error::Error for LabeledError {
409 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
410 self.inner.first().map(|r| r as _)
411 }
412}
413
414impl Diagnostic for LabeledError {
415 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
416 self.code.as_ref().map(Box::new).map(|b| b as _)
417 }
418
419 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
420 self.help.as_ref().map(Box::new).map(|b| b as _)
421 }
422
423 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
424 self.url.as_ref().map(Box::new).map(|b| b as _)
425 }
426
427 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
428 Some(Box::new(
429 self.labels.iter().map(|label| label.clone().into()),
430 ))
431 }
432
433 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
434 Some(Box::new(self.inner.iter().map(|r| r as _)))
435 }
436}
437
438impl From<ShellError> for LabeledError {
439 fn from(err: ShellError) -> Self {
440 Self::from_diagnostic(&err)
441 }
442}
443
444impl From<IoError> for LabeledError {
445 fn from(err: IoError) -> Self {
446 Self::from_diagnostic(&err)
447 }
448}
449
450impl LabeledError {
451 pub fn into_value(self, span: Span, working_set: &StateWorkingSet) -> Value {
452 let LabeledError {
453 msg,
454 labels,
455 code,
456 url,
457 help,
458 inner,
459 } = self;
460 let inner = inner
461 .into_iter()
462 .map(|err| Self::from(err).into_value(span, working_set))
463 .collect::<Vec<_>>()
464 .into_value(span);
465 let labels = labels
466 .into_iter()
467 .map(|e| e.into_value_with_resolved_span(span, working_set))
468 .collect::<Vec<_>>()
469 .into_value(span);
470 let record = record! {
471 "msg" => msg.into_value(span),
472 "labels" => labels,
473 "code" => code.into_value(span),
474 "url" => url.into_value(span),
475 "help" => help.into_value(span),
476 "inner" => inner,
477 };
478 Value::record(record, span)
479 }
480}
481
482pub const DEFAULT_ERROR_CONTEXT: usize = 4096;
485
486pub fn truncated_source_window(input: &str, byte_span: Span, context: usize) -> (String, Span) {
502 let mid = (byte_span.start + byte_span.end) / 2;
503
504 const TIGHT_CONTEXT: usize = 128;
508 let is_single_line = if context > TIGHT_CONTEXT {
509 let probe_start = input.floor_char_boundary(mid.saturating_sub(TIGHT_CONTEXT));
510 let probe_end = input.ceil_char_boundary(input.len().min(mid + TIGHT_CONTEXT));
511 !input[probe_start..probe_end].contains('\n')
512 } else {
513 false
514 };
515 let effective = if is_single_line {
516 TIGHT_CONTEXT
517 } else {
518 context
519 };
520
521 let mut window_start = mid.saturating_sub(effective);
522 let mut window_end = input.len().min(mid + effective);
523
524 window_start = input.floor_char_boundary(window_start);
526 window_end = input.ceil_char_boundary(window_end);
527
528 if !is_single_line && context > TIGHT_CONTEXT {
529 window_start = if let Some(pos) = input[..window_start].rfind('\n') {
532 let line_start = pos + 1;
533 if window_start - line_start <= context * 2 {
534 line_start
535 } else {
536 window_start
537 }
538 } else {
539 window_start
540 };
541 window_end = if let Some(pos) = input[window_end..].find('\n') {
542 let line_end = window_end + pos + 1;
543 if line_end - window_end <= context * 2 {
544 line_end
545 } else {
546 window_end
547 }
548 } else {
549 window_end
550 };
551 }
552
553 let truncated = input[window_start..window_end].to_string();
554 let adjusted_span = Span::new(
555 byte_span.start.saturating_sub(window_start),
556 byte_span.end.saturating_sub(window_start),
557 );
558 (truncated, adjusted_span)
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn truncated_source_window_middle() {
567 let input = format!("{:a<40}ERROR{:b<40}", "", "");
571 assert_eq!(input.len(), 85);
572 let byte_span = Span::new(40, 45);
573 let (src, span) = truncated_source_window(&input, byte_span, 8);
574 assert!(
575 src.contains("ERROR"),
576 "truncated source should contain the error"
577 );
578 assert_eq!(span.start, 6, "40 - 34 = 6");
579 assert_eq!(span.end, 11, "45 - 34 = 11");
580 }
581
582 #[test]
583 fn truncated_source_window_near_start() {
584 let input = format!("{:x<80}", "");
587 let byte_span = Span::new(0, 4);
588 let (src, span) = truncated_source_window(&input, byte_span, 8);
589 assert_eq!(span.start, 0, "0 - 0 = 0");
590 assert_eq!(span.end, 4, "4 - 0 = 4");
591 assert_eq!(src.len(), 10, "window [0, 10) is 10 bytes");
592 }
593
594 #[test]
595 fn truncated_source_window_near_end() {
596 let input = format!("{:x<80}", "");
599 let byte_span = Span::new(76, 80);
600 let (src, span) = truncated_source_window(&input, byte_span, 8);
601 assert_eq!(span.start, 6, "76 - 70 = 6");
602 assert_eq!(span.end, 10, "80 - 70 = 10");
603 assert_eq!(src.len(), 10, "window [70, 80) is 10 bytes");
604 }
605
606 #[test]
607 fn truncated_source_window_small_input() {
608 let input = "small";
609 let byte_span = Span::new(2, 4);
610 let (src, span) = truncated_source_window(input, byte_span, 100);
611 assert_eq!(
613 src, "small",
614 "should be the full input when context > input.len()"
615 );
616 assert_eq!(span.start, 2, "adjusted span start should match original");
617 assert_eq!(span.end, 4, "adjusted span end should match original");
618 }
619
620 #[test]
621 fn truncated_source_window_span_adjustment() {
622 let input = "aaaaaaaaaaXXXXXbbbbbbbbbb"; let byte_span = Span::new(10, 15);
627 let (src, span) = truncated_source_window(input, byte_span, 5);
628 assert_eq!(src.len(), 10, "window should be 10 bytes");
631 assert!(src.starts_with("aaa"), "window should start with aaa");
632 assert!(src.ends_with("bb"), "window should end with bb");
633 assert!(
634 src.contains("XXXXX"),
635 "window should contain the error marker"
636 );
637 assert_eq!(
639 span.start, 3,
640 "adjusted start should be original - window_start"
641 );
642 assert_eq!(
643 span.end, 8,
644 "adjusted end should be original - window_start"
645 );
646 assert_eq!(
647 &src[3..8],
648 "XXXXX",
649 "error marker should be at the right adjusted position"
650 );
651 }
652
653 #[test]
654 fn truncated_source_window_zero_width_span() {
655 let input = "abcdefghijklmnopqrstuvwxyz";
656 let byte_span = Span::new(13, 13); let (src, span) = truncated_source_window(input, byte_span, 5);
658 assert_eq!(
659 span.start, span.end,
660 "zero-width span should stay zero-width"
661 );
662 assert!(src.len() <= 11, "window should be bounded");
663 }
664
665 #[test]
666 fn truncated_source_window_multibyte_utf8() {
667 let input = "你好世界ERROR世界";
669 let byte_span = Span::new(12, 17);
671 let (src, span) = truncated_source_window(input, byte_span, 3);
672 assert!(
673 src.contains("ERROR"),
674 "window must contain the error region"
675 );
676 assert_eq!(
677 &src[span.start..span.end],
678 "ERROR",
679 "adjusted span must slice correctly"
680 );
681 }
682
683 #[test]
684 fn truncated_source_window_multibyte_utf8_boundary_crossing() {
685 let input = "aaaaa你好世界ERROR世界你好";
688 let byte_span = Span::new(17, 22);
690 let (src, span) = truncated_source_window(input, byte_span, 8);
692 assert!(
693 src.contains("ERROR"),
694 "window must contain the error region"
695 );
696 assert_eq!(&src[span.start..span.end], "ERROR");
697 }
698
699 #[test]
700 fn truncated_source_window_single_line_minified() {
701 let mut input = String::new();
703 input.push_str(&"\"key\":\"value\",".repeat(500)); let err_byte = input.len(); input.push_str("\"broken"); let byte_span = Span::new(err_byte, err_byte + 1); let (src, span) = truncated_source_window(&input, byte_span, DEFAULT_ERROR_CONTEXT);
708 assert!(
710 src.len() < 1000,
711 "single-line window should be tight, got {} bytes",
712 src.len()
713 );
714 assert_eq!(
715 &src[span.start..span.end],
716 "\"",
717 "should point at the opening quote"
718 );
719 }
720
721 #[test]
722 fn truncated_source_window_multiline_uses_full_context() {
723 let mut input = String::new();
725 for i in 0..200 {
726 use std::fmt::Write;
727 writeln!(&mut input, "line {i}").unwrap();
728 }
729 input.push_str("ERROR here\nlast line");
730 let err_offset = input.find("ERROR").expect("ERROR should be in input");
732 let byte_span = Span::new(err_offset, err_offset + 5);
733 let (src, span) = truncated_source_window(&input, byte_span, DEFAULT_ERROR_CONTEXT);
735 assert!(
737 src.len() > 1000,
738 "multi-line window should be large, got {} bytes",
739 src.len()
740 );
741 assert!(src.contains("ERROR"), "should contain the error region");
742 assert_eq!(&src[span.start..span.end], "ERROR");
743 }
744}