use std::path::PathBuf;
use std::process;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use clap::{Parser, ValueEnum};
mod analyze;
mod composer;
mod config;
mod format;
mod report;
use config::Config;
#[derive(Parser, Debug)]
#[command(name = "mir", version, about, long_about = None)]
struct Cli {
#[arg(value_name = "PATH")]
paths: Vec<PathBuf>,
#[arg(long, value_enum, default_value = "text")]
format: OutputFormat,
#[arg(long)]
show_info: bool,
#[arg(short, long)]
quiet: bool,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
no_progress: bool,
#[arg(short = 'j', long)]
threads: Option<usize>,
#[arg(long)]
stats: bool,
#[arg(long, value_name = "X.Y")]
php_version: Option<String>,
#[arg(long, value_name = "DIR")]
cache_dir: Option<PathBuf>,
#[arg(short = 'c', long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(long, value_name = "FILE")]
baseline: Option<PathBuf>,
#[arg(long, value_name = "1-8")]
error_level: Option<u8>,
#[arg(long, value_name = "FILE", num_args = 0..=1, default_missing_value = "psalm-baseline.xml")]
set_baseline: Option<PathBuf>,
#[arg(long)]
update_baseline: bool,
#[arg(long)]
ignore_baseline: bool,
#[arg(long)]
no_cache: bool,
#[arg(long)]
clear_cache: bool,
#[arg(long)]
find_dead_code: bool,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
enum OutputFormat {
Text,
Json,
GithubActions,
Junit,
Sarif,
}
fn main() {
let cli = Cli::parse();
if cli.clear_cache {
clear_cache(&cli);
}
let (mut config, config_base) = load_config(&cli);
if let Some(level) = cli.error_level {
config.error_level = level.clamp(1, 8);
}
if let Some(ver) = &cli.php_version {
config.php_version = Some(ver.clone());
}
if let Some(n) = cli.threads {
rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build_global()
.ok();
}
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let composer_root = resolve_composer_root(&cli, &cwd);
let baseline = report::load_baseline(&cli, &config);
let (files, result, elapsed) = if let Some(ref root) = composer_root {
analyze::run_composer_flow(&cli, &config, &config_base, root)
} else {
analyze::run_plain_flow(&cli, &config, &config_base)
};
report::run_output(&cli, &config, &files, result, baseline, elapsed);
}
fn clear_cache(cli: &Cli) -> ! {
let cache_dir = cli.cache_dir.clone().or_else(analyze::default_cache_dir);
if let Some(cache_dir) = cache_dir {
let cache_file = cache_dir.join("cache.json");
if cache_file.exists() {
if let Err(e) = std::fs::remove_file(&cache_file) {
eprintln!("mir: failed to remove cache file: {}", e);
process::exit(1);
}
}
if !cli.quiet {
eprintln!("mir: cache cleared ({})", cache_dir.display());
}
} else {
eprintln!("mir: --clear-cache requires --cache-dir (no platform cache dir found)");
process::exit(2);
}
process::exit(0);
}
fn load_config(cli: &Cli) -> (Config, PathBuf) {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
if let Some(path) = &cli.config {
let config_base = path
.parent()
.map_or_else(|| cwd.clone(), |p| p.to_path_buf());
let config = match Config::from_file(path) {
Ok(c) => c,
Err(e) => {
eprintln!("mir: config error: {e}");
process::exit(2);
}
};
return (config, config_base);
}
if let Some(found) = Config::find(&cwd) {
let config_base = found
.parent()
.map_or_else(|| cwd.clone(), |p| p.to_path_buf());
let config = match Config::from_file(&found) {
Ok(c) => {
if !cli.quiet {
eprintln!("mir: using config {}", found.display());
}
c
}
Err(e) => {
eprintln!("mir: config error in {}: {}", found.display(), e);
process::exit(2);
}
};
return (config, config_base);
}
(Config::default(), cwd)
}
fn resolve_composer_root(cli: &Cli, cwd: &std::path::Path) -> Option<PathBuf> {
if cli.paths.is_empty() {
if cwd.join("composer.json").exists() {
Some(cwd.to_path_buf())
} else {
None
}
} else if cli.paths.len() == 1 {
composer::find_composer_root_for_path(&cli.paths[0])
} else {
None
}
}