use std::path::{Path, PathBuf};
use rustc_hash::{FxHashMap, FxHashSet};
use fallow_config::{ResolvedConfig, WorkspaceInfo};
use crate::discover::FileId;
use crate::graph::ModuleGraph;
use crate::results::RouteCollision;
use crate::suppress::{IssueKind, SuppressionContext};
use super::route_tree::classify_route_file;
type BucketKey = (String, String, String);
#[must_use]
pub fn find_route_collisions(
graph: &ModuleGraph,
config: &ResolvedConfig,
workspaces: &[WorkspaceInfo],
declared_deps: &FxHashSet<String>,
suppressions: &SuppressionContext<'_>,
) -> Vec<RouteCollision> {
if !declared_deps.contains("next") {
return Vec::new();
}
let pkg_roots = collect_pkg_roots(config, workspaces);
let pkg_root_refs: Vec<&Path> = pkg_roots.iter().map(PathBuf::as_path).collect();
let mut buckets: FxHashMap<BucketKey, Vec<(FileId, &Path)>> = FxHashMap::default();
for module in &graph.modules {
let Some(classified) = classify_route_file(module.path.as_path(), &pkg_root_refs) else {
continue;
};
if !classified.is_url_leaf() {
continue;
}
let key = classified.collision_bucket();
buckets
.entry(key)
.or_default()
.push((module.file_id, module.path.as_path()));
}
let mut findings = Vec::new();
for ((_, _, url), mut files) in buckets {
if files.len() < 2 {
continue;
}
files.sort_by(|a, b| a.1.cmp(b.1));
for &(file_id, path) in &files {
if suppressions.is_file_suppressed(file_id, IssueKind::RouteCollision) {
continue;
}
let conflicting_paths: Vec<PathBuf> = files
.iter()
.filter(|(_, p)| *p != path)
.map(|(_, p)| p.to_path_buf())
.collect();
findings.push(RouteCollision {
path: path.to_path_buf(),
url: url.clone(),
conflicting_paths,
line: 1,
col: 0,
});
}
}
findings
}
fn collect_pkg_roots(config: &ResolvedConfig, workspaces: &[WorkspaceInfo]) -> Vec<PathBuf> {
let mut roots = Vec::with_capacity(workspaces.len() + 1);
roots.push(config.root.clone());
roots.extend(workspaces.iter().map(|w| w.root.clone()));
roots
}