just/src/compiler.rs

201 lines
5.2 KiB
Rust
Raw Normal View History

use super::*;
pub(crate) struct Compiler;
impl Compiler {
pub(crate) fn compile<'src>(
2023-12-27 20:27:15 -08:00
unstable: bool,
loader: &'src Loader,
root: &Path,
) -> RunResult<'src, Compilation<'src>> {
let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
let mut srcs: HashMap<PathBuf, &str> = HashMap::new();
let mut loaded = Vec::new();
let mut stack: Vec<PathBuf> = Vec::new();
stack.push(root.into());
while let Some(current) = stack.pop() {
let (relative, src) = loader.load(root, &current)?;
loaded.push(relative.into());
let tokens = Lexer::lex(relative, src)?;
let mut ast = Parser::parse(&tokens)?;
paths.insert(current.clone(), relative.into());
srcs.insert(current.clone(), src);
for item in &mut ast.items {
2023-12-27 20:27:15 -08:00
match item {
Item::Mod { name, absolute } => {
if !unstable {
return Err(Error::Unstable {
message: "Modules are currently unstable.".into(),
});
}
let parent = current.parent().unwrap();
let import = Self::find_module_file(parent, *name)?;
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
*absolute = Some(import.clone());
stack.push(import);
}
2023-12-27 20:27:15 -08:00
Item::Import { relative, absolute } => {
let import = current.parent().unwrap().join(&relative.cooked).lexiclean();
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
*absolute = Some(import.clone());
stack.push(import);
}
_ => {}
}
}
asts.insert(current.clone(), ast.clone());
}
2023-12-27 20:27:15 -08:00
let justfile = Analyzer::analyze(&loaded, &paths, &asts, root)?;
Ok(Compilation {
asts,
srcs,
justfile,
root: root.into(),
})
}
2023-12-27 20:27:15 -08:00
fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, PathBuf> {
let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")]
.into_iter()
.filter(|path| parent.join(path).is_file())
.collect::<Vec<String>>();
let directory = parent.join(module.lexeme());
if directory.exists() {
let entries = fs::read_dir(&directory).map_err(|io_error| SearchError::Io {
io_error,
directory: directory.clone(),
})?;
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 justfile_name in search::JUSTFILE_NAMES {
if name.eq_ignore_ascii_case(justfile_name) {
candidates.push(format!("{module}/{name}"));
}
}
}
}
}
match candidates.as_slice() {
[] => Err(Error::MissingModuleFile { module }),
[file] => Ok(parent.join(file).lexiclean()),
found => Err(Error::AmbiguousModuleFile {
found: found.into(),
module,
}),
}
}
#[cfg(test)]
pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
let tokens = Lexer::test_lex(src)?;
let ast = Parser::parse(&tokens)?;
let root = PathBuf::from("justfile");
let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
asts.insert(root.clone(), ast);
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
paths.insert(root.clone(), root.clone());
2023-12-27 20:27:15 -08:00
Analyzer::analyze(&[], &paths, &asts, &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");
2023-12-27 20:27:15 -08:00
let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap();
assert_eq!(compilation.root_src(), justfile_a);
}
#[test]
fn recursive_includes_fail() {
let justfile_a = r#"
# A comment at the top of the file
import "./subdir/justfile_b"
some_recipe: recipe_b
echo "some recipe"
"#;
let justfile_b = r#"
import "../justfile"
recipe_b:
echo "recipe b"
"#;
let tmp = temptree! {
justfile: justfile_a,
subdir: {
justfile_b: justfile_b
}
};
let loader = Loader::new();
let justfile_a_path = tmp.path().join("justfile");
2023-12-27 20:27:15 -08:00
let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err();
assert_matches!(loader_output, Error::CircularImport { current, import }
if current == tmp.path().join("subdir").join("justfile_b").lexiclean() &&
import == tmp.path().join("justfile").lexiclean()
);
}
}