use crate::error::{CliError, Result};
use crate::output;
use aprender::format::{lint_model_file, LintLevel, LintReport};
use colored::Colorize;
use std::path::Path;
#[provable_contracts_macros::contract(
"apr-cli-operations-v1",
equation = "side_effect_classification"
)]
pub(crate) fn run(file: &Path, json: bool, quiet: bool) -> Result<()> {
contract_pre_apr_model_validity!();
contract_pre_lint_model_conventions!();
if !file.exists() {
return Err(CliError::FileNotFound(file.to_path_buf()));
}
let report = lint_model_file(file).map_err(|e| CliError::ValidationFailed(e.to_string()))?;
if json {
return print_json_report(file, &report);
}
output::header("Model Lint");
println!(" Checking: {}", file.display().to_string().cyan());
println!();
display_report(&report, quiet);
if report.passed() {
contract_post_apr_model_validity!(&());
contract_post_lint_model_conventions!(&());
Ok(())
} else {
Err(CliError::ValidationFailed(format!(
"Lint failed with {} error(s), {} warning(s), {} info(s)",
report.error_count, report.warn_count, report.info_count
)))
}
}
#[allow(clippy::disallowed_methods)]
fn print_json_report(file: &Path, report: &LintReport) -> Result<()> {
let issues: Vec<serde_json::Value> = report
.issues
.iter()
.map(|issue| {
serde_json::json!({
"level": format!("{}", issue.level),
"category": issue.category.name(),
"message": issue.message,
"suggestion": issue.suggestion,
})
})
.collect();
let output_json = serde_json::json!({
"model": file.display().to_string(),
"passed": report.passed(),
"error_count": report.error_count,
"warn_count": report.warn_count,
"info_count": report.info_count,
"total_issues": report.total_issues(),
"issues": issues,
});
println!(
"{}",
serde_json::to_string_pretty(&output_json).unwrap_or_default()
);
if report.passed() {
Ok(())
} else {
Err(CliError::ValidationFailed(format!(
"Lint failed with {} error(s), {} warning(s), {} info(s)",
report.error_count, report.warn_count, report.info_count
)))
}
}
fn level_badge(level: LintLevel) -> String {
match level {
LintLevel::Info => output::badge_info("INFO"),
LintLevel::Warn => output::badge_warn("WARN"),
LintLevel::Error => output::badge_fail("ERROR"),
}
}
fn print_summary(report: &LintReport) {
let total = report.total_issues();
if report.passed() {
println!(
" {} {} issue(s) ({} info)",
output::badge_pass("Lint passed"),
total,
report.info_count,
);
} else {
println!(
" {} {} issue(s): {} error(s), {} warning(s), {} info(s)",
output::badge_fail("Lint failed"),
total,
report.error_count,
report.warn_count,
report.info_count,
);
}
}
fn display_report(report: &LintReport, quiet: bool) {
if report.issues.is_empty() {
if !quiet {
println!(" {}", output::badge_pass("No issues found"));
println!();
}
return;
}
let mut rows: Vec<Vec<String>> = Vec::new();
for issue in &report.issues {
if quiet && issue.level != LintLevel::Error {
continue;
}
let badge = level_badge(issue.level);
let suggestion = issue.suggestion.as_deref().unwrap_or("").to_string();
rows.push(vec![
badge,
issue.category.name().to_string(),
issue.message.clone(),
suggestion,
]);
}
println!(
"{}",
output::table(&["Level", "Category", "Message", "Suggestion"], &rows)
);
println!();
print_summary(report);
}
#[cfg(test)]
mod tests {
use super::*;
use aprender::format::{LintCategory, LintIssue, LintReport};
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_level_badge_info() {
let badge = level_badge(LintLevel::Info);
assert!(badge.contains("INFO"));
}
#[test]
fn test_level_badge_warn() {
let badge = level_badge(LintLevel::Warn);
assert!(badge.contains("WARN"));
}
#[test]
fn test_level_badge_error() {
let badge = level_badge(LintLevel::Error);
assert!(badge.contains("ERROR"));
}
#[test]
fn test_print_summary_passed() {
let report = LintReport::new();
print_summary(&report);
}
#[test]
fn test_print_summary_with_issues() {
let mut report = LintReport::new();
report.add_issue(LintIssue::metadata_warn("Test warning"));
report.add_issue(LintIssue::new(
LintLevel::Error,
LintCategory::Metadata,
"Test error",
));
print_summary(&report);
}
#[test]
fn test_print_summary_info_only() {
let mut report = LintReport::new();
report.add_issue(LintIssue::efficiency_info("Alignment suggestion"));
assert!(report.passed());
print_summary(&report);
}
#[test]
fn test_display_report_empty() {
let report = LintReport::new();
display_report(&report, false);
}
#[test]
fn test_display_report_with_all_categories() {
let mut report = LintReport::new();
report.add_issue(LintIssue::metadata_warn("Missing license"));
report.add_issue(LintIssue::naming_info("Use full names"));
report.add_issue(LintIssue::efficiency_info("Consider alignment"));
display_report(&report, false);
}
#[test]
fn test_run_file_not_found() {
let result = run(std::path::Path::new("/nonexistent/model.apr"), false, false);
assert!(result.is_err());
match result {
Err(CliError::FileNotFound(path)) => {
assert!(path.to_string_lossy().contains("nonexistent"));
}
_ => panic!("Expected FileNotFound error"),
}
}
#[test]
fn test_run_invalid_file() {
let mut file = NamedTempFile::with_suffix(".apr").expect("create temp file");
file.write_all(b"not a valid APR file")
.expect("write to temp file");
let result = run(file.path(), false, false);
assert!(result.is_err());
}
#[test]
fn test_lint_report_counts() {
let mut report = LintReport::new();
assert_eq!(report.info_count, 0);
assert_eq!(report.warn_count, 0);
assert_eq!(report.error_count, 0);
report.add_issue(LintIssue::efficiency_info("Info 1"));
report.add_issue(LintIssue::efficiency_info("Info 2"));
report.add_issue(LintIssue::metadata_warn("Warn 1"));
report.add_issue(LintIssue::new(
LintLevel::Error,
LintCategory::Naming,
"Error 1",
));
assert_eq!(report.info_count, 2);
assert_eq!(report.warn_count, 1);
assert_eq!(report.error_count, 1);
assert_eq!(report.total_issues(), 4);
}
#[test]
fn test_lint_report_passed() {
let mut report = LintReport::new();
assert!(report.passed());
report.add_issue(LintIssue::efficiency_info("Just info"));
assert!(report.passed());
report.add_issue(LintIssue::metadata_warn("Warning"));
assert!(!report.passed());
}
#[test]
fn test_lint_report_passed_strict() {
let mut report = LintReport::new();
assert!(report.passed_strict());
report.add_issue(LintIssue::efficiency_info("Just info"));
assert!(!report.passed_strict());
}
#[test]
fn test_lint_report_issues_at_level() {
let mut report = LintReport::new();
report.add_issue(LintIssue::efficiency_info("Info 1"));
report.add_issue(LintIssue::metadata_warn("Warn 1"));
report.add_issue(LintIssue::efficiency_info("Info 2"));
let infos = report.issues_at_level(LintLevel::Info);
assert_eq!(infos.len(), 2);
let warns = report.issues_at_level(LintLevel::Warn);
assert_eq!(warns.len(), 1);
let errors = report.issues_at_level(LintLevel::Error);
assert!(errors.is_empty());
}
#[test]
fn test_lint_report_issues_in_category() {
let mut report = LintReport::new();
report.add_issue(LintIssue::metadata_warn("Meta 1"));
report.add_issue(LintIssue::naming_info("Name 1"));
report.add_issue(LintIssue::metadata_warn("Meta 2"));
let meta_issues = report.issues_in_category(LintCategory::Metadata);
assert_eq!(meta_issues.len(), 2);
let naming_issues = report.issues_in_category(LintCategory::Naming);
assert_eq!(naming_issues.len(), 1);
let efficiency_issues = report.issues_in_category(LintCategory::Efficiency);
assert!(efficiency_issues.is_empty());
}
#[test]
fn test_lint_issue_display() {
let issue = LintIssue::new(LintLevel::Warn, LintCategory::Metadata, "Missing license");
let display = format!("{}", issue);
assert!(display.contains("WARN"));
assert!(display.contains("Metadata"));
assert!(display.contains("Missing license"));
}
#[test]
fn test_lint_issue_display_with_suggestion() {
let issue = LintIssue::new(LintLevel::Info, LintCategory::Naming, "Short name")
.with_suggestion("Use longer name");
let display = format!("{}", issue);
assert!(display.contains("suggestion"));
assert!(display.contains("Use longer name"));
}
#[test]
fn test_lint_level_display() {
assert_eq!(format!("{}", LintLevel::Info), "INFO");
assert_eq!(format!("{}", LintLevel::Warn), "WARN");
assert_eq!(format!("{}", LintLevel::Error), "ERROR");
}
#[test]
fn test_lint_category_display() {
assert_eq!(format!("{}", LintCategory::Metadata), "Metadata");
assert_eq!(format!("{}", LintCategory::Naming), "Tensor Naming");
assert_eq!(format!("{}", LintCategory::Efficiency), "Efficiency");
}
}