luauperf 0.1.6

A static performance linter for Luau
mod config;
mod fix;
mod ignore;
mod lint;
mod rules;
mod scanner;
mod visit;

use std::{env, path::PathBuf, process, time::Instant};

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();

    if args.is_empty() {
        usage();
        process::exit(1);
    }

    match args[0].as_str() {
        "--help" | "-h" => return usage(),
        "--list-rules" => return rules::print_all(),
        "--init" => return config::write_default(),
        "--explain" => {
            if let Some(rule_id) = args.get(1) {
                return rules::explain(rule_id);
            } else {
                eprintln!("\x1b[31merror\x1b[0m: --explain requires a rule ID");
                eprintln!("Example: luauperf --explain roblox::deprecated_wait");
                process::exit(1);
            }
        }
        _ => {}
    }

    let path = PathBuf::from(&args[0]);
    let json = has_flag(&args, "--format", "json");
    let fix_mode = args.iter().any(|a| a == "--fix");
    let dry_run = args.iter().any(|a| a == "--dry-run");
    let quiet = args.iter().any(|a| a == "--quiet" || a == "-q");
    let max_warnings = parse_usize_flag(&args, "--max-warnings");

    if !path.exists() {
        eprintln!("\x1b[31merror\x1b[0m: '{}' does not exist", path.display());
        process::exit(1);
    }

    let cfg = config::load(&path);
    let level = parse_level_flag(&args).unwrap_or_else(|| {
        cfg.level.unwrap_or(lint::Level::Default)
    });
    let t = Instant::now();
    let result = scanner::run(&path, &cfg, fix_mode, dry_run, level);
    let elapsed = t.elapsed();

    let scanner::RunResult { n_files, diags, files_fixed, fixes_applied, parse_errors } = result;

    if fix_mode && dry_run {
        let fixable = diags.iter().filter(|d| d.fix.is_some()).count();
        eprintln!(
            "\n \x1b[1;33mDry run\x1b[0m: {} fixable {} found (no changes written)",
            fixable,
            if fixable == 1 { "issue" } else { "issues" },
        );
    } else if fix_mode && fixes_applied > 0 {
        eprintln!(
            "\n \x1b[1;32mFixed\x1b[0m {} {} in {} {}",
            fixes_applied,
            if fixes_applied == 1 { "issue" } else { "issues" },
            files_fixed,
            if files_fixed == 1 { "file" } else { "files" },
        );
    }

    if json {
        lint::print_json(&diags);
    } else if quiet {
        lint::print_summary(&diags, n_files, elapsed, parse_errors);
    } else if !diags.is_empty() {
        lint::print_report(&diags, &path, n_files, elapsed, parse_errors);
    } else if fix_mode {
        eprintln!(
            "\n {} files checked · no remaining issues · {:.2}s",
            n_files,
            elapsed.as_secs_f64()
        );
    } else {
        lint::print_report(&diags, &path, n_files, elapsed, parse_errors);
    }

    if diags.iter().any(|d| d.severity == lint::Severity::Error) {
        process::exit(1);
    }

    if let Some(max) = max_warnings {
        let warn_count = diags.iter().filter(|d| d.severity == lint::Severity::Warn).count();
        if warn_count > max {
            eprintln!(
                "\n \x1b[1;31merror\x1b[0m: {} warnings exceed --max-warnings {}",
                warn_count, max,
            );
            process::exit(1);
        }
    }
}

fn has_flag(args: &[String], flag: &str, value: &str) -> bool {
    args.windows(2).any(|w| w[0] == flag && w[1] == value)
}

fn parse_usize_flag(args: &[String], flag: &str) -> Option<usize> {
    args.windows(2)
        .find(|w| w[0] == flag)
        .and_then(|w| w[1].parse().ok())
}

fn parse_level_flag(args: &[String]) -> Option<lint::Level> {
    args.windows(2)
        .find(|w| w[0] == "--level")
        .map(|w| match w[1].as_str() {
            "default" | "1" => lint::Level::Default,
            "strict" | "2" => lint::Level::Strict,
            "pedantic" | "3" | "all" => lint::Level::Pedantic,
            other => {
                eprintln!(
                    "\x1b[31merror\x1b[0m: unknown level '{}' (expected: default, strict, pedantic)",
                    other
                );
                process::exit(1);
            }
        })
}

fn usage() {
    eprintln!("luauperf - static performance analyzer for Luau\n");
    eprintln!("usage: luauperf <path> [options]\n");
    eprintln!("  --level <level>    set lint aggressiveness (default, strict, pedantic)");
    eprintln!("  --fix              auto-fix safely fixable issues");
    eprintln!("  --fix --dry-run    show what --fix would change without writing");
    eprintln!("  --format json      JSON output");
    eprintln!("  --quiet, -q        only show summary, no individual diagnostics");
    eprintln!("  --max-warnings N   exit 1 if more than N warnings");
    eprintln!("  --list-rules       show all rules");
    eprintln!("  --explain <id>     explain a specific rule");
    eprintln!("  --init             create default luauperf.toml");
    eprintln!("  -h, --help         this message");
}