use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::Result;
use serde::Deserialize;
use crate::offense::OffenseKind;
#[derive(Debug, Deserialize, Default)]
struct RawConfig {
#[serde(default)]
speedups: HashMap<String, bool>,
#[serde(default)]
exclude_paths: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
disabled_offenses: Vec<OffenseKind>,
pub exclude_patterns: Vec<String>,
}
impl Config {
pub fn load(start_dir: &Path) -> Result<Self> {
match find_config_file(start_dir) {
Some(path) => Self::from_file(&path),
None => Ok(Self::default()),
}
}
pub fn from_file(path: &Path) -> Result<Self> {
let contents = std::fs::read_to_string(path)?;
Self::parse_yaml(&contents)
}
pub fn parse_yaml(yaml: &str) -> Result<Self> {
let raw: Option<RawConfig> = serde_yaml::from_str(yaml)?;
let raw = raw.unwrap_or_default();
let disabled_offenses = OffenseKind::all()
.iter()
.filter(|kind| raw.speedups.get(kind.config_key()) == Some(&false))
.copied()
.collect();
Ok(Self {
disabled_offenses,
exclude_patterns: raw.exclude_paths,
})
}
pub fn is_enabled(&self, kind: OffenseKind) -> bool {
!self.disabled_offenses.contains(&kind)
}
}
fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
let mut dir = start_dir.to_path_buf();
loop {
let preferred = dir.join(".rubyfast.yml");
if preferred.is_file() {
return Some(preferred);
}
let fallback = dir.join(".fasterer.yml");
if fallback.is_file() {
return Some(fallback);
}
if !dir.pop() {
return None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_enables_all() {
let config = Config::default();
for kind in OffenseKind::all() {
assert!(config.is_enabled(*kind));
}
}
#[test]
fn empty_yaml_enables_all() {
let config = Config::parse_yaml("").unwrap();
for kind in OffenseKind::all() {
assert!(config.is_enabled(*kind));
}
}
#[test]
fn disable_specific_offense() {
let yaml = "speedups:\n for_loop_vs_each: false\n";
let config = Config::parse_yaml(yaml).unwrap();
assert!(!config.is_enabled(OffenseKind::ForLoopVsEach));
assert!(config.is_enabled(OffenseKind::ShuffleFirstVsSample));
}
#[test]
fn exclude_paths_parsed() {
let yaml = "exclude_paths:\n - 'vendor/**/*.rb'\n - 'spec/**/*.rb'\n";
let config = Config::parse_yaml(yaml).unwrap();
assert_eq!(config.exclude_patterns.len(), 2);
assert_eq!(config.exclude_patterns[0], "vendor/**/*.rb");
}
#[test]
fn all_speedups_true_enables_all() {
let yaml = "speedups:\n for_loop_vs_each: true\n gsub_vs_tr: true\n";
let config = Config::parse_yaml(yaml).unwrap();
assert!(config.is_enabled(OffenseKind::ForLoopVsEach));
assert!(config.is_enabled(OffenseKind::GsubVsTr));
}
#[test]
fn unknown_speedup_key_ignored() {
let yaml = "speedups:\n made_up_rule: false\n";
let config = Config::parse_yaml(yaml).unwrap();
for kind in OffenseKind::all() {
assert!(config.is_enabled(*kind));
}
}
#[test]
fn invalid_yaml_returns_error() {
let result = Config::parse_yaml("speedups: [invalid");
assert!(result.is_err());
}
#[test]
fn load_rubyfast_yml() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".rubyfast.yml"),
"speedups:\n gsub_vs_tr: false\n",
)
.unwrap();
let config = Config::load(dir.path()).unwrap();
assert!(!config.is_enabled(OffenseKind::GsubVsTr));
}
#[test]
fn load_fasterer_yml_fallback() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".fasterer.yml"),
"speedups:\n for_loop_vs_each: false\n",
)
.unwrap();
let config = Config::load(dir.path()).unwrap();
assert!(!config.is_enabled(OffenseKind::ForLoopVsEach));
}
#[test]
fn load_no_config_returns_default() {
let dir = tempfile::TempDir::new().unwrap();
let config = Config::load(dir.path()).unwrap();
for kind in OffenseKind::all() {
assert!(config.is_enabled(*kind));
}
}
#[test]
fn from_file_nonexistent_returns_error() {
let result = Config::from_file(std::path::Path::new("/nonexistent/.rubyfast.yml"));
assert!(result.is_err());
}
#[test]
fn load_walks_up_parent_directories() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(
dir.path().join(".rubyfast.yml"),
"speedups:\n gsub_vs_tr: false\n",
)
.unwrap();
let sub = dir.path().join("nested").join("deep");
std::fs::create_dir_all(&sub).unwrap();
let config = Config::load(&sub).unwrap();
assert!(!config.is_enabled(OffenseKind::GsubVsTr));
}
}