Repository: casey/just (rust)

Issue:
1.42.0 breaks existing justfiles around modules with imports
Given the following justfiles, `just a::foo` and `just b::foo` both print `A`, when `just b::foo` should print `B`.

```just
# justfile
mod a
mod b
```

```just
# a.just
X := 'A'
import 'shared.just'
```

```just
# b.just
X := 'B'
import 'shared.just'
```

```just
# shared.just
foo:
  @echo {{ X }}
```


Patch:
diff --git a/src/alias.rs b/src/alias.rs
index 45d38acc1a..fbeaf769b7 100644
--- a/src/alias.rs
+++ b/src/alias.rs
@@ -14,7 +14,7 @@ pub(crate) struct Alias<'src, T = Arc<Recipe<'src>>> {
 
 impl<'src> Alias<'src, Namepath<'src>> {
   pub(crate) fn resolve(self, target: Arc<Recipe<'src>>) -> Alias<'src> {
-    assert!(self.target.last().lexeme() == target.namepath.last().lexeme());
+    assert!(self.target.last().lexeme() == target.name());
 
     Alias {
       attributes: self.attributes,
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 7a63e45186..81d61e51c4 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -147,6 +147,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
 
     let recipes = RecipeResolver::resolve_recipes(
       &assignments,
+      &ast.module_path,
       &self.modules,
       &settings,
       deduplicated_recipes,
diff --git a/src/compiler.rs b/src/compiler.rs
index ea0683db8a..14083a001c 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -16,8 +16,13 @@ impl Compiler {
     stack.push(Source::root(root));
 
     while let Some(current) = stack.pop() {
+      if paths.contains_key(&current.path) {
+        continue;
+      }
+
       let (relative, src) = loader.load(root, &current.path)?;
       loaded.push(relative.into());
+
       let tokens = Lexer::lex(relative, src)?;
       let mut ast = Parser::parse(
         current.file_depth,
diff --git a/src/justfile.rs b/src/justfile.rs
index bb113d6262..bf4623de84 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -303,12 +303,12 @@ impl<'src> Justfile<'src> {
     config: &Config,
     dotenv: &BTreeMap<String, String>,
     is_dependency: bool,
-    ran: &Ran<'src>,
+    ran: &Ran,
     recipe: &Recipe<'src>,
     scopes: &BTreeMap<String, (&Justfile<'src>, &Scope<'src, '_>)>,
     search: &Search,
   ) -> RunResult<'src> {
-    let mutex = ran.mutex(&recipe.namepath, arguments);
+    let mutex = ran.mutex(recipe, arguments);
 
     let mut guard = mutex.lock().unwrap();
 
@@ -323,7 +323,7 @@ impl<'src> Justfile<'src> {
     }
 
     let (module, scope) = scopes
-      .get(&recipe.module_path())
+      .get(recipe.module_path())
       .expect("failed to retrieve scope for module");
 
     let context = ExecutionContext {
@@ -382,7 +382,7 @@ impl<'src> Justfile<'src> {
     dependencies: &[Dependency<'src>],
     dotenv: &BTreeMap<String, String>,
     evaluator: &mut Evaluator<'src, 'run>,
-    ran: &Ran<'src>,
+    ran: &Ran,
     recipe: &Recipe<'src>,
     scopes: &BTreeMap<String, (&Justfile<'src>, &Scope<'src, 'run>)>,
     search: &Search,
diff --git a/src/namepath.rs b/src/namepath.rs
index 66db701782..8860c8e113 100644
--- a/src/namepath.rs
+++ b/src/namepath.rs
@@ -8,13 +8,6 @@ impl<'src> Namepath<'src> {
     Self(self.0.iter().copied().chain(iter::once(name)).collect())
   }
 
-  pub(crate) fn spaced(&self) -> ModulePath {
-    ModulePath {
-      path: self.0.iter().map(|name| name.lexeme().into()).collect(),
-      spaced: true,
-    }
-  }
-
   pub(crate) fn push(&mut self, name: Name<'src>) {
     self.0.push(name);
   }
diff --git a/src/parser.rs b/src/parser.rs
index 07d3a947df..6ddd161c2e 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -1013,9 +1013,7 @@ impl<'run, 'src> Parser<'run, 'src> {
       file_depth: self.file_depth,
       import_offsets: self.import_offsets.clone(),
       name,
-      namepath: self
-        .module_namepath
-        .map_or_else(|| name.into(), |module_namepath| module_namepath.join(name)),
+      namepath: None,
       parameters: positional.into_iter().chain(variadic).collect(),
       priors,
       private,
diff --git a/src/ran.rs b/src/ran.rs
index 8854046bee..c884402f86 100644
--- a/src/ran.rs
+++ b/src/ran.rs
@@ -1,17 +1,15 @@
 use super::*;
 
 #[derive(Default)]
-pub(crate) struct Ran<'src>(
-  Mutex<BTreeMap<Namepath<'src>, BTreeMap<Vec<String>, Arc<Mutex<bool>>>>>,
-);
+pub(crate) struct Ran(Mutex<BTreeMap<String, BTreeMap<Vec<String>, Arc<Mutex<bool>>>>>);
 
-impl<'src> Ran<'src> {
-  pub(crate) fn mutex(&self, recipe: &Namepath<'src>, arguments: &[String]) -> Arc<Mutex<bool>> {
+impl Ran {
+  pub(crate) fn mutex(&self, recipe: &Recipe, arguments: &[String]) -> Arc<Mutex<bool>> {
     self
       .0
       .lock()
       .unwrap()
-      .entry(recipe.clone())
+      .entry(recipe.namepath().into())
       .or_default()
       .entry(arguments.into())
       .or_default()
diff --git a/src/recipe.rs b/src/recipe.rs
index 6b5727b63f..11b8f2edac 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -28,7 +28,7 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
   #[serde(skip)]
   pub(crate) import_offsets: Vec<usize>,
   pub(crate) name: Name<'src>,
-  pub(crate) namepath: Namepath<'src>,
+  pub(crate) namepath: Option<String>,
   pub(crate) parameters: Vec<Parameter<'src>>,
   pub(crate) priors: usize,
   pub(crate) private: bool,
@@ -36,6 +36,21 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
   pub(crate) shebang: bool,
 }
 
+impl Recipe<'_> {
+  pub(crate) fn module_path(&self) -> &str {
+    let namepath = self.namepath();
+    &namepath[0..namepath.rfind("::").unwrap_or_default()]
+  }
+
+  pub(crate) fn namepath(&self) -> &str {
+    self.namepath.as_ref().unwrap()
+  }
+
+  pub(crate) fn spaced_namepath(&self) -> String {
+    self.namepath().replace("::", " ")
+  }
+}
+
 impl<'src, D> Recipe<'src, D> {
   pub(crate) fn argument_range(&self) -> RangeInclusive<usize> {
     self.min_arguments()..=self.max_arguments()
@@ -57,12 +72,6 @@ impl<'src, D> Recipe<'src, D> {
     }
   }
 
-  pub(crate) fn module_path(&self) -> String {
-    let mut path = self.namepath.to_string();
-    path.truncate(path.rfind("::").unwrap_or_default());
-    path
-  }
-
   pub(crate) fn name(&self) -> &'src str {
     self.name.lexeme()
   }
diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs
index 2e542d01a9..1bc1493a9c 100644
--- a/src/recipe_resolver.rs
+++ b/src/recipe_resolver.rs
@@ -2,6 +2,7 @@ use {super::*, CompileErrorKind::*};
 
 pub(crate) struct RecipeResolver<'src: 'run, 'run> {
   assignments: &'run Table<'src, Assignment<'src>>,
+  module_path: &'run str,
   modules: &'run Table<'src, Justfile<'src>>,
   resolved_recipes: Table<'src, Arc<Recipe<'src>>>,
   unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
@@ -10,15 +11,17 @@ pub(crate) struct RecipeResolver<'src: 'run, 'run> {
 impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
   pub(crate) fn resolve_recipes(
     assignments: &'run Table<'src, Assignment<'src>>,
+    module_path: &'run str,
     modules: &'run Table<'src, Justfile<'src>>,
     settings: &Settings,
     unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
   ) -> CompileResult<'src, Table<'src, Arc<Recipe<'src>>>> {
     let mut resolver = Self {
-      resolved_recipes: Table::new(),
-      unresolved_recipes,
       assignments,
+      module_path,
       modules,
+      resolved_recipes: Table::new(),
+      unresolved_recipes,
     };
 
     while let Some(unresolved) = resolver.unresolved_recipes.pop() {
@@ -106,7 +109,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
 
     stack.pop();
 
-    let resolved = Arc::new(recipe.resolve(dependencies)?);
+    let resolved = Arc::new(recipe.resolve(self.module_path, dependencies)?);
     self.resolved_recipes.insert(Arc::clone(&resolved));
     Ok(resolved)
   }
diff --git a/src/subcommand.rs b/src/subcommand.rs
index a61ab1c533..c2e19deaa2 100644
--- a/src/subcommand.rs
+++ b/src/subcommand.rs
@@ -246,7 +246,7 @@ impl Subcommand {
 
     let stdin = child.stdin.as_mut().unwrap();
     for recipe in recipes {
-      if let Err(io_error) = writeln!(stdin, "{}", recipe.namepath.spaced()) {
+      if let Err(io_error) = writeln!(stdin, "{}", recipe.spaced_namepath()) {
         if io_error.kind() != std::io::ErrorKind::BrokenPipe {
           return Err(Error::ChooserWrite { io_error, chooser });
         }
diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs
index 55f987efa2..8c8e41eca4 100644
--- a/src/unresolved_recipe.rs
+++ b/src/unresolved_recipe.rs
@@ -5,6 +5,7 @@ pub(crate) type UnresolvedRecipe<'src> = Recipe<'src, UnresolvedDependency<'src>
 impl<'src> UnresolvedRecipe<'src> {
   pub(crate) fn resolve(
     self,
+    module_path: &str,
     resolved: Vec<Arc<Recipe<'src>>>,
   ) -> CompileResult<'src, Recipe<'src>> {
     assert_eq!(
@@ -42,6 +43,14 @@ impl<'src> UnresolvedRecipe<'src> {
       })
       .collect();
 
+    let mut namepath = String::from(module_path);
+
+    if !namepath.is_empty() {
+      namepath.push_str("::");
+    }
+
+    namepath.push_str(self.name.lexeme());
+
     Ok(Recipe {
       attributes: self.attributes,
       body: self.body,
@@ -50,7 +59,7 @@ impl<'src> UnresolvedRecipe<'src> {
       file_depth: self.file_depth,
       import_offsets: self.import_offsets,
       name: self.name,
-      namepath: self.namepath,
+      namepath: Some(namepath),
       parameters: self.parameters,
       priors: self.priors,
       private: self.private,


Test patch:
diff --git a/tests/scope.rs b/tests/scope.rs
index 97c36c07ed..a34bdb4fde 100644
--- a/tests/scope.rs
+++ b/tests/scope.rs
@@ -51,3 +51,20 @@ foo:
     .args(["a", "b", "foo"])
     .run();
 }
+
+#[test]
+fn imported_recipes_run_in_correct_scope() {
+  Test::new()
+    .justfile(
+      "
+        mod a
+        mod b
+      ",
+    )
+    .write("a.just", "X := 'A'\nimport 'shared.just'")
+    .write("b.just", "X := 'B'\nimport 'shared.just'")
+    .write("shared.just", "foo:\n @echo {{ X }}")
+    .args(["a::foo", "b::foo"])
+    .stdout("A\nB\n")
+    .run();
+}