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

@ -19,6 +19,7 @@ nu-engine = { path = "../nu-engine", version = "0.104.2", default-features = fal
nu-parser = { path = "../nu-parser", version = "0.104.2" }
nu-protocol = { path = "../nu-protocol", version = "0.104.2", default-features = false }
nu-utils = { path = "../nu-utils", version = "0.104.2", default-features = false }
nu-cmd-base = { path = "../nu-cmd-base", version = "0.104.2" }
itertools = { workspace = true }
shadow-rs = { version = "1.1", default-features = false }
@ -29,6 +30,7 @@ shadow-rs = { version = "1.1", default-features = false, features = ["build"] }
[dev-dependencies]
quickcheck = { workspace = true }
quickcheck_macros = { workspace = true }
miette = { workspace = true }
[features]
default = ["os"]

View File

@ -0,0 +1,148 @@
use nu_cmd_base::WrapCall;
use nu_engine::command_prelude::*;
#[derive(Clone)]
pub struct AttrDeprecated;
impl Command for AttrDeprecated {
fn name(&self) -> &str {
"attr deprecated"
}
fn signature(&self) -> Signature {
Signature::build("attr deprecated")
.input_output_types(vec![
(Type::Nothing, Type::Nothing),
(Type::Nothing, Type::String),
])
.optional(
"message",
SyntaxShape::String,
"Help message to include with deprecation warning.",
)
.named(
"flag",
SyntaxShape::String,
"Mark a flag as deprecated rather than the command",
None,
)
.named(
"since",
SyntaxShape::String,
"Denote a version when this item was deprecated",
Some('s'),
)
.named(
"remove",
SyntaxShape::String,
"Denote a version when this item will be removed",
Some('r'),
)
.named(
"report",
SyntaxShape::String,
"How to warn about this item. One of: first (default), every",
None,
)
.category(Category::Core)
}
fn description(&self) -> &str {
"Attribute for marking a command or flag as deprecated."
}
fn extra_description(&self) -> &str {
"Mark a command (default) or flag/switch (--flag) as deprecated. By default, only the first usage will trigger a deprecation warning.
A help message can be included to provide more context for the deprecation, such as what to use as a replacement.
Also consider setting the category to deprecated with @category deprecated"
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let call = WrapCall::Eval(engine_state, stack, call);
Ok(deprecated_record(call)?.into_pipeline_data())
}
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let call = WrapCall::ConstEval(working_set, call);
Ok(deprecated_record(call)?.into_pipeline_data())
}
fn is_const(&self) -> bool {
true
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Add a deprecation warning to a custom command",
example: r###"@deprecated
def outdated [] {}"###,
result: Some(Value::nothing(Span::test_data())),
},
Example {
description: "Add a deprecation warning with a custom message",
example: r###"@deprecated "Use my-new-command instead."
@category deprecated
def my-old-command [] {}"###,
result: Some(Value::string(
"Use my-new-command instead.",
Span::test_data(),
)),
},
]
}
}
fn deprecated_record(call: WrapCall) -> Result<Value, ShellError> {
let (call, message): (_, Option<Spanned<String>>) = call.opt(0)?;
let (call, flag): (_, Option<Spanned<String>>) = call.get_flag("flag")?;
let (call, since): (_, Option<Spanned<String>>) = call.get_flag("since")?;
let (call, remove): (_, Option<Spanned<String>>) = call.get_flag("remove")?;
let (call, report): (_, Option<Spanned<String>>) = call.get_flag("report")?;
let mut record = Record::new();
if let Some(message) = message {
record.push("help", Value::string(message.item, message.span))
}
if let Some(flag) = flag {
record.push("flag", Value::string(flag.item, flag.span))
}
if let Some(since) = since {
record.push("since", Value::string(since.item, since.span))
}
if let Some(remove) = remove {
record.push("expected_removal", Value::string(remove.item, remove.span))
}
let report = if let Some(Spanned { item, span }) = report {
match item.as_str() {
"every" => Value::string(item, span),
"first" => Value::string(item, span),
_ => {
return Err(ShellError::IncorrectValue {
msg: "The report mode must be one of: every, first".into(),
val_span: span,
call_span: call.head(),
});
}
}
} else {
Value::string("first", call.head())
};
record.push("report", report);
Ok(Value::record(record, call.head()))
}

