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