use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Result, anyhow};
use cargo_metadata::{Metadata, Package, Target};
use ignore::{DirEntry, WalkBuilder}; use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{manifest::Manifest, source_parser::ParsedSource, util::read_to_string};
pub struct WorkspaceContext {
pub root: PathBuf,
pub manifest_path: PathBuf,
pub manifest_content: String,
pub manifest: Manifest,
pub files: FxHashMap<PathBuf, ParsedSource>,
pub linked: FxHashSet<PathBuf>,
pub packages: usize,
pub dep_to_pkg: FxHashMap<String, String>,
pub ignored_deps: FxHashSet<String>,
}
impl WorkspaceContext {
pub fn new(metadata: &Metadata) -> Result<Self> {
let root = metadata.workspace_root.as_std_path().to_path_buf();
let manifest_path = root.join("Cargo.toml");
let manifest_content = read_to_string(&manifest_path)?;
let manifest: Manifest = toml::from_str(&manifest_content)?;
let package_roots: Arc<FxHashSet<PathBuf>> = Arc::new(
metadata
.workspace_packages()
.iter()
.filter_map(|pkg| pkg.manifest_path.parent())
.map(|path| path.as_std_path().to_path_buf())
.collect(),
);
let entry_points: FxHashSet<PathBuf> = metadata
.workspace_packages()
.iter()
.flat_map(|pkg| pkg.targets.iter())
.map(|target| target.src_path.as_std_path().to_path_buf())
.collect();
let parents: FxHashSet<PathBuf> =
entry_points.iter().filter_map(|path| path.parent()).map(Path::to_path_buf).collect();
let walked: FxHashSet<PathBuf> = parents
.into_par_iter()
.flat_map_iter(|parent| {
let package_roots = Arc::clone(&package_roots);
WalkBuilder::new(&parent)
.filter_entry(move |entry| {
if let Some(file_type) = entry.file_type()
&& file_type.is_dir()
{
let path = entry.path();
if path.join("Cargo.toml").exists() {
return package_roots.contains(path);
}
}
true
})
.build()
.filter_map(Result::ok)
.filter(|entry| {
entry.file_type().is_some_and(|file_type| file_type.is_file())
&& entry.path().extension().is_some_and(|extension| extension == "rs")
})
.map(DirEntry::into_path)
})
.collect();
let paths: FxHashSet<PathBuf> =
entry_points.iter().filter(|path| path.is_file()).cloned().chain(walked).collect();
let files: FxHashMap<PathBuf, ParsedSource> = paths
.into_par_iter()
.filter_map(|path| {
let is_entry_point = entry_points.contains(&path);
ParsedSource::from_path(&path, is_entry_point).ok().map(|source| (path, source))
})
.collect();
let linked: FxHashSet<PathBuf> = entry_points
.into_iter()
.chain(files.values().flat_map(|parsed| {
parsed.paths.iter().filter_map(|path| {
if path.as_os_str().as_encoded_bytes().starts_with(b".") {
path.canonicalize().ok()
} else {
Some(path.clone())
}
})
}))
.collect();
let dep_to_pkg = manifest
.workspace
.dependencies
.iter()
.map(|(dep, dependency)| {
let dep = dep.get_ref();
let pkg = dependency.get_ref().package().unwrap_or(dep);
(dep.to_owned(), pkg.to_owned())
})
.collect();
let ignored_deps = manifest
.workspace
.metadata
.cargo_shear
.ignored
.iter()
.map(|ignore| ignore.get_ref().clone())
.collect();
Ok(Self {
root,
manifest_path,
manifest_content,
manifest,
packages: metadata.workspace_packages().len(),
files,
linked,
dep_to_pkg,
ignored_deps,
})
}
}
pub struct PackageContext<'a> {
pub workspace: &'a WorkspaceContext,
pub name: String,
pub directory: PathBuf,
pub manifest_path: PathBuf,
pub manifest_content: String,
pub manifest: Manifest,
pub targets: Vec<Target>,
pub import_to_pkg: FxHashMap<String, String>,
pub pkg_to_import: FxHashMap<String, String>,
pub ignored_imports: FxHashSet<String>,
}
impl<'a> PackageContext<'a> {
pub fn new(
workspace: &'a WorkspaceContext,
package: &Package,
metadata: &Metadata,
) -> Result<Self> {
let manifest_path = package.manifest_path.as_std_path();
let manifest_content = read_to_string(manifest_path)?;
let manifest: Manifest = toml::from_str(&manifest_content)?;
let resolved = metadata
.resolve
.as_ref()
.ok_or_else(|| {
anyhow!("`cargo_metadata::MetadataCommand::no_deps` should not be called.")
})?
.nodes
.iter()
.find(|node| node.id == package.id)
.ok_or_else(|| anyhow!("Package not found: {}", package.name))?;
let directory = manifest_path
.parent()
.ok_or_else(|| anyhow!("Package has no parent directory: {}", package.name))?
.to_path_buf();
let mut import_to_pkg = FxHashMap::default();
let mut pkg_to_import = FxHashMap::default();
for dep in &resolved.deps {
if let Some(pkg) = metadata.packages.iter().find(|package| package.id == dep.pkg) {
import_to_pkg.insert(dep.name.clone(), pkg.name.to_string());
pkg_to_import.insert(pkg.name.to_string(), dep.name.clone());
}
}
let package_ignored_deps = &manifest.package.metadata.cargo_shear.ignored;
let workspace_ignored_deps = &workspace.manifest.workspace.metadata.cargo_shear.ignored;
let ignored_imports = package_ignored_deps
.iter()
.chain(workspace_ignored_deps)
.map(|dep| dep.get_ref().replace('-', "_"))
.collect();
Ok(Self {
workspace,
name: package.name.to_string(),
directory,
manifest_path: manifest_path.to_path_buf(),
manifest_content,
manifest,
targets: package.targets.clone(),
import_to_pkg,
pkg_to_import,
ignored_imports,
})
}
}