use crate::audit::issue::{AuditIssue, IssueSeverity};
use crate::audit::report::{AuditReport, AuditSections};
use crate::audit::sections::BreakingChangeSource;
use serde_json;
use std::fmt::Write as FmtWrite;
#[derive(Debug, Clone)]
pub struct FormatOptions {
pub colors: bool,
pub verbosity: Verbosity,
pub include_suggestions: bool,
pub include_metadata: bool,
}
impl Default for FormatOptions {
fn default() -> Self {
Self {
colors: false,
verbosity: Verbosity::Normal,
include_suggestions: true,
include_metadata: false,
}
}
}
impl FormatOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_colors(mut self, colors: bool) -> Self {
self.colors = colors;
self
}
#[must_use]
pub fn with_verbosity(mut self, verbosity: Verbosity) -> Self {
self.verbosity = verbosity;
self
}
#[must_use]
pub fn with_suggestions(mut self, include: bool) -> Self {
self.include_suggestions = include;
self
}
#[must_use]
pub fn with_metadata(mut self, include: bool) -> Self {
self.include_metadata = include;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verbosity {
Minimal,
Normal,
Detailed,
}
#[must_use]
pub fn format_markdown(report: &AuditReport, options: &FormatOptions) -> String {
let mut output = String::new();
let _ = writeln!(output, "# Audit Report");
let _ = writeln!(output);
let _ =
writeln!(output, "**Generated**: {}", report.audited_at.format("%Y-%m-%d %H:%M:%S UTC"));
let _ = writeln!(output, "**Workspace**: `{}`", report.workspace_root.display());
let _ = writeln!(
output,
"**Project Type**: {}",
if report.is_monorepo { "Monorepo" } else { "Single Package" }
);
let _ = writeln!(output, "**Health Score**: {}/100", report.health_score);
let _ = writeln!(output);
format_summary_markdown(&mut output, report, options);
if options.verbosity != Verbosity::Minimal {
if report.sections.upgrades.total_upgrades > 0
|| !report.sections.upgrades.issues.is_empty()
{
format_upgrades_section_markdown(&mut output, &report.sections, options);
}
if !report.sections.dependencies.circular_dependencies.is_empty()
|| !report.sections.dependencies.version_conflicts.is_empty()
|| !report.sections.dependencies.issues.is_empty()
{
format_dependencies_section_markdown(&mut output, &report.sections, options);
}
if report.sections.breaking_changes.total_breaking_changes > 0
|| !report.sections.breaking_changes.issues.is_empty()
{
format_breaking_changes_section_markdown(&mut output, &report.sections, options);
}
if options.verbosity == Verbosity::Detailed {
format_categorization_section_markdown(&mut output, &report.sections, options);
}
if !report.sections.version_consistency.inconsistencies.is_empty()
|| !report.sections.version_consistency.issues.is_empty()
{
format_version_consistency_section_markdown(&mut output, &report.sections, options);
}
if !report.all_issues().is_empty() {
format_issues_section_markdown(&mut output, report, options);
}
}
if options.include_suggestions && !report.summary.suggested_actions.is_empty() {
format_suggestions_markdown(&mut output, report);
}
output
}
fn format_summary_markdown(output: &mut String, report: &AuditReport, _options: &FormatOptions) {
let _ = writeln!(output, "## Summary");
let _ = writeln!(output);
let _ = writeln!(output, "| Metric | Value |");
let _ = writeln!(output, "|--------|-------|");
let _ = writeln!(output, "| Packages Analyzed | {} |", report.summary.packages_analyzed);
let _ =
writeln!(output, "| Dependencies Analyzed | {} |", report.summary.dependencies_analyzed);
let _ = writeln!(output, "| Total Issues | {} |", report.summary.total_issues);
let _ = writeln!(output, "| Critical Issues | {} |", report.summary.critical_issues);
let _ = writeln!(output, "| Warnings | {} |", report.summary.warnings);
let _ = writeln!(output, "| Info Items | {} |", report.summary.info_items);
let _ = writeln!(
output,
"| Status | {} |",
if report.passed() { "✅ Passed" } else { "❌ Failed" }
);
let _ = writeln!(output);
}
fn format_upgrades_section_markdown(
output: &mut String,
sections: &AuditSections,
_options: &FormatOptions,
) {
let _ = writeln!(output, "## Upgrades Available");
let _ = writeln!(output);
let _ = writeln!(output, "- **Total Upgrades**: {}", sections.upgrades.total_upgrades);
let _ = writeln!(output, "- **Major**: {} ⚠️", sections.upgrades.major_upgrades);
let _ = writeln!(output, "- **Minor**: {}", sections.upgrades.minor_upgrades);
let _ = writeln!(output, "- **Patch**: {}", sections.upgrades.patch_upgrades);
let _ = writeln!(output);
if !sections.upgrades.deprecated_packages.is_empty() {
let _ = writeln!(output, "### Deprecated Packages");
let _ = writeln!(output);
for pkg in §ions.upgrades.deprecated_packages {
let _ = writeln!(output, "- **{}** (v{})", pkg.name, pkg.current_version);
let _ = writeln!(output, " - {}", pkg.deprecation_message);
if let Some(alt) = &pkg.alternative {
let _ = writeln!(output, " - Alternative: `{}`", alt);
}
}
let _ = writeln!(output);
}
}
fn format_dependencies_section_markdown(
output: &mut String,
sections: &AuditSections,
_options: &FormatOptions,
) {
let _ = writeln!(output, "## Dependency Analysis");
let _ = writeln!(output);
if !sections.dependencies.circular_dependencies.is_empty() {
let _ = writeln!(output, "### Circular Dependencies");
let _ = writeln!(output);
for circular in §ions.dependencies.circular_dependencies {
let _ = writeln!(output, "- {}", circular.cycle.join(" → "));
}
let _ = writeln!(output);
}
if !sections.dependencies.version_conflicts.is_empty() {
let _ = writeln!(output, "### Version Conflicts");
let _ = writeln!(output);
for conflict in §ions.dependencies.version_conflicts {
let _ = writeln!(output, "#### {}", conflict.dependency_name);
let _ = writeln!(output);
for usage in &conflict.versions {
let _ =
writeln!(output, "- `{}` uses `{}`", usage.package_name, usage.version_spec);
}
let _ = writeln!(output);
}
}
}
fn format_breaking_changes_section_markdown(
output: &mut String,
sections: &AuditSections,
_options: &FormatOptions,
) {
let _ = writeln!(output, "## Breaking Changes");
let _ = writeln!(output);
let _ = writeln!(
output,
"- **Packages with Breaking Changes**: {}",
sections.breaking_changes.packages_with_breaking.len()
);
let _ = writeln!(
output,
"- **Total Breaking Changes**: {}",
sections.breaking_changes.total_breaking_changes
);
let _ = writeln!(output);
if !sections.breaking_changes.packages_with_breaking.is_empty() {
for pkg in §ions.breaking_changes.packages_with_breaking {
let _ = writeln!(output, "### {}", pkg.package_name);
let _ = writeln!(output);
if let Some(current) = &pkg.current_version
&& let Some(next) = &pkg.next_version
{
let _ = writeln!(output, "**Version**: {} → {}", current, next);
let _ = writeln!(output);
}
for change in &pkg.breaking_changes {
let source_label = match change.source {
BreakingChangeSource::ConventionalCommit => "Commit",
BreakingChangeSource::Changelog => "Changelog",
BreakingChangeSource::Changeset => "Changeset",
};
let _ = writeln!(output, "- [{}] {}", source_label, change.description);
if let Some(hash) = &change.commit_hash {
let _ = writeln!(output, " - Commit: `{}`", hash);
}
}
let _ = writeln!(output);
}
}
}
fn format_categorization_section_markdown(
output: &mut String,
sections: &AuditSections,
_options: &FormatOptions,
) {
let _ = writeln!(output, "## Dependency Categorization");
let _ = writeln!(output);
let stats = §ions.categorization.stats;
let _ = writeln!(output, "| Category | Count |");
let _ = writeln!(output, "|----------|-------|");
let _ = writeln!(output, "| Total Packages | {} |", stats.total_packages);
let _ = writeln!(output, "| Internal Packages | {} |", stats.internal_packages);
let _ = writeln!(output, "| External Packages | {} |", stats.external_packages);
let _ = writeln!(output, "| Workspace Links | {} |", stats.workspace_links);
let _ = writeln!(output, "| Local Links | {} |", stats.local_links);
let _ = writeln!(output);
if !sections.categorization.internal_packages.is_empty() {
let _ = writeln!(output, "### Internal Packages");
let _ = writeln!(output);
for pkg in §ions.categorization.internal_packages {
let version = pkg.version.as_ref().map_or("unknown".to_string(), |v| v.to_string());
let _ = writeln!(output, "- **{}** (v{})", pkg.name, version);
if !pkg.used_by.is_empty() {
let _ = writeln!(output, " - Used by: {}", pkg.used_by.join(", "));
}
}
let _ = writeln!(output);
}
}
fn format_version_consistency_section_markdown(
output: &mut String,
sections: &AuditSections,
_options: &FormatOptions,
) {
let _ = writeln!(output, "## Version Consistency");
let _ = writeln!(output);
if sections.version_consistency.inconsistencies.is_empty() {
let _ = writeln!(output, "✅ No version inconsistencies found.");
let _ = writeln!(output);
} else {
let _ = writeln!(
output,
"⚠️ Found {} version inconsistenc(y/ies):",
sections.version_consistency.inconsistencies.len()
);
let _ = writeln!(output);
for inconsistency in §ions.version_consistency.inconsistencies {
let _ = writeln!(output, "### {}", inconsistency.package_name);
let _ = writeln!(output);
let _ = writeln!(output, "**Versions in use**:");
for usage in &inconsistency.versions_used {
let _ =
writeln!(output, "- `{}` uses `{}`", usage.package_name, usage.version_spec);
}
let _ = writeln!(output);
let _ = writeln!(output, "**Recommended**: `{}`", inconsistency.recommended_version);
let _ = writeln!(output);
}
}
}
fn format_issues_section_markdown(
output: &mut String,
report: &AuditReport,
options: &FormatOptions,
) {
let _ = writeln!(output, "## Issues");
let _ = writeln!(output);
let critical = report.critical_issues();
let warnings = report.warnings();
let info = report.info_items();
if !critical.is_empty() {
let _ = writeln!(output, "### Critical Issues ({})", critical.len());
let _ = writeln!(output);
format_issue_list_markdown(output, &critical, options);
}
if !warnings.is_empty() {
let _ = writeln!(output, "### Warnings ({})", warnings.len());
let _ = writeln!(output);
format_issue_list_markdown(output, &warnings, options);
}
if !info.is_empty() && options.verbosity == Verbosity::Detailed {
let _ = writeln!(output, "### Informational ({})", info.len());
let _ = writeln!(output);
format_issue_list_markdown(output, &info, options);
}
}
fn format_issue_list_markdown(
output: &mut String,
issues: &[&AuditIssue],
options: &FormatOptions,
) {
for issue in issues {
let icon = match issue.severity {
IssueSeverity::Critical => "❌",
IssueSeverity::Warning => "⚠️",
IssueSeverity::Info => "ℹ️",
};
let _ = writeln!(output, "{} **{}**", icon, issue.title);
let _ = writeln!(output);
let _ = writeln!(output, "{}", issue.description);
let _ = writeln!(output);
if !issue.affected_packages.is_empty() {
let _ =
writeln!(output, "**Affected packages**: {}", issue.affected_packages.join(", "));
let _ = writeln!(output);
}
if let Some(suggestion) = &issue.suggestion {
let _ = writeln!(output, "**Suggestion**: {}", suggestion);
let _ = writeln!(output);
}
if options.include_metadata && !issue.metadata.is_empty() {
let _ = writeln!(output, "**Metadata**:");
for (key, value) in &issue.metadata {
let _ = writeln!(output, "- {}: {}", key, value);
}
let _ = writeln!(output);
}
}
}
fn format_suggestions_markdown(output: &mut String, report: &AuditReport) {
let _ = writeln!(output, "## Suggested Actions");
let _ = writeln!(output);
for (i, action) in report.summary.suggested_actions.iter().enumerate() {
let _ = writeln!(output, "{}. {}", i + 1, action);
}
let _ = writeln!(output);
}
pub fn format_json(report: &AuditReport) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(report)
}
pub fn format_json_compact(report: &AuditReport) -> Result<String, serde_json::Error> {
serde_json::to_string(report)
}
pub trait AuditReportExt {
fn to_markdown(&self) -> String;
fn to_markdown_with_options(&self, options: &FormatOptions) -> String;
fn to_json(&self) -> Result<String, serde_json::Error>;
fn to_json_compact(&self) -> Result<String, serde_json::Error>;
}
impl AuditReportExt for AuditReport {
fn to_markdown(&self) -> String {
format_markdown(self, &FormatOptions::default())
}
fn to_markdown_with_options(&self, options: &FormatOptions) -> String {
format_markdown(self, options)
}
fn to_json(&self) -> Result<String, serde_json::Error> {
format_json(self)
}
fn to_json_compact(&self) -> Result<String, serde_json::Error> {
format_json_compact(self)
}
}