use std::process::ExitCode;
use fallow_config::{OutputFormat, discover_workspaces};
use crate::error::emit_error;
pub fn filter_to_workspace(
results: &mut fallow_core::results::AnalysisResults,
ws_root: &std::path::Path,
) {
results.unused_files.retain(|f| f.path.starts_with(ws_root));
results
.unused_exports
.retain(|e| e.path.starts_with(ws_root));
results.unused_types.retain(|e| e.path.starts_with(ws_root));
results
.unused_enum_members
.retain(|m| m.path.starts_with(ws_root));
results
.unused_class_members
.retain(|m| m.path.starts_with(ws_root));
results
.unresolved_imports
.retain(|i| i.path.starts_with(ws_root));
let ws_pkg = ws_root.join("package.json");
results.unused_dependencies.retain(|d| d.path == ws_pkg);
results.unused_dev_dependencies.retain(|d| d.path == ws_pkg);
results
.unused_optional_dependencies
.retain(|d| d.path == ws_pkg);
results.type_only_dependencies.retain(|d| d.path == ws_pkg);
results.test_only_dependencies.retain(|d| d.path == ws_pkg);
results
.unlisted_dependencies
.retain(|d| d.imported_from.iter().any(|s| s.path.starts_with(ws_root)));
for dup in &mut results.duplicate_exports {
dup.locations.retain(|loc| loc.path.starts_with(ws_root));
}
results.duplicate_exports.retain(|d| d.locations.len() >= 2);
results
.circular_dependencies
.retain(|c| c.files.iter().any(|f| f.starts_with(ws_root)));
results
.boundary_violations
.retain(|v| v.from_path.starts_with(ws_root));
results
.stale_suppressions
.retain(|s| s.path.starts_with(ws_root));
}
pub fn resolve_workspace_filter(
root: &std::path::Path,
workspace_name: &str,
output: OutputFormat,
) -> Result<std::path::PathBuf, ExitCode> {
let workspaces = discover_workspaces(root);
if workspaces.is_empty() {
let msg = format!(
"--workspace '{workspace_name}' specified but no workspaces found. \
Ensure root package.json has a \"workspaces\" field, pnpm-workspace.yaml exists, \
or tsconfig.json has \"references\"."
);
return Err(emit_error(&msg, 2, output));
}
workspaces
.iter()
.find(|ws| ws.name == workspace_name)
.map_or_else(
|| {
let names: Vec<&str> = workspaces.iter().map(|ws| ws.name.as_str()).collect();
let msg = format!(
"workspace '{workspace_name}' not found. Available workspaces: {}",
names.join(", ")
);
Err(emit_error(&msg, 2, output))
},
|ws| Ok(ws.root.clone()),
)
}
pub(super) fn filter_changed_files(
results: &mut fallow_core::results::AnalysisResults,
changed_files: &rustc_hash::FxHashSet<std::path::PathBuf>,
) {
results
.unused_files
.retain(|f| changed_files.contains(&f.path));
results
.unused_exports
.retain(|e| changed_files.contains(&e.path));
results
.unused_types
.retain(|e| changed_files.contains(&e.path));
results
.unused_enum_members
.retain(|m| changed_files.contains(&m.path));
results
.unused_class_members
.retain(|m| changed_files.contains(&m.path));
results
.unresolved_imports
.retain(|i| changed_files.contains(&i.path));
results.unlisted_dependencies.retain(|d| {
d.imported_from
.iter()
.any(|s| changed_files.contains(&s.path))
});
for dup in &mut results.duplicate_exports {
dup.locations
.retain(|loc| changed_files.contains(&loc.path));
}
results.duplicate_exports.retain(|d| d.locations.len() >= 2);
results
.circular_dependencies
.retain(|c| c.files.iter().any(|f| changed_files.contains(f)));
results
.boundary_violations
.retain(|v| changed_files.contains(&v.from_path));
results
.stale_suppressions
.retain(|s| changed_files.contains(&s.path));
}
pub fn get_changed_files(
root: &std::path::Path,
git_ref: &str,
) -> Option<rustc_hash::FxHashSet<std::path::PathBuf>> {
let output = match std::process::Command::new("git")
.args(["diff", "--name-only", &format!("{git_ref}...HEAD")])
.current_dir(root)
.output()
{
Ok(o) => o,
Err(e) => {
eprintln!("Warning: --changed-since ignored: failed to run git: {e}");
return None;
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("not a git repository") {
eprintln!("Warning: --changed-since ignored: not a git repository");
} else {
eprintln!(
"Warning: --changed-since failed for ref '{}': {}",
git_ref,
stderr.trim()
);
}
return None;
}
let files: rustc_hash::FxHashSet<std::path::PathBuf> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| root.join(line))
.collect();
Some(files)
}
#[cfg(test)]
mod tests {
use super::*;
use fallow_core::extract::MemberKind;
use fallow_core::results::*;
use std::path::PathBuf;
#[test]
fn filter_to_workspace_keeps_files_under_ws_root() {
let mut results = AnalysisResults::default();
results.unused_files.push(UnusedFile {
path: PathBuf::from("/project/packages/ui/src/button.ts"),
});
results.unused_files.push(UnusedFile {
path: PathBuf::from("/project/packages/api/src/handler.ts"),
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unused_files.len(), 1);
assert_eq!(
results.unused_files[0].path,
PathBuf::from("/project/packages/ui/src/button.ts")
);
}
#[test]
fn filter_to_workspace_scopes_dependencies_to_ws_package_json() {
let mut results = AnalysisResults::default();
results.unused_dependencies.push(UnusedDependency {
package_name: "lodash".into(),
location: DependencyLocation::Dependencies,
path: PathBuf::from("/project/package.json"),
line: 5,
});
results.unused_dependencies.push(UnusedDependency {
package_name: "react".into(),
location: DependencyLocation::Dependencies,
path: PathBuf::from("/project/packages/ui/package.json"),
line: 5,
});
results.unused_dev_dependencies.push(UnusedDependency {
package_name: "vitest".into(),
location: DependencyLocation::DevDependencies,
path: PathBuf::from("/project/packages/ui/package.json"),
line: 5,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unused_dependencies.len(), 1);
assert_eq!(results.unused_dependencies[0].package_name, "react");
assert_eq!(results.unused_dev_dependencies.len(), 1);
assert_eq!(results.unused_dev_dependencies[0].package_name, "vitest");
}
#[test]
fn filter_to_workspace_scopes_unlisted_deps_by_importer() {
let mut results = AnalysisResults::default();
results.unlisted_dependencies.push(UnlistedDependency {
package_name: "chalk".into(),
imported_from: vec![ImportSite {
path: PathBuf::from("/project/packages/ui/src/a.ts"),
line: 1,
col: 0,
}],
});
results.unlisted_dependencies.push(UnlistedDependency {
package_name: "debug".into(),
imported_from: vec![ImportSite {
path: PathBuf::from("/project/packages/api/src/b.ts"),
line: 1,
col: 0,
}],
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unlisted_dependencies.len(), 1);
assert_eq!(results.unlisted_dependencies[0].package_name, "chalk");
}
#[test]
fn filter_to_workspace_drops_duplicate_exports_below_two_locations() {
let mut results = AnalysisResults::default();
results.duplicate_exports.push(DuplicateExport {
export_name: "helper".into(),
locations: vec![
DuplicateLocation {
path: PathBuf::from("/project/packages/ui/src/a.ts"),
line: 15,
col: 0,
},
DuplicateLocation {
path: PathBuf::from("/project/packages/api/src/b.ts"),
line: 30,
col: 0,
},
],
});
results.duplicate_exports.push(DuplicateExport {
export_name: "utils".into(),
locations: vec![
DuplicateLocation {
path: PathBuf::from("/project/packages/ui/src/c.ts"),
line: 15,
col: 0,
},
DuplicateLocation {
path: PathBuf::from("/project/packages/ui/src/d.ts"),
line: 30,
col: 0,
},
],
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.duplicate_exports.len(), 1);
assert_eq!(results.duplicate_exports[0].export_name, "utils");
}
#[test]
fn filter_to_workspace_scopes_exports_and_types() {
let mut results = AnalysisResults::default();
results.unused_exports.push(UnusedExport {
path: PathBuf::from("/project/packages/ui/src/a.ts"),
export_name: "A".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_exports.push(UnusedExport {
path: PathBuf::from("/project/packages/api/src/b.ts"),
export_name: "B".into(),
is_type_only: false,
line: 2,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_types.push(UnusedExport {
path: PathBuf::from("/project/packages/ui/src/types.ts"),
export_name: "T".into(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unused_exports.len(), 1);
assert_eq!(results.unused_exports[0].export_name, "A");
assert_eq!(results.unused_types.len(), 1);
assert_eq!(results.unused_types[0].export_name, "T");
}
#[test]
fn filter_to_workspace_scopes_type_only_dependencies() {
let mut results = AnalysisResults::default();
results.type_only_dependencies.push(TypeOnlyDependency {
package_name: "zod".into(),
path: PathBuf::from("/project/packages/ui/package.json"),
line: 8,
});
results.type_only_dependencies.push(TypeOnlyDependency {
package_name: "yup".into(),
path: PathBuf::from("/project/package.json"),
line: 8,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.type_only_dependencies.len(), 1);
assert_eq!(results.type_only_dependencies[0].package_name, "zod");
}
#[test]
fn filter_to_workspace_scopes_enum_and_class_members() {
let mut results = AnalysisResults::default();
results.unused_enum_members.push(UnusedMember {
path: PathBuf::from("/project/packages/ui/src/enums.ts"),
parent_name: "Color".into(),
member_name: "Red".into(),
kind: MemberKind::EnumMember,
line: 2,
col: 0,
});
results.unused_enum_members.push(UnusedMember {
path: PathBuf::from("/project/packages/api/src/enums.ts"),
parent_name: "Status".into(),
member_name: "Active".into(),
kind: MemberKind::EnumMember,
line: 3,
col: 0,
});
results.unused_class_members.push(UnusedMember {
path: PathBuf::from("/project/packages/ui/src/service.ts"),
parent_name: "Svc".into(),
member_name: "init".into(),
kind: MemberKind::ClassMethod,
line: 5,
col: 0,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unused_enum_members.len(), 1);
assert_eq!(results.unused_enum_members[0].member_name, "Red");
assert_eq!(results.unused_class_members.len(), 1);
assert_eq!(results.unused_class_members[0].member_name, "init");
}
#[test]
fn filter_changed_files_keeps_only_changed() {
let mut results = AnalysisResults::default();
results.unused_files.push(UnusedFile {
path: PathBuf::from("/project/src/a.ts"),
});
results.unused_files.push(UnusedFile {
path: PathBuf::from("/project/src/b.ts"),
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_files.len(), 1);
assert_eq!(
results.unused_files[0].path,
PathBuf::from("/project/src/a.ts")
);
}
#[test]
fn filter_changed_files_preserves_unused_deps() {
let mut results = AnalysisResults::default();
results.unused_dependencies.push(UnusedDependency {
package_name: "lodash".into(),
location: DependencyLocation::Dependencies,
path: PathBuf::from("/project/package.json"),
line: 5,
});
results.unused_dev_dependencies.push(UnusedDependency {
package_name: "jest".into(),
location: DependencyLocation::DevDependencies,
path: PathBuf::from("/project/package.json"),
line: 10,
});
let changed = rustc_hash::FxHashSet::default();
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_dependencies.len(), 1);
assert_eq!(results.unused_dev_dependencies.len(), 1);
}
#[test]
fn filter_changed_files_filters_exports_by_path() {
let mut results = AnalysisResults::default();
results.unused_exports.push(UnusedExport {
path: PathBuf::from("/project/src/a.ts"),
export_name: "foo".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_exports.push(UnusedExport {
path: PathBuf::from("/project/src/b.ts"),
export_name: "bar".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/b.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_exports.len(), 1);
assert_eq!(results.unused_exports[0].export_name, "bar");
}
#[test]
fn filter_changed_files_drops_duplicate_exports_below_two() {
let mut results = AnalysisResults::default();
results.duplicate_exports.push(DuplicateExport {
export_name: "helper".into(),
locations: vec![
DuplicateLocation {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
},
DuplicateLocation {
path: PathBuf::from("/project/src/b.ts"),
line: 2,
col: 0,
},
],
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
filter_changed_files(&mut results, &changed);
assert!(results.duplicate_exports.is_empty());
}
#[test]
fn filter_changed_files_keeps_circular_deps_if_any_file_changed() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(CircularDependency {
files: vec![
PathBuf::from("/project/src/a.ts"),
PathBuf::from("/project/src/b.ts"),
],
length: 2,
line: 1,
col: 0,
is_cross_package: false,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/b.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.circular_dependencies.len(), 1);
}
#[test]
fn filter_changed_files_removes_circular_deps_if_no_file_changed() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(CircularDependency {
files: vec![
PathBuf::from("/project/src/a.ts"),
PathBuf::from("/project/src/b.ts"),
],
length: 2,
line: 1,
col: 0,
is_cross_package: false,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/c.ts"));
filter_changed_files(&mut results, &changed);
assert!(results.circular_dependencies.is_empty());
}
#[test]
fn filter_changed_files_keeps_unlisted_dep_if_importer_changed() {
let mut results = AnalysisResults::default();
results.unlisted_dependencies.push(UnlistedDependency {
package_name: "chalk".into(),
imported_from: vec![ImportSite {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
}],
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unlisted_dependencies.len(), 1);
}
#[test]
fn filter_changed_files_removes_unlisted_dep_if_no_importer_changed() {
let mut results = AnalysisResults::default();
results.unlisted_dependencies.push(UnlistedDependency {
package_name: "chalk".into(),
imported_from: vec![ImportSite {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
}],
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/b.ts"));
filter_changed_files(&mut results, &changed);
assert!(results.unlisted_dependencies.is_empty());
}
#[test]
fn filter_to_workspace_scopes_optional_dependencies() {
let mut results = AnalysisResults::default();
results.unused_optional_dependencies.push(UnusedDependency {
package_name: "fsevents".into(),
location: DependencyLocation::OptionalDependencies,
path: PathBuf::from("/project/packages/ui/package.json"),
line: 3,
});
results.unused_optional_dependencies.push(UnusedDependency {
package_name: "esbuild".into(),
location: DependencyLocation::OptionalDependencies,
path: PathBuf::from("/project/package.json"),
line: 7,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unused_optional_dependencies.len(), 1);
assert_eq!(
results.unused_optional_dependencies[0].package_name,
"fsevents"
);
}
#[test]
fn filter_to_workspace_scopes_test_only_dependencies() {
let mut results = AnalysisResults::default();
results.test_only_dependencies.push(TestOnlyDependency {
package_name: "msw".into(),
path: PathBuf::from("/project/packages/ui/package.json"),
line: 4,
});
results.test_only_dependencies.push(TestOnlyDependency {
package_name: "nock".into(),
path: PathBuf::from("/project/packages/api/package.json"),
line: 6,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.test_only_dependencies.len(), 1);
assert_eq!(results.test_only_dependencies[0].package_name, "msw");
}
#[test]
fn filter_to_workspace_scopes_circular_dependencies() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(CircularDependency {
files: vec![
PathBuf::from("/project/packages/ui/src/a.ts"),
PathBuf::from("/project/packages/ui/src/b.ts"),
],
length: 2,
line: 1,
col: 0,
is_cross_package: false,
});
results.circular_dependencies.push(CircularDependency {
files: vec![
PathBuf::from("/project/packages/api/src/x.ts"),
PathBuf::from("/project/packages/api/src/y.ts"),
],
length: 2,
line: 1,
col: 0,
is_cross_package: false,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.circular_dependencies.len(), 1);
assert_eq!(
results.circular_dependencies[0].files[0],
PathBuf::from("/project/packages/ui/src/a.ts")
);
}
#[test]
fn filter_to_workspace_keeps_circular_dep_if_any_file_in_workspace() {
let mut results = AnalysisResults::default();
results.circular_dependencies.push(CircularDependency {
files: vec![
PathBuf::from("/project/packages/ui/src/a.ts"),
PathBuf::from("/project/packages/api/src/b.ts"),
],
length: 2,
line: 1,
col: 0,
is_cross_package: false,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.circular_dependencies.len(), 1);
}
#[test]
fn filter_to_workspace_scopes_unresolved_imports() {
let mut results = AnalysisResults::default();
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("/project/packages/ui/src/a.ts"),
specifier: "./missing".into(),
line: 1,
col: 0,
specifier_col: 0,
});
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("/project/packages/api/src/b.ts"),
specifier: "./gone".into(),
line: 2,
col: 0,
specifier_col: 0,
});
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.unresolved_imports.len(), 1);
assert_eq!(results.unresolved_imports[0].specifier, "./missing");
}
#[test]
fn filter_to_workspace_on_empty_results_stays_empty() {
let mut results = AnalysisResults::default();
let ws_root = PathBuf::from("/project/packages/ui");
filter_to_workspace(&mut results, &ws_root);
assert_eq!(results.total_issues(), 0);
}
#[test]
fn filter_changed_files_filters_types_by_path() {
let mut results = AnalysisResults::default();
results.unused_types.push(UnusedExport {
path: PathBuf::from("/project/src/types.ts"),
export_name: "Foo".into(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_types.push(UnusedExport {
path: PathBuf::from("/project/src/other.ts"),
export_name: "Bar".into(),
is_type_only: true,
line: 2,
col: 0,
span_start: 0,
is_re_export: false,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/types.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_types.len(), 1);
assert_eq!(results.unused_types[0].export_name, "Foo");
}
#[test]
fn filter_changed_files_filters_enum_members_by_path() {
let mut results = AnalysisResults::default();
results.unused_enum_members.push(UnusedMember {
path: PathBuf::from("/project/src/enums.ts"),
parent_name: "Color".into(),
member_name: "Red".into(),
kind: MemberKind::EnumMember,
line: 2,
col: 0,
});
results.unused_enum_members.push(UnusedMember {
path: PathBuf::from("/project/src/other.ts"),
parent_name: "Status".into(),
member_name: "Active".into(),
kind: MemberKind::EnumMember,
line: 3,
col: 0,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/enums.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_enum_members.len(), 1);
assert_eq!(results.unused_enum_members[0].member_name, "Red");
}
#[test]
fn filter_changed_files_filters_class_members_by_path() {
let mut results = AnalysisResults::default();
results.unused_class_members.push(UnusedMember {
path: PathBuf::from("/project/src/service.ts"),
parent_name: "Svc".into(),
member_name: "init".into(),
kind: MemberKind::ClassMethod,
line: 5,
col: 0,
});
results.unused_class_members.push(UnusedMember {
path: PathBuf::from("/project/src/other.ts"),
parent_name: "Other".into(),
member_name: "run".into(),
kind: MemberKind::ClassMethod,
line: 10,
col: 0,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/service.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_class_members.len(), 1);
assert_eq!(results.unused_class_members[0].member_name, "init");
}
#[test]
fn filter_changed_files_preserves_optional_and_type_only_and_test_only_deps() {
let mut results = AnalysisResults::default();
results.unused_optional_dependencies.push(UnusedDependency {
package_name: "fsevents".into(),
location: DependencyLocation::OptionalDependencies,
path: PathBuf::from("/project/package.json"),
line: 3,
});
results.type_only_dependencies.push(TypeOnlyDependency {
package_name: "zod".into(),
path: PathBuf::from("/project/package.json"),
line: 8,
});
results.test_only_dependencies.push(TestOnlyDependency {
package_name: "msw".into(),
path: PathBuf::from("/project/package.json"),
line: 12,
});
let changed = rustc_hash::FxHashSet::default();
filter_changed_files(&mut results, &changed);
assert_eq!(results.unused_optional_dependencies.len(), 1);
assert_eq!(results.type_only_dependencies.len(), 1);
assert_eq!(results.test_only_dependencies.len(), 1);
}
#[test]
fn filter_changed_files_keeps_duplicate_exports_when_both_changed() {
let mut results = AnalysisResults::default();
results.duplicate_exports.push(DuplicateExport {
export_name: "helper".into(),
locations: vec![
DuplicateLocation {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
},
DuplicateLocation {
path: PathBuf::from("/project/src/b.ts"),
line: 2,
col: 0,
},
],
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
changed.insert(PathBuf::from("/project/src/b.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.duplicate_exports.len(), 1);
assert_eq!(results.duplicate_exports[0].locations.len(), 2);
}
#[test]
fn filter_changed_files_empty_set_clears_file_scoped_issues() {
let mut results = AnalysisResults::default();
results.unused_files.push(UnusedFile {
path: PathBuf::from("/project/src/a.ts"),
});
results.unused_exports.push(UnusedExport {
path: PathBuf::from("/project/src/b.ts"),
export_name: "foo".into(),
is_type_only: false,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_types.push(UnusedExport {
path: PathBuf::from("/project/src/c.ts"),
export_name: "T".into(),
is_type_only: true,
line: 1,
col: 0,
span_start: 0,
is_re_export: false,
});
results.unused_enum_members.push(UnusedMember {
path: PathBuf::from("/project/src/d.ts"),
parent_name: "E".into(),
member_name: "A".into(),
kind: MemberKind::EnumMember,
line: 1,
col: 0,
});
results.unused_class_members.push(UnusedMember {
path: PathBuf::from("/project/src/e.ts"),
parent_name: "C".into(),
member_name: "m".into(),
kind: MemberKind::ClassMethod,
line: 1,
col: 0,
});
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("/project/src/f.ts"),
specifier: "./x".into(),
line: 1,
col: 0,
specifier_col: 0,
});
let changed = rustc_hash::FxHashSet::default();
filter_changed_files(&mut results, &changed);
assert!(results.unused_files.is_empty());
assert!(results.unused_exports.is_empty());
assert!(results.unused_types.is_empty());
assert!(results.unused_enum_members.is_empty());
assert!(results.unused_class_members.is_empty());
assert!(results.unresolved_imports.is_empty());
}
#[test]
fn filter_changed_files_on_empty_results_stays_empty() {
let mut results = AnalysisResults::default();
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.total_issues(), 0);
}
#[test]
fn filter_changed_files_unlisted_dep_with_multiple_importers_keeps_if_any_changed() {
let mut results = AnalysisResults::default();
results.unlisted_dependencies.push(UnlistedDependency {
package_name: "chalk".into(),
imported_from: vec![
ImportSite {
path: PathBuf::from("/project/src/a.ts"),
line: 1,
col: 0,
},
ImportSite {
path: PathBuf::from("/project/src/b.ts"),
line: 5,
col: 0,
},
],
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/b.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unlisted_dependencies.len(), 1);
}
#[test]
fn filter_changed_files_filters_unresolved_imports_by_path() {
let mut results = AnalysisResults::default();
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("/project/src/a.ts"),
specifier: "./missing".into(),
line: 1,
col: 0,
specifier_col: 0,
});
results.unresolved_imports.push(UnresolvedImport {
path: PathBuf::from("/project/src/b.ts"),
specifier: "./gone".into(),
line: 2,
col: 0,
specifier_col: 0,
});
let mut changed = rustc_hash::FxHashSet::default();
changed.insert(PathBuf::from("/project/src/a.ts"));
filter_changed_files(&mut results, &changed);
assert_eq!(results.unresolved_imports.len(), 1);
assert_eq!(results.unresolved_imports[0].specifier, "./missing");
}
}