View File

@ -1,7 +1,9 @@
mod category;
mod deprecated;
mod example;
mod search_terms;
pub use category::AttrCategory;
pub use deprecated::AttrDeprecated;
pub use example::AttrExample;
pub use search_terms::AttrSearchTerms;

View File

@ -17,6 +17,7 @@ pub fn create_default_context() -> EngineState {
bind_command! {
Alias,
AttrCategory,
AttrDeprecated,
AttrExample,
AttrSearchTerms,
Break,

View File

@ -0,0 +1,114 @@
use miette::{Diagnostic, LabeledSpan};
use nu_cmd_lang::{Alias, Def};
use nu_parser::parse;
use nu_protocol::engine::{EngineState, StateWorkingSet};
use nu_cmd_lang::AttrDeprecated;
#[test]
pub fn test_deprecated_attribute() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(Def));
working_set.add_decl(Box::new(Alias));
working_set.add_decl(Box::new(AttrDeprecated));
// test deprecation with no message
let source = br#"
@deprecated
def foo [] {}
"#;
let _ = parse(&mut working_set, None, source, false);
// there should be no warning until the command is called
assert!(working_set.parse_errors.is_empty());
assert!(working_set.parse_warnings.is_empty());
let source = b"foo";
let _ = parse(&mut working_set, None, source, false);
// command called, there should be a deprecation warning
assert!(working_set.parse_errors.is_empty());
assert!(!working_set.parse_warnings.is_empty());
let labels: Vec<LabeledSpan> = working_set.parse_warnings[0].labels().unwrap().collect();
let label = labels.first().unwrap().label().unwrap();
assert!(label.contains("foo is deprecated"));
working_set.parse_warnings.clear();
// test deprecation with message
let source = br#"
@deprecated "Use new-command instead"
def old-command [] {}
old-command
"#;
let _ = parse(&mut working_set, None, source, false);
assert!(working_set.parse_errors.is_empty());
assert!(!working_set.parse_warnings.is_empty());
let help = &working_set.parse_warnings[0].help().unwrap().to_string();
assert!(help.contains("Use new-command instead"));
}
#[test]
pub fn test_deprecated_attribute_flag() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(Def));
working_set.add_decl(Box::new(Alias));
working_set.add_decl(Box::new(AttrDeprecated));
let source = br#"
@deprecated "Use foo instead of bar" --flag bar
@deprecated "Use foo instead of baz" --flag baz
def old-command [--foo, --bar, --baz] {}
old-command --foo
old-command --bar
old-command --baz
old-command --foo --bar --baz
"#;
let _ = parse(&mut working_set, None, source, false);
assert!(working_set.parse_errors.is_empty());
assert!(!working_set.parse_warnings.is_empty());
let help = &working_set.parse_warnings[0].help().unwrap().to_string();
assert!(help.contains("Use foo instead of bar"));
let help = &working_set.parse_warnings[1].help().unwrap().to_string();
assert!(help.contains("Use foo instead of baz"));
let help = &working_set.parse_warnings[2].help().unwrap().to_string();
assert!(help.contains("Use foo instead of bar"));
let help = &working_set.parse_warnings[3].help().unwrap().to_string();
assert!(help.contains("Use foo instead of baz"));
}
#[test]
pub fn test_deprecated_attribute_since_remove() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(Def));
working_set.add_decl(Box::new(Alias));
working_set.add_decl(Box::new(AttrDeprecated));
let source = br#"
@deprecated --since 0.10000.0 --remove 1.0
def old-command [] {}
old-command
"#;
let _ = parse(&mut working_set, None, source, false);
assert!(working_set.parse_errors.is_empty());
assert!(!working_set.parse_warnings.is_empty());
let labels: Vec<LabeledSpan> = working_set.parse_warnings[0].labels().unwrap().collect();
let label = labels.first().unwrap().label().unwrap();
assert!(label.contains("0.10000.0"));
assert!(label.contains("1.0"));
}

View File

@ -0,0 +1 @@
mod deprecated;

View File

@ -0,0 +1 @@
mod attr;

View File

@ -0,0 +1 @@
mod commands;