use serde::Serialize;
use crate::scanner::{InstalledLints, lints_for_library};
#[must_use]
pub fn format_human(lints: &InstalledLints, active_toolchain: Option<&str>) -> String {
if lints.is_empty() {
return String::from(
"No lints installed.\n\nRun `whitaker-installer` to install the default lint suite.",
);
}
let mut output = String::from("Installed lints:\n");
for (toolchain, libraries) in &lints.by_toolchain {
output.push('\n');
let active_marker = active_toolchain
.filter(|active| *active == toolchain)
.map_or(String::new(), |_| " (active)".to_owned());
output.push_str(&format!("Toolchain: {toolchain}{active_marker}\n"));
output.push_str(" Libraries:\n");
for library in libraries {
output.push_str(&format!(" {}\n", library.crate_name));
let lint_names = lints_for_library(&library.crate_name);
for lint in lint_names {
output.push_str(&format!(" - {lint}\n"));
}
}
}
output
}
#[must_use]
pub fn format_json(lints: &InstalledLints, active_toolchain: Option<&str>) -> String {
let json_data = InstalledLintsJson::from_installed(lints, active_toolchain);
serde_json::to_string_pretty(&json_data).unwrap_or_else(|_| "{}".to_owned())
}
#[derive(Debug, Serialize)]
pub struct InstalledLintsJson {
pub toolchains: Vec<ToolchainEntry>,
}
impl InstalledLintsJson {
fn from_installed(lints: &InstalledLints, active_toolchain: Option<&str>) -> Self {
let toolchains = lints
.by_toolchain
.iter()
.map(|(toolchain, libraries)| {
let active = active_toolchain.is_some_and(|active| active == toolchain);
let libs = libraries
.iter()
.map(|lib| {
let lint_names = lints_for_library(&lib.crate_name);
LibraryEntry {
name: lib.crate_name.as_str().to_owned(),
lints: lint_names.iter().map(|s| (*s).to_owned()).collect(),
}
})
.collect();
ToolchainEntry {
channel: toolchain.clone(),
active,
libraries: libs,
}
})
.collect();
Self { toolchains }
}
}
#[derive(Debug, Serialize)]
pub struct ToolchainEntry {
pub channel: String,
pub active: bool,
pub libraries: Vec<LibraryEntry>,
}
#[derive(Debug, Serialize)]
pub struct LibraryEntry {
pub name: String,
pub lints: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::CrateName;
use crate::scanner::InstalledLibrary;
use camino::Utf8PathBuf;
use std::collections::BTreeMap;
fn sample_lints() -> InstalledLints {
let mut by_toolchain = BTreeMap::new();
by_toolchain.insert(
"nightly-2025-09-18".to_owned(),
vec![InstalledLibrary {
crate_name: CrateName::from("whitaker_suite"),
toolchain: "nightly-2025-09-18".to_owned(),
path: Utf8PathBuf::from("/fake/path/libwhitaker_suite@nightly-2025-09-18.so"),
}],
);
InstalledLints { by_toolchain }
}
#[test]
fn format_human_empty_shows_no_lints() {
let lints = InstalledLints::default();
let output = format_human(&lints, None);
assert!(output.contains("No lints installed"));
assert!(output.contains("whitaker-installer"));
}
#[test]
fn format_human_shows_toolchain_and_lints() {
let lints = sample_lints();
let output = format_human(&lints, None);
assert!(output.contains("Installed lints:"));
assert!(output.contains("Toolchain: nightly-2025-09-18"));
assert!(output.contains("whitaker_suite"));
assert!(output.contains("module_max_lines"));
}
#[test]
fn format_human_marks_active_toolchain() {
let lints = sample_lints();
let output = format_human(&lints, Some("nightly-2025-09-18"));
assert!(output.contains("(active)"));
}
#[test]
fn format_human_does_not_mark_inactive_toolchain() {
let lints = sample_lints();
let output = format_human(&lints, Some("other-toolchain"));
assert!(!output.contains("(active)"));
}
#[test]
fn format_json_empty_has_empty_toolchains() {
let lints = InstalledLints::default();
let json = format_json(&lints, None);
assert!(json.contains("\"toolchains\""));
assert!(json.contains("[]"));
}
#[test]
fn format_json_includes_all_fields() {
let lints = sample_lints();
let json = format_json(&lints, Some("nightly-2025-09-18"));
assert!(json.contains("\"channel\""));
assert!(json.contains("\"active\": true"));
assert!(json.contains("\"libraries\""));
assert!(json.contains("\"name\""));
assert!(json.contains("\"lints\""));
assert!(json.contains("\"whitaker_suite\""));
}
#[test]
fn format_json_is_valid_json() {
let lints = sample_lints();
let json = format_json(&lints, None);
let parsed: serde_json::Value = serde_json::from_str(&json).expect("should be valid JSON");
assert!(parsed.is_object());
assert!(parsed.get("toolchains").is_some());
}
}