cargo-shear 1.11.1

Detect and fix unused/misplaced dependencies from Cargo.toml
Documentation
//! Cargo.toml editing module for cargo-shear.
//!
//! This module provides functionality to safely remove unused dependencies
//! from Cargo.toml files while preserving formatting and other content.
//! It handles:
//!
//! - Package-level dependencies (`[dependencies]`, `[dev-dependencies]`, etc.)
//! - Workspace dependencies (`[workspace.dependencies]`)
//! - Target specific dependencies (`[target.'cfg(...)'.dependencies]`)
//! - Feature flags that reference removed dependencies

use rustc_hash::FxHashSet;
use toml_edit::{DocumentMut, Item, Table, value};

use crate::{
    manifest::DepLocation,
    package_processor::{MisplacedDependency, UnusedDependency, UnusedWorkspaceDependency},
};

const DEP_TABLE_KEYS: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];

/// Provides methods to edit Cargo.toml files and remove unused dependencies.
pub struct CargoTomlEditor;

impl CargoTomlEditor {
    /// Remove unused dependencies from a manifest.
    ///
    /// # Returns
    ///
    /// The number of dependencies removed.
    pub fn remove_dependencies(
        manifest: &mut DocumentMut,
        unused_deps: &[UnusedDependency],
    ) -> usize {
        let mut removed = FxHashSet::default();

        for dep in unused_deps {
            let success = match &dep.location {
                DepLocation::Root(table) => manifest
                    .get_mut(&table.to_string())
                    .and_then(|item| item.as_table_mut())
                    .and_then(|deps| deps.remove(dep.name.get_ref()))
                    .is_some(),

                DepLocation::Target { cfg, table } => manifest
                    .get_mut("target")
                    .and_then(|item| item.as_table_mut())
                    .and_then(|targets| targets.get_mut(cfg))
                    .and_then(|item| item.as_table_mut())
                    .and_then(|target| target.get_mut(&table.to_string()))
                    .and_then(|item| item.as_table_mut())
                    .and_then(|deps| deps.remove(dep.name.get_ref()))
                    .is_some(),
            };

            if success {
                removed.insert(dep.name.get_ref().as_str());
            }
        }

        let count = removed.len();
        Self::fix_features(manifest, &removed);
        Self::cleanup_empty_tables(manifest);
        count
    }

    /// Remove unused workspace dependencies from a manifest.
    ///
    /// # Returns
    ///
    /// The number of dependencies removed.
    pub fn remove_workspace_deps(
        manifest: &mut DocumentMut,
        unused_deps: &[UnusedWorkspaceDependency],
    ) -> usize {
        let mut removed = FxHashSet::default();

        for dep in unused_deps {
            let success = manifest
                .get_mut("workspace")
                .and_then(|item| item.as_table_mut())
                .and_then(|workspace| workspace.get_mut("dependencies"))
                .and_then(|item| item.as_table_mut())
                .and_then(|deps| deps.remove(dep.name.get_ref()))
                .is_some();

            if success {
                removed.insert(dep.name.get_ref().as_str());
            }
        }

        let count = removed.len();
        Self::fix_features(manifest, &removed);
        Self::cleanup_empty_tables(manifest);
        count
    }

    /// Move dependencies from `[dependencies]` to `[dev-dependencies]`.
    ///
    /// # Returns
    ///
    /// The number of dependencies moved.
    pub fn move_to_dev_dependencies(
        manifest: &mut DocumentMut,
        misplaced_deps: &[MisplacedDependency],
    ) -> usize {
        let mut count = 0;

        for dep in misplaced_deps {
            let success = match &dep.location {
                DepLocation::Root(_) => Self::move_root_to_dev(manifest, dep),
                DepLocation::Target { .. } => Self::move_target_to_dev(manifest, dep),
            };

            if success {
                count += 1;
            }
        }

        Self::cleanup_empty_tables(manifest);
        count
    }

    fn move_root_to_dev(manifest: &mut DocumentMut, dep: &MisplacedDependency) -> bool {
        // Remove from `[dependencies]`
        let Some(value) = manifest
            .get_mut("dependencies")
            .and_then(|item| item.as_table_mut())
            .and_then(|deps| deps.remove(dep.name.get_ref()))
        else {
            return false;
        };

        // Ensure `[dev-dependencies]` exists
        if !manifest.contains_key("dev-dependencies") {
            manifest["dev-dependencies"] = Item::Table(Table::new());
        }

        // Insert into `[dev-dependencies]`
        if let Some(dev_deps) =
            manifest.get_mut("dev-dependencies").and_then(|item| item.as_table_mut())
        {
            dev_deps.insert(dep.name.get_ref(), value);
            return true;
        }

        false
    }

