Skip to content
Draft
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
35 changes: 23 additions & 12 deletions src/cargo/core/workspace.rs

@weihanglo weihanglo Jun 28, 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.

@raushan728 I really appreciate your work and directly went with an implementation. You've contributed quite a lot useful fixes and improvements recently!

I believe you may already know but just remind you again that the issue is not marked as S-accepted and hasn't yet got sufficient design discussions.

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.

Thanks for the kind words. I understand the issue isn't accepted yet. #17089 (comment)

I'm assuming this wouldn't qualify as a hard coded diagnostic and would need to be implemented as a lint.

Also, warning is a Solution. Lack of it is not Problem. Be sure to reframe the Problem statement in terms of end user impact.

Based on that, it seemed clear that a lint (not a hard-coded diagnostic) was the right path. There wasn’t any pushback on the issue, so I went ahead with an implementation that tracks which exclude patterns actually matched a crate during resolution only truly unused ones warn

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.

Not exactly a pushback, though I have one new comment #17089 (comment). I don't really have time thinking on this harder unfortunately

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.

Thanks! I've seen #6745 the nested-path exclude behavior is definitely wonky but, this lint is completely independent though. It only warns when an exclude pattern matched zero packages during resolution, so it catches typos and stale config without touching the buggy nested-path logic. That bug exists with or without the lint.

If you'd prefer to land fixes for #6745 first, we can convert this PR to draft until then. Just let me know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Based on that, it seemed clear that a lint (not a hard-coded diagnostic) was the right path. There wasn’t any pushback on the issue, so I went ahead with an implementation that tracks which exclude patterns actually matched a crate during resolution only truly unused ones warn

To add to what was said, no pushback does not mean everything has been figured out. In particular, this was still in the Triage state which means we haven't really thought at all about this. There is one lint where we are unsure if it is worth the review and maintainance cost. We could also have a different way to reframe a lint to take it in a different direction.

One particular aspect of our label system is to align contributions with our availability, including people pressuring us (intententionally or not) to commit time we do not have by posting PRs. What we most need is thinking, not code, and that is what we lack to move a lot of these issues forward. Note that Weihang did not have time to think more on this but that led to more questions. This is why we encourage contributing through working on accepted issues and enabling decisions on those that are not.

Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ pub struct Workspace<'gctx> {

/// Local overlay configuration. See [`crate::sources::overlay`].
local_overlays: HashMap<SourceId, PathBuf>,

/// A set of exclude patterns from the workspace manifest that actually matched a crate.
used_exclude_patterns: HashSet<String>,
}

