use std::{
fs,
io::{self, Read, Write},
path::{Path, PathBuf},
process,
};
use clap::Parser;
use masterror::AppError;
use rust_diff_analyzer::{
analysis::map_changes,
classifier::rules::{calculate_weight, exceeded_per_type_limits},
config::{Config, OutputFormat},
error::FileReadError,
git::parse_diff,
output::format_output,
types::{AnalysisResult, SemanticUnitKind, Summary},
};
#[derive(Parser, Debug)]
#[command(name = "rust-diff-analyzer")]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short, long)]
diff_file: Option<PathBuf>,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(short, long, value_enum)]
format: Option<OutputFormatArg>,
#[arg(long)]
max_units: Option<usize>,
#[arg(long)]
max_score: Option<usize>,
#[arg(long)]
max_lines: Option<usize>,
#[arg(short, long, default_value = ".")]
base_dir: PathBuf,
#[arg(long)]
no_fail: bool,
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum OutputFormatArg {
Github,
Json,
Human,
Comment,
}
fn main() {
if let Err(e) = run() {
eprintln!("Error: {}", e);
process::exit(1);
}
}
fn run() -> Result<(), AppError> {
let args = Args::parse();
let mut config = if let Some(config_path) = &args.config {
Config::from_file(config_path)?
} else {
let default_path = Path::new(".rust-diff-analyzer.toml");
if default_path.exists() {
Config::from_file(default_path)?
} else {
Config::default()
}
};
if let Some(format) = args.format {
config.output.format = match format {
OutputFormatArg::Github => OutputFormat::Github,
OutputFormatArg::Json => OutputFormat::Json,
OutputFormatArg::Human => OutputFormat::Human,
OutputFormatArg::Comment => OutputFormat::Comment,
};
}
if let Some(max_units) = args.max_units {
config.limits.max_prod_units = max_units;
}
if let Some(max_score) = args.max_score {
config.limits.max_weighted_score = max_score;
}
if let Some(max_lines) = args.max_lines {
config.limits.max_prod_lines = Some(max_lines);
}
config.validate()?;
let diff_content = read_diff(&args.diff_file)?;
let file_diffs = parse_diff(&diff_content)?;
let base_dir = args.base_dir.clone();
let map_result = map_changes(&file_diffs, &config, |path| {
let full_path = base_dir.join(path);
fs::read_to_string(full_path)
})?;
let changes = map_result.changes;
let scope = map_result.scope;
let mut summary = Summary::default();
for change in &changes {
if change.classification.is_production() {
match change.unit.kind {
SemanticUnitKind::Function => summary.prod_functions += 1,
SemanticUnitKind::Struct | SemanticUnitKind::Enum => summary.prod_structs += 1,
_ => summary.prod_other += 1,
}
summary.prod_lines_added += change.lines_added;
summary.prod_lines_removed += change.lines_removed;
summary.weighted_score += calculate_weight(&change.unit, &config);
} else {
summary.test_units += 1;
summary.test_lines_added += change.lines_added;
summary.test_lines_removed += change.lines_removed;
}
}
summary.exceeds_limit = summary.total_prod_units() > config.limits.max_prod_units
|| summary.weighted_score > config.limits.max_weighted_score
|| config
.limits
.max_prod_lines
.map(|limit| summary.prod_lines_added > limit)
.unwrap_or(false)
|| !exceeded_per_type_limits(&changes, &config).is_empty();
let result = AnalysisResult::new(changes, summary, scope);
let output = format_output(&result, &config)?;
print!("{}", output);
io::stdout()
.flush()
.map_err(|e| AppError::from(rust_diff_analyzer::error::IoError(e)))?;
if result.summary.exceeds_limit && config.limits.fail_on_exceed && !args.no_fail {
process::exit(1);
}
Ok(())
}
fn read_diff(path: &Option<PathBuf>) -> Result<String, AppError> {
match path {
Some(p) => fs::read(p)
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
.map_err(|e| AppError::from(FileReadError::new(p.clone(), e))),
None => {
let mut buffer = Vec::new();
io::stdin()
.read_to_end(&mut buffer)
.map_err(|e| AppError::from(rust_diff_analyzer::error::IoError(e)))?;
Ok(String::from_utf8_lossy(&buffer).into_owned())
}
}
}