From 30246f4ac04fa3bdcff056ef0edcac65ed76dc38 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:43:09 +1000 Subject: [PATCH 1/2] feat(task): skip dependencies with empty templated names --- e2e/tasks/test_task_dep_args | 55 ++++++++++++++++++++++++++++++++++++ src/task/mod.rs | 48 +++++++++++++++---------------- src/task/task_dep.rs | 47 ++++++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 26 deletions(-) diff --git a/e2e/tasks/test_task_dep_args b/e2e/tasks/test_task_dep_args index c557e36476c..8b5355d2d8a 100644 --- a/e2e/tasks/test_task_dep_args +++ b/e2e/tasks/test_task_dep_args @@ -46,6 +46,61 @@ EOF assert_contains "mise run lint" "fix=false" assert_contains "mise run lint --fix" "fix=true" +# Skip dependency entries whose task name template renders empty, both during +# initial rendering and deferred usage rendering. +cat <<'EOF' >mise.toml +[tasks.initial-dependency] +run = 'echo "initial dependency ran"' + +[tasks.usage-dependency] +run = 'echo "usage dependency ran"' + +[tasks.usage-post-dependency] +run = 'echo "usage post-dependency ran"' + +[tasks.usage-wait-task] +run = 'sleep 0.3; echo wait >> wait-order.txt; echo "usage wait task ran"' + +[tasks.optional] +usage = 'flag "--enabled" default=#false' +depends = [ + "{% if false %}initial-dependency{% endif %}", + "{% if true %}initial-dependency{% endif %}", + "{% if usage.enabled %}usage-dependency{% endif %}", +] +depends_post = [ + "{% if false %}initial-post-dependency{% endif %}", + "{% if usage.enabled %}usage-post-dependency{% endif %}", +] +wait_for = [ + "{% if false %}initial-wait-task{% endif %}", + "{% if usage.enabled %}usage-wait-task{% endif %}", +] +run = 'echo optional >> wait-order.txt; echo "optional ran"' +EOF + +assert_contains "mise run optional" "optional ran" +assert_contains "mise run optional" "initial dependency ran" +output=$(mise run optional --enabled 2>&1) +assert_contains_text "$output" "usage dependency ran" +assert_contains_text "$output" "usage post-dependency ran" +assert_contains_text "$output" "optional ran" + +rm -f wait-order.txt +mise run optional --enabled ::: usage-wait-task +assert "cat wait-order.txt" "wait +optional" + +# Literal empty task names remain invalid rather than being treated as an +# optional templated dependency. +cat <<'EOF' >mise.toml +[tasks.invalid] +depends = [""] +run = 'echo "should not run"' +EOF + +assert_fail "mise run invalid" "task not found:" + # Test with multiple args cat <<'EOF' >mise.toml [tasks.start-backend] diff --git a/src/task/mod.rs b/src/task/mod.rs index 3417a1b67ff..a5d92f7e46e 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -1905,21 +1905,9 @@ impl Task { // Render deps that don't contain {{usage.*}} references. Deps with usage // references are deferred until render_depends_with_usage() is called with // the actual arg values from CLI or parent dependency. - for d in &mut self.depends { - if !dep_has_usage_ref(d) { - d.render(&mut tera, &tera_ctx)?; - } - } - for d in &mut self.depends_post { - if !dep_has_usage_ref(d) { - d.render(&mut tera, &tera_ctx)?; - } - } - for d in &mut self.wait_for { - if !dep_has_usage_ref(d) { - d.render(&mut tera, &tera_ctx)?; - } - } + render_task_deps(&mut self.depends, &mut tera, &tera_ctx, true)?; + render_task_deps(&mut self.depends_post, &mut tera, &tera_ctx, true)?; + render_task_deps(&mut self.wait_for, &mut tera, &tera_ctx, true)?; if let Some(dir) = &mut self.dir && contains_template_syntax(dir) { @@ -1996,25 +1984,19 @@ impl Task { && let Some(raw) = &self.depends_raw { self.depends = raw.clone(); - for d in &mut self.depends { - d.render(&mut tera, &tera_ctx)?; - } + render_task_deps(&mut self.depends, &mut tera, &tera_ctx, false)?; } if !self.depends_post.is_empty() && let Some(raw) = &self.depends_post_raw { self.depends_post = raw.clone(); - for d in &mut self.depends_post { - d.render(&mut tera, &tera_ctx)?; - } + render_task_deps(&mut self.depends_post, &mut tera, &tera_ctx, false)?; } if !self.wait_for.is_empty() && let Some(raw) = &self.wait_for_raw { self.wait_for = raw.clone(); - for d in &mut self.wait_for { - d.render(&mut tera, &tera_ctx)?; - } + render_task_deps(&mut self.wait_for, &mut tera, &tera_ctx, false)?; } Ok(()) } @@ -2683,6 +2665,24 @@ pub(crate) fn dep_has_usage_ref(dep: &TaskDep) -> bool { || dep.env.values().any(|v| tera_template_has_usage_ref(v)) } +fn render_task_deps( + deps: &mut Vec, + tera: &mut TeraEngine, + tera_ctx: &tera::Context, + defer_usage: bool, +) -> Result<()> { + let mut rendered = Vec::with_capacity(deps.len()); + for mut dep in std::mem::take(deps) { + if defer_usage && dep_has_usage_ref(&dep) { + rendered.push(dep); + } else if dep.render(tera, tera_ctx)? { + rendered.push(dep); + } + } + *deps = rendered; + Ok(()) +} + fn tera_template_has_usage_ref(s: &str) -> bool { const TAGS: [(&str, &str); 2] = [("{{", "}}"), ("{%", "%}")]; for (open, close) in TAGS { diff --git a/src/task/task_dep.rs b/src/task/task_dep.rs index 98ae3775d65..e21b626dc72 100644 --- a/src/task/task_dep.rs +++ b/src/task/task_dep.rs @@ -20,9 +20,12 @@ impl TaskDep { &mut self, tera: &mut TeraEngine, tera_ctx: &tera::Context, - ) -> crate::Result<&mut Self> { + ) -> crate::Result { if contains_template_syntax(&self.task) { self.task = render_str(tera, &self.task, tera_ctx)?; + if self.task.trim().is_empty() { + return Ok(false); + } } let mut rendered_args = Vec::with_capacity(self.args.len()); for a in &self.args { @@ -43,7 +46,7 @@ impl TaskDep { } } self.parse_shell_style_env()?; - Ok(self) + Ok(true) } pub fn parse_shell_style_env(&mut self) -> crate::Result<&mut Self> { @@ -372,6 +375,46 @@ mod tests { assert_eq!(td.args, vec!["--fix"]); } + #[test] + fn test_task_dep_render_skips_empty_templated_task() { + let mut td: TaskDep = "{% if false %}lint{% endif %}".parse().unwrap(); + let mut tera = TeraEngine::V2(Box::default()); + let ctx = tera::Context::new(); + + assert!(!td.render(&mut tera, &ctx).unwrap()); + assert!(td.task.is_empty()); + } + + #[test] + fn test_task_dep_render_skips_whitespace_only_templated_task() { + let mut td: TaskDep = "\n{% if false %}lint{% endif %}\n".parse().unwrap(); + let mut tera = TeraEngine::V2(Box::default()); + let ctx = tera::Context::new(); + + assert!(!td.render(&mut tera, &ctx).unwrap()); + assert_eq!(td.task, "\n\n"); + } + + #[test] + fn test_task_dep_render_keeps_non_empty_templated_task() { + let mut td: TaskDep = "{% if true %}lint{% endif %}".parse().unwrap(); + let mut tera = TeraEngine::V2(Box::default()); + let ctx = tera::Context::new(); + + assert!(td.render(&mut tera, &ctx).unwrap()); + assert_eq!(td.task, "lint"); + } + + #[test] + fn test_task_dep_render_keeps_literal_empty_task() { + let mut td: TaskDep = "".parse().unwrap(); + let mut tera = TeraEngine::V2(Box::default()); + let ctx = tera::Context::new(); + + assert!(td.render(&mut tera, &ctx).unwrap()); + assert!(td.task.is_empty()); + } + #[test] fn test_task_dep_single_token_with_equals() { // Single token like "build=release" should be treated as task name, not env var From e15799d848cfc91b215901944cba35f21c745a16 Mon Sep 17 00:00:00 2001 From: Taku Kodma <79110363+risu729@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:30:02 +1000 Subject: [PATCH 2/2] fix(task): satisfy clippy in dependency rendering --- src/task/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/task/mod.rs b/src/task/mod.rs index a5d92f7e46e..9b863f70e0b 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -2673,9 +2673,7 @@ fn render_task_deps( ) -> Result<()> { let mut rendered = Vec::with_capacity(deps.len()); for mut dep in std::mem::take(deps) { - if defer_usage && dep_has_usage_ref(&dep) { - rendered.push(dep); - } else if dep.render(tera, tera_ctx)? { + if (defer_usage && dep_has_usage_ref(&dep)) || dep.render(tera, tera_ctx)? { rendered.push(dep); } }