use std::path::Path;
use fallow_types::output_dead_code::{BoundaryViolationFinding, CircularDependencyFinding};
use rustc_hash::FxHashSet;
use crate::audit_keys::relative_key_path;
#[must_use]
pub fn boundary_edge_key(finding: &BoundaryViolationFinding) -> String {
format!(
"{}->-{}",
finding.violation.from_zone, finding.violation.to_zone
)
}
#[must_use]
pub fn cycle_key(finding: &CircularDependencyFinding, root: &Path) -> String {
let mut files: Vec<String> = finding
.cycle
.files
.iter()
.map(|p| relative_key_path(p, root))
.collect();
files.sort_unstable();
files.dedup();
files.join("|")
}
#[must_use]
pub fn boundary_edge_keys(findings: &[BoundaryViolationFinding]) -> FxHashSet<String> {
findings.iter().map(boundary_edge_key).collect()
}
#[must_use]
pub fn cycle_keys(findings: &[CircularDependencyFinding], root: &Path) -> FxHashSet<String> {
findings.iter().map(|f| cycle_key(f, root)).collect()
}
#[must_use]
pub fn public_export_keys_for(
graph: &fallow_engine::module_graph::RetainedModuleGraph,
config: &fallow_config::ResolvedConfig,
root_pkg: Option<&fallow_config::PackageJson>,
workspaces: &[fallow_config::WorkspaceInfo],
root: &Path,
) -> FxHashSet<String> {
let public_entries = fallow_engine::project_analysis::public_api_package_entry_points(
graph, config, root_pkg, workspaces,
);
graph.public_export_keys(&public_entries, root)
}
#[must_use]
#[allow(
clippy::implicit_hasher,
reason = "callers always pass FxHashSet; generalizing the hasher adds noise for no in-tree benefit"
)]
pub fn introduced_keys(head: &FxHashSet<String>, base: &FxHashSet<String>) -> Vec<String> {
let mut introduced: Vec<String> = head.difference(base).cloned().collect();
introduced.sort_unstable();
introduced
}
#[cfg(test)]
mod tests {
use super::*;
use fallow_types::output_dead_code::{BoundaryViolationFinding, CircularDependencyFinding};
use fallow_types::results::{BoundaryViolation, CircularDependency};
use std::path::PathBuf;
fn boundary(from_zone: &str, to_zone: &str, specifier: &str) -> BoundaryViolationFinding {
BoundaryViolationFinding::with_actions(BoundaryViolation {
from_path: PathBuf::from("/p/src/a.ts"),
to_path: PathBuf::from("/p/src/b.ts"),
from_zone: from_zone.to_string(),
to_zone: to_zone.to_string(),
import_specifier: specifier.to_string(),
line: 1,
col: 0,
})
}
fn cycle(files: &[&str]) -> CircularDependencyFinding {
CircularDependencyFinding::with_actions(CircularDependency {
files: files.iter().map(PathBuf::from).collect(),
length: files.len(),
line: 1,
col: 0,
edges: vec![],
is_cross_package: false,
})
}
#[test]
fn boundary_edges_dedup_per_zone_pair_r2() {
let findings = vec![
boundary("ui", "db", "./b"),
boundary("ui", "db", "./c"),
boundary("app", "db", "./d"),
];
let keys = boundary_edge_keys(&findings);
assert_eq!(keys.len(), 2, "one key per zone pair: {keys:?}");
assert!(keys.contains("ui->-db"));
assert!(keys.contains("app->-db"));
}
#[test]
fn boundary_delta_fires_only_on_new_zone_pair() {
let base = boundary_edge_keys(&[boundary("ui", "db", "./b")]);
let head = boundary_edge_keys(&[
boundary("ui", "db", "./b"),
boundary("ui", "db", "./c"),
boundary("app", "db", "./d"),
]);
let introduced = introduced_keys(&head, &base);
assert_eq!(introduced, vec!["app->-db".to_string()]);
}
#[test]
fn cycle_key_is_rotation_independent() {
let root = Path::new("/p");
let a = cycle(&["/p/a.ts", "/p/b.ts", "/p/c.ts"]);
let b = cycle(&["/p/c.ts", "/p/a.ts", "/p/b.ts"]);
assert_eq!(cycle_key(&a, root), cycle_key(&b, root));
}
#[test]
fn cycle_delta_new_vs_pre_existing() {
let root = Path::new("/p");
let base = cycle_keys(&[cycle(&["/p/a.ts", "/p/b.ts"])], root);
let head = cycle_keys(
&[
cycle(&["/p/a.ts", "/p/b.ts"]),
cycle(&["/p/x.ts", "/p/y.ts"]),
],
root,
);
let introduced = introduced_keys(&head, &base);
assert_eq!(introduced, vec!["x.ts|y.ts".to_string()]);
}
#[test]
fn relocation_fires_no_delta() {
let root = Path::new("/p");
let base = cycle_keys(&[cycle(&["/p/a.ts", "/p/b.ts"])], root);
let head = cycle_keys(&[cycle(&["/p/b.ts", "/p/a.ts"])], root);
assert!(introduced_keys(&head, &base).is_empty());
}
#[test]
fn public_api_delta_zero_for_internal_only_one_for_exports() {
let base: FxHashSet<String> = std::iter::once("src/impl.ts::pub".to_string()).collect();
let head: FxHashSet<String> = [
"src/impl.ts::pub".to_string(),
"src/impl.ts::widget".to_string(),
]
.into_iter()
.collect();
let introduced = introduced_keys(&head, &base);
assert_eq!(introduced, vec!["src/impl.ts::widget".to_string()]);
}
}