use inquire::{set_global_render_config, Confirm, MultiSelect};
use std::{env, error::Error, path::PathBuf, process::Command};
use thag_styling::{
auto_help, file_navigator, help_system::check_help_and_exit, sprtln, themed_inquire_config,
AnsiStyleExt, Color, Role, Style, Styleable, StyledPrint,
};
file_navigator! {}
#[derive(Debug, Clone)] struct ClippyLintGroup {
name: &'static str,
description: &'static str,
level: LintLevel, }
#[derive(Debug, Clone)]
enum LintLevel {
Basic, Style, Extra, Strict, Deprecated, }
impl LintLevel {
fn color(&self) -> Style {
match self {
Self::Basic => Color::green(),
Self::Style => Color::cyan(),
Self::Extra => Color::yellow(),
Self::Strict => Color::red(),
Self::Deprecated => Color::dark_gray(),
}
}
}
impl ClippyLintGroup {
const fn new(name: &'static str, description: &'static str, level: LintLevel) -> Self {
Self {
name,
description,
level,
}
}
fn all() -> Vec<Self> {
vec![
Self::new(
"cargo",
"Checks for common mistakes when using Cargo",
LintLevel::Basic,
),
Self::new(
"complexity",
"Checks for code that might be too complex",
LintLevel::Style,
),
Self::new(
"correctness",
"Checks for common programming mistakes",
LintLevel::Basic,
),
Self::new(
"nursery",
"New lints that are still under development",
LintLevel::Extra,
),
Self::new(
"pedantic",
"Stricter checks for code quality",
LintLevel::Extra,
),
Self::new(
"perf",
"Checks that impact runtime performance",
LintLevel::Style,
),
Self::new("restriction", "Highly restrictive lints", LintLevel::Strict),
Self::new("style", "Checks for common style issues", LintLevel::Style),
Self::new(
"suspicious",
"Checks for suspicious code constructs",
LintLevel::Strict,
),
Self::new(
"deprecated",
"Previously deprecated lints",
LintLevel::Deprecated,
),
]
}
fn to_arg(&self) -> String {
format!("clippy::{}", self.name)
}
fn format_for_display(&self) -> String {
format!(
"{}: {}",
self.name.style_with(self.level.color()).bold(),
self.description
)
}
}
fn select_script() -> Result<PathBuf, Box<dyn std::error::Error>> {
let mut navigator = FileNavigator::new();
loop {
let items = navigator.list_items(Some("rs"), false, false, false);
let selection = Select::new(
&format!("Current dir: {}", navigator.current_dir.display()),
items,
)
.with_help_message("↑↓ navigate, Enter select")
.with_page_size(20)
.prompt()?;
if let NavigationResult::SelectionComplete(script_path) =
navigator.navigate(&selection, false)
{
if Confirm::new(&format!("Use {}?", script_path.display()))
.with_default(true)
.prompt()?
{
return Ok(script_path);
}
}
}
}
fn select_lint_groups() -> Result<Vec<ClippyLintGroup>, Box<dyn Error>> {
let lint_groups = ClippyLintGroup::all();
let formatted_groups: Vec<String> = lint_groups
.iter()
.map(ClippyLintGroup::format_for_display)
.collect();
let selections = MultiSelect::new("Select Clippy lint groups:", formatted_groups.clone())
.with_help_message("Space to select/unselect, Enter to confirm")
.with_default(&[2, 4]) .prompt()?;
let selected_groups: Vec<ClippyLintGroup> = lint_groups
.into_iter()
.enumerate()
.filter(|(i, _)| selections.contains(&formatted_groups[*i]))
.map(|(_, group)| group)
.collect();
Ok(selected_groups)
}
fn get_script_mode() -> ScriptMode {
if atty::isnt(atty::Stream::Stdin) {
ScriptMode::Stdin
} else if std::env::args().len() > 1 {
ScriptMode::File
} else {
ScriptMode::Interactive
}
}
enum ScriptMode {
Stdin,
File,
Interactive,
}
fn main() -> Result<(), Box<dyn Error>> {
let help = auto_help!();
check_help_and_exit(&help);
set_global_render_config(themed_inquire_config());
let script_path = match get_script_mode() {
ScriptMode::Stdin => {
sprtln!(Role::Error, "This tool cannot be run with stdin input. Please provide a file path or run interactively.");
std::process::exit(1);
}
ScriptMode::File => {
let args: Vec<String> = std::env::args().collect();
PathBuf::from(args[1].clone())
}
ScriptMode::Interactive => {
select_script()?
}
};
sprtln!(Role::Heading1, "\nSelect lint groups to apply:");
match select_lint_groups() {
Ok(selected_groups) => {
if selected_groups.is_empty() {
"\nNo lint groups selected. Using default Clippy checks."
.warning()
.println();
"\nRunning command:".normal().bold().println();
format!("thag --cargo {} -- clippy", script_path.display())
.code()
.println();
} else {
let mut by_level: Vec<(&str, Vec<&ClippyLintGroup>)> = Vec::new();
for level in [
LintLevel::Basic,
LintLevel::Style,
LintLevel::Extra,
LintLevel::Strict,
] {
let groups: Vec<_> = selected_groups
.iter()
.filter(|g| {
std::mem::discriminant(&g.level) == std::mem::discriminant(&level)
})
.collect();
if !groups.is_empty() {
by_level.push((
match level {
LintLevel::Basic => "Basic checks",
LintLevel::Style => "Style checks",
LintLevel::Extra => "Extra checks",
LintLevel::Strict => "Strict checks",
LintLevel::Deprecated => "Deprecated",
},
groups,
));
}
}
"\nSelected lint groups:".heading3().bold().println();
for (level_name, groups) in &by_level {
println!(" {}", level_name.style().bold());
for group in groups {
println!(" • {}", group.name.style_with(group.level.color()));
}
}
let warn_flags: Vec<String> = selected_groups
.iter()
.map(|group| format!("-W{}", group.to_arg()))
.collect();
let command = format!(
"thag --cargo {} -- clippy -- {}",
script_path.display(),
warn_flags.join(" ")
);
sprtln!(Role::Heading3, "\n{}", "Command to run:".style().bold());
sprtln!(Role::Code, "{command}");
let script_path = script_path.display().to_string();
let mut thag_args = vec!["--cargo", &script_path, "--", "clippy", "--"];
thag_args.extend(warn_flags.iter().map(String::as_str));
let status = Command::new("thag").args(&thag_args).status()?;
if !status.success() {
sprtln!(Role::Error, "Clippy check failed");
return Err("Clippy check failed".into());
}
}
}
Err(e) => {
sprtln!(Role::Error, "Error selecting lint groups: {e}");
return Err(e);
}
}
Ok(())
}