Skip to main content

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}