dep_insight/
lib.rs

1//! # dep-insight
2//!
3//! purpose: help you understand and analyze dependencies in rust projects
4//!
5//! this library lets you scan cargo projects to find duplicates, security issues,
6//! license problems, and heavy dependencies. you can use it as a library or
7//! through the `cargo dep-insight` command line tool.
8//!
9//! ## example
10//!
11//! ```no_run
12//! use dep_insight::analyze_project;
13//!
14//! let report = analyze_project(".", false).expect("failed to analyze");
15//! println!("found {} dependencies!", report.summary.total_dependencies);
16//! ```
17
18pub mod analyzer;
19pub mod config;
20pub mod parser;
21pub mod report;
22pub mod risk;
23pub mod utils;
24pub mod visualize;
25
26pub use report::{DuplicateGroup, LicenseViolation, Report, Suggestion, Vulnerability};
27
28use anyhow::{Context, Result};
29use std::path::Path;
30
31/// purpose: analyze a rust project and return a full report
32/// params: path -> where the project lives on your computer, run_audit -> whether to check for vulnerabilities
33/// args: uses config from .depinsight.toml if it exists
34/// raise: returns error if the path is not a cargo project or we can't read files
35/// returns: a report with numbers and lists you can show to your friends
36///
37/// ## example
38///
39/// ```no_run
40/// # use dep_insight::analyze_project;
41/// let report = analyze_project("./my-project", false).expect("analysis failed");
42/// assert!(report.summary.total_dependencies > 0);
43/// ```
44pub fn analyze_project<P: AsRef<Path>>(path: P, run_audit: bool) -> Result<Report> {
45    analyze_project_with_config(path, None::<&Path>, run_audit)
46}
47
48/// purpose: analyze a rust project with a custom config file
49/// params: path -> project location, config_path -> optional custom config file, run_audit -> whether to check for vulnerabilities
50/// args: none
51/// raise: returns error if the path is not a cargo project or config is invalid
52/// returns: a report with analysis results
53///
54/// ## example
55///
56/// ```no_run
57/// # use dep_insight::analyze_project_with_config;
58/// let report = analyze_project_with_config("./my-project", Some("custom.toml"), false).expect("analysis failed");
59/// assert!(report.summary.total_dependencies > 0);
60/// ```
61pub fn analyze_project_with_config<P: AsRef<Path>, C: AsRef<Path>>(
62    path: P,
63    config_path: Option<C>,
64    run_audit: bool,
65) -> Result<Report> {
66    let path = path.as_ref();
67
68    // parse cargo metadata
69    let metadata =
70        parser::ProjectMetadata::load(path).context("failed to load project metadata")?;
71
72    // load config from custom path or workspace root
73    let config = if let Some(cfg_path) = config_path {
74        config::Config::load(cfg_path.as_ref()).context("failed to load custom config file")?
75    } else {
76        let workspace_root_path = metadata.metadata.workspace_root.as_std_path();
77        config::Config::discover(workspace_root_path)
78    };
79
80    // check for missing lockfile
81    if !metadata.has_lockfile() {
82        tracing::warn!(
83            "no Cargo.lock found! run 'cargo generate-lockfile' for more accurate results"
84        );
85    }
86
87    // build analyzer
88    let analyzer =
89        analyzer::DependencyAnalyzer::new(&metadata).context("failed to build dependency graph")?;
90
91    // create report
92    let workspace_root = metadata.metadata.workspace_root.to_string();
93    let mut report = Report::new(workspace_root);
94
95    // fill in summary
96    let unique_names = metadata.unique_crate_names();
97    report.summary.total_dependencies = analyzer.total_dependencies();
98    report.summary.unique_crates = unique_names.len();
99
100    // find duplicates
101    let duplicates = metadata.find_duplicates();
102    report.summary.duplicate_crates = duplicates.len();
103
104    for (name, versions) in duplicates {
105        report.diagnostics.duplicates.push(report::DuplicateGroup {
106            name: name.clone(),
107            versions,
108            edges: vec![], // simplified for now
109        });
110    }
111
112    // check licenses
113    report.diagnostics.licenses = risk::check_licenses(&metadata, &config.license);
114
115    // check security (only if requested and audit feature enabled)
116    report.diagnostics.vulnerabilities = if run_audit {
117        risk::check_vulnerabilities(&metadata, &config.audit).unwrap_or_else(|e| {
118            tracing::warn!("vulnerability check failed: {}", e);
119            vec![]
120        })
121    } else {
122        vec![]
123    };
124
125    // find heavy crates
126    report.diagnostics.heavy = analyzer.find_heavy_crates(config.output.max_heavy);
127
128    // build graph for report
129    report.graph = analyzer.to_report_graph();
130
131    // generate suggestions
132    if !report.diagnostics.duplicates.is_empty() {
133        report.suggestions.push(Suggestion {
134            kind: "unify".to_string(),
135            detail: "consider merging duplicate crate versions".to_string(),
136        });
137    }
138
139    Ok(report)
140}
141
142/// purpose: convert a report to json text
143/// params: report -> the report to convert
144/// args: none
145/// raise: error if serialization fails
146/// returns: json string you can save to a file
147///
148/// ## example
149///
150/// ```no_run
151/// # use dep_insight::{analyze_project, report_to_json};
152/// let report = analyze_project(".", false).expect("failed");
153/// let json = report_to_json(&report).expect("failed to serialize");
154/// println!("{}", json);
155/// ```
156pub fn report_to_json(report: &Report) -> Result<String> {
157    serde_json::to_string_pretty(report).context("failed to serialize report to json")
158}
159
160// re-export for convenience
161pub use report::LicenseViolation as LicenseFinding;