Skip to main content

dry4rust/
scanner.rs

1// Copyright (c) 2026 Matjaz Domen Pecan
2// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
3// Licensed under the MIT License
4// SPDX-License-Identifier: MIT
5
6use std::path::{Path, PathBuf};
7use walkdir::WalkDir;
8
9/// Configuration for scanning the filesystem for source files.
10pub struct ScanConfig {
11    /// Root directory to scan.
12    pub root: PathBuf,
13    /// Glob patterns to exclude (simple substring matching for now).
14    pub exclude_patterns: Vec<String>,
15    /// File extensions to include (without the leading dot). Defaults to `["rs"]`.
16    pub extensions: Vec<String>,
17}
18
19impl ScanConfig {
20    #[must_use]
21    pub fn new(root: PathBuf) -> Self {
22        Self {
23            root,
24            exclude_patterns: Vec::new(),
25            extensions: vec!["rs".to_string()],
26        }
27    }
28
29    #[must_use]
30    pub fn with_excludes(mut self, patterns: Vec<String>) -> Self {
31        self.exclude_patterns = patterns;
32        self
33    }
34
35    #[must_use]
36    pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
37        self.extensions = extensions;
38        self
39    }
40}
41
42/// Scan for source files under the given config.
43/// Always skips `target/` directories.
44#[must_use]
45pub fn scan_files(config: &ScanConfig) -> Vec<PathBuf> {
46    let mut files = Vec::new();
47
48    for entry in WalkDir::new(&config.root)
49        .into_iter()
50        .filter_entry(|e| {
51            let path = e.path();
52            // Only filter directories (not the root itself for hidden check)
53            if path.is_dir()
54                && let Some(name) = path.file_name().and_then(|n| n.to_str())
55            {
56                if name == "target" {
57                    return false;
58                }
59                // Skip hidden directories, but not the root
60                if name.starts_with('.') && path != config.root.as_path() {
61                    return false;
62                }
63            }
64            true
65        })
66        .flatten()
67    {
68        let path = entry.path();
69        if path.is_file()
70            && path
71                .extension()
72                .and_then(|ext| ext.to_str())
73                .is_some_and(|ext| {
74                    config
75                        .extensions
76                        .iter()
77                        .any(|e| e.eq_ignore_ascii_case(ext))
78                })
79            && !is_excluded(path, &config.exclude_patterns)
80        {
81            files.push(path.to_path_buf());
82        }
83    }
84
85    files
86}
87
88/// Check if a path should be excluded based on exclusion patterns.
89#[must_use]
90pub fn is_excluded(path: &Path, patterns: &[String]) -> bool {
91    let path_str = path.to_string_lossy();
92    patterns
93        .iter()
94        .any(|pattern| path_str.contains(pattern.as_str()))
95}