// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! Path utilities for expanding file lists with exclusion and extension filtering. //! //! This module resolves a mixed list of files and directories into a flat list //! of files, applying glob-based exclusions and extension filters. It is used //! by the batch runner to turn mozlint's input paths into the concrete set of //! files each checker should process.
use std::path::Path;
use globset::{Glob, GlobSet, GlobSetBuilder}; use ignore::overrides::OverrideBuilder; use ignore::types::TypesBuilder; use ignore::WalkBuilder;
/// Expands a list of files and directories into individual file paths, /// filtering by `extensions` and removing entries matching `exclude` patterns. /// /// Paths are resolved relative to `root`. When `find_dotfiles` is true, /// hidden files and directories (starting with `.`) are included. pubfn expand_exclusions(
paths: &[String],
extensions: &[String],
exclude: &[String],
root: &str,
find_dotfiles: bool,
) -> Vec<String> { let root_path = Path::new(root); let excludes: Vec<String> = exclude.iter().map(|e| normalize(e, root_path)).collect(); let glob_excludes = build_glob_excludes(&excludes);
letmut result = Vec::new(); for path in paths { let p = Path::new(path); if p.is_file() { if is_excluded(path, &excludes, &glob_excludes) { continue;
}
result.push(path.clone());
} elseif p.is_dir() {
walk_directory(path, extensions, &excludes, find_dotfiles, &mut result);
}
}
result
}
/// Converts a relative path to absolute by joining it with `root`. fn normalize(path: &str, root: &Path) -> String { if Path::new(path).is_absolute() {
path.to_string()
} else {
root.join(path).to_string_lossy().to_string()
}
}
/// Builds a [`GlobSet`] from exclude patterns that contain wildcards. fn build_glob_excludes(excludes: &[String]) -> GlobSet { letmut builder = GlobSetBuilder::new(); for pattern in excludes.iter().filter(|e| e.contains('*')) { iflet Ok(glob) = Glob::new(pattern) {
builder.add(glob);
}
}
builder
.build()
.unwrap_or_else(|_| GlobSetBuilder::new().build().unwrap())
}
/// Returns `true` if `path` matches any literal prefix or glob in the exclude list. fn is_excluded(path: &str, excludes: &[String], glob_excludes: &GlobSet) -> bool { let p = Path::new(path);
excludes
.iter()
.any(|e| !e.contains('*') && p.starts_with(e))
|| glob_excludes.is_match(p)
}
/// Recursively walks `dir`, collecting files that match `extensions` while /// skipping paths in `excludes`. Uses the `ignore` crate for efficient /// directory traversal with override-based filtering. fn walk_directory(
dir: &str,
extensions: &[String],
excludes: &[String],
find_dotfiles: bool,
result: &mut Vec<String>,
) { if extensions.is_empty() { return;
}
// Use TypesBuilder for extension filtering. letmut types = TypesBuilder::new(); for ext in extensions { let _ = types.add("lint", &format!("*.{ext}"));
}
types.select("lint"); iflet Ok(t) = types.build() {
builder.types(t);
}
// Use overrides for exclusion patterns (both literal paths and globs). letmut ob = OverrideBuilder::new("/"); if !find_dotfiles { let _ = ob.add("!.*"); let _ = ob.add("!.*/**");
} for pattern in excludes { let _ = ob.add(&format!("!{pattern}")); if !pattern.contains('*') { let _ = ob.add(&format!("!{pattern}/**"));
}
} iflet Ok(overrides) = ob.build() {
builder.overrides(overrides);
}
for entry in builder.build().flatten() { let Some(ft) = entry.file_type() else { continue;
}; if ft.is_file() {
result.push(entry.path().to_string_lossy().to_string());
}
}
}
#[cfg(test)] mod tests { usesuper::*; use std::fs;
#[test] fn test_expand_exclusions_files() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let f1 = dir.path().join("foo.js"); let f2 = dir.path().join("bar.js"); let f3 = dir.path().join("excluded.js");
fs::write(&f1, "").unwrap();
fs::write(&f2, "").unwrap();
fs::write(&f3, "").unwrap();
let paths = vec![
f1.to_str().unwrap().to_string(),
f2.to_str().unwrap().to_string(),
f3.to_str().unwrap().to_string(),
]; let exclude = vec![f3.to_str().unwrap().to_string()];
let result = expand_exclusions(&paths, &["js".to_string()], &exclude, &root, false);
assert_eq!(result.len(), 2);
assert!(result.contains(&f1.to_str().unwrap().to_string()));
assert!(result.contains(&f2.to_str().unwrap().to_string()));
}
#[test] fn test_expand_exclusions_directory() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("a.js"), "").unwrap();
fs::write(sub.join("b.py"), "").unwrap();
fs::write(sub.join("c.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let result = expand_exclusions(&paths, &["js".to_string()], &[], &root, false);
#[test] fn test_expand_exclusions_glob_exclude() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap(); let test_dir = sub.join("test");
fs::create_dir(&test_dir).unwrap();
fs::write(sub.join("main.js"), "").unwrap();
fs::write(test_dir.join("test.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let exclude = vec![format!("{}/**/test/**", root)]; let result = expand_exclusions(&paths, &["js".to_string()], &exclude, &root, false);
#[test] fn test_expand_exclusions_no_extensions_skips_dirs() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("a.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let result = expand_exclusions(&paths, &[], &[], &root, false);
assert!(result.is_empty());
}
#[test] fn test_expand_exclusions_relative_exclude() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let f1 = dir.path().join("keep.js"); let sub = dir.path().join("skip");
fs::create_dir(&sub).unwrap(); let f2 = sub.join("bad.js");
fs::write(&f1, "").unwrap();
fs::write(&f2, "").unwrap();
let paths = vec![
f1.to_str().unwrap().to_string(),
f2.to_str().unwrap().to_string(),
]; let exclude = vec!["skip".to_string()];
let result = expand_exclusions(&paths, &["js".to_string()], &exclude, &root, false);
assert_eq!(result.len(), 1);
assert!(result[0].ends_with("keep.js"));
}
#[test] fn test_expand_exclusions_skips_dotfiles() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("visible.js"), "").unwrap();
fs::write(sub.join(".hidden.js"), "").unwrap(); let dotdir = sub.join(".dotdir");
fs::create_dir(&dotdir).unwrap();
fs::write(dotdir.join("inside.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let result = expand_exclusions(&paths, &["js".to_string()], &[], &root, false);
#[test] fn test_exclude_uses_path_components_not_string_prefix() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap(); let test_dir = sub.join("test");
fs::create_dir(&test_dir).unwrap(); let testing_dir = sub.join("testing");
fs::create_dir(&testing_dir).unwrap();
fs::write(test_dir.join("a.js"), "").unwrap();
fs::write(testing_dir.join("b.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let exclude = vec![test_dir.to_str().unwrap().to_string()]; let result = expand_exclusions(&paths, &["js".to_string()], &exclude, &root, false);
// "testing/b.js" must NOT be excluded by the "test" exclude
assert_eq!(result.len(), 1);
assert!(result[0].ends_with("testing/b.js"));
}
#[test] fn test_exclude_specific_file_during_directory_walk() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("keep.js"), "").unwrap();
fs::write(sub.join("skip.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let exclude = vec![sub.join("skip.js").to_str().unwrap().to_string()]; let result = expand_exclusions(&paths, &["js".to_string()], &exclude, &root, false);
#[test] fn test_exclude_prunes_directory_walk() { let dir = tempfile::tempdir().unwrap(); let root = dir.path().to_str().unwrap().to_string();
let sub = dir.path().join("src");
fs::create_dir(&sub).unwrap(); let excluded_dir = sub.join("vendor");
fs::create_dir(&excluded_dir).unwrap(); let deep = excluded_dir.join("deep");
fs::create_dir(&deep).unwrap();
fs::write(sub.join("main.js"), "").unwrap();
fs::write(excluded_dir.join("lib.js"), "").unwrap();
fs::write(deep.join("nested.js"), "").unwrap();
let paths = vec![sub.to_str().unwrap().to_string()]; let exclude = vec![excluded_dir.to_str().unwrap().to_string()]; let result = expand_exclusions(&paths, &["js".to_string()], &exclude, &root, false);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.