// Separate structure for tracking loaded packages (to avoid loading anything
Expand Down Expand Up @@ -178,7 +181,7 @@ impl WorkspaceConfig {
match self {
WorkspaceConfig::Root(ances_root_config) => {
debug!("find_root - found a root checking exclusion");
if !ances_root_config.is_excluded(look_from) {
if ances_root_config.is_excluded(look_from).is_none() {
debug!("find_root - found!");
Some(self_path.to_owned())
} else {
Expand Down Expand Up @@ -267,6 +270,7 @@ impl<'gctx> Workspace<'gctx> {
resolve_publish_time: None,
custom_metadata: None,
local_overlays: HashMap::new(),
used_exclude_patterns: HashSet::new(),
}
}

Expand Down Expand Up @@ -627,6 +631,11 @@ impl<'gctx> Workspace<'gctx> {
self.packages.packages.values()
}

/// Returns a set of all workspace exclude patterns that actually matched a path.
pub fn used_exclude_patterns(&self) -> &HashSet<String> {
&self.used_exclude_patterns
}

/// Returns an iterator over all packages in this workspace
pub fn members(&self) -> impl Iterator<Item = &Package> {
let packages = &self.packages;
Expand Down Expand Up @@ -910,10 +919,11 @@ impl<'gctx> Workspace<'gctx> {
// manifest path, both because `members_paths` doesn't
// include `/Cargo.toml`, and because excluded paths may not
// be crates.
let exclude = members_paths.iter().any(|(m, _)| *m == normalized_path)
&& workspace_config.is_excluded(&normalized_path);
if exclude {
continue;
if members_paths.iter().any(|(m, _)| *m == normalized_path) {
if let Some(pattern) = workspace_config.is_excluded(&normalized_path) {
self.used_exclude_patterns.insert(pattern.clone());
continue;
}
}
bail!(
"package `{}` is listed in default-members{} but is not a member\n\
Expand Down Expand Up @@ -962,7 +972,8 @@ impl<'gctx> Workspace<'gctx> {
if let WorkspaceConfig::Root(ref root_config) =
*self.packages.load(root_manifest)?.workspace_config()
{
if root_config.is_excluded(&manifest_path) {
if let Some(pattern) = root_config.is_excluded(&manifest_path) {
self.used_exclude_patterns.insert(pattern.clone());
return Ok(());
}
}
Expand Down Expand Up @@ -1982,11 +1993,11 @@ impl WorkspaceRootConfig {
/// Checks the path against the `excluded` list.
///
/// This method does **not** consider the `members` list.
fn is_excluded(&self, manifest_path: &Path) -> bool {
fn is_excluded(&self, manifest_path: &Path) -> Option<&String> {
let excluded = self
.exclude
.iter()
.any(|ex| manifest_path.starts_with(self.root_dir.join(ex)));
.find(|ex| manifest_path.starts_with(self.root_dir.join(*ex)));

let explicit_member = match self.members {
Some(ref members) => members
Expand All @@ -1995,7 +2006,7 @@ impl WorkspaceRootConfig {
None => false,
};

!explicit_member && excluded
if !explicit_member { excluded } else { None }
}

/// Checks if the path is explicitly listed as a workspace member.
Expand Down Expand Up @@ -2202,7 +2213,7 @@ pub fn find_workspace_root_with_membership_check(
// Verify the workspace includes this package in its members
if let WorkspaceConfig::Root(ref root_config) = *ws_manifest.workspace_config() {
if root_config.is_explicitly_listed_member(manifest_path)
&& !root_config.is_excluded(manifest_path)
&& root_config.is_excluded(manifest_path).is_none()
{
return Ok(Some(ws_manifest_path));
}
Expand All @@ -2217,7 +2228,7 @@ pub fn find_workspace_root_with_membership_check(
let manifest = read_manifest(candidate_manifest_path, source_id, gctx)?;
if let WorkspaceConfig::Root(ref root_config) = *manifest.workspace_config() {
if root_config.is_explicitly_listed_member(manifest_path)
&& !root_config.is_excluded(manifest_path)
&& root_config.is_excluded(manifest_path).is_none()
{
return Ok(Some(candidate_manifest_path.to_path_buf()));
}
Expand All @@ -2244,7 +2255,7 @@ fn find_workspace_root_with_loader(
// root. Note we skip the first item since that is just the path itself
for current in manifest_path.ancestors().skip(1) {
if let Some(ws_config) = roots.get(current) {
if !ws_config.is_excluded(manifest_path) {
if ws_config.is_excluded(manifest_path).is_none() {
// Add `Cargo.toml` since ws_root is the root and not the file
return Ok(Some(current.join("Cargo.toml")));
}
Expand Down
6 changes: 6 additions & 0 deletions src/cargo/diagnostics/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod text_direction_codepoint_in_literal;
mod unknown_lints;
pub mod unused_dependencies;
mod unused_workspace_dependencies;
mod unused_workspace_exclude;
mod unused_workspace_package_fields;

use super::LintGroup;
Expand Down Expand Up @@ -50,6 +51,10 @@ pub const PARSE_PASS_RULES: &[ParsePassRule<'static>] = &[
rule: unused_workspace_dependencies::lint_workspace,
lint: unused_workspace_dependencies::LINT,
},
ParsePassRule::LintWorkspace {
rule: unused_workspace_exclude::lint_workspace,
lint: unused_workspace_exclude::LINT,
},
ParsePassRule::LintWorkspace {
rule: unused_workspace_package_fields::lint_workspace,
lint: unused_workspace_package_fields::LINT,
Expand Down Expand Up @@ -123,6 +128,7 @@ pub static LINTS: &[&crate::diagnostics::Lint] = &[
unknown_lints::LINT,
unused_dependencies::LINT,
unused_workspace_dependencies::LINT,
unused_workspace_exclude::LINT,
unused_workspace_package_fields::LINT,
];

Expand Down
100 changes: 100 additions & 0 deletions src/cargo/diagnostics/rules/unused_workspace_exclude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::path::Path;

use cargo_util_terminal::report::{AnnotationKind, Group, Level, Origin, Snippet};
use tracing::instrument;

use super::SUSPICIOUS;
use crate::diagnostics::{
Lint, LintLevelProduct, ScopedDiagnosticStats, get_key_value_span, workspace_rel_path,
};
use crate::{
CargoResult, GlobalContext,
core::{MaybePackage, Workspace},
};

pub static LINT: &Lint = &Lint {
name: "unused_workspace_exclude",
desc: "unused workspace exclude",
primary_group: &SUSPICIOUS,
msrv: Some(super::CARGO_LINTS_MSRV),
feature_gate: None,
docs: Some(
r#"
### What it does
Checks for any entry in `[workspace.exclude]` that does not match any workspace member

### Why it is bad
They can give the false impression that a package is excluded when it is actually not present

### Example
```toml
[workspace]
exclude = ["does-not-exist"]
```
"#,
),
};

#[instrument(skip_all)]
pub(crate) fn lint_workspace(
ws: &Workspace<'_>,
maybe_pkg: &MaybePackage,
manifest_path: &Path,
level: LintLevelProduct,
pkg_stats: &mut ScopedDiagnosticStats<'_>,
gctx: &GlobalContext,
) -> CargoResult<()> {
let LintLevelProduct {
level: lint_level,
source,
} = level;

let Some(original_toml) = maybe_pkg.original_toml() else {
return Ok(());
};

let Some(workspace) = original_toml.workspace.as_ref() else {
return Ok(());
};

let Some(exclude) = workspace.exclude.as_ref() else {
return Ok(());
};

let used_exclude_patterns = ws.used_exclude_patterns();

for (i, unused) in exclude
.iter()
.filter(|pattern| !used_exclude_patterns.contains(*pattern))
.enumerate()
{
let document = maybe_pkg.document();
let contents = maybe_pkg.contents();
let level = lint_level.to_diagnostic_level();
let manifest_path = workspace_rel_path(ws, manifest_path);
let emitted_source = LINT.emitted_source(lint_level, source);

let mut primary =
Group::with_title(level.primary_title(format!("unused exclude pattern '{}'", unused)));
if let Some(document) = document
&& let Some(contents) = contents
{
let mut snippet = Snippet::source(contents).path(&manifest_path);
if let Some(span) = get_key_value_span(document, &["workspace", "exclude"]) {
snippet = snippet.annotation(AnnotationKind::Primary.span(span.key));
}
primary = primary.element(snippet);
} else {
primary = primary.element(Origin::path(&manifest_path));
}
if i == 0 {
primary = primary.element(Level::NOTE.message(emitted_source));
}
let report = vec![primary];

pkg_stats.record_lint(lint_level);
gctx.shell().print_report(&report, lint_level.force())?;
}

Ok(())
}
21 changes: 21 additions & 0 deletions src/doc/src/reference/lints.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ These lints are all set to the 'warn' level by default.
- [`unknown_lints`](#unknown_lints)
- [`unused_dependencies`](#unused_dependencies)
- [`unused_workspace_dependencies`](#unused_workspace_dependencies)
- [`unused_workspace_exclude`](#unused_workspace_exclude)
- [`unused_workspace_package_fields`](#unused_workspace_package_fields)

## Deny-by-default
Expand Down Expand Up @@ -542,6 +543,26 @@ regex = "1"
```


## `unused_workspace_exclude`
Group: `suspicious`

Level: `warn`

MSRV: `1.79.0`

### What it does
Checks for any entry in `[workspace.exclude]` that does not match any workspace member

### Why it is bad
They can give the false impression that a package is excluded when it is actually not present

### Example
```toml
[workspace]
exclude = ["does-not-exist"]
```


## `unused_workspace_package_fields`
Group: `suspicious`

Expand Down
1 change: 1 addition & 0 deletions tests/testsuite/lints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod text_direction_codepoint;
mod unknown_lints;
mod unused_dependencies;
mod unused_workspace_dependencies;
mod unused_workspace_exclude;
mod unused_workspace_package_fields;
mod warning;

Expand Down
Loading
Loading