mago 1.20.1

A comprehensive suite of PHP tooling inspired by Rust’s approach, providing parsing, linting, formatting, and more through a unified CLI and library interface.
use std::borrow::Cow;
use std::io::IsTerminal;

use clap::ColorChoice;
use diffy::PatchFormatter;

use mago_database::change::ChangeLog;
use mago_database::file::File;
use mago_linter::integration::IntegrationSet;
use mago_linter::settings::RulesSettings;
use mago_linter::settings::Settings;
use mago_orchestrator::Orchestrator;
use mago_orchestrator::OrchestratorConfiguration;

use crate::config::Configuration;
use crate::error::Error;

pub mod git;
pub mod logger;
pub mod progress;
pub mod version;

/// Determines whether colors should be used based on the color choice and environment.
///
/// This function considers:
/// - The explicit color choice (Always/Never/Auto)
/// - The FORCE_COLOR environment variable (if Auto) — any non-empty value forces
///   colors, except `FORCE_COLOR=0` which explicitly disables them.
/// - The NO_COLOR environment variable (if Auto) — any non-empty value (including
///   `"0"`) disables colors. An empty value has no effect.
/// - Whether stdout is a terminal (if Auto)
///
/// Priority (for Auto mode): FORCE_COLOR > NO_COLOR > TTY check
///
/// See: <https://force-color.org/> and <https://no-color.org/>
#[inline]
pub fn should_use_colors(color_choice: ColorChoice) -> bool {
    match color_choice {
        ColorChoice::Always => true,
        ColorChoice::Never => false,
        ColorChoice::Auto => {
            // FORCE_COLOR takes precedence.
            if let Some(force_color) = std::env::var_os("FORCE_COLOR")
                && !force_color.is_empty()
            {
                return force_color != "0";
            }

            // Then NO_COLOR: any non-empty value disables colors.
            if std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()) {
                return false;
            }

            std::io::stdout().is_terminal()
        }
    }
}

/// Configures global color settings based on the color choice.
///
/// This should be called early in the application to ensure consistent color behavior
/// across all crates that respect global color settings (like `colored`).
#[inline]
pub fn configure_colors(color_choice: ColorChoice) {
    let use_colors = should_use_colors(color_choice);
    colored::control::set_override(use_colors);
}

pub(crate) fn create_orchestrator<'a>(
    configuration: &'a Configuration,
    color_choice: ColorChoice,
    pedantic_linter: bool,
    use_progress_bars: bool,
    enable_diff: bool,
) -> Orchestrator<'a> {
    let glob = configuration.source.glob.to_database_settings();
    let linter_settings = if pedantic_linter {
        Settings {
            php_version: configuration.php_version,
            integrations: IntegrationSet::all(),
            rules: RulesSettings::default(),
            glob,
        }
    } else {
        Settings {
            php_version: configuration.php_version,
            integrations: IntegrationSet::from_slice(&configuration.linter.integrations),
            rules: configuration.linter.rules.clone(),
            glob,
        }
    };

    let orchestrator_config = OrchestratorConfiguration {
        php_version: configuration.php_version,
        parser_settings: configuration.parser.to_settings(),
        analyzer_settings: configuration.analyzer.to_settings(configuration.php_version, color_choice, enable_diff),
        linter_settings,
        guard_settings: configuration.guard.settings.clone(),
        formatter_settings: configuration.formatter.settings,
        disable_default_analyzer_plugins: configuration.analyzer.disable_default_plugins,
        analyzer_plugins: configuration.analyzer.plugins.clone(),
        use_progress_bars,
        use_colors: should_use_colors(color_choice),
        paths: configuration.source.paths.clone(),
        excludes: configuration.source.excludes.iter().map(|p| p.as_ref()).collect(),
        extensions: configuration.source.extensions.iter().map(|e| e.as_ref()).collect(),
        includes: configuration.source.includes.clone(),
        glob,
    };

    Orchestrator::new(orchestrator_config)
}

/// Processes the result of a modifying a single file.
///
/// This function compares the original file content with the newly modified content.
/// If there's a difference, it either prints a colorized diff to the console (if in
/// `dry_run` mode) or records an update operation in the provided [`ChangeLog`].
///
/// # Arguments
///
/// * `change_log`: The log where file updates are recorded when not in dry-run mode.
/// * `file`: The original file, used for comparison and context.
/// * `modified_contents`: The newly modified content.
/// * `dry_run`: If `true`, a diff is printed to standard output; otherwise, the
///   change is recorded in the `change_log`.
///
/// # Returns
///
/// Returns `true` if the file content was changed, `false` otherwise.
pub fn apply_update(
    change_log: &ChangeLog,
    file: &File,
    modified_contents: &str,
    dry_run: bool,
    color_choice: ColorChoice,
) -> Result<bool, Error> {
    if file.contents == modified_contents {
        return Ok(false);
    }

    if dry_run {
        let patch = diffy::create_patch(&file.contents, modified_contents);
        let mut formatter = PatchFormatter::new();

        if should_use_colors(color_choice) {
            formatter = formatter.with_color();
        };

        println!("diff of '{}':", file.name);
        println!("{}", formatter.fmt_patch(&patch));
    } else {
        change_log.update(file.id, Cow::Owned(modified_contents.to_owned()))?;
    }

    Ok(true)
}