Skip to main content

driven/parser/
copilot.rs

1//! GitHub Copilot instructions parser (copilot-instructions.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 GitHub Copilot copilot-instructions.md files
8#[derive(Debug, Default)]
9pub struct CopilotParser;
10
11impl CopilotParser {
12    /// Create a new Copilot parser
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl RuleParser for CopilotParser {
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        self.parse_content(&content)
23    }
24
25    fn parse_content(&self, content: &str) -> Result<Vec<UnifiedRule>> {
26        let mut rules = Vec::new();
27        let sections = parse_markdown_sections(content);
28
29        if sections.is_empty() {
30            // No markdown structure, treat as raw content
31            if !content.trim().is_empty() {
32                rules.push(UnifiedRule::raw(content.trim()));
33            }
34            return Ok(rules);
35        }
36
37        for (heading, body) in sections {
38            let lower_heading = heading.to_lowercase();
39
40            if lower_heading.contains("project")
41                || lower_heading.contains("overview")
42                || lower_heading.contains("context")
43            {
44                // Project context section
45                let points = extract_bullet_points(&body);
46                if !points.is_empty() {
47                    rules.push(UnifiedRule::Context {
48                        includes: Vec::new(),
49                        excludes: Vec::new(),
50                        focus: points,
51                    });
52                }
53            } else if lower_heading.contains("code")
54                || lower_heading.contains("style")
55                || lower_heading.contains("convention")
56                || lower_heading.contains("quality")
57            {
58                // Coding standards section
59                let points = extract_bullet_points(&body);
60                for (i, point) in points.into_iter().enumerate() {
61                    rules.push(UnifiedRule::Standard {
62                        category: crate::format::RuleCategory::Style,
63                        priority: i as u8,
64                        description: point,
65                        pattern: None,
66                    });
67                }
68            } else if lower_heading.contains("structure") || lower_heading.contains("architecture")
69            {
70                // Architecture standards
71                let points = extract_bullet_points(&body);
72                for (i, point) in points.into_iter().enumerate() {
73                    rules.push(UnifiedRule::Standard {
74                        category: crate::format::RuleCategory::Architecture,
75                        priority: i as u8,
76                        description: point,
77                        pattern: None,
78                    });
79                }
80            } else if lower_heading.contains("test") {
81                // Testing standards
82                let points = extract_bullet_points(&body);
83                for (i, point) in points.into_iter().enumerate() {
84                    rules.push(UnifiedRule::Standard {
85                        category: crate::format::RuleCategory::Testing,
86                        priority: i as u8,
87                        description: point,
88                        pattern: None,
89                    });
90                }
91            } else if lower_heading.contains("doc") {
92                // Documentation standards
93                let points = extract_bullet_points(&body);
94                for (i, point) in points.into_iter().enumerate() {
95                    rules.push(UnifiedRule::Standard {
96                        category: crate::format::RuleCategory::Documentation,
97                        priority: i as u8,
98                        description: point,
99                        pattern: None,
100                    });
101                }
102            } else {
103                // Other sections as raw content
104                if !body.trim().is_empty() {
105                    rules.push(UnifiedRule::raw(format!("# {}\n{}", heading, body)));
106                }
107            }
108        }
109
110        Ok(rules)
111    }
112
113    fn editor(&self) -> Editor {
114        Editor::Copilot
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_parse_copilot_instructions() {
124        let content = r#"# Project Overview
125
126This is a Rust project for binary-first web development.
127
128- Focus on performance
129- Zero-copy operations
130
131# Code Quality
132
133- Use `cargo fmt` before commits
134- Run `cargo clippy` and fix warnings
135- Write unit tests for all public functions
136
137# Testing Standards
138
139- Use property-based testing where applicable
140- Aim for 80% code coverage
141"#;
142
143        let parser = CopilotParser::new();
144        let rules = parser.parse_content(content).unwrap();
145
146        assert!(!rules.is_empty());
147
148        // Should have context, style standards, and testing standards
149        let has_context = rules
150            .iter()
151            .any(|r| matches!(r, UnifiedRule::Context { .. }));
152        let has_testing = rules.iter().any(|r| {
153            matches!(
154                r,
155                UnifiedRule::Standard {
156                    category: crate::format::RuleCategory::Testing,
157                    ..
158                }
159            )
160        });
161
162        assert!(has_context);
163        assert!(has_testing);
164    }
165
166    #[test]
167    fn test_parse_empty() {
168        let parser = CopilotParser::new();
169        let rules = parser.parse_content("").unwrap();
170        assert!(rules.is_empty());
171    }
172}