Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add negated ignores support #1472

Merged
merged 6 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qlty-analysis/src/workspace_entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub use all_source::AllSource;
pub use args_source::ArgsSource;
pub use diff_source::DiffSource;
pub use matchers::{
AndMatcher, AnyMatcher, FileMatcher, GlobsMatcher, LanguageGlobsMatcher,
AndMatcher, AnyMatcher, FileMatcher, GlobsMatcher, IgnoreGroupsMatcher, LanguageGlobsMatcher,
LanguagesShebangMatcher, OrMatcher, PrefixMatcher, WorkspaceEntryMatcher,
};
pub use target_mode::TargetMode;
Expand Down
356 changes: 21 additions & 335 deletions qlty-analysis/src/workspace_entries/matchers.rs
Original file line number Diff line number Diff line change
@@ -1,339 +1,25 @@
use super::workspace_entry::{WorkspaceEntry, WorkspaceEntryKind};
use crate::{code::language_detector::get_language_from_shebang, utils::fs::path_to_string};
use anyhow::{Context, Result};
use globset::{Glob, GlobSet, GlobSetBuilder};
use qlty_config::FileType;
use std::{collections::HashMap, path::PathBuf};
mod and_matcher;
mod any_matcher;
mod file_matcher;
mod globs_matcher;
mod ignore_groups_matcher;
mod language_globs_matcher;
mod languages_shebang_matcher;
mod or_matcher;
mod prefix_matcher;

pub use and_matcher::AndMatcher;
pub use any_matcher::AnyMatcher;
pub use file_matcher::FileMatcher;
pub use globs_matcher::GlobsMatcher;
pub use ignore_groups_matcher::IgnoreGroupsMatcher;
pub use language_globs_matcher::LanguageGlobsMatcher;
pub use languages_shebang_matcher::LanguagesShebangMatcher;
pub use or_matcher::OrMatcher;
pub use prefix_matcher::PrefixMatcher;

use super::workspace_entry::WorkspaceEntry;

pub trait WorkspaceEntryMatcher: core::fmt::Debug {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry>;
}

#[derive(Debug)]
pub struct AnyMatcher;

impl WorkspaceEntryMatcher for AnyMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
Some(workspace_entry)
}
}

/// Matches workspace entries that are of type `WorkspaceEntryKind::File`
#[derive(Debug)]
pub struct FileMatcher;

impl WorkspaceEntryMatcher for FileMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
if workspace_entry.kind == WorkspaceEntryKind::File {
Some(workspace_entry)
} else {
None
}
}
}

/// Matches workspace entries that are within a path prefix (a directory)
#[derive(Debug)]
pub struct PrefixMatcher {
path_prefix: String,
root: PathBuf,
}

impl PrefixMatcher {
pub fn new(path_prefix: String, root: PathBuf) -> Self {
Self { path_prefix, root }
}
}

impl WorkspaceEntryMatcher for PrefixMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
match workspace_entry.full_path(&self.root) {
Ok(full_path) => {
if full_path.starts_with(path_to_string(&self.path_prefix)) {
Some(workspace_entry)
} else {
None
}
}
Err(_e) => None,
}
}
}

/// Compares workspace entries against a `GlobSet`. Either includes or excludes.
#[derive(Debug)]
pub struct GlobsMatcher {
glob_set: GlobSet,
include: bool,
}

impl GlobsMatcher {
pub fn new(glob_set: GlobSet, include: bool) -> Self {
Self { glob_set, include }
}

pub fn new_for_globs(globs: &[String], include: bool) -> Result<Self> {
let glob_set = globs_to_globset(globs)?;
Ok(Self { glob_set, include })
}

pub fn new_for_file_types(file_types: &[FileType]) -> Result<Self> {
let globs = file_types
.iter()
.flat_map(|file_type| file_type.globs.to_owned())
.collect::<Vec<String>>();
Self::new_for_globs(&globs, true)
}
}

impl WorkspaceEntryMatcher for GlobsMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
let matches = self.glob_set.is_match(workspace_entry.path_string());

if matches == self.include {
Some(workspace_entry)
} else {
None
}
}
}

/// Matches workspace entries that are of a single specific language and
/// amends the `WorkspaceEntry` with the `Language`
///
/// Uses GlobSetWorkspaceEntryMatcher under the hood.
#[derive(Debug)]
pub struct LanguageGlobsMatcher {
language_name: String,
matcher: GlobsMatcher,
}

impl LanguageGlobsMatcher {
pub fn new(language_name: &str, globs: &[String]) -> Result<Self> {
let matcher = GlobsMatcher::new_for_globs(globs, true)?;

Ok(Self {
language_name: language_name.to_string(),
matcher,
})
}
}

impl WorkspaceEntryMatcher for LanguageGlobsMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
match self.matcher.matches(workspace_entry) {
Some(mut workspace_entry) => {
workspace_entry.language_name = Some(self.language_name.clone());
Some(workspace_entry)
}
None => None,
}
}
}

/// Matches workspace entries that have a sheband line that includes an interpretter
/// This requires reading the contents of the file.
#[derive(Debug)]
pub struct LanguagesShebangMatcher {
interpreters: HashMap<String, Vec<String>>,
}

impl LanguagesShebangMatcher {
pub fn new(interpreters: HashMap<String, Vec<String>>) -> Self {
Self { interpreters }
}
}

