Skip to main content

aspect_core/pointcut/
parser.rs

1//! Parser for pointcut expressions.
2//!
3//! Parses pointcut strings like:
4//! - `execution(pub fn *(..))`
5//! - `within(crate::api)`
6//! - `execution(pub fn *(..)) && within(crate::api)`
7//! - `(execution(pub fn *(..)) || within(crate::admin)) && !within(crate::internal)`
8
9use super::ast::Pointcut;
10use super::pattern::{ExecutionPattern, ModulePattern, NamePattern, Visibility};
11
12/// Parse a pointcut expression from a string.
13///
14/// # Examples
15///
16/// ```rust
17/// use aspect_core::pointcut::parse_pointcut;
18///
19/// let pc = parse_pointcut("execution(pub fn *(..))").unwrap();
20/// let pc = parse_pointcut("within(crate::api)").unwrap();
21/// let pc = parse_pointcut("execution(pub fn *(..)) && within(crate::api)").unwrap();
22/// ```
23pub fn parse_pointcut(input: &str) -> Result<Pointcut, String> {
24    let input = input.trim();
25
26    // Handle parentheses for grouping
27    if input.starts_with('(') && input.ends_with(')') {
28        // Check if this is a balanced outer parenthesis
29        if let Some(inner) = strip_outer_parens(input) {
30            return parse_pointcut(inner);
31        }
32    }
33
34    // Handle NOT operator (highest precedence)
35    if input.starts_with('!') {
36        let inner = parse_pointcut(input[1..].trim())?;
37        return Ok(Pointcut::Not(Box::new(inner)));
38    }
39
40    // Handle OR operator (lowest precedence)
41    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    // Handle AND operator (medium precedence)
48    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    // Parse basic pointcuts
55    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
64/// Strip outer parentheses if they are balanced and wrap the entire expression.
65fn 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    // Check if the parentheses are balanced for the entire inner content
73    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; // Unbalanced
81                }
82            }
83            _ => {}
84        }
85    }
86
87    if depth == 0 {
88        Some(inner)
89    } else {
90        None
91    }
92}
93
94/// Find an operator outside of parentheses.
95/// Returns the position of the operator, or None if not found.
96fn 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
119/// Parse an execution pointcut: `execution(pub fn save(..))`
120fn 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    // Parse visibility
128    let (visibility, rest) = parse_visibility(content);
129
130    // Expect "fn" keyword
131    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    // Parse function name pattern
138    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    // TODO: Parse parameters and return type
147
148    Ok(Pointcut::Execution(ExecutionPattern {
149        visibility,
150        name: name_pattern,
151        return_type: None,
152    }))
153}
154
155/// Parse a within pointcut: `within(crate::api)`
156fn 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
168/// Parse visibility from the beginning of a string.
169/// Returns (Option<Visibility>, remaining_string)
170fn 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
182/// Parse a name pattern (exact, wildcard, prefix, suffix).
183fn 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        // (A || B) && C should parse as AND with OR on the left
288        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        // Without parens: A || B && C should parse as A || (B && C)
305        // because AND has higher precedence than OR
306        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    // Property-based tests
327    #[cfg(test)]
328    mod proptests {
329        use super::*;
330        use proptest::prelude::*;
331
332        // Generate valid function names
333        fn arb_function_name() -> impl Strategy<Value = String> {
334            prop::string::string_regex("[a-z_][a-z0-9_]*").unwrap()
335        }
336
337        // Generate valid module paths
338        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        // Generate valid visibility
344        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                // Should either parse or return error, never panic
365                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                // Should parse successfully
385                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                // Should parse successfully
398                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                    // Should be an Execution pointcut
414                    prop_assert!(matches!(pc, Pointcut::Execution(_)));
415                }
416            }
417        }
418    }
419}