cargo-whys 0.1.0

A cargo subcommand that explains why dependencies are in your tree
Documentation
use anyhow::Result;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use walkdir::WalkDir;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageLocation {
    pub file: String,
    pub line: usize,
    pub content: String,
}

pub struct CodeScanner;

impl CodeScanner {
    pub fn scan_for_usage(
        project_path: &Path,
        dependency_name: &str,
    ) -> Result<Vec<UsageLocation>> {
        let mut locations = Vec::new();

        let use_pattern = format!(r"\buse\s+{}(?:::|;|\s)", regex::escape(dependency_name));
        let use_regex = Regex::new(&use_pattern)?;

        let extern_pattern = format!(r"\bextern\s+crate\s+{}\b", regex::escape(dependency_name));
        let extern_regex = Regex::new(&extern_pattern)?;

        for entry in WalkDir::new(project_path)
            .follow_links(true)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            let path = entry.path();

            if path.extension().and_then(|s| s.to_str()) != Some("rs") {
                continue;
            }

            if path.to_str().is_some_and(|s| s.contains("target/")) {
                continue;
            }

            if let Ok(content) = fs::read_to_string(path) {
                for (line_num, line) in content.lines().enumerate() {
                    if use_regex.is_match(line) || extern_regex.is_match(line) {
                        locations.push(UsageLocation {
                            file: path.display().to_string(),
                            line: line_num + 1,
                            content: line.trim().to_string(),
                        });
                    }
                }
            }
        }

        Ok(locations)
    }
}