// pay-respects-parser: Compile time rule parser for pay-respects // Copyright (C) 2023 iff // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use std::path::Path; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; mod replaces; #[proc_macro] pub fn parse_rules(input: TokenStream) -> TokenStream { let directory = input.to_string().trim_matches('"').to_owned(); let rules = get_rules(directory); gen_match_rules(rules) } #[derive(serde::Deserialize)] struct Rule { command: String, match_err: Vec, } #[derive(serde::Deserialize)] struct MatchError { pattern: Vec, suggest: Vec, } fn get_rules(directory: String) -> Vec { 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 } fn gen_match_rules(rules: &[Rule]) -> TokenStream { let command = rules .iter() .map(|x| x.command.to_owned()) .collect::>(); let command_matches = rules .iter() .map(|x| { x.match_err .iter() .map(|x| { let pattern = x .pattern .iter() .map(|x| x.to_lowercase()) .collect::>(); let suggests = x .suggest .iter() .map(|x| x.to_string()) .collect::>(); (pattern, suggests) }) .collect::, Vec)>>() }) .collect::, Vec)>>>(); 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!("[\"{}\"]", string_patterns).parse().unwrap(); patterns_tokens.push(string_patterns); } matches_tokens.push(quote! { #( for pattern in #patterns_tokens { if error_msg.contains(pattern) { let split = split_command(&last_command); #suggestion_tokens; }; })* }) } quote! { let mut last_command = last_command.to_string(); match executable { #( #command => { #matches_tokens } )* _ => {} }; } .into() } 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.") } fn parse_conditions(suggest: &str) -> (String, Vec) { let mut eval_conditions = Vec::new(); if suggest.starts_with('#') { let mut lines = suggest.lines().collect::>(); 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::>(); 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! {executables.contains(&#arg.to_string())}, "err_contains" => quote! {error_msg.contains(#arg)}, "cmd_contains" => quote! {last_command.contains(#arg)}, "min_length" => quote! {(split.len() >= #arg.parse::().unwrap())}, "length" => quote! {(split.len() == #arg.parse::().unwrap())}, "max_length" => quote! {(split.len() <= #arg.parse::().unwrap() + 1)}, "shell" => quote! {(shell == #arg)}, _ => unreachable!("Unknown condition when evaluation condition: {}", condition), } } fn eval_suggest(suggest: &str) -> TokenStream2 { let mut suggest = suggest.to_owned(); if suggest.contains("{{command}}") { suggest = suggest.replace("{{command}}", "{last_command}"); } let mut replace_list = Vec::new(); let mut opt_list = Vec::new(); let mut cmd_list = Vec::new(); replaces::opts(&mut suggest, &mut replace_list, &mut opt_list); replaces::cmd_reg(&mut suggest, &mut replace_list); replaces::err(&mut suggest, &mut replace_list); replaces::command(&mut suggest, &mut replace_list); replaces::shell(&mut suggest, &mut cmd_list); replaces::typo(&mut suggest, &mut replace_list); replaces::shell_tag(&mut suggest, &mut replace_list, cmd_list); quote! { #(#opt_list)* candidates.push(format!{#suggest, #(#replace_list),*}); } }