Repository: casey/just
Pull request #1788 (2023-12-29 00:35:29)

Run submodule recipes in submodule directory


Patch:
diff --git a/src/compiler.rs b/src/compiler.rs
index 457e8894..82730bde 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -20,7 +20,7 @@ impl Compiler {
       let (relative, src) = loader.load(root, &current)?;
       loaded.push(relative.into());
       let tokens = Lexer::lex(relative, src)?;
-      let mut ast = Parser::parse(&tokens)?;
+      let mut ast = Parser::parse(current != root, &current, &tokens)?;
 
       paths.insert(current.clone(), relative.into());
       srcs.insert(current.clone(), src);
@@ -120,7 +120,7 @@ impl Compiler {
   #[cfg(test)]
   pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
     let tokens = Lexer::test_lex(src)?;
-    let ast = Parser::parse(&tokens)?;
+    let ast = Parser::parse(false, &PathBuf::new(), &tokens)?;
     let root = PathBuf::from("justfile");
     let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
     asts.insert(root.clone(), ast);
diff --git a/src/config.rs b/src/config.rs
index ec611606..1c20aee7 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -18,7 +18,6 @@ pub(crate) fn chooser_default(justfile: &Path) -> OsString {
 }
 
 #[derive(Debug, PartialEq)]
-#[allow(clippy::struct_excessive_bools)]
 pub(crate) struct Config {
   pub(crate) check: bool,
   pub(crate) color: Color,
diff --git a/src/justfile.rs b/src/justfile.rs
index 23134208..148c936e 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -299,7 +299,6 @@ impl<'src> Justfile<'src> {
       .or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
   }
 
-  #[allow(clippy::too_many_arguments)]
   fn invocation<'run>(
     &'run self,
     depth: usize,
diff --git a/src/lib.rs b/src/lib.rs
index fa88edee..0ace8fb7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,9 +4,13 @@
   clippy::let_underscore_untyped,
   clippy::needless_pass_by_value,
   clippy::similar_names,
+  clippy::struct_excessive_bools,
+  clippy::struct_field_names,
+  clippy::too_many_arguments,
   clippy::too_many_lines,
   clippy::unnecessary_wraps,
-  clippy::wildcard_imports
+  clippy::wildcard_imports,
+  overlapping_range_endpoints
 )]
 
 pub(crate) use {
diff --git a/src/parser.rs b/src/parser.rs
index 37589885..cb254f16 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -32,22 +32,28 @@ pub(crate) struct Parser<'tokens, 'src> {
   expected: BTreeSet<TokenKind>,
   /// Current recursion depth
   depth: usize,
+  /// Path to the file being parsed
+  path: PathBuf,
+  /// Parsing a submodule
+  submodule: bool,
 }
 
 impl<'tokens, 'src> Parser<'tokens, 'src> {
   /// Parse `tokens` into an `Ast`
-  pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompileResult<'src, Ast<'src>> {
-    Self::new(tokens).parse_ast()
-  }
-
-  /// Construct a new Parser from a token stream
-  fn new(tokens: &'tokens [Token<'src>]) -> Parser<'tokens, 'src> {
+  pub(crate) fn parse(
+    submodule: bool,
+    path: &Path,
+    tokens: &'tokens [Token<'src>],
+  ) -> CompileResult<'src, Ast<'src>> {
     Parser {
-      next: 0,
+      depth: 0,
       expected: BTreeSet::new(),
+      next: 0,
+      path: path.into(),
+      submodule,
       tokens,
-      depth: 0,
     }
+    .parse_ast()
   }
 
   fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> {
@@ -707,16 +713,18 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
     let body = self.parse_body()?;
 
     Ok(Recipe {
-      parameters: positional.into_iter().chain(variadic).collect(),
-      private: name.lexeme().starts_with('_'),
       shebang: body.first().map_or(false, Line::is_shebang),
       attributes,
-      priors,
       body,
       dependencies,
       doc,
       name,
+      parameters: positional.into_iter().chain(variadic).collect(),
+      path: self.path.clone(),
+      priors,
+      private: name.lexeme().starts_with('_'),
       quiet,
+      submodule: self.submodule,
     })
   }
 
@@ -934,7 +942,7 @@ mod tests {
   fn test(text: &str, want: Tree) {
     let unindented = unindent(text);
     let tokens = Lexer::test_lex(&unindented).expect("lexing failed");
-    let justfile = Parser::parse(&tokens).expect("parsing failed");
+    let justfile = Parser::parse(false, &PathBuf::new(), &tokens).expect("parsing failed");
     let have = justfile.tree();
     if have != want {
       println!("parsed text: {unindented}");
@@ -972,7 +980,7 @@ mod tests {
   ) {
     let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
 
-    match Parser::parse(&tokens) {
+    match Parser::parse(false, &PathBuf::new(), &tokens) {
       Ok(_) => panic!("Parsing unexpectedly succeeded"),
       Err(have) => {
         let want = CompileError {
diff --git a/src/recipe.rs b/src/recipe.rs
index e526b8b0..598e6a1b 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -28,10 +28,14 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
   pub(crate) doc: Option<&'src str>,
   pub(crate) name: Name<'src>,
   pub(crate) parameters: Vec<Parameter<'src>>,
+  #[serde(skip)]
+  pub(crate) path: PathBuf,
   pub(crate) priors: usize,
   pub(crate) private: bool,
   pub(crate) quiet: bool,
   pub(crate) shebang: bool,
+  #[serde(skip)]
+  pub(crate) submodule: bool,
 }
 
 impl<'src, D> Recipe<'src, D> {
@@ -222,7 +226,11 @@ impl<'src, D> Recipe<'src, D> {
       let mut cmd = context.settings.shell_command(config);
 
       if self.change_directory() {
-        cmd.current_dir(&context.search.working_directory);
+        cmd.current_dir(if self.submodule {
+          self.path.parent().unwrap()
+        } else {
+          &context.search.working_directory
+        });
       }
 
       cmd.arg(command);
@@ -358,7 +366,11 @@ impl<'src, D> Recipe<'src, D> {
     let mut command = Platform::make_shebang_command(
       &path,
       if self.change_directory() {
-        Some(&context.search.working_directory)
+        if self.submodule {
+          Some(self.path.parent().unwrap())
+        } else {
+          Some(&context.search.working_directory)
+        }
       } else {
         None
       },
diff --git a/src/settings.rs b/src/settings.rs
index 26765e0f..5c5e64ce 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -6,7 +6,6 @@ pub(crate) const WINDOWS_POWERSHELL_SHELL: &str = "powershell.exe";
 pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
 
 #[derive(Debug, PartialEq, Serialize, Default)]
-#[allow(clippy::struct_excessive_bools)]
 pub(crate) struct Settings<'src> {
   pub(crate) allow_duplicate_recipes: bool,
   pub(crate) dotenv_filename: Option<String>,
diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs
index 0a49443d..b7f379fa 100644
--- a/src/unresolved_recipe.rs
+++ b/src/unresolved_recipe.rs
@@ -45,16 +45,18 @@ impl<'src> UnresolvedRecipe<'src> {
       .collect();
 
     Ok(Recipe {
+      attributes: self.attributes,
       body: self.body,
+      dependencies,
       doc: self.doc,
       name: self.name,
       parameters: self.parameters,
+      path: self.path,
+      priors: self.priors,
       private: self.private,
       quiet: self.quiet,
       shebang: self.shebang,
-      priors: self.priors,
-      attributes: self.attributes,
-      dependencies,
+      submodule: self.submodule,
     })
   }
 }


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index b5134545..adb62434 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -59,7 +59,8 @@ pub(crate) fn analysis_error(
 ) {
   let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
 
-  let ast = Parser::parse(&tokens).expect("Parsing failed in analysis test...");
+  let ast =
+    Parser::parse(false, &PathBuf::new(), &tokens).expect("Parsing failed in analysis test...");
 
   let root = PathBuf::from("justfile");
   let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
diff --git a/tests/modules.rs b/tests/modules.rs
index ca69340f..ea4796a4 100644
--- a/tests/modules.rs
+++ b/tests/modules.rs
@@ -493,3 +493,39 @@ fn recipes_may_be_named_mod() {
     .stdout("FOO\n")
     .run();
 }
+
+#[test]
+fn submodule_linewise_recipes_run_in_submodule_directory() {
+  Test::new()
+    .write("foo/bar", "BAR")
+    .write("foo/mod.just", "foo:\n @cat bar")
+    .justfile(
+      "
+        mod foo
+      ",
+    )
+    .test_round_trip(false)
+    .arg("--unstable")
+    .arg("foo")
+    .arg("foo")
+    .stdout("BAR")
+    .run();
+}
+
+#[test]
+fn submodule_shebang_recipes_run_in_submodule_directory() {
+  Test::new()
+    .write("foo/bar", "BAR")
+    .write("foo/mod.just", "foo:\n #!/bin/sh\n cat bar")
+    .justfile(
+      "
+        mod foo
+      ",
+    )
+    .test_round_trip(false)
+    .arg("--unstable")
+    .arg("foo")
+    .arg("foo")
+    .stdout("BAR")
+    .run();
+}
diff --git a/tests/shebang.rs b/tests/shebang.rs
index 18091bb5..04451b73 100644
--- a/tests/shebang.rs
+++ b/tests/shebang.rs
@@ -1,3 +1,5 @@
+use super::*;
+
 #[cfg(windows)]
 test! {
   name:     powershell,
@@ -41,3 +43,17 @@ default:
 "#,
   stdout: "Hello-World\r\n",
 }
+
+#[test]
+fn simple() {
+  Test::new()
+    .justfile(
+      "
+        foo:
+          #!/bin/sh
+          echo bar
+      ",
+    )
+    .stdout("bar\n")
+    .run();
+}