Skip to main content

ryo_pattern/
concrete.rs

1//! Concrete Syntax Parser
2//!
3//! Parses Rust-like pattern syntax with metavariables into `CodePattern`.
4//!
5//! # Syntax
6//!
7//! | Syntax | Meaning | Example |
8//! |--------|---------|---------|
9//! | `$VAR` | Capture single node | `$receiver.unwrap()` |
10//! | `$_` | Match but don't capture | `foo($_, $second)` |
11//! | `$...VAR` | Capture sequence (ellipsis) | `fn $name($...args)` |
12//!
13//! # Important
14//!
15//! Patterns must be **valid Rust syntax** after metavariable substitution.
16//! For example, `match $x { $_ }` is invalid because match arms require `=> body`.
17//!
18//! # Examples
19//!
20//! ```ignore
21//! use ryo_pattern::concrete::parse_pattern;
22//!
23//! // Parse a method call pattern
24//! let pattern = parse_pattern("$receiver.unwrap()").unwrap();
25//!
26//! // Parse a macro call pattern
27//! let pattern = parse_pattern("vec![$x]").unwrap();
28//!
29//! // Parse a match expression (requires complete arm syntax)
30//! let pattern = parse_pattern("match $x { _ => $body }").unwrap();
31//! ```
32
33use crate::{CodePattern, NameMatcher, NodeKind, PatternExpr};
34use std::collections::HashMap;
35use thiserror::Error;
36
37/// Errors that can occur during pattern parsing
38#[derive(Debug, Error)]
39pub enum ParseError {
40    /// `syn` parse failure.
41    #[error("Failed to parse pattern: {0}")]
42    SynError(#[from] syn::Error),
43
44    /// Syntax category that the concrete parser does not yet support.
45    #[error("Unsupported syntax: {0}")]
46    UnsupportedSyntax(String),
47
48    /// Metavariable name is invalid (e.g. empty or malformed).
49    #[error("Invalid metavariable: {0}")]
50    InvalidMetavariable(String),
51}
52
53/// Result type for pattern parsing
54pub type ParseResult<T> = Result<T, ParseError>;
55
56/// Parse a concrete syntax pattern string into a CodePattern
57pub fn parse_pattern(input: &str) -> ParseResult<CodePattern> {
58    let parser = ConcreteParser::new();
59    parser.parse(input)
60}
61
62/// Prefix for metavariable placeholders during parsing
63const METAVAR_PREFIX: &str = "__mv_";
64/// Prefix for ellipsis metavariable placeholders
65const ELLIPSIS_PREFIX: &str = "__ell_";
66
67/// Parser for concrete syntax patterns
68pub struct ConcreteParser {
69    // Reserved for future configuration
70}
71
72impl ConcreteParser {
73    /// Construct a new `ConcreteParser`.
74    pub fn new() -> Self {
75        Self {}
76    }
77
78    /// Parse a pattern string into CodePattern
79    pub fn parse(&self, input: &str) -> ParseResult<CodePattern> {
80        // Preprocess: replace $...VAR and $VAR with valid Rust identifiers
81        let preprocessed = self.preprocess(input);
82
83        // Try to parse as expression first
84        if let Ok(expr) = syn::parse_str::<syn::Expr>(&preprocessed) {
85            return self.convert_expr(&expr);
86        }
87
88        // Try to parse as statement
89        if let Ok(stmt) = syn::parse_str::<syn::Stmt>(&preprocessed) {
90            return self.convert_stmt(&stmt);
91        }
92
93        // Try to parse as item
94        if let Ok(item) = syn::parse_str::<syn::Item>(&preprocessed) {
95            return self.convert_item(&item);
96        }
97
98        Err(ParseError::UnsupportedSyntax(format!(
99            "Could not parse as expression, statement, or item: {}",
100            input
101        )))
102    }
103
104    /// Preprocess input to replace metavariables with valid Rust identifiers
105    fn preprocess(&self, input: &str) -> String {
106        let mut result = String::with_capacity(input.len() * 2);
107        let mut chars = input.chars().peekable();
108
109        while let Some(c) = chars.next() {
110            if c == '$' {
111                // Check for ellipsis pattern: $...VAR
112                let mut is_ellipsis = false;
113                let mut dots = String::new();
114
115                while chars.peek() == Some(&'.') {
116                    dots.push(chars.next().unwrap());
117                }
118
119                if dots.len() >= 3 {
120                    is_ellipsis = true;
121                } else {
122                    // Not ellipsis, put dots back by adding to result
123                    // Actually we can't put back, so we handle differently
124                }
125
126                // Collect the variable name
127                let mut var_name = String::new();
128                while let Some(&ch) = chars.peek() {
129                    if ch.is_alphanumeric() || ch == '_' {
130                        var_name.push(chars.next().unwrap());
131                    } else {
132                        break;
133                    }
134                }
135
136                if is_ellipsis {
137                    result.push_str(ELLIPSIS_PREFIX);
138                    result.push_str(&var_name);
139                } else if !var_name.is_empty() {
140                    result.push_str(METAVAR_PREFIX);
141                    result.push_str(&dots); // Include any dots that weren't 3+
142                    result.push_str(&var_name);
143                } else {
144                    // Standalone $ or $. - keep as is (will likely fail parse)
145                    result.push('$');
146                    result.push_str(&dots);
147                }
148            } else {
149                result.push(c);
150            }
151        }
152
153        result
154    }
155
156    /// Convert a syn::Expr to CodePattern
157    fn convert_expr(&self, expr: &syn::Expr) -> ParseResult<CodePattern> {
158        match expr {
159            syn::Expr::MethodCall(mc) => self.convert_method_call(mc),
160            syn::Expr::Call(call) => self.convert_call(call),
161            syn::Expr::Macro(mac) => self.convert_macro(mac),
162            syn::Expr::Path(path) => self.convert_path(path),
163            syn::Expr::If(if_expr) => self.convert_if(if_expr),
164            syn::Expr::Match(match_expr) => self.convert_match(match_expr),
165            syn::Expr::Block(block) => self.convert_block(block),
166            syn::Expr::Binary(bin) => self.convert_binary(bin),
167            syn::Expr::Unary(unary) => self.convert_unary(unary),
168            syn::Expr::Lit(lit) => self.convert_literal(lit),
169            syn::Expr::Try(try_expr) => self.convert_try(try_expr),
170            syn::Expr::Return(ret) => self.convert_return(ret),
171            syn::Expr::Await(await_expr) => self.convert_await(await_expr),
172            syn::Expr::Closure(closure) => self.convert_closure(closure),
173            _ => Err(ParseError::UnsupportedSyntax(
174                "Unsupported expression type".to_string(),
175            )),
176        }
177    }
178
179    /// Convert a method call expression: $receiver.method($args)
180    fn convert_method_call(&self, mc: &syn::ExprMethodCall) -> ParseResult<CodePattern> {
181        let mut pattern = CodePattern::new(NodeKind::MethodCall);
182        let mut children = HashMap::new();
183
184        // Convert receiver
185        if let Some(capture) = self.extract_metavar_from_expr(&mc.receiver) {
186            children.insert(
187                "receiver".to_string(),
188                PatternExpr::Capture(to_metavar_name(&capture)),
189            );
190        } else {
191            let receiver_pattern = self.convert_expr(&mc.receiver)?;
192            children.insert(
193                "receiver".to_string(),
194                PatternExpr::Pattern(Box::new(receiver_pattern)),
195            );
196        }
197
198        // Convert method name
199        let method_name = mc.method.to_string();
200        if is_metavariable(&method_name) {
201            children.insert(
202                "method".to_string(),
203                PatternExpr::Capture(to_metavar_name(&method_name)),
204            );
205        } else {
206            children.insert(
207                "method".to_string(),
208                PatternExpr::Name(NameMatcher::Exact(method_name)),
209            );
210        }
211
212        // Convert arguments
213        if !mc.args.is_empty() {
214            let args_pattern = self.convert_args(&mc.args)?;
215            children.insert("args".to_string(), args_pattern);
216        }
217
218        pattern.children = children;
219        Ok(pattern)
220    }
221
222    /// Convert a function call expression: func($args)
223    fn convert_call(&self, call: &syn::ExprCall) -> ParseResult<CodePattern> {
224        let mut pattern = CodePattern::new(NodeKind::FunctionCall);
225        let mut children = HashMap::new();
226
227        // Convert function
228        if let Some(capture) = self.extract_metavar_from_expr(&call.func) {
229            children.insert(
230                "func".to_string(),
231                PatternExpr::Capture(to_metavar_name(&capture)),
232            );
233        } else {
234            let func_pattern = self.convert_expr(&call.func)?;
235            children.insert(
236                "func".to_string(),
237                PatternExpr::Pattern(Box::new(func_pattern)),
238            );
239        }
240
241        // Convert arguments
242        if !call.args.is_empty() {
243            let args_pattern = self.convert_args(&call.args)?;
244            children.insert("args".to_string(), args_pattern);
245        }
246
247        pattern.children = children;
248        Ok(pattern)
249    }
250
251    /// Convert a macro call: macro!($...args)
252    fn convert_macro(&self, mac: &syn::ExprMacro) -> ParseResult<CodePattern> {
253        let mut pattern = CodePattern::new(NodeKind::MacroCall);
254        let mut children = HashMap::new();
255
256        // Get macro name
257        let macro_name = mac
258            .mac
259            .path
260            .segments
261            .last()
262            .map(|s| s.ident.to_string())
263            .unwrap_or_default();
264
265        children.insert(
266            "name".to_string(),
267            PatternExpr::Name(NameMatcher::Exact(macro_name)),
268        );
269
270        // Check if tokens contain ellipsis metavariable
271        let tokens_str = mac.mac.tokens.to_string();
272        if let Some(ellipsis_var) = extract_ellipsis_var(&tokens_str) {
273            pattern.ellipsis = true;
274            pattern.capture = Some(ellipsis_var);
275        }
276
277        pattern.children = children;
278        Ok(pattern)
279    }
280
281    /// Convert a path expression (identifier or qualified path)
282    fn convert_path(&self, path: &syn::ExprPath) -> ParseResult<CodePattern> {
283        let path_str = path_to_string(&path.path);
284
285        if is_metavariable(&path_str) {
286            // This is a metavariable capture
287            let mut pattern = CodePattern::new(NodeKind::Expr);
288            pattern.capture = Some(to_metavar_name(&path_str));
289            Ok(pattern)
290        } else {
291            let mut pattern = CodePattern::new(NodeKind::Path);
292            pattern.children.insert(
293                "path".to_string(),
294                PatternExpr::Name(NameMatcher::Exact(path_str)),
295            );
296            Ok(pattern)
297        }
298    }
299
300    /// Convert if expression
301    fn convert_if(&self, if_expr: &syn::ExprIf) -> ParseResult<CodePattern> {
302        let mut pattern = CodePattern::new(NodeKind::If);
303        let mut children = HashMap::new();
304
305        // Convert condition
306        if let Some(capture) = self.extract_metavar_from_expr(&if_expr.cond) {
307            children.insert(
308                "condition".to_string(),
309                PatternExpr::Capture(to_metavar_name(&capture)),
310            );
311        } else {
312            let cond_pattern = self.convert_expr(&if_expr.cond)?;
313            children.insert(
314                "condition".to_string(),
315                PatternExpr::Pattern(Box::new(cond_pattern)),
316            );
317        }
318
319        pattern.children = children;
320        Ok(pattern)
321    }
322
323    /// Convert match expression
324    fn convert_match(&self, match_expr: &syn::ExprMatch) -> ParseResult<CodePattern> {
325        let mut pattern = CodePattern::new(NodeKind::Match);
326        let mut children = HashMap::new();
327
328        // Convert scrutinee
329        if let Some(capture) = self.extract_metavar_from_expr(&match_expr.expr) {
330            children.insert(
331                "scrutinee".to_string(),
332                PatternExpr::Capture(to_metavar_name(&capture)),
333            );
334        } else {
335            let scrutinee_pattern = self.convert_expr(&match_expr.expr)?;
336            children.insert(
337                "scrutinee".to_string(),
338                PatternExpr::Pattern(Box::new(scrutinee_pattern)),
339            );
340        }
341
342        // Add arm count if relevant
343        children.insert(
344            "arms_count".to_string(),
345            PatternExpr::Literal(serde_json::json!(match_expr.arms.len())),
346        );
347
348        pattern.children = children;
349        Ok(pattern)
350    }
351
352    /// Convert block expression
353    fn convert_block(&self, _block: &syn::ExprBlock) -> ParseResult<CodePattern> {
354        let pattern = CodePattern::new(NodeKind::Block);
355        // Block patterns typically use ellipsis for body
356        Ok(pattern)
357    }
358
359    /// Convert binary expression
360    fn convert_binary(&self, bin: &syn::ExprBinary) -> ParseResult<CodePattern> {
361        let mut pattern = CodePattern::new(NodeKind::BinaryOp);
362        let mut children = HashMap::new();
363
364        // Convert left operand
365        if let Some(capture) = self.extract_metavar_from_expr(&bin.left) {
366            children.insert(
367                "left".to_string(),
368                PatternExpr::Capture(to_metavar_name(&capture)),
369            );
370        } else {
371            let left_pattern = self.convert_expr(&bin.left)?;
372            children.insert(
373                "left".to_string(),
374                PatternExpr::Pattern(Box::new(left_pattern)),
375            );
376        }
377
378        // Convert operator
379        let op_str = binop_to_string(&bin.op);
380        children.insert(
381            "op".to_string(),
382            PatternExpr::Literal(serde_json::json!(op_str)),
383        );
384
385        // Convert right operand
386        if let Some(capture) = self.extract_metavar_from_expr(&bin.right) {
387            children.insert(
388                "right".to_string(),
389                PatternExpr::Capture(to_metavar_name(&capture)),
390            );
391        } else {
392            let right_pattern = self.convert_expr(&bin.right)?;
393            children.insert(
394                "right".to_string(),
395                PatternExpr::Pattern(Box::new(right_pattern)),
396            );
397        }
398
399        pattern.children = children;
400        Ok(pattern)
401    }
402
403    /// Convert unary expression
404    fn convert_unary(&self, unary: &syn::ExprUnary) -> ParseResult<CodePattern> {
405        let mut pattern = CodePattern::new(NodeKind::UnaryOp);
406        let mut children = HashMap::new();
407
408        // Convert operand
409        if let Some(capture) = self.extract_metavar_from_expr(&unary.expr) {
410            children.insert(
411                "operand".to_string(),
412                PatternExpr::Capture(to_metavar_name(&capture)),
413            );
414        } else {
415            let operand_pattern = self.convert_expr(&unary.expr)?;
416            children.insert(
417                "operand".to_string(),
418                PatternExpr::Pattern(Box::new(operand_pattern)),
419            );
420        }
421
422        pattern.children = children;
423        Ok(pattern)
424    }
425
426    /// Convert literal expression
427    fn convert_literal(&self, lit: &syn::ExprLit) -> ParseResult<CodePattern> {
428        let mut pattern = CodePattern::new(NodeKind::Literal);
429
430        let value = match &lit.lit {
431            syn::Lit::Str(s) => serde_json::json!(s.value()),
432            syn::Lit::Int(i) => serde_json::json!(i.base10_parse::<i64>().unwrap_or(0)),
433            syn::Lit::Float(f) => serde_json::json!(f.base10_parse::<f64>().unwrap_or(0.0)),
434            syn::Lit::Bool(b) => serde_json::json!(b.value()),
435            syn::Lit::Char(c) => serde_json::json!(c.value().to_string()),
436            _ => serde_json::json!(null),
437        };
438
439        pattern
440            .children
441            .insert("value".to_string(), PatternExpr::Literal(value));
442        Ok(pattern)
443    }
444
445    /// Convert try expression (?)
446    fn convert_try(&self, try_expr: &syn::ExprTry) -> ParseResult<CodePattern> {
447        let mut pattern = CodePattern::new(NodeKind::Try);
448        let mut children = HashMap::new();
449
450        if let Some(capture) = self.extract_metavar_from_expr(&try_expr.expr) {
451            children.insert(
452                "expr".to_string(),
453                PatternExpr::Capture(to_metavar_name(&capture)),
454            );
455        } else {
456            let expr_pattern = self.convert_expr(&try_expr.expr)?;
457            children.insert(
458                "expr".to_string(),
459                PatternExpr::Pattern(Box::new(expr_pattern)),
460            );
461        }
462
463        pattern.children = children;
464        Ok(pattern)
465    }
466
467    /// Convert return expression
468    fn convert_return(&self, ret: &syn::ExprReturn) -> ParseResult<CodePattern> {
469        let mut pattern = CodePattern::new(NodeKind::Return);
470
471        if let Some(expr) = &ret.expr {
472            if let Some(capture) = self.extract_metavar_from_expr(expr) {
473                pattern.children.insert(
474                    "value".to_string(),
475                    PatternExpr::Capture(to_metavar_name(&capture)),
476                );
477            } else {
478                let expr_pattern = self.convert_expr(expr)?;
479                pattern.children.insert(
480                    "value".to_string(),
481                    PatternExpr::Pattern(Box::new(expr_pattern)),
482                );
483            }
484        }
485
486        Ok(pattern)
487    }
488
489    /// Convert await expression
490    fn convert_await(&self, await_expr: &syn::ExprAwait) -> ParseResult<CodePattern> {
491        let mut pattern = CodePattern::new(NodeKind::Await);
492        let mut children = HashMap::new();
493
494        if let Some(capture) = self.extract_metavar_from_expr(&await_expr.base) {
495            children.insert(
496                "base".to_string(),
497                PatternExpr::Capture(to_metavar_name(&capture)),
498            );
499        } else {
500            let base_pattern = self.convert_expr(&await_expr.base)?;
501            children.insert(
502                "base".to_string(),
503                PatternExpr::Pattern(Box::new(base_pattern)),
504            );
505        }
506
507        pattern.children = children;
508        Ok(pattern)
509    }
510
511    /// Convert closure expression
512    fn convert_closure(&self, _closure: &syn::ExprClosure) -> ParseResult<CodePattern> {
513        let pattern = CodePattern::new(NodeKind::Closure);
514        // Closure patterns typically use ellipsis
515        Ok(pattern)
516    }
517
518    /// Convert statement
519    fn convert_stmt(&self, stmt: &syn::Stmt) -> ParseResult<CodePattern> {
520        match stmt {
521            syn::Stmt::Expr(expr, _) => self.convert_expr(expr),
522            syn::Stmt::Local(local) => self.convert_local(local),
523            _ => Err(ParseError::UnsupportedSyntax(
524                "Unsupported statement type".to_string(),
525            )),
526        }
527    }
528
529    /// Convert local (let) statement
530    fn convert_local(&self, local: &syn::Local) -> ParseResult<CodePattern> {
531        let mut pattern = CodePattern::new(NodeKind::LetExpr);
532
533        // Extract pattern
534        if let syn::Pat::Ident(ident) = &local.pat {
535            let name = ident.ident.to_string();
536            if is_metavariable(&name) {
537                pattern.children.insert(
538                    "pattern".to_string(),
539                    PatternExpr::Capture(to_metavar_name(&name)),
540                );
541            }
542        }
543
544        // Extract init expression
545        if let Some(init) = &local.init {
546            if let Some(capture) = self.extract_metavar_from_expr(&init.expr) {
547                pattern.children.insert(
548                    "init".to_string(),
549                    PatternExpr::Capture(to_metavar_name(&capture)),
550                );
551            } else {
552                let init_pattern = self.convert_expr(&init.expr)?;
553                pattern.children.insert(
554                    "init".to_string(),
555                    PatternExpr::Pattern(Box::new(init_pattern)),
556                );
557            }
558        }
559
560        Ok(pattern)
561    }
562
563    /// Convert item (fn, struct, etc.)
564    fn convert_item(&self, item: &syn::Item) -> ParseResult<CodePattern> {
565        match item {
566            syn::Item::Fn(func) => self.convert_fn(func),
567            syn::Item::Struct(s) => self.convert_struct(s),
568            _ => Err(ParseError::UnsupportedSyntax(
569                "Unsupported item type".to_string(),
570            )),
571        }
572    }
573
574    /// Convert function item
575    fn convert_fn(&self, func: &syn::ItemFn) -> ParseResult<CodePattern> {
576        let mut pattern = CodePattern::new(NodeKind::Function);
577        let mut children = HashMap::new();
578
579        let name = func.sig.ident.to_string();
580        if is_metavariable(&name) {
581            children.insert(
582                "name".to_string(),
583                PatternExpr::Capture(to_metavar_name(&name)),
584            );
585        } else {
586            children.insert(
587                "name".to_string(),
588                PatternExpr::Name(NameMatcher::Exact(name)),
589            );
590        }
591
592        pattern.children = children;
593        Ok(pattern)
594    }
595
596    /// Convert struct item
597    fn convert_struct(&self, s: &syn::ItemStruct) -> ParseResult<CodePattern> {
598        let mut pattern = CodePattern::new(NodeKind::Struct);
599        let mut children = HashMap::new();
600
601        let name = s.ident.to_string();
602        if is_metavariable(&name) {
603            children.insert(
604                "name".to_string(),
605                PatternExpr::Capture(to_metavar_name(&name)),
606            );
607        } else {
608            children.insert(
609                "name".to_string(),
610                PatternExpr::Name(NameMatcher::Exact(name)),
611            );
612        }
613
614        pattern.children = children;
615        Ok(pattern)
616    }
617
618    /// Convert arguments list
619    fn convert_args(
620        &self,
621        args: &syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>,
622    ) -> ParseResult<PatternExpr> {
623        // Check for ellipsis pattern in any argument
624        for arg in args {
625            if let Some(ellipsis_var) = self.extract_ellipsis_from_expr(arg) {
626                return Ok(PatternExpr::Capture(to_metavar_name(&ellipsis_var)));
627            }
628        }
629
630        // Otherwise, create a list of patterns
631        let mut arg_patterns = Vec::new();
632        for arg in args {
633            if let Some(capture) = self.extract_metavar_from_expr(arg) {
634                arg_patterns.push(serde_json::json!({ "capture": to_metavar_name(&capture) }));
635            } else {
636                let pattern = self.convert_expr(arg)?;
637                arg_patterns.push(serde_json::to_value(&pattern).unwrap_or_default());
638            }
639        }
640
641        Ok(PatternExpr::Literal(serde_json::json!(arg_patterns)))
642    }
643
644    /// Extract metavariable from expression if it's a simple $VAR
645    fn extract_metavar_from_expr(&self, expr: &syn::Expr) -> Option<String> {
646        if let syn::Expr::Path(path) = expr {
647            let path_str = path_to_string(&path.path);
648            if is_metavariable(&path_str) {
649                return Some(path_str);
650            }
651        }
652        None
653    }
654
655    /// Extract ellipsis metavariable ($...VAR) from expression
656    fn extract_ellipsis_from_expr(&self, expr: &syn::Expr) -> Option<String> {
657        if let syn::Expr::Path(path) = expr {
658            let path_str = path_to_string(&path.path);
659            if is_ellipsis_metavariable(&path_str) {
660                return Some(path_str);
661            }
662        }
663        None
664    }
665}
666
667impl Default for ConcreteParser {
668    fn default() -> Self {
669        Self::new()
670    }
671}
672
673// ========== Helper Functions ==========
674
675/// Check if a string is a metavariable (preprocessed form: __mv_VAR)
676fn is_metavariable(s: &str) -> bool {
677    s.starts_with(METAVAR_PREFIX) || s.starts_with(ELLIPSIS_PREFIX)
678}
679
680/// Check if a string is an ellipsis metavariable (__ell_VAR)
681fn is_ellipsis_metavariable(s: &str) -> bool {
682    s.starts_with(ELLIPSIS_PREFIX)
683}
684
685/// Convert preprocessed metavariable back to $VAR form
686fn to_metavar_name(s: &str) -> String {
687    if let Some(stripped) = s.strip_prefix(ELLIPSIS_PREFIX) {
688        format!("$...{}", stripped)
689    } else if let Some(stripped) = s.strip_prefix(METAVAR_PREFIX) {
690        format!("${}", stripped)
691    } else {
692        s.to_string()
693    }
694}
695
696/// Extract ellipsis variable from token string
697fn extract_ellipsis_var(tokens: &str) -> Option<String> {
698    // Look for __ell_VAR or __mv_VAR pattern in tokens
699    let trimmed = tokens.trim();
700    if trimmed.starts_with(ELLIPSIS_PREFIX) {
701        Some(to_metavar_name(trimmed))
702    } else if trimmed.starts_with(METAVAR_PREFIX) {
703        // Single metavar in args, treat as ellipsis for macro args
704        Some(to_metavar_name(trimmed))
705    } else {
706        None
707    }
708}
709
710/// Convert syn::Path to string
711fn path_to_string(path: &syn::Path) -> String {
712    path.segments
713        .iter()
714        .map(|s| s.ident.to_string())
715        .collect::<Vec<_>>()
716        .join("::")
717}
718
719/// Convert binary operator to string
720fn binop_to_string(op: &syn::BinOp) -> &'static str {
721    match op {
722        syn::BinOp::Add(_) => "+",
723        syn::BinOp::Sub(_) => "-",
724        syn::BinOp::Mul(_) => "*",
725        syn::BinOp::Div(_) => "/",
726        syn::BinOp::Rem(_) => "%",
727        syn::BinOp::And(_) => "&&",
728        syn::BinOp::Or(_) => "||",
729        syn::BinOp::BitXor(_) => "^",
730        syn::BinOp::BitAnd(_) => "&",
731        syn::BinOp::BitOr(_) => "|",
732        syn::BinOp::Shl(_) => "<<",
733        syn::BinOp::Shr(_) => ">>",
734        syn::BinOp::Eq(_) => "==",
735        syn::BinOp::Lt(_) => "<",
736        syn::BinOp::Le(_) => "<=",
737        syn::BinOp::Ne(_) => "!=",
738        syn::BinOp::Ge(_) => ">=",
739        syn::BinOp::Gt(_) => ">",
740        syn::BinOp::AddAssign(_) => "+=",
741        syn::BinOp::SubAssign(_) => "-=",
742        syn::BinOp::MulAssign(_) => "*=",
743        syn::BinOp::DivAssign(_) => "/=",
744        syn::BinOp::RemAssign(_) => "%=",
745        syn::BinOp::BitXorAssign(_) => "^=",
746        syn::BinOp::BitAndAssign(_) => "&=",
747        syn::BinOp::BitOrAssign(_) => "|=",
748        syn::BinOp::ShlAssign(_) => "<<=",
749        syn::BinOp::ShrAssign(_) => ">>=",
750        _ => "?",
751    }
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    #[test]
759    fn test_preprocess() {
760        let parser = ConcreteParser::new();
761
762        // Simple metavariable
763        assert_eq!(parser.preprocess("$var"), "__mv_var");
764        assert_eq!(
765            parser.preprocess("$receiver.unwrap()"),
766            "__mv_receiver.unwrap()"
767        );
768
769        // Ellipsis
770        assert_eq!(parser.preprocess("$...args"), "__ell_args");
771
772        // Multiple
773        assert_eq!(parser.preprocess("$a + $b"), "__mv_a + __mv_b");
774    }
775
776    #[test]
777    fn test_parse_method_call() {
778        let pattern = parse_pattern("$receiver.unwrap()").unwrap();
779        assert_eq!(pattern.node, NodeKind::MethodCall);
780        assert!(pattern.children.contains_key("receiver"));
781        assert!(pattern.children.contains_key("method"));
782
783        // Check that receiver is captured as $receiver
784        if let Some(PatternExpr::Capture(name)) = pattern.children.get("receiver") {
785            assert_eq!(name, "$receiver");
786        } else {
787            panic!("Expected receiver to be a capture");
788        }
789    }
790
791    #[test]
792    fn test_parse_method_call_with_args() {
793        let pattern = parse_pattern("$receiver.expect($msg)").unwrap();
794        assert_eq!(pattern.node, NodeKind::MethodCall);
795        assert!(pattern.children.contains_key("args"));
796    }
797
798    #[test]
799    fn test_parse_macro_call() {
800        let pattern = parse_pattern("panic!($msg)").unwrap();
801        assert_eq!(pattern.node, NodeKind::MacroCall);
802        assert!(pattern.children.contains_key("name"));
803    }
804
805    #[test]
806    fn test_parse_function_call() {
807        let pattern = parse_pattern("foo($a, $b)").unwrap();
808        assert_eq!(pattern.node, NodeKind::FunctionCall);
809    }
810
811    #[test]
812    fn test_parse_binary_op() {
813        let pattern = parse_pattern("$a + $b").unwrap();
814        assert_eq!(pattern.node, NodeKind::BinaryOp);
815        assert!(pattern.children.contains_key("left"));
816        assert!(pattern.children.contains_key("right"));
817
818        // Check captures
819        if let Some(PatternExpr::Capture(name)) = pattern.children.get("left") {
820            assert_eq!(name, "$a");
821        }
822        if let Some(PatternExpr::Capture(name)) = pattern.children.get("right") {
823            assert_eq!(name, "$b");
824        }
825    }
826
827    #[test]
828    fn test_parse_if_expr() {
829        let pattern = parse_pattern("if $cond { }").unwrap();
830        assert_eq!(pattern.node, NodeKind::If);
831        assert!(pattern.children.contains_key("condition"));
832    }
833
834    #[test]
835    fn test_parse_try_expr() {
836        let pattern = parse_pattern("$expr?").unwrap();
837        assert_eq!(pattern.node, NodeKind::Try);
838    }
839
840    #[test]
841    fn test_metavariable_detection() {
842        // After preprocessing, metavars have __mv_ prefix
843        assert!(is_metavariable("__mv_VAR"));
844        assert!(is_metavariable("__mv_receiver"));
845        assert!(is_metavariable("__ell_args"));
846        assert!(!is_metavariable("var"));
847    }
848
849    #[test]
850    fn test_ellipsis_detection() {
851        assert!(is_ellipsis_metavariable("__ell_args"));
852        assert!(!is_ellipsis_metavariable("__mv_args"));
853    }
854
855    #[test]
856    fn test_to_metavar_name() {
857        assert_eq!(to_metavar_name("__mv_receiver"), "$receiver");
858        assert_eq!(to_metavar_name("__ell_args"), "$...args");
859        assert_eq!(to_metavar_name("normal"), "normal");
860    }
861
862    #[test]
863    fn test_parse_qualified_path() {
864        let pattern = parse_pattern("Filter::Recurse").unwrap();
865        assert_eq!(pattern.node, NodeKind::Path);
866        // Should store path as Name(Exact(...)) under "path" key
867        if let Some(PatternExpr::Name(NameMatcher::Exact(path))) = pattern.children.get("path") {
868            assert_eq!(path, "Filter::Recurse");
869        } else {
870            panic!(
871                "Expected Name(Exact(\"Filter::Recurse\")) under \"path\" key, got: {:?}",
872                pattern.children
873            );
874        }
875    }
876
877    #[test]
878    fn test_parse_simple_path() {
879        let pattern = parse_pattern("foo").unwrap();
880        assert_eq!(pattern.node, NodeKind::Path);
881        if let Some(PatternExpr::Name(NameMatcher::Exact(path))) = pattern.children.get("path") {
882            assert_eq!(path, "foo");
883        } else {
884            panic!("Expected Name(Exact(\"foo\")) under \"path\" key");
885        }
886    }
887}