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("\"###, r###\"");
90 let string_patterns: TokenStream2 =
91 format!("[r###\"{}\"###]", 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" && !x.starts_with("err") && !x.starts_with("!err")
144 });
145 }
146
147 let suggestion = parse_suggestion(&suggestion_no_condition, conditions);
148 pattern_suggestions.push(suggestion);
149 }
150 let match_tokens = quote! {
151 #(#pattern_suggestions)*
152 };
153
154 if match_tokens.is_empty() {
155 continue;
156 }
157 suggestion_tokens.push(match_tokens);
158 }
159
160 matches_tokens.push(quote! {
161 #(
162 #suggestion_tokens;
163 )*
164 })
165 }
166 quote! {
167 let mut last_command = last_command.to_string();
168 match executable {
169 #(
170 #command => {
171 #matches_tokens
172 }
173 )*
174 _ => {}
175 };
176 }
177 .into()
178}
179
180#[allow(clippy::type_complexity)]
181fn parse_match_err(rules: &[Rule]) -> Vec<Vec<(Option<Vec<String>>, Vec<String>)>> {
182 rules
183 .iter()
184 .map(|x| {
185 x.match_err
186 .iter()
187 .map(|x| {
188 let pattern = if let Some(pattern) = &x.pattern {
189 let pattern = pattern
190 .iter()
191 .map(|x| x.to_lowercase())
192 .collect::<Vec<String>>();
193 Some(pattern)
194 } else {
195 None
196 };
197 let suggests = x
198 .suggest
199 .iter()
200 .map(|x| x.to_string())
201 .collect::<Vec<String>>();
202 (pattern, suggests)
203 })
204 .collect::<Vec<(Option<Vec<String>>, Vec<String>)>>()
205 })
206 .collect::<Vec<Vec<(Option<Vec<String>>, Vec<String>)>>>()
207}
208
209fn rule_commands(rules: &[Rule]) -> Vec<TokenStream2> {
210 rules
211 .iter()
212 .map(|x| {
213 if let Some(extends) = &x.extends {
214 format!(
215 "\"{}\"|{}",
216 x.command,
217 extends
218 .iter()
219 .map(|x| format!("\"{}\"", x))
220 .collect::<Vec<String>>()
221 .join("|")
222 )
223 .parse()
224 .unwrap()
225 } else {
226 format!("\"{}\"", x.command).parse().unwrap()
227 }
228 })
229 .collect::<Vec<TokenStream2>>()
230}
231
232fn parse_file(file: &Path) -> Rule {
233 let file = std::fs::read_to_string(file).expect("Failed to read file.");
234 toml::from_str(&file).expect("Failed to parse toml.")
235}
236
237fn parse_conditions(suggest: &str) -> (String, Option<Vec<String>>) {
238 if suggest.starts_with('#') {
239 let mut lines = suggest.lines().collect::<Vec<&str>>();
240 let mut conditions = String::new();
241 for (i, line) in lines[0..].iter().enumerate() {
242 conditions.push_str(line);
243 if line.ends_with(']') {
244 lines = lines[i + 1..].to_vec();
245 break;
246 }
247 }
248
249 let conditions = conditions
250 .trim_start_matches(['#', '['])
251 .trim_end_matches(']');
252 let conditions = split_unescaped_character(conditions, ',')
253 .into_iter()
254 .map(|x| x.trim().to_string())
255 .collect::<Vec<String>>();
256 let suggest = lines.join("\n");
257 return (suggest, Some(conditions));
258 }
259 (suggest.to_owned(), None)
260}
261
262fn tokenize_conditions(conditions: &[String]) -> Vec<TokenStream2> {
263 let mut eval_conditions = Vec::new();
264 for condition in conditions {
265 let (mut condition, arg) = condition.split_once('(').unwrap();
266 condition = condition.trim();
267 let arg = arg
270 .to_string()
271 .chars()
272 .take(arg.len() - 1)
273 .collect::<String>();
274
275 let reverse = match condition.starts_with('!') {
276 true => {
277 condition = condition.trim_start_matches('!');
278 true
279 }
280 false => false,
281 };
282 let evaluated_condition = eval_condition(condition, &arg);
283
284 eval_conditions.push(quote! {#evaluated_condition == !#reverse});
285 }
286 eval_conditions
287}
288
289fn parse_suggestion(suggestion: &str, conditions: Option<Vec<String>>) -> TokenStream2 {
290 if conditions.is_none() {
291 return eval_suggest(suggestion);
292 }
293 let (conditions, is_function) = {
294 let mut conditions = conditions.unwrap();
295 if conditions.contains(&"FUNCTION".to_string()) {
296 conditions.retain(|x| x != "FUNCTION");
297 (conditions, true)
298 } else {
299 (conditions, false)
300 }
301 };
302
303 let suggest = if is_function {
304 let suggestion: TokenStream2 = suggestion.trim_matches('"').parse().unwrap();
305 quote! {
306 rules_function(#suggestion, &error_msg, &error_lower, &shell, &last_command, &executables, &split, &mut candidates, data);
307 }
308 } else {
309 eval_suggest(suggestion)
310 };
311
312 if conditions.is_empty() {
313 return suggest;
314 }
315
316 let conditions = tokenize_conditions(&conditions);
317 quote! {
318 if #(#conditions)&&* {
319 #suggest
320 }
321 }
322}
323
324fn eval_condition(condition: &str, arg: &str) -> TokenStream2 {
325 match condition {
326 "executable" => quote! {executables.contains(&#arg.to_string())},
327 "err_contains" => quote! {regex_match(#arg, &error_lower)},
328 "cmd_contains" => quote! {regex_match(#arg, &last_command)},
329 "min_length" => quote! {(split.len() >= #arg.parse::<usize>().unwrap())},
330 "length" => quote! {(split.len() == #arg.parse::<usize>().unwrap())},
331 "max_length" => quote! {(split.len() <= #arg.parse::<usize>().unwrap() + 1)},
332 "shell" => quote! {(shell == #arg)},
333 _ => unreachable!("Unknown condition when evaluation condition: {}", condition),
334 }
335}
336
337fn eval_suggest(suggest: &str) -> TokenStream2 {
338 let mut suggest = suggest.to_owned();
339 if suggest.contains("{{command}}") {
340 suggest = suggest.replace("{{command}}", "{last_command}");
341 }
342
343 let mut replace_list = Vec::new();
344 let mut select_list = Vec::new();
345 let mut opt_list = Vec::new();
346 let mut cmd_list = Vec::new();
347
348 replaces::opts(&mut suggest, &mut replace_list, &mut opt_list);
349 replaces::cmd_reg(&mut suggest, &mut replace_list);
350 replaces::err(&mut suggest, &mut replace_list);
351 replaces::command(&mut suggest, &mut replace_list);
352 replaces::shell(&mut suggest, &mut cmd_list);
353 replaces::typo(&mut suggest, &mut replace_list);
354 replaces::select(&mut suggest, &mut select_list);
355 replaces::shell_tag(&mut suggest, &mut replace_list, &cmd_list);
356
357 let suggests = if select_list.is_empty() {
358 quote! {
359 candidates.push(format!{#suggest, #(#replace_list),*});
360 }
361 } else {
362 quote! {
363 #(#select_list)*
364 let suggest = format!{#suggest, #(#replace_list),*};
365 for select in selects {
366 let suggest = suggest.replace("{{selection}}", &select);
367 candidates.push(suggest);
368 }
369 }
370 };
371
372 quote! {
373 #(#opt_list)*
374 #suggests
375 }
376}