impl WorkspaceEntryMatcher for LanguagesShebangMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
let file = std::fs::File::open(&workspace_entry.path);

if let Ok(file) = file {
let mut reader = std::io::BufReader::new(file);
if let Ok(language_name) = get_language_from_shebang(&mut reader, &self.interpreters) {
if !language_name.is_empty() {
return Some(WorkspaceEntry {
language_name: Some(language_name),
..workspace_entry
});
}
}
}
None
}
}

/// Matches workspace entries that match ANY of the provided matchers
#[derive(Default, Debug)]
pub struct OrMatcher {
matchers: Vec<Box<dyn WorkspaceEntryMatcher>>,
}

impl OrMatcher {
pub fn new(matchers: Vec<Box<dyn WorkspaceEntryMatcher>>) -> Self {
Self { matchers }
}

pub fn push(&mut self, matcher: Box<dyn WorkspaceEntryMatcher>) {
self.matchers.push(matcher);
}
}

impl WorkspaceEntryMatcher for OrMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
for matcher in &self.matchers {
if let Some(matched_workspace_entry) = matcher.matches(workspace_entry.clone()) {
return Some(matched_workspace_entry);
}
}

None
}
}

/// Matches workspace entries that match ALL of the provided matchers
#[derive(Default, Debug)]
pub struct AndMatcher {
matchers: Vec<Box<dyn WorkspaceEntryMatcher>>,
}

impl AndMatcher {
pub fn new(matchers: Vec<Box<dyn WorkspaceEntryMatcher>>) -> Self {
Self { matchers }
}

pub fn push(&mut self, matcher: Box<dyn WorkspaceEntryMatcher>) {
self.matchers.push(matcher);
}
}

impl WorkspaceEntryMatcher for AndMatcher {
fn matches(&self, workspace_entry: WorkspaceEntry) -> Option<WorkspaceEntry> {
let mut matched_workspace_entry = workspace_entry.clone();

for matcher in &self.matchers {
if let Some(matched) = matcher.matches(matched_workspace_entry) {
matched_workspace_entry = matched;
} else {
return None;
}
}

Some(matched_workspace_entry)
}
}

fn globs_to_globset(globs: &[String]) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();

for glob in globs {
builder
.add(Glob::new(glob).context("Failed to create a new Glob from the provided pattern")?);
}

Ok(builder.build()?)
}

#[cfg(test)]
mod test {
use super::*;
use qlty_config::config::Builder;
use std::path::PathBuf;
use std::time::SystemTime;

// Tests for FileWorkspaceEntryMatcher
#[test]
fn test_file_workspace_entry_matcher_matches_file() {
let workspace_entry = WorkspaceEntry {
path: PathBuf::from("/path/to/file.txt"),
kind: WorkspaceEntryKind::File,
content_modified: SystemTime::now(),
contents_size: 100,
language_name: None,
};

let matcher = FileMatcher;
assert!(
matcher.matches(workspace_entry).is_some(),
"Expected workspace_entry to match as it is of type File"
);
}

#[test]
fn test_file_workspace_entry_matcher_does_not_match_directory() {
let workspace_entry = WorkspaceEntry {
path: PathBuf::from("/path/to/directory/"),
kind: WorkspaceEntryKind::Directory,
content_modified: SystemTime::now(),
contents_size: 0, // Assuming directories have a size of 0
language_name: None,
};

let matcher = FileMatcher;
assert!(
matcher.matches(workspace_entry).is_none(),
"Expected workspace_entry not to match as it is of type Directory"
);
}

// Tests for FileWorkspaceEntryMatcher
#[test]
fn test_glob_set_workspace_entry_matcher_matches_file() {
let workspace_entry = WorkspaceEntry {
path: PathBuf::from("/path/to/file.rs"),
kind: WorkspaceEntryKind::File,
content_modified: SystemTime::now(),
contents_size: 100,
language_name: None,
};
let config = Builder::default_config().unwrap().to_owned();
let all_file_types = config.file_types.to_owned();
let file_types_names = vec!["rust".to_owned()];
let file_types = all_file_types
.iter()
.filter_map(|(name, file_type)| {
if file_types_names.contains(&name) {
Some(file_type.clone())
} else {
None
}
})
.collect::<Vec<_>>();
let matcher = GlobsMatcher::new_for_file_types(&file_types).unwrap();

assert!(
matcher.matches(workspace_entry).is_some(),
"Expected workspace_entry to match as it is of type rust"
);
}

#[test]
fn test_glob_set_workspace_entry_matcher_does_not_match_file() {
let workspace_entry = WorkspaceEntry {
path: PathBuf::from("/path/to/file.rb"),
kind: WorkspaceEntryKind::File,
content_modified: SystemTime::now(),
contents_size: 100,
language_name: None,
};
let config = Builder::default_config().unwrap().to_owned();
let all_file_types = config.file_types.to_owned();
let file_types_names = vec!["rust".to_owned()];
let file_types = all_file_types
.iter()
.filter_map(|(name, file_type)| {
if file_types_names.contains(&name) {
Some(file_type.clone())
} else {
None
}
})
.collect::<Vec<_>>();
let matcher = GlobsMatcher::new_for_file_types(&file_types).unwrap();

assert!(
matcher.matches(workspace_entry).is_none(),
"Expected workspace_entry not to match as it is of type ruby"
);
}
}
Loading
Loading