Skip to content

Commit

Permalink
Support for test-file exclusions (#96)
Browse files Browse the repository at this point in the history
Exclude test files with the new `testFilePatterns` config option.

Files matching these globs will be treated as used and tagged as test
files, as well as the transitive imports of files matching these globs.
  • Loading branch information
Adjective-Object authored Nov 11, 2024
1 parent a97631c commit 9b19dd7
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Add support for usage from test files",
"packageName": "@good-fences/api",
"email": "mhuan13@gmail.com",
"dependentChangeType": "patch"
}
37 changes: 33 additions & 4 deletions crates/unused_finder/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
use package_match_rules::PackageMatchRules;
use serde::Deserialize;

mod package_match_rules;
pub mod package_match_rules;

#[derive(Debug, Eq, PartialEq)]
pub struct ErrList<E>(Vec<E>);
Expand Down Expand Up @@ -89,11 +89,25 @@ pub struct UnusedFinderJSONConfig {
/// All transitive imports from the exposed exports of these packages
/// will be considered used
///
/// Note that the only files that are considered roots are the ones
/// that are _explicitly exported_, either as an entry in the package's "exports" config,
/// or as a main/module export
///
/// Items are parsed in one of three ways:
/// 1. If the item starts with "./", it is treated as a path glob, and evaluated against the paths of package folders, relative to the repo root.
/// 2. If the item contains any of "~)('!*", it is treated as a name-glob, and evaluated as a glob against the names of packages.
/// 3. Otherwise, the item is treated as the name of an individual package, and matched literally.
/// 1. If the item starts with "./", it is treated as a path glob, and evaluated
/// against the paths of package folders, relative to the repo root.
/// 2. If the item contains any of "~)('!*", it is treated as a name-glob, and evaluated
/// as a glob against the names of packages.
/// 3. Otherwise, the item is treated as the name of an individual package, and matched
/// literally.
pub entry_packages: Vec<String>,
/// List of globs that will be matched against files in the repository
///
/// 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
#[serde(default)]
pub test_file_patterns: Vec<String>,
}

/// Configuration for the unused symbols finder
Expand All @@ -115,6 +129,13 @@ pub struct UnusedFinderConfig {
/// packages we should consider as "entry" packages
pub entry_packages: PackageMatchRules,

/// List of globs that will be matched against files in the repository
///
/// 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_file_patterns: Vec<glob::Pattern>,

/// Globs of individual files & directories to skip during the file walk.
///
/// Some internal directories are always skipped.
Expand All @@ -133,6 +154,14 @@ 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_file_patterns: value
.test_file_patterns
.iter()
.map(|p| glob::Pattern::new(p))
.collect::<Result<_, _>>()
.map_err(|e| {
ConfigError::InvalidGlobPatterns(ErrList(vec![PatErr(0, GlobInterp::Path, e)]))
})?,
skip: value.skip,
})
}
Expand Down
6 changes: 6 additions & 0 deletions crates/unused_finder/src/cfg/package_match_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ impl PackageMatchRules {
}
}

impl PackageMatchRules {
pub fn empty() -> Self {
Self::default()
}
}

