Skip to main content

driven/parser/
claude.rs

1//! Claude Code rules parser (.claude/ folder, CLAUDE.md)
2
3use super::{RuleParser, UnifiedRule, extract_bullet_points, parse_markdown_sections};
4use crate::{DrivenError, Editor, Result};
5use std::path::Path;
6
7/// Parser for Claude Code .claude/ folder and CLAUDE.md files
8#[derive(Debug, Default)]
9pub struct ClaudeParser;
10
11impl ClaudeParser {
12    /// Create a new Claude parser
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl RuleParser for ClaudeParser {
19    fn parse_file(&self, path: &Path) -> Result<Vec<UnifiedRule>> {
20        // Check if it's a directory
21        if path.is_dir() {
22            return self.parse_directory(path);
23        }
24
25        let content = std::fs::read_to_string(path)
26            .map_err(|e| DrivenError::Parse(format!("Failed to read {}: {}", path.display(), e)))?;
27        self.parse_content(&content)
28    }
29
30    fn parse_content(&self, content: &str) -> Result<Vec<UnifiedRule>> {
31        let mut rules = Vec::new();
32        let sections = parse_markdown_sections(content);
33
34        if sections.is_empty() {
35            if !content.trim().is_empty() {
36                rules.push(UnifiedRule::raw(content.trim()));
37            }
38            return Ok(rules);
39        }
40
41        for (heading, body) in sections {
42            let lower = heading.to_lowercase();
43
44            if lower.contains("instructions") || lower.contains("guidelines") {
45                let points = extract_bullet_points(&body);
46                for (i, point) in points.into_iter().enumerate() {
47                    rules.push(UnifiedRule::Standard {
48                        category: crate::format::RuleCategory::Style,
49                        priority: i as u8,
50                        description: point,
51                        pattern: None,
52                    });
53                }
54            } else if lower.contains("context") || lower.contains("about") {
55                let points = extract_bullet_points(&body);
56                rules.push(UnifiedRule::Context {
57                    includes: Vec::new(),
58                    excludes: Vec::new(),
59                    focus: points,
60                });
61            } else if lower.contains("memory") || lower.contains("remember") {
62                // Claude's memory/persistence section
63                let points = extract_bullet_points(&body);
64                for (i, point) in points.into_iter().enumerate() {
65                    rules.push(UnifiedRule::Standard {
66                        category: crate::format::RuleCategory::Other,
67                        priority: (100 + i) as u8,
68                        description: point,
69                        pattern: None,
70                    });
71                }
72            } else if !body.trim().is_empty() {
73                rules.push(UnifiedRule::raw(format!("# {}\n{}", heading, body)));
74            }
75        }
76
77        Ok(rules)
78    }
79
80    fn editor(&self) -> Editor {
81        Editor::Claude
82    }
83}
84
85impl ClaudeParser {
86    fn parse_directory(&self, path: &Path) -> Result<Vec<UnifiedRule>> {
87        let mut rules = Vec::new();
88
89        // Look for common files
90        let files_to_check = ["settings.json", "CLAUDE.md", "instructions.md"];
91
92        for file_name in files_to_check {
93            let file_path = path.join(file_name);
94            if file_path.exists() {
95                if file_name.ends_with(".json") {
96                    // Parse JSON settings
97                    let content = std::fs::read_to_string(&file_path)?;
98                    if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
99                        rules.extend(self.parse_json_settings(&json)?);
100                    }
101                } else {
102                    // Parse markdown
103                    let content = std::fs::read_to_string(&file_path)?;
104                    rules.extend(self.parse_content(&content)?);
105                }
106            }
107        }
108
109        Ok(rules)
110    }
111
112    fn parse_json_settings(&self, json: &serde_json::Value) -> Result<Vec<UnifiedRule>> {
113        let mut rules = Vec::new();
114
115        if let Some(obj) = json.as_object() {
116            if let Some(instructions) = obj.get("instructions") {
117                if let Some(text) = instructions.as_str() {
118                    rules.push(UnifiedRule::raw(text));
119                } else if let Some(arr) = instructions.as_array() {
120                    for (i, item) in arr.iter().enumerate() {
121                        if let Some(text) = item.as_str() {
122                            rules.push(UnifiedRule::Standard {
123                                category: crate::format::RuleCategory::Style,
124                                priority: i as u8,
125                                description: text.to_string(),
126                                pattern: None,
127                            });
128                        }
129                    }
130                }
131            }
132        }
133
134        Ok(rules)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_parse_claude_md() {
144        let content = r#"# CLAUDE.md
145
146## Instructions
147
148- Always explain your reasoning
149- Use clear, concise language
150- Follow the coding standards
151
152## Project Context
153
154- This is a Rust project
155- Focus on performance
156"#;
157
158        let parser = ClaudeParser::new();
159        let rules = parser.parse_content(content).unwrap();
160
161        assert!(!rules.is_empty());
162    }
163}