Repository: casey/just
Pull request #2105 (2024-05-29 00:58:13)

Add `module_file()` and `module_directory()` functions


Patch:
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 822b8128..ed9fb37b 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -166,6 +166,7 @@ impl<'src> Analyzer<'src> {
       name,
       recipes,
       settings,
+      source: root.into(),
       warnings,
     })
   }
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 2794b467..1e029c79 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -4,9 +4,10 @@ pub(crate) struct Evaluator<'src: 'run, 'run> {
   pub(crate) assignments: Option<&'run Table<'src, Assignment<'src>>>,
   pub(crate) config: &'run Config,
   pub(crate) dotenv: &'run BTreeMap<String, String>,
+  pub(crate) module_source: &'run Path,
   pub(crate) scope: Scope<'src, 'run>,
-  pub(crate) settings: &'run Settings<'run>,
   pub(crate) search: &'run Search,
+  pub(crate) settings: &'run Settings<'run>,
 }
 
 impl<'src, 'run> Evaluator<'src, 'run> {
@@ -14,17 +15,19 @@ impl<'src, 'run> Evaluator<'src, 'run> {
     assignments: &'run Table<'src, Assignment<'src>>,
     config: &'run Config,
     dotenv: &'run BTreeMap<String, String>,
-    overrides: Scope<'src, 'run>,
-    settings: &'run Settings<'run>,
+    module_source: &'run Path,
+    scope: Scope<'src, 'run>,
     search: &'run Search,
+    settings: &'run Settings<'run>,
   ) -> RunResult<'src, Scope<'src, 'run>> {
     let mut evaluator = Self {
-      scope: overrides,
       assignments: Some(assignments),
       config,
       dotenv,
-      settings,
+      module_source,
+      scope,
       search,
+      settings,
     };
 
     for assignment in assignments.values() {
@@ -250,21 +253,23 @@ impl<'src, 'run> Evaluator<'src, 'run> {
   }
 
   pub(crate) fn evaluate_parameters(
+    arguments: &[String],
     config: &'run Config,
     dotenv: &'run BTreeMap<String, String>,
+    module_source: &'run Path,
     parameters: &[Parameter<'src>],
-    arguments: &[String],
     scope: &'run Scope<'src, 'run>,
-    settings: &'run Settings,
     search: &'run Search,
+    settings: &'run Settings,
   ) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
     let mut evaluator = Self {
       assignments: None,
+      config,
+      dotenv,
+      module_source,
       scope: scope.child(),
       search,
       settings,
-      dotenv,
-      config,
     };
 
     let mut scope = scope.child();
@@ -307,17 +312,19 @@ impl<'src, 'run> Evaluator<'src, 'run> {
   pub(crate) fn recipe_evaluator(
     config: &'run Config,
     dotenv: &'run BTreeMap<String, String>,
+    module_source: &'run Path,
     scope: &'run Scope<'src, 'run>,
-    settings: &'run Settings,
     search: &'run Search,
-  ) -> Evaluator<'src, 'run> {
+    settings: &'run Settings,
+  ) -> Self {
     Self {
       assignments: None,
+      config,
+      dotenv,
+      module_source,
       scope: Scope::child(scope),
       search,
       settings,
-      dotenv,
-      config,
     }
   }
 }
diff --git a/src/function.rs b/src/function.rs
index 52fcf00e..e6820c9d 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -67,6 +67,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
     "kebabcase" => Unary(kebabcase),
     "lowercamelcase" => Unary(lowercamelcase),
     "lowercase" => Unary(lowercase),
+    "module_directory" => Nullary(module_directory),
+    "module_file" => Nullary(module_file),
     "num_cpus" => Nullary(num_cpus),
     "os" => Nullary(os),
     "os_family" => Nullary(os_family),
@@ -406,6 +408,44 @@ fn lowercase(_context: Context, s: &str) -> Result<String, String> {
   Ok(s.to_lowercase())
 }
 
+fn module_directory(context: Context) -> Result<String, String> {
+  context
+    .evaluator
+    .search
+    .justfile
+    .parent()
+    .unwrap()
+    .join(context.evaluator.module_source)
+    .parent()
+    .unwrap()
+    .to_str()
+    .map(str::to_owned)
+    .ok_or_else(|| {
+      format!(
+        "Module directory is not valid unicode: {}",
+        context.evaluator.module_source.parent().unwrap().display(),
+      )
+    })
+}
+
+fn module_file(context: Context) -> Result<String, String> {
+  context
+    .evaluator
+    .search
+    .justfile
+    .parent()
+    .unwrap()
+    .join(context.evaluator.module_source)
+    .to_str()
+    .map(str::to_owned)
+    .ok_or_else(|| {
+      format!(
+        "Module file path is not valid unicode: {}",
+        context.evaluator.module_source.display(),
+      )
+    })
+}
+
 fn num_cpus(_context: Context) -> Result<String, String> {
   let num = num_cpus::get();
   Ok(num.to_string())
diff --git a/src/justfile.rs b/src/justfile.rs
index a74a94c1..e86f6c88 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -3,9 +3,10 @@ use {super::*, serde::Serialize};
 #[derive(Debug)]
 struct Invocation<'src: 'run, 'run> {
   arguments: Vec<&'run str>,
+  module_source: &'run Path,
   recipe: &'run Recipe<'src>,
-  settings: &'run Settings<'src>,
   scope: &'run Scope<'src, 'run>,
+  settings: &'run Settings<'src>,
 }
 
 #[derive(Debug, PartialEq, Serialize)]
@@ -21,6 +22,8 @@ pub(crate) struct Justfile<'src> {
   pub(crate) name: Option<Name<'src>>,
   pub(crate) recipes: Table<'src, Rc<Recipe<'src>>>,
   pub(crate) settings: Settings<'src>,
+  #[serde(skip)]
+  pub(crate) source: PathBuf,
   pub(crate) warnings: Vec<Warning>,
 }
 
@@ -106,9 +109,10 @@ impl<'src> Justfile<'src> {
       &self.assignments,
       config,
       dotenv,
+      &self.source,
       scope,
-      &self.settings,
       search,
+      &self.settings,
     )
   }
 
@@ -276,10 +280,12 @@ impl<'src> Justfile<'src> {
     let mut ran = Ran::default();
     for invocation in invocations {
       let context = RecipeContext {
-        settings: invocation.settings,
         config,
+        dotenv: &dotenv,
+        module_source: invocation.module_source,
         scope: invocation.scope,
         search,
+        settings: invocation.settings,
       };
 
       Self::run_recipe(
@@ -290,7 +296,6 @@ impl<'src> Justfile<'src> {
           .map(str::to_string)
           .collect::<Vec<String>>(),
         &context,
-        &dotenv,
         &mut ran,
         invocation.recipe,
         search,
@@ -346,6 +351,7 @@ impl<'src> Justfile<'src> {
               recipe,
               arguments: Vec::new(),
               scope,
+              module_source: &self.source,
             },
             depth,
           )));
@@ -373,6 +379,7 @@ impl<'src> Justfile<'src> {
             recipe,
             scope: parent,
             settings: &self.settings,
+            module_source: &self.source,
           },
           depth,
         )))
