use std::borrow::Cow; use std::collections::BTreeSet; use std::fs::File; use std::io::{BufReader, Read}; use std::ops::Deref; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Result}; use log::debug; use path_absolutize::{path_dedot, Absolutize}; use walkdir::{DirEntry, WalkDir}; use crate::checks::CheckCode; use crate::settings::types::{FilePattern, PerFileIgnore}; /// Extract the absolute path and basename (as strings) from a Path. fn extract_path_names(path: &Path) -> Result<(&str, &str)> { let file_path = path .to_str() .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?; let file_basename = path .file_name() .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))? .to_str() .ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?; Ok((file_path, file_basename)) } fn is_excluded<'a, T>(file_path: &str, file_basename: &str, exclude: T) -> bool where T: Iterator, { for pattern in exclude { match pattern { FilePattern::Simple(basename) => { if *basename == file_basename { return true; } } FilePattern::Complex(absolute, basename) => { if absolute.matches(file_path) { return true; } if basename .as_ref() .map(|pattern| pattern.matches(file_basename)) .unwrap_or_default() { return true; } } }; } false } fn is_included(path: &Path) -> bool { let file_name = path.to_string_lossy(); file_name.ends_with(".py") || file_name.ends_with(".pyi") } pub fn iter_python_files<'a>( path: &'a Path, exclude: &'a [FilePattern], extend_exclude: &'a [FilePattern], ) -> impl Iterator> + 'a { // Run some checks over the provided patterns, to enable optimizations below. let has_exclude = !exclude.is_empty(); let has_extend_exclude = !extend_exclude.is_empty(); let exclude_simple = exclude .iter() .all(|pattern| matches!(pattern, FilePattern::Simple(_))); let extend_exclude_simple = extend_exclude .iter() .all(|pattern| matches!(pattern, FilePattern::Simple(_))); WalkDir::new(normalize_path(path)) .into_iter() .filter_entry(move |entry| { if !has_exclude && !has_extend_exclude { return true; } let path = entry.path(); match extract_path_names(path) { Ok((file_path, file_basename)) => { let file_type = entry.file_type(); if has_exclude && (!exclude_simple || file_type.is_dir()) && is_excluded(file_path, file_basename, exclude.iter()) { debug!("Ignored path via `exclude`: {:?}", path); false } else if has_extend_exclude && (!extend_exclude_simple || file_type.is_dir()) && is_excluded(file_path, file_basename, extend_exclude.iter()) { debug!("Ignored path via `extend-exclude`: {:?}", path); false } else { true } } Err(_) => { debug!("Ignored path due to error in parsing: {:?}", path); true } } }) .filter(|entry| { entry.as_ref().map_or(true, |entry| { (entry.depth() == 0 || is_included(entry.path())) && !entry.file_type().is_dir() && !(entry.file_type().is_symlink() && entry.path().is_dir()) }) }) } /// Create tree set with codes matching the pattern/code pairs. pub fn ignores_from_path<'a>( path: &Path, pattern_code_pairs: &'a [PerFileIgnore], ) -> Result> { let (file_path, file_basename) = extract_path_names(path)?; Ok(pattern_code_pairs .iter() .filter(|pattern_code_pair| { is_excluded( file_path, file_basename, [&pattern_code_pair.pattern].into_iter(), ) }) .flat_map(|pattern_code_pair| &pattern_code_pair.codes) .collect()) } /// Convert any path to an absolute path (based on the current working /// directory). pub fn normalize_path(path: &Path) -> PathBuf { if let Ok(path) = path.absolutize() { return path.to_path_buf(); } path.to_path_buf() } /// Convert any path to an absolute path (based on the specified project root). pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf { if let Ok(path) = path.absolutize_from(project_root) { return path.to_path_buf(); } path.to_path_buf() } /// Convert an absolute path to be relative to the current working directory. pub fn relativize_path(path: &Path) -> Cow { if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) { return path.to_string_lossy(); } path.to_string_lossy() } /// Read a file's contents from disk. pub fn read_file(path: &Path) -> Result { let file = File::open(path)?; let mut buf_reader = BufReader::new(file); let mut contents = String::new(); buf_reader.read_to_string(&mut contents)?; Ok(contents) } #[cfg(test)] mod tests { use std::path::Path; use anyhow::Result; use path_absolutize::Absolutize; use crate::fs::{extract_path_names, is_excluded, is_included}; use crate::settings::types::FilePattern; #[test] fn inclusions() { let path = Path::new("foo/bar/baz.py").absolutize().unwrap(); assert!(is_included(&path)); let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap(); assert!(is_included(&path)); let path = Path::new("foo/bar/baz.js").absolutize().unwrap(); assert!(!is_included(&path)); let path = Path::new("foo/bar/baz").absolutize().unwrap(); assert!(!is_included(&path)); } #[test] fn exclusions() -> Result<()> { let project_root = Path::new("/tmp/"); let path = Path::new("foo").absolutize_from(project_root).unwrap(); let exclude = vec![FilePattern::from_user( "foo", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, exclude.iter())); let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); let exclude = vec![FilePattern::from_user( "bar", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, exclude.iter())); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); let exclude = vec![FilePattern::from_user( "baz.py", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, exclude.iter())); let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); let exclude = vec![FilePattern::from_user( "foo/bar", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, exclude.iter())); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); let exclude = vec![FilePattern::from_user( "foo/bar/baz.py", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, exclude.iter())); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); let exclude = vec![FilePattern::from_user( "foo/bar/*.py", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, exclude.iter())); let path = Path::new("foo/bar/baz.py") .absolutize_from(project_root) .unwrap(); let exclude = vec![FilePattern::from_user( "baz", &Some(project_root.to_path_buf()), )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(!is_excluded(file_path, file_basename, exclude.iter())); Ok(()) } }