Skip to content
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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "change to devongovett/glob-match for js-like globs",
"packageName": "@good-fences/api",
"email": "[email protected]",
"dependentChangeType": "patch"
}
2 changes: 1 addition & 1 deletion crates/unused_finder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ crate-type = ["lib"]

[dependencies]
anyhow.workspace = true
glob = "0.3.1"
import_resolver = { path = "../import_resolver" }
js_err = { path = "../js_err" }
path-slash.workspace = true
Expand Down Expand Up @@ -41,6 +40,7 @@ schemars.workspace = true
logger = { version = "0.2.0", path = "../logger" }
logger_srcfile = { version = "0.2.0", path = "../logger_srcfile" }
multi_err = { version = "0.2.0", path = "../multi_err" }
globset = "0.4.16"

[dev-dependencies]
stringreader = "0.1.1"
Expand Down
165 changes: 101 additions & 64 deletions crates/unused_finder/src/cfg/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::fmt::{Display, Formatter};
use core::fmt;
use std::{
fmt::{Debug, Display, Formatter},
path::Path,
};

use itertools::Itertools;
use package_match_rules::PackageMatchRules;
Expand Down Expand Up @@ -36,7 +40,7 @@ impl Display for GlobInterp {
}

#[derive(Debug)]
pub struct PatErr(usize, GlobInterp, glob::PatternError);
pub struct PatErr(usize, GlobInterp, globset::Error);

impl Display for PatErr {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Expand All @@ -48,15 +52,17 @@ impl PartialEq for PatErr {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
&& self.1 == other.1
&& self.2.pos == other.2.pos
&& self.2.msg == other.2.msg
&& self.2.glob() == other.2.glob()
&& self.2.kind() == other.2.kind()
}
}

#[derive(Debug, PartialEq, thiserror::Error)]
pub enum ConfigError {
#[error("Error parsing package match rules: {0}")]
InvalidGlobPatterns(ErrList<PatErr>),
InvalidPackageMatchGlob(ErrList<PatErr>),
#[error("Error parsing testFile glob(s): {0}")]
InvalidTestsGlob(ErrList<PatErr>),
}

/// A JSON serializable proxy for the UnusedFinderConfig struct
Expand Down Expand Up @@ -114,6 +120,19 @@ pub struct UnusedFinderJSONConfig {
pub test_files: Vec<String>,
}

#[derive(Default, Clone)]
pub struct GlobGroup {
pub globset: globset::GlobSet,
// keep these around for debugging
pub globs: Vec<globset::Glob>,
}

impl Debug for GlobGroup {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "[{}]", self.globs.iter().map(|m| m.glob()).join(", "))
}
}

