Repository: sagiegurari/cargo-make (rust)

Issue:
merge together alias tasks in `--list-all-steps`
### Feature Description
<!-- A clear description of the feature request. -->


### Describe The Solution You'd Like
<!-- A clear and concise description of what you want to happen. -->

Change
```
Misc
----------
l - Description
logs - Description
```
To
```
Misc
----------
logs - Description [aliases: l]
```

Would that be better? 

### Code Sample

```rust
/// paste code here
```


Pull request:
feat: group aliases in --list-all-steps
close #862

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4335b173..33b79c51 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -176,6 +176,7 @@ dependencies = [
  "duckscript",
  "duckscriptsdk",
  "envmnt",
+ "expect-test",
  "fern",
  "fsio",
  "git_info",
@@ -369,6 +370,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "dissimilar"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "210ec60ae7d710bed8683e333e9d2855a8a56a3e9892b38bad3bb0d4d29b0d5e"
+
 [[package]]
 name = "duckscript"
 version = "0.7.5"
@@ -468,6 +475,16 @@ version = "10.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c6cffb2e9992a43637801ac84ca58bdb5acb2a426ab88a1e3d343b96cfaa845c"
 
+[[package]]
+name = "expect-test"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3"
+dependencies = [
+ "dissimilar",
+ "once_cell",
+]
+
 [[package]]
 name = "fastrand"
 version = "1.9.0"
@@ -1203,18 +1220,18 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.164"
+version = "1.0.163"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
+checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.164"
+version = "1.0.163"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
+checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index 51a77611..33a0a616 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -75,6 +75,9 @@ serde_json = "^1"
 shell2batch = "^0.4.5"
 toml = "^0.7"
 
+[dev-dependencies]
+expect-test = "^1"
+
 [target.'cfg(windows)'.dependencies]
 ansi_term = "^0.12.1"
 
diff --git a/examples/env.toml b/examples/env.toml
index e1b799bb..daaa6acc 100755
--- a/examples/env.toml
+++ b/examples/env.toml
@@ -53,7 +53,6 @@ IF_UNDEFINED = { value = "defined_in_makefile", condition = { env_not_set = [
 ] } }
 COMPOSITE_OF_MAPPED = "VALUE: ${IF_UNDEFINED}"
 PATH_GLOB = { glob = "./src/**/mod.rs", include_files = true, include_dirs = false, ignore_type = "git" }
-CURRENT_DATE = { script = ["date"] }
 
 [env.override-test]
 PREFER_EXISTING = { value = "new", condition = { env_not_set = [
@@ -104,7 +103,6 @@ echo CONDITIONED_VALUE: ${CONDITIONED_VALUE}
 echo CONDITIONAL_SCRIPT: ${CONDITIONAL_SCRIPT}
 echo ENV1_TEST: ${ENV1_TEST}
 echo new_env: ${new_env}
-echo CURRENT_DATE: ${CURRENT_DATE}
 '''
 
 [tasks.is_linux]
diff --git a/src/lib/cli_commands/list_steps.rs b/src/lib/cli_commands/list_steps.rs
index 9456e3f6..9099a1cc 100644
--- a/src/lib/cli_commands/list_steps.rs
+++ b/src/lib/cli_commands/list_steps.rs
@@ -12,6 +12,7 @@ use crate::execution_plan;
 use crate::io;
 use crate::types::{Config, DeprecationInfo};
 use std::collections::BTreeMap;
+use std::collections::BTreeSet;
 
 pub(crate) fn run(
     config: &Config,
@@ -19,8 +20,8 @@ pub(crate) fn run(
     output_file: &Option<String>,
     category: Option<String>,
     hide_uninteresting: bool,
-) -> u32 {
-    let (output, count) = create_list(&config, output_format, category, hide_uninteresting);
+) {
+    let output = create_list(&config, output_format, category, hide_uninteresting);
 
     match output_file {
         Some(file) => {
@@ -29,33 +30,29 @@ pub(crate) fn run(
         }
         None => print!("{}", output),
     }
-
-    count
 }
 
+/// Panics if task does not exist.
 pub(crate) fn create_list(
     config: &Config,
     output_format: &str,
     category_filter: Option<String>,
     hide_uninteresting: bool,
-) -> (String, u32) {
-    let mut count = 0;
-    let mut buffer = String::new();
-
-    let single_page_markdown = output_format == "markdown-single-page";
-    let markdown = single_page_markdown
-        || output_format == "markdown"
-        || output_format == "markdown-sub-section";
-    let just_task_name = output_format == "autocomplete";
-
-    let mut categories = BTreeMap::new();
-
-    if single_page_markdown {
-        buffer.push_str(&format!("# Task List\n\n"));
-    }
+) -> String {
+    // category -> actual_task -> description
+    let mut categories: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
+    // actual_task -> aliases
+    let mut aliases: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
 
+    // iterate over all tasks to build categories and aliases
     for key in config.tasks.keys() {
-        let task = execution_plan::get_normalized_task(&config, &key, true);
+        let actual_task_name =
+            execution_plan::get_actual_task_name(&config, &key).unwrap_or_else(|| {
+                error!("Task {} not found", &key);
+                panic!("Task {} not found", &key);
+            });
+
+        let task = execution_plan::get_normalized_task(&config, &actual_task_name, true);
 
         let is_private = match task.private {
             Some(private) => private,
@@ -87,7 +84,13 @@ pub(crate) fn create_list(
                 continue;
             }
 
-            count = count + 1;
+            if &actual_task_name != key {
+                aliases
+                    .entry(actual_task_name)
+                    .or_default()
+                    .insert(key.clone());
+                continue;
+            }
 
             let description = match task.description {
                 Some(value) => value,
@@ -117,17 +120,25 @@ pub(crate) fn create_list(
             let mut text = String::from(description);
             text.push_str(&deprecated_message);
 
-            let mut tasks_map = BTreeMap::new();
-            match categories.get_mut(&category) {
-                Some(value) => tasks_map.append(value),
-                _ => (),
-            };
-
-            tasks_map.insert(key.clone(), text.clone());
-            categories.insert(category, tasks_map);
+            categories
+                .entry(category)
+                .or_default()
+                .insert(key.clone(), text);
         }
     }
 
+    // build the task list output string
+    let single_page_markdown = output_format == "markdown-single-page";
+    let markdown = single_page_markdown
+        || output_format == "markdown"
+        || output_format == "markdown-sub-section";
+    let just_task_name = output_format == "autocomplete";
+
+    let mut buffer = String::new();
+    if single_page_markdown {
+        buffer.push_str(&format!("# Task List\n\n"));
+    }
+
     let post_key = if markdown { "**" } else { "" };
     for (category, tasks) in &categories {
         if category_filter
@@ -152,10 +163,22 @@ pub(crate) fn create_list(
                 buffer.push_str(&format!("* **"));
             }
 
+            let aliases = if let Some(aliases) = aliases.remove(key) {
+                format!(
+                    " [aliases: {}]",
+                    aliases.into_iter().collect::<Vec<String>>().join(", ")
+                )
+            } else {
+                "".to_string()
+            };
+
             if just_task_name {
-                buffer.push_str(&format!("{} ", &key));
+                buffer.push_str(&format!("{}{}", &key, aliases));
             } else {
-                buffer.push_str(&format!("{}{} - {}\n", &key, &post_key, &description));
+                buffer.push_str(&format!(
+                    "{}{} - {}{}\n",
+                    &key, &post_key, &description, aliases
+                ));
             }
         }
 
@@ -164,5 +187,5 @@ pub(crate) fn create_list(
         }
     }
 
-    (buffer, count)
+    buffer
 }
diff --git a/src/lib/execution_plan.rs b/src/lib/execution_plan.rs
index c9d66708..f11fb5fc 100644
--- a/src/lib/execution_plan.rs
+++ b/src/lib/execution_plan.rs
@@ -24,7 +24,10 @@ use std::env;
 use std::path::Path;
 use std::vec::Vec;
 
-/// Resolve aliases to different tasks, checking for cycles
+/// Resolve aliases recursively until a task without alias is found.
+///
+/// # Panics
+/// if there is a cycle in the alias chain.
 fn get_task_name_recursive(config: &Config, name: &str, seen: &mut Vec<String>) -> Option<String> {
     seen.push(name.to_string());
 
@@ -39,20 +42,27 @@ fn get_task_name_recursive(config: &Config, name: &str, seen: &mut Vec<String>)
                     panic!("Detected cycle while resolving alias {}: {}", &name, chain);
                 }
                 Some(ref alias) => get_task_name_recursive(config, alias, seen),
-                _ => Some(name.to_string()),
+                None => Some(name.to_string()),
             }
         }
         None => None,
     }
 }
 
-/// Returns the actual task name to invoke as tasks may have aliases
-fn get_task_name(config: &Config, name: &str) -> Option<String> {
+/// Returns the actual task name to invoke as tasks may have aliases.
+///
+/// # Panics
+/// if there is a cycle in the alias chain.
+pub(crate) fn get_actual_task_name(config: &Config, name: &str) -> Option<String> {
     let mut seen = Vec::new();
 
     get_task_name_recursive(config, name, &mut seen)
 }
 
+/// Resolves alias and normalizes task.
+///
+/// # Panics
+/// if task is not found or there is a cycle in the alias chain.
 pub(crate) fn get_normalized_task(config: &Config, name: &str, support_alias: bool) -> Task {
     match get_optional_normalized_task(config, name, support_alias) {
         Some(task) => task,
@@ -63,9 +73,13 @@ pub(crate) fn get_normalized_task(config: &Config, name: &str, support_alias: bo
     }
 }
 
+/// Resolves alias and normalizes task.
+///
+/// # Panics
+/// if there is a cycle in the alias chain.
 fn get_optional_normalized_task(config: &Config, name: &str, support_alias: bool) -> Option<Task> {
     let actual_task_name_option = if support_alias {
-        get_task_name(config, name)
+        get_actual_task_name(config, name)
     } else {
         Some(name.to_string())
     };


Test patch:
diff --git a/src/lib/cli_commands/list_steps_test.rs b/src/lib/cli_commands/list_steps_test.rs
index d164e0bc..58815651 100644
--- a/src/lib/cli_commands/list_steps_test.rs
+++ b/src/lib/cli_commands/list_steps_test.rs
@@ -2,9 +2,43 @@ use super::*;
 
 use crate::io;
 use crate::types::{ConfigSection, EnvValue, Task};
+use expect_test::{expect, Expect};
 use indexmap::IndexMap;
 use std::path::PathBuf;
 
+fn check(
+    config: &Config,
+    output_format: &str,
+    output_file: &Option<String>,
+    category: Option<String>,
+    hide_uninteresting: bool,
+    expect: Expect,
+) {
+    match output_file {
+        Some(file) => {
+            run(
+                &config,
+                output_format,
+                output_file,
+                category,
+                hide_uninteresting,
+            );
+
+            let mut path = PathBuf::new();
+            path.push(&file);
+
+            let actual = io::read_text_file(&path);
+            io::delete_file(&file);
+
+            expect.assert_eq(&actual);
+        }
+        None => {
+            let actual = create_list(&config, output_format, category, hide_uninteresting);
+            expect.assert_eq(&actual);
+        }
+    }
+}
+
 #[test]
 fn run_empty() {
     let config_section = ConfigSection::new();
@@ -19,9 +53,7 @@ fn run_empty() {
         plugins: None,
     };
 
-    let count = run(&config, "default", &None, None, false);
-
-    assert_eq!(count, 0);
+    check(&config, "default", &None, None, false, expect![[""]]);
 }
 
 #[test]
@@ -46,9 +78,20 @@ fn run_all_public() {
         plugins: None,
     };
 
-    let count = run(&config, "default", &None, None, false);
+    check(
+        &config,
+        "default",
+        &None,
+        None,
+        false,
+        expect![[r#"
+        No Category
+        ----------
+        post-2 - 2
+        pre-1 - 1
 
-    assert_eq!(count, 2);
+    "#]],
+    );
 }
 
 #[test]
@@ -76,9 +119,66 @@ fn run_all_public_hide_uninteresting() {
         plugins: None,
     };
 
-    let count = run(&config, "default", &None, None, true);
+    check(
+        &config,
+        "default",
+        &None,
+        None,
+        true,
+        expect![[r#"
+        No Category
+        ----------
+        3 - 3
 
-    assert_eq!(count, 1);
+    "#]],
+    );
+}
+
+#[test]
+fn run_aliases() {
+    let config_section = ConfigSection::new();
+    let env = IndexMap::<String, EnvValue>::new();
+
+    let mut tasks = IndexMap::<String, Task>::new();
+    let mut task1 = Task::new();
+    task1.description = Some("1".to_string());
+    tasks.insert("1".to_string(), task1);
+    let mut task2 = Task::new();
+    task2.description = Some("2".to_string());
+    tasks.insert("2".to_string(), task2);
+    let mut task3 = Task::new();
+    // 4->3->1
+    task3.description = Some("3".to_string());
+    task3.alias = Some("1".to_string());
+    tasks.insert("3".to_string(), task3);
+    let mut task4 = Task::new();
+    task4.description = Some("4".to_string());
+    task4.alias = Some("3".to_string());
+    tasks.insert("4".to_string(), task4);
+
+    let config = Config {
+        config: config_section,
+        env_files: vec![],
+        env,
+        env_scripts: vec![],
+        tasks,
+        plugins: None,
+    };
+
+    check(
+        &config,
+        "default",
+        &None,
+        None,
+        false,
+        expect![[r#"
+            No Category
+            ----------
+            1 - 1 [aliases: 3, 4]
+            2 - 2
+
+        "#]],
+    );
 }
 
 #[test]
@@ -103,9 +203,20 @@ fn run_all_public_markdown() {
         plugins: None,
     };
 
-    let count = run(&config, "markdown", &None, None, false);
+    check(
+        &config,
+        "markdown",
+        &None,
+        None,
+        false,
+        expect![[r#"
+        #### No Category
+
+        * **1** - 1
+        * **2** - 2
 
-    assert_eq!(count, 2);
+    "#]],
+    );
 }
 
 #[test]
@@ -130,9 +241,20 @@ fn run_all_public_markdown_sub_section() {
         plugins: None,
     };
 
-    let count = run(&config, "markdown-sub-section", &None, None, false);
+    check(
+        &config,
+        "markdown-sub-section",
+        &None,
+        None,
+        false,
+        expect![[r#"
+            #### No Category
+
+            * **1** - 1
+            * **2** - 2
 
-    assert_eq!(count, 2);
+        "#]],
+    );
 }
 
 #[test]
@@ -157,9 +279,22 @@ fn run_all_public_markdown_single_page() {
         plugins: None,
     };
 
-    let count = run(&config, "markdown-single-page", &None, None, false);
+    check(
+        &config,
+        "markdown-single-page",
+        &None,
+        None,
+        false,
+        expect![[r#"
+            # Task List
+
+            ## No Category
 
-    assert_eq!(count, 2);
+            * **1** - 1
+            * **2** - 2
+
+        "#]],
+    );
 }
 
 #[test]
@@ -186,9 +321,7 @@ fn run_all_private() {
         plugins: None,
     };
 
-    let count = run(&config, "default", &None, None, false);
-
-    assert_eq!(count, 0);
+    check(&config, "default", &None, None, false, expect![[""]]);
 }
 
 #[test]
@@ -222,9 +355,21 @@ fn run_mixed() {
         plugins: None,
     };
 
-    let count = run(&config, "default", &None, None, false);
-
-    assert_eq!(count, 3);
+    check(
+        &config,
+        "default",
+        &None,
+        None,
+        false,
+        expect![[r#"
+        No Category
+        ----------
+        2 - 2
+        3 - 3 (deprecated)
+        4 - 4 (deprecated - test)
+
+    "#]],
+    );
 }
 
 #[test]
@@ -250,23 +395,22 @@ fn run_write_to_file() {
     };
 
     let file = "./target/_temp/tasklist.md";
-    let count = run(
+    check(
         &config,
         "markdown-single-page",
         &Some(file.to_string()),
         None,
         false,
-    );
-
-    assert_eq!(count, 2);
+        expect![[r#"
+            # Task List
 
-    let mut path = PathBuf::new();
-    path.push(&file);
+            ## No Category
 
-    let text = io::read_text_file(&path);
-    io::delete_file(&file);
+            * **1** - 1
+            * **2** - 2
 
-    assert!(text.contains("# Task List"));
+        "#]],
+    );
 }
 
 #[test]
@@ -297,13 +441,18 @@ fn run_category_public() {
         plugins: None,
     };
 
-    let count = run(
+    check(
         &config,
         "default",
         &None,
         Some("TestCategory1".to_owned()),
         false,
-    );
+        expect![[r#"
+            TestCategory1
+            ----------
+            1 - 1
+            2 - 2
 
-    assert_eq!(count, 2);
+        "#]],
+    );
 }
diff --git a/src/lib/execution_plan_test.rs b/src/lib/execution_plan_test.rs
index 4343d253..745f5df2 100644
--- a/src/lib/execution_plan_test.rs
+++ b/src/lib/execution_plan_test.rs
@@ -8,7 +8,7 @@ use indexmap::IndexMap;
 use std::env;
 
 #[test]
-fn get_task_name_not_found() {
+fn get_actual_task_name_not_found() {
     let config = Config {
         config: ConfigSection::new(),
         env_files: vec![],
@@ -18,13 +18,13 @@ fn get_task_name_not_found() {
         plugins: None,
     };
 
-    let name = get_task_name(&config, "test");
+    let name = get_actual_task_name(&config, "test");
 
     assert!(name.is_none());
 }
 
 #[test]
-fn get_task_name_no_alias() {
+fn get_actual_task_name_no_alias() {
     let mut config = Config {
         config: ConfigSection::new(),
         env_files: vec![],
@@ -36,13 +36,13 @@ fn get_task_name_no_alias() {
 
     config.tasks.insert("test".to_string(), Task::new());
 
-    let name = get_task_name(&config, "test");
+    let name = get_actual_task_name(&config, "test");
 
     assert_eq!(name.unwrap(), "test");
 }
 
 #[test]
-fn get_task_name_alias() {
+fn get_actual_task_name_alias() {
     let mut config = Config {
         config: ConfigSection::new(),
         env_files: vec![],
@@ -58,14 +58,14 @@ fn get_task_name_alias() {
 
     config.tasks.insert("test2".to_string(), Task::new());
 
-    let name = get_task_name(&config, "test");
+    let name = get_actual_task_name(&config, "test");
 
     assert_eq!(name.unwrap(), "test2");
 }
 
 #[test]
 #[should_panic]
-fn get_task_name_alias_self_referential() {
+fn get_actual_task_name_alias_self_referential() {
     let mut config = Config {
         config: ConfigSection::new(),
         env_files: vec![],
@@ -79,12 +79,12 @@ fn get_task_name_alias_self_referential() {
     task.alias = Some("rec".to_string());
     config.tasks.insert("rec".to_string(), task);
 
-    get_task_name(&config, "rec");
+    get_actual_task_name(&config, "rec");
 }
 
 #[test]
 #[should_panic]
-fn get_task_name_alias_circular() {
+fn get_actual_task_name_alias_circular() {
     let mut config = Config {
         config: ConfigSection::new(),
         env_files: vec![],
@@ -103,11 +103,11 @@ fn get_task_name_alias_circular() {
     config.tasks.insert("rec-mut-a".to_string(), task_a);
     config.tasks.insert("rec-mut-b".to_string(), task_b);
 
-    get_task_name(&config, "rec-mut-a");
+    get_actual_task_name(&config, "rec-mut-a");
 }
 
 #[test]
-fn get_task_name_platform_alias() {
+fn get_actual_task_name_platform_alias() {
     let mut config = Config {
         config: ConfigSection::new(),
         env_files: vec![],
@@ -130,7 +130,7 @@ fn get_task_name_platform_alias() {
 
     config.tasks.insert("test2".to_string(), Task::new());
 
-    let name = get_task_name(&config, "test");
+    let name = get_actual_task_name(&config, "test");
 
     assert_eq!(name.unwrap(), "test2");
 }