pay_respects_parser/
lib.rs1use itertools::sorted_unstable;
6use pay_respects_utils::strings::split_unescaped_character;
7use std::path::Path;
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12
13mod replaces;
14
15#[proc_macro]
16pub fn parse_rules(input: TokenStream) -> TokenStream {
17 let rules = get_rules(input.to_string().trim_matches('"'));
18 gen_match_rules(&rules)
19}
20
21#[proc_macro]
22pub fn parse_inline_rules(input: TokenStream) -> TokenStream {
23 let rules = get_rules(input.to_string().trim_matches('"'));
24 gen_inline_rules(&rules)
25}
26
27#[derive(serde::Deserialize)]
28struct Rule {
29 command: String,
30 extends: Option<Vec<String>>,
31 match_err: Vec<MatchError>,
32}
33
34#[derive(serde::Deserialize)]
35struct MatchError {
36 pattern: Option<Vec<String>>,
37 suggest: Vec<String>,
38}
39
40fn get_rules(directory: &str) -> Vec<Rule> {
41 let files = std::fs::read_dir(directory)
42 .expect("Failed to read directory.")
43 .map(|entry| {
44 let entry = entry.expect("Failed to read directory entry.");
45 entry
46 .path()
47 .to_str()
48 .expect("Failed to convert path to string.")
49 .to_string()
50 });
51 let files = sorted_unstable(files).collect::<Vec<String>>();
52
53 let mut rules = Vec::new();
54 for path in files {
55 let rule_file = parse_file(Path::new(&path));
56 rules.push(rule_file);
57 }
58 rules
59}
60
61fn gen_match_rules(rules: &[Rule]) -> TokenStream {
62 let command = rule_commands(rules);
63 let command_matches = parse_match_err(rules);
64
65 let mut matches_tokens = Vec::new();
66
67 for match_err in command_matches {
68 let mut suggestion_tokens = Vec::new();
69 let mut patterns_tokens = Vec::new();
70 for (pattern, suggests) in match_err {
71 let mut pattern_suggestions = Vec::new();
73 for suggest in suggests {
74 let (suggestion_no_condition, mut conditions) = parse_conditions(&suggest);
75 if let Some(conditions) = &mut conditions {
76 conditions.retain(|x| x != "INLINE");
77 }
78
79 let suggestion = parse_suggestion(&suggestion_no_condition, conditions);
80 pattern_suggestions.push(suggestion);
81 }
82 let match_tokens = quote! {
83 #(#pattern_suggestions)*
84 };
85
86 suggestion_tokens.push(match_tokens);
87
88 if let Some(pattern) = pattern {
89 let string_patterns = pattern.join("\", \"");
90 let string_patterns: TokenStream2 =
91 format!("[\"{}\"]", string_patterns).parse().unwrap();
92 patterns_tokens.push(string_patterns);
93 } else {
94 patterns_tokens.push("[\"\"]".parse().unwrap());
95 }
96 }
97
98 matches_tokens.push(quote! {
99 #(
100 for pattern in #patterns_tokens {
101 if error_lower.contains(pattern) {
102 #suggestion_tokens;
103 break;
104 };
105 })*
106 })
107 }
108 quote! {
109 let mut last_command = last_command.to_string();
110 match executable {
111 #(
112 #command => {
113 #matches_tokens
114 }
115 )*
116 _ => {}
117 };
118 }
119 .into()
120}
121
122fn gen_inline_rules(rules: &[Rule]) -> TokenStream {
123 let command = rule_commands(rules);
124 let command_matches = parse_match_err(rules);
125
126 let mut matches_tokens = Vec::new();
127
128 for match_err in command_matches {
129 let mut suggestion_tokens = Vec::new();
130 for (_, suggests) in match_err {
131 let mut pattern_suggestions = Vec::new();
133 for suggest in suggests {
134 let (suggestion_no_condition, mut conditions) = parse_conditions(&suggest);
135 if conditions.is_none() {
136 continue;
137 }
138 if let Some(conditions) = &mut conditions {
139 if !conditions.contains(&"INLINE".to_string()) {
140 continue;
141 }
142 conditions.retain(|x| {
143 x != "INLINE"
144 && x != "FUNCTION" && !x.starts_with("err")
145 && !x.starts_with("!err")
146 });
147 }
148
149 let suggestion = parse_suggestion(&suggestion_no_condition, conditions);
150 pattern_suggestions.push(suggestion);
151 }
152 let match_tokens = quote! {
153 #(#pattern_suggestions)*
154 };
155
156 if match_tokens.is_empty() {
157 continue;
158 }
159 suggestion_tokens.push(match_tokens);
160 }
161
162 matches_tokens.push(quote! {
163 #(
164 #suggestion_tokens;
165 )*
166 })
167 }
168 quote! {
169 let mut last_command = last_command.to_string();
170 match executable {
171 #(
172 #command => {
173 #matches_tokens
174 }
175 )*
176 _ => {}
177 };
178 }
179 .into()
180}
181
182#[allow(clippy::type_complexity)]
183fn parse_match_err(rules: &[Rule]) -> Vec<Vec<(Option<Vec<String>>, Vec<String>)>> {
184 rules
185 .iter()
186 .map(|x| {
187 x.match_err
188 .iter()
189 .map(|x| {
190 let pattern = if let Some(pattern) = &x.pattern {
191 let pattern = pattern
192 .iter()
193 .map(|x| x.to_lowercase())
194 .collect::<Vec<String>>();
195 Some(pattern)
196 } else {
197 None
198 };
199 let suggests = x
200 .suggest
201 .iter()
202 .map(|x| x.to_string())
203 .collect::<Vec<String>>();
204 (pattern, suggests)
205 })
206 .collect::<Vec<(Option<Vec<String>>, Vec<String>)>>()
207 })
208 .collect::<Vec<Vec<(Option<Vec<String>>, Vec<String>)>>>()
209}
210
211fn rule_commands(rules: &[Rule]) -> Vec<TokenStream2> {
212 rules
213 .iter()
214 .map(|x| {
215 if let Some(extends) = &x.extends {
216 format!(
217 "\"{}\"|{}",
218 x.command,
219 extends
220 .iter()
221 .map(|x| format!("\"{}\"", x))
222 .collect::<Vec<String>>()
223 .join("|")
224 )
225 .parse()
226 .unwrap()
227 } else {
228 format!("\"{}\"", x.command).parse().unwrap()
229 }
230 })
231 .collect::<Vec<TokenStream2>>()
232}
233
234fn parse_file(file: &Path) -> Rule {
235 let file = std::fs::read_to_string(file).expect("Failed to read file.");
236 toml::from_str(&file).expect("Failed to parse toml.")
237}
238
239fn parse_conditions(suggest: &str) -> (String, Option<Vec<String>>) {
240 if suggest.starts_with('#') {
241 let mut lines = suggest.lines().collect::<Vec<&str>>();
242 let mut conditions = String::new();
243 for (i, line) in lines[0..].iter().enumerate() {
244 conditions.push_str(line);
245 if line.ends_with(']') {
246 lines = lines[i + 1..].to_vec();
247 break;
248 }
249 }
250
251 let conditions = conditions
252 .trim_start_matches(['#', '['])
253 .trim_end_matches(']');
254 let conditions = split_unescaped_character(conditions, ',')
255 .into_iter()
256 .map(|x| x.trim().to_string())
257 .collect::<Vec<String>>();
258 let suggest = lines.join("\n");
259 return (suggest, Some(conditions));
260 }
261 (suggest.to_owned(), None)
262}
263
264fn tokenize_conditions(conditions: &[String]) -> Vec<TokenStream2> {
265 let mut eval_conditions = Vec::new();
266 for condition in conditions {
267 let (mut condition, arg) = condition.split_once('(').unwrap();
268 condition = condition.trim();
269 let arg = arg
272 .to_string()
273 .chars()
274 .take(arg.len() - 1)
275 .collect::<String>();
276
277 let reverse = match condition.starts_with('!') {
278 true => {
279 condition = condition.trim_start_matches('!');
280 true
281 }
282 false => false,
283 };
284 let evaluated_condition = eval_condition(condition, &arg);
285
286 eval_conditions.push(quote! {#evaluated_condition == !#reverse});
287 }
288 eval_conditions
289}
290
291fn parse_suggestion(suggestion: &str, conditions: Option<Vec<String>>) -> TokenStream2 {
292 if conditions.is_none() {
293 return eval_suggest(suggestion);
294 }
295 let (conditions, is_function) = {
296 let mut conditions = conditions.unwrap();
297 if conditions.contains(&"FUNCTION".to_string()) {
298 conditions.retain(|x| x != "FUNCTION");
299 (conditions, true)
300 } else {
301 (conditions, false)
302 }
303 };
304
305 let suggest = if is_function {
306 let suggestion: TokenStream2 = suggestion.trim_matches('"').parse().unwrap();
307 quote! {
308 rules_function(#suggestion, &error_msg, &error_lower, &shell, &last_command, &executables, &split, &mut candidates);
309 }
310 } else {
311 eval_suggest(suggestion)
312 };
313
314 if conditions.is_empty() {
315 return suggest;
316 }
317
318 let conditions = tokenize_conditions(&conditions);
319 quote! {
320 if #(#conditions)&&* {
321 #suggest
322 }
323 }
324}
325
326fn eval_condition(condition: &str, arg: &str) -> TokenStream2 {
327 match condition {
328 "executable" => quote! {executables.contains(&#arg.to_string())},
329 "err_contains" => quote! {regex_match(#arg, &error_lower)},
330 "cmd_contains" => quote! {regex_match(#arg, &last_command)},
331 "min_length" => quote! {(split.len() >= #arg.parse::<usize>().unwrap())},
332 "length" => quote! {(split.len() == #arg.parse::<usize>().unwrap())},
333 "max_length" => quote! {(split.len() <= #arg.parse::<usize>().unwrap() + 1)},
334 "shell" => quote! {(shell == #arg)},
335 _ => unreachable!("Unknown condition when evaluation condition: {}", condition),
336 }
337}
338
339fn eval_suggest(suggest: &str) -> TokenStream2 {
340 let mut suggest = suggest.to_owned();
341 if suggest.contains("{{command}}") {
342 suggest = suggest.replace("{{command}}", "{last_command}");
343 }
344
345 let mut replace_list = Vec::new();
346 let mut select_list = Vec::new();
347 let mut opt_list = Vec::new();
348 let mut cmd_list = Vec::new();
349
350 replaces::opts(&mut suggest, &mut replace_list, &mut opt_list);
351 replaces::cmd_reg(&mut suggest, &mut replace_list);
352 replaces::err(&mut suggest, &mut replace_list);
353 replaces::command(&mut suggest, &mut replace_list);
354 replaces::shell(&mut suggest, &mut cmd_list);
355 replaces::typo(&mut suggest, &mut replace_list);
356 replaces::select(&mut suggest, &mut select_list);
357 replaces::shell_tag(&mut suggest, &mut replace_list, &cmd_list);
358
359 let suggests = if select_list.is_empty() {
360 quote! {
361 candidates.push(format!{#suggest, #(#replace_list),*});
362 }
363 } else {
364 quote! {
365 #(#select_list)*
366 let suggest = format!{#suggest, #(#replace_list),*};
367 for select in selects {
368 let suggest = suggest.replace("{{selection}}", &select);
369 candidates.push(suggest);
370 }
371 }
372 };
373
374 quote! {
375 #(#opt_list)*
376 #suggests
377 }
378}