use anyhow::{Context, Result};
pub(crate) fn init_tracing(verbose: u8, quiet: bool, no_color: bool) {
use tracing_subscriber::EnvFilter;
let default_level = if quiet {
"error"
} else {
match verbose {
0 => "info",
1 => "info",
2 => "debug",
_ => "trace",
}
};
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_ansi(!no_color)
.with_writer(std::io::stderr)
.without_time()
.with_target(false)
.init();
}
pub(crate) fn load_config(
path: &std::path::Path,
) -> Result<(
crate::core::config::WorkspaceConfig,
Vec<crate::core::config::ResolvedCrateConfig>,
)> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read config: {}", path.display()))?;
crate::core::config::detect_legacy_keys(&content).with_context(|| {
format!(
"legacy schema detected in {} — run `alef migrate` to update automatically",
path.display()
)
})?;
let cfg: crate::core::config::NewAlefConfig =
toml::from_str(&content).with_context(|| format!("Failed to parse alef.toml ({})", path.display()))?;
let resolved = cfg
.resolve()
.with_context(|| format!("failed to resolve crates in {}", path.display()))?;
for resolved_cfg in &resolved {
crate::core::config::validation::validate_resolved(resolved_cfg)
.with_context(|| format!("invalid resolved config for crate `{}`", resolved_cfg.name))?;
}
Ok((cfg.workspace, resolved))
}
pub(crate) fn resolve_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, false)
}
pub(crate) fn resolve_doc_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, true)
}
pub(crate) fn resolve_readme_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
) -> Result<Vec<crate::core::config::Language>> {
resolve_languages_inner(config, filter, true)
}
pub(crate) fn resolve_test_languages(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
include_e2e: bool,
) -> Result<Vec<crate::core::config::Language>> {
match filter {
Some(langs) => {
let mut result = vec![];
for lang_str in langs {
let lang = parse_language(lang_str)?;
if config.languages.contains(&lang) || config.test.contains_key(&lang.to_string()) {
result.push(lang);
} else {
anyhow::bail!("Language '{lang_str}' not in config languages list or test configuration");
}
}
Ok(result)
}
None => {
let mut langs = config.languages.clone();
if include_e2e {
let mut extra_test_langs = vec![];
for (lang_str, test_config) in &config.test {
if test_config.e2e.is_none() {
continue;
}
let lang = parse_language(lang_str)
.with_context(|| format!("Invalid test language in alef.toml: {lang_str}"))?;
if !langs.contains(&lang) {
extra_test_langs.push(lang);
}
}
extra_test_langs.sort_by_key(|lang| lang.to_string());
for lang in extra_test_langs {
if !langs.contains(&lang) {
langs.push(lang);
}
}
}
Ok(langs)
}
}
}
pub(crate) fn resolve_languages_inner(
config: &crate::core::config::ResolvedCrateConfig,
filter: Option<&[String]>,
allow_rust: bool,
) -> Result<Vec<crate::core::config::Language>> {
match filter {
Some(langs) => {
let mut result = vec![];
for lang_str in langs {
let lang = parse_language(lang_str)?;
if config.languages.contains(&lang) || (allow_rust && lang == crate::core::config::Language::Rust) {
result.push(lang);
} else {
anyhow::bail!("Language '{lang_str}' not in config languages list");
}
}
Ok(result)
}
None => {
let mut langs = config.languages.clone();
if allow_rust && !langs.contains(&crate::core::config::Language::Rust) {
langs.push(crate::core::config::Language::Rust);
}
Ok(langs)
}
}
}
pub(crate) fn parse_language(lang_str: &str) -> Result<crate::core::config::Language> {
toml::Value::String(lang_str.to_string())
.try_into()
.with_context(|| format!("Unknown language: {lang_str}"))
}
pub(crate) fn format_languages(languages: &[crate::core::config::Language]) -> String {
languages.iter().map(|l| l.to_string()).collect::<Vec<_>>().join(", ")
}
pub(crate) fn verify_walk_multi(base_dir: &std::path::Path, inputs_hashes: &[String]) -> anyhow::Result<Vec<String>> {
if inputs_hashes.is_empty() {
return Ok(Vec::new());
}
if inputs_hashes.len() == 1 {
return verify_walk(base_dir, &inputs_hashes[0]);
}
const SKIP_DIRS: &[&str] = &[
".git",
".alef",
"target",
"node_modules",
"_build",
"deps",
"parsers",
"dist",
"dist-node",
"vendor",
".venv",
".cache",
".remote-cache",
"__pycache__",
"build",
"tmp",
"out",
".idea",
".vscode",
];
const SCAN_EXTENSIONS: &[&str] = &[
"rs", "py", "pyi", "ts", "tsx", "js", "mjs", "cjs", "rb", "rbs", "php", "phpstub", "go", "java", "cs", "ex",
"exs", "R", "r", "toml", "json", "md", "h", "c", "yaml", "yml",
];
let mut stale: Vec<String> = Vec::new();
let mut stack: Vec<std::path::PathBuf> = vec![base_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if SKIP_DIRS.contains(&name) || name.starts_with('.') {
continue;
}
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
let ext_ok = path
.extension()
.and_then(|e| e.to_str())
.map(|e| SCAN_EXTENSIONS.iter().any(|allowed| allowed.eq_ignore_ascii_case(e)))
.unwrap_or(false);
if !ext_ok {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
let Some(disk_hash) = crate::core::hash::extract_hash(&content) else {
continue;
};
let valid = inputs_hashes.iter().any(|ih| ih == &disk_hash);
if !valid {
stale.push(path.display().to_string());
}
}
}
stale.sort();
Ok(stale)
}
pub(crate) fn verify_walk(base_dir: &std::path::Path, inputs_hash: &str) -> anyhow::Result<Vec<String>> {
const SKIP_DIRS: &[&str] = &[
".git",
".alef",
"target",
"node_modules",
"_build",
"deps",
"parsers",
"dist",
"dist-node",
"vendor",
".venv",
".cache",
".remote-cache",
"__pycache__",
"build",
"tmp",
"out",
".idea",
".vscode",
];
const SCAN_EXTENSIONS: &[&str] = &[
"rs", "py", "pyi", "ts", "tsx", "js", "mjs", "cjs", "rb", "rbs", "php", "phpstub", "go", "java", "cs", "ex",
"exs", "R", "r", "toml", "json", "md", "h", "c", "yaml", "yml",
];
let mut stale: Vec<String> = Vec::new();
let mut stack: Vec<std::path::PathBuf> = vec![base_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if file_type.is_dir() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if SKIP_DIRS.contains(&name) || name.starts_with('.') {
continue;
}
stack.push(path);
continue;
}
if !file_type.is_file() {
continue;
}
let ext_ok = path
.extension()
.and_then(|e| e.to_str())
.map(|e| SCAN_EXTENSIONS.iter().any(|allowed| allowed.eq_ignore_ascii_case(e)))
.unwrap_or(false);
if !ext_ok {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
let Some(disk_hash) = crate::core::hash::extract_hash(&content) else {
continue;
};
if disk_hash != inputs_hash {
stale.push(path.display().to_string());
}
}
}
stale.sort();
Ok(stale)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::Language;
fn resolved_test_config() -> crate::core::config::ResolvedCrateConfig {
let cfg: crate::core::config::NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.test.python]
command = "pytest"
[crates.test.rust]
e2e = "cargo test"
"#,
)
.unwrap();
cfg.resolve().unwrap().remove(0)
}
#[test]
fn resolve_test_languages_allows_explicit_test_only_language() {
let config = resolved_test_config();
let langs = resolve_test_languages(&config, Some(&["rust".to_string()]), true).unwrap();
assert_eq!(langs, vec![Language::Rust]);
}
#[test]
fn resolve_test_languages_appends_e2e_only_languages() {
let config = resolved_test_config();
let langs = resolve_test_languages(&config, None, true).unwrap();
assert_eq!(langs, vec![Language::Python, Language::Rust]);
}
#[test]
fn resolve_test_languages_omits_e2e_only_languages_without_e2e() {
let config = resolved_test_config();
let langs = resolve_test_languages(&config, None, false).unwrap();
assert_eq!(langs, vec![Language::Python]);
}
}