impl<T: AsRef<str> + ToString> TryFrom<Vec<T>> for PackageMatchRules {
type Error = ConfigError;
fn try_from(value: Vec<T>) -> Result<Self, Self::Error> {
Expand Down
47 changes: 44 additions & 3 deletions crates/unused_finder/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::{collections::HashMap, path::PathBuf};
use std::{collections::HashMap, path::PathBuf, str::FromStr};

use path_slash::PathBufExt;
use test_tmpdir::{bmap, test_tmpdir};

use crate::{
graph::UsedTag, logger, report::SymbolReport, UnusedFinder, UnusedFinderConfig,
UnusedFinderReport,
cfg::package_match_rules::PackageMatchRules, graph::UsedTag, logger, report::SymbolReport,
UnusedFinder, UnusedFinderConfig, UnusedFinderReport,
};

fn symbol(id: &str, tags: UsedTag) -> SymbolReport {
Expand Down Expand Up @@ -409,3 +409,44 @@ fn test_typeonly_interface_allowed() {
},
);
}

#[test]
fn test_testfiles_ignored() {
// Tests that test files are ignored
let tmpdir = test_tmpdir!(
"search_root/packages/__tests__/myTests.test.js" => r#"
import { myFunction } from "../test-helpers/test-helpers";
"#,
"search_root/packages/test-helpers/package.json" => r#"{
"name": "test-helpers",
"exports": {
"." : {
"source": "./test-helpers.js"
}
}
}"#,
"search_root/packages/test-helpers/test-helpers.js" => r#"
export function myFunction() {}
"#,
"search_root/packages/test-helpers/unused-helpers.js" => r#"
export function myFunction() {}
"#
);

run_unused_test(
&tmpdir,
UnusedFinderConfig {
repo_root: tmpdir.root().to_string_lossy().to_string(),
root_paths: vec!["search_root".to_string()],
entry_packages: PackageMatchRules::empty(),
test_file_patterns: vec![glob::Pattern::from_str("**/__tests__/**").unwrap()],
..Default::default()
},
UnusedFinderReport {
unused_files: vec!["<root>/search_root/packages/test-helpers/unused-helpers.js".into()],
unused_symbols: bmap![
"<root>/search_root/packages/test-helpers/unused-helpers.js" => vec![symbol("myFunction", UsedTag::default())]
],
},
);
}
30 changes: 30 additions & 0 deletions crates/unused_finder/src/unused_finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,16 @@ impl UnusedFinder {
)
.map_err(JsErr::generic_failure)?;

let test_entrypoints = self.get_test_files();
logger.log(format!(
"Starting {} graph traversal with {} entrypoints",
UsedTag::FROM_TEST,
test_entrypoints.len(),
));
graph
.traverse_bfs(&logger, test_entrypoints, vec![], UsedTag::FROM_TEST)
.map_err(JsErr::generic_failure)?;

// mark all typeonly symbols
if self.config.allow_unused_types {
for (path, source_file) in self.last_walk_result.source_files.iter() {
Expand Down Expand Up @@ -445,6 +455,26 @@ impl UnusedFinder {
.unwrap_or(false)
}

fn get_test_files(&self) -> Vec<PathBuf> {
// get the list of files that match the test file patterns
self.last_walk_result
.source_files
.par_iter()
.filter_map(|(file_path, _)| {
if self
.config
.test_file_patterns
.iter()
.any(|pattern| pattern.matches_path(file_path))
{
Some(file_path.clone())
} else {
None
}
})
.collect()
}

fn get_ignored_files(&self) -> Vec<PathBuf> {
// TODO: this is n^2, which is bad! Could build a treemap of ignore files?
self.last_walk_result
Expand Down
8 changes: 8 additions & 0 deletions crates/unused_finder_napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ pub struct UnusedFinderJSONConfig {
/// 2. If the item contains any of "~)('!*", it is treated as a name-glob, and evaluated as a glob against the names of packages.
/// 3. Otherwise, the item is treated as the name of an individual package, and matched literally.
pub entry_packages: Vec<String>,
/// List of globs that will be matched against files in the repository
///
/// 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
#[serde(default)]
pub test_file_patterns: Vec<String>,
}

impl From<UnusedFinderJSONConfig> for unused_finder::UnusedFinderJSONConfig {
Expand All @@ -60,6 +67,7 @@ impl From<UnusedFinderJSONConfig> for unused_finder::UnusedFinderJSONConfig {
report_exported_symbols: val.report_exported_symbols,
entry_packages: val.entry_packages,
allow_unused_types: val.allow_unused_types,
test_file_patterns: val.test_file_patterns,
}
}
}
Expand Down

0 comments on commit 9b19dd7

Please sign in to comment.