chore_cli/processor/
mod.rs1mod 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 pub fn process_file(
28 &self,
29 file_path: &Path,
30 format_template: &str,
31 check_only: bool,
32 ) -> ProcessResult {
33 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 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 let expected_comment = format_template.replace("$path$file", &rel_path_str);
49
50 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 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 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 let has_shebang = lines[0].starts_with("#!");
84
85 let comment_line_idx = if has_shebang { 1 } else { 0 };
87
88 if lines.len() <= comment_line_idx {
90 return true;
91 }
92
93 let actual_line = lines[comment_line_idx].trim();
95 let expected_line = expected_comment.trim();
96
97 actual_line != expected_line
98 }
99
100 fn add_path_comment(&self, content: &str, expected_comment: &str) -> Result<String, String> {
102 let lines: Vec<&str> = content.lines().collect();
103
104 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 new_lines.push(lines[0].to_string());
116
117 new_lines.push(expected_comment.to_string());
119
120 new_lines.push(String::new());
122
123 let start_idx = find_content_start_after_shebang(&lines);
125
126 for line in lines.iter().skip(start_idx) {
128 new_lines.push(line.to_string());
129 }
130 } else {
131 new_lines.push(expected_comment.to_string());
133
134 new_lines.push(String::new());
136
137 let start_idx = find_content_start(&lines);
139
140 for line in lines.iter().skip(start_idx) {
142 new_lines.push(line.to_string());
143 }
144 }
145
146 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}