@@ -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) ;
0 commit comments