cargo_ensure_no_default_features/lib.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! A cargo sub-command that ensures every dependency in a `Cargo.toml` file is declared
5//! with `default-features = false`.
6#![doc(
7 html_logo_url = "https://media.githubusercontent.com/media/microsoft/ox-tools/refs/heads/main/crates/cargo-ensure-no-default-features/logo.png"
8)]
9#![doc(
10 html_favicon_url = "https://media.githubusercontent.com/media/microsoft/ox-tools/refs/heads/main/crates/cargo-ensure-no-default-features/favicon.ico"
11)]
12//!
13//! Enabling default features by accident pulls in code you never asked for, which inflates
14//! build times, binary size, and the dependency surface that must be audited. This tool
15//! makes that mistake a build break instead of a silent regression.
16//!
17//! If both `[workspace.dependencies]` and `[dependencies]` are present in the same
18//! manifest, both sections are checked. Dependencies that use `workspace = true` are
19//! skipped, since they inherit their settings from the workspace.
20//!
21//! # Usage
22//!
23//! Run this command in a cargo workspace or crate directory:
24//!
25//! ```bash
26//! cargo ensure-no-default-features
27//! ```
28//!
29//! The `--manifest-path` option lets you specify an explicit `Cargo.toml` file to check.
30//! Without this option, it defaults to the `Cargo.toml` in the current directory.
31//!
32//! The `--exceptions` (`-e`) option lets you specify a comma-separated list of dependencies
33//! to exclude from the `default-features` check. This is useful for dependencies that you
34//! explicitly want to have default features enabled.
35//!
36//! ```bash
37//! cargo ensure-no-default-features --manifest-path path/to/Cargo.toml --exceptions serde,tokio
38//! ```
39//!
40//! # Installation
41//!
42//! ```bash
43//! cargo install cargo-ensure-no-default-features
44//! ```
45//!
46//! # Example Output
47//!
48//! When offending dependencies are found:
49//!
50//! ```text
51//! Found 1 dependencies without default-features = false:
52//!
53//! - 'serde': missing default-features = false
54//! ```
55//!
56//! When everything checks out:
57//!
58//! ```text
59//! All required dependencies have default-features = false
60//! ```
61//!
62//! The tool exits with code 0 if all dependencies are well-formed, or code 1 otherwise.
63
64#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
65
66mod validation;
67
68use std::path::PathBuf;
69use std::process::ExitCode;
70
71use anyhow::{Context, Result};
72use clap::builder::Styles;
73use clap::builder::styling::{AnsiColor, Effects};
74use clap::{Parser, Subcommand};
75use validation::validate_dependencies;
76
77const CLAP_STYLES: Styles = Styles::styled()
78 .header(AnsiColor::Green.on_default().effects(Effects::BOLD))
79 .usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
80 .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
81 .placeholder(AnsiColor::Cyan.on_default());
82
83/// Cargo subcommand to ensure dependencies have `default-features = false`
84#[derive(Parser, Debug)]
85#[command(bin_name = "cargo", version, about, author)]
86#[command(styles = CLAP_STYLES)]
87struct Cli {
88 #[command(subcommand)]
89 command: Commands,
90}
91
92#[derive(Subcommand, Debug)]
93enum Commands {
94 /// Ensure all dependencies have `default-features = false`
95 #[command(version, display_name = "cargo-ensure-no-default-features")]
96 EnsureNoDefaultFeatures {
97 /// Path to Cargo.toml
98 #[arg(long, default_value = "Cargo.toml", value_name = "PATH")]
99 manifest_path: PathBuf,
100
101 /// List of dependencies to exclude from default-features check
102 #[arg(long, short = 'e', value_delimiter = ',')]
103 exceptions: Option<Vec<String>>,
104 },
105}
106
107/// Main entry point for the library, called from the binary crate.
108///
109/// Returns [`ExitCode::SUCCESS`] when every dependency is declared with
110/// `default-features = false` and [`ExitCode::FAILURE`] otherwise. Returning an
111/// exit code (rather than calling `std::process::exit`) lets `main` unwind
112/// normally so the process terminates through the standard runtime path --
113/// important under coverage instrumentation, where an abrupt `process::exit`
114/// skips the profile flush on some platforms (notably Windows).
115///
116/// # Errors
117///
118/// Returns an error if the manifest cannot be read or parsed, or if it contains
119/// no dependency section to check.
120pub fn run() -> Result<ExitCode> {
121 let cli = Cli::parse();
122 let Commands::EnsureNoDefaultFeatures { manifest_path, exceptions } = cli.command;
123
124 let content = std::fs::read_to_string(&manifest_path).with_context(|| format!("Failed to read {}", manifest_path.display()))?;
125 let exceptions = exceptions.unwrap_or_default();
126
127 let (errors, found_deps, checked_sections) = validate_dependencies(&content, &exceptions)?;
128 if !errors.is_empty() {
129 eprintln!("❌ Found {} dependencies without default-features = false:\n", errors.len());
130 for error in &errors {
131 eprintln!("{error}");
132 }
133
134 return Ok(ExitCode::FAILURE);
135 }
136
137 // Warn if any exception was not found in the dependencies
138 let sections_label = checked_sections.join(" or ");
139 for exception in &exceptions {
140 if !found_deps.contains(exception) {
141 eprintln!("⚠️ Warning: exception '{exception}' was not found in {sections_label}");
142 }
143 }
144
145 println!("✅ All required dependencies have default-features = false");
146
147 Ok(ExitCode::SUCCESS)
148}