1use std::path::{Path, PathBuf};
11
12use glob::glob;
13
14use crate::config::Config;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub enum RejectedBy {
19 FileTypes,
21 Include,
23 Exclude,
25}
26
27impl RejectedBy {
28 #[must_use]
30 pub const fn key(self) -> &'static str {
31 match self {
32 Self::FileTypes => "file_types",
33 Self::Include => "include",
34 Self::Exclude => "exclude",
35 }
36 }
37}
38
39#[derive(Debug, Default)]
41pub struct Selection {
42 pub selected: Vec<PathBuf>,
43 pub rejected: Vec<(PathBuf, RejectedBy)>,
46}
47
48#[must_use]
54pub fn select_files(
55 config: &Config,
56 root: &Path,
57 search_from: &Path,
58 with_rejected: bool,
59) -> Selection {
60 let mut patterns = crate::languages::all_file_patterns(config);
61 patterns.sort();
62 patterns.dedup();
63
64 let mut selection = Selection::default();
65 for (suffix, _lang) in &patterns {
66 let Ok(entries) = glob(&format!("{}/{}", search_from.to_string_lossy(), suffix)) else {
67 continue;
68 };
69 for found in entries.flatten() {
70 if config.checks(&found, root) {
71 selection.selected.push(found);
72 } else if with_rejected {
73 let by = if !config.admits_type(&found) {
74 RejectedBy::FileTypes
75 } else if config.includes(&found, root) {
76 RejectedBy::Exclude
77 } else {
78 RejectedBy::Include
79 };
80 selection.rejected.push((found, by));
81 }
82 }
83 }
84 selection.selected.sort();
85 selection.selected.dedup();
86 selection.rejected.sort();
87 selection.rejected.dedup();
88 selection
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn workspace(files: &[&str], config: &str) -> tempfile::TempDir {
96 let dir = tempfile::tempdir().unwrap();
97 for file in files {
98 let path = dir.path().join(file);
99 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
100 std::fs::write(path, "Some prose.\n").unwrap();
101 }
102 std::fs::write(dir.path().join(".languagecheck.yaml"), config).unwrap();
103 dir
104 }
105
106 fn relative(root: &Path, paths: impl IntoIterator<Item = PathBuf>) -> Vec<String> {
107 paths
108 .into_iter()
109 .map(|p| {
110 p.strip_prefix(root)
111 .unwrap()
112 .to_string_lossy()
113 .replace('\\', "/")
114 })
115 .collect()
116 }
117
118 #[test]
119 fn include_selects_and_exclude_subtracts() {
120 let dir = workspace(
121 &["docs/a.md", "docs/drafts/b.md", "notes.md", "page.html"],
122 "include: [\"docs/**\"]\nexclude: [\"docs/drafts/**\"]\n",
123 );
124 let config = Config::load(dir.path()).unwrap();
125 let selection = select_files(&config, dir.path(), dir.path(), true);
126
127 assert_eq!(relative(dir.path(), selection.selected), ["docs/a.md"]);
128 let rejected: Vec<(String, RejectedBy)> = selection
129 .rejected
130 .into_iter()
131 .map(|(p, by)| (relative(dir.path(), [p]).remove(0), by))
132 .collect();
133 assert!(rejected.contains(&("docs/drafts/b.md".into(), RejectedBy::Exclude)));
134 assert!(rejected.contains(&("notes.md".into(), RejectedBy::Include)));
135 assert!(rejected.contains(&("page.html".into(), RejectedBy::Include)));
136 }
137
138 #[test]
139 fn file_types_is_named_before_include() {
140 let dir = workspace(&["a.md", "b.html"], "file_types: [md]\n");
141 let config = Config::load(dir.path()).unwrap();
142 let selection = select_files(&config, dir.path(), dir.path(), true);
143
144 assert_eq!(relative(dir.path(), selection.selected), ["a.md"]);
145 assert_eq!(selection.rejected.len(), 1);
146 assert_eq!(selection.rejected[0].1, RejectedBy::FileTypes);
147 }
148
149 #[test]
150 fn rejected_is_left_empty_unless_asked_for() {
151 let dir = workspace(&["a.md", "b.md"], "exclude: [\"b.md\"]\n");
152 let config = Config::load(dir.path()).unwrap();
153 let selection = select_files(&config, dir.path(), dir.path(), false);
154
155 assert_eq!(relative(dir.path(), selection.selected), ["a.md"]);
156 assert_eq!(selection.rejected, Vec::<(PathBuf, RejectedBy)>::new());
157 }
158}