Skip to main content

chore_cli/processor/
mod.rs

1/* src/processor/mod.rs */
2
3mod comment_detector;
4
5use comment_detector::{find_content_start, find_content_start_after_shebang};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum ProcessResult {
11    Modified,
12    AlreadyCorrect,
13    Skipped(String),
14    Error(String),
15}
16
17pub struct Processor {
18    project_root: PathBuf,
19}
20
21impl Processor {
22    pub fn new(project_root: PathBuf) -> Self {
23        Processor { project_root }
24    }
25
26    /// Process a single file: add or update path comment
27    pub fn process_file(
28        &self,
29        file_path: &Path,
30        format_template: &str,
31        check_only: bool,
32    ) -> ProcessResult {
33        // Read file content
34        let content = match fs::read_to_string(file_path) {
35            Ok(content) => content,
36            Err(e) => return ProcessResult::Error(format!("Failed to read file: {}", e)),
37        };
38
39        // Get relative path from project root
40        let rel_path = match file_path.strip_prefix(&self.project_root) {
41            Ok(rel) => rel,
42            Err(_) => file_path,
43        };
44
45        let rel_path_str = rel_path.to_string_lossy().replace('\\', "/");
46
47        // Generate expected path comment
48        let expected_comment = format_template.replace("$path$file", &rel_path_str);
49
50        // Check if file needs processing
51        let needs_update = self.check_needs_update(&content, &expected_comment);
52
53        if !needs_update {
54            return ProcessResult::AlreadyCorrect;
55        }
56
57        if check_only {
58            return ProcessResult::Modified;
59        }
60
61        // Process the file
62        match self.add_path_comment(&content, &expected_comment) {
63            Ok(new_content) => {
64                if let Err(e) = fs::write(file_path, new_content) {
65                    ProcessResult::Error(format!("Failed to write file: {}", e))
66                } else {
67                    ProcessResult::Modified
68                }
69            }
70            Err(e) => ProcessResult::Error(e),
71        }
72    }
73
74    /// Check if file needs updating
75    fn check_needs_update(&self, content: &str, expected_comment: &str) -> bool {
76        let lines: Vec<&str> = content.lines().collect();
77
78        if lines.is_empty() {
79            return true;
80        }
81
82        // Check for shebang
83        let has_shebang = lines[0].starts_with("#!");
84
85        // Determine which line should contain the path comment
86        let comment_line_idx = if has_shebang { 1 } else { 0 };
87
88        // Check if file has enough lines
89        if lines.len() <= comment_line_idx {
90            return true;
91        }
92
93        // Check if the comment line matches expected
94        let actual_line = lines[comment_line_idx].trim();
95        let expected_line = expected_comment.trim();
96
97        actual_line != expected_line
98    }
99
100    /// Add path comment to file content
101    fn add_path_comment(&self, content: &str, expected_comment: &str) -> Result<String, String> {
102        let lines: Vec<&str> = content.lines().collect();
103
104        // Check for shebang
105        let has_shebang = if !lines.is_empty() && lines[0].starts_with("#!") {
106            true
107        } else {
108            false
109        };
110
111        let mut new_lines = Vec::new();
112
113        if has_shebang {
114            // Keep shebang as first line
115            new_lines.push(lines[0].to_string());
116
117            // Add path comment as second line
118            new_lines.push(expected_comment.to_string());
119
120            // Add blank line
121            new_lines.push(String::new());
122
123            // Determine where to start copying original content
124            let start_idx = find_content_start_after_shebang(&lines);
125
126            // Add remaining content
127            for line in lines.iter().skip(start_idx) {
128                new_lines.push(line.to_string());
129            }
130        } else {
131            // Add path comment as first line
132            new_lines.push(expected_comment.to_string());
133
134            // Add blank line
135            new_lines.push(String::new());
136
137            // Determine where to start copying original content
138            let start_idx = find_content_start(&lines);
139
140            // Add remaining content
141            for line in lines.iter().skip(start_idx) {
142                new_lines.push(line.to_string());
143            }
144        }
145
146        // Join with LF line endings
147        Ok(new_lines.join("\n"))
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::path::PathBuf;
155
156    #[test]
157    fn test_add_path_comment_simple() {
158        let processor = Processor::new(PathBuf::from("/project"));
159
160        let content = "fn main() {\n    println!(\"Hello\");\n}";
161        let result = processor.add_path_comment(content, "/* src/main.rs */");
162
163        assert!(result.is_ok());
164        let new_content = result.unwrap();
165
166        assert!(new_content.starts_with("/* src/main.rs */\n\nfn main()"));
167    }
168
169    #[test]
170    fn test_add_path_comment_with_shebang() {
171        let processor = Processor::new(PathBuf::from("/project"));
172
173        let content = "#!/usr/bin/env python3\nimport sys";
174        let result = processor.add_path_comment(content, "# scripts/deploy.py");
175
176        assert!(result.is_ok());
177        let new_content = result.unwrap();
178
179        assert!(
180            new_content.starts_with("#!/usr/bin/env python3\n# scripts/deploy.py\n\nimport sys")
181        );
182    }
183}