pub_just/
compiler.rs

1use super::*;
2
3pub struct Compiler;
4
5impl Compiler {
6  pub fn compile<'src>(loader: &'src Loader, root: &Path) -> RunResult<'src, Compilation<'src>> {
7    let mut asts = HashMap::<PathBuf, Ast>::new();
8    let mut loaded = Vec::new();
9    let mut paths = HashMap::<PathBuf, PathBuf>::new();
10    let mut srcs = HashMap::<PathBuf, &str>::new();
11
12    let mut stack = Vec::new();
13    stack.push(Source::root(root));
14
15    while let Some(current) = stack.pop() {
16      let (relative, src) = loader.load(root, &current.path)?;
17      loaded.push(relative.into());
18      let tokens = Lexer::lex(relative, src)?;
19      let mut ast = Parser::parse(
20        current.file_depth,
21        &current.import_offsets,
22        &current.namepath,
23        &tokens,
24        &current.working_directory,
25      )?;
26
27      paths.insert(current.path.clone(), relative.into());
28      srcs.insert(current.path.clone(), src);
29
30      for item in &mut ast.items {
31        match item {
32          Item::Module {
33            absolute,
34            name,
35            optional,
36            relative,
37            ..
38          } => {
39            let parent = current.path.parent().unwrap();
40
41            let relative = relative
42              .as_ref()
43              .map(|relative| Self::expand_tilde(&relative.cooked))
44              .transpose()?;
45
46            let import = Self::find_module_file(parent, *name, relative.as_deref())?;
47
48            if let Some(import) = import {
49              if current.file_path.contains(&import) {
50                return Err(Error::CircularImport {
51                  current: current.path,
52                  import,
53                });
54              }
55              *absolute = Some(import.clone());
56              stack.push(current.module(*name, import));
57            } else if !*optional {
58              return Err(Error::MissingModuleFile { module: *name });
59            }
60          }
61          Item::Import {
62            relative,
63            absolute,
64            optional,
65            path,
66          } => {
67            let import = current
68              .path
69              .parent()
70              .unwrap()
71              .join(Self::expand_tilde(&relative.cooked)?)
72              .lexiclean();
73
74            if import.is_file() {
75              if current.file_path.contains(&import) {
76                return Err(Error::CircularImport {
77                  current: current.path,
78                  import,
79                });
80              }
81              *absolute = Some(import.clone());
82              stack.push(current.import(import, path.offset));
83            } else if !*optional {
84              return Err(Error::MissingImportFile { path: *path });
85            }
86          }
87          _ => {}
88        }
89      }
90
91      asts.insert(current.path, ast.clone());
92    }
93
94    let justfile = Analyzer::analyze(&asts, None, &[], &loaded, None, &paths, root)?;
95
96    Ok(Compilation {
97      asts,
98      justfile,
99      root: root.into(),
100      srcs,
101    })
102  }
103
104  fn find_module_file<'src>(
105    parent: &Path,
106    module: Name<'src>,
107    path: Option<&Path>,
108  ) -> RunResult<'src, Option<PathBuf>> {
109    let mut candidates = Vec::new();
110
111    if let Some(path) = path {
112      let full = parent.join(path);
113
114      if full.is_file() {
115        return Ok(Some(full));
116      }
117
118      candidates.push((path.join("mod.just"), true));
119
120      for name in search::JUSTFILE_NAMES {
121        candidates.push((path.join(name), false));
122      }
123    } else {
124      candidates.push((format!("{module}.just").into(), true));
125      candidates.push((format!("{module}/mod.just").into(), true));
126
127      for name in search::JUSTFILE_NAMES {
128        candidates.push((format!("{module}/{name}").into(), false));
129      }
130    }
131
132    let mut grouped = BTreeMap::<PathBuf, Vec<(PathBuf, bool)>>::new();
133
134    for (candidate, case_sensitive) in candidates {
135      let candidate = parent.join(candidate).lexiclean();
136      grouped
137        .entry(candidate.parent().unwrap().into())
138        .or_default()
139        .push((candidate, case_sensitive));
140    }
141
142    let mut found = Vec::new();
143
144    for (directory, candidates) in grouped {
145      let entries = match fs::read_dir(&directory) {
146        Ok(entries) => entries,
147        Err(io_error) => {
148          if io_error.kind() == io::ErrorKind::NotFound {
149            continue;
150          }
151
152          return Err(
153            SearchError::Io {
154              io_error,
155              directory,
156            }
157            .into(),
158          );
159        }
160      };
161
162      for entry in entries {
163        let entry = entry.map_err(|io_error| SearchError::Io {
164          io_error,
165          directory: directory.clone(),
166        })?;
167
168        if let Some(name) = entry.file_name().to_str() {
169          for (candidate, case_sensitive) in &candidates {
170            let candidate_name = candidate.file_name().unwrap().to_str().unwrap();
171
172            let eq = if *case_sensitive {
173              name == candidate_name
174            } else {
175              name.eq_ignore_ascii_case(candidate_name)
176            };
177
178            if eq {
179              found.push(candidate.parent().unwrap().join(name));
180            }
181          }
182        }
183      }
184    }
185
186    if found.len() > 1 {
187      found.sort();
188      Err(Error::AmbiguousModuleFile {
189        found: found
190          .into_iter()
191          .map(|found| found.strip_prefix(parent).unwrap().into())
192          .collect(),
193        module,
194      })
195    } else {
196      Ok(found.into_iter().next())
197    }
198  }
199
200  fn expand_tilde(path: &str) -> RunResult<'static, PathBuf> {
201    Ok(if let Some(path) = path.strip_prefix("~/") {
202      dirs::home_dir()
203        .ok_or(Error::Homedir)?
204        .join(path.trim_start_matches('/'))
205    } else {
206      PathBuf::from(path)
207    })
208  }
209
210  #[cfg(test)]
211  pub fn test_compile(src: &str) -> CompileResult<Justfile> {
212    let tokens = Lexer::test_lex(src)?;
213    let ast = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())?;
214    let root = PathBuf::from("justfile");
215    let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
216    asts.insert(root.clone(), ast);
217    let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
218    paths.insert(root.clone(), root.clone());
219    Analyzer::analyze(&asts, None, &[], &[], None, &paths, &root)
220  }
221}
222
223#[cfg(test)]
224mod tests {
225  use {super::*, temptree::temptree};
226
227  #[test]
228  fn include_justfile() {
229    let justfile_a = r#"
230# A comment at the top of the file
231import "./justfile_b"
232
233#some_recipe: recipe_b
234some_recipe:
235    echo "some recipe"
236"#;
237
238    let justfile_b = r#"import "./subdir/justfile_c"
239
240recipe_b: recipe_c
241    echo "recipe b"
242"#;
243
244    let justfile_c = r#"recipe_c:
245    echo "recipe c"
246"#;
247
248    let tmp = temptree! {
249        justfile: justfile_a,
250        justfile_b: justfile_b,
251        subdir: {
252            justfile_c: justfile_c
253        }
254    };
255
256    let loader = Loader::new();
257
258    let justfile_a_path = tmp.path().join("justfile");
259    let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();
260
261    assert_eq!(compilation.root_src(), justfile_a);
262  }
263
264  #[test]
265  fn recursive_includes_fail() {
266    let tmp = temptree! {
267      justfile: "import './subdir/b'\na: b",
268      subdir: {
269        b: "import '../justfile'\nb:"
270      }
271    };
272
273    let loader = Loader::new();
274
275    let justfile_a_path = tmp.path().join("justfile");
276    let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();
277
278    assert_matches!(loader_output, Error::CircularImport { current, import }
279      if current == tmp.path().join("subdir").join("b").lexiclean() &&
280      import == tmp.path().join("justfile").lexiclean()
281    );
282  }
283
284  #[test]
285  fn find_module_file() {
286    #[track_caller]
287    fn case(path: Option<&str>, files: &[&str], expected: Result<Option<&str>, &[&str]>) {
288      let module = Name {
289        token: Token {
290          column: 0,
291          kind: TokenKind::Identifier,
292          length: 3,
293          line: 0,
294          offset: 0,
295          path: Path::new(""),
296          src: "foo",
297        },
298      };
299
300      let tempdir = tempfile::tempdir().unwrap();
301
302      for file in files {
303        if let Some(parent) = Path::new(file).parent() {
304          fs::create_dir_all(tempdir.path().join(parent)).unwrap();
305        }
306
307        fs::write(tempdir.path().join(file), "").unwrap();
308      }
309
310      let actual = Compiler::find_module_file(tempdir.path(), module, path.map(Path::new));
311
312      match expected {
313        Err(expected) => match actual.unwrap_err() {
314          Error::AmbiguousModuleFile { found, .. } => {
315            assert_eq!(
316              found,
317              expected
318                .iter()
319                .map(|expected| expected.replace('/', std::path::MAIN_SEPARATOR_STR).into())
320                .collect::<Vec<PathBuf>>()
321            );
322          }
323          _ => panic!("unexpected error"),
324        },
325        Ok(Some(expected)) => assert_eq!(
326          actual.unwrap().unwrap(),
327          tempdir
328            .path()
329            .join(expected.replace('/', std::path::MAIN_SEPARATOR_STR))
330        ),
331        Ok(None) => assert_eq!(actual.unwrap(), None),
332      }
333    }
334
335    case(None, &["foo.just"], Ok(Some("foo.just")));
336    case(None, &["FOO.just"], Ok(None));
337    case(None, &["foo/mod.just"], Ok(Some("foo/mod.just")));
338    case(None, &["foo/MOD.just"], Ok(None));
339    case(None, &["foo/justfile"], Ok(Some("foo/justfile")));
340    case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
341    case(None, &["foo/.justfile"], Ok(Some("foo/.justfile")));
342    case(None, &["foo/.JUSTFILE"], Ok(Some("foo/.JUSTFILE")));
343    case(
344      None,
345      &["foo/.justfile", "foo/justfile"],
346      Err(&["foo/.justfile", "foo/justfile"]),
347    );
348    case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
349
350    case(Some("bar"), &["bar"], Ok(Some("bar")));
351    case(Some("bar"), &["bar/mod.just"], Ok(Some("bar/mod.just")));
352    case(Some("bar"), &["bar/justfile"], Ok(Some("bar/justfile")));
353    case(Some("bar"), &["bar/JUSTFILE"], Ok(Some("bar/JUSTFILE")));
354    case(Some("bar"), &["bar/.justfile"], Ok(Some("bar/.justfile")));
355    case(Some("bar"), &["bar/.JUSTFILE"], Ok(Some("bar/.JUSTFILE")));
356
357    case(
358      Some("bar"),
359      &["bar/justfile", "bar/mod.just"],
360      Err(&["bar/justfile", "bar/mod.just"]),
361    );
362  }
363}