    fn move_target_to_dev(manifest: &mut DocumentMut, dep: &MisplacedDependency) -> bool {
        let DepLocation::Target { cfg, .. } = &dep.location else {
            return false;
        };

        let Some(target) = manifest
            .get_mut("target")
            .and_then(|item| item.as_table_mut())
            .and_then(|targets| targets.get_mut(cfg))
            .and_then(|item| item.as_table_mut())
        else {
            return false;
        };

        // Remove from `[target.'cfg(...)'.dependencies]`
        let Some(value) = target
            .get_mut("dependencies")
            .and_then(|item| item.as_table_mut())
            .and_then(|deps| deps.remove(dep.name.get_ref()))
        else {
            return false;
        };

        // Ensure `[target.'cfg(...)'.dev-dependencies]` exists
        if !target.contains_key("dev-dependencies") {
            target["dev-dependencies"] = Item::Table(Table::new());
        }

        // Insert into `[target.'cfg(...)'.dev-dependencies]`
        if let Some(dev_deps) =
            target.get_mut("dev-dependencies").and_then(|item| item.as_table_mut())
        {
            dev_deps.insert(dep.name.get_ref(), value);
            return true;
        }

        false
    }

    /// Remove a flag (e.g. `test` or `doctest`) from the `[lib]` section.
    pub fn remove_lib_flag(manifest: &mut DocumentMut, flag: &str) -> bool {
        let removed = manifest
            .get_mut("lib")
            .and_then(|item| item.as_table_mut())
            .and_then(|table| table.remove(flag))
            .is_some();
        if removed {
            Self::cleanup_empty_tables(manifest);
        }
        removed
    }

    /// Set a flag to `false` in the `[lib]` section, creating the section if needed.
    pub fn set_lib_flag_false(manifest: &mut DocumentMut, flag: &str) {
        if !manifest.contains_key("lib") {
            manifest["lib"] = Item::Table(Table::new());
        }
        if let Some(table) = manifest.get_mut("lib").and_then(|item| item.as_table_mut()) {
            table[flag] = value(false);
        }
    }

    fn cleanup_empty_tables(manifest: &mut DocumentMut) {
        // Clean root-level dep tables
        for key in DEP_TABLE_KEYS {
            if manifest.get(key).and_then(|i| i.as_table()).is_some_and(Table::is_empty) {
                manifest.remove(key);
            }
        }

        // Clean target-specific tables (bottom-up)
        if let Some(targets) = manifest.get_mut("target").and_then(|i| i.as_table_mut()) {
            let target_keys: Vec<String> = targets.iter().map(|(k, _)| k.to_owned()).collect();
            for cfg_key in &target_keys {
                if let Some(target) = targets.get_mut(cfg_key).and_then(|i| i.as_table_mut()) {
                    for dep_key in DEP_TABLE_KEYS {
                        if target
                            .get(dep_key)
                            .and_then(|i| i.as_table())
                            .is_some_and(Table::is_empty)
                        {
                            target.remove(dep_key);
                        }
                    }
                }
                if targets.get(cfg_key).and_then(|i| i.as_table()).is_some_and(Table::is_empty) {
                    targets.remove(cfg_key);
                }
            }
        }
        if manifest.get("target").and_then(|i| i.as_table()).is_some_and(Table::is_empty) {
            manifest.remove("target");
        }

        // Clean workspace.dependencies
        if let Some(workspace) = manifest.get_mut("workspace").and_then(|i| i.as_table_mut())
            && workspace.get("dependencies").and_then(|i| i.as_table()).is_some_and(Table::is_empty)
        {
            workspace.remove("dependencies");
        }

        // Clean empty feature entries, then [features]
        if let Some(features) = manifest.get_mut("features").and_then(|i| i.as_table_mut()) {
            let keys: Vec<String> = features.iter().map(|(k, _)| k.to_owned()).collect();
            for key in &keys {
                if features
                    .get(key)
                    .and_then(|i| i.as_array())
                    .is_some_and(toml_edit::Array::is_empty)
                {
                    features.remove(key);
                }
            }
        }
        if manifest.get("features").and_then(|i| i.as_table()).is_some_and(Table::is_empty) {
            manifest.remove("features");
        }

        // Clean [lib]
        if manifest.get("lib").and_then(|i| i.as_table()).is_some_and(Table::is_empty) {
            manifest.remove("lib");
        }
    }

    fn fix_features(manifest: &mut DocumentMut, unused_deps: &FxHashSet<&str>) {
        let Some(features) = manifest.get_mut("features").and_then(|item| item.as_table_mut())
        else {
            return;
        };

        for (_, deps) in features.iter_mut() {
            let Some(list) = deps.as_array_mut() else {
                continue;
            };

            list.retain(|value| {
                let Some(value) = value.as_str() else {
                    return true;
                };

                let dep = value.strip_prefix("dep:").unwrap_or(value);
                !unused_deps.contains(dep)
            });
        }
    }
}