Skip to main content

chore_cli/
scanner.rs

1/* src/scanner.rs */
2
3use crate::config::Config;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7pub struct Scanner {
8    config: Config,
9    project_root: PathBuf,
10}
11
12#[derive(Debug)]
13pub enum SkipReason {
14    ExcludedByDir(String),
15    ExcludedByPattern(String),
16    NoMatchingFormat,
17    NotUtf8,
18    SymbolicLink,
19}
20
21impl Scanner {
22    pub fn new(config: Config, project_root: PathBuf) -> Self {
23        Scanner {
24            config,
25            project_root,
26        }
27    }
28
29    /// Collect all files that should be processed
30    pub fn collect_files(&self, target_path: &Path) -> Vec<PathBuf> {
31        let mut files = Vec::new();
32
33        if target_path.is_file() {
34            // Single file mode
35            files.push(target_path.to_path_buf());
36        } else if target_path.is_dir() {
37            // Directory mode: walk recursively
38            for entry in WalkDir::new(target_path).follow_links(false) {
39                if let Ok(entry) = entry {
40                    if entry.file_type().is_file() {
41                        files.push(entry.path().to_path_buf());
42                    }
43                }
44            }
45        }
46
47        files
48    }
49
50    /// Check if a file should be processed or skipped
51    pub fn should_process(&self, file_path: &Path) -> Result<(), SkipReason> {
52        // Check if it's a symbolic link
53        if file_path.is_symlink() {
54            return Err(SkipReason::SymbolicLink);
55        }
56
57        // First check if file extension has a matching format
58        // Only files we intend to process should be checked for exclusion
59        if !self.has_matching_format(file_path) {
60            return Err(SkipReason::NoMatchingFormat);
61        }
62
63        // Then check exclude rules
64        // Now we only check exclusion for files that would otherwise be processed
65        if let Some(reason) = self.check_exclude_rules(file_path) {
66            return Err(reason);
67        }
68
69        // Check if file is valid UTF-8
70        if let Ok(content) = std::fs::read(file_path) {
71            if std::str::from_utf8(&content).is_err() {
72                return Err(SkipReason::NotUtf8);
73            }
74        }
75
76        Ok(())
77    }
78
79    /// Check if file matches any exclude rules
80    fn check_exclude_rules(&self, file_path: &Path) -> Option<SkipReason> {
81        let exclude = &self.config.path_comment.exclude;
82
83        // Get relative path from project root
84        let rel_path = if let Ok(rel) = file_path.strip_prefix(&self.project_root) {
85            rel
86        } else {
87            file_path
88        };
89
90        // Check excluded directories
91        for excluded_dir in &exclude.dirs {
92            if self.path_contains_dir(rel_path, excluded_dir) {
93                return Some(SkipReason::ExcludedByDir(excluded_dir.clone()));
94            }
95        }
96
97        // Check excluded patterns
98        for pattern in &exclude.patterns {
99            if self.matches_pattern(file_path, pattern) {
100                return Some(SkipReason::ExcludedByPattern(pattern.clone()));
101            }
102        }
103
104        None
105    }
106
107    /// Check if path contains a specific directory component
108    fn path_contains_dir(&self, path: &Path, dir_name: &str) -> bool {
109        for component in path.components() {
110            if let Some(name) = component.as_os_str().to_str() {
111                if name == dir_name {
112                    return true;
113                }
114            }
115        }
116        false
117    }
118
119    /// Check if file matches a glob pattern
120    fn matches_pattern(&self, file_path: &Path, pattern: &str) -> bool {
121        if let Some(file_name) = file_path.file_name() {
122            if let Some(file_name_str) = file_name.to_str() {
123                if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
124                    return glob_pattern.matches(file_name_str);
125                }
126            }
127        }
128        false
129    }
130
131    /// Check if file extension has a matching format in config
132    fn has_matching_format(&self, file_path: &Path) -> bool {
133        if let Some(ext) = file_path.extension() {
134            let ext_with_dot = format!(".{}", ext.to_string_lossy());
135            self.config.path_comment.formats.contains_key(&ext_with_dot)
136        } else {
137            false
138        }
139    }
140
141    /// Get the comment format for a file
142    pub fn get_format(&self, file_path: &Path) -> Option<&str> {
143        if let Some(ext) = file_path.extension() {
144            let ext_with_dot = format!(".{}", ext.to_string_lossy());
145            self.config
146                .path_comment
147                .formats
148                .get(&ext_with_dot)
149                .map(|s| s.as_str())
150        } else {
151            None
152        }
153    }
154}