use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Deserialize, Default, Clone)]
pub struct Config {
#[serde(default)]
pub rules: Vec<Rule>,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Rule {
pub pattern: String,
pub warn: Option<usize>,
pub error: Option<usize>,
}
pub fn load_config(path: &Path) -> Config {
let Ok(content) = fs::read_to_string(path) else {
return Config::default();
};
serde_yaml::from_str(&content).unwrap_or_else(|e| {
eprintln!("Warning: failed to parse config {}: {}", path.display(), e);
Config::default()
})
}
pub struct ConfigResolver {
explicit: Option<PathBuf>,
config_name: String,
cache: HashMap<PathBuf, Option<Config>>,
}
impl ConfigResolver {
pub fn new(explicit: Option<PathBuf>, config_name: &str) -> Self {
Self { explicit, config_name: config_name.to_owned(), cache: HashMap::new() }
}
pub fn resolve(&mut self, file: &Path) -> Option<Config> {
if let Some(ref p) = self.explicit.clone() {
return self.load_cached(p.clone());
}
let dir = file.parent()?;
for ancestor in dir.ancestors() {
let candidate = ancestor.join(&self.config_name);
if candidate.exists() {
return self.load_cached(candidate);
}
}
None
}
fn load_cached(&mut self, path: PathBuf) -> Option<Config> {
self.cache
.entry(path.clone())
.or_insert_with(|| {
fs::read_to_string(&path).ok().and_then(|s| serde_yaml::from_str(&s).ok())
})
.clone()
}
}