Add unified deprecation system and @deprecated attribute (#15770)

This commit is contained in:
132ikl
2025-06-01 09:55:47 -04:00
committed by GitHub
parent 8896ba80a4
commit cfbe835910
26 changed files with 719 additions and 54 deletions

View 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,
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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::*;

View File

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

View File

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

View File

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