Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ use crate::core::{
PatchLocation,
};
use crate::core::{EitherManifest, Package, SourceId, VirtualManifest};
use crate::lints::analyze_cargo_lints_table;
use crate::lints::rules::blanket_hint_mostly_unused;
use crate::lints::rules::check_im_a_teapot;
use crate::lints::rules::implicit_minimum_version_req_pkg;
use crate::lints::rules::implicit_minimum_version_req_ws;
use crate::lints::rules::missing_lints_features;
use crate::lints::rules::missing_lints_inheritance;
use crate::lints::rules::non_kebab_case_bins;
use crate::lints::rules::non_kebab_case_features;
Expand All @@ -36,6 +36,7 @@ use crate::lints::rules::redundant_homepage;
use crate::lints::rules::redundant_readme;
use crate::lints::rules::text_direction_codepoint_in_comment;
use crate::lints::rules::text_direction_codepoint_in_literal;
use crate::lints::rules::unknown_lints;
use crate::lints::rules::unused_build_dependencies_no_build_rs;
use crate::lints::rules::unused_workspace_dependencies;
use crate::lints::rules::unused_workspace_package_fields;
Expand Down Expand Up @@ -1351,7 +1352,14 @@ impl<'gctx> Workspace<'gctx> {
if self.gctx.cli_unstable().cargo_lints {
let mut verify_error_count = 0;

analyze_cargo_lints_table(
missing_lints_features(
pkg.into(),
&path,
&cargo_lints,
&mut verify_error_count,
self.gctx,
)?;
unknown_lints(
pkg.into(),
&path,
&cargo_lints,
Expand Down Expand Up @@ -1454,7 +1462,14 @@ impl<'gctx> Workspace<'gctx> {
if self.gctx.cli_unstable().cargo_lints {
let mut verify_error_count = 0;

analyze_cargo_lints_table(
missing_lints_features(
(self, self.root_maybe()).into(),
self.root_manifest(),
&cargo_lints,
&mut verify_error_count,
self.gctx,
)?;
unknown_lints(
(self, self.root_maybe()).into(),
self.root_manifest(),
&cargo_lints,
Expand Down
123 changes: 1 addition & 122 deletions src/cargo/lints/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
use std::path::Path;

use cargo_util_schemas::manifest::RustVersion;
use cargo_util_schemas::manifest::TomlToolLints;
use cargo_util_terminal::report::AnnotationKind;
use cargo_util_terminal::report::Group;
use cargo_util_terminal::report::Level;
use cargo_util_terminal::report::Snippet;

use crate::core::Workspace;
use crate::core::{Edition, Feature, Features, MaybePackage, Package};
use crate::{CargoResult, GlobalContext};
use crate::core::{Edition, Features, MaybePackage, Package};

mod lint;
mod report;
Expand Down Expand Up @@ -83,117 +76,3 @@ impl<'a> From<(&'a Workspace<'a>, &'a MaybePackage)> for ManifestFor<'a> {
ManifestFor::Workspace { ws, maybe_pkg }
}
}

pub fn analyze_cargo_lints_table(
manifest: ManifestFor<'_>,
manifest_path: &Path,
cargo_lints: &TomlToolLints,
error_count: &mut usize,
gctx: &GlobalContext,
) -> CargoResult<()> {
let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
let mut unknown_lints = Vec::new();
for lint_name in cargo_lints.keys().map(|name| name) {
let Some((name, default_level, feature_gate)) = find_lint_or_group(lint_name) else {
unknown_lints.push(lint_name);
continue;
};

let (_, source, _) = lint::level_priority(name, *default_level, cargo_lints);

// Only run analysis on user-specified lints
if !source.is_user_specified() {
continue;
}

// Only run this on lints that are gated by a feature
if let Some(feature_gate) = feature_gate
&& !manifest.unstable_features().is_enabled(feature_gate)
{
report_feature_not_enabled(
name,
feature_gate,
&manifest,
&manifest_path,
error_count,
gctx,
)?;
}
}

rules::output_unknown_lints(
unknown_lints,
&manifest,
&manifest_path,
cargo_lints,
error_count,
gctx,
)?;

Ok(())
}

fn find_lint_or_group<'a>(
name: &str,
) -> Option<(&'static str, &LintLevel, &Option<&'static Feature>)> {
if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
Some((
lint.name,
&lint.primary_group.default_level,
&lint.feature_gate,
))
} else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
Some((group.name, &group.default_level, &group.feature_gate))
} else {
None
}
}

fn report_feature_not_enabled(
lint_name: &str,
feature_gate: &Feature,
manifest: &ManifestFor<'_>,
manifest_path: &str,
error_count: &mut usize,
gctx: &GlobalContext,
) -> CargoResult<()> {
let dash_feature_name = feature_gate.name().replace("_", "-");
let title = format!("use of unstable lint `{}`", lint_name);
let label = format!(
"this is behind `{}`, which is not enabled",
dash_feature_name
);
let help = format!(
"consider adding `cargo-features = [\"{}\"]` to the top of the manifest",
dash_feature_name
);

let key_path = match manifest {
ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
ManifestFor::Workspace { .. } => &["workspace", "lints", "cargo", lint_name][..],
};

let mut error = Group::with_title(Level::ERROR.primary_title(title));

if let Some(document) = manifest.document()
&& let Some(contents) = manifest.contents()
{
let Some(span) = get_key_value_span(document, key_path) else {
// This lint is handled by either package or workspace lint.
return Ok(());
};

error = error.element(
Snippet::source(contents)
.path(manifest_path)
.annotation(AnnotationKind::Primary.span(span.key).label(label)),
)
}

let report = [error.element(Level::HELP.message(help))];

*error_count += 1;
gctx.shell().print_report(&report, true)?;

Ok(())
}
98 changes: 98 additions & 0 deletions src/cargo/lints/rules/missing_lints_features.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::path::Path;

use cargo_util_schemas::manifest::TomlToolLints;
use cargo_util_terminal::report::AnnotationKind;
use cargo_util_terminal::report::Group;
use cargo_util_terminal::report::Level;
use cargo_util_terminal::report::Snippet;
use tracing::instrument;

use super::find_lint_or_group;
use crate::CargoResult;
use crate::GlobalContext;
use crate::core::Feature;
use crate::lints::ManifestFor;
use crate::lints::get_key_value_span;
use crate::lints::rel_cwd_manifest_path;

#[instrument(skip_all)]
pub fn missing_lints_features(
manifest: ManifestFor<'_>,
manifest_path: &Path,
cargo_lints: &TomlToolLints,
error_count: &mut usize,
gctx: &GlobalContext,
) -> CargoResult<()> {
let manifest_path = rel_cwd_manifest_path(manifest_path, gctx);
for lint_name in cargo_lints.keys().map(|name| name) {
let Some((name, default_level, feature_gate)) = find_lint_or_group(lint_name) else {
continue;
};

let (_, source, _) = crate::lints::lint::level_priority(name, *default_level, cargo_lints);

// Only run analysis on user-specified lints
if !source.is_user_specified() {
continue;
}

// Only run this on lints that are gated by a feature
if let Some(feature_gate) = feature_gate
&& !manifest.unstable_features().is_enabled(feature_gate)
{
report_feature_not_enabled(
name,
feature_gate,
&manifest,
&manifest_path,
error_count,
gctx,
)?;
}
}

Ok(())
}

fn report_feature_not_enabled(
lint_name: &str,
feature_gate: &Feature,
manifest: &ManifestFor<'_>,
manifest_path: &str,
error_count: &mut usize,
gctx: &GlobalContext,
) -> CargoResult<()> {
let dash_feature_name = feature_gate.name().replace("_", "-");

let mut error = Group::with_title(
Level::ERROR.primary_title(format!("use of unstable lint `{lint_name}`")),
);

if let Some(document) = manifest.document()
&& let Some(contents) = manifest.contents()
{
let key_path = match manifest {
ManifestFor::Package(_) => &["lints", "cargo", lint_name][..],
ManifestFor::Workspace { .. } => &["workspace", "lints", "cargo", lint_name][..],
};
let Some(span) = get_key_value_span(document, key_path) else {
// This lint is handled by either package or workspace lint.
return Ok(());
};

error = error.element(Snippet::source(contents).path(manifest_path).annotation(
AnnotationKind::Primary.span(span.key).label(format!(
"this is behind `{dash_feature_name}`, which is not enabled"
)),
))
}

let report = [error.element(Level::HELP.message(format!(
"consider adding `cargo-features = [\"{dash_feature_name}\"]` to the top of the manifest"
)))];

*error_count += 1;
gctx.shell().print_report(&report, true)?;

Ok(())
}
26 changes: 25 additions & 1 deletion src/cargo/lints/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod blanket_hint_mostly_unused;
mod im_a_teapot;
mod implicit_minimum_version_req;
mod missing_lints_features;
mod missing_lints_inheritance;
mod non_kebab_case_bins;
mod non_kebab_case_features;
Expand All @@ -20,6 +21,7 @@ pub use blanket_hint_mostly_unused::blanket_hint_mostly_unused;
pub use im_a_teapot::check_im_a_teapot;
pub use implicit_minimum_version_req::implicit_minimum_version_req_pkg;
pub use implicit_minimum_version_req::implicit_minimum_version_req_ws;
pub use missing_lints_features::missing_lints_features;
pub use missing_lints_inheritance::missing_lints_inheritance;
pub use non_kebab_case_bins::non_kebab_case_bins;
pub use non_kebab_case_features::non_kebab_case_features;
Expand All @@ -30,13 +32,14 @@ pub use redundant_homepage::redundant_homepage;
pub use redundant_readme::redundant_readme;
pub use text_direction_codepoint_in_comment::text_direction_codepoint_in_comment;
pub use text_direction_codepoint_in_literal::text_direction_codepoint_in_literal;
pub use unknown_lints::output_unknown_lints;
pub use unknown_lints::unknown_lints;
pub use unused_dependencies::unused_build_dependencies_no_build_rs;
pub use unused_workspace_dependencies::unused_workspace_dependencies;
pub use unused_workspace_package_fields::unused_workspace_package_fields;

use super::LintGroup;
use super::LintLevel;
use crate::core::Feature;

pub static LINTS: &[&crate::lints::Lint] = &[
blanket_hint_mostly_unused::LINT,
Expand Down Expand Up @@ -150,6 +153,22 @@ const TEST_DUMMY_UNSTABLE: LintGroup = LintGroup {
hidden: true,
};

fn find_lint_or_group<'a>(
name: &str,
) -> Option<(&'static str, &LintLevel, &Option<&'static Feature>)> {
if let Some(lint) = LINTS.iter().find(|l| l.name == name) {
Some((
lint.name,
&lint.primary_group.default_level,
&lint.feature_gate,
))
} else if let Some(group) = LINT_GROUPS.iter().find(|g| g.name == name) {
Some((group.name, &group.default_level, &group.feature_gate))
} else {
None
}
}

#[cfg(test)]
mod tests {
use itertools::Itertools;
Expand Down Expand Up @@ -215,6 +234,11 @@ mod tests {
if path.ends_with("mod.rs") {
continue;
}
let content = std::fs::read_to_string(&path).unwrap();
if !content.contains("LINT") {

@weihanglo weihanglo May 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be more paranoid checking static LINT in case it might accidentally matches? Or should we list diagnostic files out explicitly?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do static LINT, we could miss some things as diagnostics. I figured it would be good to err on the being as permissive as possible.

As for a diagnostic list, maybe? In theory, we should have more lints than diagnostics but likely the first step towards making many of our deferred warning diagnostics into lints is to pull them out as dedicated diagnostics.

I also want to play with unused item warnings to help manage this. If we switch them from pub to pub(crate), we might be able to have the compiler help us catch missing ones. I just didn't want this to be blocked on that experiment and getting consensus on merging it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasonable. Merging!

// diagnostic
continue;
}
let lint_name = path.file_stem().unwrap().to_string_lossy();
assert!(expected.insert(lint_name.into()), "duplicate lint found");
}
Expand Down
Loading
Loading