Repository: denoland/deno
Pull request: fix(task): properly handle task name wildcards with --recursive

This commit fixes `deno task` by checking if the provided
task name actually has a wildcard char ("*").

Previously, if the "--recursive" flag was passed, the task name
was treated as a regex, which lead to a situation where exact task
name resulted in a regex that matched all tasks with the specific prefix.

This commit fixes it, by checking if the provided task name, is an exact
name, or is it a wildcard match.

Closes https://github.com/denoland/deno/issues/27370
Closes https://github.com/denoland/deno/issues/27401
Closes https://github.com/denoland/deno/issues/27408

Changed files:
filename: cli/tools/task.rs
patch: @@ -94,7 +94,7 @@ pub async fn execute_script(
       return Ok(0);
     };
 
-    let task_regex = arg_to_regex(task_name)?;
+    let task_name_filter = arg_to_task_name_filter(task_name)?;
     let mut packages_task_info: Vec<PackageTaskInfo> = vec![];
 
     for folder in workspace.config_folders() {
@@ -137,12 +137,20 @@ pub async fn execute_script(
 
       // Match tasks in deno.json
       for name in tasks_config.task_names() {
-        if task_regex.is_match(name) && !visited.contains(name) {
+        let matches_filter = match &task_name_filter {
+          TaskNameFilter::Exact(n) => *n == name,
+          TaskNameFilter::Regex(re) => re.is_match(name),
+        };
+        if matches_filter && !visited.contains(name) {
           matched.insert(name.to_string());
           visit_task(&tasks_config, &mut visited, name);
         }
       }
 
+      if matched.is_empty() {
+        continue;
+      }
+
       packages_task_info.push(PackageTaskInfo {
         matched_tasks: matched
           .iter()
@@ -902,3 +910,41 @@ fn strip_ansi_codes_and_escape_control_chars(s: &str) -> String {
     })
     .collect()
 }
+
+fn arg_to_task_name_filter(input: &str) -> Result<TaskNameFilter, AnyError> {
+  if !input.contains("*") {
+    return Ok(TaskNameFilter::Exact(input));
+  }
+
+  let mut regex_str = regex::escape(input);
+  regex_str = regex_str.replace("\\*", ".*");
+  let re = Regex::new(&regex_str)?;
+  Ok(TaskNameFilter::Regex(re))
+}
+
+#[derive(Debug)]
+enum TaskNameFilter<'s> {
+  Exact(&'s str),
+  Regex(regex::Regex),
+}
+
+#[cfg(test)]
+mod tests {
+  use super::*;
+
+  #[test]
+  fn test_arg_to_task_name_filter() {
+    assert!(matches!(
+      arg_to_task_name_filter("test").unwrap(),
+      TaskNameFilter::Exact("test")
+    ));
+    assert!(matches!(
+      arg_to_task_name_filter("test-").unwrap(),
+      TaskNameFilter::Exact("test-")
+    ));
+    assert!(matches!(
+      arg_to_task_name_filter("test*").unwrap(),
+      TaskNameFilter::Regex(_)
+    ));
+  }
+}

filename: tests/specs/task/workspace_regex_match/__test__.jsonc
patch: @@ -0,0 +1,11 @@
+{
+  "tempDir": true,
+  "tests": {
+    // Regression test for https://github.com/denoland/deno/issues/27370
+    "root": {
+      "args": "task test-all",
+      "output": "root.out",
+      "exitCode": 0
+    }
+  }
+}

filename: tests/specs/task/workspace_regex_match/deno.json
patch: @@ -0,0 +1,6 @@
+{
+  "workspace": ["./subdir"],
+  "tasks": {
+    "test-all": "deno task --recursive test"
+  }
+}

filename: tests/specs/task/workspace_regex_match/root.out
patch: @@ -0,0 +1,3 @@
+Task test-all deno task --recursive test
+Task test echo 'ok'
+ok

filename: tests/specs/task/workspace_regex_match/subdir/deno.json
patch: @@ -0,0 +1,5 @@
+{
+  "tasks": {
+    "test": "echo 'ok'"
+  }
+}