mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 09:55:42 +02:00
Better generic errors for plugins (and perhaps scripts) (#12236)
# Description This makes `LabeledError` much more capable of representing close to everything a `miette::Diagnostic` can, including `ShellError`, and allows plugins to generate multiple error spans, codes, help, etc. `LabeledError` is now embeddable within `ShellError` as a transparent variant. This could also be used to improve `error make` and `try/catch` to reflect `LabeledError` exactly in the future. Also cleaned up some errors in existing plugins. # User-Facing Changes Breaking change for plugins. Nicer errors for users.
This commit is contained in:
@ -16,9 +16,9 @@
|
||||
//! invoked by Nushell.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use nu_plugin::{EvaluatedCall, LabeledError, MsgPackSerializer, serve_plugin};
|
||||
//! use nu_plugin::{EvaluatedCall, MsgPackSerializer, serve_plugin};
|
||||
//! use nu_plugin::{Plugin, PluginCommand, SimplePluginCommand, EngineInterface};
|
||||
//! use nu_protocol::{PluginSignature, Value};
|
||||
//! use nu_protocol::{PluginSignature, LabeledError, Value};
|
||||
//!
|
||||
//! struct MyPlugin;
|
||||
//! struct MyCommand;
|
||||
@ -64,7 +64,7 @@ mod util;
|
||||
pub use plugin::{
|
||||
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, SimplePluginCommand,
|
||||
};
|
||||
pub use protocol::{EvaluatedCall, LabeledError};
|
||||
pub use protocol::EvaluatedCall;
|
||||
pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
|
||||
|
||||
// Used by other nu crates.
|
||||
|
@ -1,6 +1,6 @@
|
||||
use nu_protocol::{PipelineData, PluginSignature, Value};
|
||||
use nu_protocol::{LabeledError, PipelineData, PluginSignature, Value};
|
||||
|
||||
use crate::{EngineInterface, EvaluatedCall, LabeledError, Plugin};
|
||||
use crate::{EngineInterface, EvaluatedCall, Plugin};
|
||||
|
||||
/// The API for a Nushell plugin command
|
||||
///
|
||||
@ -18,7 +18,7 @@ use crate::{EngineInterface, EvaluatedCall, LabeledError, Plugin};
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value};
|
||||
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value, LabeledError};
|
||||
/// struct LowercasePlugin;
|
||||
/// struct Lowercase;
|
||||
///
|
||||
@ -108,7 +108,7 @@ pub trait PluginCommand: Sync {
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, Type, Value};
|
||||
/// # use nu_protocol::{PluginSignature, Type, Value, LabeledError};
|
||||
/// struct HelloPlugin;
|
||||
/// struct Hello;
|
||||
///
|
||||
|
@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use nu_protocol::{
|
||||
engine::Closure, Config, IntoInterruptiblePipelineData, ListStream, PipelineData,
|
||||
engine::Closure, Config, IntoInterruptiblePipelineData, LabeledError, ListStream, PipelineData,
|
||||
PluginSignature, ShellError, Spanned, Value,
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ use crate::{
|
||||
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
|
||||
ProtocolInfo,
|
||||
},
|
||||
LabeledError, PluginOutput,
|
||||
PluginOutput,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -4,8 +4,8 @@ use std::{
|
||||
};
|
||||
|
||||
use nu_protocol::{
|
||||
engine::Closure, Config, CustomValue, IntoInterruptiblePipelineData, PipelineData,
|
||||
PluginExample, PluginSignature, ShellError, Span, Spanned, Value,
|
||||
engine::Closure, Config, CustomValue, IntoInterruptiblePipelineData, LabeledError,
|
||||
PipelineData, PluginExample, PluginSignature, ShellError, Span, Spanned, Value,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@ -16,7 +16,7 @@ use crate::{
|
||||
ListStreamInfo, PipelineDataHeader, PluginCall, PluginCustomValue, PluginInput, Protocol,
|
||||
ProtocolInfo, RawStreamInfo, StreamData, StreamMessage,
|
||||
},
|
||||
EvaluatedCall, LabeledError, PluginCallResponse, PluginOutput,
|
||||
EvaluatedCall, PluginCallResponse, PluginOutput,
|
||||
};
|
||||
|
||||
use super::{EngineInterfaceManager, ReceivedPluginCall};
|
||||
@ -738,11 +738,7 @@ fn interface_write_response_with_stream() -> Result<(), ShellError> {
|
||||
fn interface_write_response_with_error() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let interface = test.engine().interface_for_context(35);
|
||||
let labeled_error = LabeledError {
|
||||
label: "this is an error".into(),
|
||||
msg: "a test error".into(),
|
||||
span: None,
|
||||
};
|
||||
let labeled_error = LabeledError::new("this is an error").with_help("a test error");
|
||||
interface
|
||||
.write_response(Err(labeled_error.clone()))?
|
||||
.write()?;
|
||||
|
@ -1,4 +1,5 @@
|
||||
use nu_engine::documentation::get_flags_section;
|
||||
use nu_protocol::LabeledError;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
@ -13,9 +14,7 @@ use std::sync::mpsc::TrySendError;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
|
||||
use crate::plugin::interface::{EngineInterfaceManager, ReceivedPluginCall};
|
||||
use crate::protocol::{
|
||||
CallInfo, CustomValueOp, LabeledError, PluginCustomValue, PluginInput, PluginOutput,
|
||||
};
|
||||
use crate::protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
|
||||
use crate::EncodingType;
|
||||
|
||||
#[cfg(unix)]
|
||||
@ -230,7 +229,7 @@ where
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, Type, Value};
|
||||
/// # use nu_protocol::{PluginSignature, LabeledError, Type, Value};
|
||||
/// struct HelloPlugin;
|
||||
/// struct Hello;
|
||||
///
|
||||
@ -537,11 +536,12 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
|
||||
let result = if let Some(command) = commands.get(&name) {
|
||||
command.run(plugin, &engine, &call, input)
|
||||
} else {
|
||||
Err(LabeledError {
|
||||
label: format!("Plugin command not found: `{name}`"),
|
||||
msg: format!("plugin `{plugin_name}` doesn't have this command"),
|
||||
span: Some(call.head),
|
||||
})
|
||||
Err(
|
||||
LabeledError::new(format!("Plugin command not found: `{name}`")).with_label(
|
||||
format!("plugin `{plugin_name}` doesn't have this command"),
|
||||
call.head,
|
||||
),
|
||||
)
|
||||
};
|
||||
let write_result = engine
|
||||
.write_response(result)
|
||||
|
@ -12,8 +12,8 @@ use std::collections::HashMap;
|
||||
|
||||
pub use evaluated_call::EvaluatedCall;
|
||||
use nu_protocol::{
|
||||
ast::Operator, engine::Closure, Config, PipelineData, PluginSignature, RawStream, ShellError,
|
||||
Span, Spanned, Value,
|
||||
ast::Operator, engine::Closure, Config, LabeledError, PipelineData, PluginSignature, RawStream,
|
||||
ShellError, Span, Spanned, Value,
|
||||
};
|
||||
pub use plugin_custom_value::PluginCustomValue;
|
||||
#[cfg(test)]
|
||||
@ -289,73 +289,6 @@ pub enum StreamMessage {
|
||||
Ack(StreamId),
|
||||
}
|
||||
|
||||
/// An error message with debugging information that can be passed to Nushell from the plugin
|
||||
///
|
||||
/// The `LabeledError` struct is a structured error message that can be returned from
|
||||
/// a [Plugin](crate::Plugin)'s [`run`](crate::Plugin::run()) method. It contains
|
||||
/// the error message along with optional [Span] data to support highlighting in the
|
||||
/// shell.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
||||
pub struct LabeledError {
|
||||
/// The name of the error
|
||||
pub label: String,
|
||||
/// A detailed error description
|
||||
pub msg: String,
|
||||
/// The [Span] in which the error occurred
|
||||
pub span: Option<Span>,
|
||||
}
|
||||
|
||||
impl From<LabeledError> for ShellError {
|
||||
fn from(error: LabeledError) -> Self {
|
||||
if error.span.is_some() {
|
||||
ShellError::GenericError {
|
||||
error: error.label,
|
||||
msg: error.msg,
|
||||
span: error.span,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}
|
||||
} else {
|
||||
ShellError::GenericError {
|
||||
error: error.label,
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: (!error.msg.is_empty()).then_some(error.msg),
|
||||
inner: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ShellError> for LabeledError {
|
||||
fn from(error: ShellError) -> Self {
|
||||
use miette::Diagnostic;
|
||||
// This is not perfect - we can only take the first labeled span as that's all we have
|
||||
// space for.
|
||||
if let Some(labeled_span) = error.labels().and_then(|mut iter| iter.nth(0)) {
|
||||
let offset = labeled_span.offset();
|
||||
let span = Span::new(offset, offset + labeled_span.len());
|
||||
LabeledError {
|
||||
label: error.to_string(),
|
||||
msg: labeled_span
|
||||
.label()
|
||||
.map(|label| label.to_owned())
|
||||
.unwrap_or_else(|| "".into()),
|
||||
span: Some(span),
|
||||
}
|
||||
} else {
|
||||
LabeledError {
|
||||
label: error.to_string(),
|
||||
msg: error
|
||||
.help()
|
||||
.map(|help| help.to_string())
|
||||
.unwrap_or_else(|| "".into()),
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response to a [`PluginCall`]. The type parameter determines the output type for pipeline data.
|
||||
///
|
||||
/// Note: exported for internal use, not public.
|
||||
|
@ -1,11 +1,11 @@
|
||||
macro_rules! generate_tests {
|
||||
($encoder:expr) => {
|
||||
use crate::protocol::{
|
||||
CallInfo, CustomValueOp, EvaluatedCall, LabeledError, PipelineDataHeader, PluginCall,
|
||||
CallInfo, CustomValueOp, EvaluatedCall, PipelineDataHeader, PluginCall,
|
||||
PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
|
||||
StreamData, StreamMessage,
|
||||
};
|
||||
use nu_protocol::{PluginSignature, Span, Spanned, SyntaxShape, Value};
|
||||
use nu_protocol::{LabeledError, PluginSignature, Span, Spanned, SyntaxShape, Value};
|
||||
|
||||
#[test]
|
||||
fn decode_eof() {
|
||||
@ -364,11 +364,15 @@ macro_rules! generate_tests {
|
||||
|
||||
#[test]
|
||||
fn response_round_trip_error() {
|
||||
let error = LabeledError {
|
||||
label: "label".into(),
|
||||
msg: "msg".into(),
|
||||
span: Some(Span::new(2, 30)),
|
||||
};
|
||||
let error = LabeledError::new("label")
|
||||
.with_code("test::error")
|
||||
.with_url("https://example.org/test/error")
|
||||
.with_help("some help")
|
||||
.with_label("msg", Span::new(2, 30))
|
||||
.with_inner(ShellError::IOError {
|
||||
msg: "io error".into(),
|
||||
});
|
||||
|
||||
let response = PluginCallResponse::Error(error.clone());
|
||||
let output = PluginOutput::CallResponse(6, response);
|
||||
|
||||
@ -392,11 +396,7 @@ macro_rules! generate_tests {
|
||||
|
||||
#[test]
|
||||
fn response_round_trip_error_none() {
|
||||
let error = LabeledError {
|
||||
label: "label".into(),
|
||||
msg: "msg".into(),
|
||||
span: None,
|
||||
};
|
||||
let error = LabeledError::new("error");
|
||||
let response = PluginCallResponse::Error(error.clone());
|
||||
let output = PluginOutput::CallResponse(7, response);
|
||||
|
||||
|
Reference in New Issue
Block a user