Repository: casey/just
Pull request #2638 (2025-02-24 22:34:27)

Guarantee that namepaths are non-empty


Patch:
diff --git a/src/compiler.rs b/src/compiler.rs
index e8d96f2e..ea0683db 100644
--- a/src/compiler.rs
+++ b/src/compiler.rs
@@ -22,7 +22,7 @@ impl Compiler {
       let mut ast = Parser::parse(
         current.file_depth,
         &current.import_offsets,
-        &current.namepath,
+        current.namepath.as_ref(),
         &tokens,
         &current.working_directory,
       )?;
@@ -213,7 +213,7 @@ impl Compiler {
   #[cfg(test)]
   pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
     let tokens = Lexer::test_lex(src)?;
-    let ast = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())?;
+    let ast = Parser::parse(0, &[], None, &tokens, &PathBuf::new())?;
     let root = PathBuf::from("justfile");
     let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
     asts.insert(root.clone(), ast);
diff --git a/src/namepath.rs b/src/namepath.rs
index 3f1a557d..c6cd2bf4 100644
--- a/src/namepath.rs
+++ b/src/namepath.rs
@@ -1,6 +1,6 @@
 use super::*;
 
-#[derive(Default, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
+#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub(crate) struct Namepath<'src>(Vec<Name<'src>>);
 
 impl<'src> Namepath<'src> {
@@ -28,6 +28,12 @@ impl Display for Namepath<'_> {
   }
 }
 
+impl<'src> From<Name<'src>> for Namepath<'src> {
+  fn from(name: Name<'src>) -> Self {
+    Self(vec![name])
+  }
+}
+
 impl Serialize for Namepath<'_> {
   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
   where
diff --git a/src/parser.rs b/src/parser.rs
index 9305a42d..6a10523a 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -27,7 +27,7 @@ pub(crate) struct Parser<'run, 'src> {
   expected_tokens: BTreeSet<TokenKind>,
   file_depth: u32,
   import_offsets: Vec<usize>,
-  module_namepath: &'run Namepath<'src>,
+  module_namepath: Option<&'run Namepath<'src>>,
   next_token: usize,
   recursion_depth: usize,
   tokens: &'run [Token<'src>],
@@ -40,7 +40,7 @@ impl<'run, 'src> Parser<'run, 'src> {
   pub(crate) fn parse(
     file_depth: u32,
     import_offsets: &[usize],
-    module_namepath: &'run Namepath<'src>,
+    module_namepath: Option<&'run Namepath<'src>>,
     tokens: &'run [Token<'src>],
     working_directory: &'run Path,
   ) -> CompileResult<'src, Ast<'src>> {
@@ -996,7 +996,9 @@ impl<'run, 'src> Parser<'run, 'src> {
       file_depth: self.file_depth,
       import_offsets: self.import_offsets.clone(),
       name,
-      namepath: self.module_namepath.join(name),
+      namepath: self
+        .module_namepath
+        .map_or_else(|| name.into(), |module_namepath| module_namepath.join(name)),
       parameters: positional.into_iter().chain(variadic).collect(),
       priors,
       private,
@@ -1255,8 +1257,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(0, &[], &Namepath::default(), &tokens, &PathBuf::new())
-      .expect("parsing failed");
+    let justfile = Parser::parse(0, &[], None, &tokens, &PathBuf::new()).expect("parsing failed");
     let have = justfile.tree();
     if have != want {
       println!("parsed text: {unindented}");
@@ -1294,7 +1295,7 @@ mod tests {
   ) {
     let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
 
-    match Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new()) {
+    match Parser::parse(0, &[], None, &tokens, &PathBuf::new()) {
       Ok(_) => panic!("Parsing unexpectedly succeeded"),
       Err(have) => {
         let want = CompileError {
diff --git a/src/source.rs b/src/source.rs
index 5b125ecc..76192aa3 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -5,7 +5,7 @@ pub(crate) struct Source<'src> {
   pub(crate) file_depth: u32,
   pub(crate) file_path: Vec<PathBuf>,
   pub(crate) import_offsets: Vec<usize>,
-  pub(crate) namepath: Namepath<'src>,
+  pub(crate) namepath: Option<Namepath<'src>>,
   pub(crate) path: PathBuf,
   pub(crate) working_directory: PathBuf,
 }
@@ -16,7 +16,7 @@ impl<'src> Source<'src> {
       file_depth: 0,
       file_path: vec![path.into()],
       import_offsets: Vec::new(),
-      namepath: Namepath::default(),
+      namepath: None,
       path: path.into(),
       working_directory: path.parent().unwrap().into(),
     }
@@ -53,7 +53,12 @@ impl<'src> Source<'src> {
         .chain(iter::once(path.clone()))
         .collect(),
       import_offsets: Vec::new(),
-      namepath: self.namepath.join(name),
+      namepath: Some(
+        self
+          .namepath
+          .as_ref()
+          .map_or_else(|| name.into(), |namepath| namepath.join(name)),
+      ),
       path: path.clone(),
       working_directory: path.parent().unwrap().into(),
     }


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index 12069fee..1045e0d7 100644
--- a/src/testing.rs
+++ b/src/testing.rs
@@ -59,7 +59,7 @@ pub(crate) fn analysis_error(
 ) {
   let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
 
-  let ast = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())
+  let ast = Parser::parse(0, &[], None, &tokens, &PathBuf::new())
     .expect("Parsing failed in analysis test...");
 
   let root = PathBuf::from("justfile");