Summary
Extend Cargo's dependency specifications so users can mark that a dependency is used, independent of what unused externs says.
Motivation
Rustc can report to Cargo unused externs which Cargo can aggregate and report back which dependencies are unused.
However, not all dependencies always use the extern in the current package, including
- activating a feature on a transitive dependency
- pinning the version of a transitive dependency in
Cargo.toml(however, normally this would be done viaCargo.lockortarget."cfg(false)".dependencies) - dependencies that are only used based on
RUSTFLAGS/build.rs(example)
The user needs a way to be able to augment what the compiler reports with their intentions.
Guide-level explanation
Say I have the following packages:
[package]
name = "user"
[dependencies]
abstraction = "1.0"
mechanism = { version = "1.0", features = ["semantic-addition"] }
[package]
name = "abstraction"
[dependencies]
mechanism = "1.0"
[package]
name = "mechanism"
[features]
semantic-addition = []
In this case, user references abstraction within its Rust source code.
The dependency on mechanism is only to activate the semantic-addition feature.
With cargo#16600,
Cargo would report mechanism as an unused dependency:
[WARNING] unused dependency
--> Cargo.toml:6:1
|
6 | mechanism = { version = "1.0", features = ["semantic-addition"] }
| ^^^^^^^^^
|
= [NOTE] `cargo::unused_dependencies` is set to `warn` by default
[HELP] remove the dependency
|
6 - mechanism = { version = "1.0", features = ["semantic-addition"] }
|
[HELP] to keep the dependency, mark it as used:
|
6 | mechanism = { version = "1.0", features = ["semantic-addition"], used = true }
| +++++++++++++
|
To resolve this, user can:
[package]
name = "user"
[dependencies]
abstraction = "1.0"
# For feature activation
mechanism = { version = "1.0", features = ["semantic-addition"], used = true }
Cargo would see that the dependency is marked as used and silence the lint.
Reference-level explanation
Within Specifying Dependencies
used
Always consider this dependency used
Applicability:
[*dependencies]: yes[workspace.dependencies]: no[patch]: no
Type: bool
Drawbacks
With any of the current options, if the dependency ever becomes truly unused, there is no way to report this to the user.
This doesn't scale to other potential dependency lints, including
- wildcard dependencies (#4377)
- non-semver version upper bound (#5340)
- Version req is lower than
Cargo.lock(#15583) - dependency isn't inherited (#15578)
- Version requirement without fully specified lower bound (#15577)
- unused
public = true - implicit version requirement
Rationale and alternatives
As we are restricted to TOML syntax, we don't have a general purpose way of handling lint control like #[expect].
used
Pros:
- Close to the dependency
Downsides:
- Manifest schema is being extended for control of a one off lint which can be incongruous with other lints and encourage the doing this for other lints which could overcomplicate manifest files
used.reason
If we added reason, it would be unused by Cargo and could just as well be a comment.
In contrast, Rust's reason can be applied to warn, deny, and forbid lints and reported back in the diagnostic
(reference).
Lint config
[lints.cargo]
unused_dependencies = { level = "warn", used = ["mechanism"] }
Pros:
- Follows a consistent pattern for where to configure lints
Downsides:
- Far away from the dependency specification, making it hard to reason about when looking at
mechanism - Need to decide what to do when
mechanismbecomes used or doesn't exist - This config should not be inherited from
[workspace.lints] - Can't inherit
[workspace.lints]and specify lint config in[lints](#13157) - Blunt hammer, not distinguishing which dependencies table this applies to
- Having it reference the dependencies table makes this more complicated and requires us to have a way to refer to
[dependencies]
- Having it reference the dependencies table makes this more complicated and requires us to have a way to refer to
_dep
_mechanism = { package = "mechanism", version = "1.0", features = ["semantic-addition"] }
Pros:
- Close to the dependency
- Aligns with Rust treating
_as "don't care" - Low overhead for design and implementation
Downsides:
- Doesn't mix well with kebab case names (e.g.
_regex-syntax) - The Cargo team was hesitant to officially support something similar for features (#10794)
- Affects sort order, moving it away from any related packages
Link control
mechanism = { version = "1.0", lib = false, features = ["semantic-addition"] }
Artifact dependencies
has a lib field to control whether a package links to that dependency.
We could stabilize that piece independent of the rest of artifact dependencies
and shift the focus of this lint to unused_lib_dependencies.
We could suggest to users to remove the dependency or set lib = false.
Open questions:
- How would artifact / unlinked dependencies resolve features? Independent of the rest of unified?
- My guess is unified with opaque dependencies being a way to make them not unified.
- Does this cover all false-positive cases?
Pros:
- Close to the dependency
- Low overhead for design and implementation
- Won't build if it goes stale, avoiding slowing down builds
Downsides:
- Requires a comment to make sense of it
- Does not help with dynamically used dependencies like with
curl
As an aside: is lib or link a more appropriate field name?
Target dependencies
[target.'cfg(resolve = "versions")'.dependencies]
# replaces `cfg(false)` which may break with https://github.com/rust-lang/cargo/issues/6179
[target.'cfg(resolve = "features")'.dependencies]
mechanism = { version = "1.0", features = ["semantic-addition"] }
Add Cargo-specific built-in cfgs for what parts of Cargo should respect or ignore the dependencies.
resolve = "versions"
- runs during version selection
- ignored for feature selection
- ignored for build
resolve = "features" is a superset of versions (ie version selection sets --cfg resolve=versions --cfg resolve=features)
- runs during version selection
- runs during feature selection
- ignored for build
Pros:
- Close to the dependency
- Builds off of existing concepts in a way that can also scale to more use cases
resolve = "versions"can work without an MSRV bump
Downsides:
- Carves into user
cfgs, possibly breaking someone - Could lead to confusing error messages if a user gets clever and does
not(resolve = "versions") - Each early stage in the process needs to know about later stages to correctly set
--cfg - Does not help with dynamically used dependencies like with
curl
Implicitly ignore duplicate dependencies
If a direct dependency is also a transitive dependency, Cargo can ignore it, assuming the user did it intentionally for dependency resolution or feature activation. False negatives is unlikely to have much of an impact because the dependency is already going to be built. The worst that can happen is some extra features get activated.
Pros:
- Automatic
Downsides
- Can lead to extra features being activated than needed
- Does not help with dynamically used dependencies like with
curl
Do nothing, have people disable the lint
This is likely insufficient on its own but may be a worthwhile answer for cases not as easily covered, like curl.
Prior art
Rust
// Will warn
let something = foo;
// Ways of resolving
let _ = foo;
let _description = foo;
let something = foo;
mod
cargo udeps
[package.metadata.cargo-udeps.ignore]
normal = ["if_chain"]
#development = []
#build = []
[dependencies]
if_chain = "1.0.0" # Used only in doc-tests, which `cargo-udeps` cannot check.
cargo machete
[dependencies]
prost = "0.10" # Used in code generated by build.rs output, which cargo-machete cannot check
# in an individual package Cargo.toml
[package.metadata.cargo-machete]
ignored = ["prost"]
# in a workspace Cargo.toml
[workspace.metadata.cargo-machete]
ignored = ["prost"]
cargo shear
[package.metadata.cargo-shear]
ignored = ["crate-name"]
cfg(false) dependencies
To specify a version requirement on a package without using it, you can do:
[target."cfg(false)".dependencies]
foo = "1.0.0"
This works for version constraints but not feature activations.
peerDependencies
npm has the concept of
peerDependencies
for requiring a version of a package if it exists without actually building against this.
This was developed for plugins to specify the version of what they plug into (announcement).
Unresolved questions
Is this the right direction?
Future possibilities
Programmatic reason
The user being able to tell Cargo the reason in a way that it can verify it and still warn if the reason no longer applies.
For example, say we had:
[package]
name = "user"
[dependencies]
abstraction = "1.0"
mechanism = { version = "1.0", features = ["semantic-addition"], used.transitive = true }
Then if abstraction is removed, we could start warning about mechanism again.
However for transitive, this could get complicated to have access to the dependency resolution graph at the time the warning is being handled.