use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use serde::Serialize;
use syn::visit::{self, Visit};
use syn::{ItemUse, UseTree};
use crate::finding::{EvidenceClass, Finding, Location, OneBasedLine, Origin, Severity};
use crate::functions::read_and_parse_source;
use crate::ingest::{CrateInfo, DependencyKind, Workspace};
pub const MISPLACED_DEPENDENCY_KIND_RULE: &str = "misplaced-dependency-kind";
pub const MISPLACED_DEPENDENCY_KIND_RULE_REVISION: u32 = 1;
pub const UNUSED_DEV_DEPENDENCY_RULE: &str = "unused-dev-dependency";
pub const UNUSED_DEV_DEPENDENCY_RULE_REVISION: u32 = 1;
pub const HEAVY_DEPENDENCY_RULE: &str = "heavy-dependency";
pub const HEAVY_DEPENDENCY_RULE_REVISION: u32 = 1;
pub const UNUSED_FEATURE_FLAG_RULE: &str = "unused-feature-flag";
pub const UNUSED_FEATURE_FLAG_RULE_REVISION: u32 = 1;
pub const DEFAULT_FEATURES_UNUSED_RULE: &str = "default-features-unused";
pub const DEFAULT_FEATURES_UNUSED_RULE_REVISION: u32 = 1;
pub const UNUSED_FEATURE_RULE: &str = "unused-feature";
pub const UNUSED_FEATURE_RULE_REVISION: u32 = 1;
pub const UNUSED_DEPENDENCY_RULE: &str = "unused-dependency";
pub const UNUSED_DEPENDENCY_RULE_REVISION: u32 = 1;
pub const DEP_WITHOUT_REPO_RULE: &str = "dep-without-repo";
pub const DEP_WITHOUT_REPO_RULE_REVISION: u32 = 1;
const HEAVY_DEPENDENCY_TRANSITIVE_THRESHOLD: usize = 20;
const HEAVY_DEPENDENCY_USED_ITEMS_THRESHOLD: usize = 3;
const SHORT_FEATURE_LIST_MAX: usize = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageDomain {
Normal,
Dev,
Build,
}
impl UsageDomain {
pub const fn label(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Dev => "dev",
Self::Build => "build",
}
}
}
pub(crate) fn classify_domain(relative: &Path) -> UsageDomain {
if relative == Path::new("build.rs") {
return UsageDomain::Build;
}
let is_dev_dir = matches!(
relative.components().next(),
Some(std::path::Component::Normal(name))
if name == "tests" || name == "examples" || name == "benches"
);
if is_dev_dir {
UsageDomain::Dev
} else {
UsageDomain::Normal
}
}
#[derive(Debug)]
pub enum DepsError {
Io(PathBuf, std::io::Error),
Parse(PathBuf, syn::Error),
Metadata(cargo_metadata::Error),
ManifestParse(PathBuf, toml::de::Error),
RustcCheck(String),
}
impl std::fmt::Display for DepsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(path, err) => write!(f, "{}: failed to read file: {err}", path.display()),
Self::Parse(path, err) => write!(f, "{}: failed to parse: {err}", path.display()),
Self::Metadata(err) => write!(f, "failed to resolve full dependency graph: {err}"),
Self::ManifestParse(path, err) => {
write!(f, "{}: failed to parse: {err}", path.display())
}
Self::RustcCheck(msg) => write!(f, "{msg}"),
}
}
}
impl std::error::Error for DepsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(_, err) => Some(err),
Self::Parse(_, err) => Some(err),
Self::Metadata(err) => Some(err),
Self::ManifestParse(_, err) => Some(err),
Self::RustcCheck(_) => None,
}
}
}
#[derive(Debug, Default)]
pub struct WorkspaceDeps {
pub findings: Vec<Finding>,
pub feature_only_candidates: Vec<String>,
pub errors: Vec<DepsError>,
}
pub fn analyze_workspace(workspace: &Workspace) -> WorkspaceDeps {
let mut findings = Vec::new();
let mut feature_only_candidates = Vec::new();
let mut errors = Vec::new();
for krate in &workspace.crates {
let (usage, failed_domains, crate_errors) = collect_crate_usage(krate);
errors.extend(crate_errors);
let manifest = read_manifest_toml(&krate.manifest_path, &mut errors);
for dep in &krate.dependencies {
let domains = usage.get(&dep.code_identifier);
let has_normal = domains.is_some_and(|d| d.contains(&UsageDomain::Normal));
let has_dev = domains.is_some_and(|d| d.contains(&UsageDomain::Dev));
let has_build = domains.is_some_and(|d| d.contains(&UsageDomain::Build));
let used_anywhere = domains.is_some_and(|d| !d.is_empty());
let zero_usage = !used_anywhere && failed_domains.is_empty();
if zero_usage && !dep.features.is_empty() {
feature_only_candidates.push(dep.name.clone());
findings.extend(unused_feature_flag_findings(krate, dep));
}
if zero_usage
&& manifest.as_ref().is_some_and(|manifest| {
manifest_explicitly_enables_default_features(manifest, &dep.name)
})
{
findings.push(default_features_unused_finding(krate, dep));
}
if zero_usage && !dep.features.is_empty() {
continue;
}
if dep.kind == DependencyKind::Development
&& !has_dev
&& failed_domains.is_empty()
&& !krate.dependencies.iter().any(|other| {
other.name == dep.name && other.kind != DependencyKind::Development
})
{
findings.push(unused_dev_dependency_finding(krate, dep));
}
let flagged = match dep.kind {
DependencyKind::Normal => {
has_dev
&& !has_normal
&& dep.features.len() <= SHORT_FEATURE_LIST_MAX
&& !failed_domains.contains(&UsageDomain::Normal)
}
DependencyKind::Build => {
!has_build && !failed_domains.contains(&UsageDomain::Build)
}
DependencyKind::Development => false,
};
if flagged {
findings.push(misplaced_finding(krate, dep));
}
}
if let Some(manifest) = &manifest {
for feature_name in declared_features_without_implications(manifest) {
if !feature_is_referenced(krate, &feature_name) {
findings.push(unused_feature_finding(krate, &feature_name));
}
}
}
}
let (metadata_findings, metadata_errors) = analyze_full_metadata_dependencies(workspace);
findings.extend(metadata_findings);
errors.extend(metadata_errors);
WorkspaceDeps {
findings,
feature_only_candidates,
errors,
}
}
const PUBLIC_API_EXPOSURE_LIMITATION: &str = "not determined \u{2014} public API exposure requires Deep-Tier semantic analysis (see `cargo judge api-surface`, built with `--features deep`); this Fast-Tier query cannot resolve whether this dependency's types appear in this crate's own public API surface";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DependencyUsageSite {
pub file: PathBuf,
pub line: OneBasedLine,
pub domain: UsageDomain,
}
#[derive(Debug, Serialize)]
pub struct DependencyDeclaration {
pub crate_name: String,
pub kind: DependencyKind,
pub features: Vec<String>,
pub default_features_explicit: bool,
pub target: Option<String>,
pub usages: Vec<DependencyUsageSite>,
}
#[derive(Debug)]
pub struct DependencyWhyReport {
pub dependency_name: String,
pub found: bool,
pub declared_by: Vec<DependencyDeclaration>,
pub graph_paths: HashMap<String, Vec<String>>,
pub public_api_exposure: &'static str,
pub errors: Vec<DepsError>,
}
pub fn why(workspace: &Workspace, dependency_name: &str) -> DependencyWhyReport {
let mut errors = Vec::new();
let mut declared_by = Vec::new();
for krate in &workspace.crates {
for dep in &krate.dependencies {
if dep.name != dependency_name {
continue;
}
let manifest = read_manifest_toml(&krate.manifest_path, &mut errors);
let default_features_explicit = manifest.as_ref().is_some_and(|manifest| {
manifest_explicitly_enables_default_features(manifest, &dep.name)
});
let (usages, usage_errors) = collect_usage_sites(krate, &dep.code_identifier);
errors.extend(usage_errors);
declared_by.push(DependencyDeclaration {
crate_name: krate.name.clone(),
kind: dep.kind,
features: dep.features.clone(),
default_features_explicit,
target: dep.target.clone(),
usages,
});
}
}
let manifest_path = workspace.root.join("Cargo.toml");
let (found_in_graph, graph_paths) = match resolve_graph_paths(&manifest_path, dependency_name) {
Ok(result) => result,
Err(err) => {
errors.push(DepsError::Metadata(err));
(false, HashMap::new())
}
};
DependencyWhyReport {
dependency_name: dependency_name.to_string(),
found: found_in_graph || !declared_by.is_empty(),
declared_by,
graph_paths,
public_api_exposure: PUBLIC_API_EXPOSURE_LIMITATION,
errors,
}
}
fn collect_usage_sites(
krate: &CrateInfo,
target: &str,
) -> (Vec<DependencyUsageSite>, Vec<DepsError>) {
let mut sites = Vec::new();
let mut errors = Vec::new();
for file in &krate.source_files {
let relative = file
.path
.strip_prefix(&krate.root)
.unwrap_or(file.path.as_path());
let domain = classify_domain(relative);
match read_and_parse_source(
&file.path,
|err| DepsError::Io(file.path.clone(), err),
|err| DepsError::Parse(file.path.clone(), err),
) {
Ok((_, ast)) => {
let mut collector = WhySiteCollector {
target,
file: &file.path,
domain,
sites: Vec::new(),
};
collector.visit_file(&ast);
sites.extend(collector.sites);
}
Err(err) => errors.push(err),
}
}
sites.sort_by(|a, b| a.file.cmp(&b.file).then(a.line.cmp(&b.line)));
sites.dedup();
(sites, errors)
}
struct WhySiteCollector<'a> {
target: &'a str,
file: &'a Path,
domain: UsageDomain,
sites: Vec<DependencyUsageSite>,
}
impl WhySiteCollector<'_> {
fn record(&mut self, ident: &proc_macro2::Ident) {
if *ident != self.target {
return;
}
if let Some(line) = OneBasedLine::new(ident.span().start().line) {
self.sites.push(DependencyUsageSite {
file: self.file.to_path_buf(),
line,
domain: self.domain,
});
}
}
fn walk_use_tree(&mut self, tree: &UseTree) {
match tree {
UseTree::Path(use_path) => self.record(&use_path.ident),
UseTree::Name(use_name) => self.record(&use_name.ident),
UseTree::Rename(use_rename) => self.record(&use_rename.ident),
UseTree::Glob(_) => {}
UseTree::Group(group) => {
for item in &group.items {
self.walk_use_tree(item);
}
}
}
}
}
impl<'ast> Visit<'ast> for WhySiteCollector<'_> {
fn visit_item_use(&mut self, node: &'ast ItemUse) {
self.walk_use_tree(&node.tree);
}
fn visit_path(&mut self, node: &'ast syn::Path) {
if let Some(first) = node.segments.first() {
self.record(&first.ident);
}
visit::visit_path(self, node);
}
}
fn resolve_graph_paths(
manifest_path: &Path,
dependency_name: &str,
) -> Result<(bool, HashMap<String, Vec<String>>), cargo_metadata::Error> {
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(manifest_path)
.exec()?;
let found = metadata
.packages
.iter()
.any(|package| package.name.as_str() == dependency_name);
let Some(resolve) = metadata.resolve else {
return Ok((found, HashMap::new()));
};
let adjacency: HashMap<&cargo_metadata::PackageId, &[cargo_metadata::PackageId]> = resolve
.nodes
.iter()
.map(|node| (&node.id, node.dependencies.as_slice()))
.collect();
let id_to_name: HashMap<&cargo_metadata::PackageId, &str> = metadata
.packages
.iter()
.map(|package| (&package.id, package.name.as_str()))
.collect();
let mut paths: HashMap<String, Vec<String>> = HashMap::new();
for member_id in &metadata.workspace_members {
let Some(&member_name) = id_to_name.get(member_id) else {
continue;
};
let path = shortest_graph_path(&adjacency, &id_to_name, member_id, dependency_name);
paths.insert(member_name.to_string(), path);
}
Ok((found, paths))
}
fn shortest_graph_path(
adjacency: &HashMap<&cargo_metadata::PackageId, &[cargo_metadata::PackageId]>,
id_to_name: &HashMap<&cargo_metadata::PackageId, &str>,
start: &cargo_metadata::PackageId,
target_name: &str,
) -> Vec<String> {
let mut visited: HashSet<&cargo_metadata::PackageId> = HashSet::new();
visited.insert(start);
let mut queue: VecDeque<(&cargo_metadata::PackageId, Vec<String>)> = VecDeque::new();
queue.push_back((start, Vec::new()));
while let Some((current, path)) = queue.pop_front() {
let Some(children) = adjacency.get(current) else {
continue;
};
for child in children.iter() {
if !visited.insert(child) {
continue;
}
let Some(&child_name) = id_to_name.get(child) else {
continue;
};
let mut next_path = path.clone();
next_path.push(child_name.to_string());
if child_name == target_name {
return next_path;
}
queue.push_back((child, next_path));
}
}
Vec::new()
}
pub(crate) fn read_manifest_toml_typed<E>(
manifest_path: &Path,
io_err: impl FnOnce(PathBuf, std::io::Error) -> E,
parse_err: impl FnOnce(PathBuf, toml::de::Error) -> E,
) -> Result<toml::Value, E> {
let text = std::fs::read_to_string(manifest_path)
.map_err(|err| io_err(manifest_path.to_path_buf(), err))?;
toml::from_str(&text).map_err(|err| parse_err(manifest_path.to_path_buf(), err))
}
fn read_manifest_toml(manifest_path: &Path, errors: &mut Vec<DepsError>) -> Option<toml::Value> {
match read_manifest_toml_typed(manifest_path, DepsError::Io, DepsError::ManifestParse) {
Ok(manifest) => Some(manifest),
Err(err) => {
errors.push(err);
None
}
}
}
fn manifest_explicitly_enables_default_features(manifest: &toml::Value, dep_name: &str) -> bool {
const DEPENDENCY_TABLE_KEYS: [&str; 3] =
["dependencies", "dev-dependencies", "build-dependencies"];
fn table_sets_default_features_true(table: &toml::Value, dep_name: &str) -> bool {
table
.get(dep_name)
.and_then(|dep| dep.get("default-features"))
.and_then(toml::Value::as_bool)
== Some(true)
}
let found_at_top_level = DEPENDENCY_TABLE_KEYS.iter().any(|key| {
manifest
.get(key)
.is_some_and(|table| table_sets_default_features_true(table, dep_name))
});
if found_at_top_level {
return true;
}
manifest
.get("target")
.and_then(toml::Value::as_table)
.is_some_and(|platforms| {
platforms.values().any(|platform| {
DEPENDENCY_TABLE_KEYS.iter().any(|key| {
platform
.get(key)
.is_some_and(|table| table_sets_default_features_true(table, dep_name))
})
})
})
}
pub(crate) fn dep_location(krate: &CrateInfo, item_path: impl Into<String>) -> Location {
Location {
file: krate.manifest_path.clone(),
line: OneBasedLine::FIRST,
item_path: item_path.into(),
}
}
fn misplaced_finding(krate: &CrateInfo, dep: &crate::ingest::DeclaredDependency) -> Finding {
Finding::new(
format!(
"{MISPLACED_DEPENDENCY_KIND_RULE}:{}:{}",
krate.name, dep.name
),
MISPLACED_DEPENDENCY_KIND_RULE,
Severity::Warn,
dep_location(krate, dep.name.clone()),
EvidenceClass::Heuristic,
Origin::Code,
None,
)
}
fn unused_dev_dependency_finding(
krate: &CrateInfo,
dep: &crate::ingest::DeclaredDependency,
) -> Finding {
Finding::new(
format!("{UNUSED_DEV_DEPENDENCY_RULE}:{}:{}", krate.name, dep.name),
UNUSED_DEV_DEPENDENCY_RULE,
Severity::Warn,
dep_location(krate, dep.name.clone()),
EvidenceClass::BoundedSemantic,
Origin::Code,
Some(serde_json::json!({
"searched": ["tests/", "examples/", "benches/", "#[cfg(test)] modules in src/"],
"reason": "no use found in the examined view (tests/examples/benches of this \
package, and #[cfg(test)] modules in its src files; doctests are not scanned)",
})),
)
}
fn unused_feature_flag_findings(
krate: &CrateInfo,
dep: &crate::ingest::DeclaredDependency,
) -> Vec<Finding> {
dep.features
.iter()
.map(|feature| {
Finding::new(
format!(
"{UNUSED_FEATURE_FLAG_RULE}:{}:{}:{feature}",
krate.name, dep.name
),
UNUSED_FEATURE_FLAG_RULE,
Severity::Warn,
dep_location(krate, dep.name.clone()),
EvidenceClass::DerivedFact,
Origin::Code,
Some(serde_json::json!({
"feature": feature,
"reason": "no other usage of this dependency was found in the examined view",
})),
)
})
.collect()
}
fn default_features_unused_finding(
krate: &CrateInfo,
dep: &crate::ingest::DeclaredDependency,
) -> Finding {
Finding::new(
format!("{DEFAULT_FEATURES_UNUSED_RULE}:{}:{}", krate.name, dep.name),
DEFAULT_FEATURES_UNUSED_RULE,
Severity::Warn,
dep_location(krate, dep.name.clone()),
EvidenceClass::DerivedFact,
Origin::Code,
Some(serde_json::json!({
"reason": "no other usage of this dependency was found in the examined view, and \
the manifest explicitly sets default-features = true",
})),
)
}
fn declared_features_without_implications(manifest: &toml::Value) -> Vec<String> {
let Some(table) = manifest.get("features").and_then(toml::Value::as_table) else {
return Vec::new();
};
table
.iter()
.filter(|(name, _)| name.as_str() != "default")
.filter(|(_, value)| {
value
.as_array()
.is_some_and(|implications| implications.is_empty())
})
.map(|(name, _)| name.clone())
.collect()
}
fn feature_is_referenced(krate: &CrateInfo, feature_name: &str) -> bool {
let spaced = format!("feature = \"{feature_name}\"");
let tight = format!("feature=\"{feature_name}\"");
krate
.source_files
.iter()
.filter(|file| file.kind.is_locally_reportable())
.any(|file| {
std::fs::read_to_string(&file.path)
.is_ok_and(|text| text.contains(&spaced) || text.contains(&tight))
})
}
fn unused_feature_finding(krate: &CrateInfo, feature_name: &str) -> Finding {
Finding::new(
format!("{UNUSED_FEATURE_RULE}:{}:{feature_name}", krate.name),
UNUSED_FEATURE_RULE,
Severity::Warn,
dep_location(krate, feature_name.to_string()),
EvidenceClass::DerivedFact,
Origin::Code,
Some(serde_json::json!({
"feature": feature_name,
"reason": "no `cfg(feature = \"...\")`/`cfg!(feature = \"...\")` reference to this \
declared feature was found anywhere in the crate's own authored source",
})),
)
}
fn is_heavy_dependency(transitive_deps: usize, used_items: usize) -> bool {
transitive_deps > HEAVY_DEPENDENCY_TRANSITIVE_THRESHOLD
&& used_items < HEAVY_DEPENDENCY_USED_ITEMS_THRESHOLD
}
fn analyze_full_metadata_dependencies(workspace: &Workspace) -> (Vec<Finding>, Vec<DepsError>) {
let mut findings = Vec::new();
let mut errors = Vec::new();
let manifest_path = workspace.root.join("Cargo.toml");
let metadata = match resolve_full_metadata(&manifest_path) {
Ok(metadata) => metadata,
Err(err) => {
errors.push(DepsError::Metadata(err));
return (findings, errors);
}
};
for krate in &workspace.crates {
for dep in &krate.dependencies {
if let Some(&transitive_deps) = metadata.transitive_deps.get(&dep.name) {
let used_items = collect_used_items(krate, &dep.code_identifier);
if is_heavy_dependency(transitive_deps, used_items.len()) {
findings.push(heavy_dependency_finding(
krate,
dep,
transitive_deps,
&used_items,
));
}
}
if let Some(repository) = metadata.repository.get(&dep.name)
&& repository_is_missing(repository)
{
findings.push(dep_without_repo_finding(krate, dep));
}
}
}
(findings, errors)
}
fn repository_is_missing(repository: &Option<String>) -> bool {
match repository {
None => true,
Some(text) => text.trim().is_empty(),
}
}
fn heavy_dependency_finding(
krate: &CrateInfo,
dep: &crate::ingest::DeclaredDependency,
transitive_deps: usize,
used_items: &HashSet<String>,
) -> Finding {
let mut examples: Vec<&String> = used_items.iter().collect();
examples.sort();
examples.truncate(5);
Finding::new(
format!("{HEAVY_DEPENDENCY_RULE}:{}:{}", krate.name, dep.name),
HEAVY_DEPENDENCY_RULE,
Severity::Info,
dep_location(krate, dep.name.clone()),
EvidenceClass::Heuristic,
Origin::Code,
Some(serde_json::json!({
"transitive_deps": transitive_deps,
"used_items": used_items.len(),
"examples": examples,
})),
)
}
fn dep_without_repo_finding(krate: &CrateInfo, dep: &crate::ingest::DeclaredDependency) -> Finding {
Finding::new(
format!("{DEP_WITHOUT_REPO_RULE}:{}:{}", krate.name, dep.name),
DEP_WITHOUT_REPO_RULE,
Severity::Info,
dep_location(krate, dep.name.clone()),
EvidenceClass::DerivedFact,
Origin::Code,
Some(serde_json::json!({
"reason": "no `repository` field found in this dependency's own manifest",
})),
)
}
#[derive(Debug, Default)]
struct FullMetadataResolve {
transitive_deps: HashMap<String, usize>,
repository: HashMap<String, Option<String>>,
}
fn resolve_full_metadata(
manifest_path: &Path,
) -> Result<FullMetadataResolve, cargo_metadata::Error> {
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(manifest_path)
.exec()?;
let repository: HashMap<String, Option<String>> = metadata
.packages
.iter()
.map(|package| (package.name.to_string(), package.repository.clone()))
.collect();
let Some(resolve) = metadata.resolve else {
return Ok(FullMetadataResolve {
transitive_deps: HashMap::new(),
repository,
});
};
let adjacency: HashMap<&cargo_metadata::PackageId, &[cargo_metadata::PackageId]> = resolve
.nodes
.iter()
.map(|node| (&node.id, node.dependencies.as_slice()))
.collect();
let id_to_name: HashMap<&cargo_metadata::PackageId, String> = metadata
.packages
.iter()
.map(|package| (&package.id, package.name.to_string()))
.collect();
let mut transitive_deps: HashMap<String, usize> = HashMap::new();
for node in &resolve.nodes {
let mut visited: HashSet<&cargo_metadata::PackageId> = HashSet::new();
let mut stack: Vec<&cargo_metadata::PackageId> = node.dependencies.iter().collect();
while let Some(dep_id) = stack.pop() {
if visited.insert(dep_id)
&& let Some(children) = adjacency.get(dep_id)
{
stack.extend(children.iter());
}
}
if let Some(name) = id_to_name.get(&node.id) {
transitive_deps.entry(name.clone()).or_insert(visited.len());
}
}
Ok(FullMetadataResolve {
transitive_deps,
repository,
})
}
#[derive(Debug, Default)]
pub struct RustcLintDeps {
pub findings: Vec<Finding>,
pub errors: Vec<DepsError>,
}
pub fn analyze_rustc_unused_dependencies(workspace: &Workspace) -> RustcLintDeps {
let mut findings = Vec::new();
let mut errors = Vec::new();
let manifest_path = workspace.root.join("Cargo.toml");
let metadata = match cargo_metadata::MetadataCommand::new()
.manifest_path(&manifest_path)
.no_deps()
.exec()
{
Ok(metadata) => metadata,
Err(err) => {
errors.push(DepsError::Metadata(err));
return RustcLintDeps { findings, errors };
}
};
let workspace_members: HashSet<cargo_metadata::PackageId> =
metadata.workspace_members.iter().cloned().collect();
let name_to_id: HashMap<&str, cargo_metadata::PackageId> = metadata
.packages
.iter()
.filter(|package| workspace_members.contains(&package.id))
.map(|package| (package.name.as_str(), package.id.clone()))
.collect();
let messages = match run_cargo_check_with_unused_crate_dependencies_lint(&manifest_path) {
Ok(messages) => messages,
Err(err) => {
errors.push(err);
return RustcLintDeps { findings, errors };
}
};
let mut targets_seen: HashMap<cargo_metadata::PackageId, HashSet<String>> = HashMap::new();
let mut unused_per_target: HashMap<(cargo_metadata::PackageId, String), HashSet<String>> =
HashMap::new();
for message in messages {
match message {
cargo_metadata::Message::CompilerArtifact(artifact) => {
if !workspace_members.contains(&artifact.package_id) {
continue;
}
targets_seen
.entry(artifact.package_id)
.or_default()
.insert(target_identity(&artifact.target));
}
cargo_metadata::Message::CompilerMessage(compiler_message) => {
if !workspace_members.contains(&compiler_message.package_id) {
continue;
}
let is_unused_crate_dependency = compiler_message
.message
.code
.as_ref()
.is_some_and(|code| code.code == "unused_crate_dependencies");
if !is_unused_crate_dependency {
continue;
}
let Some(dep_identifier) =
extract_unused_crate_name(&compiler_message.message.message)
else {
continue;
};
unused_per_target
.entry((
compiler_message.package_id,
target_identity(&compiler_message.target),
))
.or_default()
.insert(dep_identifier);
}
_ => {}
}
}
for krate in &workspace.crates {
let Some(package_id) = name_to_id.get(krate.name.as_str()) else {
continue;
};
let Some(all_targets) = targets_seen.get(package_id) else {
continue;
};
if all_targets.is_empty() {
continue;
}
let mut always_unused: Option<HashSet<String>> = None;
for target in all_targets {
let unused_here = unused_per_target
.get(&(package_id.clone(), target.clone()))
.cloned()
.unwrap_or_default();
always_unused = Some(match always_unused {
None => unused_here,
Some(acc) => acc.intersection(&unused_here).cloned().collect(),
});
}
let Some(always_unused) = always_unused else {
continue;
};
let mut targets_checked: Vec<&String> = all_targets.iter().collect();
targets_checked.sort();
for dep_identifier in &always_unused {
let Some(dep) = krate.dependencies.iter().find(|dep| {
dep.kind == DependencyKind::Normal && dep.code_identifier == *dep_identifier
}) else {
continue;
};
findings.push(unused_dependency_finding(krate, dep, &targets_checked));
}
}
RustcLintDeps { findings, errors }
}
fn target_identity(target: &cargo_metadata::Target) -> String {
let kinds = target
.kind
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",");
format!("{kinds}:{}", target.name)
}
fn extract_unused_crate_name(message: &str) -> Option<String> {
if !message.contains("is unused in crate") {
return None;
}
message.split('`').nth(1).map(str::to_string)
}
fn run_cargo_check_with_unused_crate_dependencies_lint(
manifest_path: &Path,
) -> Result<Vec<cargo_metadata::Message>, DepsError> {
let mut rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
if !rustflags.is_empty() {
rustflags.push(' ');
}
rustflags.push_str("-W unused_crate_dependencies");
let output = std::process::Command::new("cargo")
.arg("check")
.arg("--workspace")
.arg("--all-targets")
.arg("--message-format=json")
.arg("--manifest-path")
.arg(manifest_path)
.env("RUSTFLAGS", rustflags)
.output()
.map_err(|err| DepsError::RustcCheck(format!("failed to run `cargo check`: {err}")))?;
if !output.status.success() {
return Err(DepsError::RustcCheck(format!(
"`cargo check --workspace --all-targets` exited with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(
cargo_metadata::Message::parse_stream(output.stdout.as_slice())
.filter_map(Result::ok)
.collect(),
)
}
fn unused_dependency_finding(
krate: &CrateInfo,
dep: &crate::ingest::DeclaredDependency,
targets_checked: &[&String],
) -> Finding {
Finding::new(
format!("{UNUSED_DEPENDENCY_RULE}:{}:{}", krate.name, dep.name),
UNUSED_DEPENDENCY_RULE,
Severity::Warn,
dep_location(krate, dep.name.clone()),
EvidenceClass::BoundedSemantic,
Origin::Code,
Some(serde_json::json!({
"source": "rustc:unused_crate_dependencies",
"targets_checked": targets_checked,
"package": krate.name,
"reason": "no use found by rustc's unused_crate_dependencies lint \
across all targets of this package",
})),
)
}
fn collect_used_items(krate: &CrateInfo, target: &str) -> HashSet<String> {
let mut items = HashSet::new();
for file in &krate.source_files {
let Ok(source) = std::fs::read_to_string(&file.path) else {
continue;
};
let Ok(ast) = syn::parse_file(&source) else {
continue;
};
let mut collector = DepItemCollector {
target,
items: HashSet::new(),
};
collector.visit_file(&ast);
items.extend(collector.items);
}
items
}
struct DepItemCollector<'a> {
target: &'a str,
items: HashSet<String>,
}
impl DepItemCollector<'_> {
fn walk_use_tree(&mut self, tree: &UseTree, matched: bool) {
match tree {
UseTree::Path(use_path) => {
let ident = use_path.ident.to_string();
if matched {
self.items.insert(ident);
self.walk_use_tree(&use_path.tree, false);
} else {
self.walk_use_tree(&use_path.tree, ident == self.target);
}
}
UseTree::Name(use_name) => {
if matched {
self.items.insert(use_name.ident.to_string());
}
}
UseTree::Rename(use_rename) => {
if matched {
self.items.insert(use_rename.ident.to_string());
}
}
UseTree::Glob(_) => {}
UseTree::Group(group) => {
for item in &group.items {
self.walk_use_tree(item, matched);
}
}
}
}
}
impl<'ast> Visit<'ast> for DepItemCollector<'_> {
fn visit_item_use(&mut self, node: &'ast ItemUse) {
self.walk_use_tree(&node.tree, false);
}
fn visit_path(&mut self, node: &'ast syn::Path) {
if node.segments.len() >= 2 && node.segments[0].ident == self.target {
self.items.insert(node.segments[1].ident.to_string());
}
visit::visit_path(self, node);
}
}
fn collect_crate_usage(
krate: &CrateInfo,
) -> (
HashMap<String, HashSet<UsageDomain>>,
HashSet<UsageDomain>,
Vec<DepsError>,
) {
let mut usage: HashMap<String, HashSet<UsageDomain>> = HashMap::new();
let mut failed_domains = HashSet::new();
let mut errors = Vec::new();
for file in &krate.source_files {
let relative = file
.path
.strip_prefix(&krate.root)
.unwrap_or(file.path.as_path());
let domain = classify_domain(relative);
match collect_identifiers(&file.path) {
Ok((idents, cfg_test_idents)) => {
for ident in idents {
usage.entry(ident).or_default().insert(domain);
}
for ident in cfg_test_idents {
usage.entry(ident).or_default().insert(UsageDomain::Dev);
}
}
Err(err) => {
failed_domains.insert(domain);
errors.push(err);
}
}
}
(usage, failed_domains, errors)
}
fn collect_identifiers(path: &Path) -> Result<(HashSet<String>, HashSet<String>), DepsError> {
let (_, ast) = read_and_parse_source(
path,
|err| DepsError::Io(path.to_path_buf(), err),
|err| DepsError::Parse(path.to_path_buf(), err),
)?;
let mut collector = PathIdentCollector::default();
collector.visit_file(&ast);
let mut cfg_test_collector = CfgTestIdentCollector::default();
cfg_test_collector.visit_file(&ast);
Ok((collector.idents, cfg_test_collector.idents))
}
pub(crate) fn attrs_have_cfg_test(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(|attr| {
if !attr.path().is_ident("cfg") {
return false;
}
let syn::Meta::List(list) = &attr.meta else {
return false;
};
list.tokens
.to_string()
.split(|c: char| !c.is_alphanumeric() && c != '_')
.any(|word| word == "test")
})
}
#[derive(Default)]
struct CfgTestIdentCollector {
idents: HashSet<String>,
}
impl<'ast> Visit<'ast> for CfgTestIdentCollector {
fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
if attrs_have_cfg_test(&node.attrs) {
let mut inner = PathIdentCollector::default();
inner.visit_item_mod(node);
self.idents.extend(inner.idents);
} else {
visit::visit_item_mod(self, node);
}
}
}
#[derive(Default)]
struct PathIdentCollector {
idents: HashSet<String>,
}
impl PathIdentCollector {
fn record(&mut self, ident: &str) {
if !matches!(ident, "self" | "super" | "crate" | "Self") {
self.idents.insert(ident.to_string());
}
}
fn walk_use_tree(&mut self, tree: &UseTree) {
match tree {
UseTree::Path(use_path) => self.record(&use_path.ident.to_string()),
UseTree::Name(use_name) => self.record(&use_name.ident.to_string()),
UseTree::Rename(use_rename) => self.record(&use_rename.ident.to_string()),
UseTree::Glob(_) => {}
UseTree::Group(group) => {
for item in &group.items {
self.walk_use_tree(item);
}
}
}
}
}
impl<'ast> Visit<'ast> for PathIdentCollector {
fn visit_item_use(&mut self, node: &'ast ItemUse) {
self.walk_use_tree(&node.tree);
}
fn visit_path(&mut self, node: &'ast syn::Path) {
if let Some(first) = node.segments.first() {
self.record(&first.ident.to_string());
}
visit::visit_path(self, node);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::TempDir;
fn write_fixture(
dir: &TempDir,
dependency_section: &str,
dep_crate_name: &str,
rename: Option<&str>,
features: &[&str],
main_files: &[(&str, &str)],
) -> PathBuf {
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
let dep_line = match rename {
Some(alias) => format!(
"{alias} = {{ package = \"{dep_crate_name}\", path = \"../dep_crate\"{} }}",
features_toml(features)
),
None => format!(
"{dep_crate_name} = {{ path = \"../dep_crate\"{} }}",
features_toml(features)
),
};
std::fs::write(
dir.join("main/Cargo.toml"),
format!(
r#"
[package]
name = "fixture"
version = "0.1.0"
edition = "2021"
{dependency_section}
{dep_line}
"#
),
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
for (relative, content) in main_files {
let file_path = dir.join("main").join(relative);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(file_path, content).unwrap();
}
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
format!(
r#"
[package]
name = "{dep_crate_name}"
version = "0.1.0"
edition = "2021"
repository = "https://example.com/{dep_crate_name}"
"#
),
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
dir.join("main/Cargo.toml")
}
fn features_toml(features: &[&str]) -> String {
if features.is_empty() {
String::new()
} else {
let joined = features
.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", ");
format!(", features = [{joined}]")
}
}
#[test]
fn a_normal_dependency_used_only_from_tests_is_flagged_as_dev() {
let dir = TempDir::new("deps-dev-only");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[("tests/it.rs", "fn t() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].location.item_path, "depcrate");
assert!(report.feature_only_candidates.is_empty());
}
#[test]
fn a_normal_dependency_used_only_inside_a_platform_cfg_gate_is_not_misclassified_as_dev_only() {
let dir = TempDir::new("deps-platform-cfg-gate");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[(
"src/lib.rs",
"#[cfg(target_os = \"windows\")]\nmod windows {\n pub fn go() { depcrate::noop(); }\n}\n",
)],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == MISPLACED_DEPENDENCY_KIND_RULE),
"a cfg-gated normal-domain usage must not be misclassified as dev-only: {:?}",
report.findings
);
}
#[test]
fn a_normal_dependency_used_from_src_and_tests_is_not_flagged() {
let dir = TempDir::new("deps-src-and-tests");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[
("src/lib.rs", "pub fn hello() { depcrate::noop(); }\n"),
("tests/it.rs", "fn t() { depcrate::noop(); }\n"),
],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(report.findings.is_empty());
}
#[test]
fn a_build_dependency_never_referenced_in_build_rs_is_flagged_as_unused() {
let dir = TempDir::new("deps-build-unused");
let manifest = write_fixture(
&dir,
"[build-dependencies]",
"depcrate",
None,
&[],
&[("build.rs", "fn main() {}\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].location.item_path, "depcrate");
}
#[test]
fn a_build_dependency_referenced_in_build_rs_is_not_flagged() {
let dir = TempDir::new("deps-build-used");
let manifest = write_fixture(
&dir,
"[build-dependencies]",
"depcrate",
None,
&[],
&[("build.rs", "fn main() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(report.findings.is_empty());
}
#[test]
fn a_parse_error_in_build_rs_does_not_claim_the_dependency_is_unused() {
let dir = TempDir::new("deps-build-parse-error");
let manifest = write_fixture(
&dir,
"[build-dependencies]",
"depcrate",
None,
&[],
&[("build.rs", "fn main( { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert_eq!(report.errors.len(), 1);
assert!(report.findings.is_empty());
}
#[test]
fn a_dependency_with_zero_usage_and_features_is_flagged_per_feature() {
let dir = TempDir::new("deps-feature-only");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&["some-feature", "other-feature"],
&[],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert_eq!(report.feature_only_candidates, vec!["depcrate".to_string()]);
let flagged: Vec<&Finding> = report
.findings
.iter()
.filter(|f| f.rule == UNUSED_FEATURE_FLAG_RULE)
.collect();
assert_eq!(flagged.len(), 2);
for finding in &flagged {
assert_eq!(finding.severity, Severity::Warn);
assert_eq!(finding.evidence_class, EvidenceClass::DerivedFact);
assert_eq!(finding.location.item_path, "depcrate");
}
let features: Vec<&str> = flagged
.iter()
.map(|f| f.evidence.as_ref().unwrap()["feature"].as_str().unwrap())
.collect();
assert!(features.contains(&"some-feature"));
assert!(features.contains(&"other-feature"));
}
#[test]
fn a_dependency_used_anywhere_has_no_unused_feature_flag_finding_despite_many_features() {
let dir = TempDir::new("deps-feature-used");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&["some-feature", "other-feature"],
&[("src/lib.rs", "pub fn hello() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_FEATURE_FLAG_RULE)
);
assert!(report.feature_only_candidates.is_empty());
}
fn write_fixture_with_dep_line(
dir: &TempDir,
dep_line: &str,
main_files: &[(&str, &str)],
) -> PathBuf {
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
format!(
r#"
[package]
name = "fixture"
version = "0.1.0"
edition = "2021"
[dependencies]
{dep_line}
"#
),
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
for (relative, content) in main_files {
let file_path = dir.join("main").join(relative);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(file_path, content).unwrap();
}
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
r#"
[package]
name = "depcrate"
version = "0.1.0"
edition = "2021"
repository = "https://example.com/depcrate"
"#,
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
dir.join("main/Cargo.toml")
}
#[test]
fn explicit_default_features_true_and_zero_usage_is_flagged() {
let dir = TempDir::new("deps-default-features-unused");
let manifest = write_fixture_with_dep_line(
&dir,
r#"depcrate = { path = "../dep_crate", default-features = true }"#,
&[],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
let finding = report
.findings
.iter()
.find(|f| f.rule == DEFAULT_FEATURES_UNUSED_RULE)
.expect("expected a default-features-unused finding");
assert_eq!(finding.severity, Severity::Warn);
assert_eq!(finding.evidence_class, EvidenceClass::DerivedFact);
assert_eq!(finding.location.item_path, "depcrate");
}
#[test]
fn explicit_default_features_true_but_used_is_not_flagged() {
let dir = TempDir::new("deps-default-features-used");
let manifest = write_fixture_with_dep_line(
&dir,
r#"depcrate = { path = "../dep_crate", default-features = true }"#,
&[("src/lib.rs", "pub fn hello() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == DEFAULT_FEATURES_UNUSED_RULE)
);
}
#[test]
fn no_explicit_default_features_and_zero_usage_is_not_flagged() {
let dir = TempDir::new("deps-default-features-implicit");
let manifest =
write_fixture_with_dep_line(&dir, r#"depcrate = { path = "../dep_crate" }"#, &[]);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == DEFAULT_FEATURES_UNUSED_RULE)
);
}
#[test]
fn a_renamed_dependency_is_matched_by_its_local_alias() {
let dir = TempDir::new("deps-renamed");
let manifest = write_fixture(
&dir,
"[dependencies]",
"real-name",
Some("some_dep"),
&[],
&[("tests/it.rs", "fn t() { some_dep::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert_eq!(report.findings.len(), 1);
assert_eq!(report.findings[0].location.item_path, "real-name");
}
#[test]
fn why_finds_a_known_usage_location_and_the_direct_graph_path() {
let dir = TempDir::new("deps-why-usage");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[("src/lib.rs", "pub fn hello() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = why(&workspace, "depcrate");
assert!(report.found);
assert!(report.errors.is_empty(), "{:?}", report.errors);
assert_eq!(report.declared_by.len(), 1);
let declaration = &report.declared_by[0];
assert_eq!(declaration.crate_name, "fixture");
assert_eq!(declaration.kind, DependencyKind::Normal);
assert!(declaration.features.is_empty());
assert_eq!(declaration.usages.len(), 1);
let usage = &declaration.usages[0];
assert!(usage.file.ends_with("src/lib.rs"), "{:?}", usage.file);
assert_eq!(usage.line.get(), 1);
assert_eq!(usage.domain, UsageDomain::Normal);
assert_eq!(
report.graph_paths.get("fixture"),
Some(&vec!["depcrate".to_string()])
);
}
#[test]
fn why_reports_not_found_for_an_unknown_dependency() {
let dir = TempDir::new("deps-why-not-found");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[("src/lib.rs", "pub fn hello() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = why(&workspace, "does-not-exist");
assert!(!report.found);
assert!(report.declared_by.is_empty());
}
#[test]
fn finding_shape_matches_the_documented_contract() {
let dir = TempDir::new("deps-finding-shape");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[("tests/it.rs", "fn t() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert_eq!(report.findings.len(), 1);
let finding = &report.findings[0];
assert_eq!(finding.rule, MISPLACED_DEPENDENCY_KIND_RULE);
assert_eq!(finding.severity, Severity::Warn);
assert_eq!(finding.origin, Origin::Code);
assert_eq!(finding.evidence_class, EvidenceClass::Heuristic);
assert_eq!(finding.location.file, workspace.crates[0].manifest_path);
}
#[test]
fn classify_domain_recognizes_dev_directories_and_build_rs() {
assert_eq!(classify_domain(Path::new("build.rs")), UsageDomain::Build);
assert_eq!(classify_domain(Path::new("tests/it.rs")), UsageDomain::Dev);
assert_eq!(
classify_domain(Path::new("examples/demo.rs")),
UsageDomain::Dev
);
assert_eq!(
classify_domain(Path::new("benches/bench.rs")),
UsageDomain::Dev
);
assert_eq!(
classify_domain(Path::new("src/lib.rs")),
UsageDomain::Normal
);
}
#[test]
fn deps_error_source_preserves_the_underlying_error() {
let err = DepsError::Io(PathBuf::from("src/lib.rs"), std::io::Error::other("boom"));
let source = std::error::Error::source(&err).expect("Io must carry a source");
assert!(source.downcast_ref::<std::io::Error>().is_some());
assert_eq!(err.to_string(), "src/lib.rs: failed to read file: boom");
}
#[test]
fn a_dev_dependency_used_only_from_tests_is_not_flagged_as_unused() {
let dir = TempDir::new("deps-dev-used-in-tests");
let manifest = write_fixture(
&dir,
"[dev-dependencies]",
"depcrate",
None,
&[],
&[("tests/it.rs", "fn t() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_DEV_DEPENDENCY_RULE)
);
}
#[test]
fn a_dev_dependency_never_used_is_flagged_with_a_hedged_message() {
let dir = TempDir::new("deps-dev-unused");
let manifest = write_fixture(&dir, "[dev-dependencies]", "depcrate", None, &[], &[]);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
let finding = report
.findings
.iter()
.find(|f| f.rule == UNUSED_DEV_DEPENDENCY_RULE)
.expect("expected an unused-dev-dependency finding");
assert_eq!(finding.severity, Severity::Warn);
assert_eq!(finding.evidence_class, EvidenceClass::BoundedSemantic);
assert_eq!(finding.location.item_path, "depcrate");
let reason = finding.evidence.as_ref().unwrap()["reason"]
.as_str()
.unwrap();
assert!(reason.contains("no use found in the examined view"));
assert!(!reason.contains("is unused"));
}
#[test]
fn a_dev_dependency_used_only_in_a_cfg_test_module_in_src_is_not_flagged() {
let dir = TempDir::new("deps-dev-cfg-test");
let manifest = write_fixture(
&dir,
"[dev-dependencies]",
"depcrate",
None,
&[],
&[(
"src/lib.rs",
"pub fn hello() {}\n\n#[cfg(test)]\nmod tests {\n #[test]\n fn t() { depcrate::noop(); }\n}\n",
)],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_DEV_DEPENDENCY_RULE)
);
}
#[test]
fn a_dev_dependency_also_declared_as_a_normal_dependency_is_not_flagged() {
let dir = TempDir::new("deps-dev-also-normal");
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
r#"
[package]
name = "fixture"
version = "0.1.0"
edition = "2021"
[dependencies]
depcrate = { path = "../dep_crate" }
[dev-dependencies]
depcrate = { path = "../dep_crate" }
"#,
)
.unwrap();
std::fs::write(
dir.join("main/src/lib.rs"),
"pub fn hello() { depcrate::noop(); }\n",
)
.unwrap();
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
r#"
[package]
name = "depcrate"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
let workspace = crate::ingest::load(Some(&dir.join("main/Cargo.toml"))).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_DEV_DEPENDENCY_RULE)
);
}
#[test]
fn collect_used_items_records_next_level_path_segments() {
let dir = TempDir::new("deps-used-items");
std::fs::create_dir_all(dir.join("src")).unwrap();
let file_path = dir.join("src/lib.rs");
std::fs::write(
&file_path,
"use depcrate::{Foo, bar::Baz};\nfn use_it() { depcrate::other::thing(); }\n",
)
.unwrap();
let krate = CrateInfo {
name: "fixture".to_string(),
version: "0.1.0".to_string(),
manifest_path: dir.join("Cargo.toml"),
root: dir.to_path_buf(),
source_files: vec![crate::ingest::SourceFile {
path: file_path,
kind: crate::ingest::SourceKind::Authored,
}],
entry_points: Vec::new(),
dependencies: Vec::new(),
};
let mut items: Vec<String> = collect_used_items(&krate, "depcrate").into_iter().collect();
items.sort();
assert_eq!(items, vec!["Foo", "bar", "other"]);
}
#[test]
fn is_heavy_dependency_requires_both_thresholds_to_be_crossed() {
assert!(!is_heavy_dependency(
HEAVY_DEPENDENCY_TRANSITIVE_THRESHOLD,
0
));
assert!(!is_heavy_dependency(
HEAVY_DEPENDENCY_TRANSITIVE_THRESHOLD + 1,
HEAVY_DEPENDENCY_USED_ITEMS_THRESHOLD
));
assert!(is_heavy_dependency(
HEAVY_DEPENDENCY_TRANSITIVE_THRESHOLD + 1,
HEAVY_DEPENDENCY_USED_ITEMS_THRESHOLD - 1
));
}
#[test]
fn heavy_dependency_finding_is_advisory_only() {
let krate = CrateInfo {
name: "fixture".to_string(),
version: "0.1.0".to_string(),
manifest_path: PathBuf::from("fixture/Cargo.toml"),
root: PathBuf::from("fixture"),
source_files: Vec::new(),
entry_points: Vec::new(),
dependencies: Vec::new(),
};
let dep = crate::ingest::DeclaredDependency {
name: "heavy_crate".to_string(),
kind: DependencyKind::Normal,
code_identifier: "heavy_crate".to_string(),
target: None,
features: Vec::new(),
version_req: "*".to_string(),
};
let used_items: HashSet<String> = HashSet::new();
let finding = heavy_dependency_finding(&krate, &dep, 42, &used_items);
assert_eq!(finding.rule, HEAVY_DEPENDENCY_RULE);
assert_eq!(finding.severity, Severity::Info);
assert_eq!(finding.evidence_class, EvidenceClass::Heuristic);
assert!(!finding.is_gating());
assert_eq!(finding.evidence.unwrap()["transitive_deps"], 42);
}
#[test]
fn repository_is_missing_treats_absent_and_blank_as_missing() {
assert!(repository_is_missing(&None));
assert!(repository_is_missing(&Some(String::new())));
assert!(repository_is_missing(&Some(" ".to_string())));
assert!(!repository_is_missing(&Some(
"https://example.com/repo".to_string()
)));
}
#[test]
fn dep_without_repo_finding_is_gating_info() {
let krate = CrateInfo {
name: "fixture".to_string(),
version: "0.1.0".to_string(),
manifest_path: PathBuf::from("fixture/Cargo.toml"),
root: PathBuf::from("fixture"),
source_files: Vec::new(),
entry_points: Vec::new(),
dependencies: Vec::new(),
};
let dep = crate::ingest::DeclaredDependency {
name: "norepo_crate".to_string(),
kind: DependencyKind::Normal,
code_identifier: "norepo_crate".to_string(),
target: None,
features: Vec::new(),
version_req: "*".to_string(),
};
let finding = dep_without_repo_finding(&krate, &dep);
assert_eq!(finding.rule, DEP_WITHOUT_REPO_RULE);
assert_eq!(finding.severity, Severity::Info);
assert_eq!(finding.evidence_class, EvidenceClass::DerivedFact);
assert!(finding.is_gating());
assert_eq!(finding.location.item_path, "norepo_crate");
}
#[test]
fn a_dependency_without_a_repository_field_is_flagged_end_to_end() {
let dir = TempDir::new("deps-dep-without-repo");
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
r#"
[package]
name = "fixture"
version = "0.1.0"
edition = "2021"
[dependencies]
norepo = { path = "../dep_crate" }
"#,
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
r#"
[package]
name = "norepo"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
let workspace = crate::ingest::load(Some(&dir.join("main/Cargo.toml"))).unwrap();
let report = analyze_workspace(&workspace);
let finding = report
.findings
.iter()
.find(|f| f.rule == DEP_WITHOUT_REPO_RULE)
.expect("expected a dep-without-repo finding");
assert_eq!(finding.severity, Severity::Info);
assert_eq!(finding.evidence_class, EvidenceClass::DerivedFact);
assert_eq!(finding.location.item_path, "norepo");
}
#[test]
fn a_dependency_with_a_repository_field_is_not_flagged_end_to_end() {
let dir = TempDir::new("deps-dep-with-repo");
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
r#"
[package]
name = "fixture"
version = "0.1.0"
edition = "2021"
[dependencies]
hasrepo = { path = "../dep_crate" }
"#,
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
r#"
[package]
name = "hasrepo"
version = "0.1.0"
edition = "2021"
repository = "https://example.com/hasrepo"
"#,
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
let workspace = crate::ingest::load(Some(&dir.join("main/Cargo.toml"))).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == DEP_WITHOUT_REPO_RULE)
);
}
#[test]
fn extract_unused_crate_name_parses_the_first_backtick_quoted_name() {
assert_eq!(
extract_unused_crate_name("extern crate `depcrate` is unused in crate `fixture`"),
Some("depcrate".to_string())
);
assert_eq!(extract_unused_crate_name("some unrelated warning"), None);
}
#[test]
fn target_identity_joins_kind_and_name() {
let json = serde_json::json!({
"name": "it",
"kind": ["test"],
"crate_types": ["bin"],
"required-features": [],
"src_path": "tests/it.rs",
"edition": "2021",
"doc": false,
"doctest": false,
"test": true,
});
let target: cargo_metadata::Target = serde_json::from_value(json).unwrap();
assert_eq!(target_identity(&target), "test:it");
}
#[test]
fn rustc_lint_import_flags_a_normal_dependency_never_referenced_anywhere() {
let dir = TempDir::new("deps-rustc-lint-unused");
let manifest = write_fixture(&dir, "[dependencies]", "depcrate", None, &[], &[]);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_rustc_unused_dependencies(&workspace);
assert!(
report.errors.is_empty(),
"unexpected errors: {:?}",
report.errors
);
let finding = report
.findings
.iter()
.find(|f| f.rule == UNUSED_DEPENDENCY_RULE)
.expect("expected an unused-dependency finding");
assert_eq!(finding.severity, Severity::Warn);
assert_eq!(finding.evidence_class, EvidenceClass::BoundedSemantic);
assert_eq!(finding.location.item_path, "depcrate");
let evidence = finding.evidence.as_ref().unwrap();
assert_eq!(evidence["source"], "rustc:unused_crate_dependencies");
assert_eq!(evidence["package"], "fixture");
assert!(
evidence["reason"]
.as_str()
.unwrap()
.contains("unused_crate_dependencies")
);
}
#[test]
fn rustc_lint_import_does_not_flag_a_dependency_used_in_every_target() {
let dir = TempDir::new("deps-rustc-lint-used-everywhere");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[
("src/lib.rs", "pub fn hello() { depcrate::noop(); }\n"),
("tests/it.rs", "fn t() { depcrate::noop(); }\n"),
],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_rustc_unused_dependencies(&workspace);
assert!(
report.errors.is_empty(),
"unexpected errors: {:?}",
report.errors
);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_DEPENDENCY_RULE)
);
}
#[test]
fn rustc_lint_import_does_not_flag_a_dependency_used_by_only_one_target() {
let dir = TempDir::new("deps-rustc-lint-single-target");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[("tests/it.rs", "fn t() { depcrate::noop(); }\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_rustc_unused_dependencies(&workspace);
assert!(
report.errors.is_empty(),
"unexpected errors: {:?}",
report.errors
);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_DEPENDENCY_RULE)
);
}
#[test]
fn a_genuine_compile_error_surfaces_as_a_report_error_not_as_zero_findings() {
let dir = TempDir::new("deps-rustc-lint-compile-error");
let manifest = write_fixture(
&dir,
"[dependencies]",
"depcrate",
None,
&[],
&[("src/lib.rs", "this is not valid rust syntax {{{\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_rustc_unused_dependencies(&workspace);
assert!(
report.findings.is_empty(),
"unexpected findings from an unbuildable workspace: {:?}",
report.findings
);
assert_eq!(report.errors.len(), 1);
assert!(matches!(report.errors[0], DepsError::RustcCheck(_)));
}
fn write_crate_with_features(
dir: &TempDir,
features_block: &str,
files: &[(&str, &str)],
) -> PathBuf {
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
format!(
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n{features_block}\n"
),
)
.unwrap();
std::fs::write(dir.join("src/lib.rs"), "pub fn hello() {}\n").unwrap();
for (relative, content) in files {
let file_path = dir.join(relative);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(file_path, content).unwrap();
}
dir.join("Cargo.toml")
}
#[test]
fn unused_feature_fires_for_a_declared_feature_with_no_cfg_reference() {
let dir = TempDir::new("deps-unused-feature-fires");
let manifest = write_crate_with_features(
&dir,
"[features]\nfancy = []\n",
&[("src/lib.rs", "pub fn hello() {}\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == UNUSED_FEATURE_RULE)
.collect();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].location.item_path, "fancy");
assert_eq!(hits[0].severity, Severity::Warn);
assert_eq!(hits[0].evidence_class, EvidenceClass::DerivedFact);
assert!(hits[0].is_gating());
}
#[test]
fn unused_feature_does_not_fire_when_a_cfg_attribute_references_it() {
let dir = TempDir::new("deps-unused-feature-cfg-attr");
let manifest = write_crate_with_features(
&dir,
"[features]\nfancy = []\n",
&[(
"src/lib.rs",
"#[cfg(feature = \"fancy\")]\npub fn fancy_hello() {}\n",
)],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_FEATURE_RULE),
"unexpected findings: {:?}",
report.findings
);
}
#[test]
fn unused_feature_does_not_fire_when_a_cfg_macro_call_references_it() {
let dir = TempDir::new("deps-unused-feature-cfg-macro");
let manifest = write_crate_with_features(
&dir,
"[features]\nfancy = []\n",
&[(
"src/lib.rs",
"pub fn hello() {\n if cfg!(feature = \"fancy\") {}\n}\n",
)],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_FEATURE_RULE)
);
}
#[test]
fn unused_feature_never_fires_for_default() {
let dir = TempDir::new("deps-unused-feature-default");
let manifest = write_crate_with_features(
&dir,
"[features]\ndefault = []\n",
&[("src/lib.rs", "pub fn hello() {}\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_FEATURE_RULE)
);
}
#[test]
fn unused_feature_does_not_fire_for_an_umbrella_feature_with_implications() {
let dir = TempDir::new("deps-unused-feature-umbrella");
let manifest = write_crate_with_features(
&dir,
"[features]\nfancy = []\nfull = [\"fancy\"]\n",
&[("src/lib.rs", "#[cfg(feature = \"fancy\")]\npub fn f() {}\n")],
);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
assert!(
!report
.findings
.iter()
.any(|f| f.rule == UNUSED_FEATURE_RULE)
);
}
#[test]
fn declared_features_without_implications_excludes_default_and_umbrella_features() {
let manifest: toml::Value =
toml::from_str("[features]\ndefault = []\nfancy = []\nfull = [\"fancy\"]\n").unwrap();
let names = declared_features_without_implications(&manifest);
assert_eq!(names, vec!["fancy".to_string()]);
}
#[test]
fn declared_features_without_implications_is_empty_without_a_features_table() {
let manifest: toml::Value =
toml::from_str("[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n").unwrap();
assert!(declared_features_without_implications(&manifest).is_empty());
}
#[test]
fn misplaced_dependency_kind_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(MISPLACED_DEPENDENCY_KIND_RULE)
.expect("misplaced-dependency-kind has a registry entry")
.example
.expect("misplaced-dependency-kind has a curated example")
.before;
let manifest: toml::Value = toml::from_str(example).unwrap();
let dep_name = manifest
.get("dependencies")
.and_then(toml::Value::as_table)
.and_then(|table| table.keys().next())
.expect("example declares a dependency")
.clone();
let code_identifier = dep_name.replace('-', "_");
let dir = TempDir::new("deps-misplaced-dependency-kind-registry-example");
let usage = format!("fn t() {{ {code_identifier}::noop(); }}\n");
let manifest_path = write_fixture(
&dir,
"[dependencies]",
&dep_name,
None,
&[],
&[("tests/it.rs", usage.as_str())],
);
let workspace = crate::ingest::load(Some(&manifest_path)).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == MISPLACED_DEPENDENCY_KIND_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn unused_dev_dependency_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(UNUSED_DEV_DEPENDENCY_RULE)
.expect("unused-dev-dependency has a registry entry")
.example
.expect("unused-dev-dependency has a curated example")
.before;
let manifest: toml::Value = toml::from_str(example).unwrap();
let dep_name = manifest
.get("dev-dependencies")
.and_then(toml::Value::as_table)
.and_then(|table| table.keys().next())
.expect("example declares a dev-dependency")
.clone();
let dir = TempDir::new("deps-unused-dev-dependency-registry-example");
let manifest_path = write_fixture(&dir, "[dev-dependencies]", &dep_name, None, &[], &[]);
let workspace = crate::ingest::load(Some(&manifest_path)).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == UNUSED_DEV_DEPENDENCY_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn heavy_dependency_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(HEAVY_DEPENDENCY_RULE)
.expect("heavy-dependency has a registry entry")
.example
.expect("heavy-dependency has a curated example")
.before;
let manifest: toml::Value = toml::from_str(example).unwrap();
let dep_name = manifest
.get("dependencies")
.and_then(toml::Value::as_table)
.and_then(|table| table.keys().next())
.expect("example declares a dependency")
.clone();
let dir = TempDir::new("deps-heavy-dependency-registry-example");
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
format!(
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n{dep_name} = {{ path = \"../{dep_name}\" }}\n"
),
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
let leaf_count = HEAVY_DEPENDENCY_TRANSITIVE_THRESHOLD + 1;
let mut leaf_deps_toml = String::new();
for i in 0..leaf_count {
let leaf_name = format!("{dep_name}-leaf-{i}");
std::fs::create_dir_all(dir.join(&leaf_name).join("src")).unwrap();
std::fs::write(
dir.join(&leaf_name).join("Cargo.toml"),
format!(
"[package]\nname = \"{leaf_name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"
),
)
.unwrap();
std::fs::write(
dir.join(&leaf_name).join("src/lib.rs"),
"pub fn noop() {}\n",
)
.unwrap();
leaf_deps_toml.push_str(&format!("{leaf_name} = {{ path = \"../{leaf_name}\" }}\n"));
}
std::fs::create_dir_all(dir.join(&dep_name).join("src")).unwrap();
std::fs::write(
dir.join(&dep_name).join("Cargo.toml"),
format!(
"[package]\nname = \"{dep_name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n{leaf_deps_toml}"
),
)
.unwrap();
std::fs::write(dir.join(&dep_name).join("src/lib.rs"), "pub fn noop() {}\n").unwrap();
let workspace = crate::ingest::load(Some(&dir.join("main/Cargo.toml"))).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == HEAVY_DEPENDENCY_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn unused_feature_flag_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(UNUSED_FEATURE_FLAG_RULE)
.expect("unused-feature-flag has a registry entry")
.example
.expect("unused-feature-flag has a curated example")
.before;
let manifest: toml::Value = toml::from_str(example).unwrap();
let deps_table = manifest
.get("dependencies")
.and_then(toml::Value::as_table)
.expect("example declares a dependencies table");
let (dep_name, dep_value) = deps_table
.iter()
.next()
.expect("example declares a dependency");
let feature = dep_value
.get("features")
.and_then(toml::Value::as_array)
.and_then(|features| features.first())
.and_then(toml::Value::as_str)
.expect("example declares a feature")
.to_string();
let dir = TempDir::new("deps-unused-feature-flag-registry-example");
let manifest_path = write_fixture(&dir, "[dependencies]", dep_name, None, &[&feature], &[]);
let workspace = crate::ingest::load(Some(&manifest_path)).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == UNUSED_FEATURE_FLAG_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn default_features_unused_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(DEFAULT_FEATURES_UNUSED_RULE)
.expect("default-features-unused has a registry entry")
.example
.expect("default-features-unused has a curated example")
.before;
let manifest: toml::Value = toml::from_str(example).unwrap();
let dep_name = manifest
.get("dependencies")
.and_then(toml::Value::as_table)
.and_then(|table| table.keys().next())
.expect("example declares a dependency")
.clone();
let dir = TempDir::new("deps-default-features-unused-registry-example");
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
format!(
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n{dep_name} = {{ path = \"../dep_crate\", default-features = true }}\n"
),
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
format!("[package]\nname = \"{dep_name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
let workspace = crate::ingest::load(Some(&dir.join("main/Cargo.toml"))).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == DEFAULT_FEATURES_UNUSED_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn unused_feature_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(UNUSED_FEATURE_RULE)
.expect("unused-feature has a registry entry")
.example
.expect("unused-feature has a curated example")
.before;
let dir = TempDir::new("deps-unused-feature-registry-example");
let manifest =
write_crate_with_features(&dir, example, &[("src/lib.rs", "pub fn hello() {}\n")]);
let workspace = crate::ingest::load(Some(&manifest)).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == UNUSED_FEATURE_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
#[test]
fn dep_without_repo_registry_example_still_triggers_the_rule() {
let example = crate::rule_registry::lookup(DEP_WITHOUT_REPO_RULE)
.expect("dep-without-repo has a registry entry")
.example
.expect("dep-without-repo has a curated example")
.before;
let manifest: toml::Value = toml::from_str(example).unwrap();
let dep_name = manifest
.get("package")
.and_then(|package| package.get("name"))
.and_then(toml::Value::as_str)
.expect("example declares a package name")
.to_string();
let dir = TempDir::new("deps-dep-without-repo-registry-example");
std::fs::create_dir_all(dir.join("main/src")).unwrap();
std::fs::create_dir_all(dir.join("dep_crate/src")).unwrap();
std::fs::write(
dir.join("main/Cargo.toml"),
format!(
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\n{dep_name} = {{ path = \"../dep_crate\" }}\n"
),
)
.unwrap();
std::fs::write(dir.join("main/src/lib.rs"), "pub fn hello() {}\n").unwrap();
std::fs::write(
dir.join("dep_crate/Cargo.toml"),
format!("{example}edition = \"2021\"\n"),
)
.unwrap();
std::fs::write(dir.join("dep_crate/src/lib.rs"), "pub fn noop() {}\n").unwrap();
let workspace = crate::ingest::load(Some(&dir.join("main/Cargo.toml"))).unwrap();
let report = analyze_workspace(&workspace);
let hits: Vec<_> = report
.findings
.iter()
.filter(|f| f.rule == DEP_WITHOUT_REPO_RULE)
.collect();
assert_eq!(hits.len(), 1);
}
}