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("\"###, 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 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" && !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		// remove only the last character which is ')'
268		// other ')' are kept for regex
269		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}