Skip to main content

allow_report/
prune.rs

1use crate::contracts::PRUNE_ARTIFACT;
2use crate::json::{bool_json, option_json, push_json_fixed_artifact_preamble};
3use crate::text::markdown_cell;
4use crate::{CLAIM_BOUNDARY_TEXT, InventoryContext, PruneCandidate, PruneModeContext};
5use allow_core::json_escape;
6
7pub fn render_prune_human(candidates: &[PruneCandidate<'_>], mode: PruneModeContext<'_>) -> String {
8    render_prune_human_with_context(candidates, mode, InventoryContext::unknown_source_syntax())
9}
10
11pub fn render_prune_human_with_context(
12    candidates: &[PruneCandidate<'_>],
13    mode: PruneModeContext<'_>,
14    inventory: InventoryContext<'_>,
15) -> String {
16    let mut out = String::new();
17    out.push_str("cargo-allow prune\n\n");
18    out.push_str(&format!(
19        "Inventory: {}/{} via {}{}\n",
20        inventory.scope,
21        inventory.scanner,
22        inventory.source,
23        prune_inventory_files_suffix(inventory)
24    ));
25    if let Some(root) = inventory.root {
26        out.push_str(&format!("Source tree root: {root}\n"));
27    }
28    if mode.write_requested {
29        out.push_str("mode: write\n");
30    } else {
31        out.push_str("mode: dry-run\n");
32    }
33    if mode.explicit_dry_run {
34        out.push_str("requested: --dry-run\n");
35    }
36    out.push_str(&format!("stale entries: {}\n\n", candidates.len()));
37    if candidates.is_empty() {
38        out.push_str("No stale allow entries found.\n");
39        out.push('\n');
40        out.push_str(CLAIM_BOUNDARY_TEXT);
41        out.push('\n');
42        return out;
43    }
44    out.push_str("| Allow ID | Kind | Family | Owner | Classification | Scope | Reason |\n");
45    out.push_str("|---|---|---|---|---|---|---|\n");
46    for candidate in candidates {
47        out.push_str(&format!(
48            "| `{}` | `{}` | `{}` | `{}` | `{}` | `{}` | {} |\n",
49            markdown_cell(candidate.id),
50            candidate.kind,
51            markdown_cell(candidate.family.unwrap_or("-")),
52            markdown_cell(candidate.owner),
53            markdown_cell(candidate.classification),
54            markdown_cell(candidate.scope),
55            markdown_cell(candidate.reason)
56        ));
57    }
58    if let Some(path) = mode.written_path {
59        out.push_str(&format!(
60            "\nRemoved stale entries from `{}`.\n",
61            markdown_cell(path)
62        ));
63    } else {
64        out.push_str(
65            "\nNo files were changed. Remove these entries only after confirming the exception is gone.\n",
66        );
67    }
68    out.push('\n');
69    out.push_str(CLAIM_BOUNDARY_TEXT);
70    out.push('\n');
71    out
72}
73
74fn prune_inventory_files_suffix(inventory: InventoryContext<'_>) -> String {
75    inventory
76        .files_scanned
77        .map(|files| format!("; files scanned: {files}"))
78        .unwrap_or_default()
79}
80
81pub fn render_prune_json(
82    candidates: &[PruneCandidate<'_>],
83    mode: PruneModeContext<'_>,
84    inventory: InventoryContext<'_>,
85) -> String {
86    let mut out = String::new();
87    out.push_str("{\n");
88    push_json_fixed_artifact_preamble(&mut out, PRUNE_ARTIFACT, inventory);
89    out.push_str("  \"mode\": {\n");
90    out.push_str(&format!(
91        "    \"dry_run\": {},\n",
92        bool_json(!mode.write_requested)
93    ));
94    out.push_str(&format!(
95        "    \"write_requested\": {},\n",
96        bool_json(mode.write_requested)
97    ));
98    out.push_str(&format!(
99        "    \"explicit_dry_run\": {},\n",
100        bool_json(mode.explicit_dry_run)
101    ));
102    out.push_str(&format!(
103        "    \"written_path\": {}\n",
104        option_json(mode.written_path)
105    ));
106    out.push_str("  },\n");
107    out.push_str(&format!(
108        "  \"summary\": {{\n    \"stale_entries\": {}\n  }},\n",
109        candidates.len()
110    ));
111    out.push_str("  \"stale_entries\": [\n");
112    for (index, candidate) in candidates.iter().enumerate() {
113        if index > 0 {
114            out.push_str(",\n");
115        }
116        out.push_str(&render_prune_candidate_json(candidate, "  "));
117    }
118    out.push_str("\n  ]\n");
119    out.push_str("}\n");
120    out
121}
122
123fn render_prune_candidate_json(candidate: &PruneCandidate<'_>, indent: &str) -> String {
124    format!(
125        "{indent}  {{\n{indent}    \"id\": \"{}\",\n{indent}    \"kind\": \"{}\",\n{indent}    \"family\": {},\n{indent}    \"owner\": \"{}\",\n{indent}    \"classification\": \"{}\",\n{indent}    \"scope\": \"{}\",\n{indent}    \"reason\": \"{}\"\n{indent}  }}",
126        json_escape(candidate.id),
127        json_escape(candidate.kind),
128        option_json(candidate.family),
129        json_escape(candidate.owner),
130        json_escape(candidate.classification),
131        json_escape(candidate.scope),
132        json_escape(candidate.reason)
133    )
134}