use std::collections::HashSet;
use std::path::PathBuf;
use super::super::super::command::SuppressionsOptions;
use super::super::{DispatchResult, GlobalOptions};
use crate::analyzer::suppression_inventory::{
SilencerKind, SilencerMatch, inventory, resolve_ignore_globs,
};
pub fn handle_suppressions_command(
opts: &SuppressionsOptions,
global: &GlobalOptions,
) -> DispatchResult {
let root = opts.root.clone().unwrap_or_else(|| PathBuf::from("."));
if !root.exists() {
eprintln!(
"[loct][suppressions] root '{}' does not exist",
root.display()
);
return DispatchResult::Exit(2);
}
let mut filter: HashSet<SilencerKind> = HashSet::new();
for raw in &opts.kinds {
for token in raw.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
match SilencerKind::from_filter(token) {
Some(k) => {
filter.insert(k);
}
None => {
eprintln!(
"[loct][suppressions] unknown --type token '{}'. Valid: {}",
token,
valid_token_list()
);
return DispatchResult::Exit(2);
}
}
}
}
let extra_globs = resolve_ignore_globs(&root, !opts.include_fixtures);
let inv = inventory(&root, &filter, &extra_globs);
let want_json = opts.json || global.json;
if want_json {
match serde_json::to_string_pretty(&inv.matches) {
Ok(json) => {
println!("{}", json);
DispatchResult::Exit(0)
}
Err(e) => {
eprintln!(
"[loct][suppressions][error] JSON serialization failed: {}",
e
);
DispatchResult::Exit(1)
}
}
} else if opts.summary || filter.is_empty() {
print_summary(&inv, &root, !opts.include_fixtures);
DispatchResult::Exit(0)
} else {
print_list(&inv.matches);
DispatchResult::Exit(0)
}
}
fn valid_token_list() -> String {
SilencerKind::all()
.iter()
.map(|k| k.label())
.collect::<Vec<_>>()
.join(", ")
}
fn print_summary(
inv: &crate::analyzer::suppression_inventory::SilencerInventory,
root: &std::path::Path,
semgrepignore_applied: bool,
) {
let suffix = if semgrepignore_applied {
" (after .semgrepignore)"
} else {
" (including fixtures)"
};
println!("Suppression inventory — {}{}", root.display(), suffix);
println!();
if inv.matches.is_empty() {
println!(" (no silencers detected)");
return;
}
for kind in SilencerKind::all() {
let label = kind.label();
let Some(count) = inv.counts.get(label) else {
continue;
};
let files = inv.files_per_kind.get(label).copied().unwrap_or(0);
let note = annotation_for(*kind);
let file_word = if files == 1 { "file" } else { "files" };
if let Some(n) = note {
println!(
" {:<22} : {:>4} ({} {}){}",
label, count, files, file_word, n
);
} else {
println!(" {:<22} : {:>4} ({} {})", label, count, files, file_word);
}
}
println!();
println!(
"Total: {} silencers across {} files.",
inv.total, inv.total_files
);
println!(
"Tip: `loct suppressions --type <kind>` to list one bucket; \
`--json` for full machine output. Literal detection only — \
semantic enrichment is paid-tier (Wave 7+)."
);
}
fn annotation_for(kind: SilencerKind) -> Option<&'static str> {
match kind {
SilencerKind::DeadCode => Some(" <- forgotten gems"),
SilencerKind::UnsafeEnvVar => Some(" (Rust 2024 boilerplate)"),
_ => None,
}
}
fn print_list(matches: &[SilencerMatch]) {
if matches.is_empty() {
println!("(no matches)");
return;
}
for m in matches {
let rule = m
.rule_id
.as_ref()
.map(|r| format!(" [{}]", r))
.unwrap_or_default();
println!(
" {}:{} {}{} {}",
m.file,
m.line,
m.kind.label(),
rule,
m.snippet
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn write(root: &std::path::Path, rel: &str, content: &str) {
let path = root.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, content).unwrap();
}
#[test]
fn handler_returns_exit_0_on_clean_repo() {
let tmp = TempDir::new().unwrap();
std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(tmp.path())
.output()
.ok();
write(tmp.path(), "src/lib.rs", "fn x() {}\n");
let opts = SuppressionsOptions {
root: Some(tmp.path().to_path_buf()),
summary: true,
..Default::default()
};
let global = GlobalOptions::default();
match handle_suppressions_command(&opts, &global) {
DispatchResult::Exit(0) => {}
other => panic!("expected Exit(0), got {:?}", debug_dispatch(&other)),
}
}
#[test]
fn handler_rejects_unknown_type_filter() {
let tmp = TempDir::new().unwrap();
std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(tmp.path())
.output()
.ok();
let opts = SuppressionsOptions {
root: Some(tmp.path().to_path_buf()),
kinds: vec!["bogus-kind".to_string()],
..Default::default()
};
let global = GlobalOptions::default();
match handle_suppressions_command(&opts, &global) {
DispatchResult::Exit(2) => {}
other => panic!("expected Exit(2), got {:?}", debug_dispatch(&other)),
}
}
fn debug_dispatch(r: &DispatchResult) -> String {
match r {
DispatchResult::Exit(c) => format!("Exit({})", c),
DispatchResult::ShowHelp => "ShowHelp".to_string(),
DispatchResult::ShowLegacyHelp => "ShowLegacyHelp".to_string(),
DispatchResult::ShowVersion => "ShowVersion".to_string(),
DispatchResult::Continue(_) => "Continue".to_string(),
}
}
}