use crate::contracts::PRUNE_ARTIFACT;
use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
use crate::text::markdown_cell;
use crate::{CLAIM_BOUNDARY_TEXT, InventoryContext, PruneCandidate, PruneModeContext};
use allow_core::json_escape;
pub fn render_prune_human(candidates: &[PruneCandidate<'_>], mode: PruneModeContext<'_>) -> String {
render_prune_human_with_context(candidates, mode, InventoryContext::unknown_source_syntax())
}
pub fn render_prune_human_with_context(
candidates: &[PruneCandidate<'_>],
mode: PruneModeContext<'_>,
inventory: InventoryContext<'_>,
) -> String {
let mut out = String::new();
out.push_str("cargo-allow prune\n\n");
out.push_str(&format!(
"Inventory: {}/{} via {}{}\n",
inventory.scope,
inventory.scanner,
inventory.source,
prune_inventory_files_suffix(inventory)
));
if let Some(root) = inventory.root {
out.push_str(&format!("Source tree root: {root}\n"));
}
if mode.write_requested {
out.push_str("mode: write\n");
} else {
out.push_str("mode: dry-run\n");
}
if mode.explicit_dry_run {
out.push_str("requested: --dry-run\n");
}
out.push_str(&format!("stale entries: {}\n\n", candidates.len()));
if candidates.is_empty() {
out.push_str("No stale allow entries found.\n");
out.push('\n');
out.push_str(CLAIM_BOUNDARY_TEXT);
out.push('\n');
return out;
}
out.push_str("| Allow ID | Kind | Family | Owner | Classification | Scope | Reason |\n");
out.push_str("|---|---|---|---|---|---|---|\n");
for candidate in candidates {
out.push_str(&format!(
"| `{}` | `{}` | `{}` | `{}` | `{}` | `{}` | {} |\n",
markdown_cell(candidate.id),
candidate.kind,
markdown_cell(candidate.family.unwrap_or("-")),
markdown_cell(candidate.owner),
markdown_cell(candidate.classification),
markdown_cell(candidate.scope),
markdown_cell(candidate.reason)
));
}
if let Some(path) = mode.written_path {
out.push_str(&format!(
"\nRemoved stale entries from `{}`.\n",
markdown_cell(path)
));
} else {
out.push_str(
"\nNo files were changed. Remove these entries only after confirming the exception is gone.\n",
);
}
out.push('\n');
out.push_str(CLAIM_BOUNDARY_TEXT);
out.push('\n');
out
}
fn prune_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
inventory
.files_scanned
.map(|files| format!("; files scanned: {files}"))
.unwrap_or_default()
}
pub fn render_prune_json(
candidates: &[PruneCandidate<'_>],
mode: PruneModeContext<'_>,
inventory: InventoryContext<'_>,
) -> String {
let mut out = String::new();
out.push_str("{\n");
push_json_fixed_artifact_preamble(&mut out, PRUNE_ARTIFACT, inventory);
out.push_str(" \"mode\": {\n");
out.push_str(&format!(
" \"dry_run\": {},\n",
bool_json(!mode.write_requested)
));
out.push_str(&format!(
" \"write_requested\": {},\n",
bool_json(mode.write_requested)
));
out.push_str(&format!(
" \"explicit_dry_run\": {},\n",
bool_json(mode.explicit_dry_run)
));
out.push_str(&format!(
" \"written_path\": {}\n",
option_json(mode.written_path)
));
out.push_str(" },\n");
out.push_str(&format!(
" \"summary\": {{\n \"stale_entries\": {}\n }},\n",
candidates.len()
));
out.push_str(" \"stale_entries\": [\n");
for (index, candidate) in candidates.iter().enumerate() {
if index > 0 {
out.push_str(",\n");
}
out.push_str(&render_prune_candidate_json(candidate, " "));
}
out.push_str("\n ]\n");
out.push_str("}\n");
out
}
fn render_prune_candidate_json(candidate: &PruneCandidate<'_>, indent: &str) -> String {
format!(
"{indent} {{\n{indent} \"id\": \"{}\",\n{indent} \"kind\": \"{}\",\n{indent} \"family\": {},\n{indent} \"owner\": \"{}\",\n{indent} \"classification\": \"{}\",\n{indent} \"scope\": \"{}\",\n{indent} \"reason\": \"{}\"\n{indent} }}",
json_escape(candidate.id),
json_escape(candidate.kind),
option_json(candidate.family),
json_escape(candidate.owner),
json_escape(candidate.classification),
json_escape(candidate.scope),
json_escape(candidate.reason)
)
}