just/src/compiler.rs

223 lines
5.8 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();
2023-12-28 17:34:37 -08:00
let mut stack: Vec<(PathBuf, u32)> = Vec::new();
stack.push((root.into(), 0));
2023-12-28 17:34:37 -08:00
while let Some((current, depth)) = stack.pop() {
let (relative, src) = loader.load(root, &current)?;
loaded.push(relative.into());
let tokens = Lexer::lex(relative, src)?;
2023-12-28 17:34:37 -08:00
let mut ast = Parser::parse(depth, &current, &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,
path,
} => {
2023-12-27 20:27:15 -08:00
if !unstable {
return Err(Error::Unstable {
message: "Modules are currently unstable.".into(),
});
}
let parent = current.parent().unwrap();
let import = if let Some(path) = path {
parent.join(Self::expand_tilde(&path.cooked)?)
} else {
Self::find_module_file(parent, *name)?
};
2023-12-27 20:27:15 -08:00
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
*absolute = Some(import.clone());
2023-12-28 17:34:37 -08:00
stack.push((import, depth + 1));
}
2023-12-27 20:27:15 -08:00
Item::Import { relative, absolute } => {
let import = current
.parent()
.unwrap()
.join(Self::expand_tilde(&relative.cooked)?)
.lexiclean();
2023-12-27 20:27:15 -08:00
if srcs.contains_key(&import) {
return Err(Error::CircularImport { current, import });
}
*absolute = Some(import.clone());
2023-12-28 17:34:37 -08:00
stack.push((import, depth + 1));
2023-12-27 20:27:15 -08:00
}
_ => {}
}
}
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,
}),
}
}
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<Justfile> {
let tokens = Lexer::test_lex(src)?;
2023-12-28 17:34:37 -08:00
let ast = Parser::parse(0, &PathBuf::new(), &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()
);
}
}