use std::path::{Path, PathBuf};
use std::process;
use clap::Parser;
use ignore::WalkBuilder;
use oxicop::config::Config;
use oxicop::formatter::{create_formatter, Format};
use oxicop::registry::CopRegistry;
use oxicop::runner::Runner;
#[derive(Parser)]
#[command(name = "oxicop", about = "A blazing-fast Ruby linter", version)]
struct Cli {
#[arg(default_value = ".")]
paths: Vec<PathBuf>,
#[arg(short, long, default_value = "simple")]
format: String,
#[arg(long)]
only: Option<String>,
#[arg(long)]
except: Option<String>,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long)]
list: bool,
}
fn main() {
let cli = Cli::parse();
let mut registry = CopRegistry::new();
if cli.list {
list_cops(®istry);
return;
}
let config = if let Some(config_path) = cli.config {
match Config::from_file(&config_path) {
Ok(c) => Some(c),
Err(e) => {
eprintln!("Error loading config file: {}", e);
process::exit(1);
}
}
} else {
Config::find_and_load()
};
if let Some(ref cfg) = config {
apply_config_to_registry(&mut registry, cfg);
}
if let Some(ref only) = cli.only {
apply_only_filter(&mut registry, only);
}
if let Some(ref except) = cli.except {
apply_except_filter(&mut registry, except);
}
let ruby_files = discover_ruby_files(&cli.paths);
if ruby_files.is_empty() {
println!("No Ruby files found.");
return;
}
let runner = Runner::new(registry);
let result = runner.run(&ruby_files);
let format = cli.format.parse::<Format>().unwrap_or(Format::Simple);
let formatter = create_formatter(format);
let output = formatter.format(&result);
print!("{}", output);
if result.total_offenses > 0 {
process::exit(1);
}
}
fn list_cops(registry: &CopRegistry) {
println!("Available cops:\n");
let mut cops = registry.cop_names().to_vec();
cops.sort();
for cop_name in cops {
println!(" {}", cop_name);
}
println!("\nTotal: {} cops", registry.total_count());
}
fn apply_config_to_registry(registry: &mut CopRegistry, config: &Config) {
let cop_names: Vec<String> = registry.cop_names().iter().map(|&s| s.to_string()).collect();
for cop_name in &cop_names {
if let Some(enabled) = config.is_cop_enabled(cop_name) {
if !enabled {
registry.disable(cop_name);
} else {
registry.enable(cop_name);
}
}
}
}
fn apply_only_filter(registry: &mut CopRegistry, only: &str) {
let allowed: Vec<&str> = only.split(',').map(|s| s.trim()).collect();
let cop_names: Vec<String> = registry.cop_names().iter().map(|&s| s.to_string()).collect();
for cop_name in &cop_names {
if !allowed.contains(&cop_name.as_str()) {
registry.disable(cop_name);
}
}
}
fn apply_except_filter(registry: &mut CopRegistry, except: &str) {
let excluded: Vec<&str> = except.split(',').map(|s| s.trim()).collect();
for cop_name in excluded {
registry.disable(cop_name);
}
}
fn discover_ruby_files(paths: &[PathBuf]) -> Vec<PathBuf> {
let mut ruby_files = Vec::new();
for path in paths {
if path.is_file() {
if is_ruby_file(path) {
ruby_files.push(path.clone());
}
} else if path.is_dir() {
for entry in WalkBuilder::new(path).build().flatten() {
let entry_path = entry.path();
if entry_path.is_file() && is_ruby_file(entry_path) {
ruby_files.push(entry_path.to_path_buf());
}
}
}
}
ruby_files
}
fn is_ruby_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == "rb")
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_ruby_file() {
assert!(is_ruby_file(&PathBuf::from("test.rb")));
assert!(is_ruby_file(&PathBuf::from("path/to/file.rb")));
assert!(!is_ruby_file(&PathBuf::from("test.py")));
assert!(!is_ruby_file(&PathBuf::from("test.txt")));
assert!(!is_ruby_file(&PathBuf::from("no_extension")));
}
#[test]
fn test_apply_only_filter() {
let mut registry = CopRegistry::new();
apply_only_filter(&mut registry, "Layout/TrailingWhitespace,Style/StringLiterals");
let enabled = registry.enabled_cops();
assert!(enabled.len() <= 2);
for cop in enabled {
assert!(
cop.name() == "Layout/TrailingWhitespace"
|| cop.name() == "Style/StringLiterals"
);
}
}
#[test]
fn test_apply_except_filter() {
let mut registry = CopRegistry::new();
let initial_count = registry.enabled_count();
apply_except_filter(&mut registry, "Layout/TrailingWhitespace");
assert_eq!(registry.enabled_count(), initial_count - 1);
assert!(!registry.is_enabled("Layout/TrailingWhitespace"));
}
#[test]
fn test_apply_config_to_registry() {
use std::collections::HashMap;
let mut registry = CopRegistry::new();
let mut cops = HashMap::new();
cops.insert(
"Layout/TrailingWhitespace".to_string(),
oxicop::config::CopConfig {
enabled: Some(false),
severity: None,
},
);
let config = Config {
all_cops: None,
cops,
};
apply_config_to_registry(&mut registry, &config);
assert!(!registry.is_enabled("Layout/TrailingWhitespace"));
}
}