qlty_analysis/workspace_entries/
workspace_entry_finder_builder.rs

1use super::{LanguagesShebangMatcher, OrMatcher, TargetMode};
2use crate::{
3    git::GitDiff,
4    workspace_entries::{AndMatcher, LanguageGlobsMatcher},
5    AllSource, ArgsSource, DiffSource, FileMatcher, GlobsMatcher, WorkspaceEntryFinder,
6    WorkspaceEntryMatcher, WorkspaceEntrySource,
7};
8use anyhow::{bail, Result};
9use qlty_config::{
10    issue_transformer::{IssueTransformer, NullIssueTransformer},
11    QltyConfig,
12};
13use std::{collections::HashMap, path::PathBuf, sync::Arc};
14use tracing::debug;
15
16#[derive(Debug, Clone)]
17pub struct WorkspaceEntryFinderBuilder {
18    pub mode: TargetMode,
19    pub root: PathBuf,
20    pub paths: Vec<PathBuf>,
21    pub config: QltyConfig,
22    pub exclude_tests: bool,
23    pub cached_git_diff: Option<GitDiff>,
24}
25
26impl Default for WorkspaceEntryFinderBuilder {
27    fn default() -> Self {
28        Self {
29            mode: TargetMode::All,
30            root: std::env::current_dir().unwrap(),
31            paths: Vec::new(),
32            config: QltyConfig::default(),
33            exclude_tests: true,
34            cached_git_diff: None,
35        }
36    }
37}
38
39impl WorkspaceEntryFinderBuilder {
40    pub fn build(&mut self) -> Result<WorkspaceEntryFinder> {
41        Ok(WorkspaceEntryFinder::new(self.source()?, self.matcher()?))
42    }
43
44    pub fn diff_line_filter(&mut self) -> Result<Box<dyn IssueTransformer>> {
45        match self.mode {
46            TargetMode::HeadDiff | TargetMode::UpstreamDiff(_) => {
47                Ok(Box::new(self.git_diff()?.line_filter))
48            }
49            _ => Ok(Box::new(NullIssueTransformer)),
50        }
51    }
52
53    fn source(&mut self) -> Result<Arc<dyn WorkspaceEntrySource>> {
54        match self.mode {
55            TargetMode::All => Ok(Arc::new(AllSource::new(self.root.clone()))),
56            TargetMode::Paths(_) => Ok(Arc::new(ArgsSource::new(
57                self.root.clone(),
58                self.paths.clone(),
59            ))),
60            TargetMode::UpstreamDiff(_) => Ok(Arc::new(DiffSource::new(
61                self.git_diff()?.changed_files,
62                &self.root,
63            ))),
64            _ => bail!("Unsupported workspace entry mode: {:?}", self.mode),
65        }
66    }
67
68    fn matcher(&self) -> Result<Box<dyn WorkspaceEntryMatcher>> {
69        let mut matcher = AndMatcher::default();
70
71        // Files only
72        matcher.push(Box::new(FileMatcher));
73
74        // Ignore explicit ignores and tests
75        let mut ignores = self
76            .config
77            .ignore
78            .iter()
79            .flat_map(|i| i.file_patterns.clone())
80            .collect::<Vec<_>>();
81        debug!("Ignoring globs: {:?}", ignores);
82
83        if self.exclude_tests {
84            if !self.config.test_patterns.is_empty() {
85                debug!("Ignoring test patterns: {:?}", self.config.test_patterns);
86                ignores.extend(self.config.test_patterns.clone());
87            } else {
88                debug!("Ignoring test patterns: none");
89            }
90        }
91
92        let ignores = GlobsMatcher::new_for_globs(&ignores, false)?;
93        matcher.push(Box::new(ignores));
94
95        // Must match a language
96        matcher.push(self.languages_matcher()?);
97
98        Ok(Box::new(matcher))
99    }
100
101    fn languages_matcher(&self) -> Result<Box<dyn WorkspaceEntryMatcher>> {
102        let mut languages = OrMatcher::default();
103        let mut interpreters = HashMap::new();
104
105        for language_name in self.config.language.keys() {
106            let file_type = self.config.file_types.get(language_name).unwrap();
107            debug!(
108                "Matching {} with globs: {:?}",
109                language_name, file_type.globs
110            );
111            let matcher = LanguageGlobsMatcher::new(language_name, &file_type.globs)?;
112            languages.push(Box::new(matcher));
113
114            if !file_type.interpreters.is_empty() {
115                debug!(
116                    "Matching {} with interpretters: {:?}",
117                    language_name, file_type.interpreters
118                );
119                interpreters.insert(language_name.to_string(), file_type.interpreters.to_owned());
120            }
121        }
122
123        // If none of the globs match, we fallback to checking the shebang
124        // This is done last, so if a glob matches we never check the shebang
125        let shebangs = LanguagesShebangMatcher::new(interpreters);
126        languages.push(Box::new(shebangs));
127
128        Ok(Box::new(languages))
129    }
130
131    fn git_diff(&mut self) -> Result<GitDiff> {
132        GitDiff::compute(self.mode.diff_mode(), &self.root)
133    }
134}