mod files;
mod git;
mod git_extra;
mod system;
mod tools;
mod typos;
use crate::command::Command;
use crate::config::Config;
pub struct Rule {
pub name: &'static str,
pub description: &'static str,
pub priority: u32,
pub matches: fn(&Command) -> bool,
pub fixes: fn(&Command) -> Vec<String>,
}
pub fn all() -> Vec<Rule> {
let mut rules = Vec::new();
rules.extend(git::rules());
rules.extend(git_extra::rules());
rules.extend(files::rules());
rules.extend(system::rules());
rules.extend(tools::rules());
rules.extend(typos::rules());
rules.sort_by_key(|rule| rule.priority);
rules
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fix {
pub command: String,
pub rule: String,
}
pub fn suggest(command: &Command, config: &Config) -> Vec<Fix> {
let mut fixes: Vec<Fix> = Vec::new();
for rule in all() {
if config.exclude_rules.iter().any(|name| name == rule.name) || !(rule.matches)(command) {
continue;
}
for fix in (rule.fixes)(command) {
let fix = fix.trim().to_string();
if !fix.is_empty()
&& fix != command.script
&& !fixes.iter().any(|known| known.command == fix)
{
fixes.push(Fix {
command: fix,
rule: rule.name.to_string(),
});
}
}
}
fixes
}
pub(crate) fn replace_arg(script: &str, old: &str, new: &str) -> String {
if let Some(head) = script.strip_suffix(old)
&& head.ends_with(' ')
{
return format!("{head}{new}");
}
script.replacen(&format!(" {old} "), &format!(" {new} "), 1)
}
pub(crate) fn replace_program(script: &str, new: &str) -> String {
let (head, rest) = match script.strip_prefix("sudo ") {
Some(rest) => ("sudo ", rest.trim_start()),
None => ("", script),
};
let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
format!("{head}{new}{}", &rest[end..])
}
#[cfg(test)]
pub(crate) fn fixes_for(script: &str, output: &str) -> Vec<String> {
suggest(&Command::new(script, output), &Config::default())
.into_iter()
.map(|fix| fix.command)
.collect()
}
#[cfg(test)]
pub(crate) fn offers(script: &str, output: &str, fix: &str) -> bool {
fixes_for(script, output)
.iter()
.any(|offered| offered == fix)
}
#[cfg(test)]
pub(crate) fn rule_matches(name: &str, script: &str, output: &str) -> bool {
let rule = all()
.into_iter()
.find(|rule| rule.name == name)
.unwrap_or_else(|| panic!("no rule named {name}"));
(rule.matches)(&Command::new(script, output))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rule_names_are_unique() {
let mut names: Vec<_> = all().iter().map(|rule| rule.name).collect();
names.sort();
let count = names.len();
names.dedup();
assert_eq!(names.len(), count);
}
#[test]
fn excluded_rules_are_skipped() {
let config = Config {
exclude_rules: vec!["sl_ls".into()],
..Config::default()
};
assert!(suggest(&Command::new("sl", ""), &config).is_empty());
}
}
#[cfg(test)]
mod ordering {
use super::*;
#[test]
fn a_missing_space_beats_a_far_fetched_typo() {
let missing = all()
.into_iter()
.find(|rule| rule.name == "missing_space_before_subcommand")
.unwrap();
let typo = all()
.into_iter()
.find(|rule| rule.name == "no_command")
.unwrap();
assert!(missing.priority < typo.priority);
}
#[test]
fn switching_to_main_beats_creating_master() {
let main_master = all()
.into_iter()
.find(|rule| rule.name == "git_main_master")
.unwrap();
let checkout = all()
.into_iter()
.find(|rule| rule.name == "git_checkout")
.unwrap();
assert!(main_master.priority < checkout.priority);
}
}