Skip to content

Latest commit

 

History

History
277 lines (206 loc) · 8.35 KB

File metadata and controls

277 lines (206 loc) · 8.35 KB

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 exist for using their 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 via Cargo.lock or target."cfg(false)".dependencies)

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"
mechanism = { version = "1.0", features = ["semantic-addition"], used.reason = "feature activation" }

Cargo would see that a reason is provided for why this is 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:

  • used = <bool>: true if used, false if unused
  • used.reason = <string>: used, the description is unused and for documentation purposes only

Drawbacks

reason is 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).

With any of the current options, if the dependency ever becomes truly unused, there is no way to report this to the user.

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

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 mechanism becomes 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]

_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

Prior art

Rust

// Will warn
let something = foo();

// Ways of resolving
let _ = foo();
let _description = foo();
#[expect(unused_variables, reason = "description")]
let something = foo();

#[expect(unused_variables, reason = "description")]
mod {
  fn bar() {
    let something = foo();
  }
}

cargo udeps

Documentation

[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

Documentation

[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

Documentation

[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 would not work for 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?

Is reason justified?

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.

For transitive, this could get complicated to have access to the dependency resolution graph at the time the warning is being handled.

Potential lints that may need configuration