use super::*; pub(crate) struct Compiler; impl Compiler { pub(crate) fn compile<'src>( loader: &'src Loader, root: &Path, ) -> RunResult<'src, Compilation<'src>> { let mut asts = HashMap::::new(); let mut loaded = Vec::new(); let mut paths = HashMap::::new(); let mut srcs = HashMap::::new(); let mut stack = Vec::new(); stack.push(Source::root(root)); while let Some(current) = stack.pop() { let (relative, src) = loader.load(root, ¤t.path)?; loaded.push(relative.into()); let tokens = Lexer::lex(relative, src)?; let mut ast = Parser::parse( current.file_depth, ¤t.path, ¤t.import_offsets, ¤t.namepath, current.submodule_depth, &tokens, ¤t.working_directory, )?; paths.insert(current.path.clone(), relative.into()); srcs.insert(current.path.clone(), src); for item in &mut ast.items { match item { Item::Module { absolute, name, optional, relative, .. } => { let parent = current.path.parent().unwrap(); let relative = relative .as_ref() .map(|relative| Self::expand_tilde(&relative.cooked)) .transpose()?; let import = Self::find_module_file(parent, *name, relative.as_deref())?; if let Some(import) = import { if current.file_path.contains(&import) { return Err(Error::CircularImport { current: current.path, import, }); } *absolute = Some(import.clone()); stack.push(current.module(*name, import)); } else if !*optional { return Err(Error::MissingModuleFile { module: *name }); } } Item::Import { relative, absolute, optional, path, } => { let import = current .path .parent() .unwrap() .join(Self::expand_tilde(&relative.cooked)?) .lexiclean(); if import.is_file() { if current.file_path.contains(&import) { return Err(Error::CircularImport { current: current.path, import, }); } *absolute = Some(import.clone()); stack.push(current.import(import, path.offset)); } else if !*optional { return Err(Error::MissingImportFile { path: *path }); } } _ => {} } } asts.insert(current.path, ast.clone()); } let justfile = Analyzer::analyze(&asts, None, &loaded, None, &paths, root)?; Ok(Compilation { asts, justfile, root: root.into(), srcs, }) } fn find_module_file<'src>( parent: &Path, module: Name<'src>, path: Option<&Path>, ) -> RunResult<'src, Option> { let mut candidates = Vec::new(); if let Some(path) = path { let full = parent.join(path); if full.is_file() { return Ok(Some(full)); } candidates.push((path.join("mod.just"), true)); for name in search::JUSTFILE_NAMES { candidates.push((path.join(name), false)); } } else { candidates.push((format!("{module}.just").into(), true)); candidates.push((format!("{module}/mod.just").into(), true)); for name in search::JUSTFILE_NAMES { candidates.push((format!("{module}/{name}").into(), false)); } } let mut grouped = BTreeMap::>::new(); for (candidate, case_sensitive) in candidates { let candidate = parent.join(candidate).lexiclean(); grouped .entry(candidate.parent().unwrap().into()) .or_default() .push((candidate, case_sensitive)); } let mut found = Vec::new(); for (directory, candidates) in grouped { let entries = match fs::read_dir(&directory) { Ok(entries) => entries, Err(io_error) => { if io_error.kind() == io::ErrorKind::NotFound { continue; } return Err( SearchError::Io { io_error, directory, } .into(), ); } }; for entry in entries { let entry = entry.map_err(|io_error| SearchError::Io { io_error, directory: directory.clone(), })?; if let Some(name) = entry.file_name().to_str() { for (candidate, case_sensitive) in &candidates { let candidate_name = candidate.file_name().unwrap().to_str().unwrap(); let eq = if *case_sensitive { name == candidate_name } else { name.eq_ignore_ascii_case(candidate_name) }; if eq { found.push(candidate.parent().unwrap().join(name)); } } } } } if found.len() > 1 { found.sort(); Err(Error::AmbiguousModuleFile { found: found .into_iter() .map(|found| found.strip_prefix(parent).unwrap().into()) .collect(), module, }) } else { Ok(found.into_iter().next()) } } fn expand_tilde(path: &str) -> RunResult<'static, PathBuf> { Ok(if let Some(path) = path.strip_prefix("~/") { dirs::home_dir() .ok_or(Error::Homedir)? .join(path.trim_start_matches('/')) } else { PathBuf::from(path) }) } #[cfg(test)] pub(crate) fn test_compile(src: &str) -> CompileResult { let tokens = Lexer::test_lex(src)?; let ast = Parser::parse( 0, &PathBuf::new(), &[], &Namepath::default(), 0, &tokens, &PathBuf::new(), )?; let root = PathBuf::from("justfile"); let mut asts: HashMap = HashMap::new(); asts.insert(root.clone(), ast); let mut paths: HashMap = HashMap::new(); paths.insert(root.clone(), root.clone()); Analyzer::analyze(&asts, None, &[], None, &paths, &root) } } #[cfg(test)] mod tests { use {super::*, temptree::temptree}; #[test] fn include_justfile() { let justfile_a = r#" # A comment at the top of the file import "./justfile_b" #some_recipe: recipe_b some_recipe: echo "some recipe" "#; let justfile_b = r#"import "./subdir/justfile_c" recipe_b: recipe_c echo "recipe b" "#; let justfile_c = r#"recipe_c: echo "recipe c" "#; let tmp = temptree! { justfile: justfile_a, justfile_b: justfile_b, subdir: { justfile_c: justfile_c } }; let loader = Loader::new(); let justfile_a_path = tmp.path().join("justfile"); let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap(); assert_eq!(compilation.root_src(), justfile_a); } #[test] fn recursive_includes_fail() { let tmp = temptree! { justfile: "import './subdir/b'\na: b", subdir: { b: "import '../justfile'\nb:" } }; let loader = Loader::new(); let justfile_a_path = tmp.path().join("justfile"); let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err(); assert_matches!(loader_output, Error::CircularImport { current, import } if current == tmp.path().join("subdir").join("b").lexiclean() && import == tmp.path().join("justfile").lexiclean() ); } #[test] fn find_module_file() { #[track_caller] fn case(path: Option<&str>, files: &[&str], expected: Result, &[&str]>) { let module = Name { token: Token { column: 0, kind: TokenKind::Identifier, length: 3, line: 0, offset: 0, path: Path::new(""), src: "foo", }, }; let tempdir = tempfile::tempdir().unwrap(); for file in files { if let Some(parent) = Path::new(file).parent() { fs::create_dir_all(tempdir.path().join(parent)).unwrap(); } fs::write(tempdir.path().join(file), "").unwrap(); } let actual = Compiler::find_module_file(tempdir.path(), module, path.map(Path::new)); match expected { Err(expected) => match actual.unwrap_err() { Error::AmbiguousModuleFile { found, .. } => { assert_eq!( found, expected .iter() .map(|expected| expected.replace('/', std::path::MAIN_SEPARATOR_STR).into()) .collect::>() ); } _ => panic!("unexpected error"), }, Ok(Some(expected)) => assert_eq!( actual.unwrap().unwrap(), tempdir .path() .join(expected.replace('/', std::path::MAIN_SEPARATOR_STR)) ), Ok(None) => assert_eq!(actual.unwrap(), None), } } case(None, &["foo.just"], Ok(Some("foo.just"))); case(None, &["FOO.just"], Ok(None)); case(None, &["foo/mod.just"], Ok(Some("foo/mod.just"))); case(None, &["foo/MOD.just"], Ok(None)); case(None, &["foo/justfile"], Ok(Some("foo/justfile"))); case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE"))); case(None, &["foo/.justfile"], Ok(Some("foo/.justfile"))); case(None, &["foo/.JUSTFILE"], Ok(Some("foo/.JUSTFILE"))); case( None, &["foo/.justfile", "foo/justfile"], Err(&["foo/.justfile", "foo/justfile"]), ); case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE"))); case(Some("bar"), &["bar"], Ok(Some("bar"))); case(Some("bar"), &["bar/mod.just"], Ok(Some("bar/mod.just"))); case(Some("bar"), &["bar/justfile"], Ok(Some("bar/justfile"))); case(Some("bar"), &["bar/JUSTFILE"], Ok(Some("bar/JUSTFILE"))); case(Some("bar"), &["bar/.justfile"], Ok(Some("bar/.justfile"))); case(Some("bar"), &["bar/.JUSTFILE"], Ok(Some("bar/.JUSTFILE"))); case( Some("bar"), &["bar/justfile", "bar/mod.just"], Err(&["bar/justfile", "bar/mod.just"]), ); } }