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:
Devyn Cairns
2024-03-21 04:27:21 -07:00
committed by GitHub
parent 8237d15683
commit efe25e3f58
42 changed files with 453 additions and 307 deletions

View File

@ -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.

View File

@ -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;
///

View File

@ -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::{

View File

@ -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()?;

View File

@ -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)

View File

@ -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.

View File

@ -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);