libscilo/config/instantiated_config/
lints.rs

1//! This module contains methods and helper functions that run checks on an [`InstantiatedConfig`].
2
3use std::{cmp::Ordering, path::PathBuf};
4use std::path::Path;
5
6use log::debug;
7use regex::Regex;
8use walkdir::DirEntry;
9
10use crate::{InstantiatedConfig, LintError, fs::directory_walker};
11
12impl InstantiatedConfig {
13    /// The main method to run all the requested lints on the project directory.
14    pub fn execute_lints(self) -> Result<(), LintError> {
15        for lint in self.lints.iter() {
16            lint.check(&self)?;
17        }
18        Ok(())
19    }
20
21    /// Check for the presence of each required root directory.
22    pub(crate) fn check_root_dirs(&self) -> Result<(), LintError> {
23        let root_dirs = self.root_dirs.clone();
24        for dir in root_dirs.into_iter().filter_map(|d| d) {
25            dir_exists(dir.as_path())?;
26        }
27
28        Ok(())
29    }
30
31    /// Check for the presence of each required file in the project root.
32    pub(crate) fn check_root_files(&self) -> Result<(), LintError> {
33        for file in &self.root_files {
34            file_exists(file)?;
35        }
36
37        Ok(())
38    }
39
40    /// Check for the presence of workflow files within each [`code`][crate::RootDirs::code] subdirectory.
41    pub(crate) fn check_code_subdir_workflows(&self) -> Result<(), LintError> {
42        if let (Some(code), Some(workflows)) = (&self.root_dirs.code, &self.workflow_names) {
43            debug!("Checking for workflow files in {}", code.display());
44            let workflows: Vec<&str> = workflows.iter().map(String::as_str).collect();
45            let walker = directory_walker(code.as_path(), &self.ignored);
46            for entry in walker {
47                debug!("Checking for workflow file in {}", entry.path().display());
48                let mut subdir_workflow = false;
49
50                for workflow in &workflows {
51                    let workflow_path = entry.path().join(workflow);
52                    debug!("Checking for {}", workflow_path.display());
53                    if workflow_path.exists() {
54                        subdir_workflow = true;
55                    }
56                }
57
58                if !subdir_workflow {
59                    return Err(LintError::SubdirectoryMissingWorkflow(
60                        entry.path().to_path_buf(),
61                    ));
62                }
63            }
64        }
65
66        Ok(())
67    }
68
69    /// Check for the existence of at least one acceptable README file in the [`code`][crate::RootDirs::code] directory.
70    pub(crate) fn check_code_subdir_readmes(&self) -> Result<(), LintError> {
71        if let (Some(code), Some(readmes)) = (&self.root_dirs.code, &self.readme_names) {
72            // this remaps the variable so the input type matches the function signature
73            let readmes = readmes.iter().map(String::as_str).collect();
74            subdir_readmes(code, readmes, &self.ignored)
75        } else {
76            Ok(())
77        }
78    }
79
80    /// Check for the existence of at least one acceptable README file in the [`data`][crate::RootDirs::data] directory.
81    pub(crate) fn check_data_subdir_readmes(&self) -> Result<(), LintError> {
82        if let (Some(data), Some(readmes)) = (&self.root_dirs.data, &self.readme_names) {
83            // this remaps the variable so the input type matches the function signature
84            let readmes = readmes.iter().map(String::as_str).collect();
85            subdir_readmes(data, readmes, &self.ignored)
86        } else {
87            Ok(())
88        }
89    }
90
91    /// Check that the subdirectories of the [`code`][crate::RootDirs::code]
92    /// and [`results`][crate::RootDirs::results] follow the correct style.
93    pub(crate) fn check_code_results_subdir_regex(&self) -> Result<(), LintError> {
94        if let Some(re) = &self.code_results_subdir_regex {
95            if let Some(code) = &self.root_dirs.code {
96                subdir_regex(code, re, &self.ignored)?;
97            }
98            if let Some(results) = &self.root_dirs.results {
99                subdir_regex(results, re, &self.ignored)?;
100            }
101        }
102
103        Ok(())
104    }
105
106    /// Check that every subdirectory within [`code`][crate::RootDirs::code]
107    /// has a corresponding subdirectory in [`results`][crate::RootDirs::results]
108    /// and vice versa.
109    pub(crate) fn check_code_results_subdir_pairing(&self) -> Result<(), LintError> {
110        if let (Some(code), Some(results)) = (&self.root_dirs.code, &self.root_dirs.results) {
111            let mut code_walker = directory_walker(code, &self.ignored);
112            let mut results_walker = directory_walker(results, &self.ignored);
113
114            let mut code_subdir_opt = code_walker.next();
115            let mut results_subdir_opt = results_walker.next();
116
117            loop {
118                // because these are guaranteed to be in sorted order, we just need to
119                // compare them element by element
120                match (code_subdir_opt, results_subdir_opt) {
121                    (None, None) => break,
122                    (Some(subdir), None) => {
123                        return missing_results_subdir(code, results, subdir);
124                    }
125                    (None, Some(subdir)) => {
126                        return missing_code_subdir(code, results, subdir);
127                    }
128                    (Some(code_subdir), Some(results_subdir)) => {
129                        match code_subdir.file_name().cmp(results_subdir.file_name()) {
130                            Ordering::Equal => {
131                                // move to the next set of item to compare
132                                code_subdir_opt = code_walker.next();
133                                results_subdir_opt = results_walker.next();
134                            }
135                            Ordering::Less => {
136                                return missing_results_subdir(code, results, code_subdir);
137                            }
138                            Ordering::Greater => {
139                                return missing_code_subdir(code, results, results_subdir);
140                            }
141                        }
142                    }
143                }
144            }
145        }
146
147        Ok(())
148    }
149}
150
151/// Check that a required directory exists.
152///
153/// This is a wrapper around [`Path::is_dir()`] that returns the correct [`LintError`].
154pub fn dir_exists(path: &Path) -> Result<(), LintError> {
155    if path.exists() && path.is_dir() {
156        Ok(())
157    } else {
158        Err(LintError::RequiredDirectoryDoesNotExist(path.to_path_buf()))
159    }
160}
161
162/// Check that a required file exists.
163///
164/// This is a wrapper around [`Path::is_file()`] that returns the correct [`LintError`].
165fn file_exists(path: &Path) -> Result<(), LintError> {
166    if path.exists() && path.is_file() {
167        Ok(())
168    } else {
169        Err(LintError::RequiredFileDoesNotExist(path.to_path_buf()))
170    }
171}
172
173/// Check for the presence of README files within each subdirectory of a [root directory][crate::RootDirs].
174fn subdir_readmes(path: &Path, readmes: Vec<&str>, ignored: &Vec<PathBuf>) -> Result<(), LintError> {
175    debug!("Checking for READMEs in {}", path.display());
176    let walker = directory_walker(path, ignored);
177    for entry in walker {
178        debug!("Checking for README in {}", entry.path().display());
179        let mut subdir_readme = false;
180
181        for readme in &readmes {
182            let readme_path = entry.path().join(readme);
183            debug!("Checking for {}", readme_path.display());
184            if readme_path.exists() {
185                subdir_readme = true;
186                break
187            }
188        }
189
190        if !subdir_readme {
191            return Err(LintError::SubdirectoryMissingReadme(
192                entry.path().to_path_buf(),
193            ));
194        }
195    }
196
197    Ok(())
198}
199
200/// A helper function to check that a subdirectory's name matches a given regex.
201fn subdir_regex(path: &Path, re: &Regex, ignored: &Vec<PathBuf>) -> Result<(), LintError> {
202    let walker = directory_walker(path, ignored);
203    for entry in walker {
204        if let Some(dir_name) = entry.path().file_name() {
205            if let Some(dir_str) = dir_name.to_str() {
206                if !re.is_match_at(dir_str, 0) {
207                    return Err(LintError::CodeResultsSubdirectoryRegexMismatch(
208                        entry.into_path(),
209                        re.to_string(),
210                    ));
211                }
212            }
213        }
214    }
215    Ok(())
216}
217
218/// A helper function for formatting an error if a [`results`][crate::RootDirs::results] subdirectory is missing.
219fn missing_results_subdir(code: &Path, results: &Path, subdir: DirEntry) -> Result<(), LintError> {
220    let existing = code.join(subdir.file_name()).to_path_buf();
221    let missing = results.join(subdir.file_name()).to_path_buf();
222
223    Err(LintError::CodeSubdirectoryMissingInResults {
224        code: existing,
225        results: missing,
226    })
227}
228
229/// A helper function for formatting an error if a [`code`][crate::RootDirs::code] subdirectory is missing.
230fn missing_code_subdir(code: &Path, results: &Path, subdir: DirEntry) -> Result<(), LintError> {
231    let existing = results.join(subdir.file_name()).to_path_buf();
232    let missing = code.join(subdir.file_name()).to_path_buf();
233
234    Err(LintError::ResultsSubdirectoryMissingInCode {
235        code: missing,
236        results: existing,
237    })
238}