mirror of
https://github.com/nushell/nushell.git
synced 2025-05-05 23:42:56 +00:00
# Description After this pr, nushell is able to raise errors with a backtrace, which should make users easier to debug. To enable the feature, users need to set env variable via `$env.NU_BACKTRACE = 1`. But yeah it might not work perfectly, there are some corner cases which might not be handled. I think it should close #13379 in another way. ### About the change The implementation mostly contained with 2 parts: 1. introduce a new `ChainedError` struct as well as a new `ShellError::ChainedError` variant. If `eval_instruction` returned an error, it converts the error to `ShellError::ChainedError`. `ChainedError` struct is responsable to display errors properly. It needs to handle the following 2 cases: - if we run a function which runs `error make` internally, it needs to display the error itself along with caller span. - if we run a `error make` directly, or some commands directly returns an error, we just want nushell raise an error about `error make`. 2. Attach caller spans to `ListStream` and `ByteStream`, because they are lazy streams, and *only* contains the span that runs it directly(like `^false`, for example), so nushell needs to add all caller spans to the stream. For example: in `def a [] { ^false }; def b [] { a; 33 }; b`, when we run `b`, which runs `a`, which runs `^false`, the `ByteStream` only contains the span of `^false`, we need to make it contains the span of `a`, so nushell is able to get all spans if something bad happened. This behavior is happened after running `Instruction::Call`, if it returns a `ByteStream` and `ListStream`, it will call `push_caller_span` method to attach call spans. # User-Facing Changes It's better to demostrate how it works by examples, given the following definition: ```nushell > $env.NU_BACKTRACE = 1 > def a [x] { if $x == 3 { error make {msg: 'a custom error'}}} > def a_2 [x] { if $x == 3 { ^false } else { $x } } > def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } > def b [--list-stream --external] { if $external == true { # error with non-zero exit code, which is generated from external command. a_2 1; a_2 3; a_2 2 } else if $list_stream == true { # error generated by list-stream a_3 1; a_3 3; a_3 2 } else { # error generated by command directly a 1; a 2; a 3 } } ``` Run `b` directly shows the following error: <details> ```nushell Error: chained_error × oops ╭─[entry #27:1:1] 1 │ b · ┬ · ╰── error happened when running this ╰──── Error: chained_error × oops ╭─[entry #26:10:19] 9 │ # error generated by command directly 10 │ a 1; a 2; a 3 · ┬ · ╰── error happened when running this 11 │ } ╰──── Error: × a custom error ╭─[entry #6:1:26] 1 │ def a [x] { if $x == 3 { error make {msg: 'a custom error'}}} · ─────┬──── · ╰── originates from here ╰──── ``` </details> Run `b --list-stream` shows the following error <details> ```nushell Error: chained_error × oops ╭─[entry #28:1:1] 1 │ b --list-stream · ┬ · ╰── error happened when running this ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #26:7:16] 6 │ # error generated by list-stream 7 │ a_3 1; a_3 3; a_3 2 · ─┬─ · ╰── source value 8 │ } else { ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #23:1:29] 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } · ┬ · ╰── source value ╰──── Error: × a custom error inside list stream ╭─[entry #23:1:44] 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } · ─────┬──── · ╰── originates from here ╰──── ``` </details> Run `b --external` shows the following error: <details> ```nushell Error: chained_error × oops ╭─[entry #29:1:1] 1 │ b --external · ┬ · ╰── error happened when running this ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #26:4:16] 3 │ # error with non-zero exit code, which is generated from external command. 4 │ a_2 1; a_2 3; a_2 2 · ─┬─ · ╰── source value 5 │ } else if $list_stream == true { ╰──── Error: nu:🐚:non_zero_exit_code × External command had a non-zero exit code ╭─[entry #7:1:29] 1 │ def a_2 [x] { if $x == 3 { ^false } else { $x } } · ──┬── · ╰── exited with code 1 ╰──── ``` </details> It also added a message to guide the usage of NU_BACKTRACE, see the last line in the following example: ```shell ls asdfasd Error: nu:🐚:io::not_found × I/O error ╰─▶ × Entity not found ╭─[entry #17:1:4] 1 │ ls asdfasd · ───┬─── · ╰── Entity not found ╰──── help: The error occurred at '/home/windsoilder/projects/nushell/asdfasd' set the `NU_BACKTRACE=1` environment variable to display a backtrace. ``` # Tests + Formatting Added some tests for the behavior. # After Submitting
121 lines
3.1 KiB
Rust
121 lines
3.1 KiB
Rust
use super::shell_error::ShellError;
|
|
use crate::Span;
|
|
use miette::{LabeledSpan, Severity, SourceCode};
|
|
use thiserror::Error;
|
|
|
|
/// An error struct that contains source errors.
|
|
///
|
|
/// However, it's a bit special; if the error is constructed for the first time using
|
|
/// [`ChainedError::new`], it will behave the same as the single source error.
|
|
///
|
|
/// If it's constructed nestedly using [`ChainedError::new_chained`], it will treat all underlying errors as related.
|
|
///
|
|
/// For a usage example, please check [`ShellError::into_chainned`].
|
|
#[derive(Debug, Clone, PartialEq, Error)]
|
|
pub struct ChainedError {
|
|
first: bool,
|
|
pub(crate) sources: Vec<ShellError>,
|
|
span: Span,
|
|
}
|
|
|
|
impl std::fmt::Display for ChainedError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if self.first {
|
|
write!(f, "{}", self.sources[0])
|
|
} else {
|
|
write!(f, "oops")
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ChainedError {
|
|
pub fn new(source: ShellError, span: Span) -> Self {
|
|
Self {
|
|
first: true,
|
|
sources: vec![source],
|
|
span,
|
|
}
|
|
}
|
|
|
|
pub fn new_chained(sources: Self, span: Span) -> Self {
|
|
Self {
|
|
first: false,
|
|
sources: vec![ShellError::ChainedError(sources)],
|
|
span,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl miette::Diagnostic for ChainedError {
|
|
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn miette::Diagnostic> + 'a>> {
|
|
if self.first {
|
|
self.sources[0].related()
|
|
} else {
|
|
Some(Box::new(self.sources.iter().map(|s| s as _)))
|
|
}
|
|
}
|
|
|
|
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
|
|
if self.first {
|
|
self.sources[0].code()
|
|
} else {
|
|
Some(Box::new("chained_error"))
|
|
}
|
|
}
|
|
|
|
fn severity(&self) -> Option<Severity> {
|
|
if self.first {
|
|
self.sources[0].severity()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
|
|
if self.first {
|
|
self.sources[0].help()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
|
|
if self.first {
|
|
self.sources[0].url()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
|
|
if self.first {
|
|
self.sources[0].labels()
|
|
} else {
|
|
Some(Box::new(
|
|
vec![LabeledSpan::new_with_span(
|
|
Some("error happened when running this".to_string()),
|
|
self.span,
|
|
)]
|
|
.into_iter(),
|
|
))
|
|
}
|
|
}
|
|
|
|
// Finally, we redirect the source_code method to our own source.
|
|
fn source_code(&self) -> Option<&dyn SourceCode> {
|
|
if self.first {
|
|
self.sources[0].source_code()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
|
|
if self.first {
|
|
self.sources[0].diagnostic_source()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|