Add unchecked_non_zero lint - #17521
Open
dcsommer wants to merge 1 commit into
Open
Conversation
|
Lintcheck changes for 560fc94
This comment will be updated if you push new changes |
dcsommer
force-pushed
the
unchecked-non-zero
branch
from
August 7, 2026 20:05
d86a951 to
9a8b536
Compare
dcsommer
marked this pull request as ready for review
August 7, 2026 20:20
Contributor
There was a problem hiding this comment.
community review: looking at the lintcheck results, you might want to consider adding known_at_least support for
- size_of results with non-ZST types (
fastrand-2.1.0/src/lib.rs:539withslice.chunks_exact_mut(core::mem::size_of::<u64>())) - addition results (
chrono-0.4.38/src/offset/local/tz_info/parser.rs:62withstate.leap_seconds.chunks_exact(state.time_size + 4), whentime_sizeis alreadyusizeand thus cannot be negative 4)
Adds a restriction lint for std methods that panic unless a value is non-zero, where that value cannot be shown to be non-zero: - `chunks`, `chunks_exact`, `rchunks`, `rchunks_exact`, `windows` and their `_mut` variants (chunk/window size of 0) - `Iterator::step_by` (step of 0) - `ilog2`, `ilog10`, `ilog` (receiver of 0, or non-positive when signed; `ilog` also panics on a base below 2) A value is accepted as valid when it const-evaluates high enough, is `NonZero::get()` on an unsigned `NonZero`, or is `max(n)` for a large enough constant `n` -- the same shape of carve-out `arithmetic_side_effects` uses for division by zero. `step_by` with a literal `0` is left to `iterator_step_by_zero`, which is warn-by-default, so the two lints don't both fire on it.
dcsommer
force-pushed
the
unchecked-non-zero
branch
from
August 10, 2026 18:52
9a8b536 to
560fc94
Compare
Collaborator
|
☔ The latest upstream changes (possibly #17552) made this pull request unmergeable. Please resolve the merge conflicts. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NOTE: this PR was drafted with Opus 5. I (the human) reviewed all content, including by-hand fixes, prior to submission.
Motivation
A handful of standard library methods panic when a value is zero, and nothing in their signatures says so.
fn rows(data: &[u8], row_len: usize)looks total, butdata.chunks(row_len)aborts the process whenrow_lenis0. When the value comes from a computation, a config file or user input, the panic surfaces far from where the bad value was produced, and the type system offered no warning.This is the same class of problem clippy already covers for indexing (
indexing_slicing),Option/Result(unwrap_used), string slicing (string_slice) and arithmetic (arithmetic_side_effects) — but the zero preconditions have no lint, so codebases that deny the others still panic here.The fix a user reaches for is
NonZero<usize>, which moves the check to the boundary where the value enters the program and makes the call site total. The lint's help says so directly, and for theilogfamily points at thechecked_*variants that returnNone.Scope
chunks,chunks_exact,rchunks,rchunks_exact,windowsand their_mutvariants0Iterator::step_by0ilog2,ilog10,ilog0, or negative when signedilog(base)2Why one lint rather than three
The three families share the interesting part — deciding whether a value could be zero — so they share a single
known_at_leastprover. That has a direct maintainability consequence: the prover only has to be improved once. It currently accepts constants,NonZero::get()on an unsignedNonZero, andmax(n); the obvious follow-ups (recognising a precedingassert!, an enclosingif x != 0, orusize::from(non_zero)) would land in one place and immediately benefit every covered API. Split across three lints, each improvement would have to be written and tested three times, and would drift.Adding another zero-panicking API is likewise one match arm plus one
Preconditionliteral — no new lint registration, no new#[clippy::version], no new docs page, no new test file. ThePreconditionstruct keeps each site declarative (which expression, what to call it, what the minimum is, what help to give), so the wiring reads as data rather than control flow.This also matches existing precedent rather than inventing a category:
arithmetic_side_effectsis a single restriction lint spanning every arithmetic operator that shares an overflow/divide-by-zero precondition, and it uses the sameNonZerocarve-out. Reusing that carve-out is deliberate — it's why division and modulo are not duplicated here.The honest cost of merging them is granularity: you can't enable the slice checks without also getting
ilog. I think that's the right trade for a lint this narrow, but it's the main thing I'd want challenged.Correctness details
Three cases worth highlighting, all covered by tests:
NonZeroproves nothing here:NonZero<i32>::get()can be negative, son.get().ilog2()still fires.NonZero's ownilog2/ilog10are infallible, andNonZerois an ADT rather thanis_integral(), so they are correctly skipped.step_by(0)is left toiterator_step_by_zero(warn-by-default) so the two fire on disjoint cases. The slice family keeps its known-zero case, since nothing catcheschunks(0)today.Compile-time cost
unchecked_non_zero::checkis called once at the top ofcheck_methods, so it sees every method call in the crate. It matches on the symbol first and only touches typeck tables once an arm has matched, so the common path is a handful of integer comparisons.Checklist
.stderrfile)cargo testpasses locallycargo dev update_lintscargo dev fmtchangelog: new lint: [
unchecked_non_zero]