mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 02:55:07 +02:00
Add unified deprecation system and @deprecated attribute (#15770)
This commit is contained in:
@ -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"]
|
||||
|
148
crates/nu-cmd-lang/src/core_commands/attr/deprecated.rs
Normal file
148
crates/nu-cmd-lang/src/core_commands/attr/deprecated.rs
Normal 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()))
|
||||
}
|
@ -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;
|
||||
|
@ -17,6 +17,7 @@ pub fn create_default_context() -> EngineState {
|
||||
bind_command! {
|
||||
Alias,
|
||||
AttrCategory,
|
||||
AttrDeprecated,
|
||||
AttrExample,
|
||||
AttrSearchTerms,
|
||||
Break,
|
||||
|
114
crates/nu-cmd-lang/tests/commands/attr/deprecated.rs
Normal file
114
crates/nu-cmd-lang/tests/commands/attr/deprecated.rs
Normal 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"));
|
||||
}
|
1
crates/nu-cmd-lang/tests/commands/attr/mod.rs
Normal file
1
crates/nu-cmd-lang/tests/commands/attr/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
mod deprecated;
|
1
crates/nu-cmd-lang/tests/commands/mod.rs
Normal file
1
crates/nu-cmd-lang/tests/commands/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
mod attr;
|
1
crates/nu-cmd-lang/tests/main.rs
Normal file
1
crates/nu-cmd-lang/tests/main.rs
Normal file
@ -0,0 +1 @@
|
||||
mod commands;
|
Reference in New Issue
Block a user