feat: Add lint for global use of hint-mostly-unused

This commit is contained in:
Scott Schafer 2025-07-28 09:28:14 -06:00
parent d826a65fef
commit da5d9152fa
No known key found for this signature in database
4 changed files with 246 additions and 7 deletions

View File

@ -25,7 +25,9 @@ use crate::util::context::FeatureUnification;
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,
@ -1287,9 +1289,9 @@ impl<'gctx> Workspace<'gctx> {
}
pub fn emit_ws_lints(&self) -> CargoResult<()> {
let error_count = 0;
let mut error_count = 0;
let _cargo_lints = match self.root_maybe() {
let cargo_lints = match self.root_maybe() {
MaybePackage::Package(pkg) => {
let toml = pkg.manifest().normalized_toml();
if let Some(ws) = &toml.workspace {
@ -1314,6 +1316,19 @@ impl<'gctx> Workspace<'gctx> {
// 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"

View File

@ -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 @@ pub fn check_im_a_teapot(
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",

View File

@ -5,8 +5,37 @@ Note: [Cargo's linting system is unstable](unstable.md#lintscargo) and can only
## 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

View File

@ -22,6 +22,19 @@ hint-mostly-unused = true
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
@ -50,6 +63,16 @@ hint-mostly-unused = true
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
@ -78,6 +101,16 @@ hint-mostly-unused = true
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
@ -115,6 +148,16 @@ authors = []
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