@@ -394,6 +401,7 @@ impl<'src> Justfile<'src> {
             recipe,
             scope: parent,
             settings: &self.settings,
+            module_source: &self.source,
           },
           depth + argument_count,
         )))
@@ -410,7 +418,6 @@ impl<'src> Justfile<'src> {
   fn run_recipe(
     arguments: &[String],
     context: &RecipeContext<'src, '_>,
-    dotenv: &BTreeMap<String, String>,
     ran: &mut Ran<'src>,
     recipe: &Recipe<'src>,
     search: &Search,
@@ -426,19 +433,26 @@ impl<'src> Justfile<'src> {
     }
 
     let (outer, positional) = Evaluator::evaluate_parameters(
+      arguments,
       context.config,
-      dotenv,
+      context.dotenv,
+      context.module_source,
       &recipe.parameters,
-      arguments,
       context.scope,
-      context.settings,
       search,
+      context.settings,
     )?;
 
     let scope = outer.child();
 
-    let mut evaluator =
-      Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
+    let mut evaluator = Evaluator::recipe_evaluator(
+      context.config,
+      context.dotenv,
+      context.module_source,
+      &scope,
+      search,
+      context.settings,
+    );
 
     if !context.config.no_dependencies {
       for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
@@ -447,11 +461,11 @@ impl<'src> Justfile<'src> {
           .map(|argument| evaluator.evaluate_expression(argument))
           .collect::<RunResult<Vec<String>>>()?;
 
-        Self::run_recipe(&arguments, context, dotenv, ran, recipe, search)?;
+        Self::run_recipe(&arguments, context, ran, recipe, search)?;
       }
     }
 
-    recipe.run(context, dotenv, scope.child(), search, &positional)?;
+    recipe.run(context, &scope, &positional)?;
 
     if !context.config.no_dependencies {
       let mut ran = Ran::default();
@@ -463,11 +477,12 @@ impl<'src> Justfile<'src> {
           evaluated.push(evaluator.evaluate_expression(argument)?);
         }
 
-        Self::run_recipe(&evaluated, context, dotenv, &mut ran, recipe, search)?;
+        Self::run_recipe(&evaluated, context, &mut ran, recipe, search)?;
       }
     }
 
     ran.ran(&recipe.namepath, arguments.to_vec());
+
     Ok(())
   }
 
