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 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 match_condition = Vec::new();
80			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			// remove only the last character which is ')'
154			// other ')' are kept for regex
155			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}