diff --git a/src/analyzer.rs b/src/analyzer.rs index bb67dedb..615eea04 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -3747,6 +3747,10 @@ mod tests { "Setting `windows-shell` expects an array value", lsp::Range::at(0, 0, 1, 0), ) + .warning( + "`windows-shell` is deprecated, use `[windows]` attribute on `set shell` instead", + lsp::Range::at(0, 4, 0, 17), + ) .run(); } @@ -3760,6 +3764,28 @@ mod tests { echo \"foo\" " }) + .warning( + "`windows-shell` is deprecated, use `[windows]` attribute on `set shell` instead", + lsp::Range::at(0, 4, 0, 17), + ) + .run(); + } + + #[test] + fn settings_windows_shell_replacement() { + Test::new(indoc! { + " + set lists + + [windows] + set shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"] + set windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"] + " + }) + .warning( + "`windows-shell` is deprecated, use `[windows]` attribute on `set shell` instead", + lsp::Range::at(4, 4, 4, 17), + ) .run(); } diff --git a/src/builtin.rs b/src/builtin.rs index 7a6f1c35..27daae59 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -17,13 +17,13 @@ pub enum Builtin<'a> { aliases: &'a [&'a str], kind: FunctionKind, description: &'a str, - deprecated: Option<&'a str>, + deprecated: Option>, }, Setting { name: &'a str, kind: SettingKind, description: &'a str, - deprecated: Option<&'a str>, + deprecated: Option>, }, } @@ -250,7 +250,7 @@ mod tests { aliases: &[], kind: FunctionKind::Nullary, description: "", - deprecated: Some("bar"), + deprecated: Some(Deprecation::Replacement("bar")), } .completion_items() .into_iter() diff --git a/src/builtins.rs b/src/builtins.rs index d45b641a..41cf5c3d 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -631,11 +631,12 @@ pub const BUILTINS: &[Builtin<'_>] = &[ kind: AttributeKind::Nullary, description: indoc! { " - Enable the recipe on Windows. + Enable a recipe or setting on Windows. Part of the platform-gating family of attributes. When any platform attribute is present, the recipe is only enabled when - one of the active platforms matches. + one of the active platforms matches. It can also specialize + `set shell` for Windows. ```just [windows] @@ -1307,7 +1308,7 @@ pub const BUILTINS: &[Builtin<'_>] = &[ variable is unset. " }, - deprecated: Some("env"), + deprecated: Some(Deprecation::Replacement("env")), }, Builtin::Function { name: "env_var_or_default", @@ -1321,7 +1322,7 @@ pub const BUILTINS: &[Builtin<'_>] = &[ `default` when the variable is unset. " }, - deprecated: Some("env"), + deprecated: Some(Deprecation::Replacement("env")), }, Builtin::Function { name: "error", @@ -2762,13 +2763,16 @@ pub const BUILTINS: &[Builtin<'_>] = &[ for a more flexible, version-agnostic alternative. " }, - deprecated: Some("windows-shell"), + deprecated: Some(Deprecation::Replacement("windows-shell")), }, Builtin::Setting { name: "windows-shell", kind: SettingKind::Array, description: indoc! { " + **Deprecated**: use the `[windows]` attribute on `set shell` + instead. + Set the command used to invoke recipes and evaluate backticks on Windows. @@ -2783,7 +2787,10 @@ pub const BUILTINS: &[Builtin<'_>] = &[ ``` " }, - deprecated: None, + deprecated: Some(Deprecation::SettingAttribute { + attribute: "windows", + setting: "shell", + }), }, Builtin::Setting { name: "working-directory", diff --git a/src/deprecation.rs b/src/deprecation.rs new file mode 100644 index 00000000..3e07d462 --- /dev/null +++ b/src/deprecation.rs @@ -0,0 +1,21 @@ +use super::*; + +#[derive(Clone, Copy, Debug)] +pub enum Deprecation<'a> { + Replacement(&'a str), + SettingAttribute { + attribute: &'a str, + setting: &'a str, + }, +} + +impl Display for Deprecation<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Replacement(replacement) => write!(f, "`{replacement}`"), + Self::SettingAttribute { attribute, setting } => { + write!(f, "`[{attribute}]` attribute on `set {setting}`") + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index c5ce8d1a..f241a61c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ pub use { dependency::Dependency, dependency_argument::DependencyArgument, dependency_phase::DependencyPhase, + deprecation::Deprecation, diagnostic::Diagnostic, document::Document, error::Error, @@ -70,6 +71,7 @@ mod count; mod dependency; mod dependency_argument; mod dependency_phase; +mod deprecation; mod diagnostic; mod document; mod error; diff --git a/src/quickfix.rs b/src/quickfix.rs index 28dc38fe..76979824 100644 --- a/src/quickfix.rs +++ b/src/quickfix.rs @@ -33,4 +33,31 @@ impl Quickfix { title: format!("Replace `{}` with `{replacement}`", name.value), } } + + #[must_use] + pub fn setting_attribute( + setting: &Setting, + document: &Document, + attribute: &str, + replacement: &str, + ) -> Self { + let line = document + .content + .line(setting.range.start.line as usize) + .to_string(); + + let line = line.replacen(&setting.name.value, replacement, 1); + + Self { + edits: vec![lsp::TextEdit { + range: setting.range, + new_text: format!("[{attribute}]\n{line}"), + }], + range: setting.name.range, + title: format!( + "Replace `{}` with `[{attribute}] set {replacement}`", + setting.name.value + ), + } + } } diff --git a/src/quickfixer.rs b/src/quickfixer.rs index b6a1b1f9..20b2a1e9 100644 --- a/src/quickfixer.rs +++ b/src/quickfixer.rs @@ -226,6 +226,34 @@ mod tests { .run(); } + #[test] + fn replaces_windows_shell_setting() { + Test::new( + "set windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n", + ) + .range(lsp::Range::at(0, 4, 0, 4)) + .quickfix(Quickfix { + edits: vec![lsp::TextEdit { + range: lsp::Range::at(0, 0, 1, 0), + new_text: + "[windows]\nset shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n" + .to_string(), + }], + range: lsp::Range::at(0, 4, 0, 17), + title: "Replace `windows-shell` with `[windows] set shell`".to_string(), + }) + .run(); + } + + #[test] + fn skips_windows_shell_setting_when_replacement_exists() { + Test::new( + "[windows]\nset shell := [\"foo\"]\nset windows-shell := [\"bar\"]\n", + ) + .range(lsp::Range::at(2, 4, 2, 4)) + .run(); + } + #[test] fn skips_disabled_rules() { let config = serde_json::from_value::(serde_json::json!({ diff --git a/src/rule/deprecated_function.rs b/src/rule/deprecated_function.rs index 099daa90..a3423230 100644 --- a/src/rule/deprecated_function.rs +++ b/src/rule/deprecated_function.rs @@ -12,13 +12,13 @@ define_rule! { let function_name = &function_call.name.value; if let Some(Builtin::Function { - deprecated: Some(replacement), + deprecated: Some(deprecation), .. }) = context.builtin_function(function_name.as_str()) { diagnostics.push(Diagnostic::warning( format!( - "`{function_name}` is deprecated, use `{replacement}` instead" + "`{function_name}` is deprecated, use {deprecation} instead" ), function_call.name.range, )); @@ -34,13 +34,13 @@ define_rule! { let function_name = &function_call.name.value; if let Some(Builtin::Function { - deprecated: Some(replacement), + deprecated: Some(Deprecation::Replacement(replacement)), .. }) = context.builtin_function(function_name.as_str()) { quickfixes.push(Quickfix::replacement( &function_call.name, - replacement.to_string(), + *replacement, )); } } diff --git a/src/rule/deprecated_setting.rs b/src/rule/deprecated_setting.rs index 4a00eda4..d62bfa0b 100644 --- a/src/rule/deprecated_setting.rs +++ b/src/rule/deprecated_setting.rs @@ -10,13 +10,13 @@ define_rule! { for setting in context.settings() { if let Some(Builtin::Setting { - deprecated: Some(replacement), + deprecated: Some(deprecation), .. }) = context.builtin_setting(&setting.name.value) { diagnostics.push(Diagnostic::warning( format!( - "`{}` is deprecated, use `{replacement}` instead", + "`{}` is deprecated, use {deprecation} instead", setting.name.value ), setting.name.range, @@ -28,17 +28,39 @@ define_rule! { }, quickfixes(context) { let mut quickfixes = Vec::new(); - for setting in context.settings() { if let Some(Builtin::Setting { - deprecated: Some(replacement), + deprecated: Some(deprecation), .. }) = context.builtin_setting(&setting.name.value) { - quickfixes.push(Quickfix::replacement( - &setting.name, - replacement.to_string(), - )); + let deprecation = *deprecation; + + let quickfix = match deprecation { + Deprecation::Replacement(replacement) => { + Quickfix::replacement(&setting.name, replacement) + } + Deprecation::SettingAttribute { + attribute, + setting: replacement, + } => { + if context.settings().iter().any(|replacement_setting| { + replacement_setting.name.value == replacement + && replacement_setting.has_attribute(attribute) + }) { + continue; + } + + Quickfix::setting_attribute( + setting, + context.document(), + attribute, + replacement, + ) + } + }; + + quickfixes.push(quickfix); } } diff --git a/src/setting.rs b/src/setting.rs index f667a976..48099f90 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -94,6 +94,14 @@ impl Setting { range, }) } + + #[must_use] + pub fn has_attribute(&self, name: &str) -> bool { + self + .attributes + .iter() + .any(|attribute| attribute.name.value == name) + } } #[cfg(test)]