diff --git a/src/parser.rs b/src/parser.rs
index 3a05fea5..415e9c71 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -18,11 +18,11 @@ use {super::*, TokenKind::*};
 /// All methods starting with `parse_*` parse and return a language construct.
 ///
 /// The parser tracks an expected set of tokens as it parses. This set contains
-/// all tokens which would have been accepted at the current point in the parse.
-/// Whenever the parser tests for a token that would be accepted, but does not
-/// find it, it adds that token to the set. When the parser accepts a token, the
-/// set is cleared. If the parser finds a token which is unexpected, the
-/// contents of the set is printed in the resultant error message.
+/// all tokens which would have been accepted at the current point in the
+/// parse. Whenever the parser tests for a token that would be accepted, but
+/// does not find it, it adds that token to the set. When the parser accepts a
+/// token, the set is cleared. If the parser finds a token which is unexpected,
+/// the elements of the set are printed in the resultant error message.
 pub(crate) struct Parser<'run, 'src> {
   expected_tokens: BTreeSet<TokenKind>,
   file_depth: u32,
diff --git a/src/recipe.rs b/src/recipe.rs
index 62c6d279..446c7bbe 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -147,9 +147,7 @@ impl<'src, D> Recipe<'src, D> {
   pub(crate) fn run<'run>(
     &self,
     context: &RecipeContext<'src, 'run>,
-    dotenv: &BTreeMap<String, String>,
-    scope: Scope<'src, 'run>,
-    search: &'run Search,
+    scope: &Scope<'src, 'run>,
     positional: &[String],
   ) -> RunResult<'src, ()> {
     let config = &context.config;
@@ -164,20 +162,25 @@ impl<'src, D> Recipe<'src, D> {
       );
     }
 
-    let evaluator =
-      Evaluator::recipe_evaluator(context.config, dotenv, &scope, context.settings, search);
+    let evaluator = Evaluator::recipe_evaluator(
+      context.config,
+      context.dotenv,
+      context.module_source,
+      scope,
+      context.search,
+      context.settings,
+    );
 
     if self.shebang {
-      self.run_shebang(context, dotenv, &scope, positional, config, evaluator)
+      self.run_shebang(context, scope, positional, config, evaluator)
     } else {
-      self.run_linewise(context, dotenv, &scope, positional, config, evaluator)
+      self.run_linewise(context, scope, positional, config, evaluator)
     }
   }
 
   fn run_linewise<'run>(
     &self,
     context: &RecipeContext<'src, 'run>,
-    dotenv: &BTreeMap<String, String>,
     scope: &Scope<'src, 'run>,
     positional: &[String],
     config: &Config,
@@ -264,7 +267,7 @@ impl<'src, D> Recipe<'src, D> {
         cmd.stdout(Stdio::null());
       }
 
