swt 5.0.0-rc.11

🍬 Sweet: A blazing-fast code health and architecture analyzer.
Documentation
//! Sweet CLI: Blazing-fast code health analyzer.

#![deny(clippy::pedantic)]

use clap::{Parser, Subcommand};
use console::style;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use swt::analyzer::AnalysisEngine;
use swt::report::json::write_json_report;
use swt::report::print_reports;
use swt::uncomment::remove_comments;
use swt::update::{check_for_updates, handle_update};
use swt::{Config, FileReport};

const ASCII: &str = r"
                            __ 
   ______      _____  ___  / /_
  / ___/ | /| / / _ \/ _ \/ __/
 /__  /| |/ |/ /  __/  __/ /_  
/____/ |__/|__/\___/\___/\__/  ";

/// Sweet CLI: High-performance code health and architectural integrity analyzer.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Subcommand to execute. If omitted, performs a standard health check on the path.
    #[command(subcommand)]
    command: Option<Commands>,

    /// Path to analyze (default: current directory).
    #[arg(default_value = ".")]
    path: PathBuf,

    /// Output report in JSON format.
    #[allow(clippy::option_option)]
    #[arg(long, value_name = "FILE")]
    json: Option<Option<PathBuf>>,

    /// Minimal output for CI environments.
    #[arg(short, long)]
    quiet: bool,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Check for updates and install if available.
    Update,
    /// Check for updates without installing.
    CheckUpdates,
    /// Detailed inspection of code duplication and repetition.
    Inspect {
        /// Path to inspect (default: current directory).
        #[arg(default_value = ".")]
        path: PathBuf,

        /// Enable project-wide repetition analysis across different files.
        #[arg(long)]
        cross_file: bool,
    },
    /// Strip comments from a specific file.
    Uncomment {
        /// File to strip comments from.
        #[arg(value_name = "FILE")]
        path: PathBuf,

        /// Remove even doc comments (///, /**).
        #[arg(long, short)]
        aggressive: bool,
    },
}

fn main() -> ExitCode {
    let args = Args::parse();

    match args.command {
        Some(Commands::Update) => {
            return match handle_update() {
                Ok(()) => ExitCode::SUCCESS,
                Err(e) => {
                    eprintln!("{} {}", style("Error updating Sweet:").red().bold(), e);
                    ExitCode::FAILURE
                }
            };
        }
        Some(Commands::CheckUpdates) => {
            check_for_updates();
            return ExitCode::SUCCESS;
        }
        Some(Commands::Inspect { path, cross_file }) => {
            return run_analysis(&path, args.json.as_ref(), args.quiet, true, cross_file);
        }
        Some(Commands::Uncomment { path, aggressive }) => {
            if handle_uncomment(&path, aggressive) {
                return ExitCode::SUCCESS;
            }
            return ExitCode::FAILURE;
        }
        None => {}
    }

    run_analysis(&args.path, args.json.as_ref(), args.quiet, false, false)
}

fn run_analysis(
    path: &Path,
    #[allow(clippy::option_option)] json: Option<&Option<PathBuf>>,
    quiet: bool,
    inspect: bool,
    cross_file: bool,
) -> ExitCode {
    let config = match Config::load(path) {
        Ok(c) => c,
        Err(e) => {
            let report = miette::Report::from(e);
            eprintln!("{report:?}");
            return ExitCode::FAILURE;
        }
    };

    let engine = AnalysisEngine::new(path.to_path_buf(), config);

    if !quiet && json.is_none() {
        show_branding();
    }

    let reports = engine.run(quiet, json.is_none(), inspect, cross_file);

    if reports.is_empty() {
        if !quiet {
            println!(
                "\n{}",
                style(" 📭 No supported files found.").yellow().bold()
            );
        }
        return ExitCode::SUCCESS;
    }

    let bitter_count = reports.iter().filter(|r| !r.is_sweet).count();

    if let Some(json_opt) = json {
        handle_json_reporting(&reports, json_opt.as_ref(), quiet);
    } else {
        print_reports(&reports, quiet, None);
    }

    if bitter_count > 0 {
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}

fn handle_json_reporting(reports: &[FileReport], json_opt: Option<&PathBuf>, _quiet: bool) {
    use serde_json::json;
    use std::collections::BTreeMap;

    let mut map = BTreeMap::new();

    for report in reports {
        let mut problems = Vec::new();

        for issue in &report.issues {
            problems.push(json!({
                "line": issue.line,
                "message": issue.message
            }));
        }

        for dup in &report.duplicates {
            problems.push(json!({
                "line": dup.line,
                "message": format!(
                    "Duplicate found. Also in: {}",
                    dup.occurrences
                        .iter()
                        .map(|(p, l)| format!("{}:{}", p.display(), l))
                        .collect::<Vec<_>>()
                        .join(", ")
                )
            }));
        }

        if !problems.is_empty() {
            let value = if problems.len() == 1 {
                problems[0].clone()
            } else {
                json!(problems)
            };
            map.insert(report.path.to_string_lossy().to_string(), value);
        }
    }

    if let Some(path) = json_opt {
        write_json_report(&map, path);
    } else if let Ok(json) = serde_json::to_string_pretty(&map) {
        println!("{json}");
    }
}

fn handle_uncomment(path: &Path, aggressive: bool) -> bool {
    match fs::read_to_string(path) {
        Ok(content) => {
            let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("");
            let clean = remove_comments(&content, extension, aggressive);
            if fs::write(path, clean).is_ok() {
                println!("{}", style("Uncommented!").cyan().bold());
                true
            } else {
                eprintln!("{}", style("Error: Could not write to file").red());
                false
            }
        }
        Err(e) => {
            eprintln!("Error: Could not read file {}: {}", path.display(), e);
            false
        }
    }
}

fn show_branding() {
    println!("{}", style(ASCII).magenta().bold());
    println!(
        "\n{}",
        style("— A blazing-fast code health analyzer :)")
            .italic()
            .cyan()
    );
}