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 results => {
233 let err = LabeledError::new("Value is missing required columns.");
234 let err = results
235 .into_iter()
236 .filter_map(|x| x.err())
237 .fold(err, |err, (col, col_ty)| {
238 err.with_label(format!("missing `{col}: {col_ty}` column"), span)
239 })
240 .with_code("nu::shell::missing_required_columns");
241 return Err(err.into());
242 }
243 };
244
245 match (String::from_value(text_val), Span::from_value(span_val)) {
246 (Ok(text), Ok(span)) => Ok(Self { text, span }),
247 (r_0, r_1) => {
248 let errs = [r_0.err(), r_1.err()];
249 Err(
250 GenericError::new("Unable to parse ErrorLabel.", "here", span)
251 .with_inner(errs.into_iter().filter_map(|x| x))
252 .into(),
253 )
254 }
255 }
256 }
257
258 fn expected_type() -> crate::Type {
259 Type::Record([("text", Type::String), ("span", Span::expected_type())].into())
260 }
261}
262
263impl IntoValue for ErrorLabel {
264 fn into_value(self, span: Span) -> Value {
265 let ErrorLabel {
266 text,
267 span: label_span,
268 } = self;
269 record! {
270 "text" => Value::string(text, span),
271 "span" => label_span.into_value(span),
272 }
273 .into_value(span)
274 }
275}
276
277impl ErrorLabel {
278 fn into_value_with_resolved_span(self, span: Span, working_set: &StateWorkingSet) -> Value {
279 let ErrorLabel {
280 text,
281 span: label_span,
282 } = self;
283 let resolved_span = working_set.resolve_span(label_span);
284 record! {
285 "text" => Value::string(text, span),
286 "span" => label_span.into_value(span),
287 "location" => resolved_span.into_value(span),
288 }
289 .into_value(span)
290 }
291}
292
293#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct ErrorSource {
296 name: Option<String>,
297 text: Option<String>,
298 path: Option<String>,
299}
300
301impl ErrorSource {
302 pub fn new(name: Option<String>, text: String) -> Self {
303 Self {
304 name,
305 text: Some(text),
306 path: None,
307 }
308 }
309}
310
311impl From<ErrorSource> for NamedSource<String> {
312 fn from(value: ErrorSource) -> Self {
313 let name = value.name.unwrap_or_default();
314 match value {
315 ErrorSource {
316 text: Some(text),
317 path: None,
318 ..
319 } => NamedSource::new(name, text),
320 ErrorSource {
321 text: None,
322 path: Some(path),
323 ..
324 } => {
325 let text = fs::read_to_string(&path).unwrap_or_default();
326 NamedSource::new(path, text)
327 }
328 _ => NamedSource::new(name, "".into()),
329 }
330 }
331}
332
333impl FromValue for ErrorSource {
334 fn from_value(v: Value) -> Result<Self, ShellError> {
335 let record = v.clone().into_record()?;
336 let name = record
337 .get("name")
338 .and_then(|s| String::from_value(s.clone()).ok());
339 let text = if let Some(text) = record.get("text") {
342 String::from_value(text.clone()).ok()
343 } else {
344 None
345 };
346 let path = if let Some(path) = record.get("path") {
347 String::from_value(path.clone()).ok()
348 } else {
349 None
350 };
351
352 match (text, path) {
353 (text @ Some(_), _) => Ok(ErrorSource {
355 name,
356 text,
357 path: None,
358 }),
359 (_, path @ Some(_)) => Ok(ErrorSource {
360 name: path.clone(),
361 text: None,
362 path,
363 }),
364 _ => Err(ShellError::CantConvert {
365 to_type: Self::expected_type().to_string(),
366 from_type: v.get_type().to_string(),
367 span: v.span(),
368 help: None,
369 }),
370 }
371 }
372 fn expected_type() -> crate::Type {
373 Type::Record(
374 vec![
375 ("name".into(), Type::String),
376 ("text".into(), Type::String),
377 ("path".into(), Type::String),
378 ]
379 .into(),
380 )
381 }
382}
383
384impl IntoValue for ErrorSource {
385 fn into_value(self, span: Span) -> Value {
386 match self {
387 Self {
388 name: Some(name),
389 text: Some(text),
390 ..
391 } => record! {
392 "name" => Value::string(name, span),
393 "text" => Value::string(text, span),
394 },
395 Self {
396 text: Some(text), ..
397 } => record! {
398 "text" => Value::string(text, span)
399 },
400 Self {
401 name: Some(name),
402 path: Some(path),
403 ..
404 } => record! {
405 "name" => Value::string(name, span),
406 "path" => Value::string(path, span),
407 },
408 Self {
409 path: Some(path), ..
410 } => record! {
411 "path" => Value::string(path, span),
412 },
413 _ => record! {},
414 }
415 .into_value(span)
416 }
417}
418
419impl fmt::Display for LabeledError {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 f.write_str(&self.msg)
422 }
423}
424
425impl std::error::Error for LabeledError {
426 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
427 self.inner.first().map(|r| r as _)
428 }
429}
430
431impl Diagnostic for LabeledError {
432 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
433 self.code.as_ref().map(Box::new).map(|b| b as _)
434 }
435
436 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
437 self.help.as_ref().map(Box::new).map(|b| b as _)
438 }
439
440 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
441 self.url.as_ref().map(Box::new).map(|b| b as _)
442 }
443
444 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
445 Some(Box::new(
446 self.labels.iter().map(|label| label.clone().into()),
447 ))
448 }
449
450 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
451 Some(Box::new(self.inner.iter().map(|r| r as _)))
452 }
453}
454
455impl From<ShellError> for LabeledError {
456 fn from(err: ShellError) -> Self {
457 Self::from_diagnostic(&err)
458 }
459}
460
461impl From<IoError> for LabeledError {
462 fn from(err: IoError) -> Self {
463 Self::from_diagnostic(&err)
464 }
465}
466
467impl LabeledError {
468 pub fn into_value(self, span: Span, working_set: &StateWorkingSet) -> Value {
469 let LabeledError {
470 msg,
471 labels,
472 code,
473 url,
474 help,
475 inner,
476 } = self;
477 let inner = inner
478 .into_iter()
479 .map(|err| Self::from(err).into_value(span, working_set))
480 .collect::<Vec<_>>()
481 .into_value(span);
482 let labels = labels
483 .into_iter()
484 .map(|e| e.into_value_with_resolved_span(span, working_set))
485 .collect::<Vec<_>>()
486 .into_value(span);
487 let record = record! {
488 "msg" => msg.into_value(span),
489 "labels" => labels,
490 "code" => code.into_value(span),
491 "url" => url.into_value(span),
492 "help" => help.into_value(span),
493 "inner" => inner,
494 };
495 Value::record(record, span)
496 }
497}
498
499pub const DEFAULT_ERROR_CONTEXT: usize = 4096;
502
503pub fn truncated_source_window(input: &str, byte_span: Span, context: usize) -> (String, Span) {
519 let mid = (byte_span.start + byte_span.end) / 2;
520
521 const TIGHT_CONTEXT: usize = 128;
525 let is_single_line = if context > TIGHT_CONTEXT {
526 let probe_start = input.floor_char_boundary(mid.saturating_sub(TIGHT_CONTEXT));
527 let probe_end = input.ceil_char_boundary(input.len().min(mid + TIGHT_CONTEXT));
528 !input[probe_start..probe_end].contains('\n')
529 } else {
530 false
531 };
532 let effective = if is_single_line {
533 TIGHT_CONTEXT
534 } else {
535 context
536 };
537
538 let mut window_start = mid.saturating_sub(effective);
539 let mut window_end = input.len().min(mid + effective);
540
541 window_start = input.floor_char_boundary(window_start);
543 window_end = input.ceil_char_boundary(window_end);
544
545 if !is_single_line && context > TIGHT_CONTEXT {
546 window_start = if let Some(pos) = input[..window_start].rfind('\n') {
549 let line_start = pos + 1;
550 if window_start - line_start <= context * 2 {
551 line_start
552 } else {
553 window_start
554 }
555 } else {
556 window_start
557 };
558 window_end = if let Some(pos) = input[window_end..].find('\n') {
559 let line_end = window_end + pos + 1;
560 if line_end - window_end <= context * 2 {
561 line_end
562 } else {
563 window_end
564 }
565 } else {
566 window_end
567 };
568 }
569
570 let truncated = input[window_start..window_end].to_string();
571 let adjusted_span = Span::new(
572 byte_span.start.saturating_sub(window_start),
573 byte_span.end.saturating_sub(window_start),
574 );
575 (truncated, adjusted_span)
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn truncated_source_window_middle() {
584 let input = format!("{:a<40}ERROR{:b<40}", "", "");
588 assert_eq!(input.len(), 85);
589 let byte_span = Span::new(40, 45);
590 let (src, span) = truncated_source_window(&input, byte_span, 8);
591 assert!(
592 src.contains("ERROR"),
593 "truncated source should contain the error"
594 );
595 assert_eq!(span.start, 6, "40 - 34 = 6");
596 assert_eq!(span.end, 11, "45 - 34 = 11");
597 }
598
599 #[test]
600 fn truncated_source_window_near_start() {
601 let input = format!("{:x<80}", "");
604 let byte_span = Span::new(0, 4);
605 let (src, span) = truncated_source_window(&input, byte_span, 8);
606 assert_eq!(span.start, 0, "0 - 0 = 0");
607 assert_eq!(span.end, 4, "4 - 0 = 4");
608 assert_eq!(src.len(), 10, "window [0, 10) is 10 bytes");
609 }
610
611 #[test]
612 fn truncated_source_window_near_end() {
613 let input = format!("{:x<80}", "");
616 let byte_span = Span::new(76, 80);
617 let (src, span) = truncated_source_window(&input, byte_span, 8);
618 assert_eq!(span.start, 6, "76 - 70 = 6");
619 assert_eq!(span.end, 10, "80 - 70 = 10");
620 assert_eq!(src.len(), 10, "window [70, 80) is 10 bytes");
621 }
622
623 #[test]
624 fn truncated_source_window_small_input() {
625 let input = "small";
626 let byte_span = Span::new(2, 4);
627 let (src, span) = truncated_source_window(input, byte_span, 100);
628 assert_eq!(
630 src, "small",
631 "should be the full input when context > input.len()"
632 );
633 assert_eq!(span.start, 2, "adjusted span start should match original");
634 assert_eq!(span.end, 4, "adjusted span end should match original");
635 }
636
637 #[test]
638 fn truncated_source_window_span_adjustment() {
639 let input = "aaaaaaaaaaXXXXXbbbbbbbbbb"; let byte_span = Span::new(10, 15);
644 let (src, span) = truncated_source_window(input, byte_span, 5);
645 assert_eq!(src.len(), 10, "window should be 10 bytes");
648 assert!(src.starts_with("aaa"), "window should start with aaa");
649 assert!(src.ends_with("bb"), "window should end with bb");
650 assert!(
651 src.contains("XXXXX"),
652 "window should contain the error marker"
653 );
654 assert_eq!(
656 span.start, 3,
657 "adjusted start should be original - window_start"
658 );
659 assert_eq!(
660 span.end, 8,
661 "adjusted end should be original - window_start"
662 );
663 assert_eq!(
664 &src[3..8],
665 "XXXXX",
666 "error marker should be at the right adjusted position"
667 );
668 }
669
670 #[test]
671 fn truncated_source_window_zero_width_span() {
672 let input = "abcdefghijklmnopqrstuvwxyz";
673 let byte_span = Span::new(13, 13); let (src, span) = truncated_source_window(input, byte_span, 5);
675 assert_eq!(
676 span.start, span.end,
677 "zero-width span should stay zero-width"
678 );
679 assert!(src.len() <= 11, "window should be bounded");
680 }
681
682 #[test]
683 fn truncated_source_window_multibyte_utf8() {
684 let input = "你好世界ERROR世界";
686 let byte_span = Span::new(12, 17);
688 let (src, span) = truncated_source_window(input, byte_span, 3);
689 assert!(
690 src.contains("ERROR"),
691 "window must contain the error region"
692 );
693 assert_eq!(
694 &src[span.start..span.end],
695 "ERROR",
696 "adjusted span must slice correctly"
697 );
698 }
699
700 #[test]
701 fn truncated_source_window_multibyte_utf8_boundary_crossing() {
702 let input = "aaaaa你好世界ERROR世界你好";
705 let byte_span = Span::new(17, 22);
707 let (src, span) = truncated_source_window(input, byte_span, 8);
709 assert!(
710 src.contains("ERROR"),
711 "window must contain the error region"
712 );
713 assert_eq!(&src[span.start..span.end], "ERROR");
714 }
715
716 #[test]
717 fn truncated_source_window_single_line_minified() {
718 let mut input = String::new();
720 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);
725 assert!(
727 src.len() < 1000,
728 "single-line window should be tight, got {} bytes",
729 src.len()
730 );
731 assert_eq!(
732 &src[span.start..span.end],
733 "\"",
734 "should point at the opening quote"
735 );
736 }
737
738 #[test]
739 fn truncated_source_window_multiline_uses_full_context() {
740 let mut input = String::new();
742 for i in 0..200 {
743 use std::fmt::Write;
744 writeln!(&mut input, "line {i}").unwrap();
745 }
746 input.push_str("ERROR here\nlast line");
747 let err_offset = input.find("ERROR").expect("ERROR should be in input");
749 let byte_span = Span::new(err_offset, err_offset + 5);
750 let (src, span) = truncated_source_window(&input, byte_span, DEFAULT_ERROR_CONTEXT);
752 assert!(
754 src.len() > 1000,
755 "multi-line window should be large, got {} bytes",
756 src.len()
757 );
758 assert!(src.contains("ERROR"), "should contain the error region");
759 assert_eq!(&src[span.start..span.end], "ERROR");
760 }
761}