pay_respects_parser/
lib.rs1use std::path::Path;
6
7use proc_macro::TokenStream;
8use proc_macro2::TokenStream as TokenStream2;
9use quote::quote;
10
11mod replaces;
12
13#[proc_macro]
14pub fn parse_rules(input: TokenStream) -> TokenStream {
15 let rules = get_rules(input.to_string().trim_matches('"'));
16 gen_match_rules(&rules)
17}
18
19#[derive(serde::Deserialize)]
20struct Rule {
21 command: String,
22 match_err: Vec<MatchError>,
23}
24
25#[derive(serde::Deserialize)]
26struct MatchError {
27 pattern: Vec<String>,
28 suggest: Vec<String>,
29}
30
31fn get_rules(directory: &str) -> Vec<Rule> {
32 let files = std::fs::read_dir(directory).expect("Failed to read directory.");
33
34 let mut rules = Vec::new();
35 for file in files {
36 let file = file.expect("Failed to read file.");
37 let path = file.path();
38 let path = path.to_str().expect("Failed to convert path to string.");
39
40 let rule_file = parse_file(Path::new(path));
41 rules.push(rule_file);
42 }
43 rules
44}
45
46fn gen_match_rules(rules: &[Rule]) -> TokenStream {
47 let command = rules
48 .iter()
49 .map(|x| x.command.to_owned())
50 .collect::<Vec<String>>();
51 let command_matches = rules
52 .iter()
53 .map(|x| {
54 x.match_err
55 .iter()
56 .map(|x| {
57 let pattern = x
58 .pattern
59 .iter()
60 .map(|x| x.to_lowercase())
61 .collect::<Vec<String>>();
62 let suggests = x
63 .suggest
64 .iter()
65 .map(|x| x.to_string())
66 .collect::<Vec<String>>();
67 (pattern, suggests)
68 })
69 .collect::<Vec<(Vec<String>, Vec<String>)>>()
70 })
71 .collect::<Vec<Vec<(Vec<String>, Vec<String>)>>>();
72
73 let mut matches_tokens = Vec::new();
74
75 for match_err in command_matches {
76 let mut suggestion_tokens = Vec::new();
77 let mut patterns_tokens = Vec::new();
78 for (pattern, suggests) in match_err {
79 let mut pattern_suggestions = Vec::new();
81 for suggest in suggests {
82 let (suggestion_no_condition, conditions) = parse_conditions(&suggest);
83 let suggest = eval_suggest(&suggestion_no_condition);
84 let suggestion = quote! {
85 if #(#conditions)&&* {
86 #suggest;
87 };
88 };
89 pattern_suggestions.push(suggestion);
90 }
91 let match_tokens = quote! {
92 #(#pattern_suggestions)*
93 };
94
95 suggestion_tokens.push(match_tokens);
96
97 let string_patterns = pattern.join("\", \"");
98 let string_patterns: TokenStream2 =
99 format!("[\"{}\"]", string_patterns).parse().unwrap();
100 patterns_tokens.push(string_patterns);
101 }
102
103 matches_tokens.push(quote! {
104 #(
105 for pattern in #patterns_tokens {
106 if error_msg.contains(pattern) {
107 #suggestion_tokens;
108 break;
109 };
110 })*
111 })
112 }
113 quote! {
114 let mut last_command = last_command.to_string();
115 match executable {
116 #(
117 #command => {
118 #matches_tokens
119 }
120 )*
121 _ => {}
122 };
123 }
124 .into()
125}
126
127fn parse_file(file: &Path) -> Rule {
128 let file = std::fs::read_to_string(file).expect("Failed to read file.");
129 toml::from_str(&file).expect("Failed to parse toml.")
130}
131
132fn parse_conditions(suggest: &str) -> (String, Vec<TokenStream2>) {
133 let mut eval_conditions = Vec::new();
134 if suggest.starts_with('#') {
135 let mut lines = suggest.lines().collect::<Vec<&str>>();
136 let mut conditions = String::new();
137 for (i, line) in lines[0..].iter().enumerate() {
138 conditions.push_str(line);
139 if line.ends_with(']') {
140 lines = lines[i + 1..].to_vec();
141 break;
142 }
143 }
144 let conditions = conditions
145 .trim_start_matches(['#', '['])
146 .trim_end_matches(']')
147 .split(',')
148 .collect::<Vec<&str>>();
149
150 for condition in conditions {
151 let (mut condition, arg) = condition.split_once('(').unwrap();
152 condition = condition.trim();
153 let arg = arg
156 .to_string()
157 .chars()
158 .take(arg.len() - 1)
159 .collect::<String>();
160
161 let reverse = match condition.starts_with('!') {
162 true => {
163 condition = condition.trim_start_matches('!');
164 true
165 }
166 false => false,
167 };
168 let evaluated_condition = eval_condition(condition, &arg);
169
170 eval_conditions.push(quote! {#evaluated_condition == !#reverse});
171 }
172 let suggest = lines.join("\n");
173 return (suggest, eval_conditions);
174 }
175 (suggest.to_owned(), vec![quote! {true}])
176}
177
178fn eval_condition(condition: &str, arg: &str) -> TokenStream2 {
179 match condition {
180 "executable" => quote! {executables.contains(&#arg.to_string())},
181 "err_contains" => quote! {regex_match(#arg, &error_msg)},
182 "cmd_contains" => quote! {regex_match(#arg, &last_command)},
183 "min_length" => quote! {(split.len() >= #arg.parse::<usize>().unwrap())},
184 "length" => quote! {(split.len() == #arg.parse::<usize>().unwrap())},
185 "max_length" => quote! {(split.len() <= #arg.parse::<usize>().unwrap() + 1)},
186 "shell" => quote! {(shell == #arg)},
187 _ => unreachable!("Unknown condition when evaluation condition: {}", condition),
188 }
189}
190
191fn eval_suggest(suggest: &str) -> TokenStream2 {
192 let mut suggest = suggest.to_owned();
193 if suggest.contains("{{command}}") {
194 suggest = suggest.replace("{{command}}", "{last_command}");
195 }
196
197 let mut replace_list = Vec::new();
198 let mut select_list = Vec::new();
199 let mut opt_list = Vec::new();
200 let mut cmd_list = Vec::new();
201
202 replaces::opts(&mut suggest, &mut replace_list, &mut opt_list);
203 replaces::cmd_reg(&mut suggest, &mut replace_list);
204 replaces::err(&mut suggest, &mut replace_list);
205 replaces::command(&mut suggest, &mut replace_list);
206 replaces::shell(&mut suggest, &mut cmd_list);
207 replaces::typo(&mut suggest, &mut replace_list);
208 replaces::select(&mut suggest, &mut select_list);
209 replaces::shell_tag(&mut suggest, &mut replace_list, &cmd_list);
210
211 let suggests = if select_list.is_empty() {
212 quote! {
213 candidates.push(format!{#suggest, #(#replace_list),*});
214 }
215 } else {
216 quote! {
217 #(#select_list)*
218 let suggest = format!{#suggest, #(#replace_list),*};
219 for select in selects {
220 let suggest = suggest.replace("{{selection}}", &select);
221 candidates.push(suggest);
222 }
223 }
224 };
225
226 quote! {
227 #(#opt_list)*
228 #suggests
229 }
230}