aspect_core/pointcut/
parser.rs1use super::ast::Pointcut;
10use super::pattern::{ExecutionPattern, ModulePattern, NamePattern, Visibility};
11
12pub fn parse_pointcut(input: &str) -> Result<Pointcut, String> {
24 let input = input.trim();
25
26 if input.starts_with('(') && input.ends_with(')') {
28 if let Some(inner) = strip_outer_parens(input) {
30 return parse_pointcut(inner);
31 }
32 }
33
34 if input.starts_with('!') {
36 let inner = parse_pointcut(input[1..].trim())?;
37 return Ok(Pointcut::Not(Box::new(inner)));
38 }
39
40 if let Some(or_pos) = find_operator(input, " || ") {
42 let left = parse_pointcut(&input[..or_pos])?;
43 let right = parse_pointcut(&input[or_pos + 4..])?;
44 return Ok(Pointcut::Or(Box::new(left), Box::new(right)));
45 }
46
47 if let Some(and_pos) = find_operator(input, " && ") {
49 let left = parse_pointcut(&input[..and_pos])?;
50 let right = parse_pointcut(&input[and_pos + 4..])?;
51 return Ok(Pointcut::And(Box::new(left), Box::new(right)));
52 }
53
54 if input.starts_with("execution(") {
56 parse_execution(input)
57 } else if input.starts_with("within(") {
58 parse_within(input)
59 } else {
60 Err(format!("Unknown pointcut type: {}", input))
61 }
62}
63
64fn strip_outer_parens(input: &str) -> Option<&str> {
66 if !input.starts_with('(') || !input.ends_with(')') {
67 return None;
68 }
69
70 let inner = &input[1..input.len() - 1];
71
72 let mut depth = 0;
74 for ch in inner.chars() {
75 match ch {
76 '(' => depth += 1,
77 ')' => {
78 depth -= 1;
79 if depth < 0 {
80 return None; }
82 }
83 _ => {}
84 }
85 }
86
87 if depth == 0 {
88 Some(inner)
89 } else {
90 None
91 }
92}
93
94fn find_operator(input: &str, operator: &str) -> Option<usize> {
97 let mut depth = 0;
98 let op_len = operator.len();
99 let chars: Vec<char> = input.chars().collect();
100
101 for i in 0..chars.len() {
102 match chars[i] {
103 '(' => depth += 1,
104 ')' => depth -= 1,
105 _ => {
106 if depth == 0 && i + op_len <= chars.len() {
107 let slice: String = chars[i..i + op_len].iter().collect();
108 if slice == operator {
109 return Some(i);
110 }
111 }
112 }
113 }
114 }
115
116 None
117}
118
119fn parse_execution(input: &str) -> Result<Pointcut, String> {
121 if !input.starts_with("execution(") || !input.ends_with(')') {
122 return Err("Invalid execution syntax".to_string());
123 }
124
125 let content = &input[10..input.len() - 1].trim();
126
127 let (visibility, rest) = parse_visibility(content);
129
130 let rest = rest.trim();
132 if !rest.starts_with("fn ") {
133 return Err("Expected 'fn' keyword".to_string());
134 }
135 let rest = &rest[3..].trim();
136
137 let name = if let Some(paren_pos) = rest.find('(') {
139 &rest[..paren_pos].trim()
140 } else {
141 return Err("Expected function signature".to_string());
142 };
143
144 let name_pattern = parse_name_pattern(name);
145
146 Ok(Pointcut::Execution(ExecutionPattern {
149 visibility,
150 name: name_pattern,
151 return_type: None,
152 }))
153}
154
155fn parse_within(input: &str) -> Result<Pointcut, String> {
157 if !input.starts_with("within(") || !input.ends_with(')') {
158 return Err("Invalid within syntax".to_string());
159 }
160
161 let module_path = input[7..input.len() - 1].trim();
162
163 Ok(Pointcut::Within(ModulePattern {
164 path: module_path.to_string(),
165 }))
166}
167
168fn parse_visibility(input: &str) -> (Option<Visibility>, &str) {
171 if input.starts_with("pub(crate) ") {
172 (Some(Visibility::Crate), &input[11..])
173 } else if input.starts_with("pub(super) ") {
174 (Some(Visibility::Super), &input[11..])
175 } else if input.starts_with("pub ") {
176 (Some(Visibility::Public), &input[4..])
177 } else {
178 (None, input)
179 }
180}
181
182fn parse_name_pattern(name: &str) -> NamePattern {
184 if name == "*" {
185 NamePattern::Wildcard
186 } else if name.starts_with('*') && name.ends_with('*') && name.len() > 2 {
187 NamePattern::Contains(name[1..name.len() - 1].to_string())
188 } else if name.starts_with('*') {
189 NamePattern::Suffix(name[1..].to_string())
190 } else if name.ends_with('*') {
191 NamePattern::Prefix(name[..name.len() - 1].to_string())
192 } else {
193 NamePattern::Exact(name.to_string())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_parse_execution_wildcard() {
203 let pc = parse_pointcut("execution(pub fn *(..))").unwrap();
204 match pc {
205 Pointcut::Execution(pattern) => {
206 assert_eq!(pattern.visibility, Some(Visibility::Public));
207 assert_eq!(pattern.name, NamePattern::Wildcard);
208 }
209 _ => panic!("Expected Execution pointcut"),
210 }
211 }
212
213 #[test]
214 fn test_parse_execution_exact_name() {
215 let pc = parse_pointcut("execution(fn save_user(..))").unwrap();
216 match pc {
217 Pointcut::Execution(pattern) => {
218 assert_eq!(pattern.visibility, None);
219 assert_eq!(pattern.name, NamePattern::Exact("save_user".to_string()));
220 }
221 _ => panic!("Expected Execution pointcut"),
222 }
223 }
224
225 #[test]
226 fn test_parse_execution_prefix() {
227 let pc = parse_pointcut("execution(pub fn save*(..))").unwrap();
228 match pc {
229 Pointcut::Execution(pattern) => {
230 assert_eq!(pattern.name, NamePattern::Prefix("save".to_string()));
231 }
232 _ => panic!("Expected Execution pointcut"),
233 }
234 }
235
236 #[test]
237 fn test_parse_within() {
238 let pc = parse_pointcut("within(crate::api)").unwrap();
239 match pc {
240 Pointcut::Within(pattern) => {
241 assert_eq!(pattern.path, "crate::api");
242 }
243 _ => panic!("Expected Within pointcut"),
244 }
245 }
246
247 #[test]
248 fn test_parse_and() {
249 let pc = parse_pointcut("execution(pub fn *(..)) && within(crate::api)").unwrap();
250 match pc {
251 Pointcut::And(left, right) => {
252 assert!(matches!(*left, Pointcut::Execution(_)));
253 assert!(matches!(*right, Pointcut::Within(_)));
254 }
255 _ => panic!("Expected And pointcut"),
256 }
257 }
258
259 #[test]
260 fn test_parse_or() {
261 let pc = parse_pointcut("execution(fn save(..)) || execution(fn update(..))").unwrap();
262 match pc {
263 Pointcut::Or(_, _) => {}
264 _ => panic!("Expected Or pointcut"),
265 }
266 }
267
268 #[test]
269 fn test_parse_not() {
270 let pc = parse_pointcut("!within(crate::internal)").unwrap();
271 match pc {
272 Pointcut::Not(inner) => {
273 assert!(matches!(*inner, Pointcut::Within(_)));
274 }
275 _ => panic!("Expected Not pointcut"),
276 }
277 }
278
279 #[test]
280 fn test_parse_parentheses() {
281 let pc = parse_pointcut("(execution(pub fn *(..)))").unwrap();
282 assert!(matches!(pc, Pointcut::Execution(_)));
283 }
284
285 #[test]
286 fn test_parse_complex_with_parentheses() {
287 let pc = parse_pointcut(
289 "(execution(pub fn *(..)) || within(crate::admin)) && within(crate::api)",
290 )
291 .unwrap();
292
293 match pc {
294 Pointcut::And(left, right) => {
295 assert!(matches!(*left, Pointcut::Or(_, _)));
296 assert!(matches!(*right, Pointcut::Within(_)));
297 }
298 _ => panic!("Expected And with Or on left"),
299 }
300 }
301
302 #[test]
303 fn test_parse_operator_precedence() {
304 let pc1 = parse_pointcut(
307 "execution(fn a(..)) || execution(fn b(..)) && within(crate::api)",
308 )
309 .unwrap();
310
311 match pc1 {
312 Pointcut::Or(left, right) => {
313 assert!(matches!(*left, Pointcut::Execution(_)));
314 assert!(matches!(*right, Pointcut::And(_, _)));
315 }
316 _ => panic!("Expected Or with And on right"),
317 }
318 }
319
320 #[test]
321 fn test_parse_nested_parentheses() {
322 let pc = parse_pointcut("((execution(pub fn *(..))))").unwrap();
323 assert!(matches!(pc, Pointcut::Execution(_)));
324 }
325
326 #[cfg(test)]
328 mod proptests {
329 use super::*;
330 use proptest::prelude::*;
331
332 fn arb_function_name() -> impl Strategy<Value = String> {
334 prop::string::string_regex("[a-z_][a-z0-9_]*").unwrap()
335 }
336
337 fn arb_module_path() -> impl Strategy<Value = String> {
339 prop::collection::vec(arb_function_name(), 1..5)
340 .prop_map(|parts| format!("crate::{}", parts.join("::")))
341 }
342
343 fn arb_visibility() -> impl Strategy<Value = &'static str> {
345 prop_oneof![
346 Just("pub"),
347 Just("pub(crate)"),
348 Just("pub(super)"),
349 Just(""),
350 ]
351 }
352
353 proptest! {
354 #[test]
355 fn parse_execution_never_panics(
356 vis in arb_visibility(),
357 name in arb_function_name()
358 ) {
359 let expr = if vis.is_empty() {
360 format!("execution(fn {}(..))", name)
361 } else {
362 format!("execution({} fn {}(..))", vis, name)
363 };
364 let _ = parse_pointcut(&expr);
366 }
367
368 #[test]
369 fn parse_within_never_panics(path in arb_module_path()) {
370 let expr = format!("within({})", path);
371 let _ = parse_pointcut(&expr);
372 }
373
374 #[test]
375 fn parse_and_is_associative(
376 name1 in arb_function_name(),
377 name2 in arb_function_name(),
378 path in arb_module_path()
379 ) {
380 let expr = format!(
381 "execution(fn {}(..)) && execution(fn {}(..)) && within({})",
382 name1, name2, path
383 );
384 prop_assert!(parse_pointcut(&expr).is_ok());
386 }
387
388 #[test]
389 fn parse_with_random_parentheses(
390 name in arb_function_name(),
391 extra_parens in 0usize..3
392 ) {
393 let mut expr = format!("execution(fn {}(..))", name);
394 for _ in 0..extra_parens {
395 expr = format!("({})", expr);
396 }
397 prop_assert!(parse_pointcut(&expr).is_ok());
399 }
400
401 #[test]
402 fn roundtrip_basic_patterns(
403 vis in arb_visibility(),
404 name in arb_function_name()
405 ) {
406 let expr = if vis.is_empty() {
407 format!("execution(fn {}(..))", name)
408 } else {
409 format!("execution({} fn {}(..))", vis, name)
410 };
411
412 if let Ok(pc) = parse_pointcut(&expr) {
413 prop_assert!(matches!(pc, Pointcut::Execution(_)));
415 }
416 }
417 }
418 }
419}