pmat 3.15.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
//! Adapter layer to map existing inconsistent CLI/MCP parameters to uniform contracts
//!
//! This module provides backward compatibility by translating legacy parameter formats
//! to the current uniform contract system, ensuring seamless operation during API evolution.

#![cfg_attr(coverage_nightly, coverage(off))]
use super::{
    AnalyzeComplexityContract, AnalyzeDeadCodeContract, AnalyzeLintHotspotContract,
    AnalyzeSatdContract, AnalyzeTdgContract, BaseAnalysisContract, ContractValidation,
    OutputFormat,
};
use crate::cli::commands::AnalyzeCommands;
use anyhow::Result;
use std::path::{Path, PathBuf};

/// Adapter to handle current CLI inconsistencies
pub struct ContractAdapter;

/// Parameters for complexity command mapping
struct ComplexityMapParams<'a> {
    project_path: &'a Path,
    _file: &'a Option<PathBuf>,
    _files: &'a [PathBuf],
    output: &'a Option<PathBuf>,
    max_cyclomatic: &'a Option<u16>,
    max_cognitive: &'a Option<u16>,
    top_files: &'a usize,
    timeout: &'a u64,
}

/// Parameters for SATD command mapping
struct SatdMapParams<'a> {
    path: &'a Path,
    critical_only: &'a bool,
    strict: &'a bool,
    include_tests: &'a bool,
    output: &'a Option<PathBuf>,
    top_files: &'a usize,
    fail_on_violation: &'a bool,
    timeout: &'a u64,
}

/// Parameters for dead code command mapping
struct DeadCodeMapParams<'a> {
    path: &'a Path,
    top_files: &'a Option<usize>,
    include_unreachable: &'a bool,
    min_dead_lines: &'a usize,
    include_tests: &'a bool,
    output: &'a Option<PathBuf>,
    fail_on_violation: &'a bool,
    max_percentage: &'a f64,
    timeout: &'a u64,
}

/// Parameters for lint hotspot command mapping
struct LintHotspotMapParams<'a> {
    project_path: &'a Path,
    file: &'a Option<PathBuf>,
    max_density: &'a f64,
    min_confidence: &'a f64,
    enforce: &'a bool,
    dry_run: &'a bool,
    output: &'a Option<PathBuf>,
    top_files: &'a usize,
}

impl ContractAdapter {
    /// Generate deprecation warnings for inconsistent parameters
    #[must_use]
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn deprecation_warnings(_cmd: &AnalyzeCommands) -> Vec<String> {
        // No deprecation warnings - silently accept both --path and --project-path
        Vec::new()
    }

    /// Map existing CLI analyze commands to uniform contracts
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn from_cli(cmd: &AnalyzeCommands) -> Result<Box<dyn ContractValidation>> {
        match cmd {
            AnalyzeCommands::Complexity {
                path,
                project_path,
                file,
                files,
                output,
                max_cyclomatic,
                max_cognitive,
                top_files,
                timeout,
                ..
            } => {
                // Handle parameter migration: use new 'path' or deprecated 'project_path'
                let analysis_path = if let Some(deprecated_path) = project_path {
                    deprecated_path.clone()
                } else {
                    path.clone()
                };
                let params = ComplexityMapParams {
                    project_path: &analysis_path,
                    _file: file,
                    _files: files,
                    output,
                    max_cyclomatic,
                    max_cognitive,
                    top_files,
                    timeout,
                };
                Self::map_complexity_command(params)
            }
            AnalyzeCommands::Satd {
                path,
                critical_only,
                strict,
                include_tests,
                output,
                top_files,
                fail_on_violation,
                timeout,
                ..
            } => {
                let params = SatdMapParams {
                    path,
                    critical_only,
                    strict,
                    include_tests,
                    output,
                    top_files,
                    fail_on_violation,
                    timeout,
                };
                Self::map_satd_command(params)
            }
            AnalyzeCommands::DeadCode {
                path,
                top_files,
                include_unreachable,
                min_dead_lines,
                include_tests,
                output,
                fail_on_violation,
                max_percentage,
                timeout,
                ..
            } => {
                let params = DeadCodeMapParams {
                    path,
                    top_files,
                    include_unreachable,
                    min_dead_lines,
                    include_tests,
                    output,
                    fail_on_violation,
                    max_percentage,
                    timeout,
                };
                Self::map_dead_code_command(params)
            }
            AnalyzeCommands::Tdg {
                path,
                threshold,
                top_files,
                include_components,
                output,
                critical_only,
                ..
            } => Self::map_tdg_command(
                path,
                threshold,
                top_files,
                include_components,
                output,
                critical_only,
            ),
            AnalyzeCommands::LintHotspot {
                path,
                project_path,
                file,
                max_density,
                min_confidence,
                enforce,
                dry_run,
                output,
                top_files,
                ..
            } => {
                let resolved_path = if let Some(deprecated_path) = project_path {
                    deprecated_path.clone()
                } else {
                    path.clone()
                };
                let params = LintHotspotMapParams {
                    project_path: &resolved_path,
                    file,
                    max_density,
                    min_confidence,
                    enforce,
                    dry_run,
                    output,
                    top_files,
                };
                Self::map_lint_hotspot_command(params)
            }
            _ => {
                anyhow::bail!("Command not yet adapted to uniform contract")
            }
        }
    }

