qlty_analysis/workspace_entries/
matchers.rs1use super::workspace_entry::{WorkspaceEntry, WorkspaceEntryKind};
2use crate::{code::language_detector::get_language_from_shebang, utils::fs::path_to_string};
3use anyhow::{Context, Result};
4use globset::{Glob, GlobSet, GlobSetBuilder};
5use qlty_config::FileType;
6use std::{collections::HashMap, path::PathBuf};
7
8pub trait WorkspaceEntryMatcher: core::fmt::Debug {
9 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry>;
10}
11
12#[derive(Debug)]
13pub struct AnyMatcher;
14
15impl WorkspaceEntryMatcher for AnyMatcher {
16 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
17 Some(workspace_entry)
18 }
19}
20
21#[derive(Debug)]
23pub struct FileMatcher;
24
25impl WorkspaceEntryMatcher for FileMatcher {
26 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
27 if workspace_entry.kind == WorkspaceEntryKind::File {
28 Some(workspace_entry)
29 } else {
30 None
31 }
32 }
33}
34
35#[derive(Debug)]
37pub struct PrefixMatcher {
38 path_prefix: String,
39 root: PathBuf,
40}
41
42impl PrefixMatcher {
43 pub fn new(path_prefix: String, root: PathBuf) -> Self {
44 Self { path_prefix, root }
45 }
46}
47
48impl WorkspaceEntryMatcher for PrefixMatcher {
49 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
50 match workspace_entry.full_path(&self.root) {
51 Ok(full_path) => {
52 if full_path.starts_with(path_to_string(&self.path_prefix)) {
53 Some(workspace_entry)
54 } else {
55 None
56 }
57 }
58 Err(_e) => None,
59 }
60 }
61}
62
63#[derive(Debug)]
65pub struct GlobsMatcher {
66 glob_set: GlobSet,
67 include: bool,
68}
69
70impl GlobsMatcher {
71 pub fn new(glob_set: GlobSet, include: bool) -> Self {
72 Self { glob_set, include }
73 }
74
75 pub fn new_for_globs(globs: &[String], include: bool) -> Result<Self> {
76 let glob_set = globs_to_globset(globs)?;
77 Ok(Self { glob_set, include })
78 }
79
80 pub fn new_for_file_types(file_types: &[FileType]) -> Result<Self> {
81 let globs = file_types
82 .iter()
83 .flat_map(|file_type| file_type.globs.to_owned())
84 .collect::<Vec<String>>();
85 Self::new_for_globs(&globs, true)
86 }
87}
88
89impl WorkspaceEntryMatcher for GlobsMatcher {
90 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
91 let matches = self.glob_set.is_match(workspace_entry.path_string());
92
93 if matches == self.include {
94 Some(workspace_entry)
95 } else {
96 None
97 }
98 }
99}
100
101#[derive(Debug)]
106pub struct LanguageGlobsMatcher {
107 language_name: String,
108 matcher: GlobsMatcher,
109}
110
111impl LanguageGlobsMatcher {
112 pub fn new(language_name: &str, globs: &[String]) -> Result<Self> {
113 let matcher = GlobsMatcher::new_for_globs(globs, true)?;
114
115 Ok(Self {
116 language_name: language_name.to_string(),
117 matcher,
118 })
119 }
120}
121
122impl WorkspaceEntryMatcher for LanguageGlobsMatcher {
123 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
124 match self.matcher.matches(workspace_entry) {
125 Some(mut workspace_entry) => {
126 workspace_entry.language_name = Some(self.language_name.clone());
127 Some(workspace_entry)
128 }
129 None => None,
130 }
131 }
132}
133
134#[derive(Debug)]
137pub struct LanguagesShebangMatcher {
138 interpreters: HashMap<String, Vec<String>>,
139}
140
141impl LanguagesShebangMatcher {
142 pub fn new(interpreters: HashMap<String, Vec<String>>) -> Self {
143 Self { interpreters }
144 }
145}
146
147impl WorkspaceEntryMatcher for LanguagesShebangMatcher {
148 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
149 let file = std::fs::File::open(&workspace_entry.path);
150
151 if let Ok(file) = file {
152 let mut reader = std::io::BufReader::new(file);
153 if let Ok(language_name) = get_language_from_shebang(&mut reader, &self.interpreters) {
154 if !language_name.is_empty() {
155 return Some(WorkspaceEntry {
156 language_name: Some(language_name),
157 ..workspace_entry
158 });
159 }
160 }
161 }
162 None
163 }
164}
165
166#[derive(Default, Debug)]
168pub struct OrMatcher {
169 matchers: Vec<Box<dyn WorkspaceEntryMatcher>>,
170}
171
172impl OrMatcher {
173 pub fn new(matchers: Vec<Box<dyn WorkspaceEntryMatcher>>) -> Self {
174 Self { matchers }
175 }
176
177 pub fn push(&mut self, matcher: Box<dyn WorkspaceEntryMatcher>) {
178 self.matchers.push(matcher);
179 }
180}
181
182impl WorkspaceEntryMatcher for OrMatcher {
183 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
184 for matcher in &self.matchers {
185 if let Some(matched_workspace_entry) = matcher.matches(workspace_entry.clone()) {
186 return Some(matched_workspace_entry);
187 }
188 }
189
190 None
191 }
192}
193
194#[derive(Default, Debug)]
196pub struct AndMatcher {
197 matchers: Vec<Box<dyn WorkspaceEntryMatcher>>,
198}
199
200impl AndMatcher {
201 pub fn new(matchers: Vec<Box<dyn WorkspaceEntryMatcher>>) -> Self {
202 Self { matchers }
203 }
204
205 pub fn push(&mut self, matcher: Box<dyn WorkspaceEntryMatcher>) {
206 self.matchers.push(matcher);
207 }
208}
209
210impl WorkspaceEntryMatcher for AndMatcher {
211 fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
212 let mut matched_workspace_entry = workspace_entry.clone();
213
214 for matcher in &self.matchers {
215 if let Some(matched) = matcher.matches(matched_workspace_entry) {
216 matched_workspace_entry = matched;
217 } else {
218 return None;
219 }
220 }
221
222 Some(matched_workspace_entry)
223 }
224}
225
226fn globs_to_globset(globs: &[String]) -> Result<GlobSet> {
227 let mut builder = GlobSetBuilder::new();
228
229 for glob in globs {
230 builder
231 .add(Glob::new(glob).context("Failed to create a new Glob from the provided pattern")?);
232 }
233
234 Ok(builder.build()?)
235}
236
237#[cfg(test)]
238mod test {
239 use super::*;
240 use qlty_config::config::Builder;
241 use std::path::PathBuf;
242 use std::time::SystemTime;
243
244 #[test]
246 fn test_file_workspace_entry_matcher_matches_file() {
247 let workspace_entry = WorkspaceEntry {
248 path: PathBuf::from("/path/to/file.txt"),
249 kind: WorkspaceEntryKind::File,
250 content_modified: SystemTime::now(),
251 contents_size: 100,
252 language_name: None,
253 };
254
255 let matcher = FileMatcher;
256 assert!(
257 matcher.matches(workspace_entry).is_some(),
258 "Expected workspace_entry to match as it is of type File"
259 );
260 }
261
262 #[test]
263 fn test_file_workspace_entry_matcher_does_not_match_directory() {
264 let workspace_entry = WorkspaceEntry {
265 path: PathBuf::from("/path/to/directory/"),
266 kind: WorkspaceEntryKind::Directory,
267 content_modified: SystemTime::now(),
268 contents_size: 0, language_name: None,
270 };
271
272 let matcher = FileMatcher;
273 assert!(
274 matcher.matches(workspace_entry).is_none(),
275 "Expected workspace_entry not to match as it is of type Directory"
276 );
277 }
278
279 #[test]
281 fn test_glob_set_workspace_entry_matcher_matches_file() {
282 let workspace_entry = WorkspaceEntry {
283 path: PathBuf::from("/path/to/file.rs"),
284 kind: WorkspaceEntryKind::File,
285 content_modified: SystemTime::now(),
286 contents_size: 100,
287 language_name: None,
288 };
289 let config = Builder::default_config().unwrap().to_owned();
290 let all_file_types = config.file_types.to_owned();
291 let file_types_names = vec!["rust".to_owned()];
292 let file_types = all_file_types
293 .iter()
294 .filter_map(|(name, file_type)| {
295 if file_types_names.contains(&name) {
296 Some(file_type.clone())
297 } else {
298 None
299 }
300 })
301 .collect::<Vec<_>>();
302 let matcher = GlobsMatcher::new_for_file_types(&file_types).unwrap();
303
304 assert!(
305 matcher.matches(workspace_entry).is_some(),
306 "Expected workspace_entry to match as it is of type rust"
307 );
308 }
309
310 #[test]
311 fn test_glob_set_workspace_entry_matcher_does_not_match_file() {
312 let workspace_entry = WorkspaceEntry {
313 path: PathBuf::from("/path/to/file.rb"),
314 kind: WorkspaceEntryKind::File,
315 content_modified: SystemTime::now(),
316 contents_size: 100,
317 language_name: None,
318 };
319 let config = Builder::default_config().unwrap().to_owned();
320 let all_file_types = config.file_types.to_owned();
321 let file_types_names = vec!["rust".to_owned()];
322 let file_types = all_file_types
323 .iter()
324 .filter_map(|(name, file_type)| {
325 if file_types_names.contains(&name) {
326 Some(file_type.clone())
327 } else {
328 None
329 }
330 })
331 .collect::<Vec<_>>();
332 let matcher = GlobsMatcher::new_for_file_types(&file_types).unwrap();
333
334 assert!(
335 matcher.matches(workspace_entry).is_none(),
336 "Expected workspace_entry not to match as it is of type ruby"
337 );
338 }
339}