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;