mirror of
https://github.com/nushell/nushell.git
synced 2025-08-16 22:01:42 +02:00
Add unified deprecation system and @deprecated attribute (#15770)
This commit is contained in:
144
crates/nu-protocol/src/deprecation.rs
Normal file
144
crates/nu-protocol/src/deprecation.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use crate::{FromValue, ParseWarning, ShellError, Type, Value, ast::Call};
|
||||
|
||||
// Make nu_protocol available in this namespace, consumers of this crate will
|
||||
// have this without such an export.
|
||||
// The `FromValue` derive macro fully qualifies paths to "nu_protocol".
|
||||
use crate::{self as nu_protocol, ReportMode, Span};
|
||||
|
||||
/// A entry which indicates that some part of, or all of, a command is deprecated
|
||||
///
|
||||
/// Commands can implement [`Command::deprecation_info`] to return deprecation entries,
|
||||
/// which will cause a parse-time warning. Additionally, custom commands can use the
|
||||
/// @deprecated attribute to add a `DeprecationEntry`.
|
||||
#[derive(FromValue)]
|
||||
pub struct DeprecationEntry {
|
||||
/// The type of deprecation
|
||||
// might need to revisit this if we added additional DeprecationTypes
|
||||
#[nu_value(rename = "flag", default)]
|
||||
pub ty: DeprecationType,
|
||||
/// How this deprecation should be reported
|
||||
#[nu_value(rename = "report")]
|
||||
pub report_mode: ReportMode,
|
||||
/// When this deprecation started
|
||||
pub since: Option<String>,
|
||||
/// When this item is expected to be removed
|
||||
pub expected_removal: Option<String>,
|
||||
/// Help text, possibly including a suggestion for what to use instead
|
||||
pub help: Option<String>,
|
||||
}
|
||||
|
||||
/// What this deprecation affects
|
||||
#[derive(Default)]
|
||||
pub enum DeprecationType {
|
||||
/// Deprecation of whole command
|
||||
#[default]
|
||||
Command,
|
||||
/// Deprecation of a flag/switch
|
||||
Flag(String),
|
||||
}
|
||||
|
||||
impl FromValue for DeprecationType {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::String { val, .. } => Ok(DeprecationType::Flag(val)),
|
||||
Value::Nothing { .. } => Ok(DeprecationType::Command),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::String
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for ReportMode {
|
||||
fn from_value(v: Value) -> Result<Self, ShellError> {
|
||||
let span = v.span();
|
||||
let Value::String { val, .. } = v else {
|
||||
return Err(ShellError::CantConvert {
|
||||
to_type: Self::expected_type().to_string(),
|
||||
from_type: v.get_type().to_string(),
|
||||
span: v.span(),
|
||||
help: None,
|
||||
});
|
||||
};
|
||||
match val.as_str() {
|
||||
"first" => Ok(ReportMode::FirstUse),
|
||||
"every" => Ok(ReportMode::EveryUse),
|
||||
_ => Err(ShellError::InvalidValue {
|
||||
valid: "first or every".into(),
|
||||
actual: val,
|
||||
span,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_type() -> Type {
|
||||
Type::String
|
||||
}
|
||||
}
|
||||
|
||||
impl DeprecationEntry {
|
||||
fn check(&self, call: &Call) -> bool {
|
||||
match &self.ty {
|
||||
DeprecationType::Command => true,
|
||||
DeprecationType::Flag(flag) => call.get_named_arg(flag).is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
fn type_name(&self) -> String {
|
||||
match &self.ty {
|
||||
DeprecationType::Command => "Command".to_string(),
|
||||
DeprecationType::Flag(_) => "Flag".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self, command_name: &str) -> String {
|
||||
let name = match &self.ty {
|
||||
DeprecationType::Command => command_name,
|
||||
DeprecationType::Flag(flag) => &format!("{command_name} --{flag}"),
|
||||
};
|
||||
let since = match &self.since {
|
||||
Some(since) => format!("was deprecated in {since}"),
|
||||
None => "is deprecated".to_string(),
|
||||
};
|
||||
let removal = match &self.expected_removal {
|
||||
Some(expected) => format!("and will be removed in {expected}"),
|
||||
None => "and will be removed in a future release".to_string(),
|
||||
};
|
||||
format!("{name} {since} {removal}.")
|
||||
}
|
||||
|
||||
fn span(&self, call: &Call) -> Span {
|
||||
match &self.ty {
|
||||
DeprecationType::Command => call.span(),
|
||||
DeprecationType::Flag(flag) => call
|
||||
.get_named_arg(flag)
|
||||
.map(|arg| arg.span)
|
||||
.unwrap_or(Span::unknown()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_warning(self, command_name: &str, call: &Call) -> Option<ParseWarning> {
|
||||
if !self.check(call) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let dep_type = self.type_name();
|
||||
let label = self.label(command_name);
|
||||
let span = self.span(call);
|
||||
let report_mode = self.report_mode;
|
||||
Some(ParseWarning::DeprecationWarning {
|
||||
dep_type,
|
||||
label,
|
||||
span,
|
||||
report_mode,
|
||||
help: self.help,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use super::{EngineState, Stack, StateWorkingSet};
|
||||
use crate::{
|
||||
Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature, Value, engine::Call,
|
||||
Alias, BlockId, DeprecationEntry, Example, OutDest, PipelineData, ShellError, Signature, Value,
|
||||
engine::Call,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
@ -133,6 +134,10 @@ pub trait Command: Send + Sync + CommandClone {
|
||||
self.command_type() == CommandType::Plugin
|
||||
}
|
||||
|
||||
fn deprecation_info(&self) -> Vec<DeprecationEntry> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn pipe_redirection(&self) -> (Option<OutDest>, Option<OutDest>) {
|
||||
(None, None)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::{
|
||||
ModuleId, OverlayId, ShellError, SignalAction, Signals, Signature, Span, SpanId, Type, Value,
|
||||
VarId, VirtualPathId,
|
||||
ast::Block,
|
||||
cli_error::ReportLog,
|
||||
debugger::{Debugger, NoopDebugger},
|
||||
engine::{
|
||||
CachedFile, Command, CommandType, DEFAULT_OVERLAY_NAME, EnvVars, OverlayFrame, ScopeFrame,
|
||||
@ -115,6 +116,7 @@ pub struct EngineState {
|
||||
startup_time: i64,
|
||||
is_debugging: IsDebugging,
|
||||
pub debugger: Arc<Mutex<Box<dyn Debugger>>>,
|
||||
pub report_log: Arc<Mutex<ReportLog>>,
|
||||
|
||||
pub jobs: Arc<Mutex<Jobs>>,
|
||||
|
||||
@ -201,6 +203,7 @@ impl EngineState {
|
||||
startup_time: -1,
|
||||
is_debugging: IsDebugging::new(false),
|
||||
debugger: Arc::new(Mutex::new(Box::new(NoopDebugger))),
|
||||
report_log: Arc::default(),
|
||||
jobs: Arc::new(Mutex::new(Jobs::default())),
|
||||
current_job: CurrentJob {
|
||||
id: JobId::new(0),
|
||||
|
@ -1,6 +1,8 @@
|
||||
//! This module manages the step of turning error types into printed error messages
|
||||
//!
|
||||
//! Relies on the `miette` crate for pretty layout
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
|
||||
use crate::{
|
||||
CompileError, ErrorStyle, ParseError, ParseWarning, ShellError,
|
||||
engine::{EngineState, StateWorkingSet},
|
||||
@ -9,6 +11,7 @@ use miette::{
|
||||
LabeledSpan, MietteHandlerOpts, NarratableReportHandler, ReportHandler, RgbColors, Severity,
|
||||
SourceCode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
/// This error exists so that we can defer SourceCode handling. It simply
|
||||
@ -20,6 +23,46 @@ struct CliError<'src>(
|
||||
pub &'src StateWorkingSet<'src>,
|
||||
);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReportLog {
|
||||
// A bloom-filter like structure to store the hashes of `ParseWarning`s,
|
||||
// without actually permanently storing the entire warning in memory.
|
||||
// May rarely result in warnings incorrectly being unreported upon hash collision.
|
||||
parse_warnings: Vec<u64>,
|
||||
}
|
||||
|
||||
/// How a warning/error should be reported
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum ReportMode {
|
||||
FirstUse,
|
||||
EveryUse,
|
||||
}
|
||||
|
||||
/// Returns true if this warning should be reported
|
||||
fn should_show_warning(engine_state: &EngineState, warning: &ParseWarning) -> bool {
|
||||
match warning.report_mode() {
|
||||
ReportMode::EveryUse => true,
|
||||
ReportMode::FirstUse => {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
warning.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
let mut report_log = engine_state
|
||||
.report_log
|
||||
.lock()
|
||||
.expect("report log lock is poisioned");
|
||||
|
||||
match report_log.parse_warnings.contains(&hash) {
|
||||
true => false,
|
||||
false => {
|
||||
report_log.parse_warnings.push(hash);
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_shell_error(working_set: &StateWorkingSet, error: &ShellError) -> String {
|
||||
format!("Error: {:?}", CliError(error, working_set))
|
||||
}
|
||||
@ -30,9 +73,9 @@ pub fn report_shell_error(engine_state: &EngineState, error: &ShellError) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_shell_warning(engine_state: &EngineState, error: &ShellError) {
|
||||
if engine_state.config.display_errors.should_show(error) {
|
||||
report_warning(&StateWorkingSet::new(engine_state), error)
|
||||
pub fn report_shell_warning(engine_state: &EngineState, warning: &ShellError) {
|
||||
if engine_state.config.display_errors.should_show(warning) {
|
||||
report_warning(&StateWorkingSet::new(engine_state), warning)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,8 +83,10 @@ pub fn report_parse_error(working_set: &StateWorkingSet, error: &ParseError) {
|
||||
report_error(working_set, error);
|
||||
}
|
||||
|
||||
pub fn report_parse_warning(working_set: &StateWorkingSet, error: &ParseWarning) {
|
||||
report_warning(working_set, error);
|
||||
pub fn report_parse_warning(working_set: &StateWorkingSet, warning: &ParseWarning) {
|
||||
if should_show_warning(working_set.permanent(), warning) {
|
||||
report_warning(working_set, warning);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError) {
|
||||
@ -57,8 +102,8 @@ fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
|
||||
}
|
||||
}
|
||||
|
||||
fn report_warning(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
|
||||
eprintln!("Warning: {:?}", CliError(error, working_set));
|
||||
fn report_warning(working_set: &StateWorkingSet, warning: &dyn miette::Diagnostic) {
|
||||
eprintln!("Warning: {:?}", CliError(warning, working_set));
|
||||
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ mod parse_warning;
|
||||
pub mod shell_error;
|
||||
|
||||
pub use cli_error::{
|
||||
format_shell_error, report_parse_error, report_parse_warning, report_shell_error,
|
||||
ReportMode, format_shell_error, report_parse_error, report_parse_warning, report_shell_error,
|
||||
report_shell_warning,
|
||||
};
|
||||
pub use compile_error::CompileError;
|
||||
|
@ -1,27 +1,50 @@
|
||||
use crate::Span;
|
||||
use miette::Diagnostic;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::Hash;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::ReportMode;
|
||||
|
||||
#[derive(Clone, Debug, Error, Diagnostic, Serialize, Deserialize)]
|
||||
pub enum ParseWarning {
|
||||
#[error("Deprecated: {old_command}")]
|
||||
#[diagnostic(help("for more info see {url}"))]
|
||||
DeprecatedWarning {
|
||||
old_command: String,
|
||||
new_suggestion: String,
|
||||
#[label(
|
||||
"`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead."
|
||||
)]
|
||||
#[error("{dep_type} deprecated.")]
|
||||
#[diagnostic(code(nu::parser::deprecated))]
|
||||
DeprecationWarning {
|
||||
dep_type: String,
|
||||
#[label("{label}")]
|
||||
span: Span,
|
||||
url: String,
|
||||
label: String,
|
||||
report_mode: ReportMode,
|
||||
#[help]
|
||||
help: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ParseWarning {
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
ParseWarning::DeprecatedWarning { span, .. } => *span,
|
||||
ParseWarning::DeprecationWarning { span, .. } => *span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_mode(&self) -> ReportMode {
|
||||
match self {
|
||||
ParseWarning::DeprecationWarning { report_mode, .. } => *report_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To keep track of reported warnings
|
||||
impl Hash for ParseWarning {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
ParseWarning::DeprecationWarning {
|
||||
dep_type, label, ..
|
||||
} => {
|
||||
dep_type.hash(state);
|
||||
label.hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ pub mod ast;
|
||||
pub mod casing;
|
||||
pub mod config;
|
||||
pub mod debugger;
|
||||
mod deprecation;
|
||||
mod did_you_mean;
|
||||
pub mod engine;
|
||||
mod errors;
|
||||
@ -30,6 +31,7 @@ mod value;
|
||||
pub use alias::*;
|
||||
pub use ast::unit::*;
|
||||
pub use config::*;
|
||||
pub use deprecation::*;
|
||||
pub use did_you_mean::did_you_mean;
|
||||
pub use engine::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID};
|
||||
pub use errors::*;
|
||||
|
@ -1,8 +1,9 @@
|
||||
use crate::{
|
||||
BlockId, Example, PipelineData, ShellError, SyntaxShape, Type, Value, VarId,
|
||||
BlockId, DeprecationEntry, Example, FromValue, PipelineData, ShellError, SyntaxShape, Type,
|
||||
Value, VarId,
|
||||
engine::{Call, Command, CommandType, EngineState, Stack},
|
||||
};
|
||||
use nu_derive_value::FromValue;
|
||||
use nu_derive_value::FromValue as DeriveFromValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
@ -701,7 +702,7 @@ fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, FromValue)]
|
||||
#[derive(Clone, DeriveFromValue)]
|
||||
pub struct CustomExample {
|
||||
pub example: String,
|
||||
pub description: String,
|
||||
@ -785,4 +786,16 @@ impl Command for BlockCommand {
|
||||
.map(String::as_str)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn deprecation_info(&self) -> Vec<DeprecationEntry> {
|
||||
self.attributes
|
||||
.iter()
|
||||
.filter_map(|(key, value)| {
|
||||
(key == "deprecated")
|
||||
.then_some(value.clone())
|
||||
.map(DeprecationEntry::from_value)
|
||||
.and_then(Result::ok)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ use std::{
|
||||
/// - If `#[nu_value(rename_all = "...")]` is applied on the container (struct) the key of the
|
||||
/// field will be case-converted accordingly.
|
||||
/// - If neither attribute is applied, the field name is used as is.
|
||||
/// - If `#[nu_value(default)]` is applied to a field, the field type's [`Default`] implementation
|
||||
/// will be used if the corresponding record field is missing
|
||||
///
|
||||
/// Supported case conversions include those provided by [`heck`], such as
|
||||
/// "snake_case", "kebab-case", "PascalCase", and others.
|
||||
|
@ -669,3 +669,30 @@ fn renamed_variant_enum_roundtrip() {
|
||||
.into_test_value();
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[derive(IntoValue, FromValue, Default, Debug, PartialEq)]
|
||||
struct DefaultFieldStruct {
|
||||
#[nu_value(default)]
|
||||
field: String,
|
||||
#[nu_value(rename = "renamed", default)]
|
||||
field_two: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field_struct_from_value() {
|
||||
let populated = DefaultFieldStruct {
|
||||
field: "hello".into(),
|
||||
field_two: "world".into(),
|
||||
};
|
||||
let populated_record = Value::test_record(record! {
|
||||
"field" => Value::test_string("hello"),
|
||||
"renamed" => Value::test_string("world"),
|
||||
});
|
||||
let actual = DefaultFieldStruct::from_value(populated_record).unwrap();
|
||||
assert_eq!(populated, actual);
|
||||
|
||||
let default = DefaultFieldStruct::default();
|
||||
let default_record = Value::test_record(Record::new());
|
||||
let actual = DefaultFieldStruct::from_value(default_record).unwrap();
|
||||
assert_eq!(default, actual);
|
||||
}
|
||||
|
Reference in New Issue
Block a user