Skip to content

Commit f3e7b69

Browse files
committed
cargo-rail: adding 'unused dep' detection edge case handling; auto_remove functionality. Testing across real repos (vello, etc.)
1 parent 82727bc commit f3e7b69

File tree

7 files changed

+829
-12
lines changed

7 files changed

+829
-12
lines changed

src/cargo/manifest_analyzer.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ pub struct DepUsage {
114114
/// The key name used in Cargo.toml (alias if renamed, otherwise package name)
115115
/// Used when generating manifest edits to target the correct dependency entry
116116
pub cargo_toml_key: String,
117+
/// Whether this optional dep is referenced in the `[features]` table
118+
/// True if the dep appears as: `dep:name`, `name` (for optional deps), or `name/feat`
119+
/// Used to distinguish truly unused optional deps from feature-gated ones
120+
pub referenced_in_features: bool,
117121
}
118122

119123
/// Parsed dependency table info (used internally during manifest parsing)
@@ -294,17 +298,42 @@ impl ManifestAnalyzer {
294298
}
295299
}
296300

297-
// Parse [features] table to find conditional feature references
301+
// Parse [features] table to find conditional feature references and dep activations
302+
// This detects three patterns that reference dependencies:
303+
// 1. "dep:name" - explicit dep activation (Rust 2021+)
304+
// 2. "name" - implicit optional dep activation (when name matches an optional dep)
305+
// 3. "name/feat" - dep with specific feature enabled
298306
if let Some(features_table) = doc.get("features").and_then(|f| f.as_table()) {
299307
for (_feature_name, feature_value) in features_table {
300308
if let Some(feature_list) = feature_value.as_array() {
301309
for item in feature_list {
302310
if let Some(s) = item.as_str() {
303-
// Check for dep/feature syntax
304-
if let Some((dep, feat)) = s.split_once('/') {
305-
let dep_key = DepKey::new(dep);
311+
// Pattern 1: "dep:name" - explicit dep reference (Rust 2021+)
312+
if let Some(dep_name) = s.strip_prefix("dep:") {
313+
let dep_key = DepKey::new(dep_name);
314+
if let Some(usage) = dependencies.get_mut(&dep_key) {
315+
usage.referenced_in_features = true;
316+
}
317+
}
318+
// Pattern 2: "name/feat" - dep with feature
319+
else if let Some((dep, feat)) = s.split_once('/') {
320+
// Strip optional "dep:" prefix from the dep part
321+
let dep_name = dep.strip_prefix("dep:").unwrap_or(dep);
322+
let dep_key = DepKey::new(dep_name);
306323
if let Some(usage) = dependencies.get_mut(&dep_key) {
307324
usage.conditional_features.insert(feat.to_string());
325+
usage.referenced_in_features = true;
326+
}
327+
}
328+
// Pattern 3: bare "name" - check if it matches an optional dep
329+
else {
330+
let dep_key = DepKey::new(s);
331+
if let Some(usage) = dependencies.get_mut(&dep_key) {
332+
// Only count as referenced if the dep is optional
333+
// (non-optional deps with same name as features are already resolved)
334+
if usage.optional {
335+
usage.referenced_in_features = true;
336+
}
308337
}
309338
}
310339
}
@@ -364,7 +393,7 @@ impl ManifestAnalyzer {
364393
// which is the alias if renamed, or the package name otherwise
365394
let usage = DepUsage {
366395
unconditional_features: p.unconditional_features,
367-
conditional_features: BTreeSet::new(), // Filled in later
396+
conditional_features: BTreeSet::new(), // Filled in later by features parsing
368397
default_features: p.default_features,
369398
kind,
370399
target: target.clone(),
@@ -374,6 +403,7 @@ impl ManifestAnalyzer {
374403
declared_version: p.declared_version,
375404
manifest_path: Some(manifest_path.to_path_buf()),
376405
cargo_toml_key: dep_name.to_string(),
406+
referenced_in_features: false, // Filled in later by features parsing
377407
};
378408

379409
out.insert(dep_key, usage);

src/cargo/manifest_ops.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,40 @@ pub fn insert_target_dependency(
385385
Ok(())
386386
}
387387

388+
/// Remove a dependency from target-specific section
389+
///
390+
/// # Arguments
391+
///
392+
/// * `doc` - TOML document to modify
393+
/// * `target` - Target triple or cfg expression (e.g., "cfg(windows)")
394+
/// * `section` - Section name ("dependencies", "dev-dependencies", "build-dependencies")
395+
/// * `name` - Dependency name to remove
396+
pub fn remove_target_dependency(doc: &mut DocumentMut, target: &str, section: &str, name: &str) -> RailResult<()> {
397+
let path = format!("target.{}.{}", target, section);
398+
if let Some(target_section) = get_table_mut(doc, &path) {
399+
target_section.remove(name);
400+
}
401+
Ok(())
402+
}
403+
388404
/// Get target-specific dependencies section (mutable)
389405
fn get_target_section_mut<'a>(doc: &'a mut DocumentMut, target: &str, section: &str) -> RailResult<&'a mut Table> {
390406
let path = format!("target.{}.{}", target, section);
391407
get_or_create_table(doc, &path)
392408
}
393409

410+
/// Get a mutable reference to a table by path (returns None if not found)
411+
fn get_table_mut<'a>(doc: &'a mut DocumentMut, path: &str) -> Option<&'a mut Table> {
412+
let parts: Vec<&str> = path.split('.').collect();
413+
let mut current: &mut Item = doc.as_item_mut();
414+
415+
for part in parts {
416+
current = current.get_mut(part)?;
417+
}
418+
419+
current.as_table_mut()
420+
}
421+
394422
// ============================================================================
395423
// SECTION 5: Feature Array Operations
396424
// ============================================================================

src/cargo/manifest_writer.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,44 @@ impl ManifestWriter {
194194

195195
Ok(())
196196
}
197+
198+
/// Remove an unused dependency from a member's Cargo.toml
199+
///
200+
/// # Arguments
201+
///
202+
/// * `member_toml_path` - Path to the member's Cargo.toml
203+
/// * `dep_name` - Name of the dependency to remove
204+
/// * `dep_kind` - Type of dependency (Normal, Dev, Build)
205+
/// * `target` - Optional target platform constraint (e.g., "cfg(unix)")
206+
pub fn remove_dep(
207+
&self,
208+
member_toml_path: &Path,
209+
dep_name: &str,
210+
dep_kind: DepKind,
211+
target: Option<&str>,
212+
) -> RailResult<()> {
213+
// Read member Cargo.toml
214+
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
215+
216+
// Get section name from kind
217+
let kind_section = self.dep_kind_to_section(dep_kind);
218+
219+
// Handle target-specific vs regular sections
220+
if let Some(target_cfg) = target {
221+
// Target-specific: remove from [target.'cfg(...)'.dependencies]
222+
manifest_ops::remove_target_dependency(&mut doc, target_cfg, kind_section, dep_name)
223+
.context("Failed to remove target-specific dependency")?;
224+
} else {
225+
// Regular section: remove from [dependencies], [dev-dependencies], or [build-dependencies]
226+
if let Some(deps) = doc.get_mut(kind_section).and_then(|d| d.as_table_like_mut()) {
227+
deps.remove(dep_name);
228+
}
229+
}
230+
231+
// Format and write
232+
self.formatter.format_manifest(&mut doc)?;
233+
manifest_ops::write_toml_file(member_toml_path, &doc)?;
234+
235+
Ok(())
236+
}
197237
}

0 commit comments

Comments
 (0)