    fn map_complexity_command(params: ComplexityMapParams) -> Result<Box<dyn ContractValidation>> {
        let path = params.project_path;

        let contract = AnalyzeComplexityContract {
            base: BaseAnalysisContract {
                path: path.to_path_buf(),
                format: OutputFormat::Table,
                output: params.output.clone(),
                top_files: Some(*params.top_files),
                include_tests: false,
                timeout: *params.timeout,
            },
            max_cyclomatic: params.max_cyclomatic.map(u32::from),
            max_cognitive: params.max_cognitive.map(u32::from),
            max_halstead: None,
        };

        contract.validate()?;
        Ok(Box::new(contract))
    }

    fn map_satd_command(params: SatdMapParams) -> Result<Box<dyn ContractValidation>> {
        let contract = AnalyzeSatdContract {
            base: BaseAnalysisContract {
                path: params.path.to_path_buf(),
                format: OutputFormat::Summary,
                output: params.output.clone(),
                top_files: Some(*params.top_files),
                include_tests: *params.include_tests,
                timeout: *params.timeout,
            },
            severity: None,
            critical_only: *params.critical_only,
            strict: *params.strict,
            fail_on_violation: *params.fail_on_violation,
        };

        contract.validate()?;
        Ok(Box::new(contract))
    }

    fn map_dead_code_command(params: DeadCodeMapParams) -> Result<Box<dyn ContractValidation>> {
        let contract = AnalyzeDeadCodeContract {
            base: BaseAnalysisContract {
                path: params.path.to_path_buf(),
                format: OutputFormat::Summary,
                output: params.output.clone(),
                top_files: *params.top_files,
                include_tests: *params.include_tests,
                timeout: *params.timeout,
            },
            include_unreachable: *params.include_unreachable,
            min_dead_lines: *params.min_dead_lines,
            max_percentage: *params.max_percentage,
            fail_on_violation: *params.fail_on_violation,
        };

        contract.validate()?;
        Ok(Box::new(contract))
    }

    fn map_tdg_command(
        path: &Path,
        threshold: &f64,
        top_files: &usize,
        include_components: &bool,
        output: &Option<PathBuf>,
        critical_only: &bool,
    ) -> Result<Box<dyn ContractValidation>> {
        let contract = AnalyzeTdgContract {
            base: BaseAnalysisContract {
                path: path.to_path_buf(),
                format: OutputFormat::Table,
                output: output.clone(),
                top_files: Some(*top_files),
                include_tests: false,
                timeout: 60,
            },
            threshold: *threshold,
            include_components: *include_components,
            critical_only: *critical_only,
        };

        contract.validate()?;
        Ok(Box::new(contract))
    }

    fn map_lint_hotspot_command(
        params: LintHotspotMapParams,
    ) -> Result<Box<dyn ContractValidation>> {
        let contract = AnalyzeLintHotspotContract {
            base: BaseAnalysisContract {
                path: params.project_path.to_path_buf(),
                format: OutputFormat::Summary,
                output: params.output.clone(),
                top_files: Some(*params.top_files),
                include_tests: false,
                timeout: 60,
            },
            file: params.file.clone(),
            max_density: *params.max_density,
            min_confidence: *params.min_confidence,
            enforce: *params.enforce,
            dry_run: *params.dry_run,
        };

        contract.validate()?;
        Ok(Box::new(contract))
    }
}

/// Backward compatibility mapping for old parameter names
pub struct BackwardCompatibility;

impl BackwardCompatibility {
    /// Map old parameter names to new ones in JSON
    #[must_use]
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn map_json_params(mut params: serde_json::Value) -> serde_json::Value {
        if let Some(obj) = params.as_object_mut() {
            Self::map_project_path_to_path(obj);
            // Note: We no longer convert file to files as some tools (refactor_auto, lint_hotspot, entropy)
            // use `file` as their own parameter, not as `files` array.
            Self::map_format_types(obj);
        }
        params
    }

    /// Map old parameter names to new ones in JSON for complexity analysis
    /// This variant converts `file` to `files` array for tools that expect an array
    #[must_use]
    #[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
    pub fn map_json_params_for_complexity(mut params: serde_json::Value) -> serde_json::Value {
        if let Some(obj) = params.as_object_mut() {
            Self::map_project_path_to_path(obj);
            Self::map_file_to_files(obj);
            Self::map_format_types(obj);
        }
        params
    }

    fn map_project_path_to_path(obj: &mut serde_json::Map<String, serde_json::Value>) {
        if let Some(project_path) = obj.remove("project_path") {
            // Only set path if it's not already present
            if !obj.contains_key("path") {
                obj.insert("path".to_string(), project_path);
            }
        }
    }

    fn map_file_to_files(obj: &mut serde_json::Map<String, serde_json::Value>) {
        if let Some(file) = obj.remove("file") {
            if !obj.contains_key("files") {
                obj.insert("files".to_string(), serde_json::json!([file]));
            }
        }
    }

    fn map_format_types(obj: &mut serde_json::Map<String, serde_json::Value>) {
        if let Some(format) = obj.get_mut("format") {
            if let Some(fmt_str) = format.as_str() {
                let unified = Self::normalize_format_string(fmt_str);
                *format = serde_json::json!(unified);
            }
        }
    }

    fn normalize_format_string(fmt_str: &str) -> &'static str {
        match fmt_str {
            "human" | "pretty" | "summary" => "summary",
            "json" | "machine" => "json",
            "yaml" | "yml" => "yaml",
            "markdown" | "md" => "markdown",
            "csv" | "tsv" => "csv",
            _ => "table",
        }
    }
}

// Tests extracted to adapter_tests.rs for file health compliance (CB-040)
#[cfg(test)]
#[path = "adapter_tests.rs"]
mod tests;