pay-respects/rule_parser/src/lib.rs

208 lines
5.2 KiB
Rust
Raw Normal View History

2023-07-30 18:40:18 +02:00
use std::path::Path;
use proc_macro::TokenStream;
2023-08-03 21:26:41 +02:00
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
2023-07-30 18:40:18 +02:00
2023-08-03 22:13:23 +02:00
mod replaces;
2023-07-30 18:40:18 +02:00
#[proc_macro]
pub fn parse_rules(input: TokenStream) -> TokenStream {
let directory = input.to_string().trim_matches('"').to_owned();
let rules = get_rules(directory);
2023-08-03 21:26:41 +02:00
gen_match_rules(rules)
2023-07-30 18:40:18 +02:00
}
#[derive(serde::Deserialize)]
struct Rule {
command: String,
2023-07-31 10:20:06 +02:00
match_err: Vec<MatchError>,
2023-07-30 18:40:18 +02:00
}
#[derive(serde::Deserialize)]
2023-07-31 10:20:06 +02:00
struct MatchError {
2023-07-30 18:40:18 +02:00
pattern: Vec<String>,
2023-07-31 09:24:46 +02:00
suggest: Vec<String>,
2023-07-30 18:40:18 +02:00
}
fn get_rules(directory: String) -> Vec<Rule> {
let files = std::fs::read_dir(directory).expect("Failed to read directory.");
let mut rules = Vec::new();
for file in files {
let file = file.expect("Failed to read file.");
let path = file.path();
let path = path.to_str().expect("Failed to convert path to string.");
let rule_file = parse_file(Path::new(path));
rules.push(rule_file);
}
rules
}
2023-08-03 21:26:41 +02:00
fn gen_match_rules(rules: Vec<Rule>) -> TokenStream {
let command = rules
.iter()
.map(|x| x.command.to_owned())
.collect::<Vec<String>>();
let command_matches = rules
.iter()
.map(|x| {
x.match_err
.iter()
2023-08-03 21:26:41 +02:00
.map(|x| {
let pattern = x
.pattern
.iter()
.map(|x| x.to_lowercase())
.collect::<Vec<String>>();
let suggests = x
.suggest
.iter()
2023-08-04 00:52:49 +02:00
.map(|x| x.to_string())
2023-08-03 21:26:41 +02:00
.collect::<Vec<String>>();
(pattern, suggests)
})
.collect::<Vec<(Vec<String>, Vec<String>)>>()
})
.collect::<Vec<Vec<(Vec<String>, Vec<String>)>>>();
let mut matches_tokens = Vec::new();
for match_err in command_matches {
let mut suggestion_tokens = Vec::new();
let mut patterns_tokens = Vec::new();
for (pattern, suggests) in match_err {
// let mut match_condition = Vec::new();
let mut pattern_suggestions = Vec::new();
for suggest in suggests {
let (suggestion_no_condition, conditions) = parse_conditions(&suggest);
let suggest = eval_suggest(&suggestion_no_condition);
let suggestion = quote! {
if #(#conditions)&&* {
#suggest;
};
};
pattern_suggestions.push(suggestion);
};
let match_tokens = quote! {
#(#pattern_suggestions)*
};
suggestion_tokens.push(match_tokens);
let string_patterns = pattern.join("\", \"");
let string_patterns: TokenStream2 = format!("vec![\"{}\"]", string_patterns).parse().unwrap();
patterns_tokens.push(string_patterns);
2023-07-30 18:40:18 +02:00
}
2023-08-03 21:26:41 +02:00
matches_tokens.push(quote!{
#(
for pattern in #patterns_tokens {
if error_msg.contains(pattern) {
#suggestion_tokens;
};
})*
})
2023-07-30 18:40:18 +02:00
}
2023-08-03 21:26:41 +02:00
quote! {
let mut last_command = last_command.to_string();
match executable {
#(
#command => {
#matches_tokens
return None;
}
)*
_ => { return None; }
};
}.into()
2023-07-30 18:40:18 +02:00
}
fn parse_file(file: &Path) -> Rule {
let file = std::fs::read_to_string(file).expect("Failed to read file.");
toml::from_str(&file).expect("Failed to parse toml.")
}
2023-08-03 21:26:41 +02:00
fn parse_conditions(suggest: &str) -> (String, Vec<TokenStream2>) {
let mut eval_conditions = Vec::new();
if suggest.starts_with('#') {
let mut lines = suggest.lines().collect::<Vec<&str>>();
let mut conditions = String::new();
for (i, line) in lines[0..].iter().enumerate() {
conditions.push_str(line);
if line.ends_with(']') {
lines = lines[i + 1..].to_vec();
break;
}
}
let conditions = conditions
.trim_start_matches(['#', '['])
.trim_end_matches(']')
.split(',')
.collect::<Vec<&str>>();
for condition in conditions {
let (mut condition, arg) = condition.split_once('(').unwrap();
condition = condition.trim();
let arg = arg.trim_start_matches('(').trim_end_matches(')');
let reverse = match condition.starts_with('!') {
true => {
condition = condition.trim_start_matches('!');
true
}
false => false,
};
let evaluated_condition = eval_condition(condition, arg);
eval_conditions.push(quote!{#evaluated_condition == !#reverse});
}
let suggest = lines.join("\n");
return (suggest, eval_conditions);
}
(suggest.to_owned(), vec![quote!{true}])
}
fn eval_condition(condition: &str, arg: &str) -> TokenStream2 {
match condition {
"executable" => {
quote!{
std::process::Command::new("which")
.arg(#arg)
.output()
.expect("failed to execute process")
.status
.success()
}
},
"err_contains" => quote!{error_msg.contains(#arg)},
2023-08-04 00:18:31 +02:00
"cmd_contains" => quote!{last_command.contains(#arg)},
2023-08-03 21:26:41 +02:00
_ => unreachable!("Unknown condition when evaluation condition: {}", condition),
}
}
fn eval_suggest(suggest: &str) -> TokenStream2 {
let mut suggest = suggest.to_owned();
if suggest.contains("{{command}}") {
2023-08-03 22:13:23 +02:00
suggest = suggest.replace("{{command}}", "{last_command}");
2023-08-03 21:26:41 +02:00
}
2023-08-03 22:13:23 +02:00
let mut replace_list = Vec::new();
let mut opt_list = Vec::new();
2023-08-04 00:18:31 +02:00
let mut cmd_list = Vec::new();
2023-08-03 21:26:41 +02:00
2023-08-03 22:13:23 +02:00
replaces::opts(&mut suggest, &mut replace_list, &mut opt_list);
2023-08-04 00:18:31 +02:00
replaces::shell(&mut suggest, &mut cmd_list);
2023-08-03 22:13:23 +02:00
replaces::command(&mut suggest, &mut replace_list);
replaces::typo(&mut suggest, &mut replace_list);
2023-08-04 00:18:31 +02:00
replaces::shell_tag(&mut suggest, &mut replace_list, cmd_list);
2023-08-03 21:26:41 +02:00
quote! {
2023-08-03 22:13:23 +02:00
#(#opt_list)*
2023-08-03 21:26:41 +02:00
let split_command = split_command(&last_command);
return Some(format!{#suggest, #(#replace_list),*});
}
}
2023-08-03 22:13:23 +02:00