From a8334d29f9cecad7d69e1ddfe8a45d16b575bdc0 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 8 Jul 2026 11:38:25 -0400 Subject: [PATCH 1/7] Mark `windows-shell` setting as deprecated --- src/analyzer.rs | 8 ++++++++ src/builtins.rs | 5 ++++- src/quickfixer.rs | 9 +++++++++ src/rule/deprecated_setting.rs | 4 ++++ src/server.rs | 23 +++++++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index bb67dedb..2c273ff2 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,10 @@ mod tests { echo \"foo\" " }) + .warning( + "`windows-shell` is deprecated, use `[windows] attribute on `set shell`` instead", + lsp::Range::at(0, 4, 0, 17), + ) .run(); } diff --git a/src/builtins.rs b/src/builtins.rs index d45b641a..5d6cb450 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -2769,6 +2769,9 @@ pub const BUILTINS: &[Builtin<'_>] = &[ 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 +2786,7 @@ pub const BUILTINS: &[Builtin<'_>] = &[ ``` " }, - deprecated: None, + deprecated: Some("[windows] attribute on `set shell`"), }, Builtin::Setting { name: "working-directory", diff --git a/src/quickfixer.rs b/src/quickfixer.rs index b6a1b1f9..134b6482 100644 --- a/src/quickfixer.rs +++ b/src/quickfixer.rs @@ -226,6 +226,15 @@ mod tests { .run(); } + #[test] + fn skips_non_rename_deprecated_setting() { + Test::new( + "set windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n", + ) + .range(lsp::Range::at(0, 4, 0, 4)) + .run(); + } + #[test] fn skips_disabled_rules() { let config = serde_json::from_value::(serde_json::json!({ diff --git a/src/rule/deprecated_setting.rs b/src/rule/deprecated_setting.rs index 4a00eda4..359a2045 100644 --- a/src/rule/deprecated_setting.rs +++ b/src/rule/deprecated_setting.rs @@ -35,6 +35,10 @@ define_rule! { .. }) = context.builtin_setting(&setting.name.value) { + if replacement.chars().any(|c| c == ' ' || c == '`') { + continue; + } + quickfixes.push(Quickfix::replacement( &setting.name, replacement.to_string(), diff --git a/src/server.rs b/src/server.rs index 2aa116e8..5eebe597 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3209,6 +3209,29 @@ mod tests { .await } + #[tokio::test] + async fn code_action_windows_shell_deprecated_no_quickfix() -> Result { + Test::new() + .request(InitializeRequest { id: 1 }) + .response(InitializeResponse { id: 1 }) + .notification(DidOpenNotification { + uri: "file:///test.just", + text: "set windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n", + }) + .request(CodeActionRequest { + id: 2, + uri: "file:///test.just", + range: lsp::Range::at(0, 4, 0, 4), + }) + .response(json!({ + "jsonrpc": "2.0", + "id": 2, + "result": [] + })) + .run() + .await + } + #[tokio::test] async fn code_action_deprecated_function_outside_range() -> Result { Test::new() From ba947b7bcd070e1f9a78c44805282405b54a4a18 Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 10 Jul 2026 23:31:05 -0400 Subject: [PATCH 2/7] Add windows shell migration quickfix --- src/quickfix.rs | 26 ++++++++++++++++++++++++++ src/quickfixer.rs | 12 +++++++++++- src/rule/deprecated_setting.rs | 17 ++++++++++------- src/server.rs | 22 ++++++++++++++++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/quickfix.rs b/src/quickfix.rs index 28dc38fe..c8fe75a8 100644 --- a/src/quickfix.rs +++ b/src/quickfix.rs @@ -33,4 +33,30 @@ 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 134b6482..3f88e7f7 100644 --- a/src/quickfixer.rs +++ b/src/quickfixer.rs @@ -227,11 +227,21 @@ mod tests { } #[test] - fn skips_non_rename_deprecated_setting() { + 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(); } diff --git a/src/rule/deprecated_setting.rs b/src/rule/deprecated_setting.rs index 359a2045..e53808ae 100644 --- a/src/rule/deprecated_setting.rs +++ b/src/rule/deprecated_setting.rs @@ -35,14 +35,17 @@ define_rule! { .. }) = context.builtin_setting(&setting.name.value) { - if replacement.chars().any(|c| c == ' ' || c == '`') { - continue; - } + let quickfix = match setting.name.value.as_str() { + "windows-shell" => Quickfix::setting_attribute( + setting, + context.document(), + "windows", + "shell", + ), + _ => Quickfix::replacement(&setting.name, replacement.to_string()), + }; - quickfixes.push(Quickfix::replacement( - &setting.name, - replacement.to_string(), - )); + quickfixes.push(quickfix); } } diff --git a/src/server.rs b/src/server.rs index 5eebe597..c184efa0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3210,7 +3210,7 @@ mod tests { } #[tokio::test] - async fn code_action_windows_shell_deprecated_no_quickfix() -> Result { + async fn code_action_windows_shell_deprecated() -> Result { Test::new() .request(InitializeRequest { id: 1 }) .response(InitializeResponse { id: 1 }) @@ -3226,7 +3226,25 @@ mod tests { .response(json!({ "jsonrpc": "2.0", "id": 2, - "result": [] + "result": [ + { + "title": "Replace `windows-shell` with `[windows] set shell`", + "kind": "quickfix", + "edit": { + "changes": { + "file:///test.just": [ + { + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 1, "character": 0 } + }, + "newText": "[windows]\nset shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n" + } + ] + } + } + } + ] })) .run() .await From c926dd274fc50c1e34979fd967de3668a90778a0 Mon Sep 17 00:00:00 2001 From: Liam Date: Fri, 10 Jul 2026 23:35:11 -0400 Subject: [PATCH 3/7] Clean up deprecated setting guidance --- src/analyzer.rs | 4 ++-- src/builtins.rs | 2 +- src/rule/deprecated_setting.rs | 8 ++++++- src/server.rs | 41 ---------------------------------- 4 files changed, 10 insertions(+), 45 deletions(-) diff --git a/src/analyzer.rs b/src/analyzer.rs index 2c273ff2..57315830 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -3748,7 +3748,7 @@ mod tests { lsp::Range::at(0, 0, 1, 0), ) .warning( - "`windows-shell` is deprecated, use `[windows] attribute on `set shell`` instead", + "`windows-shell` is deprecated, use `[windows]` attribute on `set shell` instead", lsp::Range::at(0, 4, 0, 17), ) .run(); @@ -3765,7 +3765,7 @@ mod tests { " }) .warning( - "`windows-shell` is deprecated, use `[windows] attribute on `set shell`` instead", + "`windows-shell` is deprecated, use `[windows]` attribute on `set shell` instead", lsp::Range::at(0, 4, 0, 17), ) .run(); diff --git a/src/builtins.rs b/src/builtins.rs index 5d6cb450..5e724bef 100644 --- a/src/builtins.rs +++ b/src/builtins.rs @@ -2786,7 +2786,7 @@ pub const BUILTINS: &[Builtin<'_>] = &[ ``` " }, - deprecated: Some("[windows] attribute on `set shell`"), + deprecated: Some("`[windows]` attribute on `set shell`"), }, Builtin::Setting { name: "working-directory", diff --git a/src/rule/deprecated_setting.rs b/src/rule/deprecated_setting.rs index e53808ae..83eee5c6 100644 --- a/src/rule/deprecated_setting.rs +++ b/src/rule/deprecated_setting.rs @@ -14,9 +14,15 @@ define_rule! { .. }) = context.builtin_setting(&setting.name.value) { + let replacement = if replacement.contains('`') { + replacement.to_string() + } else { + format!("`{replacement}`") + }; + diagnostics.push(Diagnostic::warning( format!( - "`{}` is deprecated, use `{replacement}` instead", + "`{}` is deprecated, use {replacement} instead", setting.name.value ), setting.name.range, diff --git a/src/server.rs b/src/server.rs index c184efa0..2aa116e8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3209,47 +3209,6 @@ mod tests { .await } - #[tokio::test] - async fn code_action_windows_shell_deprecated() -> Result { - Test::new() - .request(InitializeRequest { id: 1 }) - .response(InitializeResponse { id: 1 }) - .notification(DidOpenNotification { - uri: "file:///test.just", - text: "set windows-shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n", - }) - .request(CodeActionRequest { - id: 2, - uri: "file:///test.just", - range: lsp::Range::at(0, 4, 0, 4), - }) - .response(json!({ - "jsonrpc": "2.0", - "id": 2, - "result": [ - { - "title": "Replace `windows-shell` with `[windows] set shell`", - "kind": "quickfix", - "edit": { - "changes": { - "file:///test.just": [ - { - "range": { - "start": { "line": 0, "character": 0 }, - "end": { "line": 1, "character": 0 } - }, - "newText": "[windows]\nset shell := [\"powershell.exe\", \"-NoLogo\", \"-Command\"]\n" - } - ] - } - } - } - ] - })) - .run() - .await - } - #[tokio::test] async fn code_action_deprecated_function_outside_range() -> Result { Test::new() From ecdbc1bca3d98a4ca592563906c5920489ffd07a Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 11 Jul 2026 19:51:31 -0400 Subject: [PATCH 4/7] Tweak --- src/analyzer.rs | 18 +++++++++++++ src/builtin.rs | 6 ++--- src/builtins.rs | 16 +++++++----- src/deprecation.rs | 21 +++++++++++++++ src/lib.rs | 2 ++ src/quickfixer.rs | 9 +++++++ src/rule/deprecated_function.rs | 8 +++--- src/rule/deprecated_setting.rs | 46 ++++++++++++++++++++------------- src/setting.rs | 9 +++++++ 9 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 src/deprecation.rs diff --git a/src/analyzer.rs b/src/analyzer.rs index 57315830..615eea04 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -3771,6 +3771,24 @@ mod tests { .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(); + } + #[test] fn settings_string_type_correct() { Test::new(indoc! { 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 5e724bef..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,7 +2763,7 @@ 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", @@ -2786,7 +2787,10 @@ pub const BUILTINS: &[Builtin<'_>] = &[ ``` " }, - deprecated: Some("`[windows]` attribute on `set shell`"), + 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/quickfixer.rs b/src/quickfixer.rs index 3f88e7f7..20b2a1e9 100644 --- a/src/quickfixer.rs +++ b/src/quickfixer.rs @@ -245,6 +245,15 @@ mod tests { .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 83eee5c6..a24b3a75 100644 --- a/src/rule/deprecated_setting.rs +++ b/src/rule/deprecated_setting.rs @@ -10,19 +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) { - let replacement = if replacement.contains('`') { - replacement.to_string() - } else { - format!("`{replacement}`") - }; - diagnostics.push(Diagnostic::warning( format!( - "`{}` is deprecated, use {replacement} instead", + "`{}` is deprecated, use {deprecation} instead", setting.name.value ), setting.name.range, @@ -34,21 +28,37 @@ 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) { - let quickfix = match setting.name.value.as_str() { - "windows-shell" => Quickfix::setting_attribute( - setting, - context.document(), - "windows", - "shell", - ), - _ => 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(context.attributes(), 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..270ffbe3 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -9,6 +9,15 @@ pub struct Setting { } impl Setting { + pub fn has_attribute(&self, attributes: &[Attribute], name: &str) -> bool { + attributes.iter().any(|attribute| { + attribute.name.value == name + && attribute.target == Some(AttributeTarget::Setting) + && self.range.start <= attribute.range.start + && attribute.range.end <= self.range.end + }) + } + #[must_use] pub fn from_node(node: &Node, document: &Document) -> Option { let range = node.get_range(document); From 76fbe238073d5a6452dd6a8e9f26b7113a12506e Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 16 Jul 2026 20:42:25 -0400 Subject: [PATCH 5/7] Simplify has_attribute --- src/rule/deprecated_setting.rs | 3 +-- src/setting.rs | 12 +++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/rule/deprecated_setting.rs b/src/rule/deprecated_setting.rs index a24b3a75..d62bfa0b 100644 --- a/src/rule/deprecated_setting.rs +++ b/src/rule/deprecated_setting.rs @@ -46,8 +46,7 @@ define_rule! { } => { if context.settings().iter().any(|replacement_setting| { replacement_setting.name.value == replacement - && replacement_setting - .has_attribute(context.attributes(), attribute) + && replacement_setting.has_attribute(attribute) }) { continue; } diff --git a/src/setting.rs b/src/setting.rs index 270ffbe3..add4534f 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -9,13 +9,11 @@ pub struct Setting { } impl Setting { - pub fn has_attribute(&self, attributes: &[Attribute], name: &str) -> bool { - attributes.iter().any(|attribute| { - attribute.name.value == name - && attribute.target == Some(AttributeTarget::Setting) - && self.range.start <= attribute.range.start - && attribute.range.end <= self.range.end - }) + pub fn has_attribute(&self, name: &str) -> bool { + self + .attributes + .iter() + .any(|attribute| attribute.name.value == name) } #[must_use] From 692f2f8dad79cbdc081045ea1b1bedcb75eb5312 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 16 Jul 2026 21:58:19 -0400 Subject: [PATCH 6/7] Fix lint --- src/setting.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/setting.rs b/src/setting.rs index add4534f..48099f90 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -9,13 +9,6 @@ pub struct Setting { } impl Setting { - pub fn has_attribute(&self, name: &str) -> bool { - self - .attributes - .iter() - .any(|attribute| attribute.name.value == name) - } - #[must_use] pub fn from_node(node: &Node, document: &Document) -> Option { let range = node.get_range(document); @@ -101,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)] From f9497e14a65a9bc44d30fa68cd1b57242f65da0c Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 16 Jul 2026 22:01:31 -0400 Subject: [PATCH 7/7] Tweak --- src/quickfix.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/quickfix.rs b/src/quickfix.rs index c8fe75a8..76979824 100644 --- a/src/quickfix.rs +++ b/src/quickfix.rs @@ -45,6 +45,7 @@ impl Quickfix { .content .line(setting.range.start.line as usize) .to_string(); + let line = line.replacen(&setting.name.value, replacement, 1); Self {