Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions e2e/tasks/test_task_dep_args
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,28 @@ output=$(mise run step3 hello 2>&1)
assert_contains "echo \"$output\"" "step1 hello"
assert_contains "echo \"$output\"" "step2 hello"
assert_contains "echo \"$output\"" "step3 hello"

# Test Tera statement tags and boolean usage values in dependency templates
cat <<'EOF' >mise.toml
[tasks.noop]
run = 'echo "noop"'

[tasks."postlint:check"]
run = 'echo "postlint ran"'

[tasks.lint]
usage = 'flag "--run-post" default=#false'
depends_post = ['''
{%- if usage.run_post -%}
postlint:**
{%- else -%}
noop
{%- endif -%}
''']
run = 'echo "lint ran"'
EOF

assert_contains "mise run lint" "noop"
assert_not_contains "mise run lint" "postlint ran"
assert_contains "mise run lint --run-post" "postlint ran"
assert_not_contains "mise run lint --run-post" "noop"
91 changes: 73 additions & 18 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use eyre::{Result, bail, eyre};
use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use globset::GlobBuilder;
use heck::ToSnakeCase;
use indexmap::IndexMap;
use itertools::Itertools;
use petgraph::prelude::*;
Expand Down Expand Up @@ -1397,7 +1398,7 @@ impl Task {
pub async fn render_depends_with_usage(
&mut self,
config: &Arc<Config>,
usage_values: &IndexMap<String, String>,
usage_values: &IndexMap<String, tera::Value>,
) -> Result<()> {
if usage_values.is_empty() {
return Ok(());
Expand Down Expand Up @@ -2020,25 +2021,49 @@ where
}
}

/// Check if a TaskDep contains {{usage.*}} references that need deferred rendering.
/// Strips whitespace before matching to handle Tera's `{{ usage.foo }}` syntax.
/// Check if a TaskDep contains Tera `usage` references that need deferred rendering.
pub(crate) fn dep_has_usage_ref(dep: &TaskDep) -> bool {
let has_ref = |s: &str| {
let s = s.replace(' ', "");
s.contains("{{usage.")
};
has_ref(&dep.task)
|| dep.args.iter().any(|a| has_ref(a))
|| dep.env.values().any(|v| has_ref(v))
tera_template_has_usage_ref(&dep.task)
|| dep.args.iter().any(|a| tera_template_has_usage_ref(a))
|| dep.env.values().any(|v| tera_template_has_usage_ref(v))
}

/// Parse a task's usage spec against its current args and return a map
/// of named arg/flag values (e.g., {"app": "myapp", "verbose": "true"}).
fn tera_template_has_usage_ref(s: &str) -> bool {
const TAGS: [(&str, &str); 2] = [("{{", "}}"), ("{%", "%}")];
for (open, close) in TAGS {
let mut rest = s;
while let Some(start) = rest.find(open) {
rest = &rest[start + open.len()..];
let Some(end) = rest.find(close) else {
break;
};
if tera_tag_has_usage_ref(&rest[..end]) {
return true;
}
rest = &rest[end + close.len()..];
}
}
false
}

fn tera_tag_has_usage_ref(tag: &str) -> bool {
["usage.", "usage["].iter().any(|needle| {
tag.match_indices(needle).any(|(idx, _)| {
tag[..idx]
.chars()
.next_back()
.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '.')
})
})
}

/// Parse a task's usage spec against its current args and return a map of named
/// arg/flag values preserving their Tera types (e.g., strings and booleans).
/// Used to provide `{{usage.*}}` context when rendering dependency templates.
pub async fn parse_usage_values_from_task(
config: &Arc<Config>,
task: &Task,
) -> Result<IndexMap<String, String>> {
) -> Result<IndexMap<String, tera::Value>> {
let ts = config.get_toolset().await?;
let env = ts.full_env(config).await?;
let (spec, _) = task
Expand All @@ -2059,11 +2084,25 @@ pub async fn parse_usage_values_from_task(
}
};
let mut values = IndexMap::new();
for (k, v) in po.as_env() {
// Strip "usage_" prefix to get the bare arg/flag name
if let Some(name) = k.strip_prefix("usage_") {
values.insert(name.to_string(), v);
let to_tera_value = |val: &usage::parse::ParseValue| -> tera::Value {
use tera::Value;
use usage::parse::ParseValue::*;
match val {
MultiBool(v) => Value::Number(serde_json::Number::from(v.len())),
MultiString(v) => Value::Array(v.iter().map(|s| Value::String(s.clone())).collect()),
Bool(v) => Value::Bool(*v),
String(v) => Value::String(v.clone()),
}
};
for (arg, val) in &po.args {
values.insert(arg.name.to_snake_case(), to_tera_value(val));
}
for (flag, val) in &po.flags {
values.insert(flag.name.to_snake_case(), to_tera_value(val));
}
if !spec.cmd.subcommands.is_empty() {
let cmd = po.cmds.iter().skip(1).map(|c| c.name.clone()).join(" ");
values.insert("cmd".to_string(), tera::Value::String(cmd));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subcommand cmd value unreachable in some tasks

Low Severity

The new subcommand handling that inserts a cmd value is placed after the early-return that triggers when both spec.cmd.args and spec.cmd.flags are empty. For tasks defined only with subcommands (no top-level args/flags), the function returns early and never reaches the new insertion, leaving usage.cmd undefined in dependency template rendering and silently dropping any dep that references it.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 483d702. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated usage value conversion logic

Low Severity

The new to_tera_value closure and the args/flags-to-snake-case iteration in parse_usage_values_from_task duplicate the logic already implemented in TaskScriptParser::make_usage_ctx. Future changes to usage::parse::ParseValue variants or naming conventions will need to be made in two places, and the two implementations have already drifted slightly in how usage.cmd is exposed when no subcommand is selected.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 483d702. Configure here.

}
Ok(values)
}
Expand All @@ -2079,7 +2118,7 @@ mod tests {
use crate::{config::Config, dirs};
use pretty_assertions::assert_eq;

use super::{TaskConfirm, name_from_path};
use super::{TaskConfirm, name_from_path, tera_tag_has_usage_ref, tera_template_has_usage_ref};

// Thread-local storage to capture parser state during tests
thread_local! {
Expand All @@ -2096,6 +2135,22 @@ mod tests {
CAPTURED_PARSER_FIELDS.with(|captured| captured.lock().unwrap().take())
}

#[test]
fn test_tera_template_has_usage_ref() {
assert!(tera_template_has_usage_ref("{{ usage.app }}"));
assert!(tera_template_has_usage_ref(
"{%- if usage.run_post -%}post{%- endif -%}"
));
assert!(tera_template_has_usage_ref("{{ usage['app'] }}"));
assert!(!tera_template_has_usage_ref(
"{{ env.DEPLOY_ENV }} usage.docs"
));
assert!(!tera_template_has_usage_ref("{{ config.usage.something }}"));
assert!(!tera_template_has_usage_ref("{# usage.app #}"));
assert!(tera_tag_has_usage_ref("if(usage.run_post)"));
assert!(!tera_tag_has_usage_ref("ifusage.run_post"));
}

#[tokio::test]
async fn test_from_path() {
let test_cases = [(".mise/tasks/filetask", "filetask", vec!["ft"])];
Expand Down
Loading