Skip to content

Commit 1c714ca

Browse files
authored
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
2 parents c86bc37 + da5d915 commit 1c714ca

File tree

5 files changed

+469
-38
lines changed

5 files changed

+469
-38
lines changed

src/cargo/core/workspace.rs

Lines changed: 116 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ use crate::util::context::FeatureUnification;
2525
use crate::util::edit_distance;
2626
use crate::util::errors::{CargoResult, ManifestError};
2727
use crate::util::interning::InternedString;
28-
use crate::util::lints::{analyze_cargo_lints_table, check_im_a_teapot};
28+
use crate::util::lints::{
29+
analyze_cargo_lints_table, blanket_hint_mostly_unused, check_im_a_teapot,
30+
};
2931
use crate::util::toml::{InheritableFields, read_manifest};
3032
use crate::util::{
3133
Filesystem, GlobalContext, IntoUrl, context::CargoResolverConfig, context::ConfigRelativePath,
@@ -409,10 +411,7 @@ impl<'gctx> Workspace<'gctx> {
409411
}
410412

411413
pub fn profiles(&self) -> Option<&TomlProfiles> {
412-
match self.root_maybe() {
413-
MaybePackage::Package(p) => p.manifest().profiles(),
414-
MaybePackage::Virtual(vm) => vm.profiles(),
415-
}
414+
self.root_maybe().profiles()
416415
}
417416

418417
/// Returns the root path of this workspace.
@@ -907,10 +906,7 @@ impl<'gctx> Workspace<'gctx> {
907906

908907
/// Returns the unstable nightly-only features enabled via `cargo-features` in the manifest.
909908
pub fn unstable_features(&self) -> &Features {
910-
match self.root_maybe() {
911-
MaybePackage::Package(p) => p.manifest().unstable_features(),
912-
MaybePackage::Virtual(vm) => vm.unstable_features(),
913-
}
909+
self.root_maybe().unstable_features()
914910
}
915911

916912
pub fn resolve_behavior(&self) -> ResolveBehavior {
@@ -1206,14 +1202,17 @@ impl<'gctx> Workspace<'gctx> {
12061202

12071203
pub fn emit_warnings(&self) -> CargoResult<()> {
12081204
let mut first_emitted_error = None;
1205+
1206+
if let Err(e) = self.emit_ws_lints() {
1207+
first_emitted_error = Some(e);
1208+
}
1209+
12091210
for (path, maybe_pkg) in &self.packages.packages {
12101211
if let MaybePackage::Package(pkg) = maybe_pkg {
1211-
if self.gctx.cli_unstable().cargo_lints {
1212-
if let Err(e) = self.emit_lints(pkg, &path)
1213-
&& first_emitted_error.is_none()
1214-
{
1215-
first_emitted_error = Some(e);
1216-
}
1212+
if let Err(e) = self.emit_pkg_lints(pkg, &path)
1213+
&& first_emitted_error.is_none()
1214+
{
1215+
first_emitted_error = Some(e);
12171216
}
12181217
}
12191218
let warnings = match maybe_pkg {
@@ -1248,7 +1247,7 @@ impl<'gctx> Workspace<'gctx> {
12481247
}
12491248
}
12501249

1251-
pub fn emit_lints(&self, pkg: &Package, path: &Path) -> CargoResult<()> {
1250+
pub fn emit_pkg_lints(&self, pkg: &Package, path: &Path) -> CargoResult<()> {
12521251
let mut error_count = 0;
12531252
let toml_lints = pkg
12541253
.manifest()
@@ -1262,26 +1261,74 @@ impl<'gctx> Workspace<'gctx> {
12621261
.cloned()
12631262
.unwrap_or(manifest::TomlToolLints::default());
12641263

1265-
let ws_contents = match self.root_maybe() {
1266-
MaybePackage::Package(pkg) => pkg.manifest().contents(),
1267-
MaybePackage::Virtual(v) => v.contents(),
1268-
};
1264+
let ws_contents = self.root_maybe().contents();
12691265

1270-
let ws_document = match self.root_maybe() {
1271-
MaybePackage::Package(pkg) => pkg.manifest().document(),
1272-
MaybePackage::Virtual(v) => v.document(),
1273-
};
1266+
let ws_document = self.root_maybe().document();
1267+
1268+
if self.gctx.cli_unstable().cargo_lints {
1269+
analyze_cargo_lints_table(
1270+
pkg,
1271+
&path,
1272+
&cargo_lints,
1273+
ws_contents,
1274+
ws_document,
1275+
self.root_manifest(),
1276+
self.gctx,
1277+
)?;
1278+
check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?;
1279+
}
1280+
1281+
if error_count > 0 {
1282+
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
1283+
"encountered {error_count} errors(s) while running lints"
1284+
))
1285+
.into())
1286+
} else {
1287+
Ok(())
1288+
}
1289+
}
1290+
1291+
pub fn emit_ws_lints(&self) -> CargoResult<()> {
1292+
let mut error_count = 0;
1293+
1294+
let cargo_lints = match self.root_maybe() {
1295+
MaybePackage::Package(pkg) => {
1296+
let toml = pkg.manifest().normalized_toml();
1297+
if let Some(ws) = &toml.workspace {
1298+
ws.lints.as_ref()
1299+
} else {
1300+
toml.lints.as_ref().map(|l| &l.lints)
1301+
}
1302+
}
1303+
MaybePackage::Virtual(vm) => vm
1304+
.normalized_toml()
1305+
.workspace
1306+
.as_ref()
1307+
.unwrap()
1308+
.lints
1309+
.as_ref(),
1310+
}
1311+
.and_then(|t| t.get("cargo"))
1312+
.cloned()
1313+
.unwrap_or(manifest::TomlToolLints::default());
1314+
1315+
if self.gctx.cli_unstable().cargo_lints {
1316+
// Calls to lint functions go in here
1317+
}
1318+
1319+
// This is a short term hack to allow `blanket_hint_mostly_unused`
1320+
// to run without requiring `-Zcargo-lints`, which should hopefully
1321+
// improve the testing expierience while we are collecting feedback
1322+
if self.gctx.cli_unstable().profile_hint_mostly_unused {
1323+
blanket_hint_mostly_unused(
1324+
self.root_maybe(),
1325+
self.root_manifest(),
1326+
&cargo_lints,
1327+
&mut error_count,
1328+
self.gctx,
1329+
)?;
1330+
}
12741331

1275-
analyze_cargo_lints_table(
1276-
pkg,
1277-
&path,
1278-
&cargo_lints,
1279-
ws_contents,
1280-
ws_document,
1281-
self.root_manifest(),
1282-
self.gctx,
1283-
)?;
1284-
check_im_a_teapot(pkg, &path, &cargo_lints, &mut error_count, self.gctx)?;
12851332
if error_count > 0 {
12861333
Err(crate::util::errors::AlreadyPrintedError::new(anyhow!(
12871334
"encountered {error_count} errors(s) while running lints"
@@ -1888,6 +1935,41 @@ impl MaybePackage {
18881935
MaybePackage::Virtual(_) => false,
18891936
}
18901937
}
1938+
1939+
pub fn contents(&self) -> &str {
1940+
match self {
1941+
MaybePackage::Package(p) => p.manifest().contents(),
1942+
MaybePackage::Virtual(v) => v.contents(),
1943+
}
1944+
}
1945+
1946+
pub fn document(&self) -> &toml::Spanned<toml::de::DeTable<'static>> {
1947+
match self {
1948+
MaybePackage::Package(p) => p.manifest().document(),
1949+
MaybePackage::Virtual(v) => v.document(),
1950+
}
1951+
}
1952+
1953+
pub fn edition(&self) -> Edition {
1954+
match self {
1955+
MaybePackage::Package(p) => p.manifest().edition(),
1956+
MaybePackage::Virtual(_) => Edition::default(),
1957+
}
1958+
}
1959+
1960+
pub fn profiles(&self) -> Option<&TomlProfiles> {
1961+
match self {
1962+
MaybePackage::Package(p) => p.manifest().profiles(),
1963+
MaybePackage::Virtual(v) => v.profiles(),
1964+
}
1965+
}
1966+
1967+
pub fn unstable_features(&self) -> &Features {
1968+
match self {
1969+
MaybePackage::Package(p) => p.manifest().unstable_features(),
1970+
MaybePackage::Virtual(vm) => vm.unstable_features(),
1971+
}
1972+
}
18911973
}
18921974

18931975
impl WorkspaceRootConfig {

src/cargo/util/lints.rs

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
use crate::core::{Edition, Feature, Features, Manifest, Package};
1+
use crate::core::{Edition, Feature, Features, Manifest, MaybePackage, Package};
22
use crate::{CargoResult, GlobalContext};
3-
use annotate_snippets::{AnnotationKind, Group, Level, Snippet};
4-
use cargo_util_schemas::manifest::{TomlLintLevel, TomlToolLints};
3+
use annotate_snippets::{AnnotationKind, Group, Level, Patch, Snippet};
4+
use cargo_util_schemas::manifest::{ProfilePackageSpec, TomlLintLevel, TomlToolLints};
55
use pathdiff::diff_paths;
66
use std::fmt::Display;
77
use std::ops::Range;
88
use std::path::Path;
99

1010
const LINT_GROUPS: &[LintGroup] = &[TEST_DUMMY_UNSTABLE];
11-
pub const LINTS: &[Lint] = &[IM_A_TEAPOT, UNKNOWN_LINTS];
11+
pub const LINTS: &[Lint] = &[BLANKET_HINT_MOSTLY_UNUSED, IM_A_TEAPOT, UNKNOWN_LINTS];
1212

1313
pub fn analyze_cargo_lints_table(
1414
pkg: &Package,
@@ -473,6 +473,158 @@ pub fn check_im_a_teapot(
473473
Ok(())
474474
}
475475

476+
const BLANKET_HINT_MOSTLY_UNUSED: Lint = Lint {
477+
name: "blanket_hint_mostly_unused",
478+
desc: "blanket_hint_mostly_unused lint",
479+
groups: &[],
480+
default_level: LintLevel::Warn,
481+
edition_lint_opts: None,
482+
feature_gate: None,
483+
docs: Some(
484+
r#"
485+
### What it does
486+
Checks if `hint-mostly-unused` being applied to all dependencies.
487+
488+
### Why it is bad
489+
`hint-mostly-unused` indicates that most of a crate's API surface will go
490+
unused by anything depending on it; this hint can speed up the build by
491+
attempting to minimize compilation time for items that aren't used at all.
492+
Misapplication to crates that don't fit that criteria will slow down the build
493+
rather than speeding it up. It should be selectively applied to dependencies
494+
that meet these criteria. Applying it globally is always a misapplication and
495+
will likely slow down the build.
496+
497+
### Example
498+
```toml
499+
[profile.dev.package."*"]
500+
hint-mostly-unused = true
501+
```
502+
503+
Should instead be:
504+
```toml
505+
[profile.dev.package.huge-mostly-unused-dependency]
506+
hint-mostly-unused = true
507+
```
508+
"#,
509+
),
510+
};
511+
512+
pub fn blanket_hint_mostly_unused(
513+
maybe_pkg: &MaybePackage,
514+
path: &Path,
515+
pkg_lints: &TomlToolLints,
516+
error_count: &mut usize,
517+
gctx: &GlobalContext,
518+
) -> CargoResult<()> {
519+
let (lint_level, reason) = BLANKET_HINT_MOSTLY_UNUSED.level(
520+
pkg_lints,
521+
maybe_pkg.edition(),
522+
maybe_pkg.unstable_features(),
523+
);
524+
525+
if lint_level == LintLevel::Allow {
526+
return Ok(());
527+
}
528+
529+
let level = lint_level.to_diagnostic_level();
530+
let manifest_path = rel_cwd_manifest_path(path, gctx);
531+
let mut paths = Vec::new();
532+
533+
if let Some(profiles) = maybe_pkg.profiles() {
534+
for (profile_name, top_level_profile) in &profiles.0 {
535+
if let Some(true) = top_level_profile.hint_mostly_unused {
536+
paths.push((
537+
vec!["profile", profile_name.as_str(), "hint-mostly-unused"],
538+
true,
539+
));
540+
}
541+
542+
if let Some(build_override) = &top_level_profile.build_override
543+
&& let Some(true) = build_override.hint_mostly_unused
544+
{
545+
paths.push((
546+
vec![
547+
"profile",
548+
profile_name.as_str(),
549+
"build-override",
550+
"hint-mostly-unused",
551+
],
552+
false,
553+
));
554+
}
555+
556+
if let Some(packages) = &top_level_profile.package
557+
&& let Some(profile) = packages.get(&ProfilePackageSpec::All)
558+
&& let Some(true) = profile.hint_mostly_unused
559+
{
560+
paths.push((
561+
vec![
562+
"profile",
563+
profile_name.as_str(),
564+
"package",
565+
"*",
566+
"hint-mostly-unused",
567+
],
568+
false,
569+
));
570+
}
571+
}
572+
}
573+
574+
for (i, (path, show_per_pkg_suggestion)) in paths.iter().enumerate() {
575+
if lint_level.is_error() {
576+
*error_count += 1;
577+
}
578+
let title = "`hint-mostly-unused` is being blanket applied to all dependencies";
579+
let help_txt =
580+
"scope `hint-mostly-unused` to specific packages with a lot of unused object code";
581+
if let (Some(span), Some(table_span)) = (
582+
get_key_value_span(maybe_pkg.document(), &path),
583+
get_key_value_span(maybe_pkg.document(), &path[..path.len() - 1]),
584+
) {
585+
let mut report = Vec::new();
586+
let mut primary_group = level.clone().primary_title(title).element(
587+
Snippet::source(maybe_pkg.contents())
588+
.path(&manifest_path)
589+
.annotation(
590+
AnnotationKind::Primary.span(table_span.key.start..table_span.key.end),
591+
)
592+
.annotation(AnnotationKind::Context.span(span.key.start..span.value.end)),
593+
);
594+
595+
if *show_per_pkg_suggestion {
596+
report.push(
597+
Level::HELP.secondary_title(help_txt).element(
598+
Snippet::source(maybe_pkg.contents())
599+
.path(&manifest_path)
600+
.patch(Patch::new(
601+
table_span.key.end..table_span.key.end,
602+
".package.<pkg_name>",
603+
)),
604+
),
605+
);
606+
} else {
607+
primary_group = primary_group.element(Level::HELP.message(help_txt));
608+
}
609+
610+
if i == 0 {
611+
primary_group =
612+
primary_group
613+
.element(Level::NOTE.message(
614+
BLANKET_HINT_MOSTLY_UNUSED.emitted_source(lint_level, reason),
615+
));
616+
}
617+
618+
// The primary group should always be first
619+
report.insert(0, primary_group);
620+
621+
gctx.shell().print_report(&report, lint_level.force())?;
622+
}
623+
}
624+
625+
Ok(())
626+
}
627+
476628
const UNKNOWN_LINTS: Lint = Lint {
477629
name: "unknown_lints",
478630
desc: "unknown lint",

0 commit comments

Comments
 (0)