Repository: casey/just (rust)

Issue:
`just --unstable --list --unsorted` has weird behavior with includes
The code in https://github.com/casey/just/blob/master/src/justfile.rs#L367-L370 that determines what order recipes will be listed in when not sorted alphabetically depends on the offset of a recipe from the beginning of its file. 

E.g. if you have the justfiles:

`justfile`:
```
# newlines


default:
    echo "DEFAULT"

!include ./include.justfile
```

`include.justfile`:
```
alpha:
    echo "ALPHA"

beta:
    echo "BETA"
```

Then the output of `just --unstable --list --unsorted` on latest master is:
```
Available recipes:
    alpha
    default
    beta
```

instead of the `default - alpha - beta` order I'd expect.

https://github.com/casey/just/pull/1739 has a solution for this (albeit not a working test), but it also computes the default recipe in a way different than what https://github.com/casey/just/pull/1740 decided on, so I'm not sure how much of the code from 1739 is useful.

Pull request description:
List included recipes in load order
Fixes #1742.

Patch:
diff --git a/src/analyzer.rs b/src/analyzer.rs
index e4cef06c..b5ae8727 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -9,15 +9,17 @@ pub(crate) struct Analyzer<'src> {
 
 impl<'src> Analyzer<'src> {
   pub(crate) fn analyze(
+    loaded: Vec<PathBuf>,
     paths: &HashMap<PathBuf, PathBuf>,
     asts: &HashMap<PathBuf, Ast<'src>>,
     root: &Path,
   ) -> CompileResult<'src, Justfile<'src>> {
-    Analyzer::default().justfile(paths, asts, root)
+    Analyzer::default().justfile(loaded, paths, asts, root)
   }
 
   fn justfile(
     mut self,
+    loaded: Vec<PathBuf>,
     paths: &HashMap<PathBuf, PathBuf>,
     asts: &HashMap<PathBuf, Ast<'src>>,
     root: &Path,
@@ -101,6 +103,7 @@ impl<'src> Analyzer<'src> {
         }),
       aliases,
       assignments: self.assignments,
+      loaded,
       recipes,
       settings,
       warnings,
diff --git a/src/compiler.rs b/src/compiler.rs
index 070748e0..59d981e2 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -11,12 +11,14 @@ impl Compiler {
     let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
     let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
     let mut srcs: HashMap<PathBuf, &str> = HashMap::new();
+    let mut loaded = Vec::new();
 
     let mut stack: Vec<PathBuf> = Vec::new();
     stack.push(root.into());
 
     while let Some(current) = stack.pop() {
       let (relative, src) = loader.load(root, &current)?;
+      loaded.push(relative.into());
       let tokens = Lexer::lex(relative, src)?;
       let mut ast = Parser::parse(&tokens)?;
 
@@ -42,7 +44,7 @@ impl Compiler {
       asts.insert(current.clone(), ast.clone());
     }
 
-    let justfile = Analyzer::analyze(&paths, &asts, root)?;
+    let justfile = Analyzer::analyze(loaded, &paths, &asts, root)?;
 
     Ok(Compilation {
       asts,
@@ -61,7 +63,7 @@ impl Compiler {
     asts.insert(root.clone(), ast);
     let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
     paths.insert(root.clone(), root.clone());
-    Analyzer::analyze(&paths, &asts, &root)
+    Analyzer::analyze(Vec::new(), &paths, &asts, &root)
   }
 }
 
diff --git a/src/justfile.rs b/src/justfile.rs
index b08d4319..45b327d5 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -6,6 +6,8 @@ pub(crate) struct Justfile<'src> {
   pub(crate) assignments: Table<'src, Assignment<'src>>,
   #[serde(rename = "first", serialize_with = "keyed::serialize_option")]
   pub(crate) default: Option<Rc<Recipe<'src>>>,
+  #[serde(skip)]
+  pub(crate) loaded: Vec<PathBuf>,
   pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
   pub(crate) settings: Settings<'src>,
   pub(crate) warnings: Vec<Warning>,
@@ -365,7 +367,16 @@ impl<'src> Justfile<'src> {
       .collect::<Vec<&Recipe<Dependency>>>();
 
     if source_order {
-      recipes.sort_by_key(|recipe| recipe.name.offset);
+      recipes.sort_by_key(|recipe| {
+        (
+          self
+            .loaded
+            .iter()
+            .position(|path| path == recipe.name.path)
+            .unwrap(),
+          recipe.name.offset,
+        )
+      });
     }
 
     recipes
diff --git a/src/lib.rs b/src/lib.rs
index bd4b8335..fa88edee 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@
   clippy::enum_glob_use,
   clippy::let_underscore_untyped,
   clippy::needless_pass_by_value,
+  clippy::similar_names,
   clippy::too_many_lines,
   clippy::unnecessary_wraps,
   clippy::wildcard_imports


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index b0f6c4bf..5d1cf746 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -68,7 +68,7 @@ pub(crate) fn analysis_error(
   let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
   paths.insert("justfile".into(), "justfile".into());
 
-  match Analyzer::analyze(&paths, &asts, &root) {
+  match Analyzer::analyze(Vec::new(), &paths, &asts, &root) {
     Ok(_) => panic!("Analysis unexpectedly succeeded"),
     Err(have) => {
       let want = CompileError {
diff --git a/tests/includes.rs b/tests/includes.rs
index af2e7b66..0bc615c0 100644
--- a/tests/includes.rs
+++ b/tests/includes.rs
@@ -120,3 +120,25 @@ fn include_recipes_are_not_default() {
     .stderr("error: Justfile contains no default recipe.\n")
     .run();
 }
+
+#[test]
+fn listed_recipes_in_includes_are_in_load_order() {
+  Test::new()
+    .justfile(
+      "
+      !include ./include.justfile
+      foo:
+    ",
+    )
+    .write("include.justfile", "bar:")
+    .args(["--list", "--unstable", "--unsorted"])
+    .test_round_trip(false)
+    .stdout(
+      "
+      Available recipes:
+          foo
+          bar
+    ",
+    )
+    .run();
+}