Skip to main content

tailwind_rs_scanner/
file_scanner.rs

1//! File scanner implementation
2//!
3//! This module provides file discovery and scanning capabilities for
4//! detecting Tailwind classes in source files.
5
6use crate::cache::{CacheEntry, CacheStats, ScanCache};
7use crate::class_extractor::{ClassContext, ClassExtractor, ExtractedClass};
8use crate::content_config::{ContentConfig, FilePattern, ScanConfig};
9use crate::error::{Result, ScannerError};
10use crate::parallel_processor::{ParallelProcessor, ProcessingStats};
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17/// Main content scanner
18#[derive(Debug)]
19pub struct ContentScanner {
20    config: ScanConfig,
21    file_scanner: FileScanner,
22    class_extractor: ClassExtractor,
23    parallel_processor: ParallelProcessor,
24    cache: Arc<RwLock<ScanCache>>,
25}
26
27/// File scanner for discovering and reading files
28#[derive(Debug)]
29pub struct FileScanner {
30    config: ContentConfig,
31    file_types: HashMap<String, FileType>,
32}
33
34/// File information
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct FileInfo {
37    pub path: PathBuf,
38    pub file_type: FileType,
39    pub size: u64,
40    pub modified: std::time::SystemTime,
41    pub content: Option<String>,
42}
43
44/// Supported file types
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46pub enum FileType {
47    Rust,
48    JavaScript,
49    TypeScript,
50    Html,
51    Vue,
52    Svelte,
53    Css,
54    Scss,
55    Less,
56    Json,
57    Yaml,
58    Toml,
59    Markdown,
60    Other(String),
61}
62
63/// Set of extracted classes
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct ClassSet {
66    pub classes: HashSet<String>,
67    pub file_classes: HashMap<PathBuf, Vec<ExtractedClass>>,
68    pub total_files: usize,
69    pub total_classes: usize,
70    pub unique_classes: usize,
71    pub processing_stats: ProcessingStats,
72}
73
74impl ClassSet {
75    /// Create a new empty class set
76    pub fn new() -> Self {
77        Self {
78            classes: HashSet::new(),
79            file_classes: HashMap::new(),
80            total_files: 0,
81            total_classes: 0,
82            unique_classes: 0,
83            processing_stats: ProcessingStats::default(),
84        }
85    }
86
87    /// Add classes from a file
88    pub fn add_file_classes(&mut self, file_path: PathBuf, classes: Vec<ExtractedClass>) {
89        let unique_classes: HashSet<String> =
90            classes.iter().map(|c| c.class_name.clone()).collect();
91
92        self.classes.extend(unique_classes);
93        self.file_classes.insert(file_path, classes);
94        self.total_files += 1;
95        self.unique_classes = self.classes.len();
96    }
97
98    /// Get total number of classes
99    pub fn total_classes(&self) -> usize {
100        self.total_classes
101    }
102
103    /// Get unique number of classes
104    pub fn unique_classes(&self) -> usize {
105        self.unique_classes
106    }
107
108    /// Get classes for a specific file
109    pub fn get_file_classes(&self, file_path: &Path) -> Option<&Vec<ExtractedClass>> {
110        self.file_classes.get(file_path)
111    }
112
113    /// Check if a class exists
114    pub fn has_class(&self, class: &str) -> bool {
115        self.classes.contains(class)
116    }
117
118    /// Get all classes as a sorted vector
119    pub fn get_all_classes(&self) -> Vec<String> {
120        let mut classes: Vec<String> = self.classes.iter().cloned().collect();
121        classes.sort();
122        classes
123    }
124}
125
126impl Default for ClassSet {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl ContentScanner {
133    /// Create a new content scanner
134    pub fn new(config: ScanConfig) -> Result<Self> {
135        let file_scanner = FileScanner::new(config.content_config.clone())?;
136        let class_extractor = ClassExtractor::new();
137        let parallel_processor = ParallelProcessor::new(config.parallel_processing);
138        let cache = Arc::new(RwLock::new(ScanCache::new()));
139
140        Ok(Self {
141            config,
142            file_scanner,
143            class_extractor,
144            parallel_processor,
145            cache,
146        })
147    }
148
149    /// Scan for classes in specified paths
150    pub async fn scan_paths(&self, paths: &[String]) -> Result<ClassSet> {
151        let start_time = std::time::Instant::now();
152
153        // Discover files
154        let files = self.file_scanner.discover_files(paths).await?;
155
156        // Process files in parallel
157        let results = self
158            .parallel_processor
159            .process_files(&files, &self.class_extractor, &self.config)
160            .await?;
161
162        // Build class set
163        let mut class_set = ClassSet::new();
164        for (file_path, classes) in results {
165            class_set.add_file_classes(file_path, classes);
166        }
167
168        class_set.processing_stats.total_time = start_time.elapsed();
169        class_set.total_classes = class_set.classes.len();
170
171        // Update cache
172        {
173            let mut cache = self.cache.write().await;
174            for (file_path, classes) in &class_set.file_classes {
175                cache.update_file(file_path.clone(), classes.clone());
176            }
177        }
178
179        Ok(class_set)
180    }
181
182    /// Scan for classes in a single file
183    pub async fn scan_file(&self, file_path: &Path) -> Result<Vec<ExtractedClass>> {
184        let file_info = self.file_scanner.read_file(file_path).await?;
185        self.class_extractor.extract_classes(&file_info).await
186    }
187
188    /// Get cache statistics
189    pub async fn get_cache_stats(&self) -> CacheStats {
190        let cache = self.cache.read().await;
191        cache.get_stats()
192    }
193
194    /// Clear cache
195    pub async fn clear_cache(&self) {
196        let mut cache = self.cache.write().await;
197        cache.clear();
198    }
199
200    /// Get scanner configuration
201    pub fn get_config(&self) -> &ScanConfig {
202        &self.config
203    }
204
205    /// Update scanner configuration
206    pub fn update_config(&mut self, config: ScanConfig) -> Result<()> {
207        self.config = config;
208        self.file_scanner = FileScanner::new(self.config.content_config.clone())?;
209        self.parallel_processor = ParallelProcessor::new(self.config.parallel_processing);
210        Ok(())
211    }
212}
213
214impl FileScanner {
215    /// Create a new file scanner
216    pub fn new(config: ContentConfig) -> Result<Self> {
217        let mut file_types = HashMap::new();
218
219        // Initialize file type mappings
220        file_types.insert("rs".to_string(), FileType::Rust);
221        file_types.insert("js".to_string(), FileType::JavaScript);
222        file_types.insert("jsx".to_string(), FileType::JavaScript);
223        file_types.insert("ts".to_string(), FileType::TypeScript);
224        file_types.insert("tsx".to_string(), FileType::TypeScript);
225        file_types.insert("html".to_string(), FileType::Html);
226        file_types.insert("htm".to_string(), FileType::Html);
227        file_types.insert("vue".to_string(), FileType::Vue);
228        file_types.insert("svelte".to_string(), FileType::Svelte);
229        file_types.insert("css".to_string(), FileType::Css);
230        file_types.insert("scss".to_string(), FileType::Scss);
231        file_types.insert("sass".to_string(), FileType::Scss);
232        file_types.insert("less".to_string(), FileType::Less);
233        file_types.insert("json".to_string(), FileType::Json);
234        file_types.insert("yaml".to_string(), FileType::Yaml);
235        file_types.insert("yml".to_string(), FileType::Yaml);
236        file_types.insert("toml".to_string(), FileType::Toml);
237        file_types.insert("md".to_string(), FileType::Markdown);
238        file_types.insert("markdown".to_string(), FileType::Markdown);
239
240        Ok(Self { config, file_types })
241    }
242
243    /// Discover files matching the configuration patterns
244    pub async fn discover_files(&self, paths: &[String]) -> Result<Vec<FileInfo>> {
245        let mut files = Vec::new();
246
247        for path in paths {
248            let path = Path::new(path);
249            if path.is_file() {
250                // Single file
251                if let Ok(file_info) = self.read_file(path).await {
252                    files.push(file_info);
253                }
254            } else if path.is_dir() {
255                // Directory - scan recursively
256                let dir_files = self.scan_directory(path).await?;
257                files.extend(dir_files);
258            }
259        }
260
261        Ok(files)
262    }
263
264    /// Scan a directory for files
265    async fn scan_directory(&self, dir_path: &Path) -> Result<Vec<FileInfo>> {
266        let mut files = Vec::new();
267
268        for entry in walkdir::WalkDir::new(dir_path)
269            .follow_links(false)
270            .into_iter()
271            .filter_map(|e| e.ok())
272        {
273            if entry.file_type().is_file() {
274                if let Ok(file_info) = self.read_file(entry.path()).await {
275                    // Check if file matches any of the patterns
276                    if self.matches_patterns(&file_info) {
277                        files.push(file_info);
278                    }
279                }
280            }
281        }
282
283        Ok(files)
284    }
285
286    /// Read a file and return file information
287    pub async fn read_file(&self, file_path: &Path) -> Result<FileInfo> {
288        let metadata = std::fs::metadata(file_path)?;
289        let file_type = self.detect_file_type(file_path);
290        let content = if self.should_read_content(&file_type) {
291            Some(std::fs::read_to_string(file_path)?)
292        } else {
293            None
294        };
295
296        Ok(FileInfo {
297            path: file_path.to_path_buf(),
298            file_type,
299            size: metadata.len(),
300            modified: metadata.modified()?,
301            content,
302        })
303    }
304
305    /// Detect file type from extension
306    fn detect_file_type(&self, file_path: &Path) -> FileType {
307        if let Some(extension) = file_path.extension() {
308            if let Some(extension_str) = extension.to_str() {
309                if let Some(file_type) = self.file_types.get(extension_str) {
310                    return file_type.clone();
311                }
312            }
313        }
314        FileType::Other("unknown".to_string())
315    }
316
317    /// Check if file matches any of the configured patterns
318    fn matches_patterns(&self, file_info: &FileInfo) -> bool {
319        if self.config.patterns.is_empty() {
320            return true; // No patterns means match all
321        }
322
323        for pattern in &self.config.patterns {
324            if pattern.matches_file(&file_info.path) {
325                return true;
326            }
327        }
328
329        false
330    }
331
332    /// Check if we should read file content
333    fn should_read_content(&self, file_type: &FileType) -> bool {
334        match file_type {
335            FileType::Rust
336            | FileType::JavaScript
337            | FileType::TypeScript
338            | FileType::Html
339            | FileType::Vue
340            | FileType::Svelte
341            | FileType::Css
342            | FileType::Scss
343            | FileType::Less
344            | FileType::Markdown => true,
345            _ => false,
346        }
347    }
348}
349
350impl Default for ProcessingStats {
351    fn default() -> Self {
352        Self {
353            total_files: 0,
354            processed_files: 0,
355            total_classes: 0,
356            unique_classes: 0,
357            total_time: std::time::Duration::from_millis(0),
358            average_file_time: std::time::Duration::from_millis(0),
359            memory_usage: 0,
360        }
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use std::path::PathBuf;
368
369    #[test]
370    fn test_class_set_creation() {
371        let class_set = ClassSet::new();
372        assert_eq!(class_set.total_classes(), 0);
373        assert_eq!(class_set.unique_classes(), 0);
374    }
375
376    #[test]
377    fn test_class_set_operations() {
378        let mut class_set = ClassSet::new();
379
380        let classes = vec![
381            ExtractedClass {
382                class_name: "p-4".to_string(),
383                context: ClassContext::new(),
384                line: 1,
385                column: 1,
386            },
387            ExtractedClass {
388                class_name: "bg-blue-500".to_string(),
389                context: ClassContext::new(),
390                line: 2,
391                column: 1,
392            },
393        ];
394
395        class_set.add_file_classes(PathBuf::from("test.rs"), classes);
396
397        assert_eq!(class_set.total_files, 1);
398        assert_eq!(class_set.unique_classes(), 2);
399        assert!(class_set.has_class("p-4"));
400        assert!(class_set.has_class("bg-blue-500"));
401        assert!(!class_set.has_class("text-red-500"));
402    }
403
404    #[test]
405    fn test_file_type_detection() {
406        let scanner = FileScanner::new(ContentConfig::default()).unwrap();
407
408        let rust_file = Path::new("test.rs");
409        let js_file = Path::new("test.js");
410        let html_file = Path::new("test.html");
411
412        assert_eq!(scanner.detect_file_type(rust_file), FileType::Rust);
413        assert_eq!(scanner.detect_file_type(js_file), FileType::JavaScript);
414        assert_eq!(scanner.detect_file_type(html_file), FileType::Html);
415    }
416
417    #[tokio::test]
418    async fn test_content_scanner_creation() {
419        let config = ScanConfig::default();
420        let scanner = ContentScanner::new(config);
421        assert!(scanner.is_ok());
422    }
423}