fob_graph/memory/
package_json.rs

1//! Package.json analysis methods for ModuleGraph.
2
3use rustc_hash::FxHashSet as HashSet;
4
5use super::super::package_json::{
6    DependencyCoverage, DependencyType, PackageJson, TypeCoverage, UnusedDependency,
7    extract_package_name,
8};
9use super::graph::ModuleGraph;
10use crate::Result;
11
12impl ModuleGraph {
13    /// Detect unused npm dependencies by cross-referencing package.json with imports.
14    ///
15    /// This identifies dependencies declared in package.json but never actually imported
16    /// in the codebase. Useful for cleaning up unused packages.
17    ///
18    /// # Parameters
19    ///
20    /// - `package_json`: Parsed package.json file
21    /// - `include_dev`: Whether to check devDependencies
22    /// - `include_peer`: Whether to check peerDependencies
23    pub fn unused_npm_dependencies(
24        &self,
25        package_json: &PackageJson,
26        include_dev: bool,
27        include_peer: bool,
28    ) -> Result<Vec<UnusedDependency>> {
29        let inner = self.inner.read();
30
31        // Collect all imported package names
32        let mut imported_packages = HashSet::default();
33        for module in inner.modules.values() {
34            for import in module.imports.iter() {
35                if import.is_external() {
36                    let package_name = extract_package_name(&import.source);
37                    imported_packages.insert(package_name.to_string());
38                }
39            }
40        }
41
42        let mut unused = Vec::new();
43
44        // Check each dependency type
45        let dep_types = [
46            (DependencyType::Production, true),
47            (DependencyType::Development, include_dev),
48            (DependencyType::Peer, include_peer),
49            (DependencyType::Optional, true),
50        ];
51
52        for (dep_type, should_check) in dep_types {
53            if !should_check {
54                continue;
55            }
56
57            for (package, version) in package_json.get_dependencies(dep_type) {
58                if !imported_packages.contains(package) {
59                    unused.push(UnusedDependency {
60                        package: package.clone(),
61                        version: version.clone(),
62                        dep_type,
63                    });
64                }
65            }
66        }
67
68        Ok(unused)
69    }
70
71    /// Get dependency coverage statistics.
72    ///
73    /// Provides detailed metrics about which dependencies are actually used
74    /// vs declared in package.json.
75    pub fn dependency_coverage(&self, package_json: &PackageJson) -> Result<DependencyCoverage> {
76        let inner = self.inner.read();
77
78        // Collect all imported package names
79        let mut imported_packages = HashSet::default();
80        for module in inner.modules.values() {
81            for import in module.imports.iter() {
82                if import.is_external() {
83                    let package_name = extract_package_name(&import.source);
84                    imported_packages.insert(package_name.to_string());
85                }
86            }
87        }
88
89        let mut by_type = std::collections::HashMap::new();
90        let mut total_declared = 0;
91        let mut total_used = 0;
92
93        for dep_type in [
94            DependencyType::Production,
95            DependencyType::Development,
96            DependencyType::Peer,
97            DependencyType::Optional,
98        ] {
99            let deps = package_json.get_dependencies(dep_type);
100            let declared = deps.len();
101            let used = deps
102                .keys()
103                .filter(|pkg| imported_packages.contains(*pkg))
104                .count();
105            let unused = declared - used;
106
107            total_declared += declared;
108            total_used += used;
109
110            by_type.insert(
111                dep_type,
112                TypeCoverage {
113                    declared,
114                    used,
115                    unused,
116                },
117            );
118        }
119
120        Ok(DependencyCoverage {
121            total_declared,
122            total_used,
123            total_unused: total_declared - total_used,
124            by_type,
125        })
126    }
127}