rust-lang / rust-lang/rust-clippy
New lint: multiple constructors enforcing invariant
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 13.5k
- Forks
- 2.2k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 32
Description
What it does
If a struct is supposed to enforce invariants and has multiple methods constructing it using its internal representation this may lead to problems with invariant checking going out of sync. The lint should check for these patterns, probably with the help of the developer and warn about them.
The rules for triggering:
- At least one field is private or the struct is
#[non_exhaustive] - The struct does not implement
From<Inner>(PhantomDatafields are ignored when checking this) - The struct has a
fn taking_inner(inner: T) -> Result<Self, _> - The struct is not annotated with
#[clippy::no_invariant]
When the struct both implements From<Inner> and something returning Result there should be probably another warning (another lint?)
Lint Name
multiple_constructors_enforce_invariants
Category
pedantic
Advantage
Basically DRY code. If a new invariant is added or the condition is changed in any way only one fn needs to be modified. When multiple functions are modified it leads to risk of forgetting about one.
Drawbacks
There may be multiple false positives when e.g. the invariant is implied by converting from other type and the check is not needed.
Automatic suggestions are probably impossible.
Example
/// Guarantees divisibility by 2
pub struct Even(u32);
impl TryFrom<u32> for Even {
type Error = OddNumer;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(Even(value))
} else {
Err(OddNumber(value))
}
}
}
impl<'de> serde::Deserialize<'de> for Even {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Even;
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
if v % 2 == 0 {
Ok(Even(v))
} else {
Err(E::invalid_value(Unexpected::Unsigned(v.into()), &"an even, non-negative number up to (2^32)-1"))
}
}
}
deserializer.deserialize_u32(Visitor)
}
}
Could be written as:
/// Guarantees divisibility by 2
pub struct Even(u32);
impl TryFrom<u32> for Even {
type Error = OddNumer;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value % 2 == 0 {
Ok(Even(value))
} else {
Err(OddNumber(value))
}
}
}
impl<'de> serde::Deserialize<'de> for Even {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = Even;
fn visit_u32<E: serde::de::Error>(self, v: u32) -> Result<Self::Value, E> {
v.try_into()
.map_err(|v| E::invalid_value(Unexpected::Unsigned(v.0.into()), &"an even, non-negative number up to (2^32)-1"))
}
}
deserializer.deserialize_u32(Visitor)
}
}
Note: the key issue here is having only one expression calling the Rust-provided constructor Even() (of course same for structs with named fields)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No implementation file or test is named. Start with the triggering rules and the two Even examples, then review existing Clippy lint patterns for struct and constructor analysis. Done means the multiple_constructors_enforce_invariants lint detects the specified cases, respects From conversions and #[clippy::no_invariant], and has coverage for the described behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100