Skip to main content

driven/parser/
aider.rs

1//! Aider rules parser (.aider files, aider.conf.yml)
2
3use super::{RuleParser, UnifiedRule, extract_bullet_points};
4use crate::{DrivenError, Editor, Result};
5use std::path::Path;
6
7/// Parser for Aider .aider files and aider.conf.yml
8#[derive(Debug, Default)]
9pub struct AiderParser;
10
11impl AiderParser {
12    /// Create a new Aider parser
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl RuleParser for AiderParser {
19    fn parse_file(&self, path: &Path) -> Result<Vec<UnifiedRule>> {
20        let content = std::fs::read_to_string(path)
21            .map_err(|e| DrivenError::Parse(format!("Failed to read {}: {}", path.display(), e)))?;
22
23        // Check if it's YAML
24        if path
25            .extension()
26            .is_some_and(|ext| ext == "yml" || ext == "yaml")
27        {
28            return self.parse_yaml(&content);
29        }
30
31        self.parse_content(&content)
32    }
33
34    fn parse_content(&self, content: &str) -> Result<Vec<UnifiedRule>> {
35        let mut rules = Vec::new();
36
37        // Aider uses simple line-based format
38        let points = extract_bullet_points(content);
39        if !points.is_empty() {
40            for (i, point) in points.into_iter().enumerate() {
41                rules.push(UnifiedRule::Standard {
42                    category: crate::format::RuleCategory::Style,
43                    priority: i as u8,
44                    description: point,
45                    pattern: None,
46                });
47            }
48        } else if !content.trim().is_empty() {
49            // Treat as raw content
50            rules.push(UnifiedRule::raw(content.trim()));
51        }
52
53        Ok(rules)
54    }
55
56    fn editor(&self) -> Editor {
57        Editor::Aider
58    }
59}
60
61impl AiderParser {
62    fn parse_yaml(&self, content: &str) -> Result<Vec<UnifiedRule>> {
63        let mut rules = Vec::new();
64
65        // Parse YAML config
66        if let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Value>(content) {
67            if let Some(mapping) = yaml.as_mapping() {
68                // Look for common Aider config keys
69                if let Some(conventions) = mapping.get("conventions") {
70                    if let Some(seq) = conventions.as_sequence() {
71                        for (i, item) in seq.iter().enumerate() {
72                            if let Some(text) = item.as_str() {
73                                rules.push(UnifiedRule::Standard {
74                                    category: crate::format::RuleCategory::Style,
75                                    priority: i as u8,
76                                    description: text.to_string(),
77                                    pattern: None,
78                                });
79                            }
80                        }
81                    }
82                }
83
84                if let Some(read_only) = mapping.get("read_only_files") {
85                    if let Some(seq) = read_only.as_sequence() {
86                        let patterns: Vec<String> = seq
87                            .iter()
88                            .filter_map(|v| v.as_str().map(String::from))
89                            .collect();
90
91                        if !patterns.is_empty() {
92                            rules.push(UnifiedRule::Context {
93                                includes: Vec::new(),
94                                excludes: patterns,
95                                focus: Vec::new(),
96                            });
97                        }
98                    }
99                }
100            }
101        }
102
103        Ok(rules)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_parse_aider_simple() {
113        let content = r#"
114- Use descriptive variable names
115- Write tests for all functions
116- Keep functions small and focused
117"#;
118
119        let parser = AiderParser::new();
120        let rules = parser.parse_content(content).unwrap();
121
122        assert_eq!(rules.len(), 3);
123    }
124
125    #[test]
126    fn test_parse_aider_yaml() {
127        let content = r#"
128conventions:
129  - Use snake_case for functions
130  - Use PascalCase for types
131read_only_files:
132  - "*.lock"
133  - "vendor/**"
134"#;
135
136        let parser = AiderParser::new();
137        let rules = parser.parse_yaml(content).unwrap();
138
139        assert!(!rules.is_empty());
140    }
141}