1use log::{debug, info};
4use regex::Regex;
5use std::{
6 cmp::Ordering,
7 iter::{Filter, FilterMap},
8 path::Path,
9};
10use strum::{EnumIter, EnumString};
11use walkdir::{DirEntry, Error, IntoIter, WalkDir};
12
13use super::LintError;
14use crate::InstantiatedConfig;
15
16#[derive(Clone, Copy, Debug, strum::Display, EnumIter, EnumString)]
21#[strum(serialize_all = "snake_case")]
22pub enum LintCheck {
23 CodeResultsSubdirPairing,
27
28 CodeResultsSubdirRegex,
33
34 CodeSubdirReadmes,
37
38 CodeSubdirWorkflows,
41
42 DataSubdirReadmes,
45
46 RootDirs,
49
50 RootFiles,
53}
54
55impl LintCheck {
56 pub fn check(self, cfg: &InstantiatedConfig) -> Result<(), LintError> {
61 info!("Checking lint: {}", self);
62 match self {
63 Self::CodeResultsSubdirPairing => {
64 if let (Some(code), Some(results)) = (&cfg.root_dirs.code, &cfg.root_dirs.results) {
65 code_results_subdir_pairing(code, results)?;
66 }
67 }
68 Self::CodeResultsSubdirRegex => {
69 if let (Some(code), Some(re)) =
70 (&cfg.root_dirs.code, &cfg.code_results_subdir_regex)
71 {
72 code_results_dir_regex(code, re)?;
73 }
74 if let (Some(results), Some(re)) =
75 (&cfg.root_dirs.results, &cfg.code_results_subdir_regex)
76 {
77 code_results_dir_regex(results, re)?;
78 }
79 }
80 Self::CodeSubdirReadmes => {
81 if let (Some(code), Some(readmes)) = (&cfg.root_dirs.code, &cfg.readme_names) {
82 let readmes = readmes.iter().map(String::as_str).collect();
84 subdir_readmes(code, readmes)?;
85 }
86 }
87 Self::CodeSubdirWorkflows => {
88 if let (Some(code), Some(workflows)) = (&cfg.root_dirs.code, &cfg.workflow_names) {
89 let workflows = workflows.iter().map(String::as_str).collect();
91 subdir_workflows(code, workflows)?;
92 }
93 }
94 Self::DataSubdirReadmes => {
95 if let (Some(data), Some(readmes)) = (&cfg.root_dirs.data, &cfg.readme_names) {
96 let readmes = readmes.iter().map(String::as_str).collect();
98 subdir_readmes(data, readmes)?;
99 }
100 }
101 Self::RootDirs => {
102 let root_dirs = [
103 cfg.root_dirs.code.clone(),
104 cfg.root_dirs.data.clone(),
105 cfg.root_dirs.docs.clone(),
106 cfg.root_dirs.external.clone(),
107 cfg.root_dirs.results.clone(),
108 ];
109 for dir in root_dirs.iter().filter_map(|d| d.as_ref()) {
110 dir_exists(dir)?;
111 }
112 }
113 Self::RootFiles => {
114 for file in &cfg.root_files {
115 file_exists(file)?;
116 }
117 }
118 }
119
120 Ok(())
121 }
122}
123
124fn directory_walker(
129 path: &Path,
130) -> Filter<
131 FilterMap<IntoIter, impl FnMut(Result<DirEntry, Error>) -> Option<DirEntry>>,
132 impl FnMut(&DirEntry) -> bool,
133> {
134 WalkDir::new(path)
135 .max_depth(1)
136 .min_depth(1)
137 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
138 .into_iter()
139 .filter_map(|e| e.ok())
140 .filter(|e| match e.metadata() {
141 Ok(meta) => meta.is_dir(),
142 Err(_) => false,
143 })
144}
145
146pub(crate) fn code_results_subdir_pairing(code: &Path, results: &Path) -> Result<(), LintError> {
150 let mut code_walker = directory_walker(code);
151 let mut results_walker = directory_walker(results);
152
153 let mut code_subdir_opt = code_walker.next();
154 let mut results_subdir_opt = results_walker.next();
155
156 loop {
157 match (code_subdir_opt, results_subdir_opt) {
160 (None, None) => break,
161 (Some(code_subdir), None) => {
162 let formatted_code_subdir_name = code.join(code_subdir.file_name()).to_path_buf();
163 let formatted_results_subdir_name =
164 results.join(code_subdir.file_name()).to_path_buf();
165 return Err(LintError::CodeSubdirectoryMissingInResults {
166 code: formatted_code_subdir_name,
167 results: formatted_results_subdir_name,
168 });
169 }
170 (None, Some(results_subdir)) => {
171 let formatted_results_subdir_name =
172 results.join(results_subdir.file_name()).to_path_buf();
173 let formatted_code_subdir_name =
174 code.join(results_subdir.file_name()).to_path_buf();
175 return Err(LintError::ResultsSubdirectoryMissingInCode {
176 code: formatted_code_subdir_name,
177 results: formatted_results_subdir_name,
178 });
179 }
180 (Some(code_subdir), Some(results_subdir)) => {
181 match code_subdir.file_name().cmp(results_subdir.file_name()) {
182 Ordering::Equal => {
183 code_subdir_opt = code_walker.next();
185 results_subdir_opt = results_walker.next();
186 }
187 Ordering::Less => {
188 let formatted_code_subdir_name =
189 code.join(code_subdir.file_name()).to_path_buf();
190 let formatted_results_subdir_name =
191 results.join(code_subdir.file_name()).to_path_buf();
192 return Err(LintError::CodeSubdirectoryMissingInResults {
193 code: formatted_code_subdir_name,
194 results: formatted_results_subdir_name,
195 });
196 }
197 Ordering::Greater => {
198 let formatted_results_subdir_name =
199 results.join(results_subdir.file_name()).to_path_buf();
200 let formatted_code_subdir_name =
201 code.join(results_subdir.file_name()).to_path_buf();
202 return Err(LintError::ResultsSubdirectoryMissingInCode {
203 code: formatted_code_subdir_name,
204 results: formatted_results_subdir_name,
205 });
206 }
207 }
208 }
209 }
210 }
211
212 Ok(())
213}
214
215pub(crate) fn code_results_dir_regex(path: &Path, re: &Regex) -> Result<(), LintError> {
218 let walker = directory_walker(path);
219 for entry in walker {
220 if let Some(dir_name) = entry.path().file_name() {
221 if let Some(dir_str) = dir_name.to_str() {
222 if !re.is_match_at(dir_str, 0) {
223 return Err(LintError::CodeResultsSubdirectoryRegexMismatch(
224 entry.into_path(),
225 re.to_string(),
226 ));
227 }
228 }
229 }
230 }
231 Ok(())
232}
233
234pub fn dir_exists(path: &Path) -> Result<(), LintError> {
238 if path.is_dir() && path.exists() {
239 Ok(())
240 } else {
241 Err(LintError::RequiredDirectoryDoesNotExist(path.to_path_buf()))
242 }
243}
244
245pub fn file_exists(path: &Path) -> Result<(), LintError> {
249 if path.is_file() && path.exists() {
250 Ok(())
251 } else {
252 Err(LintError::RequiredFileDoesNotExist(path.to_path_buf()))
253 }
254}
255
256fn subdir_readmes(path: &Path, readmes: Vec<&str>) -> Result<(), LintError> {
258 debug!("Checking for READMEs in {}", path.display());
259 let walker = directory_walker(path);
260 for entry in walker {
261 debug!("Checking for README in {}", entry.path().display());
262 let mut subdir_readme = false;
263
264 for readme in &readmes {
265 let readme_path = entry.path().join(readme);
266 debug!("Checking for {}", readme_path.display());
267 if readme_path.exists() {
268 subdir_readme = true;
269 }
270 }
271
272 if !subdir_readme {
273 return Err(LintError::SubdirectoryMissingReadme(
274 entry.path().to_path_buf(),
275 ));
276 }
277 }
278
279 Ok(())
280}
281
282fn subdir_workflows(path: &Path, workflows: Vec<&str>) -> Result<(), LintError> {
284 debug!("Checking for workflow files in {}", path.display());
285 let walker = directory_walker(path);
286 for entry in walker {
287 debug!("Checking for workflow file in {}", entry.path().display());
288 let mut subdir_workflow = false;
289
290 for workflow in &workflows {
291 let workflow_path = entry.path().join(workflow);
292 debug!("Checking for {}", workflow_path.display());
293 if workflow_path.exists() {
294 subdir_workflow = true;
295 }
296 }
297
298 if !subdir_workflow {
299 return Err(LintError::SubdirectoryMissingWorkflow(
300 entry.path().to_path_buf(),
301 ));
302 }
303 }
304
305 Ok(())
306}