Skip to main content

nu_protocol/errors/shell_error/
generic.rs

1use crate::{
2    ShellError, Span,
3    shell_error::{ErrorSite, ErrorSource},
4};
5use miette::Diagnostic;
6use nu_utils::location::Location;
7use std::{
8    borrow::Cow,
9    error::Error as StdError,
10    fmt::{self, Display},
11    sync::Arc,
12};
13
14/// Default code that [`GenericError`] is using as error code.
15pub const DEFAULT_CODE: &str = "nu::shell::error";
16
17/// Generic [`ShellError`].
18///
19/// This is a generic error for all cases where any of the variants of [`ShellError`] do not fit
20/// and creating new variants is too niche.
21/// Usually this should be created using [`new`](Self::new) or [`new_internal`](Self::new_internal)
22/// if absolutely no span is available, try however to provide at least some span like `call.head`
23/// inside a [`Command::run`](crate::engine::Command::run) context.
24///
25/// Using [`with_code`](Self::with_code), [`with_help`](Self::with_help),
26/// [`with_inner`](Self::with_inner) can improve the error type making it more useful.
27#[non_exhaustive]
28#[derive(Debug, Clone, PartialEq)]
29pub struct GenericError {
30    /// The diagnostic code for this error.
31    ///
32    /// Defaults to [`DEFAULT_CODE`].
33    /// Use [`with_code`](Self::with_code) to override it.
34    pub code: Cow<'static, str>,
35
36    /// A short, user-facing title for the error.
37    pub error: Cow<'static, str>,
38
39    /// The message describing what went wrong.
40    pub msg: Cow<'static, str>,
41
42    /// The error origin: either a user span or an internal Rust location.
43    pub site: ErrorSite,
44
45    /// Optional additional guidance for the user.
46    pub help: Option<Cow<'static, str>>,
47
48    /// Related errors that provide more context.
49    pub inner: Vec<ShellError>,
50
51    /// Optional error source.
52    pub source: Option<ErrorSource>,
53}
54
55impl GenericError {
56    /// Creates a new [`GenericError`] tied to user input.
57    ///
58    /// The `error` is a short title, the `msg` provides details, and the `span`
59    /// points to the user code that triggered the issue.
60    #[track_caller]
61    pub fn new(
62        error: impl Into<Cow<'static, str>>,
63        msg: impl Into<Cow<'static, str>>,
64        span: Span,
65    ) -> Self {
66        // TODO: enable this at some point to find where unknown spans are passed around
67        // debug_assert_ne!(
68        //     span,
69        //     Span::unknown(),
70        //     "do not use `Span::unknown()` in a `GenericError::new`, prefer `GenericError::new_internal`"
71        // );
72
73        Self {
74            code: DEFAULT_CODE.into(),
75            error: error.into(),
76            msg: msg.into(),
77            site: ErrorSite::Span(span),
78            help: None,
79            inner: Vec::new(),
80            source: None,
81        }
82    }
83
84    /// Creates a new [`GenericError`] for internal errors without a user span.
85    ///
86    /// This records the Rust call site in the `source` so the error can be
87    /// traced even when no user-facing span is available.
88    #[track_caller]
89    pub fn new_internal(
90        error: impl Into<Cow<'static, str>>,
91        msg: impl Into<Cow<'static, str>>,
92    ) -> Self {
93        let location = Location::caller();
94        Self {
95            code: DEFAULT_CODE.into(),
96            error: error.into(),
97            msg: msg.into(),
98            site: ErrorSite::Location(location.to_string()),
99            help: None,
100            inner: Vec::new(),
101            source: None,
102        }
103    }
104
105    /// Creates a new [`GenericError`] for internal errors without a user span but a provided
106    /// Rust location.
107    ///
108    /// Use this in places where a [`Location`] is already recorded and just needs to passed on,
109    /// otherwise prefer [`new_internal`](Self::new_internal).
110    pub fn new_internal_with_location(
111        error: impl Into<Cow<'static, str>>,
112        msg: impl Into<Cow<'static, str>>,
113        location: impl Into<Location>,
114    ) -> Self {
115        Self {
116            code: DEFAULT_CODE.into(),
117            error: error.into(),
118            msg: msg.into(),
119            site: ErrorSite::Location(location.into().to_string()),
120            help: None,
121            inner: Vec::new(),
122            source: None,
123        }
124    }
125
126    /// Overrides the diagnostic code for this error.
127    pub fn with_code(self, code: impl Into<Cow<'static, str>>) -> Self {
128        Self {
129            code: code.into(),
130            ..self
131        }
132    }
133
134    /// Adds user-facing help text for this error.
135    pub fn with_help(self, help: impl Into<Cow<'static, str>>) -> Self {
136        Self {
137            help: Some(help.into()),
138            ..self
139        }
140    }
141
142    /// Attaches related errors that provide additional context.
143    pub fn with_inner(self, inner: impl IntoIterator<Item = ShellError>) -> Self {
144        Self {
145            inner: inner.into_iter().collect(),
146            ..self
147        }
148    }
149
150    /// Attaches error source that can be used to render error chains.
151    pub fn with_source(self, source: impl StdError + Send + Sync + 'static) -> Self {
152        Self {
153            source: Some(ErrorSource(Arc::new(source))),
154            ..self
155        }
156    }
157}
158
159impl Display for GenericError {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        let GenericError { error, .. } = self;
162        write!(f, "{error}")
163    }
164}
165
166impl StdError for GenericError {}
167
168impl Diagnostic for GenericError {
169    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
170        Some(Box::new(self.code.as_ref()))
171    }
172
173    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
174        let span = match &self.site {
175            ErrorSite::Span(span) => (*span).into(),
176            ErrorSite::Location(location) => miette::SourceSpan::new(0.into(), location.len()),
177        };
178
179        let label = miette::LabeledSpan::new_with_span(Some(self.msg.to_string()), span);
180        Some(Box::new(std::iter::once(label)))
181    }
182
183    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
184        match &self.site {
185            ErrorSite::Span(_) => None,
186            ErrorSite::Location(location) => Some(location as &dyn miette::SourceCode),
187        }
188    }
189
190    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
191        self.help
192            .as_ref()
193            .map(|help| Box::new(help.as_ref()) as Box<dyn Display>)
194    }
195
196    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
197        match &self.inner.is_empty() {
198            true => None,
199            false => Some(Box::new(
200                self.inner.iter().map(|err| err as &dyn Diagnostic),
201            )),
202        }
203    }
204
205    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
206        self.source.as_ref().map(|err| err as &dyn Diagnostic)
207    }
208}