-      cmd.export(context.settings, dotenv, scope);
+      cmd.export(context.settings, context.dotenv, scope);
 
       match InterruptHandler::guard(|| cmd.status()) {
         Ok(exit_status) => {
@@ -298,7 +301,6 @@ impl<'src, D> Recipe<'src, D> {
   pub(crate) fn run_shebang<'run>(
     &self,
     context: &RecipeContext<'src, 'run>,
-    dotenv: &BTreeMap<String, String>,
     scope: &Scope<'src, 'run>,
     positional: &[String],
     config: &Config,
@@ -411,7 +413,7 @@ impl<'src, D> Recipe<'src, D> {
       command.args(positional);
     }
 
-    command.export(context.settings, dotenv, scope);
+    command.export(context.settings, context.dotenv, scope);
 
     // run it!
     match InterruptHandler::guard(|| command.status()) {
diff --git a/src/recipe_context.rs b/src/recipe_context.rs
index 0e46f5f8..f1ed5c67 100644
--- a/src/recipe_context.rs
+++ b/src/recipe_context.rs
@@ -2,6 +2,8 @@ use super::*;
 
 pub(crate) struct RecipeContext<'src: 'run, 'run> {
   pub(crate) config: &'run Config,
+  pub(crate) dotenv: &'run BTreeMap<String, String>,
+  pub(crate) module_source: &'run Path,
   pub(crate) scope: &'run Scope<'src, 'run>,
   pub(crate) search: &'run Search,
   pub(crate) settings: &'run Settings<'src>,


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index 514f5985..caf772c5 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -904,3 +904,126 @@ fn source_directory() {
     .stdout_regex(r".*[/\\]foo\n")
     .run();
 }
+
+#[test]
+fn module_paths() {
+  Test::new()
+    .write(
+      "foo/bar.just",
+      "
+imf := module_file()
+imd := module_directory()
+
+import-outer: import-inner
+
+@import-inner pmf=module_file() pmd=module_directory():
+  echo import
+  echo '{{ imf }}'
+  echo '{{ imd }}'
+  echo '{{ pmf }}'
+  echo '{{ pmd }}'
+  echo '{{ module_file() }}'
+  echo '{{ module_directory() }}'
+      ",
+    )
+    .write(
+      "baz/mod.just",
+      "
+import 'foo/bar.just'
+
+mmf := module_file()
+mmd := module_directory()
+
+outer: inner
+
+@inner pmf=module_file() pmd=module_directory():
+  echo module
+  echo '{{ mmf }}'
+  echo '{{ mmd }}'
+  echo '{{ pmf }}'
+  echo '{{ pmd }}'
+  echo '{{ module_file() }}'
+  echo '{{ module_directory() }}'
+      ",
+    )
+    .write(
+      "baz/foo/bar.just",
+      "
+imf := module_file()
+imd := module_directory()
+
+import-outer: import-inner
+
+@import-inner pmf=module_file() pmd=module_directory():
+  echo import
+  echo '{{ imf }}'
+  echo '{{ imd }}'
+  echo '{{ pmf }}'
+  echo '{{ pmd }}'
+  echo '{{ module_file() }}'
+  echo '{{ module_directory() }}'
+      ",
+    )
+    .justfile(
+      "
+        import 'foo/bar.just'
+        mod baz
+
+        rmf := module_file()
+        rmd := module_directory()
+
+        outer: inner
+
+        @inner pmf=module_file() pmd=module_directory():
+          echo root
+          echo '{{ rmf }}'
+          echo '{{ rmd }}'
+          echo '{{ pmf }}'
+          echo '{{ pmd }}'
+          echo '{{ module_file() }}'
+          echo '{{ module_directory() }}'
+      ",
+    )
+    .test_round_trip(false)
+    .args([
+      "--unstable",
+      "outer",
+      "import-outer",
+      "baz",
+      "outer",
+      "baz",
+      "import-outer",
+    ])
+    .stdout_regex(
+      r"root
+.*[/\\]just-test-tempdir......[/\\]justfile
+.*[/\\]just-test-tempdir......
+.*[/\\]just-test-tempdir......[/\\]justfile
+.*[/\\]just-test-tempdir......
+.*[/\\]just-test-tempdir......[/\\]justfile
+.*[/\\]just-test-tempdir......
+import
+.*[/\\]just-test-tempdir......[/\\]justfile
+.*[/\\]just-test-tempdir......
+.*[/\\]just-test-tempdir......[/\\]justfile
+.*[/\\]just-test-tempdir......
+.*[/\\]just-test-tempdir......[/\\]justfile
+.*[/\\]just-test-tempdir......
+module
+.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just
+.*[/\\]just-test-tempdir......[/\\]baz
+.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just
+.*[/\\]just-test-tempdir......[/\\]baz
+.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just
+.*[/\\]just-test-tempdir......[/\\]baz
+import
+.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just
+.*[/\\]just-test-tempdir......[/\\]baz
+.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just
+.*[/\\]just-test-tempdir......[/\\]baz
+.*[/\\]just-test-tempdir......[/\\]baz[/\\]mod.just
+.*[/\\]just-test-tempdir......[/\\]baz
+",
+    )
+    .run();
+}