Skip to main content

pay_respects_parser/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use 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 match_condition = Vec::new();
72			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 match_condition = Vec::new();
132			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		// remove only the last character which is ')'
270		// other ')' are kept for regex
271		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}