/// Configuration for the unused symbols finder
#[derive(Debug, Default, Clone)]
pub struct UnusedFinderConfig {
Expand All @@ -138,7 +157,7 @@ pub struct UnusedFinderConfig {
/// Matches are made against the relative file paths from the repo root.
/// A matching file will be tagged as a "test" file, and will be excluded
/// from the list of unused files
pub test_files: Vec<glob::Pattern>,
pub test_files: GlobGroup,

/// Globs of individual files & directories to skip during the file walk.
///
Expand All @@ -147,20 +166,43 @@ pub struct UnusedFinderConfig {
pub skip: Vec<String>,
}

impl UnusedFinderConfig {
pub fn is_test_path(&self, path: &Path) -> bool {
let relative = path.strip_prefix(&self.repo_root).unwrap_or(path);
let relative = relative.strip_prefix("/").unwrap_or(relative);
self.test_files.globset.is_match(relative)
}

pub fn is_test_path_str(&self, path: &str) -> bool {
let relative = path.strip_prefix(&self.repo_root).unwrap_or(path);
let relative = relative.strip_prefix('/').unwrap_or(relative);
self.test_files.globset.is_match(relative)
}
}

impl TryFrom<UnusedFinderJSONConfig> for UnusedFinderConfig {
type Error = ConfigError;
fn try_from(value: UnusedFinderJSONConfig) -> std::result::Result<Self, Self::Error> {
let (test_globs, test_glob_errs): (Vec<glob::Pattern>, Vec<_>) = value
.test_files
.iter()
.partition_map(|pat| match glob::Pattern::new(pat) {
Ok(pat) => Either::Left(pat),
Err(err) => Either::Right(PatErr(0, GlobInterp::Path, err)),
let (test_globs, test_glob_errs): (Vec<globset::Glob>, Vec<_>) =
value.test_files.iter().partition_map(|pat| {
println!("compile glob {}", pat);
match globset::Glob::new(pat) {
Ok(pat) => Either::Left(pat),
Err(err) => Either::Right(PatErr(0, GlobInterp::Path, err)),
}
});
if !test_glob_errs.is_empty() {
return Err(ConfigError::InvalidGlobPatterns(ErrList(test_glob_errs)));
return Err(ConfigError::InvalidTestsGlob(ErrList(test_glob_errs)));
}

let mut set_builder = globset::GlobSetBuilder::new();
for glob in test_globs.iter() {
set_builder.add(glob.clone());
}
let globset = set_builder.build().map_err(|err| {
ConfigError::InvalidTestsGlob(ErrList(vec![PatErr(0, GlobInterp::Path, err)]))
})?;

Ok(UnusedFinderConfig {
// raw fields that are copied from the JSON config
report_exported_symbols: value.report_exported_symbols,
Expand All @@ -169,7 +211,10 @@ impl TryFrom<UnusedFinderJSONConfig> for UnusedFinderConfig {
repo_root: value.repo_root,
// other fields that are processed before use
entry_packages: value.entry_packages.try_into()?,
test_files: test_globs,
test_files: GlobGroup {
globset,
globs: test_globs,
},
skip: value.skip,
})
}
Expand All @@ -180,86 +225,78 @@ mod test {
use super::*;

#[test]
fn test_invalid_glob_path_err() {
fn test_invalid_glob_unclosed_alternate() {
let json_config = r#"{
"repoRoot": "/path/to/repo",
"rootPaths": ["src"],
"entryPackages": ["./***"],
"entryPackages": [],
"testFiles": ["@foo/{cb,"],
"skip": []
}"#;

let config: UnusedFinderJSONConfig = serde_json::from_str(json_config).unwrap();
let err: ConfigError = UnusedFinderConfig::try_from(config).unwrap_err();
let expected_err = ConfigError::InvalidGlobPatterns(ErrList(vec![PatErr(
0,
GlobInterp::Path,
glob::PatternError {
pos: 2,
msg: "wildcards are either regular `*` or recursive `**`",
},
)]));

assert_eq!(err, expected_err);
assert_eq!(format!("{}", err), "Error parsing testFile glob(s): In path pattern at idx 0: error parsing glob '@foo/{cb,': unclosed alternate group; missing '}' (maybe escape '{' with '[{]'?)\n");
}

#[test]
fn test_invalid_glob_name_err() {
fn test_invalid_glob_nested_alternate() {
let json_config = r#"{
"repoRoot": "/path/to/repo",
"rootPaths": ["src"],
"entryPackages": ["@foo/***"],
"entryPackages": ["@foo/{a,{cb, d}}"],
"skip": []
}"#;

let config: UnusedFinderJSONConfig = serde_json::from_str(json_config).unwrap();
let err: ConfigError = UnusedFinderConfig::try_from(config).unwrap_err();
let expected_err = ConfigError::InvalidGlobPatterns(ErrList(vec![PatErr(
0,
GlobInterp::Name,
glob::PatternError {
pos: 7,
msg: "wildcards are either regular `*` or recursive `**`",
},
)]));

assert_eq!(err, expected_err);
assert_eq!(format!("{}", err), "Error parsing package match rules: In name pattern at idx 0: error parsing glob '@foo/{a,{cb, d}}': nested alternate groups are not allowed\n");
}

#[test]
fn test_invalid_glob_multi_err() {
fn test_invalid_glob_unclosed_charclass() {
let json_config = r#"{
"repoRoot": "/path/to/repo",
"rootPaths": ["src"],
"entryPackages": [
"my-pkg1",
"@foo/***",
"my-pkg2-*",
"./foo/***"
"entryPackages": ["@foo/[a"],
"testFiles": [
"@foo/[a"
],
"skip": []
}"#;

let config: UnusedFinderJSONConfig = serde_json::from_str(json_config).unwrap();
let err: ConfigError = UnusedFinderConfig::try_from(config).unwrap_err();
let expected_err = ConfigError::InvalidGlobPatterns(ErrList(vec![
PatErr(
1,
GlobInterp::Name,
glob::PatternError {
pos: 7,
msg: "wildcards are either regular `*` or recursive `**`",
},
),
PatErr(
3,
GlobInterp::Path,
glob::PatternError {
pos: 6,
msg: "wildcards are either regular `*` or recursive `**`",
},
),
]));

assert_eq!(expected_err, err);
assert_eq!(format!("{}", err), "Error parsing testFile glob(s): In path pattern at idx 0: error parsing glob '@foo/[a': unclosed character class; missing ']'\n");
}

#[test]
fn test_glob_match_test() {
let config = super::UnusedFinderJSONConfig {
repo_root: "/workspaces/demo".to_string(),
root_paths: vec!["src".to_string()],
skip: vec!["target".to_string()],
report_exported_symbols: true,
allow_unused_types: false,
entry_packages: vec!["@myorg/*".to_string()],
test_files: vec![
// just test this pattern
"**/*{Test,Tests}.{ts,tsx}".to_string(),
],
};

let cases = vec![
("/workspaces/demo/packages/cool/cool-search-bar/src/test/CoolComponent.SomethingElse.Tests.tsx", true),
("/workspaces/demo/packages/cool-common/forms/cool-forms-view-view/src/test/utils/infoBarUtilsTests/someTests.ts", true),
];

let mut result: Vec<(&str, bool)> = vec![];
for (path, _) in cases.iter() {
let config = super::UnusedFinderConfig::try_from(config.clone()).unwrap();
let actual = config.is_test_path_str(path);
result.push((*path, actual));
}

pretty_assertions::assert_eq!(result, cases);
}
}
38 changes: 25 additions & 13 deletions crates/unused_finder/src/cfg/package_match_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ use std::path::Path;

use ahashmap::AHashSet;

use super::{ConfigError, GlobInterp, PatErr};
use super::{ConfigError, GlobGroup, GlobInterp, PatErr};

#[derive(Debug, Default, Clone)]
pub struct PackageMatchRules {
pub names: AHashSet<String>,
pub name_patterns: Vec<glob::Pattern>,
pub path_patterns: Vec<glob::Pattern>,
pub name_patterns: Vec<globset::GlobMatcher>,
pub path_patterns: Vec<globset::GlobMatcher>,
}

pub fn compile_globs(glob_strs: &[&str]) -> Result<GlobGroup, globset::Error> {
let mut set = globset::GlobSetBuilder::new();
let mut globs = Vec::with_capacity(glob_strs.len());
for glob_str in glob_strs.iter() {
let as_glob = globset::Glob::new(glob_str)?;
globs.push(as_glob.clone());
set.add(as_glob);
}
Ok(GlobGroup {
globset: set.build()?,
globs,
})
}

impl PackageMatchRules {
Expand All @@ -21,7 +35,7 @@ impl PackageMatchRules {
return true;
}
for pattern in &self.name_patterns {
if pattern.matches(package_name) {
if pattern.is_match(package_name) {
return true;
}
}
Expand All @@ -35,16 +49,14 @@ impl PackageMatchRules {
path_string = package_path.to_string_lossy().into_owned();
&path_string
});
if pattern.matches(path_string_ref) {
if pattern.is_match(path_string_ref) {
return true;
}
}

false
}
}

impl PackageMatchRules {
pub fn empty() -> Self {
Self::default()
}
Expand All @@ -59,22 +71,22 @@ impl<T: AsRef<str> + ToString> TryFrom<Vec<T>> for PackageMatchRules {
let mut errs: Vec<PatErr> = Vec::new();
for (i, item) in value.into_iter().enumerate() {
if let Some(trimmed) = item.as_ref().strip_prefix("./") {
match glob::Pattern::new(trimmed) {
match globset::Glob::new(trimmed) {
Err(e) => errs.push(PatErr(i, GlobInterp::Path, e)),
Ok(r) => path_patterns.push(r),
Ok(r) => path_patterns.push(r.compile_matcher()),
};
} else if item.as_ref().chars().any(|c| "~)('!*".contains(c)) {
match glob::Pattern::new(item.as_ref()) {
} else if item.as_ref().chars().any(|c| "~)('!*,{".contains(c)) {
match globset::Glob::new(item.as_ref()) {
Err(e) => errs.push(PatErr(i, GlobInterp::Name, e)),
Ok(r) => name_patterns.push(r),
Ok(r) => name_patterns.push(r.compile_matcher()),
};
} else {
names.insert(item.to_string());
}
}

if !errs.is_empty() {
return Err(ConfigError::InvalidGlobPatterns(super::ErrList(errs)));
return Err(ConfigError::InvalidPackageMatchGlob(super::ErrList(errs)));
}

Ok(Self {
Expand Down
Loading