#![doc = include_str!("../README.md")]
use std::collections::HashMap;
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::process;
use slippy_linter::{
FileFilter, Lint, LintConfig, LintLevel, LintLevelSource, LintStats, RawSlippyConfiguration,
finalize, merge_raw,
};
use tracing::{error, info};
fn show_help() {
if writeln!(&mut anstream::stdout().lock(), "{}", help_message()).is_err() {
process::exit(1);
}
}
fn show_version() {
let version_info = rustc_tools_util::get_version_info!();
if writeln!(&mut anstream::stdout().lock(), "{version_info}").is_err() {
process::exit(1);
}
}
#[tokio::main]
pub async fn main() {
dotenvy::dotenv().ok();
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
tracing_subscriber::fmt()
.with_env_filter(env_filter)
.with_writer(std::io::stderr)
.with_target(false)
.without_time()
.init();
if env::args().any(|a| a == "--help" || a == "-h") {
show_help();
return;
}
if env::args().any(|a| a == "--version" || a == "-V") {
show_version();
return;
}
if env::args().any(|a| a == "gen-docs") {
process::exit(gen_docs());
}
if let Some(pos) = env::args().position(|a| a == "--explain") {
if let Some(mut lint) = env::args().nth(pos + 1) {
lint.make_ascii_lowercase();
process::exit(explain(
&lint.trim_start_matches("slippy::").replace('-', "_"),
));
} else {
show_help();
}
return;
}
let mut args = env::args().peekable();
let _binary = args.next();
if let Some(maybe_cargo_subcommand) = args.peek()
&& maybe_cargo_subcommand == "slippy"
{
args.next();
}
let cmd = SlippyCmd::new(args);
let mut cargo_options = Vec::new();
if cmd.frozen {
cargo_options.push("--frozen".to_string());
}
if cmd.locked {
cargo_options.push("--locked".to_string());
}
if cmd.offline {
cargo_options.push("--offline".to_string());
}
let slippy_config = load_config(&cmd);
let lint_overrides = slippy_config.lint_overrides.clone();
let mut config = LintConfig::new(slippy_config.clone(), cmd.quiet);
for (lint, (level, path)) in &lint_overrides {
config.set_level(
*lint,
*level,
LintLevelSource::ConfigFile { file: path.clone() },
);
}
for (lint, level) in &cmd.lint_flags {
config.set_level(*lint, *level, LintLevelSource::CommandLine);
}
let cwd = env::current_dir().expect("failed to determine current directory");
let filter = FileFilter::from_root_directory(&slippy_config, &cwd);
let result =
slippy_linter::lint_manifest(&cmd.manifest_path, cargo_options, &config, &filter).await;
match result {
Ok(stats) => {
print_summary(&stats);
if stats.errors > 0 {
process::exit(1);
}
}
Err(err) => {
error!("{err}");
process::exit(1);
}
}
}
fn print_summary(stats: &LintStats) {
let files = stats.files_checked;
let files_s = if files == 1 { "" } else { "s" };
let errors_s = if stats.errors == 1 { "" } else { "s" };
let warnings_s = if stats.warnings == 1 { "" } else { "s" };
if stats.errors > 0 {
let mut parts = vec![format!("{} error{errors_s}", stats.errors)];
if stats.warnings > 0 {
parts.push(format!("{} warning{warnings_s}", stats.warnings));
}
eprintln!(
"\x1b[1;31merror\x1b[0m: {files} file{files_s} checked, {} generated",
parts.join(" and "),
);
} else if stats.warnings > 0 {
eprintln!(
"\x1b[1;33mwarning\x1b[0m: {files} file{files_s} checked, {} warning{warnings_s} generated ",
stats.warnings,
);
} else if files > 0 {
eprintln!(
"\x1b[1;32m{:>12}\x1b[0m {files} file{files_s} checked, no issues found",
"Finished",
);
}
}
struct SlippyCmd {
manifest_path: PathBuf,
include_patterns: Vec<String>,
exclude_patterns: Vec<String>,
respect_gitignore: Option<bool>,
frozen: bool,
locked: bool,
offline: bool,
quiet: bool,
lint_flags: Vec<(Lint, LintLevel)>,
}
impl SlippyCmd {
fn new<I>(mut args: I) -> Self
where
I: Iterator<Item = String>,
{
let mut manifest_path: Option<PathBuf> = None;
let mut include_patterns: Vec<String> = Vec::new();
let mut exclude_patterns: Vec<String> = Vec::new();
let mut exclude_gitignore: Option<bool> = None;
let mut frozen = false;
let mut locked = false;
let mut offline = false;
let mut quiet = false;
let mut slippy_args: Vec<String> = Vec::new();
while let Some(arg) = args.next() {
match arg.as_str() {
"--manifest-path" => {
manifest_path = args.next().map(PathBuf::from);
}
"--file" | "--include" | "--files" => {
let Some(value) = args.next() else {
error!("A pattern is required for {arg}");
process::exit(1);
};
include_patterns.extend(value.split(',').map(|s| s.trim().to_string()));
}
"--exclude" => {
let Some(value) = args.next() else {
error!("A pattern is required for --exclude");
process::exit(1);
};
exclude_patterns.extend(value.split(',').map(|s| s.trim().to_string()));
}
"--no-respect-gitignore" => exclude_gitignore = Some(false),
"--frozen" => frozen = true,
"--locked" => locked = true,
"--offline" => offline = true,
"--quiet" | "-q" => quiet = true,
"--" => break,
_ => {}
}
}
slippy_args.extend(args);
let lint_flags = parse_lint_flags(&slippy_args);
let manifest_path = manifest_path.unwrap_or_else(|| {
env::current_dir()
.expect("failed to determine current directory")
.join("Cargo.toml")
});
Self {
manifest_path,
include_patterns,
exclude_patterns,
respect_gitignore: exclude_gitignore,
frozen,
locked,
offline,
quiet,
lint_flags,
}
}
}
fn parse_lint_flags(args: &[String]) -> Vec<(Lint, LintLevel)> {
let mut flags = Vec::new();
let mut iter = args.iter();
while let Some(arg) = iter.next() {
let level = match arg.as_str() {
"-W" | "--warn" => LintLevel::Warn,
"-A" | "--allow" => LintLevel::Allow,
"-D" | "--deny" => LintLevel::Deny,
"-F" | "--forbid" => LintLevel::Forbid,
_ => continue,
};
if let Some(lint_str) = iter.next() {
let name = lint_str.trim_start_matches("slippy::").replace('-', "_");
let Ok(lint) = name.parse() else {
error!("Unknown lint: {lint_str}");
process::exit(1);
};
flags.push((lint, level));
}
}
flags
}
fn explain(lint: &str) -> i32 {
let Ok(lint) = lint.parse::<Lint>() else {
error!("Unknown lint: {lint}");
eprintln!(
"\nAvailable lints: {}",
Lint::ALL
.iter()
.map(|l| l.as_str())
.collect::<Vec<_>>()
.join(", "),
);
return 1;
};
let mut out = anstream::stdout().lock();
let _ = writeln!(out, "### `slippy::{}`\n", lint.as_str());
let _ = writeln!(out, "{}", lint.explanation().trim());
0
}
fn gen_docs() -> i32 {
let lints: Vec<_> = Lint::ALL
.iter()
.map(|lint| {
serde_json::json!({
"id": lint.as_str(),
"level": format!("{:?}", lint.default_level()).to_lowercase(),
"description": lint.short_description(),
"explanation": lint.explanation().trim(),
})
})
.collect();
if serde_json::to_writer_pretty(std::io::stdout().lock(), &lints).is_err() {
return 1;
}
println!();
0
}
const CONFIG_FILE_NAMES: &[&str] = &["slippy.toml", ".slippy.toml"];
fn collect_config_paths() -> Vec<PathBuf> {
let mut configs = Vec::new();
if let Ok(mut dir) = env::current_dir() {
loop {
let mut found = false;
for name in CONFIG_FILE_NAMES {
let path = dir.join(name);
if path.is_file() {
configs.push(path);
found = true;
break;
}
}
if !found {
let nested = dir.join(".slippy").join("config.toml");
if nested.is_file() {
configs.push(nested);
}
}
if !dir.pop() {
break;
}
}
}
configs.reverse();
if let Some(home) = env::var_os("HOME")
.or_else(|| env::var_os("USERPROFILE"))
.map(PathBuf::from)
{
let global = home.join(".slippy").join("config.toml");
if global.is_file() && !configs.contains(&global) {
configs.insert(0, global);
}
}
configs
}
fn load_config(cli: &SlippyCmd) -> slippy_linter::SlippyConfiguration {
let paths = collect_config_paths();
if paths.is_empty() {
error!("No slippy configuration file found");
info!("Create slippy.toml, .slippy.toml, .slippy/config.toml, or ~/.slippy/config.toml");
process::exit(1);
}
let mut merged = RawSlippyConfiguration::default();
let mut lint_sources = HashMap::new();
for path in &paths {
let content = std::fs::read_to_string(path).unwrap_or_else(|e| {
error!("Failed to read config from {}: {e}", path.display());
process::exit(1);
});
let raw: RawSlippyConfiguration = toml::from_str(&content).unwrap_or_else(|e| {
error!("Failed to parse config from {}: {e}", path.display());
process::exit(1);
});
if let Some(lints) = &raw.lints {
for name in lints.keys() {
lint_sources.insert(*name, path.clone());
}
}
merged = merge_raw(merged, raw);
}
if let Some(v) = cli.respect_gitignore {
merged.respect_gitignore = Some(v);
}
if !cli.include_patterns.is_empty() {
let mut include = merged.include_patterns.unwrap_or_default();
include.extend(cli.include_patterns.iter().cloned());
merged.include_patterns = Some(include);
}
if !cli.exclude_patterns.is_empty() {
let mut exclude = merged.exclude_patterns.unwrap_or_default();
exclude.extend(cli.exclude_patterns.iter().cloned());
merged.exclude_patterns = Some(exclude);
}
finalize(merged, &lint_sources).unwrap_or_else(|e| {
error!("{e}");
process::exit(1);
})
}
#[must_use]
pub fn help_message() -> &'static str {
color_print::cstr!(
"Checks a package with LLMs to catch common mistakes that clippy can't catch programmatically.
<green,bold>Usage</>:
<cyan,bold>cargo slippy</> <cyan>[OPTIONS] [--] [<<ARGS>>...]</>
<green,bold>Common options:</>
<cyan,bold>-h</>, <cyan,bold>--help</> Print this message
<cyan,bold>-V</>, <cyan,bold>--version</> Print version info and exit
<cyan,bold>--explain [LINT]</> Print the documentation for a given lint
<cyan,bold>-q</>, <cyan,bold>--quiet</> Suppress progress bar and summary output
<green,bold>File Selection:</>
<cyan,bold>--include</> <cyan><<PATHS>></> Lint only these files (comma-separated)
Aliases: <cyan,bold>--file</>, <cyan,bold>--files</>
<cyan,bold>--exclude</> <cyan><<PATTERNS>></> Glob patterns for files to exclude (comma-separated)
<cyan,bold>--no-respect-gitignore</> Include gitignored files (ignored by default)
<green,bold>Allowing / Denying lints</>
To allow or deny a lint from the command line you can use <cyan,bold>cargo slippy --</> with:
<cyan,bold>-W</> / <cyan,bold>--warn</> <cyan>[LINT]</> Set lint warnings
<cyan,bold>-A</> / <cyan,bold>--allow</> <cyan>[LINT]</> Set lint allowed
<cyan,bold>-D</> / <cyan,bold>--deny</> <cyan>[LINT]</> Set lint denied
<cyan,bold>-F</> / <cyan,bold>--forbid</> <cyan>[LINT]</> Set lint forbidden
You can use tool lints to allow or deny lints from your code, e.g.:
<yellow,bold>/// allow(slippy::option_result_misuse)</>
<yellow,bold>//! forbid(slippy::string_used_as_enum)</>
Or allow lints using code comments to explain the purpose:
<bright-black,bold>// using Result type since we may add more errors later</>
<green,bold>Manifest Options:</>
<cyan,bold>--manifest-path</> <cyan><<PATH>></> Path to Cargo.toml
<cyan,bold>--frozen</> Require Cargo.lock and cache are up to date
<cyan,bold>--locked</> Require Cargo.lock is up to date
<cyan,bold>--offline</> Run without accessing the network
"
)
}