#![cfg_attr(coverage_nightly, coverage(off))]
mod baseline;
mod display;
mod explain;
mod formatting;
mod history;
mod quality_gates;
mod subcommands;
#[cfg(feature = "viz")]
mod viz;
use crate::cli::commands::TdgCommand;
use crate::cli::TdgOutputFormat;
use crate::tdg::{Grade, TdgAnalyzer, TdgConfig};
use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(not(feature = "git-lib"))]
use std::process::Command;
#[cfg(test)]
#[allow(unused_imports)]
pub(crate) use display::{display_gate_result_table, format_explain_output, format_history_output};
#[cfg(test)]
#[allow(unused_imports)]
pub(crate) use formatting::{
format_comparison, format_tdg_output, format_tdg_score, write_tdg_output,
};
#[cfg(test)]
#[allow(unused_imports)]
pub(crate) use quality_gates::{execute_tdg_analysis, validate_minimum_grade};
fn discover_git_workdir(path: &Path) -> Option<PathBuf> {
#[cfg(feature = "git-lib")]
{
git2::Repository::discover(path)
.ok()
.and_then(|repo| repo.workdir().map(Path::to_path_buf))
}
#[cfg(not(feature = "git-lib"))]
{
Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(path)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()))
}
}
pub struct TdgCommandConfig {
pub path: PathBuf,
pub command: Option<TdgCommand>,
pub format: TdgOutputFormat,
pub config: Option<PathBuf>,
pub quiet: bool,
pub include_components: bool,
pub min_grade: Option<String>,
pub output: Option<PathBuf>,
pub with_git_context: bool,
pub explain: bool,
pub threshold: u32,
pub baseline: Option<String>,
pub viz: bool,
pub viz_theme: String,
}
fn should_skip_path(config: &TdgCommandConfig) -> bool {
if !config.path.is_file() {
return false;
}
is_test_or_bench_path(&config.path)
}
fn is_test_or_bench_path(path: &Path) -> bool {
use std::path::Component;
let resolved = fs::canonicalize(path)
.or_else(|_| std::path::absolute(path))
.unwrap_or_else(|_| path.to_path_buf());
resolved
.parent()
.into_iter()
.flat_map(Path::components)
.any(|component| {
matches!(component, Component::Normal(name) if name == "tests" || name == "benches")
})
}
fn skipped_output(config: &TdgCommandConfig) -> Result<String> {
let reason = "test-or-bench file: TDG does not grade test sources";
match config.format {
TdgOutputFormat::Json => Ok(serde_json::to_string_pretty(&serde_json::json!({
"file": config.path.display().to_string(),
"analyzed": false,
"skipped": true,
"skip_reason": reason,
"score": serde_json::Value::Null,
"grade": serde_json::Value::Null,
"not_measured": ["score", "grade"],
}))?),
TdgOutputFormat::Sarif => Ok(serde_json::to_string_pretty(&serde_json::json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": { "driver": {
"name": "pmat-tdg",
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
"version": env!("CARGO_PKG_VERSION"),
}},
"properties": {
"analyzed": false,
"skip_reason": reason,
"not_measured": ["score", "grade"],
},
"results": []
}]
}))?),
TdgOutputFormat::Table | TdgOutputFormat::Markdown => {
Ok(format!("Skipping test file: {}", config.path.display()))
}
}
}
fn setup_git_context(analyzer: &mut TdgAnalyzer, config: &TdgCommandConfig) {
if !config.with_git_context {
return;
}
let search_path = if config.path.is_file() {
config.path.parent().unwrap_or(&config.path)
} else {
&config.path
};
let git_context = discover_git_workdir(search_path)
.and_then(|workdir| crate::models::git_context::GitContext::try_from_current_dir(&workdir));
analyzer.set_git_context(git_context);
}
fn load_tdg_configuration(config: &TdgCommandConfig) -> Result<TdgConfig> {
if let Some(config_path) = &config.config {
let config_content = fs::read_to_string(config_path)?;
Ok(toml::from_str(&config_content)?)
} else {
Ok(TdgConfig::default())
}
}
fn is_analyzable_file(path: &Path) -> bool {
if let Some(ext) = path.extension() {
matches!(
ext.to_str(),
Some(
"rs" | "py"
| "js"
| "ts"
| "tsx"
| "jsx"
| "java"
| "c"
| "cpp"
| "h"
| "hpp"
| "go"
| "rb"
| "php"
| "swift"
| "kt"
| "kts"
)
)
} else {
false
}
}
fn parse_grade(s: &str) -> Result<crate::tdg::Grade> {
use crate::tdg::Grade;
match s.to_uppercase().as_str() {
"A+" => Ok(Grade::APlus),
"A" => Ok(Grade::A),
"A-" => Ok(Grade::AMinus),
"B+" => Ok(Grade::BPlus),
"B" => Ok(Grade::B),
"B-" => Ok(Grade::BMinus),
"C+" => Ok(Grade::CPlus),
"C" => Ok(Grade::C),
"C-" => Ok(Grade::CMinus),
"D" => Ok(Grade::D),
"F" => Ok(Grade::F),
_ => Err(anyhow::anyhow!(
"Invalid grade: {s}. Valid grades: A+, A, A-, B+, B, B-, C+, C, C-, D, F"
)),
}
}
fn format_grade(grade: Grade) -> String {
match grade {
Grade::APlus => "A+",
Grade::A => "A",
Grade::AMinus => "A-",
Grade::BPlus => "B+",
Grade::B => "B",
Grade::BMinus => "B-",
Grade::CPlus => "C+",
Grade::C => "C",
Grade::CMinus => "C-",
Grade::D => "D",
Grade::F => "F",
}
.to_string()
}
fn truncate_string(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
format!("{:width$}", s, width = max_len)
} else {
batuta_common::display::truncate_str(s, max_len)
}
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_tdg_command(config: TdgCommandConfig) -> Result<()> {
if should_skip_path(&config) {
if !config.quiet {
let output_str = skipped_output(&config)?;
formatting::write_tdg_output(&output_str, &config)?;
}
return Ok(());
}
let tdg_config = load_tdg_configuration(&config)?;
let mut analyzer = TdgAnalyzer::with_storage(tdg_config)?;
setup_git_context(&mut analyzer, &config);
if let Some(ref cmd) = config.command {
return subcommands::handle_tdg_subcommand(cmd.clone(), &analyzer, &config).await;
}
if config.explain {
return explain::handle_explain_mode(&analyzer, &config).await;
}
#[cfg(feature = "viz")]
if config.viz {
return viz::handle_viz_mode(&analyzer, &config).await;
}
let analysis = quality_gates::run_tdg_analysis(&analyzer, &config).await?;
quality_gates::validate_minimum_grade(&analysis.score, &config)?;
let git_context = analyzer.get_git_context();
let output_str = formatting::format_tdg_analysis(&analysis, git_context, &config)?;
formatting::write_tdg_output(&output_str, &config)?;
Ok(())
}
#[cfg(test)]
#[path = "../tdg_handlers_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "skip_path_tests.rs"]
mod skip_path_tests;