qlty_analysis/workspace_entries/
workspace_entry_finder.rs

1use std::sync::Arc;
2
3use crate::{code::File, WorkspaceEntry, WorkspaceEntrySource};
4
5use super::{
6    all_source::AllSource,
7    matchers::{AnyMatcher, WorkspaceEntryMatcher},
8};
9use anyhow::Result;
10use rand::seq::SliceRandom;
11use rand::thread_rng;
12use tracing::{debug, trace, warn};
13
14pub struct WorkspaceEntryFinder {
15    source: Arc<dyn WorkspaceEntrySource>,
16    matcher: Box<dyn WorkspaceEntryMatcher>,
17    results: Option<Vec<WorkspaceEntry>>,
18}
19
20impl Default for WorkspaceEntryFinder {
21    fn default() -> Self {
22        Self::new(Arc::new(AllSource::default()), Box::new(AnyMatcher))
23    }
24}
25
26impl WorkspaceEntryFinder {
27    pub fn new(
28        source: Arc<dyn WorkspaceEntrySource>,
29        matcher: Box<dyn WorkspaceEntryMatcher>,
30    ) -> Self {
31        debug!("Creating workspace entry finder from source: {:?}", source);
32
33        Self {
34            source,
35            matcher,
36            results: None,
37        }
38    }
39
40    pub fn files(&mut self) -> Result<Vec<Arc<File>>> {
41        let workspace_entries = self.workspace_entries()?;
42        let mut files = Vec::new();
43
44        for workspace_entry in workspace_entries {
45            match File::from_workspace_entry(&workspace_entry) {
46                Ok(file) => {
47                    trace!("{:?}", workspace_entry);
48                    files.push(file)
49                }
50                Err(e) => warn!("Unable to process workspace entry: {}", e),
51            }
52        }
53
54        Ok(files)
55    }
56
57    pub fn workspace_entries(&mut self) -> Result<Vec<WorkspaceEntry>> {
58        self.compute_workspace_entries()?;
59        Ok(self.results.as_ref().unwrap().to_owned())
60    }
61
62    pub fn sample(&mut self, sample: usize) -> Result<Vec<WorkspaceEntry>> {
63        self.compute_workspace_entries()?;
64        let mut workspace_entries = self.results.as_ref().unwrap().to_owned();
65        workspace_entries.shuffle(&mut thread_rng());
66        workspace_entries.truncate(sample);
67        Ok(workspace_entries)
68    }
69
70    fn compute_workspace_entries(&mut self) -> Result<()> {
71        if self.results.is_some() {
72            return Ok(());
73        }
74
75        let mut workspace_entries = vec![];
76
77        for workspace_entry in self.source.entries().iter() {
78            if let Some(workspace_entry) = self.matcher.matches(workspace_entry.clone()) {
79                trace!("Adding workspace entry: {:?}", &workspace_entry);
80                workspace_entries.push(workspace_entry);
81            } else {
82                trace!("Skipping workspace entry: {:?}", &workspace_entry);
83            }
84        }
85
86        self.results = Some(workspace_entries);
87        Ok(())
88    }
89}
90
91#[cfg(test)]
92mod test {
93    use super::*;
94    use crate::{
95        workspace_entries::{
96            matchers::{FileMatcher, GlobsMatcher},
97            AndMatcher,
98        },
99        WorkspaceEntryKind,
100    };
101    use itertools::Itertools;
102    use qlty_config::config::Builder;
103    use qlty_test_utilities::git::build_sample_project;
104    use std::path::PathBuf;
105
106    #[test]
107    fn test_find_without_matchers() -> Result<()> {
108        let root = build_sample_project();
109        let source = AllSource::new(root.path().to_path_buf());
110        let mut workspace_entry_finder =
111            WorkspaceEntryFinder::new(Arc::new(source), Box::new(AnyMatcher));
112        let workspace_entries = workspace_entry_finder.workspace_entries().unwrap();
113        let mut paths = vec![];
114        for workspace_entry in workspace_entries {
115            paths.push((workspace_entry.path, workspace_entry.kind));
116        }
117
118        let expected_paths = build_expected_workspace_entries(vec![
119            ("", WorkspaceEntryKind::Directory),
120            (".gitignore", WorkspaceEntryKind::File),
121            ("lib", WorkspaceEntryKind::Directory),
122            ("lib/hello.rb", WorkspaceEntryKind::File),
123            ("lib/tasks", WorkspaceEntryKind::Directory),
124            ("lib/tasks/ops", WorkspaceEntryKind::Directory),
125            ("lib/tasks/ops/deploy.rb", WorkspaceEntryKind::File),
126            ("lib/tasks/ops/setup.rb", WorkspaceEntryKind::File),
127            ("lib/tasks/some.rb", WorkspaceEntryKind::File),
128            ("greetings.rb", WorkspaceEntryKind::File),
129            ("README.md", WorkspaceEntryKind::File),
130        ]);
131
132        assert_eq!(
133            paths
134                .iter()
135                .cloned()
136                .sorted()
137                .collect::<Vec<(PathBuf, WorkspaceEntryKind)>>(),
138            expected_paths
139        );
140
141        Ok(())
142    }
143
144    #[test]
145    fn test_find_with_matchers() -> Result<()> {
146        let root = build_sample_project();
147        let source = AllSource::new(root.path().to_path_buf());
148        let file_matcher = Box::new(FileMatcher) as Box<dyn WorkspaceEntryMatcher>;
149        let file_type_matcher = build_file_types_workspace_entry_matcher()?;
150        let file_type_matcher = Box::new(file_type_matcher) as Box<dyn WorkspaceEntryMatcher>;
151
152        let mut workspace_entry_finder = WorkspaceEntryFinder::new(
153            Arc::new(source),
154            Box::new(AndMatcher::new(vec![file_matcher, file_type_matcher])),
155        );
156
157        let workspace_entries = workspace_entry_finder.workspace_entries().unwrap();
158        let mut paths = vec![];
159        for workspace_entry in workspace_entries {
160            paths.push((workspace_entry.path, workspace_entry.kind));
161        }
162
163        let expected_paths = build_expected_workspace_entries(vec![
164            ("lib/hello.rb", WorkspaceEntryKind::File),
165            ("lib/tasks/ops/deploy.rb", WorkspaceEntryKind::File),
166            ("lib/tasks/ops/setup.rb", WorkspaceEntryKind::File),
167            ("lib/tasks/some.rb", WorkspaceEntryKind::File),
168            ("greetings.rb", WorkspaceEntryKind::File),
169        ]);
170
171        assert_eq!(
172            paths
173                .iter()
174                .cloned()
175                .sorted()
176                .collect::<Vec<(PathBuf, WorkspaceEntryKind)>>(),
177            expected_paths
178        );
179
180        Ok(())
181    }
182
183    fn build_file_types_workspace_entry_matcher() -> Result<GlobsMatcher> {
184        let project_config = Builder::default_config().unwrap().to_owned();
185        let all_file_types = project_config.file_types.to_owned();
186        let file_types_names = vec!["ruby".to_owned()];
187        let file_types = all_file_types
188            .iter()
189            .filter_map(|(name, file_type)| {
190                if file_types_names.contains(&name) {
191                    Some(file_type.clone())
192                } else {
193                    None
194                }
195            })
196            .collect::<Vec<_>>();
197
198        GlobsMatcher::new_for_file_types(&file_types)
199    }
200
201    #[test]
202    fn test_sample() -> Result<()> {
203        let root = build_sample_project();
204        let source = AllSource::new(root.path().to_path_buf());
205        let mut workspace_entry_finder =
206            WorkspaceEntryFinder::new(Arc::new(source), Box::new(AnyMatcher));
207        let workspace_entries = workspace_entry_finder.sample(3).unwrap();
208        let mut paths = vec![];
209        for workspace_entry in workspace_entries {
210            paths.push(workspace_entry.path);
211        }
212
213        let possible_paths = vec![
214            PathBuf::from(""),
215            PathBuf::from(".gitignore"),
216            PathBuf::from("lib"),
217            PathBuf::from("lib/hello.rb"),
218            PathBuf::from("lib/tasks"),
219            PathBuf::from("lib/tasks/ops"),
220            PathBuf::from("lib/tasks/ops/deploy.rb"),
221            PathBuf::from("lib/tasks/ops/setup.rb"),
222            PathBuf::from("lib/tasks/some.rb"),
223            PathBuf::from("greetings.rb"),
224            PathBuf::from("README.md"),
225        ];
226
227        assert!(possible_paths.contains(&paths[0]));
228        assert!(possible_paths.contains(&paths[1]));
229        assert!(possible_paths.contains(&paths[2]));
230        assert!(paths.len() == 3);
231
232        Ok(())
233    }
234
235    fn build_expected_workspace_entries(
236        workspace_entries: Vec<(&str, WorkspaceEntryKind)>,
237    ) -> Vec<(PathBuf, WorkspaceEntryKind)> {
238        workspace_entries
239            .into_iter()
240            .map(|(s, tt)| (PathBuf::from(s), tt))
241            .sorted()
242            .collect()
243    }
244}