feat: Add lint for global use of `hint-mostly-unused` (#15995)
This PR adds a lint that checks for global use of `hint-mostly-unused`.
This also adds a "workspace lint pass", since we only want to lint on
the workspace's `[profile]`, as all other `[profile]` are ignored. This
new pass uses `[workspace.lints.cargo]` for configuration if
`[workspace]` exists, and `[lints]` if it doesn't[^1].
Note: This lint will get emitted if `-Zprofile-hint-mostly-unused` is
set, regardless of whether `-Zcargo-lints` is set.
CC @joshtriplett
[^1]: We can change the behavior in the future if it is not correct
diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs
index 096b333..fc20aa2 100644
--- a/src/cargo/core/workspace.rs
+++ b/src/cargo/core/workspace.rs
@@ -25,7 +25,9 @@
use crate::util::edit_distance;
use crate::util::errors::{CargoResult, ManifestError};
use crate::util::interning::InternedString;
-use crate::util::lints::{analyze_cargo_lints_table, check_im_a_teapot};
+use crate::util::lints::{
+ analyze_cargo_lints_table, blanket_hint_mostly_unused, check_im_a_teapot,
+};
use crate::util::toml::{InheritableFields, read_manifest};
use crate::util::{
Filesystem, GlobalContext, IntoUrl, context::CargoResolverConfig, context::ConfigRelativePath,
@@ -409,10 +411,7 @@
}
pub fn profiles(&self) -> Option<&TomlProfiles> {
- match self.root_maybe() {
- MaybePackage::Package(p) => p.manifest().profiles(),
- MaybePackage::Virtual(vm) => vm.profiles(),
- }
+ self.root_maybe().profiles()
}
/// Returns the root path of this workspace.
@@ -907,10 +906,7 @@
/// Returns the unstable nightly-only features enabled via `cargo-features` in the manifest.
pub fn unstable_features(&self) -> &Features {
- match self.root_maybe() {
- MaybePackage::Package(p) => p.manifest().unstable_features(),
- MaybePackage::Virtual(vm) => vm.unstable_features(),
- }
+ self.root_maybe().unstable_features()
}
pub fn resolve_behavior(&self) -> ResolveBehavior {
@@ -1206,14 +1202,17 @@
pub fn emit_warnings(&self) -> CargoResult<()> {
let mut first_emitted_error = None;
+
+ if let Err(e) = self.emit_ws_lints() {
+ first_emitted_error = Some(e);
+ }
+
for (path, maybe_pkg) in &self.packages.packages {
if let MaybePackage::Package(pkg) = maybe_pkg {
- if self.gctx.cli_unstable().cargo_lints {
- if let Err(e) = self.emit_lints(pkg, &path)
- && first_emitted_error.is_none()
- {
- first_emitted_error = Some(e);
- }
+ if let Err(e) = self.emit_pkg_lints(pkg, &path)
+ && first_emitted_error.is_none()
+ {
+ first_emitted_error = Some(e);
}
}
let warnings = match maybe_pkg {
@@ -1248,7 +1247,7 @@
}
}
- pub fn emit_lints(&self, pkg: &Package, path: &Path) -> CargoResult<()> {
+ pub fn emit_pkg_lints(&self, pkg: &Package, path: &Path) -> CargoResult<()> {
let mut error_count = 0;
let toml_lints = pkg
.manifest()
@@ -1262,26 +1261,74 @@
.cloned()
.unwrap_or(manifest::TomlToolLints::default());
- let ws_contents = match self.root_maybe() {
- MaybePackage::Package(pkg) => pkg.manifest().contents(),
- MaybePackage::Virtual(v) => v.contents(),
- };
+ let ws_contents = self.root_maybe().contents();
- let ws_document = match self.root_maybe() {
- MaybePackage::Package(pkg) => pkg.manifest().document(),
- MaybePackage::Virtual(v) => v.document(),
- };
+ let ws_document = self.root_maybe().document();
- analyze_cargo_lints_table(
- pkg,
- &path,
- &cargo_lints,
- ws_contents,
- ws_document,
- self.root_manifest(),
- self.gctx,
- )?;
- check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?;
+ if self.gctx.cli_unstable().cargo_lints {
+ analyze_cargo_lints_table(
+ pkg,
+ &path,
+ &cargo_lints,
+ ws_contents,
+ ws_document,
+ self.root_manifest(),
+ self.gctx,
+ )?;
+ check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?;
+ }
+
+ if error_count > 0 {
+ Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
+ "encountered {error_count} errors(s) while running lints"
+ ))
+ .into())
+ } else {
+ Ok(())
+ }
+ }
+
+ pub fn emit_ws_lints(&self) -> CargoResult<()> {
+ let mut error_count = 0;
+
+ let cargo_lints = match self.root_maybe() {
+ MaybePackage::Package(pkg) => {
+ let toml = pkg.manifest().normalized_toml();
+ if let Some(ws) = &toml.workspace {
+ ws.lints.as_ref()
+ } else {
+ toml.lints.as_ref().map(|l| &l.lints)
+ }
+ }
+ MaybePackage::Virtual(vm) => vm
+ .normalized_toml()
+ .workspace
+ .as_ref()
+ .unwrap()
+ .lints
+ .as_ref(),
+ }
+ .and_then(|t| t.get("cargo"))
+ .cloned()
+ .unwrap_or(manifest::TomlToolLints::default());
+
+ if self.gctx.cli_unstable().cargo_lints {
+ // Calls to lint functions go in here
+ }
+
+ // This is a short term hack to allow `blanket_hint_mostly_unused`
+ // to run without requiring `-Zcargo-lints`, which should hopefully
+ // improve the testing expierience while we are collecting feedback
+ if self.gctx.cli_unstable().profile_hint_mostly_unused {
+ blanket_hint_mostly_unused(
+ self.root_maybe(),
+ self.root_manifest(),
+ &cargo_lints,
+ &mut error_count,
+ self.gctx,
+ )?;
+ }
+
if error_count > 0 {
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
"encountered {error_count} errors(s) while running lints"
@@ -1888,6 +1935,41 @@
MaybePackage::Virtual(_) => false,
}
}
+
+ pub fn contents(&self) -> &str {
+ match self {
+ MaybePackage::Package(p) => p.manifest().contents(),
+ MaybePackage::Virtual(v) => v.contents(),
+ }
+ }
+
+ pub fn document(&self) -> &toml::Spanned<toml::de::DeTable<'static>> {
+ match self {
+ MaybePackage::Package(p) => p.manifest().document(),
+ MaybePackage::Virtual(v) => v.document(),
+ }
+ }
+
+ pub fn edition(&self) -> Edition {
+ match self {
+ MaybePackage::Package(p) => p.manifest().edition(),
+ MaybePackage::Virtual(_) => Edition::default(),
+ }
+ }
+
+ pub fn profiles(&self) -> Option<&TomlProfiles> {
+ match self {
+ MaybePackage::Package(p) => p.manifest().profiles(),
+ MaybePackage::Virtual(v) => v.profiles(),
+ }
+ }
+
+ pub fn unstable_features(&self) -> &Features {
+ match self {
+ MaybePackage::Package(p) => p.manifest().unstable_features(),
+ MaybePackage::Virtual(vm) => vm.unstable_features(),
+ }
+ }
}
impl WorkspaceRootConfig {
diff --git a/src/cargo/util/lints.rs b/src/cargo/util/lints.rs
index 0763be7..ca43ff0 100644
--- a/src/cargo/util/lints.rs
+++ b/src/cargo/util/lints.rs
@@ -1,14 +1,14 @@
-use crate::core::{Edition, Feature, Features, Manifest, Package};
+use crate::core::{Edition, Feature, Features, Manifest, MaybePackage, Package};
use crate::{CargoResult, GlobalContext};
-use annotate_snippets::{AnnotationKind, Group, Level, Snippet};
-use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints};
+use annotate_snippets::{AnnotationKind, Group, Level, Patch, Snippet};
+use cargo_util_schemas::manifest::{ProfilePackageSpec, TomlLintLevel, TomlToolLints};
use pathdiff::diff_paths;
use std::fmt::Display;
use std::ops::Range;
use std::path::Path;
const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
-pub const LINTS: &[Lint] = &[IM_A_TEAPOT, UNKNOWN_LINTS];
+pub const LINTS: &[Lint] = &[BLANKET_HINT_MOSTLY_UNUSED, IM_A_TEAPOT, UNKNOWN_LINTS];
pub fn analyze_cargo_lints_table(
pkg: &Package,
@@ -473,6 +473,158 @@
Ok(())
}
+const BLANKET_HINT_MOSTLY_UNUSED: Lint = Lint {
+ name: "blanket_hint_mostly_unused",
+ desc: "blanket_hint_mostly_unused lint",
+ groups: &[],
+ default_level: LintLevel::Warn,
+ edition_lint_opts: None,
+ feature_gate: None,
+ docs: Some(
+ r#"
+### What it does
+Checks if `hint-mostly-unused` being applied to all dependencies.
+
+### Why it is bad
+`hint-mostly-unused` indicates that most of a crate's API surface will go
+unused by anything depending on it; this hint can speed up the build by
+attempting to minimize compilation time for items that aren't used at all.
+Misapplication to crates that don't fit that criteria will slow down the build
+rather than speeding it up. It should be selectively applied to dependencies
+that meet these criteria. Applying it globally is always a misapplication and
+will likely slow down the build.
+
+### Example
+```toml
+[profile.dev.package."*"]
+hint-mostly-unused = true
+```
+
+Should instead be:
+```toml
+[profile.dev.package.huge-mostly-unused-dependency]
+hint-mostly-unused = true
+```
+"#,
+ ),
+};
+
+pub fn blanket_hint_mostly_unused(
+ maybe_pkg: &MaybePackage,
+ path: &Path,
+ pkg_lints: &TomlToolLints,
+ error_count: &mut usize,
+ gctx: &GlobalContext,
+) -> CargoResult<()> {
+ let (lint_level, reason) = BLANKET_HINT_MOSTLY_UNUSED.level(
+ pkg_lints,
+ maybe_pkg.edition(),
+ maybe_pkg.unstable_features(),
+ );
+
+ if lint_level == LintLevel::Allow {
+ return Ok(());
+ }
+
+ let level = lint_level.to_diagnostic_level();
+ let manifest_path = rel_cwd_manifest_path(path, gctx);
+ let mut paths = Vec::new();
+
+ if let Some(profiles) = maybe_pkg.profiles() {
+ for (profile_name, top_level_profile) in &profiles.0 {
+ if let Some(true) = top_level_profile.hint_mostly_unused {
+ paths.push((
+ vec!["profile", profile_name.as_str(), "hint-mostly-unused"],
+ true,
+ ));
+ }
+
+ if let Some(build_override) = &top_level_profile.build_override
+ && let Some(true) = build_override.hint_mostly_unused
+ {
+ paths.push((
+ vec![
+ "profile",
+ profile_name.as_str(),
+ "build-override",
+ "hint-mostly-unused",
+ ],
+ false,
+ ));
+ }
+
+ if let Some(packages) = &top_level_profile.package
+ && let Some(profile) = packages.get(&ProfilePackageSpec::All)
+ && let Some(true) = profile.hint_mostly_unused
+ {
+ paths.push((
+ vec![
+ "profile",
+ profile_name.as_str(),
+ "package",
+ "*",
+ "hint-mostly-unused",
+ ],
+ false,
+ ));
+ }
+ }
+ }
+
+ for (i, (path, show_per_pkg_suggestion)) in paths.iter().enumerate() {
+ if lint_level.is_error() {
+ *error_count += 1;
+ }
+ let title = "`hint-mostly-unused` is being blanket applied to all dependencies";
+ let help_txt =
+ "scope `hint-mostly-unused` to specific packages with a lot of unused object code";
+ if let (Some(span), Some(table_span)) = (
+ get_key_value_span(maybe_pkg.document(), &path),
+ get_key_value_span(maybe_pkg.document(), &path[..path.len() - 1]),
+ ) {
+ let mut report = Vec::new();
+ let mut primary_group = level.clone().primary_title(title).element(
+ Snippet::source(maybe_pkg.contents())
+ .path(&manifest_path)
+ .annotation(
+ AnnotationKind::Primary.span(table_span.key.start..table_span.key.end),
+ )
+ .annotation(AnnotationKind::Context.span(span.key.start..span.value.end)),
+ );
+
+ if *show_per_pkg_suggestion {
+ report.push(
+ Level::HELP.secondary_title(help_txt).element(
+ Snippet::source(maybe_pkg.contents())
+ .path(&manifest_path)
+ .patch(Patch::new(
+ table_span.key.end..table_span.key.end,
+ ".package.<pkg_name>",
+ )),
+ ),
+ );
+ } else {
+ primary_group = primary_group.element(Level::HELP.message(help_txt));
+ }
+
+ if i == 0 {
+ primary_group =
+ primary_group
+ .element(Level::NOTE.message(
+ BLANKET_HINT_MOSTLY_UNUSED.emitted_source(lint_level, reason),
+ ));
+ }
+
+ // The primary group should always be first
+ report.insert(0, primary_group);
+
+ gctx.shell().print_report(&report, lint_level.force())?;
+ }
+ }
+
+ Ok(())
+}
+
const UNKNOWN_LINTS: Lint = Lint {
name: "unknown_lints",
desc: "unknown lint",
diff --git a/src/doc/src/reference/lints.md b/src/doc/src/reference/lints.md
index d1f1244..8393efb 100644
--- a/src/doc/src/reference/lints.md
+++ b/src/doc/src/reference/lints.md
@@ -5,8 +5,37 @@
## Warn-by-default
These lints are all set to the 'warn' level by default.
+- [`blanket_hint_mostly_unused`](#blanket_hint_mostly_unused)
- [`unknown_lints`](#unknown_lints)
+## `blanket_hint_mostly_unused`
+Set to `warn` by default
+
+### What it does
+Checks if `hint-mostly-unused` being applied to all dependencies.
+
+### Why it is bad
+`hint-mostly-unused` indicates that most of a crate's API surface will go
+unused by anything depending on it; this hint can speed up the build by
+attempting to minimize compilation time for items that aren't used at all.
+Misapplication to crates that don't fit that criteria will slow down the build
+rather than speeding it up. It should be selectively applied to dependencies
+that meet these criteria. Applying it globally is always a misapplication and
+will likely slow down the build.
+
+### Example
+```toml
+[profile.dev.package."*"]
+hint-mostly-unused = true
+```
+
+Should instead be:
+```toml
+[profile.dev.package.huge-mostly-unused-dependency]
+hint-mostly-unused = true
+```
+
+
## `unknown_lints`
Set to `warn` by default
diff --git a/tests/testsuite/lints/blanket_hint_mostly_unused.rs b/tests/testsuite/lints/blanket_hint_mostly_unused.rs
new file mode 100644
index 0000000..18e55a8
--- /dev/null
+++ b/tests/testsuite/lints/blanket_hint_mostly_unused.rs
@@ -0,0 +1,167 @@
+use crate::prelude::*;
+use cargo_test_support::project;
+use cargo_test_support::str;
+
+#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")]
+fn named_profile_blanket() {
+ let p = project()
+ .file(
+ "Cargo.toml",
+ r#"
+[package]
+name = "foo"
+version = "0.0.1"
+edition = "2015"
+
+[profile.dev]
+hint-mostly-unused = true
+"#,
+ )
+ .file("src/main.rs", "fn main() {}")
+ .build();
+ p.cargo("check -Zprofile-hint-mostly-unused -v")
+ .masquerade_as_nightly_cargo(&["profile-hint-mostly-unused", "cargo-lints"])
+ .with_stderr_data(str![[r#"
+[WARNING] `hint-mostly-unused` is being blanket applied to all dependencies
+ --> Cargo.toml:7:10
+ |
+7 | [profile.dev]
+ | ^^^
+8 | hint-mostly-unused = true
+ | -------------------------
+ |
+ = [NOTE] `cargo::blanket_hint_mostly_unused` is set to `warn` by default
+[HELP] scope `hint-mostly-unused` to specific packages with a lot of unused object code
+ |
+7 | [profile.dev.package.<pkg_name>]
+ | +++++++++++++++++++
+[CHECKING] foo v0.0.1 ([ROOT]/foo)
+[RUNNING] `rustc --crate-name foo [..]`
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]])
+ .run();
+}
+
+#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")]
+fn profile_package_wildcard() {
+ let p = project()
+ .file(
+ "Cargo.toml",
+ r#"
+[package]
+name = "foo"
+version = "0.0.1"
+edition = "2015"
+
+[profile.dev.package."*"]
+hint-mostly-unused = true
+"#,
+ )
+ .file("src/main.rs", "fn main() {}")
+ .build();
+ p.cargo("check -Zprofile-hint-mostly-unused -v")
+ .masquerade_as_nightly_cargo(&["profile-hint-mostly-unused", "cargo-lints"])
+ .with_stderr_data(str![[r#"
+[WARNING] `hint-mostly-unused` is being blanket applied to all dependencies
+ --> Cargo.toml:7:22
+ |
+7 | [profile.dev.package."*"]
+ | ^^^
+8 | hint-mostly-unused = true
+ | -------------------------
+ |
+ = [HELP] scope `hint-mostly-unused` to specific packages with a lot of unused object code
+ = [NOTE] `cargo::blanket_hint_mostly_unused` is set to `warn` by default
+[CHECKING] foo v0.0.1 ([ROOT]/foo)
+[RUNNING] `rustc --crate-name foo [..]`
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]])
+ .run();
+}
+
+#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")]
+fn profile_build_override() {
+ let p = project()
+ .file(
+ "Cargo.toml",
+ r#"
+[package]
+name = "foo"
+version = "0.0.1"
+edition = "2015"
+
+[profile.dev.build-override]
+hint-mostly-unused = true
+"#,
+ )
+ .file("src/main.rs", "fn main() {}")
+ .build();
+ p.cargo("check -Zprofile-hint-mostly-unused -v")
+ .masquerade_as_nightly_cargo(&["profile-hint-mostly-unused", "cargo-lints"])
+ .with_stderr_data(str![[r#"
+[WARNING] `hint-mostly-unused` is being blanket applied to all dependencies
+ --> Cargo.toml:7:14
+ |
+7 | [profile.dev.build-override]
+ | ^^^^^^^^^^^^^^
+8 | hint-mostly-unused = true
+ | -------------------------
+ |
+ = [HELP] scope `hint-mostly-unused` to specific packages with a lot of unused object code
+ = [NOTE] `cargo::blanket_hint_mostly_unused` is set to `warn` by default
+[CHECKING] foo v0.0.1 ([ROOT]/foo)
+[RUNNING] `rustc --crate-name foo [..]`
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]])
+ .run();
+}
+
+#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")]
+fn workspace_profile_package_wildcard() {
+ let p = project()
+ .file(
+ "Cargo.toml",
+ r#"
+[workspace]
+members = ["foo"]
+
+[profile.dev.package."*"]
+hint-mostly-unused = true
+"#,
+ )
+ .file(
+ "foo/Cargo.toml",
+ r#"
+[package]
+name = "foo"
+version = "0.0.1"
+edition = "2015"
+authors = []
+"#,
+ )
+ .file("foo/src/lib.rs", "")
+ .build();
+
+ p.cargo("check -Zprofile-hint-mostly-unused -v")
+ .masquerade_as_nightly_cargo(&["profile-hint-mostly-unused", "cargo-lints"])
+ .with_stderr_data(str![[r#"
+[WARNING] `hint-mostly-unused` is being blanket applied to all dependencies
+ --> Cargo.toml:5:22
+ |
+5 | [profile.dev.package."*"]
+ | ^^^
+6 | hint-mostly-unused = true
+ | -------------------------
+ |
+ = [HELP] scope `hint-mostly-unused` to specific packages with a lot of unused object code
+ = [NOTE] `cargo::blanket_hint_mostly_unused` is set to `warn` by default
+[CHECKING] foo v0.0.1 ([ROOT]/foo/foo)
+[RUNNING] `rustc --crate-name foo [..]`
+[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
+
+"#]])
+ .run();
+}
diff --git a/tests/testsuite/lints/mod.rs b/tests/testsuite/lints/mod.rs
index 908663a..ac92d79 100644
--- a/tests/testsuite/lints/mod.rs
+++ b/tests/testsuite/lints/mod.rs
@@ -3,6 +3,7 @@
use cargo_test_support::registry::Package;
use cargo_test_support::str;
+mod blanket_hint_mostly_unused;
mod error;
mod inherited;
mod unknown_lints;